@kahwee/sf-map-svg 0.3.0 → 1.0.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +23 -4
  3. package/README.md +213 -2
  4. package/SOURCES.md +4 -0
  5. package/{data → dist/data}/README.md +1 -0
  6. package/dist/data/bart-stations.json +134 -0
  7. package/dist/data/catalog.json +4718 -0
  8. package/dist/data/coast.json +36 -0
  9. package/dist/data/districts-2002.json +325 -0
  10. package/dist/data/districts-2012.json +325 -0
  11. package/dist/data/districts-2022.json +325 -0
  12. package/dist/data/highways.json +4138 -0
  13. package/dist/data/index.d.ts +26 -0
  14. package/{data → dist/data}/index.js +37 -39
  15. package/dist/data/key-roads.json +12662 -0
  16. package/dist/data/landmarks.json +167 -0
  17. package/dist/data/neighborhoods-analysis.json +817 -0
  18. package/dist/data/neighborhoods-realtor.json +1978 -0
  19. package/dist/data/neighborhoods.json +2267 -0
  20. package/dist/data/types.d.ts +117 -0
  21. package/dist/data/types.js +1 -0
  22. package/dist/src/data.d.ts +25 -0
  23. package/{src → dist/src}/data.js +17 -20
  24. package/dist/src/explorer-layout.d.ts +23 -0
  25. package/dist/src/explorer-layout.js +72 -0
  26. package/dist/src/explorer.d.ts +4 -0
  27. package/dist/src/explorer.js +955 -0
  28. package/dist/src/geometry.d.ts +5 -0
  29. package/dist/src/geometry.js +53 -0
  30. package/dist/src/immutable.d.ts +2 -0
  31. package/dist/src/immutable.js +9 -0
  32. package/dist/src/index.d.ts +13 -0
  33. package/dist/src/index.js +125 -0
  34. package/dist/src/interactive.d.ts +4 -0
  35. package/dist/src/interactive.js +5 -0
  36. package/dist/src/layers.d.ts +36 -0
  37. package/dist/src/layers.js +64 -0
  38. package/dist/src/navigation.d.ts +6 -0
  39. package/dist/src/navigation.js +145 -0
  40. package/dist/src/overlays.d.ts +25 -0
  41. package/dist/src/overlays.js +15 -0
  42. package/dist/src/svg.d.ts +4 -0
  43. package/dist/src/svg.js +7 -0
  44. package/dist/src/types.d.ts +108 -0
  45. package/dist/src/types.js +1 -0
  46. package/dist/src/viewport.d.ts +5 -0
  47. package/dist/src/viewport.js +27 -0
  48. package/package.json +34 -24
  49. package/data/bart-stations.json +0 -134
  50. package/data/catalog.json +0 -4699
  51. package/data/coast.json +0 -36
  52. package/data/districts-2002.json +0 -325
  53. package/data/districts-2012.json +0 -325
  54. package/data/districts-2022.json +0 -325
  55. package/data/highways.json +0 -4138
  56. package/data/index.d.ts +0 -117
  57. package/data/landmarks.json +0 -167
  58. package/data/neighborhoods-analysis.json +0 -817
  59. package/data/neighborhoods-realtor.json +0 -1978
  60. package/data/neighborhoods.json +0 -2267
  61. package/src/geometry.d.ts +0 -8
  62. package/src/geometry.js +0 -51
  63. package/src/immutable.js +0 -8
  64. package/src/index.d.ts +0 -51
  65. package/src/index.js +0 -122
  66. package/src/layers.js +0 -54
  67. package/src/overlays.js +0 -18
  68. package/src/svg.js +0 -12
@@ -0,0 +1,117 @@
1
+ export type Position = readonly [number, number, ...number[]];
2
+ export type Bounds = readonly [number, number, number, number];
3
+ export type Geometry = {
4
+ readonly type: 'Point';
5
+ readonly coordinates: Position;
6
+ } | {
7
+ readonly type: 'LineString' | 'MultiPoint';
8
+ readonly coordinates: readonly Position[];
9
+ } | {
10
+ readonly type: 'Polygon';
11
+ readonly coordinates: readonly (readonly Position[])[];
12
+ } | {
13
+ readonly type: 'MultiLineString';
14
+ readonly coordinates: readonly (readonly Position[])[];
15
+ } | {
16
+ readonly type: 'MultiPolygon';
17
+ readonly coordinates: readonly (readonly (readonly Position[])[])[];
18
+ } | {
19
+ readonly type: 'GeometryCollection';
20
+ readonly geometries: readonly Geometry[];
21
+ };
22
+ export interface Feature<P = Readonly<Record<string, unknown>>> {
23
+ readonly type: 'Feature';
24
+ readonly id: string;
25
+ readonly bbox: Bounds;
26
+ readonly properties: P;
27
+ readonly geometry: Geometry;
28
+ }
29
+ export interface DataSource {
30
+ readonly id: string;
31
+ readonly title: string;
32
+ readonly url: string;
33
+ readonly retrievedAt: string;
34
+ readonly definitionYear?: number;
35
+ readonly licenseUrl: string;
36
+ }
37
+ export interface Definition {
38
+ readonly kind: string;
39
+ readonly year?: number;
40
+ readonly description: string;
41
+ }
42
+ export interface FeatureCollection<P = Readonly<Record<string, unknown>>> {
43
+ readonly type: 'FeatureCollection';
44
+ readonly schemaVersion: 1;
45
+ readonly id: string;
46
+ readonly title: string;
47
+ readonly coordinateSystem: string;
48
+ readonly definition: Definition;
49
+ readonly sources: readonly DataSource[];
50
+ readonly topology?: {
51
+ readonly policy: 'disjoint-interiors';
52
+ readonly sharedBoundariesAllowed: boolean;
53
+ readonly method: string;
54
+ readonly tool: string;
55
+ readonly processedAt: string;
56
+ readonly sourceOverlapPairs: number;
57
+ };
58
+ readonly features: readonly Feature<P>[];
59
+ }
60
+ export type NeighborhoodSource = 'sf-find' | 'analysis' | 'realtor';
61
+ export interface NeighborhoodProperties {
62
+ readonly name: string;
63
+ readonly canonicalName: string;
64
+ readonly sourceName: string;
65
+ readonly aliases: readonly string[];
66
+ readonly definitionSource: NeighborhoodSource;
67
+ readonly nameSources: readonly string[];
68
+ readonly note?: string;
69
+ readonly sourceCode?: string;
70
+ readonly realtorDistrict?: string;
71
+ }
72
+ export interface NeighborhoodEntry extends NeighborhoodProperties {
73
+ readonly id: string;
74
+ readonly source: NeighborhoodSource;
75
+ readonly file: string;
76
+ readonly bbox: Bounds;
77
+ }
78
+ export interface DistrictProperties {
79
+ readonly district: number;
80
+ readonly name: string;
81
+ readonly year: 2002 | 2012 | 2022;
82
+ readonly label: Position;
83
+ readonly labelPoints: readonly Position[];
84
+ readonly displayExtras: Geometry | null;
85
+ }
86
+ export interface Catalog {
87
+ readonly schemaVersion: 1;
88
+ readonly description: string;
89
+ readonly datasets: readonly {
90
+ readonly id: string;
91
+ readonly file: string;
92
+ readonly title: string;
93
+ readonly featureCount: number;
94
+ readonly definition: Definition;
95
+ readonly sources: readonly DataSource[];
96
+ }[];
97
+ readonly neighborhoods: readonly NeighborhoodEntry[];
98
+ }
99
+ export type NeighborhoodFeature = Feature<NeighborhoodProperties>;
100
+ export type PolygonGeometry = Extract<Geometry, {
101
+ readonly type: 'Polygon';
102
+ }>;
103
+ export type MultiPolygonGeometry = Extract<Geometry, {
104
+ readonly type: 'MultiPolygon';
105
+ }>;
106
+ export interface LandmarkProperties {
107
+ readonly name: string;
108
+ readonly label: Position;
109
+ readonly offset: readonly [number, number];
110
+ readonly anchor: 'middle' | 'start' | 'end';
111
+ }
112
+ export interface KeyRoadProperties {
113
+ readonly name: string;
114
+ readonly sourceNames: readonly string[];
115
+ readonly label: Position;
116
+ readonly segmentIds: readonly string[];
117
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import type { Geometry, Position } from '../data/types.js';
2
+ export interface DistrictRow {
3
+ id: number;
4
+ label: Position;
5
+ labelPoints: readonly Position[];
6
+ geometry: Geometry;
7
+ extras: Geometry | null;
8
+ }
9
+ declare const _default: {
10
+ coast: Geometry;
11
+ districts: {
12
+ 2002: DistrictRow[];
13
+ 2012: DistrictRow[];
14
+ 2022: DistrictRow[];
15
+ };
16
+ neighborhoods: {
17
+ name: string;
18
+ geometry: Geometry;
19
+ }[];
20
+ highways: {
21
+ route: string;
22
+ geometry: Geometry;
23
+ }[];
24
+ };
25
+ export default _default;
@@ -4,32 +4,29 @@ import coast from '../data/coast.json' with { type: 'json' };
4
4
  import districts2002 from '../data/districts-2002.json' with { type: 'json' };
5
5
  import districts2012 from '../data/districts-2012.json' with { type: 'json' };
6
6
  import districts2022 from '../data/districts-2022.json' with { type: 'json' };
7
- import neighborhoods from '../data/neighborhoods-realtor.json' with { type: 'json' };
8
7
  import highways from '../data/highways.json' with { type: 'json' };
8
+ import neighborhoods from '../data/neighborhoods-realtor.json' with { type: 'json' };
9
9
  import { deepFreeze } from './immutable.js';
10
-
11
- const districtRows = (collection) =>
12
- collection.features.map(({ geometry, properties }) => ({
10
+ const districtRows = (collection) => collection.features.map(({ geometry, properties }) => ({
13
11
  id: properties.district,
14
12
  label: properties.label,
15
13
  labelPoints: properties.labelPoints,
16
14
  geometry,
17
15
  extras: properties.displayExtras,
18
- }));
19
-
16
+ }));
20
17
  export default deepFreeze({
21
- coast: coast.features[0].geometry,
22
- districts: {
23
- 2002: districtRows(districts2002),
24
- 2012: districtRows(districts2012),
25
- 2022: districtRows(districts2022),
26
- },
27
- neighborhoods: neighborhoods.features.map(({ geometry, properties }) => ({
28
- name: properties.sourceName,
29
- geometry,
30
- })),
31
- highways: highways.features.map(({ geometry, properties }) => ({
32
- route: properties.route,
33
- geometry,
34
- })),
18
+ coast: coast.features[0].geometry,
19
+ districts: {
20
+ 2002: districtRows(districts2002),
21
+ 2012: districtRows(districts2012),
22
+ 2022: districtRows(districts2022),
23
+ },
24
+ neighborhoods: neighborhoods.features.map(({ geometry, properties }) => ({
25
+ name: properties.sourceName,
26
+ geometry,
27
+ })),
28
+ highways: highways.features.map(({ geometry, properties }) => ({
29
+ route: properties.route,
30
+ geometry,
31
+ })),
35
32
  });
@@ -0,0 +1,23 @@
1
+ import type { Bounds, Geometry, Position } from '../data/index.js';
2
+ type Project = (position: Position) => [number, number];
3
+ export type View = [number, number, number];
4
+ export interface LabelCandidate {
5
+ x: number;
6
+ y: number;
7
+ textWidth: number;
8
+ textHeight?: number;
9
+ offset?: number;
10
+ }
11
+ export type PlacedLabel<T> = T & {
12
+ left: number;
13
+ top: number;
14
+ box: Bounds;
15
+ };
16
+ export declare function projectedBounds(geometry: Geometry, project: Project): Bounds;
17
+ /** Find an interior label anchor using even-odd scanline intervals, including holes. */
18
+ export declare function interiorAnchor(geometry: Geometry, project: Project): [number, number] | undefined;
19
+ export declare function clampView([x, y, size]: View): View;
20
+ export declare function fitBounds([minX, minY, maxX, maxY]: Bounds, padding?: number): View;
21
+ /** Candidates arrive in priority order and carry measured screen-pixel widths. */
22
+ export declare function layoutLabels<T extends LabelCandidate>(candidates: readonly T[], width: number, height: number, obstacles?: readonly Bounds[]): PlacedLabel<T>[];
23
+ export {};
@@ -0,0 +1,72 @@
1
+ import { positions } from './geometry.js';
2
+ export function projectedBounds(geometry, project) {
3
+ return positions(geometry)
4
+ .map(project)
5
+ .reduce((b, [x, y]) => [Math.min(b[0], x), Math.min(b[1], y), Math.max(b[2], x), Math.max(b[3], y)], [Infinity, Infinity, -Infinity, -Infinity]);
6
+ }
7
+ /** Find an interior label anchor using even-odd scanline intervals, including holes. */
8
+ export function interiorAnchor(geometry, project) {
9
+ if (geometry.type !== 'Polygon' && geometry.type !== 'MultiPolygon')
10
+ return undefined;
11
+ const polygons = geometry.type === 'Polygon' ? [geometry.coordinates] : geometry.coordinates;
12
+ let best;
13
+ let bestScore = -Infinity;
14
+ for (const polygon of polygons) {
15
+ const rings = polygon.map((ring) => ring.map(project));
16
+ const ys = rings[0].map((p) => p[1]);
17
+ const lo = Math.min(...ys), hi = Math.max(...ys);
18
+ for (let step = 1; step < 32; step++) {
19
+ const y = lo + ((hi - lo) * step) / 32;
20
+ const intersections = [];
21
+ for (const ring of rings) {
22
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
23
+ const a = ring[i], b = ring[j];
24
+ if (a[1] > y !== b[1] > y)
25
+ intersections.push(a[0] + ((y - a[1]) * (b[0] - a[0])) / (b[1] - a[1]));
26
+ }
27
+ }
28
+ intersections.sort((a, b) => a - b);
29
+ for (let i = 0; i + 1 < intersections.length; i += 2) {
30
+ const width = intersections[i + 1] - intersections[i];
31
+ const score = width * (1 - Math.abs(step - 16) / 64);
32
+ if (width > 0 && score > bestScore) {
33
+ bestScore = score;
34
+ best = [(intersections[i] + intersections[i + 1]) / 2, y];
35
+ }
36
+ }
37
+ }
38
+ }
39
+ return best;
40
+ }
41
+ export function clampView([x, y, size]) {
42
+ size = Math.max(800 / 12, Math.min(800, size));
43
+ return [Math.max(0, Math.min(800 - size, x)), Math.max(0, Math.min(800 - size, y)), size];
44
+ }
45
+ export function fitBounds([minX, minY, maxX, maxY], padding = 0.18) {
46
+ const size = Math.max(800 / 12, Math.min(800, Math.max(maxX - minX, maxY - minY) * (1 + padding * 2)));
47
+ return clampView([(minX + maxX - size) / 2, (minY + maxY - size) / 2, size]);
48
+ }
49
+ /** Candidates arrive in priority order and carry measured screen-pixel widths. */
50
+ export function layoutLabels(candidates, width, height, obstacles = []) {
51
+ const placed = [];
52
+ for (const candidate of candidates) {
53
+ const { x, y, textWidth, textHeight = 15, offset = 0 } = candidate;
54
+ const alternatives = offset
55
+ ? [
56
+ [x + offset, y - textHeight / 2],
57
+ [x - offset - textWidth, y - textHeight / 2],
58
+ [x - textWidth / 2, y - offset - textHeight],
59
+ ]
60
+ : [[x - textWidth / 2, y - textHeight / 2]];
61
+ for (const [left, top] of alternatives) {
62
+ const box = [left - 3, top - 3, left + textWidth + 3, top + textHeight + 3];
63
+ if (box[0] < 2 || box[1] < 2 || box[2] > width - 2 || box[3] > height - 2)
64
+ continue;
65
+ if ([...obstacles, ...placed.map((item) => item.box)].some((b) => box[0] < b[2] && box[2] > b[0] && box[1] < b[3] && box[3] > b[1]))
66
+ continue;
67
+ placed.push({ ...candidate, left, top, box });
68
+ break;
69
+ }
70
+ }
71
+ return placed;
72
+ }
@@ -0,0 +1,4 @@
1
+ import type { NeighborhoodExplorerElement, NeighborhoodExplorerOptions } from './types.js';
2
+ export type { ExplorerMode, NeighborhoodExplorerElement, NeighborhoodExplorerOptions, } from './types.js';
3
+ /** Create an offline, browser-only neighborhood explorer. Call destroy() before disposal. */
4
+ export declare function createNeighborhoodExplorer({ source, mode, labels, neighborhood, year, theme, interface: chrome, layers, selectableNeighborhoods, labelSize, fitPadding, markers: initialMarkers, markerRadius, markerHitSize, markerColor, selectedMarkerColor, onMarkerActivate, }?: NeighborhoodExplorerOptions): NeighborhoodExplorerElement;