diff --git a/.changeset/plugin-map-readme-truth-5002.md b/.changeset/plugin-map-readme-truth-5002.md new file mode 100644 index 0000000000..17393a2fdd --- /dev/null +++ b/.changeset/plugin-map-readme-truth-5002.md @@ -0,0 +1,25 @@ +--- +--- + +Docs only: `packages/plugin-map/README.md` is rewritten against the code it documents +(objectui#5002). The old text taught a component that does not exist — an authored +`markers` array, `layers`, `height`, `useGeolocation`, a `center: { lat, lng }` object, +a `zoom` "default: 10", and an `import { mapComponents }` + `Object.entries()` manual +registration — so every snippet in it rendered an empty map with no diagnostic. What the +component actually reads is the ObjectQL query (`objectName` / `staticData` / `data`, +with `filter` and `sort` as the query's own) plus the declared `map` block +(`latitudeField`, `longitudeField`, `locationField`, `titleField`, `descriptionField`, +`zoom`, `center` as a `[latitude, longitude]` tuple, `style`), and registration is a side +effect of the import. + +The rewrite is deliberately shorter than what it replaces: the exhaustive authoring +reference stays in the one gated copy, `content/docs/plugins/plugin-map.mdx`, and the +README keeps only what is local to the package — what it registers, what it exports, one +working schema of each provider shape, the camera rule since objectui#4941/#5000 (no +default zoom: an undeclared camera fits the records), and the traps a reader cannot infer +(a `map` block replaces the field-name defaults instead of merging with them; an +unreadable `center` is diagnosed, not adapted). Restating the whole schema in an ungated +second copy is what produced this drift, the same failure shape as objectui#3881. + +No code, types, or runtime behaviour change, so this declares no release; the corrected +README reaches npm with `@object-ui/plugin-map`'s next publish. diff --git a/packages/plugin-map/README.md b/packages/plugin-map/README.md index c38bdb7366..e9a91b93bc 100644 --- a/packages/plugin-map/README.md +++ b/packages/plugin-map/README.md @@ -1,15 +1,19 @@ # @object-ui/plugin-map -Map visualization plugin for Object UI - Display geographic data with interactive maps. +Map view plugin for Object UI. -## Features +Renders the records of an **ObjectQL query** as markers on a MapLibre map: every +marker comes from a record's own coordinate fields, and the first paint frames the +records that were fetched. It is a *view over data* — there is no authored marker +list, and no pin you place by hand. -- **Interactive Maps** - Zoomable, pannable map visualization -- **Markers** - Add markers with custom icons and popups -- **Layers** - Multiple map layers support -- **Geolocation** - User location detection -- **Customizable** - Tailwind CSS styling support -- **Responsive** - Mobile-friendly map controls +Importing the package registers two component types on the `ComponentRegistry`, +both resolving to the same renderer: + +- `object-map` — the object-bound renderer +- `map` — the bare spec view-type name (`ViewTypeSchema`'s `'map'`), for a node + authored with it directly. Inside an `ObjectView`, a `map` view is compiled to + an `object-map` node, so both spellings end at the same component. ## Installation @@ -19,261 +23,144 @@ pnpm add @object-ui/plugin-map ## Usage -### Automatic Registration (Side-Effect Import) +Registration is a side effect of the import. There is no manual-registration +export to iterate over — the import *is* the registration. -```typescript -// In your app entry point (e.g., App.tsx or main.tsx) +```ts import '@object-ui/plugin-map'; -// Now you can use map types in your schemas -const schema = { - type: 'map', - center: { lat: 37.7749, lng: -122.4194 }, - zoom: 12, - markers: [ - { lat: 37.7749, lng: -122.4194, label: 'San Francisco' } - ] -}; -``` - -### Manual Registration - -```typescript -import { mapComponents } from '@object-ui/plugin-map'; -import { ComponentRegistry } from '@object-ui/core'; - -// Register map components -Object.entries(mapComponents).forEach(([type, component]) => { - ComponentRegistry.register(type, component); -}); -``` - -## Schema API - -### Map - -Display an interactive map: - -```typescript -{ - type: 'map', - center: { lat: number, lng: number }, - zoom?: number, // Default: 10 - markers?: MapMarker[], - layers?: MapLayer[], - height?: number | string, - onMarkerClick?: (marker) => void, - className?: string -} -``` - -### Map Marker - -```typescript -interface MapMarker { - lat: number; - lng: number; - label?: string; - icon?: string; // Icon URL or name - popup?: string | ReactNode; - color?: string; // Marker color -} -``` - -## Examples - -### Basic Map - -```typescript -const schema = { - type: 'map', - center: { lat: 40.7128, lng: -74.0060 }, - zoom: 13, - height: 500, - markers: [ - { - lat: 40.7128, - lng: -74.0060, - label: 'New York City', - popup: 'The Big Apple' - } - ] -}; -``` - -### Multiple Markers - -```typescript -const schema = { - type: 'map', - center: { lat: 37.7749, lng: -122.4194 }, - zoom: 10, - markers: [ - { - lat: 37.7749, - lng: -122.4194, - label: 'San Francisco', - color: 'red', - popup: 'Golden Gate Bridge' - }, - { - lat: 37.8044, - lng: -122.2712, - label: 'Oakland', - color: 'blue', - popup: 'Port of Oakland' - }, - { - lat: 37.3382, - lng: -121.8863, - label: 'San Jose', - color: 'green', - popup: 'Silicon Valley' - } - ] -}; -``` - -### Interactive Map - -```typescript +// Object-bound: the markers are the records the query returns. const schema = { - type: 'map', - center: { lat: 51.5074, lng: -0.1278 }, - zoom: 12, - markers: [/* markers */], - onMarkerClick: (marker) => { - console.log('Marker clicked:', marker); - // Show marker details + type: 'object-map', + objectName: 'stores', + map: { + latitudeField: 'lat', + longitudeField: 'lng', + titleField: 'name', + descriptionField: 'address', }, - onMapClick: (coordinates) => { - console.log('Map clicked at:', coordinates); - // Add new marker - } }; ``` -### Map with Data Binding - -```typescript -const schema = { - type: 'map', - center: { lat: 37.7749, lng: -122.4194 }, - zoom: 10, - markers: '${data.locations.map(loc => ({ lat: loc.latitude, lng: loc.longitude, label: loc.name }))}', - onMarkerClick: (marker) => { - // Handle marker click - } -}; -``` - -## Integration with ObjectQL - -Connect map to ObjectStack data sources: - -```typescript -import { createObjectStackAdapter } from '@object-ui/data-objectstack'; - -const dataSource = createObjectStackAdapter({ - baseUrl: 'https://api.example.com', - token: 'your-auth-token' -}); +A literal record array instead of a query, with the same `map` block: +```ts const schema = { type: 'object-map', - dataSource, - object: 'locations', - latField: 'latitude', - lngField: 'longitude', - labelField: 'name', - popupField: 'description', - center: { lat: 37.7749, lng: -122.4194 }, - zoom: 10 + staticData: [ + { id: 1, name: 'San Francisco HQ', lat: 37.7749, lng: -122.4194 }, + { id: 2, name: 'Oakland Office', lat: 37.8044, lng: -122.2711 }, + ], + map: { latitudeField: 'lat', longitudeField: 'lng', titleField: 'name' }, }; ``` -## Map Features - -### Custom Markers - -```typescript -const schema = { - type: 'map', - markers: [ - { - lat: 40.7128, - lng: -74.0060, - icon: '/custom-marker.png', - popup: { - type: 'card', - title: 'Location Details', - body: 'Custom popup content' - } - } - ] -}; +`filter` and `sort` are the **query's** filter and order — they reach the data +source as `$filter` / `$orderby`, and the spec's per-element `dataSource` binding +is honoured as well. The map issues no row cap of its own. + +## The `map` block + +The declared configuration input. Every key is optional: + +| Key | Description | +| --- | --- | +| `latitudeField` | Record field holding the latitude. Needs `longitudeField` alongside it; both values must be numbers. | +| `longitudeField` | Record field holding the longitude. | +| `locationField` | Single field holding both coordinates — see the formats below. Used when the lat/lng pair yields nothing. | +| `titleField` | Field shown as the marker title. Omitted, markers are titled `Marker`. | +| `descriptionField` | Field shown under the title in the marker popup. | +| `zoom` | Zoom level. Declaring it opts this view out of the auto-fit (see below). | +| `center` | `[latitude, longitude]` — a two-number **tuple**, latitude first. Declaring it opts this view out of the auto-fit. | +| `style` | MapLibre style URL/spec, replacing the default public demo style. | + +**The block replaces the field-name defaults, it is not merged with them.** With +no map configuration at all the component falls back to the field names +`latitude` / `longitude` / `location` / `name` / `description`; the moment a `map` +block is present, only what it declares is read. So `map: { titleField: 'name' }` +on its own names no coordinate field, places nothing, and renders an empty map +under the excluded-records notice — the defaults do not fill the gap. + +## Initial camera + +There is no default zoom and no default centre. With records to show and no +camera declared, the map **fits the records**: their bounding box, measured along +the shortest arc that contains them (so a set straddling the antimeridian is +framed across the line, not around the far side of the planet), with 48px of +padding and a city-scale zoom ceiling of 12 — a single record does not become a +rooftop view. + +Two cases sit outside the fit: + +- **Nothing placeable** (empty result, or no record yielding coordinates): the + whole world, centred on `0, 0`. +- **A declared camera**: `zoom` or `center` in the `map` block wins and the fit is + skipped. Declaring one half keeps the other derived — `zoom` alone is applied at + the centre of the records, `center` alone at a continental zoom. + +A `center` that is not a two-number tuple (the `{ lat, lng }` object form, say) is +rejected by the config schema, warned about in the console, and **not** adapted — +and it does not cost the view its fit. + +## Coordinate formats + +`locationField` reads any of: + +```ts +{ location: { lat: 37.7749, lng: -122.4194 } } // also latitude/longitude, lon +{ location: '37.7749,-122.4194' } // "lat,lng" +{ location: [37.7749, -122.4194] } // [lat, lng] ``` -### Geolocation +A record whose coordinates are missing, unparseable, or out of range (latitude +beyond ±90, longitude beyond ±180) is left off the map and counted in a notice +above it, rather than being silently dropped or rescued. -```typescript -const schema = { - type: 'map', - useGeolocation: true, // Center map on user's location - zoom: 15, - markers: [] -}; -``` +## What this component does not read -### Map Layers +Keys that look plausible on a map schema but have no read site here: `markers` +(markers are records), `layers`, `height` (the container is a fixed responsive +height, 300px through 600px), `useGeolocation` (the map carries a +user-initiated "show my location" button instead), and per-marker `icon` / +`color` / `popup` styling. A `map` configuration stashed under `filter.map` — a +shape predating the `map` input — is no longer read either, and says so in the +console. -```typescript -const schema = { - type: 'map', - center: { lat: 37.7749, lng: -122.4194 }, - layers: [ - { type: 'heatmap', data: [/* heatmap data */] }, - { type: 'polygon', coordinates: [/* polygon coordinates */], color: 'rgba(255, 0, 0, 0.3)' } - ] -}; -``` - -## TypeScript Support +## Using `ObjectMap` directly -```typescript -import type { MapSchema, MapMarker } from '@object-ui/plugin-map'; +`ObjectMap` (the component), `ObjectMapRenderer` (the registered wrapper, for a +host that registers types itself) and the `ObjectMapProps` type are the package's +exports: -const marker: MapMarker = { - lat: 37.7749, - lng: -122.4194, - label: 'San Francisco', - color: 'red' -}; +```tsx +import { ObjectMap } from '@object-ui/plugin-map'; -const map: MapSchema = { - type: 'map', - center: { lat: 37.7749, lng: -122.4194 }, - zoom: 12, - markers: [marker] -}; + console.log(record)} +/>; ``` -## Customization - -Style the map with Tailwind classes: - -```typescript -const schema = { - type: 'map', - className: 'rounded-lg shadow-xl border-2 border-gray-200', - height: '600px', - center: { lat: 37.7749, lng: -122.4194 } -}; -``` +| Prop | Description | +| --- | --- | +| `schema` | The map schema — the keys above. | +| `dataSource` | Resolves the `object` provider. Not needed for `staticData` or an inline `data` array. | +| `className` | Classes for the wrapper around the map. | +| `onMarkerClick` | Called with the clicked record. | +| `onRowClick` | Record click handler; takes priority over the `navigation` overlay. | +| `onEdit` / `onDelete` | Passing either adds that button to the marker popup (and to the mobile record sheet). | +| `enableClustering` | Forces clustering on; without it, clustering starts above 100 visible markers. | +| `clusterRadius` | Clustering granularity (default `50`): the grid cell is `clusterRadius / 2 ** zoom`, so a larger value groups more aggressively. | + +In a schema-driven page these handlers may equally be authored on the node +itself: `SchemaRenderer` spreads a node's non-metadata properties onto the +component. ## Links -- 📚 [Documentation](https://www.objectui.org/docs/plugins/plugin-map) +- 📚 [Documentation](https://www.objectui.org/docs/plugins/plugin-map) — the full + authoring reference for the schema and the `map` block - 📦 [npm package](https://www.npmjs.com/package/@object-ui/plugin-map) - 📝 [Changelog](./CHANGELOG.md) - 🐛 [Report an issue](https://github.com/objectstack-ai/objectui/issues)