@atlas-composer/projection-loader 1.1.0-rc.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,459 +1,51 @@
1
1
  # @atlas-composer/projection-loader
2
2
 
3
- > Zero-dependency standalone loader for composite map projections with plugin architecture
3
+ > The runtime engine for Atlas Composer maps.
4
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.
5
+ This package allows you to render composite projections exported from **Atlas Composer** in your own applications. It is lightweight, and works seamlessly with D3.js.
6
6
 
7
- **✨ NEW**: Now supports the updated preset format from Atlas composer 2.0+ with backward compatibility for legacy exports.
7
+ ## Features
8
8
 
9
- ## Features
9
+ - **Zero Runtime Dependencies** (excluding your choice of projection library).
10
+ - **Plugin Architecture**: Only import the projection definitions you need.
11
+ - **Type-Safe**: Written in TypeScript with full type definitions.
10
12
 
11
- - 🎯 **Zero Dependencies** - Bring your own projections (D3, Proj4, or custom)
12
- - 📦 **Tree-Shakeable** - Only bundle what you use (~6KB vs 100KB)
13
- - 🔌 **Plugin Architecture** - Register projections on-demand
14
- - 🌐 **Framework Agnostic** - Works with D3, Observable Plot, React, Vue, Svelte
15
- - 📘 **Full TypeScript Support** - Complete type definitions included
16
- - ⚡ **Fast** - Optimized stream multiplexing for efficient rendering
13
+ ## 🚀 Usage
17
14
 
18
- ## Installation
15
+ ### 1. Install
19
16
 
20
17
  ```bash
21
- npm install @atlas-composer/projection-loader d3-geo d3-geo-projection
22
- # or
23
- pnpm add @atlas-composer/projection-loader d3-geo d3-geo-projection
24
- # or
25
- yarn add @atlas-composer/projection-loader d3-geo d3-geo-projection
18
+ npm install @atlas-composer/projection-loader d3-geo
26
19
  ```
27
20
 
28
- ## Quick Start
21
+ ### 2. Implementation
29
22
 
30
23
  ```typescript
31
24
  import { loadCompositeProjection, registerProjection } from '@atlas-composer/projection-loader'
32
25
  import * as d3 from 'd3-geo'
26
+ import config from './my-exported-map.json'
33
27
 
34
- // 2. Load your exported configuration
35
- import config from './france-composite.json'
36
-
37
- // 1. Register projections (only what you need!)
28
+ // 1. Register the projections required by your map
38
29
  registerProjection('mercator', () => d3.geoMercator())
39
30
  registerProjection('conic-conformal', () => d3.geoConicConformal())
40
31
 
32
+ // 2. Load the composite projection
33
+ // The resulting object is a standard D3 stream-compatible projection
41
34
  const projection = loadCompositeProjection(config, {
42
35
  width: 800,
43
36
  height: 600
44
37
  })
45
38
 
46
- // 3. Use with D3
39
+ // 3. Render using D3
47
40
  const path = d3.geoPath(projection)
48
41
 
49
- svg.selectAll('path')
50
- .data(features)
42
+ d3.select('svg')
43
+ .selectAll('path')
44
+ .data(geojson.features)
51
45
  .join('path')
52
46
  .attr('d', path)
53
- .attr('fill', 'lightgray')
54
- .attr('stroke', 'white')
55
- ```
56
-
57
- ## Usage with Helpers (Convenience)
58
-
59
- For quick prototyping, use the optional helpers module:
60
-
61
- ```typescript
62
- import { registerProjections } from '@atlas-composer/projection-loader'
63
- import { d3ProjectionFactories } from '@atlas-composer/projection-loader/helpers'
64
-
65
- // Register all standard D3 projections at once
66
- registerProjections(d3ProjectionFactories)
67
-
68
- // Now load your configuration
69
- const projection = loadCompositeProjection(config, { width: 800, height: 600 })
70
- ```
71
-
72
- ## Tree-Shaking (Production)
73
-
74
- For optimal bundle sizes, import only what you need:
75
-
76
- ```typescript
77
- import { loadCompositeProjection, registerProjections } from '@atlas-composer/projection-loader'
78
- import { geoConicConformal, geoMercator } from 'd3-geo'
79
-
80
- // Only these two projections will be in your bundle
81
- registerProjections({
82
- 'mercator': () => geoMercator(),
83
- 'conic-conformal': () => geoConicConformal()
84
- })
85
-
86
- const projection = loadCompositeProjection(config, { width: 800, height: 600 })
87
47
  ```
88
48
 
89
- **Result**: ~6KB instead of ~100KB (94% reduction) 🎉
90
-
91
- ## Configuration Format Support
92
-
93
- The loader supports multiple configuration formats for maximum compatibility:
94
-
95
- ### New Format (Atlas composer 2.0+)
96
- ```json
97
- {
98
- "version": "1.0",
99
- "metadata": { "atlasId": "france", "atlasName": "France" },
100
- "pattern": "single-focus",
101
- "referenceScale": 2700,
102
- "territories": [
103
- {
104
- "code": "FR-MET",
105
- "projection": {
106
- "id": "conic-conformal",
107
- "family": "CONIC",
108
- "parameters": {
109
- "rotate": [-3, -46.2, 0],
110
- "scaleMultiplier": 1
111
- }
112
- },
113
- "layout": { "translateOffset": [0, 0] }
114
- }
115
- ]
116
- }
117
- ```
118
-
119
- ### Legacy Format (Atlas composer 1.x)
120
- ```json
121
- {
122
- "version": "1.0",
123
- "territories": [
124
- {
125
- "code": "FR-MET",
126
- "projectionId": "conic-conformal",
127
- "parameters": {
128
- "scale": 2700,
129
- "baseScale": 2700,
130
- "scaleMultiplier": 1
131
- }
132
- }
133
- ]
134
- }
135
- ```
136
-
137
- The loader automatically detects and handles both formats seamlessly.
138
-
139
- ## Observable Plot Integration
140
-
141
- ```typescript
142
- import { loadCompositeProjection, registerProjection } from '@atlas-composer/projection-loader'
143
- import * as Plot from '@observablehq/plot'
144
- import * as d3 from 'd3-geo'
145
-
146
- // Register projections
147
- registerProjection('mercator', () => d3.geoMercator())
148
- registerProjection('conic-conformal', () => d3.geoConicConformal())
149
-
150
- // Create projection factory for Plot
151
- function createProjection({ width, height }) {
152
- return loadCompositeProjection(config, { width, height })
153
- }
154
-
155
- // Use with Plot
156
- Plot.plot({
157
- width: 975,
158
- height: 610,
159
- projection: createProjection,
160
- marks: [
161
- Plot.geo(countries, { fill: 'lightgray', stroke: 'white' })
162
- ]
163
- })
164
- ```
165
-
166
- ## Framework Examples
167
-
168
- ### React
169
-
170
- ```tsx
171
- import { loadCompositeProjection, registerProjections } from '@atlas-composer/projection-loader'
172
- import * as d3 from 'd3-geo'
173
- import { useEffect, useRef } from 'react'
174
-
175
- function MapComponent({ config }) {
176
- const svgRef = useRef<SVGSVGElement>(null)
177
-
178
- useEffect(() => {
179
- // Register projections once
180
- registerProjections({
181
- 'mercator': () => d3.geoMercator(),
182
- 'conic-conformal': () => d3.geoConicConformal()
183
- })
184
-
185
- // Create projection
186
- const projection = loadCompositeProjection(config, {
187
- width: 800,
188
- height: 600
189
- })
190
-
191
- // Render map
192
- const path = d3.geoPath(projection)
193
- const svg = d3.select(svgRef.current)
194
-
195
- svg.selectAll('path')
196
- .data(features)
197
- .join('path')
198
- .attr('d', path)
199
- .attr('fill', 'lightgray')
200
- }, [config])
201
-
202
- return <svg ref={svgRef} width={800} height={600} />
203
- }
204
- ```
205
-
206
- ### Vue 3
207
-
208
- ```vue
209
- <script setup lang="ts">
210
- import { loadCompositeProjection, registerProjections } from '@atlas-composer/projection-loader'
211
- import * as d3 from 'd3-geo'
212
- import { onMounted, ref } from 'vue'
213
-
214
- const props = defineProps<{ config: any }>()
215
- const svgRef = ref<SVGSVGElement>()
216
-
217
- onMounted(() => {
218
- registerProjections({
219
- 'mercator': () => d3.geoMercator(),
220
- 'conic-conformal': () => d3.geoConicConformal()
221
- })
222
-
223
- const projection = loadCompositeProjection(props.config, {
224
- width: 800,
225
- height: 600
226
- })
227
-
228
- const path = d3.geoPath(projection)
229
- const svg = d3.select(svgRef.value)
230
-
231
- svg.selectAll('path')
232
- .data(features)
233
- .join('path')
234
- .attr('d', path)
235
- .attr('fill', 'lightgray')
236
- })
237
- </script>
238
-
239
- <template>
240
- <svg
241
- ref="svgRef"
242
- width="800"
243
- height="600"
244
- />
245
- </template>
246
- ```
247
-
248
- ### Svelte
249
-
250
- ```svelte
251
- <script lang="ts">
252
- import * as d3 from 'd3-geo'
253
- import { onMount } from 'svelte'
254
- import { loadCompositeProjection, registerProjections } from '@atlas-composer/projection-loader'
255
-
256
- export let config: any
257
-
258
- let svgElement: SVGSVGElement
259
-
260
- onMount(() => {
261
- registerProjections({
262
- 'mercator': () => d3.geoMercator(),
263
- 'conic-conformal': () => d3.geoConicConformal()
264
- })
265
-
266
- const projection = loadCompositeProjection(config, {
267
- width: 800,
268
- height: 600
269
- })
270
-
271
- const path = d3.geoPath(projection)
272
- const svg = d3.select(svgElement)
273
-
274
- svg.selectAll('path')
275
- .data(features)
276
- .join('path')
277
- .attr('d', path)
278
- .attr('fill', 'lightgray')
279
- })
280
- </script>
281
-
282
- <svg bind:this={svgElement} width="800" height="600" />
283
- ```
284
-
285
- ## Custom Projections
286
-
287
- You can register any projection factory, including custom implementations:
288
-
289
- ```typescript
290
- import { registerProjection } from '@atlas-composer/projection-loader'
291
-
292
- // Custom projection
293
- registerProjection('my-custom', () => {
294
- // Return a D3-compatible projection
295
- return {
296
- // Implement projection interface
297
- (coordinates) => [x, y],
298
- scale: (s?) => s ? (scale = s, this) : scale,
299
- translate: (t?) => t ? (translate = t, this) : translate,
300
- // ... other D3 projection methods
301
- }
302
- })
303
-
304
- // Proj4 wrapper
305
- import proj4 from 'proj4'
306
-
307
- registerProjection('lambert93', () => {
308
- const projection = proj4('EPSG:2154')
309
- return {
310
- (coords) => projection.forward(coords),
311
- scale: () => 1,
312
- translate: () => [0, 0]
313
- }
314
- })
315
- ```
316
-
317
- ## API Reference
318
-
319
- ### Core Functions
320
-
321
- #### `registerProjection(id: string, factory: ProjectionFactory): void`
322
-
323
- Register a projection factory with a given ID.
324
-
325
- ```typescript
326
- registerProjection('mercator', () => d3.geoMercator())
327
- ```
328
-
329
- #### `registerProjections(factories: Record<string, ProjectionFactory>): void`
330
-
331
- Register multiple projections at once.
332
-
333
- ```typescript
334
- registerProjections({
335
- mercator: () => d3.geoMercator(),
336
- albers: () => d3.geoAlbers()
337
- })
338
- ```
339
-
340
- #### `loadCompositeProjection(config: ExportedConfig, options: LoaderOptions): ProjectionLike`
341
-
342
- Load a composite projection from an exported configuration.
343
-
344
- ```typescript
345
- const projection = loadCompositeProjection(config, {
346
- width: 800,
347
- height: 600,
348
- enableClipping: true, // optional, default: true
349
- debug: false // optional, default: false
350
- })
351
- ```
352
-
353
- #### `loadFromJSON(jsonString: string, options: LoaderOptions): ProjectionLike`
354
-
355
- Load a composite projection from a JSON string.
356
-
357
- ```typescript
358
- const jsonString = fs.readFileSync('config.json', 'utf-8')
359
- const projection = loadFromJSON(jsonString, { width: 800, height: 600 })
360
- ```
361
-
362
- ### Utility Functions
363
-
364
- #### `getRegisteredProjections(): string[]`
365
-
366
- Get list of registered projection IDs.
367
-
368
- #### `isProjectionRegistered(id: string): boolean`
369
-
370
- Check if a projection is registered.
371
-
372
- #### `unregisterProjection(id: string): boolean`
373
-
374
- Remove a projection from the registry.
375
-
376
- #### `clearProjections(): void`
377
-
378
- Clear all registered projections.
379
-
380
- #### `validateConfig(config: any): boolean`
381
-
382
- Validate a configuration object. Throws descriptive errors if invalid.
383
-
384
- ## Configuration Format
385
-
386
- Exported configurations follow this structure:
387
-
388
- ```typescript
389
- interface ExportedConfig {
390
- version: '1.0'
391
- metadata: {
392
- atlasId: string
393
- atlasName: string
394
- }
395
- pattern: 'single-focus' | 'equal-members'
396
- referenceScale: number
397
- territories: Territory[]
398
- }
399
-
400
- interface Territory {
401
- code: string
402
- name: string
403
- role: 'primary' | 'secondary' | 'member'
404
- projectionId: string
405
- projectionFamily: string
406
- parameters: {
407
- center?: [number, number]
408
- rotate?: [number, number, number]
409
- parallels?: [number, number]
410
- scale: number
411
- baseScale: number
412
- scaleMultiplier: number
413
- }
414
- layout: {
415
- translateOffset: [number, number]
416
- clipExtent: [[number, number], [number, number]] | null
417
- }
418
- bounds: [[number, number], [number, number]]
419
- }
420
- ```
421
-
422
- ## Bundle Size
423
-
424
- | Approach | Bundle Size | Savings |
425
- |----------|-------------|---------|
426
- | All D3 projections | ~100KB | - |
427
- | With projection-loader | ~6KB | **94%** 🎉 |
428
-
429
- ## TypeScript Support
430
-
431
- Full TypeScript definitions are included. No need for `@types/*` packages.
432
-
433
- ```typescript
434
- import type {
435
- ExportedConfig,
436
- LoaderOptions,
437
- ProjectionFactory,
438
- ProjectionLike,
439
- Territory
440
- } from '@atlas-composer/projection-loader'
441
- ```
442
-
443
- ## Browser Support
444
-
445
- Works in all modern browsers that support ES2020+. For older browsers, transpile with your build tool.
446
-
447
- ## Contributing
448
-
449
- Contributions are welcome! This package is part of the [Atlas composer](https://github.com/ShallowRed/atlas-composer) monorepo.
450
-
451
- ## License
452
-
453
- MIT © 2025 Lucas Poulain
454
-
455
- ## Related
49
+ ## 📖 Documentation
456
50
 
457
- - [Atlas composer](https://github.com/ShallowRed/atlas-composer) - Create custom composite projections
458
- - [D3.js](https://d3js.org/) - Data visualization library
459
- - [Observable Plot](https://observablehq.com/plot/) - High-level plotting library
51
+ For more context on the ecosystem, see the [root architecture documentation](../../docs/architecture.md).
@@ -1,4 +1,5 @@
1
1
  import { ProjectionFactory } from './index.js';
2
+ import '@atlas-composer/specification';
2
3
 
3
4
  /**
4
5
  * D3 Projection Helpers
package/dist/index.d.ts CHANGED
@@ -1,11 +1,14 @@
1
+ import { CompositeProjectionConfig, LayoutConfig, ProjectionParameters as ProjectionParameters$1, TerritoryConfig } from '@atlas-composer/specification';
2
+
1
3
  /**
2
4
  * Standalone Composite Projection Loader (Zero Dependencies)
3
5
  *
4
6
  * A pure JavaScript/TypeScript module that consumes exported composite projection
5
7
  * configurations and creates D3-compatible projections using a plugin architecture.
6
8
  *
7
- * This package has ZERO dependencies. Users must register projection factories
8
- * before loading configurations.
9
+ * This package uses @atlas-composer/projection-core for the shared composite
10
+ * projection building logic. Users must register projection factories before
11
+ * loading configurations.
9
12
  *
10
13
  * @example
11
14
  * ```typescript
@@ -22,6 +25,7 @@
22
25
  *
23
26
  * @packageDocumentation
24
27
  */
28
+
25
29
  /**
26
30
  * Generic projection-like interface that matches D3 projections
27
31
  * without requiring d3-geo as a dependency
@@ -84,56 +88,30 @@ interface StreamLike {
84
88
  * Factory function that creates a projection instance
85
89
  */
86
90
  type ProjectionFactory = () => ProjectionLike;
91
+
87
92
  /**
88
- * Exported configuration format (subset needed for loading)
93
+ * Projection parameters with loader-specific extensions.
94
+ * @deprecated Specification now includes all legacy parameters
89
95
  */
90
- interface ExportedConfig {
91
- version: string;
92
- metadata: {
93
- atlasId: string;
94
- atlasName: string;
95
- exportDate?: string;
96
- createdWith?: string;
97
- notes?: string;
98
- };
99
- pattern: string;
100
- referenceScale?: number;
101
- canvasDimensions?: {
102
- width: number;
103
- height: number;
104
- };
105
- territories: Territory[];
106
- }
107
- interface Territory {
108
- code: string;
109
- name: string;
110
- role: string;
111
- projectionId?: string;
112
- projectionFamily?: string;
113
- projection?: {
114
- id: string;
115
- family: string;
116
- parameters: ProjectionParameters;
117
- };
118
- parameters?: ProjectionParameters;
119
- layout: Layout;
120
- bounds: [[number, number], [number, number]];
121
- }
122
- interface ProjectionParameters {
123
- center?: [number, number];
124
- rotate?: [number, number, number];
125
- scale?: number;
126
- baseScale?: number;
127
- scaleMultiplier?: number;
128
- parallels?: [number, number];
129
- translate?: [number, number];
130
- clipAngle?: number;
131
- precision?: number;
132
- }
133
- interface Layout {
134
- translateOffset?: [number, number];
135
- clipExtent?: [[number, number], [number, number]] | null;
136
- }
96
+ type ProjectionParameters = ProjectionParameters$1;
97
+ /**
98
+ * Exported configuration format.
99
+ * Alias for CompositeProjectionConfig for backward compatibility.
100
+ * @deprecated Use CompositeProjectionConfig from @atlas-composer/specification
101
+ */
102
+ type ExportedConfig = CompositeProjectionConfig;
103
+ /**
104
+ * Territory configuration.
105
+ * Alias for TerritoryConfig for backward compatibility.
106
+ * @deprecated Use TerritoryConfig from @atlas-composer/specification
107
+ */
108
+ type Territory = TerritoryConfig;
109
+ /**
110
+ * Layout configuration.
111
+ * Alias for LayoutConfig for backward compatibility.
112
+ * @deprecated Use LayoutConfig from @atlas-composer/specification
113
+ */
114
+ type Layout = LayoutConfig;
137
115
  /**
138
116
  * Options for creating the composite projection
139
117
  */
package/dist/index.js CHANGED
@@ -1,4 +1,12 @@
1
+ import { buildCompositeProjection, calculateClipExtentFromPixelOffset } from '@atlas-composer/projection-core';
2
+
1
3
  // src/standalone-projection-loader.ts
4
+ function resolveI18nString(value) {
5
+ if (typeof value === "string") {
6
+ return value;
7
+ }
8
+ return value.en || Object.values(value).find((v) => typeof v === "string") || "";
9
+ }
2
10
  var projectionRegistry = /* @__PURE__ */ new Map();
3
11
  function registerProjection(id, factory) {
4
12
  projectionRegistry.set(id, factory);
@@ -28,152 +36,35 @@ function loadCompositeProjection(config, options) {
28
36
  if (!config.territories || config.territories.length === 0) {
29
37
  throw new Error("Configuration must contain at least one territory");
30
38
  }
31
- const subProjections = config.territories.map((territory) => {
39
+ const entries = config.territories.map((territory) => {
32
40
  const proj = createSubProjection(territory, width, height, config.referenceScale, debug);
33
41
  return {
34
- territory,
42
+ id: territory.code,
43
+ name: resolveI18nString(territory.name),
35
44
  projection: proj,
36
- bounds: territory.bounds
45
+ bounds: {
46
+ minLon: territory.bounds[0][0],
47
+ minLat: territory.bounds[0][1],
48
+ maxLon: territory.bounds[1][0],
49
+ maxLat: territory.bounds[1][1]
50
+ }
37
51
  };
38
52
  });
39
53
  if (debug) {
40
54
  console.log("[CompositeProjection] Created sub-projections:", {
41
- territories: config.territories.map((t) => ({ code: t.code, name: t.name })),
42
- count: subProjections.length
55
+ territories: config.territories.map((t) => ({ code: t.code, name: resolveI18nString(t.name) })),
56
+ count: entries.length
43
57
  });
44
58
  }
45
- let capturedPoint = null;
46
- const pointStream = {
47
- point: (x, y) => {
48
- capturedPoint = [x, y];
49
- },
50
- lineStart: () => {
51
- },
52
- lineEnd: () => {
53
- },
54
- polygonStart: () => {
55
- },
56
- polygonEnd: () => {
57
- },
58
- sphere: () => {
59
- }
60
- };
61
- const subProjPoints = subProjections.map((subProj) => ({
62
- subProj: {
63
- territoryCode: subProj.territory.code,
64
- territoryName: subProj.territory.name,
65
- bounds: subProj.bounds,
66
- projection: subProj.projection
67
- },
68
- stream: subProj.projection.stream(pointStream)
69
- }));
70
- const compositeProjection = (coordinates) => {
71
- const [lon, lat] = coordinates;
72
- capturedPoint = null;
73
- for (const { subProj, stream } of subProjPoints) {
74
- if (subProj.bounds) {
75
- const [[minLon, minLat], [maxLon, maxLat]] = subProj.bounds;
76
- if (lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat) {
77
- stream.point(lon, lat);
78
- if (capturedPoint) {
79
- return capturedPoint;
80
- }
81
- }
82
- }
83
- }
84
- return null;
85
- };
86
- compositeProjection.stream = (stream) => {
87
- const streams = subProjections.map((sp) => sp.projection.stream(stream));
88
- return {
89
- point: (x, y) => {
90
- for (const s of streams) s.point(x, y);
91
- },
92
- sphere: () => {
93
- for (const s of streams) {
94
- if (s.sphere)
95
- s.sphere();
96
- }
97
- },
98
- lineStart: () => {
99
- for (const s of streams) s.lineStart();
100
- },
101
- lineEnd: () => {
102
- for (const s of streams) s.lineEnd();
103
- },
104
- polygonStart: () => {
105
- for (const s of streams) s.polygonStart();
106
- },
107
- polygonEnd: () => {
108
- for (const s of streams) s.polygonEnd();
109
- }
110
- };
111
- };
112
- compositeProjection.invert = (coordinates) => {
113
- if (!coordinates || !Array.isArray(coordinates) || coordinates.length < 2) {
114
- return null;
115
- }
116
- const [x, y] = coordinates;
117
- for (const { projection } of subProjections) {
118
- if (projection.invert) {
119
- try {
120
- const result = projection.invert([x, y]);
121
- if (result && Array.isArray(result) && result.length >= 2) {
122
- return result;
123
- }
124
- } catch (error) {
125
- if (debug) {
126
- console.warn("[Invert] Error in sub-projection invert:", error);
127
- }
128
- }
129
- }
130
- }
131
- return null;
132
- };
133
- compositeProjection.scale = function(_s) {
134
- var _a, _b;
135
- if (arguments.length === 0) {
136
- return subProjections[0] ? ((_b = (_a = subProjections[0].projection).scale) == null ? void 0 : _b.call(_a)) || 1 : 1;
137
- }
138
- return compositeProjection;
139
- };
140
- compositeProjection.translate = function(_t) {
141
- if (arguments.length === 0) {
142
- return [width / 2, height / 2];
143
- }
144
- return compositeProjection;
145
- };
146
- return compositeProjection;
147
- }
148
- function inferProjectionIdFromFamily(family, parameters) {
149
- switch (family.toUpperCase()) {
150
- case "CYLINDRICAL":
151
- return "mercator";
152
- // Most common cylindrical projection
153
- case "CONIC":
154
- return parameters.parallels ? "conic-conformal" : "conic-equal-area";
155
- case "AZIMUTHAL":
156
- return "azimuthal-equal-area";
157
- default:
158
- console.warn(`Unknown projection family: ${family}, falling back to mercator`);
159
- return "mercator";
160
- }
59
+ const composite = buildCompositeProjection({ entries, debug });
60
+ return composite;
161
61
  }
162
62
  function createSubProjection(territory, width, height, referenceScale, debug) {
163
- var _a, _b;
164
- let projectionId;
165
- let parameters;
63
+ var _a, _b, _c;
166
64
  const { layout } = territory;
167
- if (territory.projection) {
168
- projectionId = territory.projection.id;
169
- parameters = territory.projection.parameters;
170
- } else if (territory.projectionId && territory.parameters) {
171
- projectionId = territory.projectionId;
172
- parameters = territory.parameters;
173
- } else if (territory.projectionFamily && territory.parameters) {
174
- projectionId = inferProjectionIdFromFamily(territory.projectionFamily, territory.parameters);
175
- parameters = territory.parameters;
176
- } else {
65
+ const projectionId = territory.projection.id;
66
+ const parameters = territory.projection.parameters;
67
+ if (!projectionId || !parameters) {
177
68
  throw new Error(`Territory ${territory.code} missing projection configuration`);
178
69
  }
179
70
  const factory = projectionRegistry.get(projectionId);
@@ -185,10 +76,16 @@ function createSubProjection(territory, width, height, referenceScale, debug) {
185
76
  );
186
77
  }
187
78
  const projection = factory();
188
- if (parameters.center && projection.center) {
79
+ const hasFocus = parameters.focusLongitude !== void 0 && parameters.focusLatitude !== void 0;
80
+ const projFamily = territory.projection.family;
81
+ if (hasFocus && projFamily === "CONIC" && projection.rotate) {
82
+ projection.rotate([-parameters.focusLongitude, -parameters.focusLatitude, 0]);
83
+ } else if (hasFocus && projection.center) {
84
+ projection.center([parameters.focusLongitude, parameters.focusLatitude]);
85
+ } else if (parameters.center && projection.center) {
189
86
  projection.center(parameters.center);
190
87
  }
191
- if (parameters.rotate && projection.rotate) {
88
+ if (parameters.rotate && projection.rotate && !hasFocus) {
192
89
  const rotate = Array.isArray(parameters.rotate) ? [...parameters.rotate, 0, 0].slice(0, 3) : [0, 0, 0];
193
90
  projection.rotate(rotate);
194
91
  }
@@ -196,14 +93,10 @@ function createSubProjection(territory, width, height, referenceScale, debug) {
196
93
  const parallels = Array.isArray(parameters.parallels) ? [...parameters.parallels, 0].slice(0, 2) : [0, 60];
197
94
  projection.parallels(parallels);
198
95
  }
199
- if (projection.scale) {
200
- if (parameters.scale) {
201
- projection.scale(parameters.scale);
202
- } else if (parameters.scaleMultiplier) {
203
- const effectiveReferenceScale = referenceScale || 2700;
204
- const calculatedScale = effectiveReferenceScale * parameters.scaleMultiplier;
205
- projection.scale(calculatedScale);
206
- }
96
+ if (projection.scale && parameters.scaleMultiplier) {
97
+ const effectiveReferenceScale = referenceScale || 2700;
98
+ const calculatedScale = effectiveReferenceScale * parameters.scaleMultiplier;
99
+ projection.scale(calculatedScale);
207
100
  }
208
101
  if (parameters.clipAngle && projection.clipAngle) {
209
102
  projection.clipAngle(parameters.clipAngle);
@@ -218,36 +111,24 @@ function createSubProjection(territory, width, height, referenceScale, debug) {
218
111
  height / 2 + offsetY
219
112
  ]);
220
113
  }
221
- if (parameters.translate && projection.translate) {
222
- const currentTranslate = projection.translate();
223
- const [additionalX, additionalY] = parameters.translate;
224
- projection.translate([
225
- currentTranslate[0] + additionalX,
226
- currentTranslate[1] + additionalY
227
- ]);
228
- }
229
- if (layout.clipExtent && projection.clipExtent) {
230
- const effectiveReferenceScale = referenceScale || 2700;
231
- const centerX = width / 2;
232
- const centerY = height / 2;
233
- const [[x1, y1], [x2, y2]] = layout.clipExtent;
234
- const epsilon = 1e-6;
235
- const transformedClipExtent = [
236
- [centerX + x1 * effectiveReferenceScale + epsilon, centerY + y1 * effectiveReferenceScale + epsilon],
237
- [centerX + x2 * effectiveReferenceScale - epsilon, centerY + y2 * effectiveReferenceScale - epsilon]
238
- ];
239
- projection.clipExtent(transformedClipExtent);
114
+ if (layout.pixelClipExtent && projection.clipExtent) {
115
+ const territoryCenter = ((_a = projection.translate) == null ? void 0 : _a.call(projection)) || [width / 2, height / 2];
116
+ const clipExtent = calculateClipExtentFromPixelOffset(
117
+ territoryCenter,
118
+ layout.pixelClipExtent
119
+ );
120
+ projection.clipExtent(clipExtent);
240
121
  if (debug) {
241
122
  console.log(
242
- `[Clipping] Applied Atlas Composer-style clip extent for ${territory.code}:`,
243
- `original: ${JSON.stringify(layout.clipExtent)} -> transformed: ${JSON.stringify(transformedClipExtent)}`
123
+ `[Clipping] Applied pixelClipExtent for ${territory.code}:`,
124
+ `original: ${JSON.stringify(layout.pixelClipExtent)} -> transformed: ${JSON.stringify(clipExtent)}`
244
125
  );
245
126
  }
246
127
  } else if (projection.clipExtent) {
247
128
  const bounds = territory.bounds;
248
129
  if (bounds && bounds.length === 2 && bounds[0].length === 2 && bounds[1].length === 2) {
249
- const scale = ((_a = projection.scale) == null ? void 0 : _a.call(projection)) || 1;
250
- const translate = ((_b = projection.translate) == null ? void 0 : _b.call(projection)) || [0, 0];
130
+ const scale = ((_b = projection.scale) == null ? void 0 : _b.call(projection)) || 1;
131
+ const translate = ((_c = projection.translate) == null ? void 0 : _c.call(projection)) || [0, 0];
251
132
  const padding = scale * 0.1;
252
133
  const clipExtent = [
253
134
  [translate[0] - padding, translate[1] - padding],
@@ -281,11 +162,8 @@ function validateConfig(config) {
281
162
  if (!territory.code) {
282
163
  throw new Error(`Territory missing required field 'code': ${JSON.stringify(territory)}`);
283
164
  }
284
- const hasLegacyFormat = territory.projectionId && territory.parameters;
285
- const hasNewFormat = territory.projection && territory.projection.id && territory.projection.parameters;
286
- const hasIncompleteFormat = territory.projectionFamily && territory.parameters;
287
- if (!hasLegacyFormat && !hasNewFormat && !hasIncompleteFormat) {
288
- throw new Error(`Territory ${territory.code} missing projection configuration. Available fields: ${Object.keys(territory).join(", ")}`);
165
+ if (!territory.projection || !territory.projection.id || !territory.projection.parameters) {
166
+ throw new Error(`Territory ${territory.code} missing projection configuration. Required: projection.id and projection.parameters`);
289
167
  }
290
168
  if (!territory.bounds) {
291
169
  throw new Error(`Territory ${territory.code} missing bounds`);
package/dist/index.js.map CHANGED
@@ -1 +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 clipAngle?: {\n (): number\n (angle: number): 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 exportDate?: string\n createdWith?: string\n notes?: string\n }\n pattern: string\n referenceScale?: number\n canvasDimensions?: {\n width: number\n height: number\n }\n territories: Territory[]\n}\n\nexport interface Territory {\n code: string\n name: string\n role: string\n // Support multiple formats\n projectionId?: string // Legacy format\n projectionFamily?: string // Migration script format\n projection?: {\n id: string\n family: string\n parameters: ProjectionParameters\n } // New format\n parameters?: ProjectionParameters // Used in legacy and migration formats\n layout: Layout\n bounds: [[number, number], [number, number]]\n}\n\nexport interface ProjectionParameters {\n center?: [number, number]\n rotate?: [number, number, number]\n // Legacy format support\n scale?: number\n baseScale?: number\n scaleMultiplier?: number\n parallels?: [number, number]\n // Additional parameters from new format\n translate?: [number, number]\n clipAngle?: number\n precision?: number\n}\n\nexport interface Layout {\n translateOffset?: [number, number] // Make optional for migration script format\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 */\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, config.referenceScale, debug)\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 // Point capture mechanism (like Atlas Composer's CompositeProjection)\n let capturedPoint: [number, number] | null = null\n const pointStream = {\n point: (x: number, y: number) => {\n capturedPoint = [x, y]\n },\n lineStart: () => {},\n lineEnd: () => {},\n polygonStart: () => {},\n polygonEnd: () => {},\n sphere: () => {},\n }\n\n // Create point capture for each sub-projection\n const subProjPoints = subProjections.map(subProj => ({\n subProj: {\n territoryCode: subProj.territory.code,\n territoryName: subProj.territory.name,\n bounds: subProj.bounds,\n projection: subProj.projection,\n },\n stream: subProj.projection.stream(pointStream),\n }))\n\n // Main projection function (like Atlas Composer's CompositeProjection)\n const compositeProjection = (coordinates: [number, number]): [number, number] | null => {\n const [lon, lat] = coordinates\n\n capturedPoint = null\n\n // Try each sub-projection's bounds\n for (const { subProj, stream } of subProjPoints) {\n if (subProj.bounds) {\n const [[minLon, minLat], [maxLon, maxLat]] = subProj.bounds\n\n if (lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat) {\n // Project through stream (offset already applied in projection.translate)\n stream.point(lon, lat)\n\n if (capturedPoint) {\n return capturedPoint\n }\n }\n }\n }\n\n // No match found\n return null\n }\n\n // Multiplex stream (like Atlas Composer's CompositeProjection)\n ;(compositeProjection as any).stream = (stream: any) => {\n const streams = subProjections.map(sp => sp.projection.stream(stream))\n return {\n point: (x: number, y: number) => {\n for (const s of streams) s.point(x, y)\n },\n sphere: () => {\n for (const s of streams) {\n if (s.sphere)\n s.sphere()\n }\n },\n lineStart: () => {\n for (const s of streams) s.lineStart()\n },\n lineEnd: () => {\n for (const s of streams) s.lineEnd()\n },\n polygonStart: () => {\n for (const s of streams) s.polygonStart()\n },\n polygonEnd: () => {\n for (const s of streams) s.polygonEnd()\n },\n }\n }\n\n // Add invert method (like Atlas Composer's CompositeProjection)\n ;(compositeProjection as any).invert = (coordinates: [number, number]) => {\n if (!coordinates || !Array.isArray(coordinates) || coordinates.length < 2) {\n return null\n }\n\n const [x, y] = coordinates\n\n // Try each sub-projection's invert\n for (const { projection } of subProjections) {\n if ((projection as any).invert) {\n try {\n const result = (projection as any).invert([x, y])\n if (result && Array.isArray(result) && result.length >= 2) {\n return result as [number, number]\n }\n }\n catch (error) {\n // Continue to next projection\n if (debug) {\n console.warn('[Invert] Error in sub-projection invert:', error)\n }\n }\n }\n }\n\n return null\n }\n\n // Add scale and translate methods (like Atlas Composer's CompositeProjection)\n ;(compositeProjection as any).scale = function (_s?: number): any {\n if (arguments.length === 0) {\n // Return scale from first sub-projection as reference\n return subProjections[0] ? subProjections[0].projection.scale?.() || 1 : 1\n }\n // Setting scale - not typically used for composite projections\n // Individual territories manage their own scales\n return compositeProjection\n }\n\n ;(compositeProjection as any).translate = function (_t?: [number, number]): any {\n if (arguments.length === 0) {\n // Return center point as reference\n return [width / 2, height / 2]\n }\n // Setting translate - not typically used for composite projections\n // Individual territories manage their own translations\n return compositeProjection\n }\n\n return compositeProjection as ProjectionLike\n}\n\n/**\n * Infer projection ID from family and parameters (for migration script format)\n */\nfunction inferProjectionIdFromFamily(family: string, parameters: ProjectionParameters): string {\n // Common projection mappings based on family and parameters\n switch (family.toUpperCase()) {\n case 'CYLINDRICAL':\n return 'mercator' // Most common cylindrical projection\n case 'CONIC':\n return parameters.parallels ? 'conic-conformal' : 'conic-equal-area'\n case 'AZIMUTHAL':\n return 'azimuthal-equal-area'\n default:\n // Fallback to mercator if we can't determine\n console.warn(`Unknown projection family: ${family}, falling back to mercator`)\n return 'mercator'\n }\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 referenceScale?: number,\n debug?: boolean,\n): ProjectionLike {\n // Extract projection info and parameters from multiple formats\n let projectionId: string\n let parameters: ProjectionParameters\n const { layout } = territory\n\n if (territory.projection) {\n // New format: nested projection object\n projectionId = territory.projection.id\n parameters = territory.projection.parameters\n }\n else if (territory.projectionId && territory.parameters) {\n // Legacy format: direct properties\n projectionId = territory.projectionId\n parameters = territory.parameters\n }\n else if (territory.projectionFamily && territory.parameters) {\n // Migration script format: has projectionFamily but missing projectionId\n // Try to infer projection ID from family and parameters\n projectionId = inferProjectionIdFromFamily(territory.projectionFamily as string, territory.parameters)\n parameters = territory.parameters\n }\n else {\n throw new Error(`Territory ${territory.code} missing projection configuration`)\n }\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 // Ensure rotate has exactly 3 elements\n const rotate = Array.isArray(parameters.rotate)\n ? [...parameters.rotate, 0, 0].slice(0, 3) as [number, number, number]\n : [0, 0, 0] as [number, number, number]\n projection.rotate(rotate)\n }\n\n if (parameters.parallels && projection.parallels) {\n // Ensure parallels has exactly 2 elements\n const parallels = Array.isArray(parameters.parallels)\n ? [...parameters.parallels, 0].slice(0, 2) as [number, number]\n : [0, 60] as [number, number]\n projection.parallels(parallels)\n }\n\n // Handle scale - support both legacy (scale) and new (scaleMultiplier + referenceScale) formats\n if (projection.scale) {\n if (parameters.scale) {\n // Legacy format: use direct scale value\n projection.scale(parameters.scale)\n }\n else if (parameters.scaleMultiplier) {\n // New format: calculate scale from reference scale and multiplier\n const effectiveReferenceScale = referenceScale || 2700 // Default reference scale\n const calculatedScale = effectiveReferenceScale * parameters.scaleMultiplier\n projection.scale(calculatedScale)\n }\n }\n\n // Apply additional parameters\n if (parameters.clipAngle && projection.clipAngle) {\n projection.clipAngle(parameters.clipAngle)\n }\n\n if (parameters.precision && projection.precision) {\n projection.precision(parameters.precision)\n }\n\n // Apply layout translate\n if (projection.translate) {\n const [offsetX, offsetY] = layout.translateOffset || [0, 0] // Default to center if missing\n projection.translate([\n width / 2 + offsetX,\n height / 2 + offsetY,\n ])\n }\n\n // Apply parameter-level translate (additional adjustment)\n if (parameters.translate && projection.translate) {\n const currentTranslate = projection.translate()\n const [additionalX, additionalY] = parameters.translate\n projection.translate([\n currentTranslate[0] + additionalX,\n currentTranslate[1] + additionalY,\n ])\n }\n\n // Apply clipping - this is CRITICAL for composite projections\n // Each sub-projection MUST have clipping to avoid geometry processing conflicts\n if (layout.clipExtent && projection.clipExtent) {\n // Like Atlas Composer: clipExtent is relative to the main projection coordinate system\n // Get the reference scale (use same as Atlas Composer does)\n const effectiveReferenceScale = referenceScale || 2700\n const centerX = width / 2\n const centerY = height / 2\n \n // Apply Atlas Composer's approach: clipExtent values are multiplied by reference scale\n // and positioned relative to map center, with small epsilon for padding\n const [[x1, y1], [x2, y2]] = layout.clipExtent\n const epsilon = 1e-6 // Small value for padding, matching d3-composite-projections\n \n const transformedClipExtent: [[number, number], [number, number]] = [\n [centerX + x1 * effectiveReferenceScale + epsilon, centerY + y1 * effectiveReferenceScale + epsilon],\n [centerX + x2 * effectiveReferenceScale - epsilon, centerY + y2 * effectiveReferenceScale - epsilon],\n ]\n \n projection.clipExtent(transformedClipExtent)\n if (debug) {\n console.log(`[Clipping] Applied Atlas Composer-style clip extent for ${territory.code}:`, \n `original: ${JSON.stringify(layout.clipExtent)} -> transformed: ${JSON.stringify(transformedClipExtent)}`)\n }\n }\n else if (projection.clipExtent) {\n // If no clip extent is specified, create a default one based on territory bounds\n // This prevents the projection from processing geometry outside its area\n const bounds = territory.bounds\n if (bounds && bounds.length === 2 && bounds[0].length === 2 && bounds[1].length === 2) {\n // Convert geographic bounds to pixel bounds (approximate)\n const scale = projection.scale?.() || 1\n const translate = projection.translate?.() || [0, 0]\n\n // Create a reasonable clip extent based on the geographic bounds\n // This is a simplified approach - in practice, you'd want more precise clipping\n const padding = scale * 0.1 // 10% padding\n const clipExtent: [[number, number], [number, number]] = [\n [translate[0] - padding, translate[1] - padding],\n [translate[0] + padding, translate[1] + padding],\n ]\n\n projection.clipExtent(clipExtent)\n\n if (debug) {\n console.log(`[Clipping] Applied default clip extent for ${territory.code}:`, clipExtent)\n }\n }\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) {\n throw new Error(`Territory missing required field 'code': ${JSON.stringify(territory)}`)\n }\n\n // Check for projection info in either format\n const hasLegacyFormat = territory.projectionId && territory.parameters\n const hasNewFormat = territory.projection && territory.projection.id && territory.projection.parameters\n const hasIncompleteFormat = territory.projectionFamily && territory.parameters // Migration script format\n\n if (!hasLegacyFormat && !hasNewFormat && !hasIncompleteFormat) {\n throw new Error(`Territory ${territory.code} missing projection configuration. Available fields: ${Object.keys(territory).join(', ')}`)\n }\n\n if (!territory.bounds) {\n throw new Error(`Territory ${territory.code} missing 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"]}
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 uses @atlas-composer/projection-core for the shared composite\n * projection building logic. Users must register projection factories before\n * 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\nimport type { ProjectionLike as CoreProjectionLike, SubProjectionEntry } from '@atlas-composer/projection-core'\nimport type {\n CompositeProjectionConfig,\n GeoBounds,\n I18nString,\n LayoutConfig,\n ProjectionParameters as SpecProjectionParameters,\n TerritoryConfig,\n} from '@atlas-composer/specification'\n\nimport {\n buildCompositeProjection,\n calculateClipExtentFromPixelOffset,\n} from '@atlas-composer/projection-core'\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 clipAngle?: {\n (): number\n (angle: number): 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// Re-export specification types for convenience\nexport type { CompositeProjectionConfig, GeoBounds, I18nString, LayoutConfig, TerritoryConfig }\n\n/**\n * Resolve an I18nString to a plain string.\n * For i18n objects, returns the English value or the first available translation.\n */\nfunction resolveI18nString(value: I18nString): string {\n if (typeof value === 'string') {\n return value\n }\n // Return English first, or the first available language\n return value.en || Object.values(value).find(v => typeof v === 'string') || ''\n}\n\n/**\n * Projection parameters with loader-specific extensions.\n * @deprecated Specification now includes all legacy parameters\n */\nexport type ProjectionParameters = SpecProjectionParameters\n\n/**\n * Exported configuration format.\n * Alias for CompositeProjectionConfig for backward compatibility.\n * @deprecated Use CompositeProjectionConfig from @atlas-composer/specification\n */\nexport type ExportedConfig = CompositeProjectionConfig\n\n/**\n * Territory configuration.\n * Alias for TerritoryConfig for backward compatibility.\n * @deprecated Use TerritoryConfig from @atlas-composer/specification\n */\nexport type Territory = TerritoryConfig\n\n/**\n * Layout configuration.\n * Alias for LayoutConfig for backward compatibility.\n * @deprecated Use LayoutConfig from @atlas-composer/specification\n */\nexport type Layout = LayoutConfig\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 */\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 and convert to SubProjectionEntry format\n const entries: SubProjectionEntry[] = config.territories.map((territory) => {\n const proj = createSubProjection(territory, width, height, config.referenceScale, debug)\n\n return {\n id: territory.code,\n name: resolveI18nString(territory.name),\n projection: proj as CoreProjectionLike,\n bounds: {\n minLon: territory.bounds[0][0],\n minLat: territory.bounds[0][1],\n maxLon: territory.bounds[1][0],\n maxLat: territory.bounds[1][1],\n },\n }\n })\n\n if (debug) {\n console.log('[CompositeProjection] Created sub-projections:', {\n territories: config.territories.map(t => ({ code: t.code, name: resolveI18nString(t.name) })),\n count: entries.length,\n })\n }\n\n // Use projection-core to build the composite projection\n const composite = buildCompositeProjection({ entries, debug })\n\n return composite as ProjectionLike\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 referenceScale?: number,\n debug?: boolean,\n): ProjectionLike {\n // Extract projection info and parameters from nested projection object\n const { layout } = territory\n const projectionId = territory.projection.id\n const parameters = territory.projection.parameters\n\n if (!projectionId || !parameters) {\n throw new Error(`Territory ${territory.code} missing projection configuration`)\n }\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 // Convert legacy focusLongitude/focusLatitude to center/rotate\n const hasFocus = parameters.focusLongitude !== undefined && parameters.focusLatitude !== undefined\n const projFamily = territory.projection.family\n\n // Apply parameters based on projection family\n if (hasFocus && projFamily === 'CONIC' && projection.rotate) {\n // Conic projections use rotate: [-focusLon, -focusLat, 0]\n projection.rotate([-parameters.focusLongitude!, -parameters.focusLatitude!, 0])\n }\n else if (hasFocus && projection.center) {\n // Other projections use center: [focusLon, focusLat]\n projection.center([parameters.focusLongitude!, parameters.focusLatitude!])\n }\n else if (parameters.center && projection.center) {\n // Standard center format\n projection.center(parameters.center)\n }\n\n if (parameters.rotate && projection.rotate && !hasFocus) {\n // Only apply rotate if not already set by focusLongitude/focusLatitude\n // Ensure rotate has exactly 3 elements\n const rotate = Array.isArray(parameters.rotate)\n ? [...parameters.rotate, 0, 0].slice(0, 3) as [number, number, number]\n : [0, 0, 0] as [number, number, number]\n projection.rotate(rotate)\n }\n\n if (parameters.parallels && projection.parallels) {\n // Ensure parallels has exactly 2 elements\n const parallels = Array.isArray(parameters.parallels)\n ? [...parameters.parallels, 0].slice(0, 2) as [number, number]\n : [0, 60] as [number, number]\n projection.parallels(parallels)\n }\n\n // Handle scale using scaleMultiplier and referenceScale\n if (projection.scale && parameters.scaleMultiplier) {\n const effectiveReferenceScale = referenceScale || 2700 // Default reference scale\n const calculatedScale = effectiveReferenceScale * parameters.scaleMultiplier\n projection.scale(calculatedScale)\n }\n\n // Apply additional parameters\n if (parameters.clipAngle && projection.clipAngle) {\n projection.clipAngle(parameters.clipAngle)\n }\n\n if (parameters.precision && projection.precision) {\n projection.precision(parameters.precision)\n }\n\n // Apply layout translate\n if (projection.translate) {\n const [offsetX, offsetY] = layout.translateOffset || [0, 0] // Default to center if missing\n projection.translate([\n width / 2 + offsetX,\n height / 2 + offsetY,\n ])\n }\n\n // Apply clipping - this is CRITICAL for composite projections\n // Each sub-projection MUST have clipping to avoid geometry processing conflicts\n if (layout.pixelClipExtent && projection.clipExtent) {\n // Get territory center from translate\n const territoryCenter = projection.translate?.() || [width / 2, height / 2]\n\n // Use core utility for clip extent calculation\n const clipExtent = calculateClipExtentFromPixelOffset(\n territoryCenter,\n layout.pixelClipExtent,\n )\n\n projection.clipExtent(clipExtent)\n if (debug) {\n console.log(\n `[Clipping] Applied pixelClipExtent for ${territory.code}:`,\n `original: ${JSON.stringify(layout.pixelClipExtent)} -> transformed: ${JSON.stringify(clipExtent)}`,\n )\n }\n }\n else if (projection.clipExtent) {\n // If no clip extent is specified, create a default one based on territory bounds\n const bounds = territory.bounds\n if (bounds && bounds.length === 2 && bounds[0].length === 2 && bounds[1].length === 2) {\n // Convert geographic bounds to pixel bounds (approximate)\n const scale = projection.scale?.() || 1\n const translate = projection.translate?.() || [0, 0]\n\n // Create a reasonable clip extent based on the geographic bounds\n const padding = scale * 0.1 // 10% padding\n const clipExtent: [[number, number], [number, number]] = [\n [translate[0] - padding, translate[1] - padding],\n [translate[0] + padding, translate[1] + padding],\n ]\n\n projection.clipExtent(clipExtent)\n\n if (debug) {\n console.log(`[Clipping] Applied default clip extent for ${territory.code}:`, clipExtent)\n }\n }\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) {\n throw new Error(`Territory missing required field 'code': ${JSON.stringify(territory)}`)\n }\n\n // Check for required nested projection format\n if (!territory.projection || !territory.projection.id || !territory.projection.parameters) {\n throw new Error(`Territory ${territory.code} missing projection configuration. Required: projection.id and projection.parameters`)\n }\n\n if (!territory.bounds) {\n throw new Error(`Territory ${territory.code} missing 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@atlas-composer/projection-loader",
3
3
  "type": "module",
4
- "version": "1.1.0-rc.9",
4
+ "version": "1.1.0",
5
5
  "description": "Zero-dependency standalone loader for composite map projections with plugin architecture. Supports Atlas composer 2.0+ presets with backward compatibility.",
6
6
  "author": "Lucas Poulain (ShallowRed)",
7
7
  "license": "MIT",
@@ -28,6 +28,9 @@
28
28
  "zero-dependencies"
29
29
  ],
30
30
  "sideEffects": false,
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
31
34
  "exports": {
32
35
  ".": {
33
36
  "types": "./dist/index.d.ts",
@@ -72,6 +75,10 @@
72
75
  "optional": true
73
76
  }
74
77
  },
78
+ "dependencies": {
79
+ "@atlas-composer/projection-core": "1.0.1",
80
+ "@atlas-composer/specification": "1.0.1"
81
+ },
75
82
  "devDependencies": {
76
83
  "@size-limit/preset-small-lib": "^11.2.0",
77
84
  "@types/d3-geo": "^3.1.0",