@kahwee/sf-map-svg 4.5.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/EXAMPLES.md CHANGED
@@ -105,3 +105,32 @@ API** includes executable motion, reduced-motion, and progressive-shell checks.
105
105
  See [consumer integration](consumer-integration.md) for server and browser recipes.
106
106
 
107
107
  For the controller and explicit data imports, see [the v3 migration guide](migration-v3.md).
108
+
109
+ ## Find areas containing a location
110
+
111
+ New in v5. This example runs in
112
+ Node or a browser without mounting a map and imports only the SFAR definition:
113
+
114
+ ```ts
115
+ import { geometryBounds, geometryContainsPoint } from '@kahwee/sf-map-svg/geometry';
116
+ import { neighborhoods } from '@kahwee/sf-map-svg/data/realtor';
117
+
118
+ const location = [-122.42, 37.76] as const;
119
+ const matches = neighborhoods.features.filter((feature) =>
120
+ geometryContainsPoint(feature.geometry, location),
121
+ );
122
+ for (const feature of matches) {
123
+ console.log(feature.id, feature.properties.canonicalName, geometryBounds(feature.geometry));
124
+ }
125
+ ```
126
+
127
+ Keep all matches when the location lies on a shared boundary. An empty result
128
+ means no supplied polygon contains the location; it does not trigger a switch
129
+ to another neighborhood definition. For an existing map using the same realtor
130
+ source, pass a chosen match to
131
+ `map.selectFeature({ kind: 'neighborhood', source: 'realtor', id: feature.id })`.
132
+ To fit it, use `map.camera.fit(feature.geometry)`. Geographic bounds cannot be
133
+ passed directly to `camera.set`, which takes projected `[x, y, size]`.
134
+
135
+ See [query semantics and limitations](API.md#geographic-queries) and the
136
+ [HTML overlay recipe](consumer-integration.md#position-html-overlays).
package/docs/api-audit.md CHANGED
@@ -20,8 +20,8 @@ unbounded geographic data can never fail.
20
20
  | Viewport marker rendering | Individual pin visuals allocate on first visibility or selection; offscreen pins and cluster symbols detach with screen-space overscan. Uncreated records receive current labels/styles on first materialization. Full catalog/picker and cached node identities remain available; selected/focused pins stay mounted. Pan reuses global cluster membership and nodes until zoom, data, selection, focus, or clustering changes. |
21
21
  | Marker entrance and reduced motion | Entrances begin on first visibility, are independent of camera motion, and cancel on preference changes or disposal, including pending entrances. Cached pins do not restart entrances on reentry. Stable IDs avoid repeated entrances on filtering. |
22
22
  | Hiding touch control while gestures are enabled | Returns touch scrolling to the page. Other toolbar controls remain independent. |
23
- | Convenience mode/source setters | Share the atomic configuration path. `{ resetView: false }` preserves the camera; omitted options retain the historic reset. Invalid options and unavailable geography leave state unchanged. |
24
- | Runtime patch omitted/false/undefined | Omitted retains; false disables boolean switches; undefined removes group/key overrides. Top-level mode/source/year/labels set to undefined retain their current values. Nested feature objects replace, rather than deep-merge. |
23
+ | Convenience mode/source setters | Share the atomic configuration path. Setters preserve the camera; explicit camera methods move it. Unavailable geography leaves state unchanged. |
24
+ | Runtime patch omitted/false/undefined | Controller patches ignore omitted/undefined values at every record depth; false disables boolean switches. Nested feature records merge. Explicit resetConfiguration clears presentation groups. Low-level element setters retain their separate contract. |
25
25
  | Compact shell and full attribution | Rejected before replacement. Use `createGuideController` for full attribution. Control overrides are honored and may deliberately alter layout. |
26
26
  | District style callbacks | Prepare and copy every style before committing. Hover/selection reuse the results; call `setDistrictStyle()` to refresh changed external data. Reentrant updates or destruction supersede pending work. |
27
27
  | Rapid district year changes | Both outgoing layers are tracked, inert, and removed on interruption, completion, reduced-motion changes, or destruction. Zero duration creates no transition copies. |
@@ -58,15 +58,32 @@ exceptions are not transactional validation errors and do not roll back an alrea
58
58
  committed change. Direct mutation of the returned DOM can invalidate invariants;
59
59
  use the public methods and `overlayElement` extension point.
60
60
 
61
- Static grouped layers and appearance reject unknown keys, while flat compatibility options retain their existing surface; strict
62
- interactive option-key validation is not a claim that all static options share the
63
- same schema. Host layout, delayed images, fonts, and application cards need their
61
+ Static flat and grouped options reject unknown keys and invalid values before
62
+ rendering. Grouped options take precedence over flat compatibility options;
63
+ static and interactive schemas retain their documented differences. Host layout, delayed images, fonts, and application cards need their
64
64
  own CLS tests. The scale bar is an approximate local Mercator scale, not survey
65
65
  instrumentation. Coincident locations use a place chooser rather than spiderfying. Geographic
66
66
  source correctness remains governed by SOURCES.md and existing topology tests.
67
67
 
68
68
  ## Extending the contract
69
69
 
70
+ ### V5 consistency contract
71
+
72
+ - Geometry helpers export their input/result types alongside functions and use
73
+ geometry-first arguments with readonly GeoJSON positions.
74
+ - Marker positions, static projection, and screen projection all use
75
+ `[longitude, latitude]`. Screen projection returns CSS pixels; camera viewports
76
+ remain projected map units and pan deltas remain CSS pixels.
77
+ - Controller `configure`, `setMode`, and `setSource` preserve the camera. Camera
78
+ movement is explicit through camera methods or selection's `fit` option.
79
+ - Controller patches ignore omitted/undefined values at every depth and merge
80
+ nested records. `resetConfiguration` clears named presentation groups to
81
+ built-in defaults without changing geographic choices or camera.
82
+ - Controller neighborhood selection requires exact IDs. Prefer source/year-qualified
83
+ `selectFeature`; name lookup belongs in source-specific dataset helpers.
84
+ - Construction-time neighborhood lookup and low-level element APIs retain their
85
+ separately documented contracts. See [v5 migration](migration-v5.md).
86
+
70
87
  For every new option, define its default, disable/reset semantics, precedence,
71
88
  validation boundary, cleanup owner, and effect on keyboard focus and selection.
72
89
  Test invalid updates against an existing live map, not only clean construction.
@@ -35,7 +35,7 @@ For a fixed-width embed, use shared inputs on the server and in the browser:
35
35
  ```ts
36
36
  // Shared inputs: coordinates, IDs, radii, colors and layer choices must match.
37
37
  const guideMarkers = [
38
- { id: 'ferry', label: 'Ferry Building', lng: -122.3937, lat: 37.7955, radius: 7 },
38
+ { id: 'ferry', label: 'Ferry Building', position: [-122.3937, 37.7955] as const, radius: 7 },
39
39
  ];
40
40
  const guideLayers = { neighborhoodLabels: true, roadLabels: true };
41
41
  const guideColors = { label: '#183e58', landmark: '#426641' };
@@ -125,3 +125,49 @@ const binding = bindMapCards(map, document.querySelector('#place-cards'), {
125
125
  Style `[data-map-selected]` with an outline or background change that remains legible in both themes. Use a visible focus outline on card buttons and links. Hover/focus only previews the location: it preserves selection and camera. Map selection reveals the card without shifting keyboard focus; set `scrollOnSelect: false` if your application owns scrolling. Match cards and markers by stable IDs, and keep hidden/filtered places out of both catalogs when that is your intended filter behavior.
126
126
 
127
127
  Run `pnpm demo` and open `examples/generated/linked-cards.html` for the complete pattern. Storybook **Maps / Linked place cards** checks hover, keyboard selection, card reveal, both palettes, and disposal.
128
+
129
+ ## Position HTML overlays
130
+
131
+ In v5, pass the same readonly `[longitude, latitude]` position used by static
132
+ projection. The former two-number call is removed; see the
133
+ [migration guide](migration-v5.md).
134
+
135
+ Call this helper after mounting the map in a container with nonzero width:
136
+
137
+ ```ts
138
+ import type { MapController } from '@kahwee/sf-map-svg';
139
+ import type { Position } from '@kahwee/sf-map-svg/geometry';
140
+
141
+ function attachCallout(map: MapController, location: Position, label: string) {
142
+ const callout = document.createElement('span');
143
+ callout.textContent = label;
144
+ callout.style.cssText = 'position:absolute;pointer-events:none;transform:translate(-50%,-100%)';
145
+ const update = () => {
146
+ const point = map.projectToScreen(location);
147
+ callout.hidden = !point.visible;
148
+ callout.style.left = `${point.x}px`;
149
+ callout.style.top = `${point.y}px`;
150
+ };
151
+ update();
152
+ map.overlayElement.append(callout);
153
+ const stopViewport = map.on('viewportchange', update);
154
+ const stopResize = map.on('mapresize', update);
155
+ return () => {
156
+ stopViewport();
157
+ stopResize();
158
+ callout.remove();
159
+ };
160
+ }
161
+ ```
162
+
163
+ Use `const detach = attachCallout(map, [-122.3937, 37.7955], 'Ferry Building')`
164
+ with your mounted controller. Call `detach()` before `map.destroy()` on unmount.
165
+ Keep the container visible while projecting; an unmounted or zero-width canvas
166
+ throws. Controller calls after destruction also throw. Camera moves and resize
167
+ both require repositioning; a one-time projection becomes stale.
168
+
169
+ `x` and `y` are CSS pixels relative to `overlayElement`, not page coordinates or
170
+ SVG units. `visible` tests inclusion in the map viewport; it does not account
171
+ for occlusion by other content. This noninteractive label uses `textContent`
172
+ and leaves pointer input to the map. Interactive callouts need their own
173
+ keyboard, focus, and dismissal behavior. See [coordinates and units](API.md#coordinates-and-units).
@@ -10,6 +10,7 @@ Choose the renderer and geography separately. `renderMap` produces a string with
10
10
  | Explore districts or several neighborhood definitions | `/map`: `createMap` | `/data/full`: `fullMapData` |
11
11
  | Embed a smaller neighborhood guide | `/guide/map`: `createGuideController` | Included lightweight guide geography |
12
12
  | Use the guide data with the general controller | `/map`: `createMap` | `/guide/data`: `guideMapData` |
13
+ | Query bounds or polygon containment without a DOM | `/geometry` | Caller-supplied geometry or explicit dataset imports |
13
14
  | Look up names without mounting a map | `/data/lookup` | Source-aware lookup helpers |
14
15
 
15
16
  Imports starting with `/` in this table are package suffixes, such as `@kahwee/sf-map-svg/static`. Node 24+ is required for server rendering. Browser examples need a DOM and a bundler that supports JSON modules. This is an ESM package.
@@ -96,22 +97,43 @@ SFAR realtor neighborhoods are the default when supplied. If only an alternative
96
97
  | Camera | `map.camera.set/pan/zoom/fit/reset(...)` |
97
98
  | Palette, typography, control styling | `map.configure({ appearance })`; retains camera, selection and focus |
98
99
 
99
- Appearance token objects merge by key; `undefined` removes an override and `appearance: undefined` resets the group. Static maps accept grouped `layers` and the shared `appearance` keys (`theme`, `colors`, `districtStyle`) too. See [the API contract](API.md#live-configuration-and-inspection) and [the design playground](https://kahwee.github.io/sf-map-svg/playground.html).
100
-
101
- `configure` merges supplied keys; omitted keys keep their existing values. Set a layer or control key to `undefined` to remove its override. Set a whole group to `undefined` to reset that group. Set a feature to `false` to disable it. Explicit layer overrides keep winning after mode changes.
100
+ Controller patches merge nested token and feature records. Omitted and `undefined`
101
+ values retain existing values. Use `false` to disable a boolean switch and
102
+ `resetConfiguration()` to clear presentation overrides explicitly:
102
103
 
103
104
  ```ts
104
105
  map.configure({ layers: { neighborhoodLabels: false } });
105
- map.configure({ layers: { neighborhoodLabels: undefined } }); // follow mode again
106
106
  map.configure({ features: { motion: false } });
107
- map.configure({ layers: undefined }); // clear all explicit layer overrides
107
+ map.resetConfiguration(['layers']); // follow mode-derived defaults again
108
+ map.resetConfiguration(['appearance']); // built-in palette and typography
108
109
  ```
109
110
 
110
- `getConfiguration()` returns current mode/source/year/labels and a detached snapshot of the presentation groups, including explicit layer and appearance overrides. Camera and selection remain separate runtime state. Use `getResolvedConfiguration()` for current mode/source/year/labels and effective layer switches, and `getCapabilities()` for supplied geography. Invalid configuration patches leave the current map unchanged. Convenience methods use the same atomic update path: pass `map.setMode(mode, { resetView: false })` or `map.setSource(source, { resetView: false })` to keep the camera. Their default still resets the view.
111
+ Reset groups are `features`, `layers`, `controls`, and `appearance`; omitting
112
+ the argument resets all four. Resets preserve geographic choices and camera,
113
+ and do not restore constructor overrides. Static maps accept grouped layers and
114
+ the shared appearance keys (`theme`, `colors`, `districtStyle`) at construction.
115
+ See [the API contract](API.md#live-configuration-and-inspection).
116
+
117
+ `getConfiguration()` returns a detached snapshot of geographic choices and
118
+ presentation overrides. Use `getResolvedConfiguration()` for effective layers
119
+ and `getCapabilities()` for supplied geography. Invalid patches preserve state.
120
+ `setMode`, `setSource`, and `configure` all preserve the camera; request movement
121
+ with `camera.reset()` or `camera.fit(geometry)`. See the
122
+ [v5 migration guide](migration-v5.md) for changed call forms.
123
+
124
+ ## Geographic queries
125
+
126
+ The v5 `/geometry` helpers `geometryBounds(geometry)` and
127
+ `geometryContainsPoint(geometry, position)` work without a DOM or bundled
128
+ geography. Import `Bounds`, `Geometry`, and `Position` types from the same entry.
129
+ Use containment for area lookup and bounds for geographic extents; pass the
130
+ original geometry to `camera.fit`, rather than geographic bounds to `camera.set`.
131
+ See the [worked example](EXAMPLES.md#find-areas-containing-a-location) and
132
+ [full query semantics](API.md#geographic-queries).
111
133
 
112
134
  ## Markers, routes and camera coordinates
113
135
 
114
- Marker positions are `{ lng, lat }`. GeoJSON coordinates and the static projection use `[longitude, latitude]`, in WGS84. Camera viewports use `[x, y, size]` in the fixed 800 × 800 projected map space; they are not geographic bounds. `camera.pan` uses projected units. `projectToScreen(lng, lat)` returns pixels relative to the map canvas, plus visibility.
136
+ Markers use `position: [longitude, latitude]`. GeoJSON coordinates and the static projection use `[longitude, latitude]`, in WGS84. Camera viewports use `[x, y, size]` in the fixed 800 × 800 projected map space; they are not geographic bounds. `camera.pan` takes deltas in CSS pixels and converts them using the mounted canvas width. `projectToScreen([lng, lat])` returns CSS pixels relative to the map canvas, plus viewport visibility. The former two-number call is removed in v5.
115
137
 
116
138
  Marker and overlay IDs must be unique. Keep marker IDs stable across updates so retained markers preserve DOM nodes, focus and entrance animations. Use `null` to clear a selection; selection methods return a boolean. Selection events can contain null values. `map.on()` returns an unsubscribe function.
117
139
 
@@ -1,12 +1,12 @@
1
1
  # Guide bundle size report
2
2
 
3
- Generated 2026-10-14 by `pnpm report:guide` with Vite production minification and gzip compression. Each emitted JS chunk is compressed independently. The report measures the explicit-data root and the optional `@kahwee/sf-map-svg/guide` preset.
3
+ Generated 2026-10-20 by `pnpm report:guide` with Vite production minification and gzip compression. Each emitted JS chunk is compressed independently. The report measures the explicit-data root and the optional `@kahwee/sf-map-svg/guide` preset.
4
4
 
5
5
  | Entry | Initial JS, raw | Initial JS, gzip | Explicit detail JS, gzip |
6
6
  | --- | ---: | ---: | ---: |
7
- | Renderer root (explicit data) | 122.8 KB | 36.3 KB | — |
8
- | Static renderer | 33.2 KB | 10.2 KB | — |
9
- | Guide preset | 507.0 KB | 122.1 KB | 467.0 KB |
7
+ | Renderer root (explicit data) | 124.2 KB | 36.5 KB | — |
8
+ | Static renderer | 33.4 KB | 10.3 KB | — |
9
+ | Guide preset | 508.4 KB | 122.3 KB | 467.0 KB |
10
10
 
11
11
  **500 KB target:** met.
12
12
 
@@ -0,0 +1,100 @@
1
+ # Migrating to v5
2
+
3
+ Version 5.0.0 makes coordinate inputs, controller updates, and reset behavior
4
+ consistent. It requires Node 24+ and retains offline rendering, explicit data
5
+ imports, zero runtime dependencies, and the existing default map appearance.
6
+ The version bump prepares the next release; it does not publish the package.
7
+
8
+ ## Coordinates use GeoJSON positions
9
+
10
+ Markers now require `position: [longitude, latitude]`. The old `lng` and `lat`
11
+ properties are rejected. `projectToScreen` takes one readonly position; its
12
+ old two-number form is removed. Altitude is accepted and ignored by projection.
13
+
14
+ ```ts
15
+ // v4
16
+ const oldMarker = { id: 'ferry', lng: -122.3937, lat: 37.7955 };
17
+ // v5
18
+ const marker = { id: 'ferry', position: [-122.3937, 37.7955] as const };
19
+ map.setMarkers([marker]);
20
+ const point = map.projectToScreen(marker.position);
21
+ ```
22
+
23
+ Use `Position` from `/geometry` for reusable positions and `ScreenPoint` or
24
+ `ProjectToScreen` from the root or `/map` for screen-projection contracts.
25
+ `x` and `y` remain canvas-relative CSS pixels; `visible` means inside the
26
+ viewport, not unobscured. Mount the map in a nonzero-width container first.
27
+ Camera viewports still use projected `[x, y, size]`; camera pan deltas are CSS
28
+ pixels. Geographic bounds are not camera viewports.
29
+
30
+ ## Controller updates preserve the camera
31
+
32
+ `configure`, `setMode`, and `setSource` preserve the camera. The second
33
+ `{ resetView }` argument to the controller convenience setters is removed.
34
+ To reproduce an old automatic reset, request it explicitly:
35
+
36
+ ```ts
37
+ map.setMode('neighborhoods');
38
+ map.camera.reset({ animate: false });
39
+ ```
40
+
41
+ Selection still fits by default. Use `{ fit: false }` to change selection
42
+ without moving the camera, or `camera.fit(geometry)` for an explicit fit.
43
+
44
+ ## Reset presentation explicitly
45
+
46
+ In controller `configure` patches, omitted and `undefined` values are no-ops at
47
+ every record depth. Nested token and feature records merge; an explicit boolean
48
+ replaces a feature record and `false` disables a boolean switch.
49
+
50
+ ```ts
51
+ map.configure({ appearance: { colors: { water: '#142d45' } } });
52
+ map.configure({ appearance: undefined }); // keeps the current appearance
53
+ map.resetConfiguration(['appearance']); // restores built-in appearance defaults
54
+ map.resetConfiguration(['layers', 'controls']); // removes explicit overrides
55
+ map.resetConfiguration(); // resets all four presentation groups
56
+ ```
57
+
58
+ The reset groups are `features`, `layers`, `controls`, and `appearance`.
59
+ Resetting a group does not restore initial constructor overrides, change the
60
+ camera, or change mode/source/year/labels. It restores built-in defaults and
61
+ mode-derived layer choices. Supply geographic choices explicitly through
62
+ `configure`. To remove a single token while retaining other overrides, build
63
+ the desired group, reset that group, then configure the retained values.
64
+ Each call commits atomically; the two calls are separate updates.
65
+
66
+ These rules describe `MapController` from `createMap`, `createGuideController`,
67
+ and `mountGuideController`. Low-level element factories retain their separate
68
+ `setFeatures` / `setLayers` APIs; use controllers for the grouped update contract.
69
+
70
+ ## Select neighborhoods by exact identity
71
+
72
+ Controller `selectNeighborhood(id)` accepts an exact ID in the current source,
73
+ rather than a display name or alias. Unknown IDs return `false` without changing
74
+ selection. Resolve a human-entered name using the source-specific lookup first.
75
+ For integrations, prefer the source-qualified identity:
76
+
77
+ ```ts
78
+ import { getRealtorNeighborhood } from '@kahwee/sf-map-svg/data/realtor';
79
+
80
+ const feature = getRealtorNeighborhood('Inner Mission');
81
+ if (feature) {
82
+ map.selectFeature({ kind: 'neighborhood', source: 'realtor', id: feature.id }, { fit: false });
83
+ }
84
+ ```
85
+
86
+ `selectFeature` requires the current supplied source/year and never switches
87
+ geography implicitly. Marker IDs and numeric district IDs remain unchanged.
88
+ The construction-time `neighborhood` option and low-level element lookup APIs
89
+ continue to resolve names; the stricter ID contract applies to controller
90
+ selection methods. Existing selection event names remain available alongside
91
+ `selectionchange`.
92
+
93
+ ## Offline geometry queries
94
+
95
+ `geometryBounds(geometry)` and `geometryContainsPoint(geometry, position)` are
96
+ new in v5. They accept readonly GeoJSON without a DOM or geographic imports.
97
+ Bounds are `[west, south, east, north]` or `null` for empty geometry. Containment
98
+ respects holes, includes polygon boundaries, and supports islands and nested
99
+ collections. Shared edges can match more than one feature. See the
100
+ [query contract](API.md#geographic-queries) for validation and planar limitations.