@atlas-composer/projection-loader 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.
package/README.md ADDED
@@ -0,0 +1,409 @@
1
+ # @atlas-composer/projection-loader
2
+
3
+ > Zero-dependency standalone loader for composite map projections with plugin architecture
4
+
5
+ A lightweight, framework-agnostic library for loading composite map projections exported from [Atlas composer](https://github.com/ShallowRed/atlas-composer). Features a plugin architecture that lets you register only the projections you need, achieving **94% smaller bundle sizes** compared to including all D3 projections.
6
+
7
+ ## Features
8
+
9
+ - 🎯 **Zero Dependencies** - Bring your own projections (D3, Proj4, or custom)
10
+ - 📦 **Tree-Shakeable** - Only bundle what you use (~6KB vs 100KB)
11
+ - 🔌 **Plugin Architecture** - Register projections on-demand
12
+ - 🌐 **Framework Agnostic** - Works with D3, Observable Plot, React, Vue, Svelte
13
+ - 📘 **Full TypeScript Support** - Complete type definitions included
14
+ - ⚡ **Fast** - Optimized stream multiplexing for efficient rendering
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @atlas-composer/projection-loader d3-geo d3-geo-projection
20
+ # or
21
+ pnpm add @atlas-composer/projection-loader d3-geo d3-geo-projection
22
+ # or
23
+ yarn add @atlas-composer/projection-loader d3-geo d3-geo-projection
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```typescript
29
+ import { loadCompositeProjection, registerProjection } from '@atlas-composer/projection-loader'
30
+ import * as d3 from 'd3-geo'
31
+
32
+ // 2. Load your exported configuration
33
+ import config from './france-composite.json'
34
+
35
+ // 1. Register projections (only what you need!)
36
+ registerProjection('mercator', () => d3.geoMercator())
37
+ registerProjection('conic-conformal', () => d3.geoConicConformal())
38
+
39
+ const projection = loadCompositeProjection(config, {
40
+ width: 800,
41
+ height: 600
42
+ })
43
+
44
+ // 3. Use with D3
45
+ const path = d3.geoPath(projection)
46
+
47
+ svg.selectAll('path')
48
+ .data(features)
49
+ .join('path')
50
+ .attr('d', path)
51
+ .attr('fill', 'lightgray')
52
+ .attr('stroke', 'white')
53
+ ```
54
+
55
+ ## Usage with Helpers (Convenience)
56
+
57
+ For quick prototyping, use the optional helpers module:
58
+
59
+ ```typescript
60
+ import { registerProjections } from '@atlas-composer/projection-loader'
61
+ import { d3ProjectionFactories } from '@atlas-composer/projection-loader/helpers'
62
+
63
+ // Register all standard D3 projections at once
64
+ registerProjections(d3ProjectionFactories)
65
+
66
+ // Now load your configuration
67
+ const projection = loadCompositeProjection(config, { width: 800, height: 600 })
68
+ ```
69
+
70
+ ## Tree-Shaking (Production)
71
+
72
+ For optimal bundle sizes, import only what you need:
73
+
74
+ ```typescript
75
+ import { loadCompositeProjection, registerProjections } from '@atlas-composer/projection-loader'
76
+ import { geoConicConformal, geoMercator } from 'd3-geo'
77
+
78
+ // Only these two projections will be in your bundle
79
+ registerProjections({
80
+ 'mercator': () => geoMercator(),
81
+ 'conic-conformal': () => geoConicConformal()
82
+ })
83
+
84
+ const projection = loadCompositeProjection(config, { width: 800, height: 600 })
85
+ ```
86
+
87
+ **Result**: ~6KB instead of ~100KB (94% reduction) 🎉
88
+
89
+ ## Observable Plot Integration
90
+
91
+ ```typescript
92
+ import { loadCompositeProjection, registerProjection } from '@atlas-composer/projection-loader'
93
+ import * as Plot from '@observablehq/plot'
94
+ import * as d3 from 'd3-geo'
95
+
96
+ // Register projections
97
+ registerProjection('mercator', () => d3.geoMercator())
98
+ registerProjection('conic-conformal', () => d3.geoConicConformal())
99
+
100
+ // Create projection factory for Plot
101
+ function createProjection({ width, height }) {
102
+ return loadCompositeProjection(config, { width, height })
103
+ }
104
+
105
+ // Use with Plot
106
+ Plot.plot({
107
+ width: 975,
108
+ height: 610,
109
+ projection: createProjection,
110
+ marks: [
111
+ Plot.geo(countries, { fill: 'lightgray', stroke: 'white' })
112
+ ]
113
+ })
114
+ ```
115
+
116
+ ## Framework Examples
117
+
118
+ ### React
119
+
120
+ ```tsx
121
+ import { loadCompositeProjection, registerProjections } from '@atlas-composer/projection-loader'
122
+ import * as d3 from 'd3-geo'
123
+ import { useEffect, useRef } from 'react'
124
+
125
+ function MapComponent({ config }) {
126
+ const svgRef = useRef<SVGSVGElement>(null)
127
+
128
+ useEffect(() => {
129
+ // Register projections once
130
+ registerProjections({
131
+ 'mercator': () => d3.geoMercator(),
132
+ 'conic-conformal': () => d3.geoConicConformal()
133
+ })
134
+
135
+ // Create projection
136
+ const projection = loadCompositeProjection(config, {
137
+ width: 800,
138
+ height: 600
139
+ })
140
+
141
+ // Render map
142
+ const path = d3.geoPath(projection)
143
+ const svg = d3.select(svgRef.current)
144
+
145
+ svg.selectAll('path')
146
+ .data(features)
147
+ .join('path')
148
+ .attr('d', path)
149
+ .attr('fill', 'lightgray')
150
+ }, [config])
151
+
152
+ return <svg ref={svgRef} width={800} height={600} />
153
+ }
154
+ ```
155
+
156
+ ### Vue 3
157
+
158
+ ```vue
159
+ <script setup lang="ts">
160
+ import { loadCompositeProjection, registerProjections } from '@atlas-composer/projection-loader'
161
+ import * as d3 from 'd3-geo'
162
+ import { onMounted, ref } from 'vue'
163
+
164
+ const props = defineProps<{ config: any }>()
165
+ const svgRef = ref<SVGSVGElement>()
166
+
167
+ onMounted(() => {
168
+ registerProjections({
169
+ 'mercator': () => d3.geoMercator(),
170
+ 'conic-conformal': () => d3.geoConicConformal()
171
+ })
172
+
173
+ const projection = loadCompositeProjection(props.config, {
174
+ width: 800,
175
+ height: 600
176
+ })
177
+
178
+ const path = d3.geoPath(projection)
179
+ const svg = d3.select(svgRef.value)
180
+
181
+ svg.selectAll('path')
182
+ .data(features)
183
+ .join('path')
184
+ .attr('d', path)
185
+ .attr('fill', 'lightgray')
186
+ })
187
+ </script>
188
+
189
+ <template>
190
+ <svg
191
+ ref="svgRef"
192
+ width="800"
193
+ height="600"
194
+ />
195
+ </template>
196
+ ```
197
+
198
+ ### Svelte
199
+
200
+ ```svelte
201
+ <script lang="ts">
202
+ import * as d3 from 'd3-geo'
203
+ import { onMount } from 'svelte'
204
+ import { loadCompositeProjection, registerProjections } from '@atlas-composer/projection-loader'
205
+
206
+ export let config: any
207
+
208
+ let svgElement: SVGSVGElement
209
+
210
+ onMount(() => {
211
+ registerProjections({
212
+ 'mercator': () => d3.geoMercator(),
213
+ 'conic-conformal': () => d3.geoConicConformal()
214
+ })
215
+
216
+ const projection = loadCompositeProjection(config, {
217
+ width: 800,
218
+ height: 600
219
+ })
220
+
221
+ const path = d3.geoPath(projection)
222
+ const svg = d3.select(svgElement)
223
+
224
+ svg.selectAll('path')
225
+ .data(features)
226
+ .join('path')
227
+ .attr('d', path)
228
+ .attr('fill', 'lightgray')
229
+ })
230
+ </script>
231
+
232
+ <svg bind:this={svgElement} width="800" height="600" />
233
+ ```
234
+
235
+ ## Custom Projections
236
+
237
+ You can register any projection factory, including custom implementations:
238
+
239
+ ```typescript
240
+ import { registerProjection } from '@atlas-composer/projection-loader'
241
+
242
+ // Custom projection
243
+ registerProjection('my-custom', () => {
244
+ // Return a D3-compatible projection
245
+ return {
246
+ // Implement projection interface
247
+ (coordinates) => [x, y],
248
+ scale: (s?) => s ? (scale = s, this) : scale,
249
+ translate: (t?) => t ? (translate = t, this) : translate,
250
+ // ... other D3 projection methods
251
+ }
252
+ })
253
+
254
+ // Proj4 wrapper
255
+ import proj4 from 'proj4'
256
+
257
+ registerProjection('lambert93', () => {
258
+ const projection = proj4('EPSG:2154')
259
+ return {
260
+ (coords) => projection.forward(coords),
261
+ scale: () => 1,
262
+ translate: () => [0, 0]
263
+ }
264
+ })
265
+ ```
266
+
267
+ ## API Reference
268
+
269
+ ### Core Functions
270
+
271
+ #### `registerProjection(id: string, factory: ProjectionFactory): void`
272
+
273
+ Register a projection factory with a given ID.
274
+
275
+ ```typescript
276
+ registerProjection('mercator', () => d3.geoMercator())
277
+ ```
278
+
279
+ #### `registerProjections(factories: Record<string, ProjectionFactory>): void`
280
+
281
+ Register multiple projections at once.
282
+
283
+ ```typescript
284
+ registerProjections({
285
+ mercator: () => d3.geoMercator(),
286
+ albers: () => d3.geoAlbers()
287
+ })
288
+ ```
289
+
290
+ #### `loadCompositeProjection(config: ExportedConfig, options: LoaderOptions): ProjectionLike`
291
+
292
+ Load a composite projection from an exported configuration.
293
+
294
+ ```typescript
295
+ const projection = loadCompositeProjection(config, {
296
+ width: 800,
297
+ height: 600,
298
+ enableClipping: true, // optional, default: true
299
+ debug: false // optional, default: false
300
+ })
301
+ ```
302
+
303
+ #### `loadFromJSON(jsonString: string, options: LoaderOptions): ProjectionLike`
304
+
305
+ Load a composite projection from a JSON string.
306
+
307
+ ```typescript
308
+ const jsonString = fs.readFileSync('config.json', 'utf-8')
309
+ const projection = loadFromJSON(jsonString, { width: 800, height: 600 })
310
+ ```
311
+
312
+ ### Utility Functions
313
+
314
+ #### `getRegisteredProjections(): string[]`
315
+
316
+ Get list of registered projection IDs.
317
+
318
+ #### `isProjectionRegistered(id: string): boolean`
319
+
320
+ Check if a projection is registered.
321
+
322
+ #### `unregisterProjection(id: string): boolean`
323
+
324
+ Remove a projection from the registry.
325
+
326
+ #### `clearProjections(): void`
327
+
328
+ Clear all registered projections.
329
+
330
+ #### `validateConfig(config: any): boolean`
331
+
332
+ Validate a configuration object. Throws descriptive errors if invalid.
333
+
334
+ ## Configuration Format
335
+
336
+ Exported configurations follow this structure:
337
+
338
+ ```typescript
339
+ interface ExportedConfig {
340
+ version: '1.0'
341
+ metadata: {
342
+ atlasId: string
343
+ atlasName: string
344
+ }
345
+ pattern: 'single-focus' | 'equal-members'
346
+ referenceScale: number
347
+ territories: Territory[]
348
+ }
349
+
350
+ interface Territory {
351
+ code: string
352
+ name: string
353
+ role: 'primary' | 'secondary' | 'member'
354
+ projectionId: string
355
+ projectionFamily: string
356
+ parameters: {
357
+ center?: [number, number]
358
+ rotate?: [number, number, number]
359
+ parallels?: [number, number]
360
+ scale: number
361
+ baseScale: number
362
+ scaleMultiplier: number
363
+ }
364
+ layout: {
365
+ translateOffset: [number, number]
366
+ clipExtent: [[number, number], [number, number]] | null
367
+ }
368
+ bounds: [[number, number], [number, number]]
369
+ }
370
+ ```
371
+
372
+ ## Bundle Size
373
+
374
+ | Approach | Bundle Size | Savings |
375
+ |----------|-------------|---------|
376
+ | All D3 projections | ~100KB | - |
377
+ | With projection-loader | ~6KB | **94%** 🎉 |
378
+
379
+ ## TypeScript Support
380
+
381
+ Full TypeScript definitions are included. No need for `@types/*` packages.
382
+
383
+ ```typescript
384
+ import type {
385
+ ExportedConfig,
386
+ LoaderOptions,
387
+ ProjectionFactory,
388
+ ProjectionLike,
389
+ Territory
390
+ } from '@atlas-composer/projection-loader'
391
+ ```
392
+
393
+ ## Browser Support
394
+
395
+ Works in all modern browsers that support ES2020+. For older browsers, transpile with your build tool.
396
+
397
+ ## Contributing
398
+
399
+ Contributions are welcome! This package is part of the [Atlas composer](https://github.com/ShallowRed/atlas-composer) monorepo.
400
+
401
+ ## License
402
+
403
+ MIT © 2025 Lucas Poulain
404
+
405
+ ## Related
406
+
407
+ - [Atlas composer](https://github.com/ShallowRed/atlas-composer) - Create custom composite projections
408
+ - [D3.js](https://d3js.org/) - Data visualization library
409
+ - [Observable Plot](https://observablehq.com/plot/) - High-level plotting library
@@ -0,0 +1,91 @@
1
+ import { ProjectionFactory } from './index.js';
2
+
3
+ /**
4
+ * D3 Projection Helpers
5
+ *
6
+ * Optional companion file that provides ready-to-use D3 projection factory mappings.
7
+ * This file has dependencies on d3-geo and d3-geo-projection, but the main loader does not.
8
+ *
9
+ * Users can import this to quickly register all standard D3 projections, or they can
10
+ * selectively import only the projections they need for tree-shaking.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * // Register all projections at once
15
+ * import { registerProjections } from './standalone-projection-loader'
16
+ * import { d3ProjectionFactories } from './d3-projection-helpers'
17
+ *
18
+ * registerProjections(d3ProjectionFactories)
19
+ * ```
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * // Tree-shakeable: import only what you need
24
+ * import { registerProjection } from './standalone-projection-loader'
25
+ * import { mercator, albers } from './d3-projection-helpers'
26
+ *
27
+ * registerProjection('mercator', mercator)
28
+ * registerProjection('albers', albers)
29
+ * ```
30
+ *
31
+ * @packageDocumentation
32
+ */
33
+
34
+ declare const azimuthalEqualArea: ProjectionFactory;
35
+ declare const azimuthalEquidistant: ProjectionFactory;
36
+ declare const gnomonic: ProjectionFactory;
37
+ declare const orthographic: ProjectionFactory;
38
+ declare const stereographic: ProjectionFactory;
39
+ declare const conicConformal: ProjectionFactory;
40
+ declare const conicEqualArea: ProjectionFactory;
41
+ declare const conicEquidistant: ProjectionFactory;
42
+ declare const albers: ProjectionFactory;
43
+ declare const mercator: ProjectionFactory;
44
+ declare const transverseMercator: ProjectionFactory;
45
+ declare const equirectangular: ProjectionFactory;
46
+ declare const naturalEarth1: ProjectionFactory;
47
+ declare const equalEarth: ProjectionFactory;
48
+ /**
49
+ * Object containing all standard D3 projection factories
50
+ * Keyed by the projection ID used in Atlas Composer configurations
51
+ */
52
+ declare const d3ProjectionFactories: Record<string, ProjectionFactory>;
53
+ /**
54
+ * Convenience function to register all D3 projections at once
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * import { registerProjections } from './standalone-projection-loader'
59
+ * import { registerAllD3Projections } from './d3-projection-helpers'
60
+ *
61
+ * registerAllD3Projections(registerProjections)
62
+ * ```
63
+ *
64
+ * @param registerFn - The registerProjections function from the loader
65
+ */
66
+ declare function registerAllD3Projections(registerFn: (factories: Record<string, ProjectionFactory>) => void): void;
67
+ /**
68
+ * Get list of available D3 projection IDs
69
+ */
70
+ declare function getAvailableD3Projections(): string[];
71
+ declare const _default: {
72
+ d3ProjectionFactories: Record<string, ProjectionFactory>;
73
+ registerAllD3Projections: typeof registerAllD3Projections;
74
+ getAvailableD3Projections: typeof getAvailableD3Projections;
75
+ azimuthalEqualArea: ProjectionFactory;
76
+ azimuthalEquidistant: ProjectionFactory;
77
+ gnomonic: ProjectionFactory;
78
+ orthographic: ProjectionFactory;
79
+ stereographic: ProjectionFactory;
80
+ conicConformal: ProjectionFactory;
81
+ conicEqualArea: ProjectionFactory;
82
+ conicEquidistant: ProjectionFactory;
83
+ albers: ProjectionFactory;
84
+ mercator: ProjectionFactory;
85
+ transverseMercator: ProjectionFactory;
86
+ equirectangular: ProjectionFactory;
87
+ naturalEarth1: ProjectionFactory;
88
+ equalEarth: ProjectionFactory;
89
+ };
90
+
91
+ export { albers, azimuthalEqualArea, azimuthalEquidistant, conicConformal, conicEqualArea, conicEquidistant, d3ProjectionFactories, _default as default, equalEarth, equirectangular, getAvailableD3Projections, gnomonic, mercator, naturalEarth1, orthographic, registerAllD3Projections, stereographic, transverseMercator };
@@ -0,0 +1,68 @@
1
+ import * as d3Geo from 'd3-geo';
2
+ import * as d3GeoProjection from 'd3-geo-projection';
3
+
4
+ // src/d3-projection-helpers.ts
5
+ var azimuthalEqualArea = () => d3Geo.geoAzimuthalEqualArea();
6
+ var azimuthalEquidistant = () => d3Geo.geoAzimuthalEquidistant();
7
+ var gnomonic = () => d3Geo.geoGnomonic();
8
+ var orthographic = () => d3Geo.geoOrthographic();
9
+ var stereographic = () => d3Geo.geoStereographic();
10
+ var conicConformal = () => d3Geo.geoConicConformal();
11
+ var conicEqualArea = () => d3Geo.geoConicEqualArea();
12
+ var conicEquidistant = () => d3Geo.geoConicEquidistant();
13
+ var albers = () => d3Geo.geoAlbers();
14
+ var mercator = () => d3Geo.geoMercator();
15
+ var transverseMercator = () => d3Geo.geoTransverseMercator();
16
+ var equirectangular = () => d3Geo.geoEquirectangular();
17
+ var naturalEarth1 = () => d3GeoProjection.geoNaturalEarth1();
18
+ var equalEarth = () => d3Geo.geoEqualEarth();
19
+ var d3ProjectionFactories = {
20
+ // Azimuthal
21
+ "azimuthal-equal-area": azimuthalEqualArea,
22
+ "azimuthal-equidistant": azimuthalEquidistant,
23
+ "gnomonic": gnomonic,
24
+ "orthographic": orthographic,
25
+ "stereographic": stereographic,
26
+ // Conic
27
+ "conic-conformal": conicConformal,
28
+ "conic-equal-area": conicEqualArea,
29
+ "conic-equidistant": conicEquidistant,
30
+ "albers": albers,
31
+ // Cylindrical
32
+ "mercator": mercator,
33
+ "transverse-mercator": transverseMercator,
34
+ "equirectangular": equirectangular,
35
+ "natural-earth-1": naturalEarth1,
36
+ // Other
37
+ "equal-earth": equalEarth
38
+ };
39
+ function registerAllD3Projections(registerFn) {
40
+ registerFn(d3ProjectionFactories);
41
+ }
42
+ function getAvailableD3Projections() {
43
+ return Object.keys(d3ProjectionFactories);
44
+ }
45
+ var d3_projection_helpers_default = {
46
+ d3ProjectionFactories,
47
+ registerAllD3Projections,
48
+ getAvailableD3Projections,
49
+ // Individual projections for tree-shaking
50
+ azimuthalEqualArea,
51
+ azimuthalEquidistant,
52
+ gnomonic,
53
+ orthographic,
54
+ stereographic,
55
+ conicConformal,
56
+ conicEqualArea,
57
+ conicEquidistant,
58
+ albers,
59
+ mercator,
60
+ transverseMercator,
61
+ equirectangular,
62
+ naturalEarth1,
63
+ equalEarth
64
+ };
65
+
66
+ export { albers, azimuthalEqualArea, azimuthalEquidistant, conicConformal, conicEqualArea, conicEquidistant, d3ProjectionFactories, d3_projection_helpers_default as default, equalEarth, equirectangular, getAvailableD3Projections, gnomonic, mercator, naturalEarth1, orthographic, registerAllD3Projections, stereographic, transverseMercator };
67
+ //# sourceMappingURL=d3-projection-helpers.js.map
68
+ //# sourceMappingURL=d3-projection-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/d3-projection-helpers.ts"],"names":[],"mappings":";;;;AAoCO,IAAM,kBAAA,GAAwC,MAAY,KAAA,CAAA,qBAAA;AAC1D,IAAM,oBAAA,GAA0C,MAAY,KAAA,CAAA,uBAAA;AAC5D,IAAM,QAAA,GAA8B,MAAY,KAAA,CAAA,WAAA;AAChD,IAAM,YAAA,GAAkC,MAAY,KAAA,CAAA,eAAA;AACpD,IAAM,aAAA,GAAmC,MAAY,KAAA,CAAA,gBAAA;AAGrD,IAAM,cAAA,GAAoC,MAAY,KAAA,CAAA,iBAAA;AACtD,IAAM,cAAA,GAAoC,MAAY,KAAA,CAAA,iBAAA;AACtD,IAAM,gBAAA,GAAsC,MAAY,KAAA,CAAA,mBAAA;AACxD,IAAM,MAAA,GAA4B,MAAY,KAAA,CAAA,SAAA;AAG9C,IAAM,QAAA,GAA8B,MAAY,KAAA,CAAA,WAAA;AAChD,IAAM,kBAAA,GAAwC,MAAY,KAAA,CAAA,qBAAA;AAC1D,IAAM,eAAA,GAAqC,MAAY,KAAA,CAAA,kBAAA;AACvD,IAAM,aAAA,GAAmC,MAA+B,eAAA,CAAA,gBAAA;AAGxE,IAAM,UAAA,GAAgC,MAAY,KAAA,CAAA,aAAA;AAMlD,IAAM,qBAAA,GAA2D;AAAA;AAAA,EAEtE,sBAAA,EAAwB,kBAAA;AAAA,EACxB,uBAAA,EAAyB,oBAAA;AAAA,EACzB,UAAA,EAAY,QAAA;AAAA,EACZ,cAAA,EAAgB,YAAA;AAAA,EAChB,eAAA,EAAiB,aAAA;AAAA;AAAA,EAGjB,iBAAA,EAAmB,cAAA;AAAA,EACnB,kBAAA,EAAoB,cAAA;AAAA,EACpB,mBAAA,EAAqB,gBAAA;AAAA,EACrB,QAAA,EAAU,MAAA;AAAA;AAAA,EAGV,UAAA,EAAY,QAAA;AAAA,EACZ,qBAAA,EAAuB,kBAAA;AAAA,EACvB,iBAAA,EAAmB,eAAA;AAAA,EACnB,iBAAA,EAAmB,aAAA;AAAA;AAAA,EAGnB,aAAA,EAAe;AACjB;AAeO,SAAS,yBACd,UAAA,EACM;AACN,EAAA,UAAA,CAAW,qBAAqB,CAAA;AAClC;AAKO,SAAS,yBAAA,GAAsC;AACpD,EAAA,OAAO,MAAA,CAAO,KAAK,qBAAqB,CAAA;AAC1C;AAGA,IAAO,6BAAA,GAAQ;AAAA,EACb,qBAAA;AAAA,EACA,wBAAA;AAAA,EACA,yBAAA;AAAA;AAAA,EAGA,kBAAA;AAAA,EACA,oBAAA;AAAA,EACA,QAAA;AAAA,EACA,YAAA;AAAA,EACA,aAAA;AAAA,EACA,cAAA;AAAA,EACA,cAAA;AAAA,EACA,gBAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,kBAAA;AAAA,EACA,eAAA;AAAA,EACA,aAAA;AAAA,EACA;AACF","file":"d3-projection-helpers.js","sourcesContent":["/**\n * D3 Projection Helpers\n *\n * Optional companion file that provides ready-to-use D3 projection factory mappings.\n * This file has dependencies on d3-geo and d3-geo-projection, but the main loader does not.\n *\n * Users can import this to quickly register all standard D3 projections, or they can\n * selectively import only the projections they need for tree-shaking.\n *\n * @example\n * ```typescript\n * // Register all projections at once\n * import { registerProjections } from './standalone-projection-loader'\n * import { d3ProjectionFactories } from './d3-projection-helpers'\n *\n * registerProjections(d3ProjectionFactories)\n * ```\n *\n * @example\n * ```typescript\n * // Tree-shakeable: import only what you need\n * import { registerProjection } from './standalone-projection-loader'\n * import { mercator, albers } from './d3-projection-helpers'\n *\n * registerProjection('mercator', mercator)\n * registerProjection('albers', albers)\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { ProjectionFactory } from './standalone-projection-loader'\nimport * as d3Geo from 'd3-geo'\nimport * as d3GeoProjection from 'd3-geo-projection'\n\n// Azimuthal projections\nexport const azimuthalEqualArea: ProjectionFactory = () => d3Geo.geoAzimuthalEqualArea()\nexport const azimuthalEquidistant: ProjectionFactory = () => d3Geo.geoAzimuthalEquidistant()\nexport const gnomonic: ProjectionFactory = () => d3Geo.geoGnomonic()\nexport const orthographic: ProjectionFactory = () => d3Geo.geoOrthographic()\nexport const stereographic: ProjectionFactory = () => d3Geo.geoStereographic()\n\n// Conic projections\nexport const conicConformal: ProjectionFactory = () => d3Geo.geoConicConformal()\nexport const conicEqualArea: ProjectionFactory = () => d3Geo.geoConicEqualArea()\nexport const conicEquidistant: ProjectionFactory = () => d3Geo.geoConicEquidistant()\nexport const albers: ProjectionFactory = () => d3Geo.geoAlbers()\n\n// Cylindrical projections\nexport const mercator: ProjectionFactory = () => d3Geo.geoMercator()\nexport const transverseMercator: ProjectionFactory = () => d3Geo.geoTransverseMercator()\nexport const equirectangular: ProjectionFactory = () => d3Geo.geoEquirectangular()\nexport const naturalEarth1: ProjectionFactory = () => (d3GeoProjection as any).geoNaturalEarth1()\n\n// Other projections\nexport const equalEarth: ProjectionFactory = () => d3Geo.geoEqualEarth()\n\n/**\n * Object containing all standard D3 projection factories\n * Keyed by the projection ID used in Atlas Composer configurations\n */\nexport const d3ProjectionFactories: Record<string, ProjectionFactory> = {\n // Azimuthal\n 'azimuthal-equal-area': azimuthalEqualArea,\n 'azimuthal-equidistant': azimuthalEquidistant,\n 'gnomonic': gnomonic,\n 'orthographic': orthographic,\n 'stereographic': stereographic,\n\n // Conic\n 'conic-conformal': conicConformal,\n 'conic-equal-area': conicEqualArea,\n 'conic-equidistant': conicEquidistant,\n 'albers': albers,\n\n // Cylindrical\n 'mercator': mercator,\n 'transverse-mercator': transverseMercator,\n 'equirectangular': equirectangular,\n 'natural-earth-1': naturalEarth1,\n\n // Other\n 'equal-earth': equalEarth,\n}\n\n/**\n * Convenience function to register all D3 projections at once\n *\n * @example\n * ```typescript\n * import { registerProjections } from './standalone-projection-loader'\n * import { registerAllD3Projections } from './d3-projection-helpers'\n *\n * registerAllD3Projections(registerProjections)\n * ```\n *\n * @param registerFn - The registerProjections function from the loader\n */\nexport function registerAllD3Projections(\n registerFn: (factories: Record<string, ProjectionFactory>) => void,\n): void {\n registerFn(d3ProjectionFactories)\n}\n\n/**\n * Get list of available D3 projection IDs\n */\nexport function getAvailableD3Projections(): string[] {\n return Object.keys(d3ProjectionFactories)\n}\n\n// Default export\nexport default {\n d3ProjectionFactories,\n registerAllD3Projections,\n getAvailableD3Projections,\n\n // Individual projections for tree-shaking\n azimuthalEqualArea,\n azimuthalEquidistant,\n gnomonic,\n orthographic,\n stereographic,\n conicConformal,\n conicEqualArea,\n conicEquidistant,\n albers,\n mercator,\n transverseMercator,\n equirectangular,\n naturalEarth1,\n equalEarth,\n}\n"]}
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Standalone Composite Projection Loader (Zero Dependencies)
3
+ *
4
+ * A pure JavaScript/TypeScript module that consumes exported composite projection
5
+ * configurations and creates D3-compatible projections using a plugin architecture.
6
+ *
7
+ * This package has ZERO dependencies. Users must register projection factories
8
+ * before loading configurations.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * // Register projections first
13
+ * import * as d3 from 'd3-geo'
14
+ * import { registerProjection, loadCompositeProjection } from './standalone-projection-loader'
15
+ *
16
+ * registerProjection('mercator', () => d3.geoMercator())
17
+ * registerProjection('albers', () => d3.geoAlbers())
18
+ *
19
+ * // Then load your configuration
20
+ * const projection = loadCompositeProjection(config, { width: 800, height: 600 })
21
+ * ```
22
+ *
23
+ * @packageDocumentation
24
+ */
25
+ /**
26
+ * Generic projection-like interface that matches D3 projections
27
+ * without requiring d3-geo as a dependency
28
+ *
29
+ * Note: D3 projections use getter/setter pattern where calling without
30
+ * arguments returns the current value, and with arguments sets and returns this.
31
+ */
32
+ interface ProjectionLike {
33
+ (coordinates: [number, number]): [number, number] | null;
34
+ center?: {
35
+ (): [number, number];
36
+ (center: [number, number]): ProjectionLike;
37
+ };
38
+ rotate?: {
39
+ (): [number, number, number];
40
+ (angles: [number, number, number]): ProjectionLike;
41
+ };
42
+ parallels?: {
43
+ (): [number, number];
44
+ (parallels: [number, number]): ProjectionLike;
45
+ };
46
+ scale?: {
47
+ (): number;
48
+ (scale: number): ProjectionLike;
49
+ };
50
+ translate?: {
51
+ (): [number, number];
52
+ (translate: [number, number]): ProjectionLike;
53
+ };
54
+ clipExtent?: {
55
+ (): [[number, number], [number, number]] | null;
56
+ (extent: [[number, number], [number, number]] | null): ProjectionLike;
57
+ };
58
+ stream?: (stream: StreamLike) => StreamLike;
59
+ precision?: {
60
+ (): number;
61
+ (precision: number): ProjectionLike;
62
+ };
63
+ fitExtent?: (extent: [[number, number], [number, number]], object: any) => ProjectionLike;
64
+ fitSize?: (size: [number, number], object: any) => ProjectionLike;
65
+ fitWidth?: (width: number, object: any) => ProjectionLike;
66
+ fitHeight?: (height: number, object: any) => ProjectionLike;
67
+ }
68
+ /**
69
+ * Stream protocol interface for D3 geographic transforms
70
+ */
71
+ interface StreamLike {
72
+ point: (x: number, y: number) => void;
73
+ lineStart: () => void;
74
+ lineEnd: () => void;
75
+ polygonStart: () => void;
76
+ polygonEnd: () => void;
77
+ sphere?: () => void;
78
+ }
79
+ /**
80
+ * Factory function that creates a projection instance
81
+ */
82
+ type ProjectionFactory = () => ProjectionLike;
83
+ /**
84
+ * Exported configuration format (subset needed for loading)
85
+ */
86
+ interface ExportedConfig {
87
+ version: string;
88
+ metadata: {
89
+ atlasId: string;
90
+ atlasName: string;
91
+ };
92
+ pattern: string;
93
+ referenceScale: number;
94
+ territories: Territory[];
95
+ }
96
+ interface Territory {
97
+ code: string;
98
+ name: string;
99
+ role: string;
100
+ projectionId: string;
101
+ projectionFamily: string;
102
+ parameters: ProjectionParameters;
103
+ layout: Layout;
104
+ bounds: [[number, number], [number, number]];
105
+ }
106
+ interface ProjectionParameters {
107
+ center?: [number, number];
108
+ rotate?: [number, number, number];
109
+ scale: number;
110
+ baseScale: number;
111
+ scaleMultiplier: number;
112
+ parallels?: [number, number];
113
+ }
114
+ interface Layout {
115
+ translateOffset: [number, number];
116
+ clipExtent: [[number, number], [number, number]] | null;
117
+ }
118
+ /**
119
+ * Options for creating the composite projection
120
+ */
121
+ interface LoaderOptions {
122
+ /** Canvas width in pixels */
123
+ width: number;
124
+ /** Canvas height in pixels */
125
+ height: number;
126
+ /** Whether to apply clipping to territories (default: true) */
127
+ enableClipping?: boolean;
128
+ /** Debug mode - logs territory selection (default: false) */
129
+ debug?: boolean;
130
+ }
131
+ /**
132
+ * Register a projection factory with a given ID
133
+ *
134
+ * @example
135
+ * ```typescript
136
+ * import * as d3 from 'd3-geo'
137
+ * import { registerProjection } from '@atlas-composer/projection-loader'
138
+ *
139
+ * registerProjection('mercator', () => d3.geoMercator())
140
+ * registerProjection('albers', () => d3.geoAlbers())
141
+ * ```
142
+ *
143
+ * @param id - Projection identifier (e.g., 'mercator', 'albers')
144
+ * @param factory - Function that creates a new projection instance
145
+ */
146
+ declare function registerProjection(id: string, factory: ProjectionFactory): void;
147
+ /**
148
+ * Register multiple projections at once
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * import * as d3 from 'd3-geo'
153
+ * import { registerProjections } from '@atlas-composer/projection-loader'
154
+ *
155
+ * registerProjections({
156
+ * 'mercator': () => d3.geoMercator(),
157
+ * 'albers': () => d3.geoAlbers(),
158
+ * 'conic-equal-area': () => d3.geoConicEqualArea()
159
+ * })
160
+ * ```
161
+ *
162
+ * @param factories - Object mapping projection IDs to factory functions
163
+ */
164
+ declare function registerProjections(factories: Record<string, ProjectionFactory>): void;
165
+ /**
166
+ * Unregister a projection
167
+ *
168
+ * @param id - Projection identifier to remove
169
+ * @returns True if the projection was removed, false if it wasn't registered
170
+ */
171
+ declare function unregisterProjection(id: string): boolean;
172
+ /**
173
+ * Clear all registered projections
174
+ */
175
+ declare function clearProjections(): void;
176
+ /**
177
+ * Get list of currently registered projection IDs
178
+ *
179
+ * @returns Array of registered projection identifiers
180
+ */
181
+ declare function getRegisteredProjections(): string[];
182
+ /**
183
+ * Check if a projection is registered
184
+ *
185
+ * @param id - Projection identifier to check
186
+ * @returns True if the projection is registered
187
+ */
188
+ declare function isProjectionRegistered(id: string): boolean;
189
+ /**
190
+ * Create a D3-compatible projection from an exported composite projection configuration
191
+ *
192
+ * @example
193
+ * ```typescript
194
+ * import * as d3 from 'd3-geo'
195
+ * import { registerProjection, loadCompositeProjection } from '@atlas-composer/projection-loader'
196
+ *
197
+ * // Register projections first
198
+ * registerProjection('mercator', () => d3.geoMercator())
199
+ * registerProjection('albers', () => d3.geoAlbers())
200
+ *
201
+ * // Load configuration
202
+ * const config = JSON.parse(jsonString)
203
+ *
204
+ * // Create projection
205
+ * const projection = loadCompositeProjection(config, {
206
+ * width: 800,
207
+ * height: 600
208
+ * })
209
+ *
210
+ * // Use with D3
211
+ * const path = d3.geoPath(projection)
212
+ * svg.selectAll('path')
213
+ * .data(countries.features)
214
+ * .join('path')
215
+ * .attr('d', path)
216
+ * ```
217
+ *
218
+ * @param config - Exported composite projection configuration
219
+ * @param options - Canvas dimensions and options
220
+ * @returns D3-compatible projection that routes geometry to appropriate sub-projections
221
+ */
222
+ declare function loadCompositeProjection(config: ExportedConfig, options: LoaderOptions): ProjectionLike;
223
+ /**
224
+ * Validate an exported configuration
225
+ *
226
+ * @param config - Configuration to validate
227
+ * @returns True if valid, throws error otherwise
228
+ */
229
+ declare function validateConfig(config: any): config is ExportedConfig;
230
+ /**
231
+ * Load composite projection from JSON string
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * import * as d3 from 'd3-geo'
236
+ * import { registerProjection, loadFromJSON } from '@atlas-composer/projection-loader'
237
+ *
238
+ * // Register projections first
239
+ * registerProjection('mercator', () => d3.geoMercator())
240
+ *
241
+ * // Load from JSON
242
+ * const jsonString = fs.readFileSync('france-composite.json', 'utf-8')
243
+ * const projection = loadFromJSON(jsonString, { width: 800, height: 600 })
244
+ * ```
245
+ */
246
+ declare function loadFromJSON(jsonString: string, options: LoaderOptions): ProjectionLike;
247
+
248
+ export { type ExportedConfig, type Layout, type LoaderOptions, type ProjectionFactory, type ProjectionLike, type ProjectionParameters, type StreamLike, type Territory, clearProjections, getRegisteredProjections, isProjectionRegistered, loadCompositeProjection, loadFromJSON, registerProjection, registerProjections, unregisterProjection, validateConfig };
package/dist/index.js ADDED
@@ -0,0 +1,239 @@
1
+ // src/standalone-projection-loader.ts
2
+ var projectionRegistry = /* @__PURE__ */ new Map();
3
+ function registerProjection(id, factory) {
4
+ projectionRegistry.set(id, factory);
5
+ }
6
+ function registerProjections(factories) {
7
+ for (const [id, factory] of Object.entries(factories)) {
8
+ registerProjection(id, factory);
9
+ }
10
+ }
11
+ function unregisterProjection(id) {
12
+ return projectionRegistry.delete(id);
13
+ }
14
+ function clearProjections() {
15
+ projectionRegistry.clear();
16
+ }
17
+ function getRegisteredProjections() {
18
+ return Array.from(projectionRegistry.keys());
19
+ }
20
+ function isProjectionRegistered(id) {
21
+ return projectionRegistry.has(id);
22
+ }
23
+ function createProjectionWrapper(project) {
24
+ let _scale = 150;
25
+ let _translate = [480, 250];
26
+ const projection = function(coordinates) {
27
+ const point = project(coordinates[0] * Math.PI / 180, coordinates[1] * Math.PI / 180);
28
+ if (!point)
29
+ return null;
30
+ return [point[0] * _scale + _translate[0], point[1] * _scale + _translate[1]];
31
+ };
32
+ projection.scale = ((s) => {
33
+ if (arguments.length === 0)
34
+ return _scale;
35
+ _scale = s;
36
+ return projection;
37
+ });
38
+ projection.translate = ((t) => {
39
+ if (arguments.length === 0)
40
+ return _translate;
41
+ _translate = t;
42
+ return projection;
43
+ });
44
+ return projection;
45
+ }
46
+ function loadCompositeProjection(config, options) {
47
+ const { width, height, debug = false } = options;
48
+ if (config.version !== "1.0") {
49
+ throw new Error(`Unsupported configuration version: ${config.version}`);
50
+ }
51
+ if (!config.territories || config.territories.length === 0) {
52
+ throw new Error("Configuration must contain at least one territory");
53
+ }
54
+ const subProjections = config.territories.map((territory) => {
55
+ const proj = createSubProjection(territory, width, height);
56
+ return {
57
+ territory,
58
+ projection: proj,
59
+ bounds: territory.bounds
60
+ };
61
+ });
62
+ if (debug) {
63
+ console.log("[CompositeProjection] Created sub-projections:", {
64
+ territories: config.territories.map((t) => ({ code: t.code, name: t.name })),
65
+ count: subProjections.length
66
+ });
67
+ }
68
+ const compositeProjection = createProjectionWrapper((lambda, phi) => {
69
+ const lon = lambda * 180 / Math.PI;
70
+ const lat = phi * 180 / Math.PI;
71
+ let selectedProj = null;
72
+ for (const { projection, bounds } of subProjections) {
73
+ if (lon >= bounds[0][0] && lon <= bounds[1][0] && lat >= bounds[0][1] && lat <= bounds[1][1]) {
74
+ selectedProj = projection;
75
+ break;
76
+ }
77
+ }
78
+ if (!selectedProj && subProjections[0]) {
79
+ selectedProj = subProjections[0].projection;
80
+ }
81
+ return selectedProj([lambda, phi]);
82
+ });
83
+ compositeProjection.stream = function(stream) {
84
+ let activeStream = null;
85
+ let bufferedPoints = [];
86
+ let activeTerritoryCode = "";
87
+ return {
88
+ point(lon, lat) {
89
+ bufferedPoints.push([lon, lat]);
90
+ if (activeStream) {
91
+ const lonDeg = lon * 180 / Math.PI;
92
+ const latDeg = lat * 180 / Math.PI;
93
+ if (debug) {
94
+ console.log(`[Stream] Point: [${lonDeg.toFixed(2)}, ${latDeg.toFixed(2)}] \u2192 ${activeTerritoryCode}`);
95
+ }
96
+ activeStream.point(lon, lat);
97
+ }
98
+ },
99
+ lineStart() {
100
+ if (bufferedPoints.length > 0 && bufferedPoints[0]) {
101
+ const [lon, lat] = bufferedPoints[0];
102
+ const lonDeg = lon * 180 / Math.PI;
103
+ const latDeg = lat * 180 / Math.PI;
104
+ for (const { territory, projection, bounds } of subProjections) {
105
+ if (lonDeg >= bounds[0][0] && lonDeg <= bounds[1][0] && latDeg >= bounds[0][1] && latDeg <= bounds[1][1]) {
106
+ if (projection.stream) {
107
+ activeStream = projection.stream(stream);
108
+ activeTerritoryCode = territory.code;
109
+ }
110
+ if (debug) {
111
+ console.log(`[Stream] Line started in territory: ${territory.code}`);
112
+ }
113
+ break;
114
+ }
115
+ }
116
+ if (!activeStream && subProjections[0]) {
117
+ const firstProj = subProjections[0].projection;
118
+ if (firstProj.stream) {
119
+ activeStream = firstProj.stream(stream);
120
+ activeTerritoryCode = subProjections[0].territory.code;
121
+ }
122
+ if (debug) {
123
+ console.log(`[Stream] Line started (fallback): ${activeTerritoryCode}`);
124
+ }
125
+ }
126
+ }
127
+ if (activeStream) {
128
+ activeStream.lineStart();
129
+ for (const [lon, lat] of bufferedPoints) {
130
+ activeStream.point(lon, lat);
131
+ }
132
+ }
133
+ bufferedPoints = [];
134
+ },
135
+ lineEnd() {
136
+ if (activeStream) {
137
+ activeStream.lineEnd();
138
+ }
139
+ },
140
+ polygonStart() {
141
+ bufferedPoints = [];
142
+ activeStream = null;
143
+ },
144
+ polygonEnd() {
145
+ if (activeStream) {
146
+ activeStream.polygonEnd();
147
+ }
148
+ activeStream = null;
149
+ },
150
+ sphere() {
151
+ if (debug) {
152
+ console.warn("[Stream] sphere() called - not supported in composite projections");
153
+ }
154
+ }
155
+ };
156
+ };
157
+ if (compositeProjection.scale) {
158
+ compositeProjection.scale(1);
159
+ }
160
+ if (compositeProjection.translate) {
161
+ compositeProjection.translate([width / 2, height / 2]);
162
+ }
163
+ return compositeProjection;
164
+ }
165
+ function createSubProjection(territory, width, height) {
166
+ const { projectionId, parameters, layout } = territory;
167
+ const factory = projectionRegistry.get(projectionId);
168
+ if (!factory) {
169
+ const registered = getRegisteredProjections();
170
+ const availableList = registered.length > 0 ? registered.join(", ") : "none";
171
+ throw new Error(
172
+ `Projection "${projectionId}" is not registered. Available projections: ${availableList}. Use registerProjection('${projectionId}', factory) to register it.`
173
+ );
174
+ }
175
+ const projection = factory();
176
+ if (parameters.center && projection.center) {
177
+ projection.center(parameters.center);
178
+ }
179
+ if (parameters.rotate && projection.rotate) {
180
+ projection.rotate(parameters.rotate);
181
+ }
182
+ if (parameters.parallels && projection.parallels) {
183
+ projection.parallels(parameters.parallels);
184
+ }
185
+ if (projection.scale) {
186
+ projection.scale(parameters.scale);
187
+ }
188
+ if (projection.translate) {
189
+ const [offsetX, offsetY] = layout.translateOffset;
190
+ projection.translate([
191
+ width / 2 + offsetX,
192
+ height / 2 + offsetY
193
+ ]);
194
+ }
195
+ if (layout.clipExtent && projection.clipExtent) {
196
+ projection.clipExtent(layout.clipExtent);
197
+ }
198
+ return projection;
199
+ }
200
+ function validateConfig(config) {
201
+ if (!config || typeof config !== "object") {
202
+ throw new Error("Configuration must be an object");
203
+ }
204
+ if (!config.version) {
205
+ throw new Error("Configuration must have a version field");
206
+ }
207
+ if (!config.metadata || !config.metadata.atlasId) {
208
+ throw new Error("Configuration must have metadata with atlasId");
209
+ }
210
+ if (!config.territories || !Array.isArray(config.territories)) {
211
+ throw new Error("Configuration must have territories array");
212
+ }
213
+ if (config.territories.length === 0) {
214
+ throw new Error("Configuration must have at least one territory");
215
+ }
216
+ for (const territory of config.territories) {
217
+ if (!territory.code || !territory.projectionId) {
218
+ throw new Error(`Territory missing required fields: ${JSON.stringify(territory)}`);
219
+ }
220
+ if (!territory.parameters || !territory.bounds) {
221
+ throw new Error(`Territory ${territory.code} missing parameters or bounds`);
222
+ }
223
+ }
224
+ return true;
225
+ }
226
+ function loadFromJSON(jsonString, options) {
227
+ let config;
228
+ try {
229
+ config = JSON.parse(jsonString);
230
+ } catch (error) {
231
+ throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : "Unknown error"}`);
232
+ }
233
+ validateConfig(config);
234
+ return loadCompositeProjection(config, options);
235
+ }
236
+
237
+ export { clearProjections, getRegisteredProjections, isProjectionRegistered, loadCompositeProjection, loadFromJSON, registerProjection, registerProjections, unregisterProjection, validateConfig };
238
+ //# sourceMappingURL=index.js.map
239
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/standalone-projection-loader.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Standalone Composite Projection Loader (Zero Dependencies)\n *\n * A pure JavaScript/TypeScript module that consumes exported composite projection\n * configurations and creates D3-compatible projections using a plugin architecture.\n *\n * This package has ZERO dependencies. Users must register projection factories\n * before loading configurations.\n *\n * @example\n * ```typescript\n * // Register projections first\n * import * as d3 from 'd3-geo'\n * import { registerProjection, loadCompositeProjection } from './standalone-projection-loader'\n *\n * registerProjection('mercator', () => d3.geoMercator())\n * registerProjection('albers', () => d3.geoAlbers())\n *\n * // Then load your configuration\n * const projection = loadCompositeProjection(config, { width: 800, height: 600 })\n * ```\n *\n * @packageDocumentation\n */\n\n/**\n * Generic projection-like interface that matches D3 projections\n * without requiring d3-geo as a dependency\n *\n * Note: D3 projections use getter/setter pattern where calling without\n * arguments returns the current value, and with arguments sets and returns this.\n */\nexport interface ProjectionLike {\n (coordinates: [number, number]): [number, number] | null\n center?: {\n (): [number, number]\n (center: [number, number]): ProjectionLike\n }\n rotate?: {\n (): [number, number, number]\n (angles: [number, number, number]): ProjectionLike\n }\n parallels?: {\n (): [number, number]\n (parallels: [number, number]): ProjectionLike\n }\n scale?: {\n (): number\n (scale: number): ProjectionLike\n }\n translate?: {\n (): [number, number]\n (translate: [number, number]): ProjectionLike\n }\n clipExtent?: {\n (): [[number, number], [number, number]] | null\n (extent: [[number, number], [number, number]] | null): ProjectionLike\n }\n stream?: (stream: StreamLike) => StreamLike\n precision?: {\n (): number\n (precision: number): ProjectionLike\n }\n fitExtent?: (extent: [[number, number], [number, number]], object: any) => ProjectionLike\n fitSize?: (size: [number, number], object: any) => ProjectionLike\n fitWidth?: (width: number, object: any) => ProjectionLike\n fitHeight?: (height: number, object: any) => ProjectionLike\n}\n\n/**\n * Stream protocol interface for D3 geographic transforms\n */\nexport interface StreamLike {\n point: (x: number, y: number) => void\n lineStart: () => void\n lineEnd: () => void\n polygonStart: () => void\n polygonEnd: () => void\n sphere?: () => void\n}\n\n/**\n * Factory function that creates a projection instance\n */\nexport type ProjectionFactory = () => ProjectionLike\n\n/**\n * Exported configuration format (subset needed for loading)\n */\nexport interface ExportedConfig {\n version: string\n metadata: {\n atlasId: string\n atlasName: string\n }\n pattern: string\n referenceScale: number\n territories: Territory[]\n}\n\nexport interface Territory {\n code: string\n name: string\n role: string\n projectionId: string\n projectionFamily: string\n parameters: ProjectionParameters\n layout: Layout\n bounds: [[number, number], [number, number]]\n}\n\nexport interface ProjectionParameters {\n center?: [number, number]\n rotate?: [number, number, number]\n scale: number\n baseScale: number\n scaleMultiplier: number\n parallels?: [number, number]\n}\n\nexport interface Layout {\n translateOffset: [number, number]\n clipExtent: [[number, number], [number, number]] | null\n}\n\n/**\n * Options for creating the composite projection\n */\nexport interface LoaderOptions {\n /** Canvas width in pixels */\n width: number\n /** Canvas height in pixels */\n height: number\n /** Whether to apply clipping to territories (default: true) */\n enableClipping?: boolean\n /** Debug mode - logs territory selection (default: false) */\n debug?: boolean\n}\n\n/**\n * Runtime registry for projection factories\n * Users must register projections before loading configurations\n */\nconst projectionRegistry = new Map<string, ProjectionFactory>()\n\n/**\n * Register a projection factory with a given ID\n *\n * @example\n * ```typescript\n * import * as d3 from 'd3-geo'\n * import { registerProjection } from '@atlas-composer/projection-loader'\n *\n * registerProjection('mercator', () => d3.geoMercator())\n * registerProjection('albers', () => d3.geoAlbers())\n * ```\n *\n * @param id - Projection identifier (e.g., 'mercator', 'albers')\n * @param factory - Function that creates a new projection instance\n */\nexport function registerProjection(id: string, factory: ProjectionFactory): void {\n projectionRegistry.set(id, factory)\n}\n\n/**\n * Register multiple projections at once\n *\n * @example\n * ```typescript\n * import * as d3 from 'd3-geo'\n * import { registerProjections } from '@atlas-composer/projection-loader'\n *\n * registerProjections({\n * 'mercator': () => d3.geoMercator(),\n * 'albers': () => d3.geoAlbers(),\n * 'conic-equal-area': () => d3.geoConicEqualArea()\n * })\n * ```\n *\n * @param factories - Object mapping projection IDs to factory functions\n */\nexport function registerProjections(factories: Record<string, ProjectionFactory>): void {\n for (const [id, factory] of Object.entries(factories)) {\n registerProjection(id, factory)\n }\n}\n\n/**\n * Unregister a projection\n *\n * @param id - Projection identifier to remove\n * @returns True if the projection was removed, false if it wasn't registered\n */\nexport function unregisterProjection(id: string): boolean {\n return projectionRegistry.delete(id)\n}\n\n/**\n * Clear all registered projections\n */\nexport function clearProjections(): void {\n projectionRegistry.clear()\n}\n\n/**\n * Get list of currently registered projection IDs\n *\n * @returns Array of registered projection identifiers\n */\nexport function getRegisteredProjections(): string[] {\n return Array.from(projectionRegistry.keys())\n}\n\n/**\n * Check if a projection is registered\n *\n * @param id - Projection identifier to check\n * @returns True if the projection is registered\n */\nexport function isProjectionRegistered(id: string): boolean {\n return projectionRegistry.has(id)\n}\n\n/**\n * Create a minimal projection wrapper (similar to d3.geoProjection)\n * This allows us to avoid the d3-geo dependency\n */\nfunction createProjectionWrapper(\n project: (lambda: number, phi: number) => [number, number] | null,\n): ProjectionLike {\n let _scale = 150\n let _translate: [number, number] = [480, 250]\n\n const projection = function (coordinates: [number, number]): [number, number] | null {\n const point = project(coordinates[0] * Math.PI / 180, coordinates[1] * Math.PI / 180)\n if (!point)\n return null\n return [point[0] * _scale + _translate[0], point[1] * _scale + _translate[1]]\n } as ProjectionLike\n\n // D3-style getter/setter for scale\n projection.scale = ((s?: number): any => {\n if (arguments.length === 0)\n return _scale\n _scale = s!\n return projection\n }) as any\n\n // D3-style getter/setter for translate\n projection.translate = ((t?: [number, number]): any => {\n if (arguments.length === 0)\n return _translate\n _translate = t!\n return projection\n }) as any\n\n return projection\n}\n\n/**\n * Create a D3-compatible projection from an exported composite projection configuration\n *\n * @example\n * ```typescript\n * import * as d3 from 'd3-geo'\n * import { registerProjection, loadCompositeProjection } from '@atlas-composer/projection-loader'\n *\n * // Register projections first\n * registerProjection('mercator', () => d3.geoMercator())\n * registerProjection('albers', () => d3.geoAlbers())\n *\n * // Load configuration\n * const config = JSON.parse(jsonString)\n *\n * // Create projection\n * const projection = loadCompositeProjection(config, {\n * width: 800,\n * height: 600\n * })\n *\n * // Use with D3\n * const path = d3.geoPath(projection)\n * svg.selectAll('path')\n * .data(countries.features)\n * .join('path')\n * .attr('d', path)\n * ```\n *\n * @param config - Exported composite projection configuration\n * @param options - Canvas dimensions and options\n * @returns D3-compatible projection that routes geometry to appropriate sub-projections\n */\nexport function loadCompositeProjection(\n config: ExportedConfig,\n options: LoaderOptions,\n): ProjectionLike {\n const { width, height, debug = false } = options\n\n // Validate configuration version\n if (config.version !== '1.0') {\n throw new Error(`Unsupported configuration version: ${config.version}`)\n }\n\n if (!config.territories || config.territories.length === 0) {\n throw new Error('Configuration must contain at least one territory')\n }\n\n // Create sub-projections for each territory\n const subProjections = config.territories.map((territory) => {\n const proj = createSubProjection(territory, width, height)\n\n return {\n territory,\n projection: proj,\n bounds: territory.bounds,\n }\n })\n\n if (debug) {\n console.log('[CompositeProjection] Created sub-projections:', {\n territories: config.territories.map(t => ({ code: t.code, name: t.name })),\n count: subProjections.length,\n })\n }\n\n // Create composite projection using custom stream multiplexing\n const compositeProjection = createProjectionWrapper((lambda: number, phi: number) => {\n // Convert radians to degrees for bounds checking\n const lon = (lambda * 180) / Math.PI\n const lat = (phi * 180) / Math.PI\n\n // Find which territory this point belongs to\n let selectedProj = null\n for (const { projection, bounds } of subProjections) {\n if (\n lon >= bounds[0][0] && lon <= bounds[1][0]\n && lat >= bounds[0][1] && lat <= bounds[1][1]\n ) {\n selectedProj = projection\n break\n }\n }\n\n // If no territory matched, use first projection (fallback)\n if (!selectedProj && subProjections[0]) {\n selectedProj = subProjections[0].projection\n }\n\n // Project the point (should always have a projection by this point)\n return selectedProj!([lambda, phi])\n })\n\n // Implement stream multiplexing for proper geometry routing\n compositeProjection.stream = function (stream: StreamLike): StreamLike {\n let activeStream: StreamLike | null = null\n let bufferedPoints: Array<[number, number]> = []\n let activeTerritoryCode = ''\n\n return {\n point(lon: number, lat: number) {\n // Buffer points until we can determine which territory they belong to\n bufferedPoints.push([lon, lat])\n\n // If we have an active stream, forward the point\n if (activeStream) {\n const lonDeg = (lon * 180) / Math.PI\n const latDeg = (lat * 180) / Math.PI\n\n if (debug) {\n console.log(`[Stream] Point: [${lonDeg.toFixed(2)}, ${latDeg.toFixed(2)}] → ${activeTerritoryCode}`)\n }\n\n activeStream.point(lon, lat)\n }\n },\n\n lineStart() {\n // Determine which territory this line belongs to\n if (bufferedPoints.length > 0 && bufferedPoints[0]) {\n const [lon, lat] = bufferedPoints[0]\n const lonDeg = (lon * 180) / Math.PI\n const latDeg = (lat * 180) / Math.PI\n\n // Find matching territory\n for (const { territory, projection, bounds } of subProjections) {\n if (\n lonDeg >= bounds[0][0] && lonDeg <= bounds[1][0]\n && latDeg >= bounds[0][1] && latDeg <= bounds[1][1]\n ) {\n // Use the projection's stream if available\n if (projection.stream) {\n activeStream = projection.stream(stream)\n activeTerritoryCode = territory.code\n }\n\n if (debug) {\n console.log(`[Stream] Line started in territory: ${territory.code}`)\n }\n break\n }\n }\n\n // Fallback to first projection\n if (!activeStream && subProjections[0]) {\n const firstProj = subProjections[0].projection\n if (firstProj.stream) {\n activeStream = firstProj.stream(stream)\n activeTerritoryCode = subProjections[0].territory.code\n }\n\n if (debug) {\n console.log(`[Stream] Line started (fallback): ${activeTerritoryCode}`)\n }\n }\n }\n\n if (activeStream) {\n activeStream.lineStart()\n\n // Replay buffered points\n for (const [lon, lat] of bufferedPoints) {\n activeStream.point(lon, lat)\n }\n }\n\n bufferedPoints = []\n },\n\n lineEnd() {\n if (activeStream) {\n activeStream.lineEnd()\n }\n },\n\n polygonStart() {\n bufferedPoints = []\n activeStream = null\n },\n\n polygonEnd() {\n if (activeStream) {\n activeStream.polygonEnd()\n }\n activeStream = null\n },\n\n sphere() {\n // Not supported in composite projections\n if (debug) {\n console.warn('[Stream] sphere() called - not supported in composite projections')\n }\n },\n }\n }\n\n // Set reasonable defaults for the composite projection\n // Note: Individual territories handle their own scale/translate\n if (compositeProjection.scale) {\n compositeProjection.scale(1)\n }\n if (compositeProjection.translate) {\n compositeProjection.translate([width / 2, height / 2])\n }\n\n return compositeProjection\n}\n\n/**\n * Create a sub-projection for a single territory\n */\nfunction createSubProjection(\n territory: Territory,\n width: number,\n height: number,\n): ProjectionLike {\n const { projectionId, parameters, layout } = territory\n\n // Get projection factory from registry\n const factory = projectionRegistry.get(projectionId)\n if (!factory) {\n const registered = getRegisteredProjections()\n const availableList = registered.length > 0 ? registered.join(', ') : 'none'\n throw new Error(\n `Projection \"${projectionId}\" is not registered. `\n + `Available projections: ${availableList}. `\n + `Use registerProjection('${projectionId}', factory) to register it.`,\n )\n }\n\n // Create projection instance\n const projection = factory()\n\n // Apply parameters\n if (parameters.center && projection.center) {\n projection.center(parameters.center)\n }\n\n if (parameters.rotate && projection.rotate) {\n projection.rotate(parameters.rotate)\n }\n\n if (parameters.parallels && projection.parallels) {\n projection.parallels(parameters.parallels)\n }\n\n if (projection.scale) {\n projection.scale(parameters.scale)\n }\n\n // Apply layout translate\n if (projection.translate) {\n const [offsetX, offsetY] = layout.translateOffset\n projection.translate([\n width / 2 + offsetX,\n height / 2 + offsetY,\n ])\n }\n\n // Apply clipping if specified\n if (layout.clipExtent && projection.clipExtent) {\n projection.clipExtent(layout.clipExtent)\n }\n\n return projection\n}\n\n/**\n * Validate an exported configuration\n *\n * @param config - Configuration to validate\n * @returns True if valid, throws error otherwise\n */\nexport function validateConfig(config: any): config is ExportedConfig {\n if (!config || typeof config !== 'object') {\n throw new Error('Configuration must be an object')\n }\n\n if (!config.version) {\n throw new Error('Configuration must have a version field')\n }\n\n if (!config.metadata || !config.metadata.atlasId) {\n throw new Error('Configuration must have metadata with atlasId')\n }\n\n if (!config.territories || !Array.isArray(config.territories)) {\n throw new Error('Configuration must have territories array')\n }\n\n if (config.territories.length === 0) {\n throw new Error('Configuration must have at least one territory')\n }\n\n // Validate each territory\n for (const territory of config.territories) {\n if (!territory.code || !territory.projectionId) {\n throw new Error(`Territory missing required fields: ${JSON.stringify(territory)}`)\n }\n\n if (!territory.parameters || !territory.bounds) {\n throw new Error(`Territory ${territory.code} missing parameters or bounds`)\n }\n }\n\n return true\n}\n\n/**\n * Load composite projection from JSON string\n *\n * @example\n * ```typescript\n * import * as d3 from 'd3-geo'\n * import { registerProjection, loadFromJSON } from '@atlas-composer/projection-loader'\n *\n * // Register projections first\n * registerProjection('mercator', () => d3.geoMercator())\n *\n * // Load from JSON\n * const jsonString = fs.readFileSync('france-composite.json', 'utf-8')\n * const projection = loadFromJSON(jsonString, { width: 800, height: 600 })\n * ```\n */\nexport function loadFromJSON(\n jsonString: string,\n options: LoaderOptions,\n): ProjectionLike {\n let config: any\n\n try {\n config = JSON.parse(jsonString)\n }\n catch (error) {\n throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : 'Unknown error'}`)\n }\n\n validateConfig(config)\n return loadCompositeProjection(config, options)\n}\n\n// Default export\nexport default {\n // Core loading functions\n loadCompositeProjection,\n loadFromJSON,\n validateConfig,\n\n // Registry management\n registerProjection,\n registerProjections,\n unregisterProjection,\n clearProjections,\n getRegisteredProjections,\n isProjectionRegistered,\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "@atlas-composer/projection-loader",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "description": "Zero-dependency standalone loader for composite map projections with plugin architecture",
6
+ "author": "Lucas Poulain (ShallowRed)",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/ShallowRed/atlas-composer/tree/main/packages/projection-loader#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/ShallowRed/atlas-composer.git",
12
+ "directory": "packages/projection-loader"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/ShallowRed/atlas-composer/issues"
16
+ },
17
+ "keywords": [
18
+ "cartography",
19
+ "composite",
20
+ "d3",
21
+ "d3-geo",
22
+ "geography",
23
+ "gis",
24
+ "map",
25
+ "observable-plot",
26
+ "projection",
27
+ "tree-shakeable",
28
+ "zero-dependencies"
29
+ ],
30
+ "sideEffects": false,
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js"
35
+ },
36
+ "./helpers": {
37
+ "types": "./dist/d3-projection-helpers.d.ts",
38
+ "import": "./dist/d3-projection-helpers.js"
39
+ }
40
+ },
41
+ "main": "./dist/index.js",
42
+ "module": "./dist/index.js",
43
+ "types": "./dist/index.d.ts",
44
+ "files": [
45
+ "LICENSE",
46
+ "README.md",
47
+ "dist"
48
+ ],
49
+ "scripts": {
50
+ "build": "tsup",
51
+ "dev": "tsup --watch",
52
+ "typecheck": "tsc --noEmit",
53
+ "size": "size-limit",
54
+ "size:why": "size-limit --why"
55
+ },
56
+ "size-limit": [
57
+ {
58
+ "name": "Main bundle (ESM)",
59
+ "path": "dist/index.js",
60
+ "limit": "10 KB",
61
+ "gzip": true
62
+ },
63
+ {
64
+ "name": "D3 helpers (optional)",
65
+ "path": "dist/d3-projection-helpers.js",
66
+ "limit": "3 KB",
67
+ "gzip": true
68
+ }
69
+ ],
70
+ "peerDependencies": {
71
+ "d3-geo": ">=3.0.0",
72
+ "d3-geo-projection": ">=4.0.0"
73
+ },
74
+ "peerDependenciesMeta": {
75
+ "d3-geo": {
76
+ "optional": false
77
+ },
78
+ "d3-geo-projection": {
79
+ "optional": true
80
+ }
81
+ },
82
+ "devDependencies": {
83
+ "@size-limit/preset-small-lib": "^11.2.0",
84
+ "@types/d3-geo": "^3.1.0",
85
+ "tsup": "^8.0.0",
86
+ "typescript": "~5.9.3"
87
+ }
88
+ }