@atlas-composer/projection-loader 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -19
- package/dist/d3-projection-helpers.d.ts +0 -51
- package/dist/d3-projection-helpers.js +0 -5
- package/dist/d3-projection-helpers.js.map +1 -1
- package/dist/index.d.ts +15 -191
- package/dist/index.js +310 -142
- package/dist/index.js.map +1 -1
- package/package.json +4 -6
package/README.md
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
1
|
# @atlas-composer/projection-loader
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> A simple runtime engine for composite projections.
|
|
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
|
-
- **Zero
|
|
10
|
-
- **
|
|
9
|
+
- **Zero runtime dependencies** (excluding your choice of projection library).
|
|
10
|
+
- **plugin architecture**: Only import the projection definitions you need.
|
|
11
11
|
- **Type-Safe**: Written in TypeScript with full type definitions.
|
|
12
12
|
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
### 1. Install
|
|
13
|
+
## Installation
|
|
16
14
|
|
|
17
15
|
```bash
|
|
18
16
|
npm install @atlas-composer/projection-loader d3-geo
|
|
19
17
|
```
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
## Usage
|
|
22
20
|
|
|
23
21
|
```typescript
|
|
24
|
-
import {
|
|
22
|
+
import { ProjectionLoader } from '@atlas-composer/projection-loader'
|
|
25
23
|
import * as d3 from 'd3-geo'
|
|
26
|
-
import config from './my-exported-
|
|
24
|
+
import config from './my-exported-config.json'
|
|
25
|
+
|
|
26
|
+
// Create a loader instance
|
|
27
|
+
const loader = new ProjectionLoader()
|
|
27
28
|
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// Register the projections required by your composite projection
|
|
30
|
+
loader.register('mercator', () => d3.geoMercator())
|
|
31
|
+
loader.register('conic-conformal', () => d3.geoConicConformal())
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
const projection = loadCompositeProjection(config, {
|
|
33
|
+
// Load the composite projection
|
|
34
|
+
const projection = loader.load(config, {
|
|
35
35
|
width: 800,
|
|
36
36
|
height: 600
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
//
|
|
39
|
+
// Use with D3
|
|
40
40
|
const path = d3.geoPath(projection)
|
|
41
41
|
|
|
42
42
|
d3.select('svg')
|
|
@@ -46,6 +46,16 @@ d3.select('svg')
|
|
|
46
46
|
.attr('d', path)
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
##
|
|
49
|
+
## API
|
|
50
|
+
|
|
51
|
+
### ProjectionLoader
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
- `new ProjectionLoader()` - Create a loader instance
|
|
54
|
+
- `loader.register(id, factory)` - Register a projection factory
|
|
55
|
+
- `loader.registerAll(factories)` - Register multiple projections from an object
|
|
56
|
+
- `loader.load(config, options)` - Load a composite projection from configuration
|
|
57
|
+
- `loader.loadFromJSON(jsonString, options)` - Load from a JSON string
|
|
58
|
+
- `loader.isRegistered(id)` - Check if a projection is registered
|
|
59
|
+
- `loader.getRegistered()` - Get all registered projection IDs
|
|
60
|
+
- `loader.unregister(id)` - Remove a registered projection
|
|
61
|
+
- `loader.clear()` - Clear all registered projections
|
|
@@ -1,37 +1,6 @@
|
|
|
1
1
|
import { ProjectionFactory } from './index.js';
|
|
2
2
|
import '@atlas-composer/specification';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* D3 Projection Helpers
|
|
6
|
-
*
|
|
7
|
-
* Optional companion file that provides ready-to-use D3 projection factory mappings.
|
|
8
|
-
* This file has dependencies on d3-geo and d3-geo-projection, but the main loader does not.
|
|
9
|
-
*
|
|
10
|
-
* Users can import this to quickly register all standard D3 projections, or they can
|
|
11
|
-
* selectively import only the projections they need for tree-shaking.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```typescript
|
|
15
|
-
* // Register all projections at once
|
|
16
|
-
* import { registerProjections } from './standalone-projection-loader'
|
|
17
|
-
* import { d3ProjectionFactories } from './d3-projection-helpers'
|
|
18
|
-
*
|
|
19
|
-
* registerProjections(d3ProjectionFactories)
|
|
20
|
-
* ```
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* ```typescript
|
|
24
|
-
* // Tree-shakeable: import only what you need
|
|
25
|
-
* import { registerProjection } from './standalone-projection-loader'
|
|
26
|
-
* import { mercator, albers } from './d3-projection-helpers'
|
|
27
|
-
*
|
|
28
|
-
* registerProjection('mercator', mercator)
|
|
29
|
-
* registerProjection('albers', albers)
|
|
30
|
-
* ```
|
|
31
|
-
*
|
|
32
|
-
* @packageDocumentation
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
4
|
declare const azimuthalEqualArea: ProjectionFactory;
|
|
36
5
|
declare const azimuthalEquidistant: ProjectionFactory;
|
|
37
6
|
declare const gnomonic: ProjectionFactory;
|
|
@@ -46,28 +15,8 @@ declare const transverseMercator: ProjectionFactory;
|
|
|
46
15
|
declare const equirectangular: ProjectionFactory;
|
|
47
16
|
declare const naturalEarth1: ProjectionFactory;
|
|
48
17
|
declare const equalEarth: ProjectionFactory;
|
|
49
|
-
/**
|
|
50
|
-
* Object containing all standard D3 projection factories
|
|
51
|
-
* Keyed by the projection ID used in Atlas composer configurations
|
|
52
|
-
*/
|
|
53
18
|
declare const d3ProjectionFactories: Record<string, ProjectionFactory>;
|
|
54
|
-
/**
|
|
55
|
-
* Convenience function to register all D3 projections at once
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* ```typescript
|
|
59
|
-
* import { registerProjections } from './standalone-projection-loader'
|
|
60
|
-
* import { registerAllD3Projections } from './d3-projection-helpers'
|
|
61
|
-
*
|
|
62
|
-
* registerAllD3Projections(registerProjections)
|
|
63
|
-
* ```
|
|
64
|
-
*
|
|
65
|
-
* @param registerFn - The registerProjections function from the loader
|
|
66
|
-
*/
|
|
67
19
|
declare function registerAllD3Projections(registerFn: (factories: Record<string, ProjectionFactory>) => void): void;
|
|
68
|
-
/**
|
|
69
|
-
* Get list of available D3 projection IDs
|
|
70
|
-
*/
|
|
71
20
|
declare function getAvailableD3Projections(): string[];
|
|
72
21
|
declare const _default: {
|
|
73
22
|
d3ProjectionFactories: Record<string, ProjectionFactory>;
|
|
@@ -17,23 +17,19 @@ var equirectangular = () => d3Geo.geoEquirectangular();
|
|
|
17
17
|
var naturalEarth1 = () => d3GeoProjection.geoNaturalEarth1();
|
|
18
18
|
var equalEarth = () => d3Geo.geoEqualEarth();
|
|
19
19
|
var d3ProjectionFactories = {
|
|
20
|
-
// Azimuthal
|
|
21
20
|
"azimuthal-equal-area": azimuthalEqualArea,
|
|
22
21
|
"azimuthal-equidistant": azimuthalEquidistant,
|
|
23
22
|
"gnomonic": gnomonic,
|
|
24
23
|
"orthographic": orthographic,
|
|
25
24
|
"stereographic": stereographic,
|
|
26
|
-
// Conic
|
|
27
25
|
"conic-conformal": conicConformal,
|
|
28
26
|
"conic-equal-area": conicEqualArea,
|
|
29
27
|
"conic-equidistant": conicEquidistant,
|
|
30
28
|
"albers": albers,
|
|
31
|
-
// Cylindrical
|
|
32
29
|
"mercator": mercator,
|
|
33
30
|
"transverse-mercator": transverseMercator,
|
|
34
31
|
"equirectangular": equirectangular,
|
|
35
32
|
"natural-earth-1": naturalEarth1,
|
|
36
|
-
// Other
|
|
37
33
|
"equal-earth": equalEarth
|
|
38
34
|
};
|
|
39
35
|
function registerAllD3Projections(registerFn) {
|
|
@@ -46,7 +42,6 @@ var d3_projection_helpers_default = {
|
|
|
46
42
|
d3ProjectionFactories,
|
|
47
43
|
registerAllD3Projections,
|
|
48
44
|
getAvailableD3Projections,
|
|
49
|
-
// Individual projections for tree-shaking
|
|
50
45
|
azimuthalEqualArea,
|
|
51
46
|
azimuthalEquidistant,
|
|
52
47
|
gnomonic,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/d3-projection-helpers.ts"],"names":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"sources":["../src/d3-projection-helpers.ts"],"names":[],"mappings":";;;;AAIO,IAAM,kBAAA,GAAwC,MAAY,KAAA,CAAA,qBAAA;AAC1D,IAAM,oBAAA,GAA0C,MAAY,KAAA,CAAA,uBAAA;AAC5D,IAAM,QAAA,GAA8B,MAAY,KAAA,CAAA,WAAA;AAChD,IAAM,YAAA,GAAkC,MAAY,KAAA,CAAA,eAAA;AACpD,IAAM,aAAA,GAAmC,MAAY,KAAA,CAAA,gBAAA;AACrD,IAAM,cAAA,GAAoC,MAAY,KAAA,CAAA,iBAAA;AACtD,IAAM,cAAA,GAAoC,MAAY,KAAA,CAAA,iBAAA;AACtD,IAAM,gBAAA,GAAsC,MAAY,KAAA,CAAA,mBAAA;AACxD,IAAM,MAAA,GAA4B,MAAY,KAAA,CAAA,SAAA;AAC9C,IAAM,QAAA,GAA8B,MAAY,KAAA,CAAA,WAAA;AAChD,IAAM,kBAAA,GAAwC,MAAY,KAAA,CAAA,qBAAA;AAC1D,IAAM,eAAA,GAAqC,MAAY,KAAA,CAAA,kBAAA;AACvD,IAAM,aAAA,GAAmC,MAA+B,eAAA,CAAA,gBAAA;AACxE,IAAM,UAAA,GAAgC,MAAY,KAAA,CAAA,aAAA;AAElD,IAAM,qBAAA,GAA2D;AAAA,EACtE,sBAAA,EAAwB,kBAAA;AAAA,EACxB,uBAAA,EAAyB,oBAAA;AAAA,EACzB,UAAA,EAAY,QAAA;AAAA,EACZ,cAAA,EAAgB,YAAA;AAAA,EAChB,eAAA,EAAiB,aAAA;AAAA,EACjB,iBAAA,EAAmB,cAAA;AAAA,EACnB,kBAAA,EAAoB,cAAA;AAAA,EACpB,mBAAA,EAAqB,gBAAA;AAAA,EACrB,QAAA,EAAU,MAAA;AAAA,EACV,UAAA,EAAY,QAAA;AAAA,EACZ,qBAAA,EAAuB,kBAAA;AAAA,EACvB,iBAAA,EAAmB,eAAA;AAAA,EACnB,iBAAA,EAAmB,aAAA;AAAA,EACnB,aAAA,EAAe;AACjB;AAEO,SAAS,yBACd,UAAA,EACM;AACN,EAAA,UAAA,CAAW,qBAAqB,CAAA;AAClC;AAEO,SAAS,yBAAA,GAAsC;AACpD,EAAA,OAAO,MAAA,CAAO,KAAK,qBAAqB,CAAA;AAC1C;AAEA,IAAO,6BAAA,GAAQ;AAAA,EACb,qBAAA;AAAA,EACA,wBAAA;AAAA,EACA,yBAAA;AAAA,EACA,kBAAA;AAAA,EACA,oBAAA;AAAA,EACA,QAAA;AAAA,EACA,YAAA;AAAA,EACA,aAAA;AAAA,EACA,cAAA;AAAA,EACA,cAAA;AAAA,EACA,gBAAA;AAAA,EACA,MAAA;AAAA,EACA,QAAA;AAAA,EACA,kBAAA;AAAA,EACA,eAAA;AAAA,EACA,aAAA;AAAA,EACA;AACF","file":"d3-projection-helpers.js","sourcesContent":["import type { ProjectionFactory } from './projection-loader'\nimport * as d3Geo from 'd3-geo'\nimport * as d3GeoProjection from 'd3-geo-projection'\n\nexport const azimuthalEqualArea: ProjectionFactory = () => d3Geo.geoAzimuthalEqualArea()\nexport const azimuthalEquidistant: ProjectionFactory = () => d3Geo.geoAzimuthalEquidistant()\nexport const gnomonic: ProjectionFactory = () => d3Geo.geoGnomonic()\nexport const orthographic: ProjectionFactory = () => d3Geo.geoOrthographic()\nexport const stereographic: ProjectionFactory = () => d3Geo.geoStereographic()\nexport const conicConformal: ProjectionFactory = () => d3Geo.geoConicConformal()\nexport const conicEqualArea: ProjectionFactory = () => d3Geo.geoConicEqualArea()\nexport const conicEquidistant: ProjectionFactory = () => d3Geo.geoConicEquidistant()\nexport const albers: ProjectionFactory = () => d3Geo.geoAlbers()\nexport const mercator: ProjectionFactory = () => d3Geo.geoMercator()\nexport const transverseMercator: ProjectionFactory = () => d3Geo.geoTransverseMercator()\nexport const equirectangular: ProjectionFactory = () => d3Geo.geoEquirectangular()\nexport const naturalEarth1: ProjectionFactory = () => (d3GeoProjection as any).geoNaturalEarth1()\nexport const equalEarth: ProjectionFactory = () => d3Geo.geoEqualEarth()\n\nexport const d3ProjectionFactories: Record<string, ProjectionFactory> = {\n 'azimuthal-equal-area': azimuthalEqualArea,\n 'azimuthal-equidistant': azimuthalEquidistant,\n 'gnomonic': gnomonic,\n 'orthographic': orthographic,\n 'stereographic': stereographic,\n 'conic-conformal': conicConformal,\n 'conic-equal-area': conicEqualArea,\n 'conic-equidistant': conicEquidistant,\n 'albers': albers,\n 'mercator': mercator,\n 'transverse-mercator': transverseMercator,\n 'equirectangular': equirectangular,\n 'natural-earth-1': naturalEarth1,\n 'equal-earth': equalEarth,\n}\n\nexport function registerAllD3Projections(\n registerFn: (factories: Record<string, ProjectionFactory>) => void,\n): void {\n registerFn(d3ProjectionFactories)\n}\n\nexport function getAvailableD3Projections(): string[] {\n return Object.keys(d3ProjectionFactories)\n}\n\nexport default {\n d3ProjectionFactories,\n registerAllD3Projections,\n getAvailableD3Projections,\n azimuthalEqualArea,\n azimuthalEquidistant,\n gnomonic,\n orthographic,\n stereographic,\n conicConformal,\n conicEqualArea,\n conicEquidistant,\n albers,\n mercator,\n transverseMercator,\n equirectangular,\n naturalEarth1,\n equalEarth,\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,38 +1,5 @@
|
|
|
1
|
-
import { CompositeProjectionConfig
|
|
1
|
+
import { CompositeProjectionConfig } from '@atlas-composer/specification';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Standalone Composite Projection Loader (Zero Dependencies)
|
|
5
|
-
*
|
|
6
|
-
* A pure JavaScript/TypeScript module that consumes exported composite projection
|
|
7
|
-
* configurations and creates D3-compatible projections using a plugin architecture.
|
|
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.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```typescript
|
|
15
|
-
* // Register projections first
|
|
16
|
-
* import * as d3 from 'd3-geo'
|
|
17
|
-
* import { registerProjection, loadCompositeProjection } from './standalone-projection-loader'
|
|
18
|
-
*
|
|
19
|
-
* registerProjection('mercator', () => d3.geoMercator())
|
|
20
|
-
* registerProjection('albers', () => d3.geoAlbers())
|
|
21
|
-
*
|
|
22
|
-
* // Then load your configuration
|
|
23
|
-
* const projection = loadCompositeProjection(config, { width: 800, height: 600 })
|
|
24
|
-
* ```
|
|
25
|
-
*
|
|
26
|
-
* @packageDocumentation
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Generic projection-like interface that matches D3 projections
|
|
31
|
-
* without requiring d3-geo as a dependency
|
|
32
|
-
*
|
|
33
|
-
* Note: D3 projections use getter/setter pattern where calling without
|
|
34
|
-
* arguments returns the current value, and with arguments sets and returns this.
|
|
35
|
-
*/
|
|
36
3
|
interface ProjectionLike {
|
|
37
4
|
(coordinates: [number, number]): [number, number] | null;
|
|
38
5
|
center?: {
|
|
@@ -73,9 +40,6 @@ interface ProjectionLike {
|
|
|
73
40
|
fitWidth?: (width: number, object: any) => ProjectionLike;
|
|
74
41
|
fitHeight?: (height: number, object: any) => ProjectionLike;
|
|
75
42
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Stream protocol interface for D3 geographic transforms
|
|
78
|
-
*/
|
|
79
43
|
interface StreamLike {
|
|
80
44
|
point: (x: number, y: number) => void;
|
|
81
45
|
lineStart: () => void;
|
|
@@ -84,166 +48,26 @@ interface StreamLike {
|
|
|
84
48
|
polygonEnd: () => void;
|
|
85
49
|
sphere?: () => void;
|
|
86
50
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Factory function that creates a projection instance
|
|
89
|
-
*/
|
|
90
51
|
type ProjectionFactory = () => ProjectionLike;
|
|
91
52
|
|
|
92
|
-
/**
|
|
93
|
-
* Projection parameters with loader-specific extensions.
|
|
94
|
-
* @deprecated Specification now includes all legacy parameters
|
|
95
|
-
*/
|
|
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;
|
|
115
|
-
/**
|
|
116
|
-
* Options for creating the composite projection
|
|
117
|
-
*/
|
|
118
53
|
interface LoaderOptions {
|
|
119
|
-
/** Canvas width in pixels */
|
|
120
54
|
width: number;
|
|
121
|
-
/** Canvas height in pixels */
|
|
122
55
|
height: number;
|
|
123
|
-
/** Whether to apply clipping to territories (default: true) */
|
|
124
56
|
enableClipping?: boolean;
|
|
125
|
-
/** Debug mode - logs territory selection (default: false) */
|
|
126
57
|
debug?: boolean;
|
|
127
58
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
* @param factory - Function that creates a new projection instance
|
|
142
|
-
*/
|
|
143
|
-
declare function registerProjection(id: string, factory: ProjectionFactory): void;
|
|
144
|
-
/**
|
|
145
|
-
* Register multiple projections at once
|
|
146
|
-
*
|
|
147
|
-
* @example
|
|
148
|
-
* ```typescript
|
|
149
|
-
* import * as d3 from 'd3-geo'
|
|
150
|
-
* import { registerProjections } from '@atlas-composer/projection-loader'
|
|
151
|
-
*
|
|
152
|
-
* registerProjections({
|
|
153
|
-
* 'mercator': () => d3.geoMercator(),
|
|
154
|
-
* 'albers': () => d3.geoAlbers(),
|
|
155
|
-
* 'conic-equal-area': () => d3.geoConicEqualArea()
|
|
156
|
-
* })
|
|
157
|
-
* ```
|
|
158
|
-
*
|
|
159
|
-
* @param factories - Object mapping projection IDs to factory functions
|
|
160
|
-
*/
|
|
161
|
-
declare function registerProjections(factories: Record<string, ProjectionFactory>): void;
|
|
162
|
-
/**
|
|
163
|
-
* Unregister a projection
|
|
164
|
-
*
|
|
165
|
-
* @param id - Projection identifier to remove
|
|
166
|
-
* @returns True if the projection was removed, false if it wasn't registered
|
|
167
|
-
*/
|
|
168
|
-
declare function unregisterProjection(id: string): boolean;
|
|
169
|
-
/**
|
|
170
|
-
* Clear all registered projections
|
|
171
|
-
*/
|
|
172
|
-
declare function clearProjections(): void;
|
|
173
|
-
/**
|
|
174
|
-
* Get list of currently registered projection IDs
|
|
175
|
-
*
|
|
176
|
-
* @returns Array of registered projection identifiers
|
|
177
|
-
*/
|
|
178
|
-
declare function getRegisteredProjections(): string[];
|
|
179
|
-
/**
|
|
180
|
-
* Check if a projection is registered
|
|
181
|
-
*
|
|
182
|
-
* @param id - Projection identifier to check
|
|
183
|
-
* @returns True if the projection is registered
|
|
184
|
-
*/
|
|
185
|
-
declare function isProjectionRegistered(id: string): boolean;
|
|
186
|
-
/**
|
|
187
|
-
* Create a minimal projection wrapper (similar to d3.geoProjection)
|
|
188
|
-
* This allows us to avoid the d3-geo dependency
|
|
189
|
-
*/
|
|
190
|
-
/**
|
|
191
|
-
* Create a D3-compatible projection from an exported composite projection configuration
|
|
192
|
-
*
|
|
193
|
-
* @example
|
|
194
|
-
* ```typescript
|
|
195
|
-
* import * as d3 from 'd3-geo'
|
|
196
|
-
* import { registerProjection, loadCompositeProjection } from '@atlas-composer/projection-loader'
|
|
197
|
-
*
|
|
198
|
-
* // Register projections first
|
|
199
|
-
* registerProjection('mercator', () => d3.geoMercator())
|
|
200
|
-
* registerProjection('albers', () => d3.geoAlbers())
|
|
201
|
-
*
|
|
202
|
-
* // Load configuration
|
|
203
|
-
* const config = JSON.parse(jsonString)
|
|
204
|
-
*
|
|
205
|
-
* // Create projection
|
|
206
|
-
* const projection = loadCompositeProjection(config, {
|
|
207
|
-
* width: 800,
|
|
208
|
-
* height: 600
|
|
209
|
-
* })
|
|
210
|
-
*
|
|
211
|
-
* // Use with D3
|
|
212
|
-
* const path = d3.geoPath(projection)
|
|
213
|
-
* svg.selectAll('path')
|
|
214
|
-
* .data(countries.features)
|
|
215
|
-
* .join('path')
|
|
216
|
-
* .attr('d', path)
|
|
217
|
-
* ```
|
|
218
|
-
*
|
|
219
|
-
* @param config - Exported composite projection configuration
|
|
220
|
-
* @param options - Canvas dimensions and options
|
|
221
|
-
* @returns D3-compatible projection that routes geometry to appropriate sub-projections
|
|
222
|
-
*/
|
|
223
|
-
declare function loadCompositeProjection(config: ExportedConfig, options: LoaderOptions): ProjectionLike;
|
|
224
|
-
/**
|
|
225
|
-
* Validate an exported configuration
|
|
226
|
-
*
|
|
227
|
-
* @param config - Configuration to validate
|
|
228
|
-
* @returns True if valid, throws error otherwise
|
|
229
|
-
*/
|
|
230
|
-
declare function validateConfig(config: any): config is ExportedConfig;
|
|
231
|
-
/**
|
|
232
|
-
* Load composite projection from JSON string
|
|
233
|
-
*
|
|
234
|
-
* @example
|
|
235
|
-
* ```typescript
|
|
236
|
-
* import * as d3 from 'd3-geo'
|
|
237
|
-
* import { registerProjection, loadFromJSON } from '@atlas-composer/projection-loader'
|
|
238
|
-
*
|
|
239
|
-
* // Register projections first
|
|
240
|
-
* registerProjection('mercator', () => d3.geoMercator())
|
|
241
|
-
*
|
|
242
|
-
* // Load from JSON
|
|
243
|
-
* const jsonString = fs.readFileSync('france-composite.json', 'utf-8')
|
|
244
|
-
* const projection = loadFromJSON(jsonString, { width: 800, height: 600 })
|
|
245
|
-
* ```
|
|
246
|
-
*/
|
|
247
|
-
declare function loadFromJSON(jsonString: string, options: LoaderOptions): ProjectionLike;
|
|
59
|
+
declare class ProjectionLoader {
|
|
60
|
+
private factories;
|
|
61
|
+
register(id: string, factory: ProjectionFactory): void;
|
|
62
|
+
registerAll(factories: Record<string, ProjectionFactory>): void;
|
|
63
|
+
unregister(id: string): boolean;
|
|
64
|
+
clear(): void;
|
|
65
|
+
getRegistered(): string[];
|
|
66
|
+
isRegistered(id: string): boolean;
|
|
67
|
+
load(config: CompositeProjectionConfig, options: LoaderOptions): ProjectionLike;
|
|
68
|
+
loadFromJSON(jsonString: string, options: LoaderOptions): ProjectionLike;
|
|
69
|
+
private createSubProjection;
|
|
70
|
+
}
|
|
71
|
+
declare function validateConfig(config: unknown): asserts config is CompositeProjectionConfig;
|
|
248
72
|
|
|
249
|
-
export { type
|
|
73
|
+
export { type LoaderOptions, type ProjectionFactory, type ProjectionLike, ProjectionLoader, type StreamLike, validateConfig };
|
package/dist/index.js
CHANGED
|
@@ -1,187 +1,355 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
function resolveI18nString(value) {
|
|
5
|
-
if (typeof value === "string") {
|
|
6
|
-
return value;
|
|
7
|
-
}
|
|
8
|
-
return value.en || Object.values(value).find((v) => typeof v === "string") || "";
|
|
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;
|
|
9
4
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
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
|
+
};
|
|
18
12
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
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
|
+
];
|
|
21
19
|
}
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
function isProjectionRegistered(id) {
|
|
29
|
-
return projectionRegistry.has(id);
|
|
20
|
+
function normalizeBounds(bounds) {
|
|
21
|
+
if ("minLon" in bounds) {
|
|
22
|
+
return bounds;
|
|
23
|
+
}
|
|
24
|
+
return boundsFromArray(bounds);
|
|
30
25
|
}
|
|
31
|
-
function
|
|
32
|
-
const {
|
|
33
|
-
|
|
34
|
-
|
|
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;
|
|
33
|
+
}
|
|
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}]`);
|
|
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}]`);
|
|
53
|
+
}
|
|
54
|
+
return { coordinates: result, territoryId: id };
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (debug) {
|
|
58
|
+
console.warn(`[Invert] Error in ${id}:`, error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
35
61
|
}
|
|
36
|
-
if (
|
|
37
|
-
|
|
62
|
+
if (debug) {
|
|
63
|
+
console.log(`[Invert] Failed to invert [${x}, ${y}]`);
|
|
38
64
|
}
|
|
39
|
-
|
|
40
|
-
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function createStreamMultiplexer(projections) {
|
|
68
|
+
return (stream) => {
|
|
69
|
+
const streams = projections.map((p) => p.stream(stream));
|
|
41
70
|
return {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
71
|
+
point: (x, y) => {
|
|
72
|
+
for (const s of streams) s.point(x, y);
|
|
73
|
+
},
|
|
74
|
+
lineStart: () => {
|
|
75
|
+
for (const s of streams) s.lineStart();
|
|
76
|
+
},
|
|
77
|
+
lineEnd: () => {
|
|
78
|
+
for (const s of streams) s.lineEnd();
|
|
79
|
+
},
|
|
80
|
+
polygonStart: () => {
|
|
81
|
+
for (const s of streams) s.polygonStart();
|
|
82
|
+
},
|
|
83
|
+
polygonEnd: () => {
|
|
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
|
+
}
|
|
50
91
|
}
|
|
51
92
|
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
93
|
+
};
|
|
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;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function normalizeBounds2(bounds) {
|
|
121
|
+
if ("minLon" in bounds) {
|
|
122
|
+
return bounds;
|
|
58
123
|
}
|
|
59
|
-
|
|
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;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
};
|
|
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;
|
|
161
|
+
if (arguments.length === 0) {
|
|
162
|
+
return ((_a = entries[0]) == null ? void 0 : _a.projection.scale()) ?? 1;
|
|
163
|
+
}
|
|
164
|
+
return composite;
|
|
165
|
+
};
|
|
166
|
+
composite.translate = function(_t) {
|
|
167
|
+
var _a;
|
|
168
|
+
if (arguments.length === 0) {
|
|
169
|
+
return ((_a = entries[0]) == null ? void 0 : _a.projection.translate()) ?? [0, 0];
|
|
170
|
+
}
|
|
171
|
+
return composite;
|
|
172
|
+
};
|
|
60
173
|
return composite;
|
|
61
174
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (projection.translate) {
|
|
108
|
-
const [offsetX, offsetY] = layout.translateOffset || [0, 0];
|
|
109
|
-
projection.translate([
|
|
110
|
-
width / 2 + offsetX,
|
|
111
|
-
height / 2 + offsetY
|
|
112
|
-
]);
|
|
113
|
-
}
|
|
114
|
-
if (layout.pixelClipExtent && projection.clipExtent) {
|
|
115
|
-
const territoryCenter = ((_a = projection.translate) == null ? void 0 : _a.call(projection)) || [width / 2, height / 2];
|
|
116
|
-
const clipExtent = calculateClipExtentFromPixelOffset(
|
|
117
|
-
territoryCenter,
|
|
118
|
-
layout.pixelClipExtent
|
|
119
|
-
);
|
|
120
|
-
projection.clipExtent(clipExtent);
|
|
175
|
+
|
|
176
|
+
// src/projection-loader.ts
|
|
177
|
+
var ProjectionLoader = class {
|
|
178
|
+
factories = /* @__PURE__ */ new Map();
|
|
179
|
+
register(id, factory) {
|
|
180
|
+
this.factories.set(id, factory);
|
|
181
|
+
}
|
|
182
|
+
registerAll(factories) {
|
|
183
|
+
for (const [id, factory] of Object.entries(factories)) {
|
|
184
|
+
this.register(id, factory);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
unregister(id) {
|
|
188
|
+
return this.factories.delete(id);
|
|
189
|
+
}
|
|
190
|
+
clear() {
|
|
191
|
+
this.factories.clear();
|
|
192
|
+
}
|
|
193
|
+
getRegistered() {
|
|
194
|
+
return Array.from(this.factories.keys());
|
|
195
|
+
}
|
|
196
|
+
isRegistered(id) {
|
|
197
|
+
return this.factories.has(id);
|
|
198
|
+
}
|
|
199
|
+
load(config, options) {
|
|
200
|
+
const { width, height, debug = false } = options;
|
|
201
|
+
if (config.version !== "1.0") {
|
|
202
|
+
throw new Error(`Unsupported configuration version: ${config.version}`);
|
|
203
|
+
}
|
|
204
|
+
if (!config.territories || config.territories.length === 0) {
|
|
205
|
+
throw new Error("Configuration must contain at least one territory");
|
|
206
|
+
}
|
|
207
|
+
const entries = config.territories.map((territory) => {
|
|
208
|
+
const proj = this.createSubProjection(territory, width, height, config.referenceScale, debug);
|
|
209
|
+
return {
|
|
210
|
+
id: territory.code,
|
|
211
|
+
projection: proj,
|
|
212
|
+
bounds: {
|
|
213
|
+
minLon: territory.bounds[0][0],
|
|
214
|
+
minLat: territory.bounds[0][1],
|
|
215
|
+
maxLon: territory.bounds[1][0],
|
|
216
|
+
maxLat: territory.bounds[1][1]
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
});
|
|
121
220
|
if (debug) {
|
|
122
|
-
console.log(
|
|
123
|
-
|
|
124
|
-
|
|
221
|
+
console.log("[CompositeProjection] Created sub-projections:", {
|
|
222
|
+
territories: config.territories.map((t) => t.code),
|
|
223
|
+
count: entries.length
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return buildCompositeProjection({ entries, debug });
|
|
227
|
+
}
|
|
228
|
+
loadFromJSON(jsonString, options) {
|
|
229
|
+
let config;
|
|
230
|
+
try {
|
|
231
|
+
config = JSON.parse(jsonString);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
234
|
+
}
|
|
235
|
+
validateConfig(config);
|
|
236
|
+
return this.load(config, options);
|
|
237
|
+
}
|
|
238
|
+
createSubProjection(territory, width, height, referenceScale, debug) {
|
|
239
|
+
var _a, _b, _c;
|
|
240
|
+
const { layout } = territory;
|
|
241
|
+
const projectionId = territory.projection.id;
|
|
242
|
+
const parameters = territory.projection.parameters;
|
|
243
|
+
if (!projectionId || !parameters) {
|
|
244
|
+
throw new Error(`Territory ${territory.code} missing projection configuration`);
|
|
245
|
+
}
|
|
246
|
+
const factory = this.factories.get(projectionId);
|
|
247
|
+
if (!factory) {
|
|
248
|
+
const registered = this.getRegistered();
|
|
249
|
+
const availableList = registered.length > 0 ? registered.join(", ") : "none";
|
|
250
|
+
throw new Error(
|
|
251
|
+
`Projection "${projectionId}" is not registered. Available projections: ${availableList}. Use loader.register('${projectionId}', factory) to register it.`
|
|
125
252
|
);
|
|
126
253
|
}
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
254
|
+
const projection = factory();
|
|
255
|
+
const hasFocus = parameters.focusLongitude !== void 0 && parameters.focusLatitude !== void 0;
|
|
256
|
+
const projFamily = territory.projection.family;
|
|
257
|
+
if (hasFocus && projFamily === "CONIC" && projection.rotate) {
|
|
258
|
+
projection.rotate([-parameters.focusLongitude, -parameters.focusLatitude, 0]);
|
|
259
|
+
} else if (hasFocus && projection.center) {
|
|
260
|
+
projection.center([parameters.focusLongitude, parameters.focusLatitude]);
|
|
261
|
+
} else if (parameters.center && projection.center) {
|
|
262
|
+
projection.center(parameters.center);
|
|
263
|
+
}
|
|
264
|
+
if (parameters.rotate && projection.rotate && !hasFocus) {
|
|
265
|
+
const rotate = Array.isArray(parameters.rotate) ? [...parameters.rotate, 0, 0].slice(0, 3) : [0, 0, 0];
|
|
266
|
+
projection.rotate(rotate);
|
|
267
|
+
}
|
|
268
|
+
if (parameters.parallels && projection.parallels) {
|
|
269
|
+
const parallels = Array.isArray(parameters.parallels) ? [...parameters.parallels, 0].slice(0, 2) : [0, 60];
|
|
270
|
+
projection.parallels(parallels);
|
|
271
|
+
}
|
|
272
|
+
if (projection.scale && parameters.scaleMultiplier) {
|
|
273
|
+
const effectiveReferenceScale = referenceScale || 2700;
|
|
274
|
+
const calculatedScale = effectiveReferenceScale * parameters.scaleMultiplier;
|
|
275
|
+
projection.scale(calculatedScale);
|
|
276
|
+
}
|
|
277
|
+
if (parameters.clipAngle && projection.clipAngle) {
|
|
278
|
+
projection.clipAngle(parameters.clipAngle);
|
|
279
|
+
}
|
|
280
|
+
if (parameters.precision && projection.precision) {
|
|
281
|
+
projection.precision(parameters.precision);
|
|
282
|
+
}
|
|
283
|
+
if (projection.translate) {
|
|
284
|
+
const [offsetX, offsetY] = layout.translateOffset || [0, 0];
|
|
285
|
+
projection.translate([
|
|
286
|
+
width / 2 + offsetX,
|
|
287
|
+
height / 2 + offsetY
|
|
288
|
+
]);
|
|
289
|
+
}
|
|
290
|
+
if (layout.pixelClipExtent && projection.clipExtent) {
|
|
291
|
+
const territoryCenter = ((_a = projection.translate) == null ? void 0 : _a.call(projection)) || [width / 2, height / 2];
|
|
292
|
+
const clipExtent = calculateClipExtentFromPixelOffset(
|
|
293
|
+
territoryCenter,
|
|
294
|
+
layout.pixelClipExtent
|
|
295
|
+
);
|
|
137
296
|
projection.clipExtent(clipExtent);
|
|
138
297
|
if (debug) {
|
|
139
|
-
console.log(
|
|
298
|
+
console.log(
|
|
299
|
+
`[Clipping] Applied pixelClipExtent for ${territory.code}:`,
|
|
300
|
+
`original: ${JSON.stringify(layout.pixelClipExtent)} -> transformed: ${JSON.stringify(clipExtent)}`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
} else if (projection.clipExtent) {
|
|
304
|
+
const bounds = territory.bounds;
|
|
305
|
+
if (bounds && bounds.length === 2 && bounds[0].length === 2 && bounds[1].length === 2) {
|
|
306
|
+
const scale = ((_b = projection.scale) == null ? void 0 : _b.call(projection)) || 1;
|
|
307
|
+
const translate = ((_c = projection.translate) == null ? void 0 : _c.call(projection)) || [0, 0];
|
|
308
|
+
const padding = scale * 0.1;
|
|
309
|
+
const clipExtent = [
|
|
310
|
+
[translate[0] - padding, translate[1] - padding],
|
|
311
|
+
[translate[0] + padding, translate[1] + padding]
|
|
312
|
+
];
|
|
313
|
+
projection.clipExtent(clipExtent);
|
|
314
|
+
if (debug) {
|
|
315
|
+
console.log(`[Clipping] Applied default clip extent for ${territory.code}:`, clipExtent);
|
|
316
|
+
}
|
|
140
317
|
}
|
|
141
318
|
}
|
|
319
|
+
return projection;
|
|
142
320
|
}
|
|
143
|
-
|
|
144
|
-
}
|
|
321
|
+
};
|
|
145
322
|
function validateConfig(config) {
|
|
146
323
|
if (!config || typeof config !== "object") {
|
|
147
324
|
throw new Error("Configuration must be an object");
|
|
148
325
|
}
|
|
149
|
-
|
|
326
|
+
const cfg = config;
|
|
327
|
+
if (!cfg.version) {
|
|
150
328
|
throw new Error("Configuration must have a version field");
|
|
151
329
|
}
|
|
152
|
-
if (!
|
|
330
|
+
if (!cfg.metadata || typeof cfg.metadata !== "object" || !cfg.metadata.atlasId) {
|
|
153
331
|
throw new Error("Configuration must have metadata with atlasId");
|
|
154
332
|
}
|
|
155
|
-
if (!
|
|
333
|
+
if (!cfg.territories || !Array.isArray(cfg.territories)) {
|
|
156
334
|
throw new Error("Configuration must have territories array");
|
|
157
335
|
}
|
|
158
|
-
if (
|
|
336
|
+
if (cfg.territories.length === 0) {
|
|
159
337
|
throw new Error("Configuration must have at least one territory");
|
|
160
338
|
}
|
|
161
|
-
for (const territory of
|
|
339
|
+
for (const territory of cfg.territories) {
|
|
162
340
|
if (!territory.code) {
|
|
163
341
|
throw new Error(`Territory missing required field 'code': ${JSON.stringify(territory)}`);
|
|
164
342
|
}
|
|
165
|
-
|
|
343
|
+
const proj = territory.projection;
|
|
344
|
+
if (!proj || !proj.id || !proj.parameters) {
|
|
166
345
|
throw new Error(`Territory ${territory.code} missing projection configuration. Required: projection.id and projection.parameters`);
|
|
167
346
|
}
|
|
168
347
|
if (!territory.bounds) {
|
|
169
348
|
throw new Error(`Territory ${territory.code} missing bounds`);
|
|
170
349
|
}
|
|
171
350
|
}
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
function loadFromJSON(jsonString, options) {
|
|
175
|
-
let config;
|
|
176
|
-
try {
|
|
177
|
-
config = JSON.parse(jsonString);
|
|
178
|
-
} catch (error) {
|
|
179
|
-
throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
180
|
-
}
|
|
181
|
-
validateConfig(config);
|
|
182
|
-
return loadCompositeProjection(config, options);
|
|
183
351
|
}
|
|
184
352
|
|
|
185
|
-
export {
|
|
353
|
+
export { ProjectionLoader, validateConfig };
|
|
186
354
|
//# sourceMappingURL=index.js.map
|
|
187
355
|
//# sourceMappingURL=index.js.map
|
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 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"]}
|
|
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/projection-loader.ts"],"names":["normalizeBounds"],"mappings":"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","file":"index.js","sourcesContent":["import type { GeoBounds, GeoBoundsArray } from '../types'\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\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\nexport function boundsToArray(bounds: GeoBounds): GeoBoundsArray {\n return [\n [bounds.minLon, bounds.minLat],\n [bounds.maxLon, bounds.maxLat],\n ]\n}\n","import type { ClipExtentOptions, GeoBounds, ProjectionLike } from '../types'\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 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\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","import type { GeoBounds, GeoBoundsArray, InvertOptions, InvertResult, SubProjectionEntry } from '../types'\n\nimport { boundsFromArray, isPointInBounds } from '../bounds/checker'\n\nfunction normalizeBounds(bounds: GeoBounds | GeoBoundsArray): GeoBounds {\n if ('minLon' in bounds) {\n return bounds\n }\n return boundsFromArray(bounds)\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 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","import type { ProjectionLike, StreamLike } from '../types'\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","import type { PointCaptureResult, StreamLike } from '../types'\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","import 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\nfunction normalizeBounds(bounds: GeoBounds | GeoBoundsArray): GeoBounds {\n if ('minLon' in bounds) {\n return bounds\n }\n return boundsFromArray(bounds)\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 const { pointStream, getCapturedPoint, resetCapture } = createPointCaptureStream()\n\n const entryStreams = entries.map(entry => ({\n entry,\n stream: entry.projection.stream(pointStream),\n }))\n\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 const composite = project as ProjectionLike\n\n composite.stream = createStreamMultiplexer(entries.map(e => e.projection))\n\n composite.invert = (coords: [number, number]) => {\n const result = invertWithBoundsValidation(coords, entries, { debug })\n return result?.coordinates ?? null\n }\n\n composite.scale = function (_s?: number): any {\n if (arguments.length === 0) {\n return entries[0]?.projection.scale() ?? 1\n }\n return composite\n } as ProjectionLike['scale']\n\n composite.translate = function (_t?: [number, number]): any {\n if (arguments.length === 0) {\n return entries[0]?.projection.translate() ?? [0, 0]\n }\n return composite\n } as ProjectionLike['translate']\n\n return composite\n}\n","import type { ProjectionLike as CoreProjectionLike, SubProjectionEntry } from '@atlas-composer/projection-core'\nimport type {\n CompositeProjectionConfig,\n GeoBounds,\n LayoutConfig,\n TerritoryConfig,\n} from '@atlas-composer/specification'\n\nimport {\n buildCompositeProjection,\n calculateClipExtentFromPixelOffset,\n} from '@atlas-composer/projection-core'\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\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\nexport type ProjectionFactory = () => ProjectionLike\n\nexport type { CompositeProjectionConfig, GeoBounds, LayoutConfig, TerritoryConfig }\n\nexport interface LoaderOptions {\n width: number\n height: number\n enableClipping?: boolean\n debug?: boolean\n}\n\nexport class ProjectionLoader {\n private factories = new Map<string, ProjectionFactory>()\n\n register(id: string, factory: ProjectionFactory): void {\n this.factories.set(id, factory)\n }\n\n registerAll(factories: Record<string, ProjectionFactory>): void {\n for (const [id, factory] of Object.entries(factories)) {\n this.register(id, factory)\n }\n }\n\n unregister(id: string): boolean {\n return this.factories.delete(id)\n }\n\n clear(): void {\n this.factories.clear()\n }\n\n getRegistered(): string[] {\n return Array.from(this.factories.keys())\n }\n\n isRegistered(id: string): boolean {\n return this.factories.has(id)\n }\n\n load(config: CompositeProjectionConfig, options: LoaderOptions): ProjectionLike {\n const { width, height, debug = false } = options\n\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 const entries: SubProjectionEntry[] = config.territories.map((territory) => {\n const proj = this.createSubProjection(territory, width, height, config.referenceScale, debug)\n\n return {\n id: territory.code,\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 => t.code),\n count: entries.length,\n })\n }\n\n return buildCompositeProjection({ entries, debug }) as ProjectionLike\n }\n\n loadFromJSON(jsonString: string, options: LoaderOptions): ProjectionLike {\n let config: unknown\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 this.load(config, options)\n }\n\n private createSubProjection(\n territory: TerritoryConfig,\n width: number,\n height: number,\n referenceScale?: number,\n debug?: boolean,\n ): ProjectionLike {\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 const factory = this.factories.get(projectionId)\n if (!factory) {\n const registered = this.getRegistered()\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 loader.register('${projectionId}', factory) to register it.`,\n )\n }\n\n const projection = factory()\n\n const hasFocus = parameters.focusLongitude !== undefined && parameters.focusLatitude !== undefined\n const projFamily = territory.projection.family\n\n if (hasFocus && projFamily === 'CONIC' && projection.rotate) {\n projection.rotate([-parameters.focusLongitude!, -parameters.focusLatitude!, 0])\n }\n else if (hasFocus && projection.center) {\n projection.center([parameters.focusLongitude!, parameters.focusLatitude!])\n }\n else if (parameters.center && projection.center) {\n projection.center(parameters.center)\n }\n\n if (parameters.rotate && projection.rotate && !hasFocus) {\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 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 if (projection.scale && parameters.scaleMultiplier) {\n const effectiveReferenceScale = referenceScale || 2700\n const calculatedScale = effectiveReferenceScale * parameters.scaleMultiplier\n projection.scale(calculatedScale)\n }\n\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 if (projection.translate) {\n const [offsetX, offsetY] = layout.translateOffset || [0, 0]\n projection.translate([\n width / 2 + offsetX,\n height / 2 + offsetY,\n ])\n }\n\n if (layout.pixelClipExtent && projection.clipExtent) {\n const territoryCenter = projection.translate?.() || [width / 2, height / 2]\n\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 const bounds = territory.bounds\n if (bounds && bounds.length === 2 && bounds[0].length === 2 && bounds[1].length === 2) {\n const scale = projection.scale?.() || 1\n const translate = projection.translate?.() || [0, 0]\n\n const padding = scale * 0.1\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\nexport function validateConfig(config: unknown): asserts config is CompositeProjectionConfig {\n if (!config || typeof config !== 'object') {\n throw new Error('Configuration must be an object')\n }\n\n const cfg = config as Record<string, unknown>\n\n if (!cfg.version) {\n throw new Error('Configuration must have a version field')\n }\n\n if (!cfg.metadata || typeof cfg.metadata !== 'object' || !(cfg.metadata as Record<string, unknown>).atlasId) {\n throw new Error('Configuration must have metadata with atlasId')\n }\n\n if (!cfg.territories || !Array.isArray(cfg.territories)) {\n throw new Error('Configuration must have territories array')\n }\n\n if (cfg.territories.length === 0) {\n throw new Error('Configuration must have at least one territory')\n }\n\n for (const territory of cfg.territories as Record<string, unknown>[]) {\n if (!territory.code) {\n throw new Error(`Territory missing required field 'code': ${JSON.stringify(territory)}`)\n }\n\n const proj = territory.projection as Record<string, unknown> | undefined\n if (!proj || !proj.id || !proj.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"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlas-composer/projection-loader",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"description": "Zero-dependency standalone loader for composite map projections with plugin architecture. Supports Atlas composer 2.0+ presets with backward compatibility.",
|
|
6
6
|
"author": "Lucas Poulain (ShallowRed)",
|
|
7
7
|
"license": "MIT",
|
|
@@ -75,10 +75,6 @@
|
|
|
75
75
|
"optional": true
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
|
-
"dependencies": {
|
|
79
|
-
"@atlas-composer/projection-core": "1.0.1",
|
|
80
|
-
"@atlas-composer/specification": "1.0.1"
|
|
81
|
-
},
|
|
82
78
|
"devDependencies": {
|
|
83
79
|
"@size-limit/preset-small-lib": "^11.2.0",
|
|
84
80
|
"@types/d3-geo": "^3.1.0",
|
|
@@ -86,7 +82,9 @@
|
|
|
86
82
|
"d3-geo-projection": "^4.0.0",
|
|
87
83
|
"tsup": "^8.0.0",
|
|
88
84
|
"typescript": "~5.9.3",
|
|
89
|
-
"vitest": "^3.2.4"
|
|
85
|
+
"vitest": "^3.2.4",
|
|
86
|
+
"@atlas-composer/projection-core": "1.0.1",
|
|
87
|
+
"@atlas-composer/specification": "1.0.1"
|
|
90
88
|
},
|
|
91
89
|
"scripts": {
|
|
92
90
|
"build": "tsup",
|