@kahwee/sf-map-svg 1.0.0 → 1.2.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/CHANGELOG.md +37 -6
- package/README.md +134 -123
- package/SOURCES.md +13 -0
- package/dist/data/README.md +5 -1
- package/dist/src/custom-map.d.ts +3 -0
- package/dist/src/custom-map.js +1 -0
- package/dist/src/explorer.d.ts +1 -1
- package/dist/src/explorer.js +107 -30
- package/dist/src/index.d.ts +5 -6
- package/dist/src/index.js +12 -116
- package/dist/src/interactive.d.ts +1 -1
- package/dist/src/map-core.d.ts +54 -0
- package/dist/src/map-core.js +125 -0
- package/dist/src/transit.d.ts +3 -0
- package/dist/src/transit.js +105 -0
- package/dist/src/types.d.ts +24 -0
- package/package.json +12 -3
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import stations from '../data/bart-stations.json' with { type: 'json' };
|
|
2
|
+
import { createSFMap } from './index.js';
|
|
3
|
+
/** A deliberately schematic, offline station-to-station animation. Starts paused. */
|
|
4
|
+
export function createTransitAnimation() {
|
|
5
|
+
const host = Object.assign(document.createElement('div'), {
|
|
6
|
+
destroy: () => { },
|
|
7
|
+
});
|
|
8
|
+
const root = host.attachShadow({ mode: 'open' });
|
|
9
|
+
const map = createSFMap({ districtLabels: false, landmarks: true, labels: false });
|
|
10
|
+
root.innerHTML = `<style>
|
|
11
|
+
:host{display:block;font:14px system-ui;color:#244747;max-width:800px}
|
|
12
|
+
svg{display:block;width:100%;height:auto} .controls{display:flex;gap:12px;align-items:center;padding:12px;flex-wrap:wrap}
|
|
13
|
+
button{font:inherit;padding:10px 18px;border:1px solid #244747;border-radius:24px;background:#244747;color:white;cursor:pointer}
|
|
14
|
+
label{display:flex;align-items:center;gap:8px;flex:1} input{width:100%;min-width:100px;accent-color:#24789a}
|
|
15
|
+
p{margin:8px 12px;line-height:1.5} output{display:block;min-height:3em;margin:8px 12px}
|
|
16
|
+
</style>${map.svg}<div class="controls"><button type="button">Play</button><label>Journey <input aria-label="Journey progress" type="range" min="0" max="1000" value="0"></label></div><output aria-live="off"></output><p>Schematic BART journey. Straight connections between official station locations; not track geometry, a timetable, or live trains. One loop takes 28 seconds.</p>`;
|
|
17
|
+
const svg = root.querySelector('svg');
|
|
18
|
+
const ns = 'http://www.w3.org/2000/svg';
|
|
19
|
+
const points = stations.features.map((station) => map.project(station.geometry.coordinates));
|
|
20
|
+
const line = document.createElementNS(ns, 'polyline');
|
|
21
|
+
line.setAttribute('points', points.map((point) => point.join(',')).join(' '));
|
|
22
|
+
line.setAttribute('fill', 'none');
|
|
23
|
+
line.setAttribute('stroke', '#24789a');
|
|
24
|
+
line.setAttribute('stroke-width', '3');
|
|
25
|
+
line.setAttribute('stroke-dasharray', '5 5');
|
|
26
|
+
svg.append(line);
|
|
27
|
+
for (const [x, y] of points) {
|
|
28
|
+
const dot = document.createElementNS(ns, 'circle');
|
|
29
|
+
dot.setAttribute('cx', String(x));
|
|
30
|
+
dot.setAttribute('cy', String(y));
|
|
31
|
+
dot.setAttribute('r', '4');
|
|
32
|
+
dot.setAttribute('fill', 'white');
|
|
33
|
+
dot.setAttribute('stroke', '#24789a');
|
|
34
|
+
dot.setAttribute('stroke-width', '2');
|
|
35
|
+
svg.append(dot);
|
|
36
|
+
}
|
|
37
|
+
const train = document.createElementNS(ns, 'circle');
|
|
38
|
+
train.setAttribute('r', '7');
|
|
39
|
+
train.setAttribute('fill', '#a85036');
|
|
40
|
+
train.setAttribute('stroke', 'white');
|
|
41
|
+
train.setAttribute('stroke-width', '2');
|
|
42
|
+
svg.append(train);
|
|
43
|
+
const button = root.querySelector('button');
|
|
44
|
+
const slider = root.querySelector('input');
|
|
45
|
+
const output = root.querySelector('output');
|
|
46
|
+
let progress = 0;
|
|
47
|
+
let frame = 0;
|
|
48
|
+
let previous = 0;
|
|
49
|
+
let playing = false;
|
|
50
|
+
let destroyed = false;
|
|
51
|
+
function draw() {
|
|
52
|
+
const position = progress * (points.length - 1);
|
|
53
|
+
const index = Math.min(Math.floor(position), points.length - 2);
|
|
54
|
+
const fraction = position - index;
|
|
55
|
+
const from = points[index];
|
|
56
|
+
const to = points[index + 1];
|
|
57
|
+
train.setAttribute('cx', String(from[0] + (to[0] - from[0]) * fraction));
|
|
58
|
+
train.setAttribute('cy', String(from[1] + (to[1] - from[1]) * fraction));
|
|
59
|
+
slider.value = String(Math.round(progress * 1000));
|
|
60
|
+
output.textContent = `${stations.features[index].properties.name} → ${stations.features[index + 1].properties.name}`;
|
|
61
|
+
}
|
|
62
|
+
function tick(now) {
|
|
63
|
+
if (!playing)
|
|
64
|
+
return;
|
|
65
|
+
if (previous)
|
|
66
|
+
progress = (progress + Math.min(now - previous, 100) / 28000) % 1;
|
|
67
|
+
previous = now;
|
|
68
|
+
draw();
|
|
69
|
+
frame = requestAnimationFrame(tick);
|
|
70
|
+
}
|
|
71
|
+
function pause() {
|
|
72
|
+
playing = false;
|
|
73
|
+
cancelAnimationFrame(frame);
|
|
74
|
+
previous = 0;
|
|
75
|
+
button.textContent = 'Play';
|
|
76
|
+
}
|
|
77
|
+
button.addEventListener('click', () => {
|
|
78
|
+
if (destroyed)
|
|
79
|
+
return;
|
|
80
|
+
if (playing)
|
|
81
|
+
pause();
|
|
82
|
+
else {
|
|
83
|
+
playing = true;
|
|
84
|
+
button.textContent = 'Pause';
|
|
85
|
+
frame = requestAnimationFrame(tick);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
slider.addEventListener('input', () => {
|
|
89
|
+
pause();
|
|
90
|
+
progress = Number(slider.value) / 1000;
|
|
91
|
+
draw();
|
|
92
|
+
});
|
|
93
|
+
const visibility = () => {
|
|
94
|
+
if (document.hidden)
|
|
95
|
+
pause();
|
|
96
|
+
};
|
|
97
|
+
document.addEventListener('visibilitychange', visibility);
|
|
98
|
+
host.destroy = () => {
|
|
99
|
+
destroyed = true;
|
|
100
|
+
pause();
|
|
101
|
+
document.removeEventListener('visibilitychange', visibility);
|
|
102
|
+
};
|
|
103
|
+
draw();
|
|
104
|
+
return host;
|
|
105
|
+
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -8,6 +8,18 @@ export interface MapMarker {
|
|
|
8
8
|
selected?: boolean;
|
|
9
9
|
color?: string;
|
|
10
10
|
}
|
|
11
|
+
export interface MapOverlay {
|
|
12
|
+
id: string;
|
|
13
|
+
geometry: Extract<Geometry, {
|
|
14
|
+
type: 'LineString' | 'MultiLineString' | 'Polygon' | 'MultiPolygon';
|
|
15
|
+
}>;
|
|
16
|
+
stroke?: string;
|
|
17
|
+
strokeWidth?: number;
|
|
18
|
+
fill?: string;
|
|
19
|
+
fillOpacity?: number;
|
|
20
|
+
visible?: boolean;
|
|
21
|
+
label?: string;
|
|
22
|
+
}
|
|
11
23
|
export interface SFMapOptions {
|
|
12
24
|
theme?: 'districts' | 'transit';
|
|
13
25
|
width?: number;
|
|
@@ -25,6 +37,7 @@ export interface SFMapOptions {
|
|
|
25
37
|
landmarks?: boolean;
|
|
26
38
|
bartStations?: boolean;
|
|
27
39
|
markers?: MapMarker[];
|
|
40
|
+
overlays?: readonly MapOverlay[];
|
|
28
41
|
title?: string;
|
|
29
42
|
idPrefix?: string;
|
|
30
43
|
colors?: Partial<Record<'water' | 'land' | 'district' | 'neighborhood' | 'highway' | 'road' | 'park' | 'landmark' | 'bart' | 'label' | 'marker' | 'selected', string>>;
|
|
@@ -81,6 +94,12 @@ export interface NeighborhoodExplorerOptions {
|
|
|
81
94
|
markerColor?: string;
|
|
82
95
|
selectedMarkerColor?: string;
|
|
83
96
|
onMarkerActivate?: (marker: MapMarker) => void;
|
|
97
|
+
overlays?: readonly MapOverlay[];
|
|
98
|
+
onOverlayActivate?: (overlay: MapOverlay) => void;
|
|
99
|
+
/** Stable theme tokens consumed by the explorer chrome. */
|
|
100
|
+
style?: Partial<Record<'ink' | 'surface' | 'accent' | 'border' | 'focus' | 'controlGap' | 'font', string>>;
|
|
101
|
+
strings?: Partial<Record<'title' | 'mode' | 'source' | 'search' | 'chooseNeighborhood' | 'chooseMarker' | 'touchNavigation' | 'reset' | 'emptyResults' | 'gestureHelp', string>>;
|
|
102
|
+
controls?: Partial<Record<'zoom' | 'pan' | 'reset' | 'labels' | 'touch' | 'legend', boolean>>;
|
|
84
103
|
}
|
|
85
104
|
export interface NeighborhoodExplorerElement extends HTMLElement {
|
|
86
105
|
selectNeighborhood(name: string | null, options?: {
|
|
@@ -100,9 +119,14 @@ export interface NeighborhoodExplorerElement extends HTMLElement {
|
|
|
100
119
|
/** Explicitly engage map touch gestures; false restores page gestures. */
|
|
101
120
|
setTouchNavigation(enabled: boolean): void;
|
|
102
121
|
setMarkers(markers: readonly MapMarker[]): void;
|
|
122
|
+
setOverlays(overlays: readonly MapOverlay[]): void;
|
|
103
123
|
selectMarker(id: string | null, options?: {
|
|
104
124
|
fit?: boolean;
|
|
105
125
|
}): boolean;
|
|
106
126
|
getSelectedMarker(): MapMarker | null;
|
|
107
127
|
destroy(): void;
|
|
108
128
|
}
|
|
129
|
+
/** Paused-by-default schematic BART animation with keyboard-operable controls. */
|
|
130
|
+
export interface TransitAnimationElement extends HTMLElement {
|
|
131
|
+
destroy(): void;
|
|
132
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kahwee/sf-map-svg",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"types": "./dist/src/index.d.ts",
|
|
11
11
|
"import": "./dist/src/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./custom-map": {
|
|
14
|
+
"types": "./dist/src/custom-map.d.ts",
|
|
15
|
+
"import": "./dist/src/custom-map.js"
|
|
16
|
+
},
|
|
13
17
|
"./data": {
|
|
14
18
|
"types": "./dist/data/index.d.ts",
|
|
15
19
|
"import": "./dist/data/index.js"
|
|
@@ -26,6 +30,10 @@
|
|
|
26
30
|
"./interactive": {
|
|
27
31
|
"types": "./dist/src/interactive.d.ts",
|
|
28
32
|
"import": "./dist/src/interactive.js"
|
|
33
|
+
},
|
|
34
|
+
"./transit": {
|
|
35
|
+
"types": "./dist/src/transit.d.ts",
|
|
36
|
+
"import": "./dist/src/transit.js"
|
|
29
37
|
}
|
|
30
38
|
},
|
|
31
39
|
"files": [
|
|
@@ -53,7 +61,7 @@
|
|
|
53
61
|
"typescript": "^7.0.2",
|
|
54
62
|
"vite": "^8.3.1"
|
|
55
63
|
},
|
|
56
|
-
"homepage": "https://github.
|
|
64
|
+
"homepage": "https://kahwee.github.io/sf-map-svg/",
|
|
57
65
|
"bugs": {
|
|
58
66
|
"url": "https://github.com/kahwee/sf-map-svg/issues"
|
|
59
67
|
},
|
|
@@ -82,6 +90,7 @@
|
|
|
82
90
|
"data:normalize-realtor": "node scripts/normalize-realtor.mjs",
|
|
83
91
|
"test:package": "node scripts/smoke-package.mjs",
|
|
84
92
|
"build": "node scripts/build.mjs",
|
|
85
|
-
"lint": "biome check ."
|
|
93
|
+
"lint": "biome check .",
|
|
94
|
+
"build:pages": "pnpm build && node scripts/build-pages.mjs"
|
|
86
95
|
}
|
|
87
96
|
}
|