@kahwee/sf-map-svg 1.2.0 → 1.3.1

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.
@@ -0,0 +1,10 @@
1
+ import type { InteractiveSFMapData } from './explorer-data.js';
2
+ import type { InteractiveSFMapElement, InteractiveSFMapOptions } from './interactive-data.js';
3
+ /** Compact, source-aware San Francisco guide data. Imports no district or alternate-neighborhood files. */
4
+ export declare const guideMapData: InteractiveSFMapData;
5
+ /** Guide map preset: SFAR neighborhoods, major parks, BART, and curated roads. */
6
+ export declare function createGuideMap(options?: InteractiveSFMapOptions): InteractiveSFMapElement;
7
+ /** Load the detailed versions of the guide's selected geography on explicit request. */
8
+ export declare function loadGuideDetailedData(): Promise<InteractiveSFMapData>;
9
+ export type { InteractiveSFMapData } from './explorer-data.js';
10
+ export type { InteractiveSFMapElement, InteractiveSFMapOptions };
@@ -0,0 +1,107 @@
1
+ import bart from '../data/guide/bart-stations.json' with { type: 'json' };
2
+ import coast from '../data/guide/coast.json' with { type: 'json' };
3
+ import highways from '../data/guide/highways.json' with { type: 'json' };
4
+ import roads from '../data/guide/key-roads.json' with { type: 'json' };
5
+ import landmarks from '../data/guide/landmarks.json' with { type: 'json' };
6
+ import neighborhoods from '../data/guide/neighborhoods-realtor.json' with { type: 'json' };
7
+ import { createInteractiveSFMapWithData } from './interactive-data.js';
8
+ const neighborhoodData = neighborhoods;
9
+ const highwayData = highways;
10
+ const parkData = landmarks;
11
+ const roadData = roads;
12
+ const stationData = bart;
13
+ /** Compact, source-aware San Francisco guide data. Imports no district or alternate-neighborhood files. */
14
+ export const guideMapData = {
15
+ map: {
16
+ coast: coast.features[0].geometry,
17
+ neighborhoods: neighborhoodData.features.map((feature) => ({
18
+ name: feature.properties.canonicalName,
19
+ geometry: feature.geometry,
20
+ })),
21
+ highways: highwayData.features.map((feature) => ({
22
+ route: String(feature.properties.route),
23
+ geometry: feature.geometry,
24
+ })),
25
+ landmarks: parkData.features.map(({ id, properties, geometry }) => ({
26
+ id,
27
+ ...properties,
28
+ geometry,
29
+ })),
30
+ keyRoads: roadData.features.map(({ id, properties, geometry }) => ({
31
+ id,
32
+ ...properties,
33
+ sourceNames: properties.sourceNames ?? [],
34
+ segmentIds: properties.segmentIds ?? [],
35
+ geometry,
36
+ })),
37
+ bartStations: stationData.features.flatMap(({ id, properties, geometry }) => geometry.type === 'Point'
38
+ ? [{ id, name: properties.name, coordinates: geometry.coordinates }]
39
+ : []),
40
+ },
41
+ neighborhoods: { realtor: neighborhoodData },
42
+ };
43
+ /** Guide map preset: SFAR neighborhoods, major parks, BART, and curated roads. */
44
+ export function createGuideMap(options = {}) {
45
+ return createInteractiveSFMapWithData(guideMapData, {
46
+ mode: 'neighborhoods',
47
+ ...options,
48
+ layers: {
49
+ districtFills: false,
50
+ districtLines: false,
51
+ districtLabels: false,
52
+ neighborhoodLines: true,
53
+ neighborhoodLabels: true,
54
+ landmarks: true,
55
+ bartStations: true,
56
+ highways: true,
57
+ keyRoads: true,
58
+ roadLabels: true,
59
+ ...options.layers,
60
+ },
61
+ });
62
+ }
63
+ /** Load the detailed versions of the guide's selected geography on explicit request. */
64
+ export async function loadGuideDetailedData() {
65
+ const [coastData, neighborhoodData, parkData, highwayData, roadData, bartData] = await Promise.all([
66
+ import('../data/coast.json', { with: { type: 'json' } }),
67
+ import('../data/neighborhoods-realtor.json', { with: { type: 'json' } }),
68
+ import('../data/landmarks.json', { with: { type: 'json' } }),
69
+ import('../data/guide/highways-detailed.json', { with: { type: 'json' } }),
70
+ import('../data/key-roads.json', { with: { type: 'json' } }),
71
+ import('../data/bart-stations.json', { with: { type: 'json' } }),
72
+ ]);
73
+ const detailedNeighborhoods = neighborhoodData.default;
74
+ const detailedRoads = roadData.default;
75
+ const detailedHighways = highwayData.default;
76
+ const detailedParks = parkData.default;
77
+ const detailedStations = bartData.default;
78
+ return {
79
+ map: {
80
+ coast: coastData.default.features[0].geometry,
81
+ neighborhoods: detailedNeighborhoods.features.map((feature) => ({
82
+ name: feature.properties.canonicalName,
83
+ geometry: feature.geometry,
84
+ })),
85
+ highways: detailedHighways.features
86
+ .filter((feature) => ['1', '101', '280'].includes(String(feature.properties.route)))
87
+ .map((feature) => ({
88
+ route: String(feature.properties.route),
89
+ geometry: feature.geometry,
90
+ })),
91
+ landmarks: detailedParks.features.map(({ id, properties, geometry }) => ({
92
+ id,
93
+ ...properties,
94
+ geometry,
95
+ })),
96
+ keyRoads: detailedRoads.features.map(({ id, properties, geometry }) => ({
97
+ id,
98
+ ...properties,
99
+ geometry,
100
+ })),
101
+ bartStations: detailedStations.features.flatMap(({ id, properties, geometry }) => geometry.type === 'Point'
102
+ ? [{ id, name: properties.name, coordinates: geometry.coordinates }]
103
+ : []),
104
+ },
105
+ neighborhoods: { realtor: detailedNeighborhoods },
106
+ };
107
+ }
@@ -0,0 +1,6 @@
1
+ import type { InteractiveSFMapData } from './explorer-data.js';
2
+ import type { NeighborhoodExplorerOptions } from './types.js';
3
+ export type { InteractiveSFMapData } from './explorer-data.js';
4
+ export type { InteractiveLayers, MapPadding, MapViewport, NeighborhoodExplorerElement as InteractiveSFMapElement, NeighborhoodExplorerOptions as InteractiveSFMapOptions, NeighborhoodSelection, } from './types.js';
5
+ /** Browser map shell that bundles only the geography supplied by the caller. */
6
+ export declare function createInteractiveSFMapWithData(data: InteractiveSFMapData, options?: NeighborhoodExplorerOptions): import("./types.js").NeighborhoodExplorerElement;
@@ -0,0 +1,5 @@
1
+ import { createNeighborhoodExplorerCore } from './explorer-core.js';
2
+ /** Browser map shell that bundles only the geography supplied by the caller. */
3
+ export function createInteractiveSFMapWithData(data, options = {}) {
4
+ return createNeighborhoodExplorerCore({ mode: 'basemap', ...options, interface: 'map' }, data);
5
+ }
@@ -52,7 +52,12 @@ export function markers(items, { project, colors }) {
52
52
  .join('')}</g>`;
53
53
  }
54
54
  export function keyRoads(items, { idPrefix, path, colors }) {
55
- return `<g data-layer="key-roads" clip-path="url(#${idPrefix}-coast)">${items.map((road) => `<path data-key-road="${escapeXml(road.id)}" d="${path(road.geometry)}" ${stroke(colors.road, 1)}><title>${escapeXml(road.name)}</title></path>`).join('')}</g>`;
55
+ return `<g data-layer="key-roads" clip-path="url(#${idPrefix}-coast)">${items
56
+ .map((road) => {
57
+ const level = road.level === 'secondary' ? 'secondary' : 'primary';
58
+ return `<path data-key-road="${escapeXml(road.id)}" data-key-road-level="${level}" d="${path(road.geometry)}" ${stroke(colors.road, 1)}><title>${escapeXml(road.name)}</title></path>`;
59
+ })
60
+ .join('')}</g>`;
56
61
  }
57
62
  export function keyRoadLabels(items, { project }) {
58
63
  return `<g data-layer="key-road-labels" ${overlayLabel} fill="#77736b">${items
@@ -34,6 +34,7 @@ export interface LandmarkData {
34
34
  export interface KeyRoadData {
35
35
  id: string;
36
36
  name: string;
37
+ level?: 'primary' | 'secondary';
37
38
  sourceNames: readonly string[];
38
39
  label: Position;
39
40
  segmentIds: readonly string[];
@@ -46,6 +46,7 @@ let sequence = 0;
46
46
  /** Make an offline SVG and the matching longitude/latitude projection. */
47
47
  export function createSFMapWithData(options, data) {
48
48
  const { theme = 'districts', width = 800, height = 800, padding = 28, year = 2022, districtLines = true, neighborhoodLines = false, districtFills = true, districtLabels = true, labels = true, highways = false, keyRoads = false, landmarks = false, bartStations = false, markers = [], title = 'San Francisco map', idPrefix = `sf-map-${++sequence}`, } = options;
49
+ const roadLabels = options.roadLabels ?? keyRoads;
49
50
  if (![width, height, padding].every(Number.isFinite) ||
50
51
  width <= 0 ||
51
52
  height <= 0 ||
@@ -87,7 +88,7 @@ export function createSFMapWithData(options, data) {
87
88
  }))
88
89
  : [];
89
90
  const parts = [
90
- `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img" aria-labelledby="${idPrefix}-title" data-sf-map="" data-year="${year}" style="max-width:100%;height:auto"><title id="${idPrefix}-title">${escapeXml(title)}</title><desc>San Francisco supervisorial district boundaries (${year}).${neighborhoodLines ? ' Dashed lines show SFAR realtor neighborhood areas, defined in August 2010.' : ''}${landmarks ? ' Highlighted areas show six parks and landmarks.' : ''}${keyRoads ? ' Thin gray lines show nine selected road corridors.' : ''}${bartStations ? ' Rings mark the eight San Francisco BART stations.' : ''} Geometry from DataSF${bartStations ? ' and BART' : ''}. See package SOURCES.md.</desc><defs><clipPath id="${idPrefix}-coast"><path d="${coastPath}" fill-rule="evenodd" clip-rule="evenodd"/></clipPath></defs><rect width="${width}" height="${height}" fill="${escapeXml(colors.water)}"/><g data-layer="geography"><path data-layer="coast" d="${coastPath}" fill="${escapeXml(colors.land)}" fill-rule="evenodd"/>`,
91
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img" aria-labelledby="${idPrefix}-title" data-sf-map="" data-year="${year}" style="max-width:100%;height:auto"><title id="${idPrefix}-title">${escapeXml(title)}</title><desc>San Francisco supervisorial district boundaries (${year}).${neighborhoodLines ? ' Dashed lines show SFAR realtor neighborhood areas, defined in August 2010.' : ''}${landmarks ? ' Highlighted areas show six parks and landmarks.' : ''}${keyRoads ? ' Thin gray lines show selected road corridors.' : ''}${bartStations ? ' Rings mark the eight San Francisco BART stations.' : ''} Geometry from DataSF${bartStations ? ' and BART' : ''}. See package SOURCES.md.</desc><defs><clipPath id="${idPrefix}-coast"><path d="${coastPath}" fill-rule="evenodd" clip-rule="evenodd"/></clipPath></defs><rect width="${width}" height="${height}" fill="${escapeXml(colors.water)}"/><g data-layer="geography"><path data-layer="coast" d="${coastPath}" fill="${escapeXml(colors.land)}" fill-rule="evenodd"/>`,
91
92
  ];
92
93
  // Explicit drawing order keeps optional overlays and user markers predictable.
93
94
  if (districtFills)
@@ -107,13 +108,19 @@ export function createSFMapWithData(options, data) {
107
108
  parts.push(layers.districtLabels(districts, context));
108
109
  if (labels && landmarks)
109
110
  parts.push(layers.landmarkLabels(landmarkData, context));
110
- if (labels && keyRoads)
111
+ if (labels && roadLabels)
111
112
  parts.push(layers.keyRoadLabels(roadData, context));
112
113
  if (bartStations)
113
114
  parts.push(layers.bartStations(stationData, context));
114
115
  for (const overlay of options.overlays ?? []) {
115
116
  if (!overlay.id || !/^[A-Za-z0-9_-]+$/.test(overlay.id))
116
117
  throw new TypeError('Overlay IDs must contain only letters, numbers, underscores, or hyphens.');
118
+ if (overlay.strokeWidth !== undefined &&
119
+ (!Number.isFinite(overlay.strokeWidth) || overlay.strokeWidth < 0))
120
+ throw new RangeError('Overlay strokeWidth must be a finite nonnegative number.');
121
+ if (overlay.fillOpacity !== undefined &&
122
+ (!Number.isFinite(overlay.fillOpacity) || overlay.fillOpacity < 0 || overlay.fillOpacity > 1))
123
+ throw new RangeError('Overlay fillOpacity must be a finite number from 0 to 1.');
117
124
  parts.push(`<path data-overlay-id="${escapeXml(overlay.id)}"${overlay.label ? ` aria-label="${escapeXml(overlay.label)}"` : ''} d="${path(overlay.geometry)}" fill="${escapeXml(overlay.fill ?? 'none')}" fill-opacity="${overlay.fillOpacity ?? 1}" stroke="${escapeXml(overlay.stroke ?? colors.road)}" stroke-width="${overlay.strokeWidth ?? 2}" vector-effect="non-scaling-stroke"${overlay.visible === false ? ' display="none"' : ''}/>`);
118
125
  }
119
126
  parts.push(layers.markers(markers, context), '</g></svg>');
@@ -16,6 +16,7 @@ export type Landmark = (typeof landmarks)[number];
16
16
  export type BartStation = (typeof bartStations)[number];
17
17
  export declare const keyRoads: {
18
18
  name: string;
19
+ level?: 'primary' | 'secondary';
19
20
  sourceNames: readonly string[];
20
21
  label: Position;
21
22
  segmentIds: readonly string[];
@@ -34,6 +34,7 @@ export interface SFMapOptions {
34
34
  labels?: boolean;
35
35
  highways?: boolean;
36
36
  keyRoads?: boolean;
37
+ roadLabels?: boolean;
37
38
  landmarks?: boolean;
38
39
  bartStations?: boolean;
39
40
  markers?: MapMarker[];
@@ -61,6 +62,7 @@ export interface InteractiveLayers {
61
62
  bartStations?: boolean;
62
63
  highways?: boolean;
63
64
  keyRoads?: boolean;
65
+ roadLabels?: boolean;
64
66
  }
65
67
  export interface NeighborhoodSelection {
66
68
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kahwee/sf-map-svg",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Offline SVG maps of San Francisco with district boundaries, parks, landmarks, and BART stations.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -31,6 +31,14 @@
31
31
  "types": "./dist/src/interactive.d.ts",
32
32
  "import": "./dist/src/interactive.js"
33
33
  },
34
+ "./interactive-data": {
35
+ "types": "./dist/src/interactive-data.d.ts",
36
+ "import": "./dist/src/interactive-data.js"
37
+ },
38
+ "./guide": {
39
+ "types": "./dist/src/guide.d.ts",
40
+ "import": "./dist/src/guide.js"
41
+ },
34
42
  "./transit": {
35
43
  "types": "./dist/src/transit.d.ts",
36
44
  "import": "./dist/src/transit.js"
@@ -88,6 +96,8 @@
88
96
  "data:check": "node scripts/build-data-catalog.mjs --check",
89
97
  "typecheck": "pnpm build && tsc -p tsconfig.json",
90
98
  "data:normalize-realtor": "node scripts/normalize-realtor.mjs",
99
+ "data:guide": "node scripts/build-guide-data.mjs",
100
+ "report:guide": "node scripts/report-guide-bundle.mjs",
91
101
  "test:package": "node scripts/smoke-package.mjs",
92
102
  "build": "node scripts/build.mjs",
93
103
  "lint": "biome check .",