# Map

## Overview

`Map` renders an interactive Google Map with advanced markers, proportional symbology, per-feature selection and hover, theme-aware colors, content-driven framing, rich info windows, circles, polygons, and native overlay layers. It uses the built-in Xertica Google Maps loader and renders explicit container states when data is missing, restricted, or still loading.

---

## Import

```tsx
import {
  Map,
  MapLegend,
  MapNotice,
  MapViewControl,
  GoogleMapsLoaderProvider,
  useGoogleMapsLoader,
  useMapLayers,
  useMapTokenColor,
  useMapTokenColors,
  resolveScale,
  GOOGLE_MAPS_ID,
  GOOGLE_MAPS_LIBRARIES,
  DEFAULT_MAP_ID,
  type MapMarkerData,
  type MapScale,
  type MapFeatureRef,
  type MapLayersConfig,
} from 'xertica-ui/ui';
```

---

## Prerequisites

- Import `xertica-ui/style.css` once at the app root.
- Provide a Google Maps JavaScript API key through `apiKey`, storage, or `<XerticaProvider googleMapsApiKey="...">`.
- If no key is configured, the component renders a non-crashing setup prompt.
- Advanced Markers require a **Map ID**. When `mapId` is omitted, `Map` falls back to `DEFAULT_MAP_ID` (Google's demo id) and warns once in development. Pass your own Cloud-styled `mapId` before shipping to production.

---

## Props

### Core

| Prop          | Type                             | Default   | Description                                               |
| ------------- | -------------------------------- | --------- | --------------------------------------------------------- |
| `center`      | `LatLng`                         | Sao Paulo | Initial map center coordinates.                           |
| `zoom`        | `number`                         | `12`      | Initial zoom level.                                       |
| `markers`     | `MapMarkerData[]`                | `[]`      | Markers to display.                                       |
| `circle`      | `object`                         | -         | Optional single circle overlay.                           |
| `polygon`     | `object`                         | -         | Optional single polygon overlay.                          |
| `layers`      | `MapLayersConfig`                | `{}`      | Native layer toggles for traffic, transit, and bicycling. |
| `height`      | `string`                         | `"400px"` | Container height.                                         |
| `apiKey`      | `string`                         | -         | Per-component Google Maps API key override.               |
| `mapId`       | `string`                         | demo id   | Cloud-styled Map ID. Required for production.             |
| `attribution` | `ReactNode`                      | -         | Cartographic credit, pinned bottom-right, theme-aware.    |
| `ariaLabel`   | `string`                         | `"Map"`   | Accessible name for the map region.                       |
| `onMapLoad`   | `(map: google.maps.Map) => void` | -         | Receives the underlying map instance.                     |

### Color tokens

The Google Maps API only accepts literal colors, so `"var(--destructive)"` never resolves in `fillColor`. Pass a **token name** instead and the library resolves it at runtime — and re-resolves it when the theme changes.

| Prop                | Type         | Description                                                 |
| ------------------- | ------------ | ----------------------------------------------------------- |
| `circleColorToken`  | `ColorToken` | Semantic token for `circle`. Wins over its literal colors.  |
| `polygonColorToken` | `ColorToken` | Semantic token for `polygon`. Wins over its literal colors. |

### Selection and events

| Prop              | Type                                                           | Description                                                |
| ----------------- | -------------------------------------------------------------- | ---------------------------------------------------------- |
| `selectedId`      | `string \| null`                                               | Controlled selection. Requires `markers[].id`.             |
| `onFeatureSelect` | `(id: string \| null, feature: MapFeatureRef \| null) => void` | Fires with `null` when the user clicks the map background. |
| `onFeatureHover`  | `(id: string \| null, feature: MapFeatureRef \| null) => void` | Fires with `null` when pointer or focus leaves a feature.  |
| `onMapClick`      | `(position: LatLng) => void`                                   | Every click on the map surface.                            |

### Framing

| Prop           | Type                                       | Default  | Description                                                                               |
| -------------- | ------------------------------------------ | -------- | ----------------------------------------------------------------------------------------- |
| `bounds`       | `LatLng[] \| { north, south, east, west }` | -        | Explicit frame. Takes precedence over `fitTo`.                                            |
| `fitTo`        | `'markers' \| 'shapes' \| 'all' \| 'none'` | `'none'` | What to frame the camera around.                                                          |
| `fitPadding`   | `number`                                   | `34`     | Pixels of breathing room around fitted content.                                           |
| `maxZoomOnFit` | `number`                                   | -        | Upper zoom bound applied after fitting.                                                   |
| `views`        | `MapView[]`                                | -        | Named camera positions. Pair with `<MapViewControl>`.                                     |
| `activeViewId` | `string`                                   | -        | Active view. Outranks automatic fitting.                                                  |
| `onViewChange` | `(id: string) => void`                     | -        | Consumed by `<MapViewControl>`; accepted here so the view triple can be spread onto both. |

### Geometry, layers and analysis

| Prop                | Type                          | Default  | Description                                                                    |
| ------------------- | ----------------------------- | -------- | ------------------------------------------------------------------------------ |
| `polygons`          | `MapPolygonShape[]`           | -        | Any number of polygons. The singular `polygon` draws alongside.                |
| `circles`           | `MapCircleShape[]`            | -        | Any number of circles. The singular `circle` draws alongside.                  |
| `dataLayers`        | `MapDataLayer[]`              | -        | Named, toggleable layers. Features join one through `layerId`.                 |
| `geojson`           | `GeoJsonFeatureCollection`    | -        | Territorial mesh. Requires `choropleth`.                                       |
| `choropleth`        | `MapChoroplethConfig`         | -        | Paints the mesh by value.                                                      |
| `onChoroplethBands` | `(bands) => void`             | -        | Receives the classes actually painted, to build the legend from.               |
| `heatmap`           | `MapHeatmapConfig`            | -        | Density surface. Needs the `visualization` library.                            |
| `cluster`           | `MapClusterConfig`            | -        | Collapses co-located records into counted markers.                             |
| `minAggregation`    | `number`                      | `0`      | Privacy floor — groups below it are merged, never plotted alone.               |
| `captionCollision`  | `'hide' \| 'none'`            | `'hide'` | Hides captions that overlap.                                                   |
| `onPopupAction`     | `(featureId, action) => void` | -        | Fired by `[data-map-action]` elements inside a popup.                          |
| `controlRef`        | `React.Ref<MapControl>`       | -        | Imperative handle. **Separate from `ref`**, which stays the container element. |

> **`dataLayers` is not `layers`.** `layers` already means Google's native traffic / transit / bicycling overlays. The new concept got a new name rather than changing the meaning of working code.

> **Choose the classification method deliberately.** `'quantile'` puts an equal _count_ in each class; `'equal-interval'` splits the _value range_ evenly. Picking the wrong one is the most common way a choropleth misleads, which is why there is no inferred default beyond `'quantile'`.

### Field tools and time

| Prop                 | Type                                     | Default | Description                                              |
| -------------------- | ---------------------------------------- | ------- | -------------------------------------------------------- |
| `sectors`            | `MapSectorShape[]`                       | -       | Antenna coverage wedges (azimuth + beam width + range).  |
| `polylines`          | `MapPolylineShape[]`                     | -       | Lines through arbitrary coordinates, with optional dash. |
| `annotations`        | `MapAnnotation[]`                        | -       | User-authored pins with authorship and timestamps.       |
| `annotationMode`     | `boolean`                                | `false` | The next map click creates an annotation.                |
| `onAnnotationAdd`    | `(position: LatLng) => void`             | -       | Fires in annotation mode.                                |
| `onAnnotationRemove` | `(id: string) => void`                   | -       | Fires when an annotation pin is activated.               |
| `drawing`            | `MapDrawingConfig`                       | -       | Free-hand geofence drawing.                              |
| `deviceLocation`     | `{ position, accuracyMeters?, mocked? }` | -       | The device's own position and accuracy halo.             |
| `timeField`          | `string`                                 | -       | Key on `markers[].data` holding the timestamp.           |
| `timeWindow`         | `[number, number]`                       | -       | Inclusive window. Applied **before** clustering.         |
| `scaleBar`           | `boolean \| { units?, position? }`       | `false` | Graphic scale bar.                                       |
| `children`           | `ReactNode`                              | -       | Overlays — see `MapOverlay`.                             |

`sectors` derive their polygon from `computeOffset`; `polylines` render a dash as repeated Maps symbols, since `Polyline` has no dash property.

### Basemap

The tiles are the one thing an API key genuinely buys — everything else the map draws is ordinary DOM. When the script cannot load, `Map` keeps the data on screen instead of replacing the panel with a setup prompt.

| Prop              | Type           | Default                            | Description                                                          |
| ----------------- | -------------- | ---------------------------------- | -------------------------------------------------------------------- |
| `basemap`         | `'none'`       | -                                  | Draws the data with no Google basemap, even when a key is available. |
| `baseGeojson`     | `GeoJsonInput` | -                                  | Reference outline drawn behind the markers, in the same projection.  |
| `basemapFallback` | `boolean`      | `true`                             | Fall back to the keyless renderer when the script cannot load.       |
| `noBasemapLabel`  | `string`       | `"Base cartográfica indisponível"` | Text of the permanent notice shown in that mode.                     |

**What survives without a key:** markers (shape, proportional size, token colors, labels), tooltips, selection, hover, keyboard navigation, ARIA, `MapLegend`, `MapNotice`, `MapViewControl`, and `attribution`.

**What does not:** the tile imagery, `circle` / `polygon` (they are `google.maps` objects), and the InfoWindow.

> **Pass `baseGeojson` whenever the geography matters.** Points on a blank surface answer "how many", not "where". With boundaries behind them the view stands on its own — which is the difference between a demo and something usable in a report.

### Container states

Precedence, most specific first: `deniedState` → `errorState` → `loading` → `isEmpty` → missing key → loading script → map. Every state inherits the container's border, radius, and height, so the layout never shifts.

| Prop              | Type        | Default                                       | Description                                     |
| ----------------- | ----------- | --------------------------------------------- | ----------------------------------------------- |
| `loading`         | `boolean`   | `false`                                       | Forces the skeleton regardless of script state. |
| `isEmpty`         | `boolean`   | `false`                                       | The filter returned nothing — a normal outcome. |
| `emptyState`      | `ReactNode` | built-in                                      | Replaces the built-in empty state.              |
| `errorState`      | `ReactNode` | -                                             | Rendering it signals a data error.              |
| `deniedState`     | `ReactNode` | -                                             | Rendering it signals restricted access.         |
| `emptyTitle`      | `string`    | `"No results for the current filters"`        | Built-in empty state title.                     |
| `emptyHint`       | `string`    | `"Adjust the filters to see data on the map"` | Built-in empty state hint.                      |
| `missingKeyTitle` | `string`    | `"Configure Google Maps API Key in Settings"` | Shown when no API key is available.             |
| `loadErrorTitle`  | `string`    | `"Failed to load Google Maps"`                | Shown when the script fails to load.            |
| `loadErrorHint`   | `string`    | `"Check API key in Settings"`                 | Hint below `loadErrorTitle`.                    |

### `MapMarkerData`

| Field                                                                                                       | Type                                     | Description                                               |
| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------- | --------------------------------------------------------- |
| `position`                                                                                                  | `LatLng`                                 | Required.                                                 |
| `id`                                                                                                        | `string`                                 | Required for selection and hover events.                  |
| `shape`                                                                                                     | `'pin' \| 'circle' \| 'square'`          | Defaults to `'pin'`, the classic teardrop.                |
| `radiusPx`                                                                                                  | `number`                                 | Fixed radius for non-pin shapes.                          |
| `scale`                                                                                                     | `MapScale`                               | Derives the radius from a value — proportional symbology. |
| `colorToken`                                                                                                | `ColorToken`                             | Semantic token. Wins over `customColor`.                  |
| `fillOpacity`                                                                                               | `number`                                 | Fill translucency for non-pin shapes. Default `0.85`.     |
| `tooltip`                                                                                                   | `ReactNode`                              | Instant label on hover and keyboard focus.                |
| `tooltipDirection`                                                                                          | `'top' \| 'right' \| 'bottom' \| 'left'` | Default `'top'`.                                          |
| `ariaLabel`                                                                                                 | `string`                                 | Overrides the generated accessible name.                  |
| `precision` / `precisionLabel`                                                                              | `MapPrecision` / `string`                | Declares how the position was obtained.                   |
| `data`                                                                                                      | `unknown`                                | Echoed back in `MapFeatureRef`.                           |
| `label`, `title`, `info`, `customColor`, `icon`, `iconSvg`, `iconColor`, `infoWindowContent`, `richContent` | —                                        | Established fields; behavior unchanged.                   |

---

## Public Utilities

`useMapTokenColor(token, fallback?)` resolves a CSS custom property to a literal color and re-resolves it on theme change. `useMapTokenColors(tokens[], fallback?)` is the batch form for array-driven props, and `useMapColorResolver()` returns a resolver function for tokens discovered during render.

`resolveScale(scale)` maps a data value to a pixel size. Use the same `MapScale` object for `markers[].scale` and `MapLegend`'s `sizeKey.scale`.

`useMapFit(options)` and `useMapLayers(map, layers)` are public for integrations that already own a `google.maps.Map` instance.

`normalizeMapColor(color)` and `withAlpha(color, alpha)` convert CSS colors into forms the Maps API accepts.

`GOOGLE_MAPS_ID`, `GOOGLE_MAPS_LIBRARIES`, and `DEFAULT_MAP_ID` are public constants for integrations that need to inspect or align with the built-in loader.

---

## Examples

### Proportional symbols with a size key

```tsx
import { Map, MapLegend, type MapScale } from 'xertica-ui/ui';

const scale: MapScale = { value: 0, domain: [0, 900], range: [6, 24], method: 'sqrt' };

<div className="relative">
  <Map
    height="520px"
    fitTo="markers"
    maxZoomOnFit={9}
    markers={municipalities.map(m => ({
      id: m.id,
      position: m.position,
      title: m.name,
      shape: 'circle',
      scale: { ...scale, value: m.attendances },
      colorToken: '--chart-1',
      fillOpacity: 0.45,
    }))}
  />
  <MapLegend
    variant="overlay"
    position="bottom-left"
    title="Atendimentos no mês"
    sizeKey={{ values: [41, 233, 874], scale, unit: 'un.', colorToken: '--chart-1' }}
  />
</div>;
```

### Click a point to filter the dashboard

```tsx
const [selectedId, setSelectedId] = useState<string | null>(null);

<Map
  height="480px"
  selectedId={selectedId}
  onFeatureSelect={id => setSelectedId(id)}
  markers={units.map(u => ({ id: u.id, position: u.position, title: u.name }))}
/>;
```

Markers are focusable, respond to Enter and Space, and expose `aria-pressed` while a selection is active. Clicking the map background calls `onFeatureSelect(null, null)`.

### Container states

```tsx
<Map
  height="400px"
  loading={isLoading}
  isEmpty={!isLoading && results.length === 0}
  errorState={error ? <p>Falha ao carregar os atendimentos</p> : undefined}
  deniedState={!canViewLocations ? <p>Acesso restrito</p> : undefined}
  markers={markers}
/>
```

### Surviving a missing key, an exhausted quota, or a blocked network

```tsx
<Map
  height="520px"
  baseGeojson={rsBoundaries} // outline gives the points context
  attribution="© IBGE — malha municipal"
  markers={municipalities.map(m => ({
    id: m.id,
    position: m.position,
    title: m.name,
    shape: 'circle',
    scale: { ...scale, value: m.attendances },
    colorToken: '--chart-1',
  }))}
/>
```

With a key this renders the Google map. Without one — or when `maps.googleapis.com` is unreachable — it renders the same markers over the outline, with a permanent notice saying the basemap is unavailable. No prop change is required for the fallback; pass `basemapFallback={false}` to opt out and get the setup prompt instead.

Force the mode with a key present (print, privacy, tile cost) using `basemap="none"`.

---

## Known limitations

- The keyless basemap mode draws markers, overlays and the `baseGeojson` outline. Everything backed by a `google.maps` object — geometry, choropleth, heatmap, drawing — needs the script.
- `useMapSnapshot` composes the data layer, not the tile imagery: Google's tiles are cross-origin and taint a canvas, so they cannot be captured client-side without a rasterizing dependency or a separate Static Maps call. See its documentation.
- `circle`, `polygon`, `polygons`, `circles`, the choropleth and the heatmap are not drawn in the keyless basemap mode — they are `google.maps` objects. Markers, overlays and the `baseGeojson` outline are.

---

## AI Rules

- **Always** set visible height through `height` or layout constraints.
- **Always** use `colorToken` (e.g. `'--chart-1'`) for new surfaces — a hex literal will not follow a light/dark switch. The `customColor` / `fillColor` / `strokeColor` literals remain accepted for compatibility.
- **Always** give every marker an `id` when using `selectedId`, `onFeatureSelect`, or `onFeatureHover`. Markers without an id are not selectable.
- **Always** pair `markers[].scale` with a `<MapLegend sizeKey>` built from the same `MapScale` object — a proportional symbol is unreadable without a stated scale.
- **Always** pass a production `mapId`. The default is Google's rate-limited demo id.
- **Never** install another Google Maps script loader; use `GoogleMapsLoaderProvider` or `XerticaProvider`.
- **Never** write `"var(--primary)"` into a color prop. It is a CSS expression, and the Maps API consumes literal colors only.
- **Never** re-implement the CSS-token-to-color resolver in application code — use `useMapTokenColor`, which also survives a theme change.
- **Never** treat an empty result as an error. Use `isEmpty`, not `errorState`.
- **Always** pass `baseGeojson` when the keyless mode has to answer "where". A scatter of points on a blank surface is a chart, not a map.
- **Never** suppress the no-basemap notice by styling it away. In a domain where position carries legal or investigative weight, a surface that looks like a map but has no basemap is a misreading risk.
- **Always** build the choropleth legend from `onChoroplethBands`, never from a second classification computed by hand.
- **Always** state `precision` / `precisionLabel` when a position is a centroid. `minAggregation` is **display** anonymization — it changes what is drawn, never what the payload contains. Coordinates precise enough to identify someone must be withheld server-side.
- **Never** swap `ref` for the imperative handle. Use `controlRef`; `ref` is the container element and consumers depend on it.
- **Always** apply `timeWindow` through the prop rather than pre-filtering `markers`, when clustering is also on — the window is applied before aggregation so badges count only what is visible.
- **Never** put a basemap filter on the map container. The library filters the tile pane only, deliberately.
- `basemap` outranks `mapTypeId`: choosing "satellite" in the basemap control is a later, more specific decision than the initial map type. Set one or the other, not both.
- Use `{ lat, lng }` coordinates, never `{ latitude, longitude }`.
- Common mistake: calling a hook per marker to resolve colors. Use `useMapTokenColors(tokens[])` — one call for the whole array.
