@atlas-composer/projection-loader 1.1.0-rc.9 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -428
- package/dist/d3-projection-helpers.d.ts +1 -0
- package/dist/index.d.ts +28 -50
- package/dist/index.js +220 -169
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,459 +1,51 @@
|
|
|
1
1
|
# @atlas-composer/projection-loader
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> The runtime engine for Atlas Composer maps.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
7
|
+
## ✨ Features
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
+
### 1. Install
|
|
19
16
|
|
|
20
17
|
```bash
|
|
21
|
-
npm install @atlas-composer/projection-loader d3-geo
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
39
|
+
// 3. Render using D3
|
|
47
40
|
const path = d3.geoPath(projection)
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
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
|
|
8
|
-
*
|
|
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
|
-
*
|
|
93
|
+
* Projection parameters with loader-specific extensions.
|
|
94
|
+
* @deprecated Specification now includes all legacy parameters
|
|
89
95
|
*/
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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,100 +1,76 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
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();
|
|
1
|
+
// ../projection-core/dist/index.js
|
|
2
|
+
function isPointInBounds(lon, lat, bounds, tolerance = 0) {
|
|
3
|
+
return lon >= bounds.minLon - tolerance && lon <= bounds.maxLon + tolerance && lat >= bounds.minLat - tolerance && lat <= bounds.maxLat + tolerance;
|
|
16
4
|
}
|
|
17
|
-
function
|
|
18
|
-
return
|
|
5
|
+
function boundsFromArray(bounds) {
|
|
6
|
+
return {
|
|
7
|
+
minLon: bounds[0][0],
|
|
8
|
+
minLat: bounds[0][1],
|
|
9
|
+
maxLon: bounds[1][0],
|
|
10
|
+
maxLat: bounds[1][1]
|
|
11
|
+
};
|
|
19
12
|
}
|
|
20
|
-
function
|
|
21
|
-
|
|
13
|
+
function calculateClipExtentFromPixelOffset(center, pixelClipExtent, epsilon = 1e-6) {
|
|
14
|
+
const [x1, y1, x2, y2] = pixelClipExtent;
|
|
15
|
+
return [
|
|
16
|
+
[center[0] + x1 + epsilon, center[1] + y1 + epsilon],
|
|
17
|
+
[center[0] + x2 - epsilon, center[1] + y2 - epsilon]
|
|
18
|
+
];
|
|
22
19
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
throw new Error(`Unsupported configuration version: ${config.version}`);
|
|
27
|
-
}
|
|
28
|
-
if (!config.territories || config.territories.length === 0) {
|
|
29
|
-
throw new Error("Configuration must contain at least one territory");
|
|
20
|
+
function normalizeBounds(bounds) {
|
|
21
|
+
if ("minLon" in bounds) {
|
|
22
|
+
return bounds;
|
|
30
23
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
console.log("[CompositeProjection] Created sub-projections:", {
|
|
41
|
-
territories: config.territories.map((t) => ({ code: t.code, name: t.name })),
|
|
42
|
-
count: subProjections.length
|
|
43
|
-
});
|
|
44
|
-
}
|
|
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: () => {
|
|
24
|
+
return boundsFromArray(bounds);
|
|
25
|
+
}
|
|
26
|
+
function invertWithBoundsValidation(screenCoords, entries, options = {}) {
|
|
27
|
+
const { tolerance = 0.01, debug = false } = options;
|
|
28
|
+
const [x, y] = screenCoords;
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const { projection, bounds, id } = entry;
|
|
31
|
+
if (!projection.invert) {
|
|
32
|
+
continue;
|
|
59
33
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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;
|
|
34
|
+
try {
|
|
35
|
+
const result = projection.invert([x, y]);
|
|
36
|
+
if (!result || !Array.isArray(result) || result.length < 2) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const [lon, lat] = result;
|
|
40
|
+
if (bounds) {
|
|
41
|
+
const geoBounds = normalizeBounds(bounds);
|
|
42
|
+
if (isPointInBounds(lon, lat, geoBounds, tolerance)) {
|
|
43
|
+
if (debug) {
|
|
44
|
+
console.log(`[Invert] Matched ${id}: [${x}, ${y}] -> [${lon}, ${lat}]`);
|
|
80
45
|
}
|
|
46
|
+
return { coordinates: result, territoryId: id };
|
|
47
|
+
} else if (debug) {
|
|
48
|
+
console.log(`[Invert] Rejected ${id}: [${lon}, ${lat}] outside bounds`);
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
if (debug) {
|
|
52
|
+
console.log(`[Invert] No bounds for ${id}, accepting [${lon}, ${lat}]`);
|
|
81
53
|
}
|
|
54
|
+
return { coordinates: result, territoryId: id };
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (debug) {
|
|
58
|
+
console.warn(`[Invert] Error in ${id}:`, error);
|
|
82
59
|
}
|
|
83
60
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
61
|
+
}
|
|
62
|
+
if (debug) {
|
|
63
|
+
console.log(`[Invert] Failed to invert [${x}, ${y}]`);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function createStreamMultiplexer(projections) {
|
|
68
|
+
return (stream) => {
|
|
69
|
+
const streams = projections.map((p) => p.stream(stream));
|
|
88
70
|
return {
|
|
89
71
|
point: (x, y) => {
|
|
90
72
|
for (const s of streams) s.point(x, y);
|
|
91
73
|
},
|
|
92
|
-
sphere: () => {
|
|
93
|
-
for (const s of streams) {
|
|
94
|
-
if (s.sphere)
|
|
95
|
-
s.sphere();
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
74
|
lineStart: () => {
|
|
99
75
|
for (const s of streams) s.lineStart();
|
|
100
76
|
},
|
|
@@ -106,74 +82,162 @@ function loadCompositeProjection(config, options) {
|
|
|
106
82
|
},
|
|
107
83
|
polygonEnd: () => {
|
|
108
84
|
for (const s of streams) s.polygonEnd();
|
|
85
|
+
},
|
|
86
|
+
sphere: () => {
|
|
87
|
+
for (const s of streams) {
|
|
88
|
+
if (s.sphere)
|
|
89
|
+
s.sphere();
|
|
90
|
+
}
|
|
109
91
|
}
|
|
110
92
|
};
|
|
111
93
|
};
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
94
|
+
}
|
|
95
|
+
function createPointCaptureStream() {
|
|
96
|
+
let capturedPoint = null;
|
|
97
|
+
const pointStream = {
|
|
98
|
+
point: (x, y) => {
|
|
99
|
+
capturedPoint = [x, y];
|
|
100
|
+
},
|
|
101
|
+
lineStart: () => {
|
|
102
|
+
},
|
|
103
|
+
lineEnd: () => {
|
|
104
|
+
},
|
|
105
|
+
polygonStart: () => {
|
|
106
|
+
},
|
|
107
|
+
polygonEnd: () => {
|
|
108
|
+
},
|
|
109
|
+
sphere: () => {
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
pointStream,
|
|
114
|
+
getCapturedPoint: () => capturedPoint,
|
|
115
|
+
resetCapture: () => {
|
|
116
|
+
capturedPoint = null;
|
|
115
117
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function normalizeBounds2(bounds) {
|
|
121
|
+
if ("minLon" in bounds) {
|
|
122
|
+
return bounds;
|
|
123
|
+
}
|
|
124
|
+
return boundsFromArray(bounds);
|
|
125
|
+
}
|
|
126
|
+
function buildCompositeProjection(config) {
|
|
127
|
+
const { entries, debug = false } = config;
|
|
128
|
+
if (entries.length === 0) {
|
|
129
|
+
throw new Error("Cannot build composite projection with no entries");
|
|
130
|
+
}
|
|
131
|
+
const { pointStream, getCapturedPoint, resetCapture } = createPointCaptureStream();
|
|
132
|
+
const entryStreams = entries.map((entry) => ({
|
|
133
|
+
entry,
|
|
134
|
+
stream: entry.projection.stream(pointStream)
|
|
135
|
+
}));
|
|
136
|
+
const project = (coordinates) => {
|
|
137
|
+
const [lon, lat] = coordinates;
|
|
138
|
+
resetCapture();
|
|
139
|
+
for (const { entry, stream } of entryStreams) {
|
|
140
|
+
if (entry.bounds) {
|
|
141
|
+
const bounds = normalizeBounds2(entry.bounds);
|
|
142
|
+
if (isPointInBounds(lon, lat, bounds)) {
|
|
143
|
+
stream.point(lon, lat);
|
|
144
|
+
const captured = getCapturedPoint();
|
|
145
|
+
if (captured) {
|
|
146
|
+
return captured;
|
|
127
147
|
}
|
|
128
148
|
}
|
|
129
149
|
}
|
|
130
150
|
}
|
|
131
151
|
return null;
|
|
132
152
|
};
|
|
133
|
-
|
|
134
|
-
|
|
153
|
+
const composite = project;
|
|
154
|
+
composite.stream = createStreamMultiplexer(entries.map((e) => e.projection));
|
|
155
|
+
composite.invert = (coords) => {
|
|
156
|
+
const result = invertWithBoundsValidation(coords, entries, { debug });
|
|
157
|
+
return (result == null ? void 0 : result.coordinates) ?? null;
|
|
158
|
+
};
|
|
159
|
+
composite.scale = function(_s) {
|
|
160
|
+
var _a;
|
|
135
161
|
if (arguments.length === 0) {
|
|
136
|
-
return
|
|
162
|
+
return ((_a = entries[0]) == null ? void 0 : _a.projection.scale()) ?? 1;
|
|
137
163
|
}
|
|
138
|
-
return
|
|
164
|
+
return composite;
|
|
139
165
|
};
|
|
140
|
-
|
|
166
|
+
composite.translate = function(_t) {
|
|
167
|
+
var _a;
|
|
141
168
|
if (arguments.length === 0) {
|
|
142
|
-
return [
|
|
169
|
+
return ((_a = entries[0]) == null ? void 0 : _a.projection.translate()) ?? [0, 0];
|
|
143
170
|
}
|
|
144
|
-
return
|
|
171
|
+
return composite;
|
|
145
172
|
};
|
|
146
|
-
return
|
|
173
|
+
return composite;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/standalone-projection-loader.ts
|
|
177
|
+
function resolveI18nString(value) {
|
|
178
|
+
if (typeof value === "string") {
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
return value.en || Object.values(value).find((v) => typeof v === "string") || "";
|
|
182
|
+
}
|
|
183
|
+
var projectionRegistry = /* @__PURE__ */ new Map();
|
|
184
|
+
function registerProjection(id, factory) {
|
|
185
|
+
projectionRegistry.set(id, factory);
|
|
186
|
+
}
|
|
187
|
+
function registerProjections(factories) {
|
|
188
|
+
for (const [id, factory] of Object.entries(factories)) {
|
|
189
|
+
registerProjection(id, factory);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function unregisterProjection(id) {
|
|
193
|
+
return projectionRegistry.delete(id);
|
|
194
|
+
}
|
|
195
|
+
function clearProjections() {
|
|
196
|
+
projectionRegistry.clear();
|
|
197
|
+
}
|
|
198
|
+
function getRegisteredProjections() {
|
|
199
|
+
return Array.from(projectionRegistry.keys());
|
|
200
|
+
}
|
|
201
|
+
function isProjectionRegistered(id) {
|
|
202
|
+
return projectionRegistry.has(id);
|
|
147
203
|
}
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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";
|
|
204
|
+
function loadCompositeProjection(config, options) {
|
|
205
|
+
const { width, height, debug = false } = options;
|
|
206
|
+
if (config.version !== "1.0") {
|
|
207
|
+
throw new Error(`Unsupported configuration version: ${config.version}`);
|
|
160
208
|
}
|
|
209
|
+
if (!config.territories || config.territories.length === 0) {
|
|
210
|
+
throw new Error("Configuration must contain at least one territory");
|
|
211
|
+
}
|
|
212
|
+
const entries = config.territories.map((territory) => {
|
|
213
|
+
const proj = createSubProjection(territory, width, height, config.referenceScale, debug);
|
|
214
|
+
return {
|
|
215
|
+
id: territory.code,
|
|
216
|
+
name: resolveI18nString(territory.name),
|
|
217
|
+
projection: proj,
|
|
218
|
+
bounds: {
|
|
219
|
+
minLon: territory.bounds[0][0],
|
|
220
|
+
minLat: territory.bounds[0][1],
|
|
221
|
+
maxLon: territory.bounds[1][0],
|
|
222
|
+
maxLat: territory.bounds[1][1]
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
if (debug) {
|
|
227
|
+
console.log("[CompositeProjection] Created sub-projections:", {
|
|
228
|
+
territories: config.territories.map((t) => ({ code: t.code, name: resolveI18nString(t.name) })),
|
|
229
|
+
count: entries.length
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const composite = buildCompositeProjection({ entries, debug });
|
|
233
|
+
return composite;
|
|
161
234
|
}
|
|
162
235
|
function createSubProjection(territory, width, height, referenceScale, debug) {
|
|
163
|
-
var _a, _b;
|
|
164
|
-
let projectionId;
|
|
165
|
-
let parameters;
|
|
236
|
+
var _a, _b, _c;
|
|
166
237
|
const { layout } = territory;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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 {
|
|
238
|
+
const projectionId = territory.projection.id;
|
|
239
|
+
const parameters = territory.projection.parameters;
|
|
240
|
+
if (!projectionId || !parameters) {
|
|
177
241
|
throw new Error(`Territory ${territory.code} missing projection configuration`);
|
|
178
242
|
}
|
|
179
243
|
const factory = projectionRegistry.get(projectionId);
|
|
@@ -185,10 +249,16 @@ function createSubProjection(territory, width, height, referenceScale, debug) {
|
|
|
185
249
|
);
|
|
186
250
|
}
|
|
187
251
|
const projection = factory();
|
|
188
|
-
|
|
252
|
+
const hasFocus = parameters.focusLongitude !== void 0 && parameters.focusLatitude !== void 0;
|
|
253
|
+
const projFamily = territory.projection.family;
|
|
254
|
+
if (hasFocus && projFamily === "CONIC" && projection.rotate) {
|
|
255
|
+
projection.rotate([-parameters.focusLongitude, -parameters.focusLatitude, 0]);
|
|
256
|
+
} else if (hasFocus && projection.center) {
|
|
257
|
+
projection.center([parameters.focusLongitude, parameters.focusLatitude]);
|
|
258
|
+
} else if (parameters.center && projection.center) {
|
|
189
259
|
projection.center(parameters.center);
|
|
190
260
|
}
|
|
191
|
-
if (parameters.rotate && projection.rotate) {
|
|
261
|
+
if (parameters.rotate && projection.rotate && !hasFocus) {
|
|
192
262
|
const rotate = Array.isArray(parameters.rotate) ? [...parameters.rotate, 0, 0].slice(0, 3) : [0, 0, 0];
|
|
193
263
|
projection.rotate(rotate);
|
|
194
264
|
}
|
|
@@ -196,14 +266,10 @@ function createSubProjection(territory, width, height, referenceScale, debug) {
|
|
|
196
266
|
const parallels = Array.isArray(parameters.parallels) ? [...parameters.parallels, 0].slice(0, 2) : [0, 60];
|
|
197
267
|
projection.parallels(parallels);
|
|
198
268
|
}
|
|
199
|
-
if (projection.scale) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const effectiveReferenceScale = referenceScale || 2700;
|
|
204
|
-
const calculatedScale = effectiveReferenceScale * parameters.scaleMultiplier;
|
|
205
|
-
projection.scale(calculatedScale);
|
|
206
|
-
}
|
|
269
|
+
if (projection.scale && parameters.scaleMultiplier) {
|
|
270
|
+
const effectiveReferenceScale = referenceScale || 2700;
|
|
271
|
+
const calculatedScale = effectiveReferenceScale * parameters.scaleMultiplier;
|
|
272
|
+
projection.scale(calculatedScale);
|
|
207
273
|
}
|
|
208
274
|
if (parameters.clipAngle && projection.clipAngle) {
|
|
209
275
|
projection.clipAngle(parameters.clipAngle);
|
|
@@ -218,36 +284,24 @@ function createSubProjection(territory, width, height, referenceScale, debug) {
|
|
|
218
284
|
height / 2 + offsetY
|
|
219
285
|
]);
|
|
220
286
|
}
|
|
221
|
-
if (
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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);
|
|
287
|
+
if (layout.pixelClipExtent && projection.clipExtent) {
|
|
288
|
+
const territoryCenter = ((_a = projection.translate) == null ? void 0 : _a.call(projection)) || [width / 2, height / 2];
|
|
289
|
+
const clipExtent = calculateClipExtentFromPixelOffset(
|
|
290
|
+
territoryCenter,
|
|
291
|
+
layout.pixelClipExtent
|
|
292
|
+
);
|
|
293
|
+
projection.clipExtent(clipExtent);
|
|
240
294
|
if (debug) {
|
|
241
295
|
console.log(
|
|
242
|
-
`[Clipping] Applied
|
|
243
|
-
`original: ${JSON.stringify(layout.
|
|
296
|
+
`[Clipping] Applied pixelClipExtent for ${territory.code}:`,
|
|
297
|
+
`original: ${JSON.stringify(layout.pixelClipExtent)} -> transformed: ${JSON.stringify(clipExtent)}`
|
|
244
298
|
);
|
|
245
299
|
}
|
|
246
300
|
} else if (projection.clipExtent) {
|
|
247
301
|
const bounds = territory.bounds;
|
|
248
302
|
if (bounds && bounds.length === 2 && bounds[0].length === 2 && bounds[1].length === 2) {
|
|
249
|
-
const scale = ((
|
|
250
|
-
const translate = ((
|
|
303
|
+
const scale = ((_b = projection.scale) == null ? void 0 : _b.call(projection)) || 1;
|
|
304
|
+
const translate = ((_c = projection.translate) == null ? void 0 : _c.call(projection)) || [0, 0];
|
|
251
305
|
const padding = scale * 0.1;
|
|
252
306
|
const clipExtent = [
|
|
253
307
|
[translate[0] - padding, translate[1] - padding],
|
|
@@ -281,11 +335,8 @@ function validateConfig(config) {
|
|
|
281
335
|
if (!territory.code) {
|
|
282
336
|
throw new Error(`Territory missing required field 'code': ${JSON.stringify(territory)}`);
|
|
283
337
|
}
|
|
284
|
-
|
|
285
|
-
|
|
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(", ")}`);
|
|
338
|
+
if (!territory.projection || !territory.projection.id || !territory.projection.parameters) {
|
|
339
|
+
throw new Error(`Territory ${territory.code} missing projection configuration. Required: projection.id and projection.parameters`);
|
|
289
340
|
}
|
|
290
341
|
if (!territory.bounds) {
|
|
291
342
|
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":["../../projection-core/src/bounds/checker.ts","../../projection-core/src/bounds/clip-extent.ts","../../projection-core/src/invert/validator.ts","../../projection-core/src/stream/multiplexer.ts","../../projection-core/src/stream/point-capture.ts","../../projection-core/src/composite/builder.ts","../src/standalone-projection-loader.ts"],"names":["normalizeBounds"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Bounds Checker\n *\n * Pure functions for checking if geographic points fall within bounds.\n * Used by composite projections to route points to the correct sub-projection.\n */\n\nimport type { GeoBounds, GeoBoundsArray } from '../types'\n\n/**\n * Check if a geographic point is within the given bounds\n *\n * @param lon - Longitude of the point\n * @param lat - Latitude of the point\n * @param bounds - Geographic bounds to check against\n * @param tolerance - Optional tolerance for edge cases (default: 0)\n * @returns True if the point is within bounds\n *\n * @example\n * ```typescript\n * const bounds = { minLon: -10, minLat: 35, maxLon: 5, maxLat: 45 }\n *\n * isPointInBounds(-5, 40, bounds) // true (inside Portugal/Spain)\n * isPointInBounds(10, 50, bounds) // false (outside bounds)\n * isPointInBounds(-10, 35, bounds, 0.01) // true (on edge, with tolerance)\n * ```\n */\nexport function isPointInBounds(\n lon: number,\n lat: number,\n bounds: GeoBounds,\n tolerance = 0,\n): boolean {\n return (\n lon >= bounds.minLon - tolerance\n && lon <= bounds.maxLon + tolerance\n && lat >= bounds.minLat - tolerance\n && lat <= bounds.maxLat + tolerance\n )\n}\n\n/**\n * Convert a bounds array to a GeoBounds object\n *\n * @param bounds - Bounds array [[minLon, minLat], [maxLon, maxLat]]\n * @returns GeoBounds object\n *\n * @example\n * ```typescript\n * const arrayBounds: GeoBoundsArray = [[-10, 35], [5, 45]]\n * const objectBounds = boundsFromArray(arrayBounds)\n * // { minLon: -10, minLat: 35, maxLon: 5, maxLat: 45 }\n * ```\n */\nexport function boundsFromArray(bounds: GeoBoundsArray): GeoBounds {\n return {\n minLon: bounds[0][0],\n minLat: bounds[0][1],\n maxLon: bounds[1][0],\n maxLat: bounds[1][1],\n }\n}\n\n/**\n * Convert a GeoBounds object to a bounds array\n *\n * @param bounds - GeoBounds object\n * @returns Bounds array [[minLon, minLat], [maxLon, maxLat]]\n *\n * @example\n * ```typescript\n * const objectBounds = { minLon: -10, minLat: 35, maxLon: 5, maxLat: 45 }\n * const arrayBounds = boundsToArray(objectBounds)\n * // [[-10, 35], [5, 45]]\n * ```\n */\nexport function boundsToArray(bounds: GeoBounds): GeoBoundsArray {\n return [\n [bounds.minLon, bounds.minLat],\n [bounds.maxLon, bounds.maxLat],\n ]\n}\n","/**\n * Clip Extent Calculator\n *\n * Pure functions for calculating clip extents from geographic bounds\n * or pixel offsets. Used by composite projections to properly clip\n * each territory's rendering area.\n */\n\nimport type { ClipExtentOptions, GeoBounds, ProjectionLike } from '../types'\n\n/**\n * Calculate clip extent from geographic bounds\n *\n * Projects the corner points of geographic bounds to get the corresponding\n * screen coordinates for clipping. This is critical for composite projections\n * to ensure each territory only renders within its designated area.\n *\n * @param projection - The projection to use for transforming bounds\n * @param bounds - Geographic bounds { minLon, minLat, maxLon, maxLat }\n * @param options - Optional configuration (epsilon for padding)\n * @returns Clip extent [[x0, y0], [x1, y1]] or null if projection fails\n *\n * @example\n * ```typescript\n * const bounds = { minLon: -10, minLat: 35, maxLon: 5, maxLat: 45 }\n * const clipExtent = calculateClipExtentFromBounds(projection, bounds)\n *\n * if (clipExtent) {\n * projection.clipExtent(clipExtent)\n * }\n * ```\n */\nexport function calculateClipExtentFromBounds(\n projection: ProjectionLike,\n bounds: GeoBounds,\n options: ClipExtentOptions = {},\n): [[number, number], [number, number]] | null {\n const { epsilon = 1e-6 } = options\n\n // Project the corner points\n // Note: Geographic coordinates are [lon, lat] where:\n // - minLon is west, maxLon is east\n // - minLat is south, maxLat is north\n // In screen coordinates:\n // - topLeft corresponds to [minLon, maxLat] (northwest corner)\n // - bottomRight corresponds to [maxLon, minLat] (southeast corner)\n const topLeft = projection([\n bounds.minLon + epsilon,\n bounds.maxLat - epsilon,\n ])\n const bottomRight = projection([\n bounds.maxLon - epsilon,\n bounds.minLat + epsilon,\n ])\n\n if (!topLeft || !bottomRight) {\n return null\n }\n\n return [\n [topLeft[0], topLeft[1]],\n [bottomRight[0], bottomRight[1]],\n ]\n}\n\n/**\n * Calculate clip extent from pixel offsets\n *\n * Converts a pixel-based clip extent (relative to territory center)\n * to absolute screen coordinates. Used when clip extent is stored\n * as offsets in exported configurations.\n *\n * @param center - The territory center in screen coordinates [x, y]\n * @param pixelClipExtent - Pixel offsets [x1, y1, x2, y2] relative to center\n * @param epsilon - Small value for padding (default: 1e-6)\n * @returns Clip extent [[x0, y0], [x1, y1]] in absolute screen coordinates\n *\n * @example\n * ```typescript\n * const center = [400, 300] // Territory center in pixels\n * const offsets = [-100, -80, 100, 80] // Relative clip bounds\n *\n * const clipExtent = calculateClipExtentFromPixelOffset(center, offsets)\n * // [[300, 220], [500, 380]]\n *\n * projection.clipExtent(clipExtent)\n * ```\n */\nexport function calculateClipExtentFromPixelOffset(\n center: [number, number],\n pixelClipExtent: [number, number, number, number],\n epsilon = 1e-6,\n): [[number, number], [number, number]] {\n const [x1, y1, x2, y2] = pixelClipExtent\n\n return [\n [center[0] + x1 + epsilon, center[1] + y1 + epsilon],\n [center[0] + x2 - epsilon, center[1] + y2 - epsilon],\n ]\n}\n","/**\n * Invert Validator\n *\n * Validates inverted coordinates against territory bounds to ensure\n * correct territory matching. This is critical for composite projections\n * where multiple territories may overlap in screen space.\n */\n\nimport type { GeoBounds, GeoBoundsArray, InvertOptions, InvertResult, SubProjectionEntry } from '../types'\n\nimport { boundsFromArray, isPointInBounds } from '../bounds/checker'\n\n/**\n * Normalize bounds to GeoBounds format\n */\nfunction normalizeBounds(bounds: GeoBounds | GeoBoundsArray): GeoBounds {\n if ('minLon' in bounds) {\n return bounds\n }\n return boundsFromArray(bounds)\n}\n\n/**\n * Invert screen coordinates with bounds validation\n *\n * Tries each sub-projection's invert method and validates the result\n * against the territory's geographic bounds. This ensures that when\n * multiple territories overlap in screen space (common in composite\n * projections), the correct territory is identified.\n *\n * @param screenCoords - Screen coordinates [x, y] to invert\n * @param entries - Array of sub-projection entries with bounds\n * @param options - Optional configuration (tolerance, debug)\n * @returns InvertResult with coordinates and territoryId, or null if no match\n *\n * @example\n * ```typescript\n * const entries = [\n * { id: 'usa', projection: usaProj, bounds: usaBounds },\n * { id: 'alaska', projection: alaskaProj, bounds: alaskaBounds },\n * ]\n *\n * const result = invertWithBoundsValidation([400, 300], entries)\n *\n * if (result) {\n * console.log(`Clicked on ${result.territoryId} at [${result.coordinates}]`)\n * }\n * ```\n */\nexport function invertWithBoundsValidation(\n screenCoords: [number, number],\n entries: SubProjectionEntry[],\n options: InvertOptions = {},\n): InvertResult | null {\n const { tolerance = 0.01, debug = false } = options\n const [x, y] = screenCoords\n\n for (const entry of entries) {\n const { projection, bounds, id } = entry\n\n if (!projection.invert) {\n continue\n }\n\n try {\n const result = projection.invert([x, y])\n\n if (!result || !Array.isArray(result) || result.length < 2) {\n continue\n }\n\n const [lon, lat] = result\n\n if (bounds) {\n const geoBounds = normalizeBounds(bounds)\n\n if (isPointInBounds(lon, lat, geoBounds, tolerance)) {\n if (debug) {\n console.log(`[Invert] Matched ${id}: [${x}, ${y}] -> [${lon}, ${lat}]`)\n }\n return { coordinates: result as [number, number], territoryId: id }\n }\n else if (debug) {\n console.log(`[Invert] Rejected ${id}: [${lon}, ${lat}] outside bounds`)\n }\n }\n else {\n // No bounds available, accept first successful invert (legacy behavior)\n if (debug) {\n console.log(`[Invert] No bounds for ${id}, accepting [${lon}, ${lat}]`)\n }\n return { coordinates: result as [number, number], territoryId: id }\n }\n }\n catch (error) {\n if (debug) {\n console.warn(`[Invert] Error in ${id}:`, error)\n }\n }\n }\n\n if (debug) {\n console.log(`[Invert] Failed to invert [${x}, ${y}]`)\n }\n\n return null\n}\n","/**\n * Stream Multiplexer\n *\n * Creates a stream that fans out geometry to multiple projections.\n * Used by composite projections to render all territories simultaneously.\n *\n * When d3.geoPath renders geometry through a composite projection,\n * the multiplexed stream ensures each sub-projection receives the\n * full geometry and applies its own clipping/transform.\n */\n\nimport type { ProjectionLike, StreamLike } from '../types'\n\n/**\n * Create a stream multiplexer for multiple projections\n *\n * @param projections - Array of projections to multiplex to\n * @returns Function that creates a multiplexed stream\n *\n * @example\n * ```typescript\n * const multiplex = createStreamMultiplexer([usaProj, alaskaProj, hawaiiProj])\n *\n * // Use as projection.stream method\n * compositeProjection.stream = multiplex\n *\n * // When d3.geoPath renders, all projections receive the geometry\n * const path = d3.geoPath(compositeProjection)\n * svg.selectAll('path')\n * .data(countries.features)\n * .join('path')\n * .attr('d', path)\n * ```\n */\nexport function createStreamMultiplexer(\n projections: ProjectionLike[],\n): (stream: StreamLike) => StreamLike {\n return (stream: StreamLike): StreamLike => {\n const streams = projections.map(p => p.stream(stream))\n\n return {\n point: (x: number, y: number) => {\n for (const s of streams) s.point(x, y)\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 sphere: () => {\n for (const s of streams) {\n if (s.sphere)\n s.sphere()\n }\n },\n }\n }\n}\n","/**\n * Point Capture Stream\n *\n * Creates a stream that captures projected point coordinates.\n * Used by composite projections to determine which sub-projection\n * successfully projects a geographic point.\n *\n * The pattern follows d3-composite-projections (albersUsa) approach:\n * 1. Create a point stream that stores the projected coordinates\n * 2. Pipe geographic coordinates through each sub-projection's stream\n * 3. Check if the point was captured (projection succeeded)\n */\n\nimport type { PointCaptureResult, StreamLike } from '../types'\n\n/**\n * Create a point capture stream for projection routing\n *\n * @returns Object with pointStream, getCapturedPoint, and resetCapture\n *\n * @example\n * ```typescript\n * const { pointStream, getCapturedPoint, resetCapture } = createPointCaptureStream()\n *\n * // Create capture stream for a projection\n * const captureStream = projection.stream(pointStream)\n *\n * // Project a point\n * resetCapture()\n * captureStream.point(longitude, latitude)\n *\n * // Check result\n * const projected = getCapturedPoint()\n * if (projected) {\n * console.log(`Projected to [${projected[0]}, ${projected[1]}]`)\n * }\n * ```\n */\nexport function createPointCaptureStream(): PointCaptureResult {\n let capturedPoint: [number, number] | null = null\n\n const pointStream: StreamLike = {\n point: (x: number, y: number) => {\n capturedPoint = [x, y]\n },\n lineStart: () => {},\n lineEnd: () => {},\n polygonStart: () => {},\n polygonEnd: () => {},\n sphere: () => {},\n }\n\n return {\n pointStream,\n getCapturedPoint: () => capturedPoint,\n resetCapture: () => {\n capturedPoint = null\n },\n }\n}\n","/**\n * Composite Projection Builder\n *\n * Creates a D3-compatible composite projection from multiple sub-projections.\n * This follows the same pattern as d3-composite-projections (albersUsa).\n *\n * The composite projection:\n * 1. Routes projection calls to the correct sub-projection based on bounds\n * 2. Multiplexes geometry streaming to all sub-projections for rendering\n * 3. Validates invert operations against territory bounds\n */\n\nimport type {\n CompositeProjectionConfig,\n GeoBounds,\n GeoBoundsArray,\n ProjectionLike,\n} from '../types'\n\nimport { boundsFromArray, isPointInBounds } from '../bounds/checker'\nimport { invertWithBoundsValidation } from '../invert/validator'\nimport { createStreamMultiplexer } from '../stream/multiplexer'\nimport { createPointCaptureStream } from '../stream/point-capture'\n\n/**\n * Normalize bounds to GeoBounds format\n */\nfunction normalizeBounds(bounds: GeoBounds | GeoBoundsArray): GeoBounds {\n if ('minLon' in bounds) {\n return bounds\n }\n return boundsFromArray(bounds)\n}\n\n/**\n * Build a composite projection from sub-projection entries\n *\n * Creates a D3-compatible projection that routes geographic points to the\n * appropriate sub-projection based on territory bounds. When rendering,\n * geometry is multiplexed to all sub-projections so each territory renders\n * its portion of the world.\n *\n * @param config - Configuration with entries and optional debug flag\n * @returns D3-compatible composite projection\n *\n * @example\n * ```typescript\n * // Create sub-projections (e.g., using d3-geo)\n * const usaProj = d3.geoAlbers()\n * .center([0, 38])\n * .rotate([96, 0])\n * .scale(1000)\n *\n * const alaskaProj = d3.geoConicEqualArea()\n * .center([0, 64])\n * .rotate([154, 0])\n * .scale(400)\n *\n * // Build composite\n * const composite = buildCompositeProjection({\n * entries: [\n * {\n * id: 'usa',\n * name: 'Continental USA',\n * projection: usaProj,\n * bounds: { minLon: -125, minLat: 24, maxLon: -66, maxLat: 50 }\n * },\n * {\n * id: 'alaska',\n * name: 'Alaska',\n * projection: alaskaProj,\n * bounds: { minLon: -180, minLat: 51, maxLon: -129, maxLat: 72 }\n * },\n * ],\n * })\n *\n * // Use with D3\n * const path = d3.geoPath(composite)\n * svg.selectAll('path')\n * .data(countries.features)\n * .join('path')\n * .attr('d', path)\n * ```\n */\nexport function buildCompositeProjection(\n config: CompositeProjectionConfig,\n): ProjectionLike {\n const { entries, debug = false } = config\n\n if (entries.length === 0) {\n throw new Error('Cannot build composite projection with no entries')\n }\n\n // Create point capture mechanism\n const { pointStream, getCapturedPoint, resetCapture } = createPointCaptureStream()\n\n // Create point capture streams for each entry\n const entryStreams = entries.map(entry => ({\n entry,\n stream: entry.projection.stream(pointStream),\n }))\n\n // Main projection function - routes to correct sub-projection based on bounds\n const project = (coordinates: [number, number]): [number, number] | null => {\n const [lon, lat] = coordinates\n resetCapture()\n\n for (const { entry, stream } of entryStreams) {\n if (entry.bounds) {\n const bounds = normalizeBounds(entry.bounds)\n\n if (isPointInBounds(lon, lat, bounds)) {\n stream.point(lon, lat)\n const captured = getCapturedPoint()\n\n if (captured) {\n return captured\n }\n }\n }\n }\n\n return null\n }\n\n // Cast to ProjectionLike and add required methods\n const composite = project as ProjectionLike\n\n // Stream method - multiplex to all sub-projections\n composite.stream = createStreamMultiplexer(entries.map(e => e.projection))\n\n // Invert method - with bounds validation\n composite.invert = (coords: [number, number]) => {\n const result = invertWithBoundsValidation(coords, entries, { debug })\n return result?.coordinates ?? null\n }\n\n // Scale getter/setter - returns reference from first sub-projection\n composite.scale = function (_s?: number): any {\n if (arguments.length === 0) {\n return entries[0]?.projection.scale() ?? 1\n }\n // Setting scale on composite is a no-op (individual territories manage their scales)\n return composite\n } as ProjectionLike['scale']\n\n // Translate getter/setter - returns reference translate\n composite.translate = function (_t?: [number, number]): any {\n if (arguments.length === 0) {\n return entries[0]?.projection.translate() ?? [0, 0]\n }\n // Setting translate on composite is a no-op (individual territories manage their translates)\n return composite\n } as ProjectionLike['translate']\n\n return composite\n}\n","/**\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.
|
|
4
|
+
"version": "1.1.1",
|
|
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",
|
|
@@ -79,7 +82,9 @@
|
|
|
79
82
|
"d3-geo-projection": "^4.0.0",
|
|
80
83
|
"tsup": "^8.0.0",
|
|
81
84
|
"typescript": "~5.9.3",
|
|
82
|
-
"vitest": "^3.2.4"
|
|
85
|
+
"vitest": "^3.2.4",
|
|
86
|
+
"@atlas-composer/projection-core": "1.0.1",
|
|
87
|
+
"@atlas-composer/specification": "1.0.1"
|
|
83
88
|
},
|
|
84
89
|
"scripts": {
|
|
85
90
|
"build": "tsup",
|