1+ class PROJJSONBuilderBase {
2+ static getId ( node ) {
3+ const idNode = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "ID" ) ;
4+ if ( idNode && idNode . length >= 3 ) {
5+ return {
6+ authority : idNode [ 1 ] ,
7+ code : parseInt ( idNode [ 2 ] , 10 ) ,
8+ } ;
9+ }
10+ return null ;
11+ }
12+
13+ static convertUnit ( node , type = "unit" ) {
14+ if ( ! node || node . length < 3 ) {
15+ return { type, name : "unknown" , conversion_factor : null } ;
16+ }
17+
18+ const name = node [ 1 ] ;
19+ const conversionFactor = parseFloat ( node [ 2 ] ) || null ;
20+
21+ const idNode = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "ID" ) ;
22+ const id = idNode
23+ ? {
24+ authority : idNode [ 1 ] ,
25+ code : parseInt ( idNode [ 2 ] , 10 ) ,
26+ }
27+ : null ;
28+
29+ return {
30+ type,
31+ name,
32+ conversion_factor : conversionFactor ,
33+ id,
34+ } ;
35+ }
36+
37+ static convertAxis ( node ) {
38+ const name = node [ 1 ] || "Unknown" ;
39+
40+ // Determine the direction
41+ let direction ;
42+ const abbreviationMatch = name . match ( / ^ \( ( .) \) $ / ) ; // Match abbreviations like "(E)" or "(N)"
43+ if ( abbreviationMatch ) {
44+ // Use the abbreviation to determine the direction
45+ const abbreviation = abbreviationMatch [ 1 ] . toUpperCase ( ) ;
46+ if ( abbreviation === 'E' ) direction = 'east' ;
47+ else if ( abbreviation === 'N' ) direction = 'north' ;
48+ else if ( abbreviation === 'U' ) direction = 'up' ;
49+ else throw new Error ( `Unknown axis abbreviation: ${ abbreviation } ` ) ;
50+ } else {
51+ // Use the explicit direction provided in the AXIS node
52+ direction = node [ 2 ] ?. toLowerCase ( ) || "unknown" ;
53+ }
54+
55+ const orderNode = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "ORDER" ) ;
56+ const order = orderNode ? parseInt ( orderNode [ 1 ] , 10 ) : null ;
57+
58+ const unitNode = node . find (
59+ ( child ) =>
60+ Array . isArray ( child ) &&
61+ ( child [ 0 ] === "LENGTHUNIT" || child [ 0 ] === "ANGLEUNIT" || child [ 0 ] === "SCALEUNIT" )
62+ ) ;
63+ const unit = this . convertUnit ( unitNode ) ;
64+
65+ return {
66+ name,
67+ direction, // Use the valid PROJJSON direction value
68+ unit,
69+ order,
70+ } ;
71+ }
72+
73+ static extractAxes ( node ) {
74+ return node
75+ . filter ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "AXIS" )
76+ . map ( ( axis ) => this . convertAxis ( axis ) )
77+ . sort ( ( a , b ) => ( a . order || 0 ) - ( b . order || 0 ) ) ; // Sort by the "order" property
78+ }
79+
80+ static convert ( node , result = { } ) {
81+
82+ switch ( node [ 0 ] ) {
83+ case "PROJCRS" :
84+ result . type = "ProjectedCRS" ;
85+ result . name = node [ 1 ] ;
86+ result . base_crs = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "BASEGEOGCRS" )
87+ ? this . convert ( node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "BASEGEOGCRS" ) )
88+ : null ;
89+ result . conversion = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "CONVERSION" )
90+ ? this . convert ( node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "CONVERSION" ) )
91+ : null ;
92+
93+ const csNode = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "CS" ) ;
94+ if ( csNode ) {
95+ result . coordinate_system = {
96+ type : csNode [ 1 ] ,
97+ axis : this . extractAxes ( node ) ,
98+ } ;
99+ }
100+
101+ const lengthUnitNode = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "LENGTHUNIT" ) ;
102+ if ( lengthUnitNode ) {
103+ const unit = this . convertUnit ( lengthUnitNode ) ;
104+ result . coordinate_system . unit = unit ; // Add unit to coordinate_system
105+ }
106+
107+ result . id = this . getId ( node ) ;
108+ break ;
109+
110+ case "BASEGEOGCRS" :
111+ case "GEOGCRS" :
112+ result . type = "GeographicCRS" ;
113+ result . name = node [ 1 ] ;
114+
115+ // Handle DATUM or ENSEMBLE
116+ const datumOrEnsembleNode = node . find (
117+ ( child ) => Array . isArray ( child ) && ( child [ 0 ] === "DATUM" || child [ 0 ] === "ENSEMBLE" )
118+ ) ;
119+ if ( datumOrEnsembleNode ) {
120+ const datumOrEnsemble = this . convert ( datumOrEnsembleNode ) ;
121+ if ( datumOrEnsembleNode [ 0 ] === "ENSEMBLE" ) {
122+ result . datum_ensemble = datumOrEnsemble ;
123+ } else {
124+ result . datum = datumOrEnsemble ;
125+ }
126+ const primem = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "PRIMEM" ) ;
127+ if ( primem && primem [ 1 ] !== 'Greenwich' ) {
128+ datumOrEnsemble . prime_meridian = {
129+ name : primem [ 1 ] ,
130+ longitude : parseFloat ( primem [ 2 ] ) ,
131+ }
132+ }
133+ }
134+
135+ result . coordinate_system = {
136+ type : "ellipsoidal" ,
137+ axis : this . extractAxes ( node ) ,
138+ } ;
139+
140+ result . id = this . getId ( node ) ;
141+ break ;
142+
143+ case "DATUM" :
144+ result . type = "GeodeticReferenceFrame" ;
145+ result . name = node [ 1 ] ;
146+ result . ellipsoid = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "ELLIPSOID" )
147+ ? this . convert ( node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "ELLIPSOID" ) )
148+ : null ;
149+ break ;
150+
151+ case "ENSEMBLE" :
152+ result . type = "DatumEnsemble" ;
153+ result . name = node [ 1 ] ;
154+
155+ // Extract ensemble members
156+ result . members = node
157+ . filter ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "MEMBER" )
158+ . map ( ( member ) => ( {
159+ type : "DatumEnsembleMember" ,
160+ name : member [ 1 ] ,
161+ id : this . getId ( member ) , // Extract ID as { authority, code }
162+ } ) ) ;
163+
164+ // Extract accuracy
165+ const accuracyNode = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "ENSEMBLEACCURACY" ) ;
166+ if ( accuracyNode ) {
167+ result . accuracy = parseFloat ( accuracyNode [ 1 ] ) ;
168+ }
169+
170+ // Extract ellipsoid
171+ const ellipsoidNode = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "ELLIPSOID" ) ;
172+ if ( ellipsoidNode ) {
173+ result . ellipsoid = this . convert ( ellipsoidNode ) ; // Convert the ellipsoid node
174+ }
175+
176+ // Extract identifier for the ensemble
177+ result . id = this . getId ( node ) ;
178+ break ;
179+
180+ case "ELLIPSOID" :
181+ result . type = "Ellipsoid" ;
182+ result . name = node [ 1 ] ;
183+ result . semi_major_axis = parseFloat ( node [ 2 ] ) ;
184+ result . inverse_flattening = parseFloat ( node [ 3 ] ) ;
185+ const units = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "LENGTHUNIT" )
186+ ? this . convert ( node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "LENGTHUNIT" ) , result )
187+ : null ;
188+ break ;
189+
190+ case "CONVERSION" :
191+ result . type = "Conversion" ;
192+ result . name = node [ 1 ] ;
193+ result . method = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "METHOD" )
194+ ? this . convert ( node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "METHOD" ) )
195+ : null ;
196+ result . parameters = node
197+ . filter ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "PARAMETER" )
198+ . map ( ( param ) => this . convert ( param ) ) ;
199+ break ;
200+
201+ case "METHOD" :
202+ result . type = "Method" ;
203+ result . name = node [ 1 ] ;
204+ result . id = this . getId ( node ) ;
205+ break ;
206+
207+ case "PARAMETER" :
208+ result . type = "Parameter" ;
209+ result . name = node [ 1 ] ;
210+ result . value = parseFloat ( node [ 2 ] ) ;
211+ result . unit = this . convertUnit (
212+ node . find (
213+ ( child ) =>
214+ Array . isArray ( child ) &&
215+ ( child [ 0 ] === "LENGTHUNIT" || child [ 0 ] === "ANGLEUNIT" || child [ 0 ] === "SCALEUNIT" )
216+ )
217+ ) ;
218+ result . id = this . getId ( node ) ;
219+ break ;
220+
221+ case "BOUNDCRS" :
222+ result . type = "BoundCRS" ;
223+
224+ // Process SOURCECRS
225+ const sourceCrsNode = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "SOURCECRS" ) ;
226+ if ( sourceCrsNode ) {
227+ const sourceCrsContent = sourceCrsNode . find ( ( child ) => Array . isArray ( child ) ) ;
228+ result . source_crs = sourceCrsContent ? this . convert ( sourceCrsContent ) : null ;
229+ }
230+
231+ // Process TARGETCRS
232+ const targetCrsNode = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "TARGETCRS" ) ;
233+ if ( targetCrsNode ) {
234+ const targetCrsContent = targetCrsNode . find ( ( child ) => Array . isArray ( child ) ) ;
235+ result . target_crs = targetCrsContent ? this . convert ( targetCrsContent ) : null ;
236+ }
237+
238+ // Process ABRIDGEDTRANSFORMATION
239+ const transformationNode = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "ABRIDGEDTRANSFORMATION" ) ;
240+ if ( transformationNode ) {
241+ result . transformation = this . convert ( transformationNode ) ;
242+ } else {
243+ result . transformation = null ;
244+ }
245+ break ;
246+
247+ case "ABRIDGEDTRANSFORMATION" :
248+ result . type = "Transformation" ;
249+ result . name = node [ 1 ] ;
250+ result . method = node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "METHOD" )
251+ ? this . convert ( node . find ( ( child ) => Array . isArray ( child ) && child [ 0 ] === "METHOD" ) )
252+ : null ;
253+
254+ result . parameters = node
255+ . filter ( ( child ) => Array . isArray ( child ) && ( child [ 0 ] === "PARAMETER" || child [ 0 ] === "PARAMETERFILE" ) )
256+ . map ( ( param ) => {
257+ if ( param [ 0 ] === "PARAMETER" ) {
258+ return this . convert ( param ) ;
259+ } else if ( param [ 0 ] === "PARAMETERFILE" ) {
260+ return {
261+ name : param [ 1 ] ,
262+ value : param [ 2 ] ,
263+ id : {
264+ "authority" : "EPSG" ,
265+ "code" : 8656
266+ }
267+ } ;
268+ }
269+ } ) ;
270+
271+ // Adjust the Scale difference parameter if present
272+ if ( result . parameters . length === 7 ) {
273+ const scaleDifference = result . parameters [ 6 ] ;
274+ if ( scaleDifference . name === "Scale difference" ) {
275+ scaleDifference . value = Math . round ( ( scaleDifference . value - 1 ) * 1e12 ) / 1e6 ;
276+ }
277+ }
278+
279+ result . id = this . getId ( node ) ;
280+ break ;
281+
282+ case "AXIS" :
283+ if ( ! result . coordinate_system ) {
284+ result . coordinate_system = { type : "unspecified" , axis : [ ] } ;
285+ }
286+ result . coordinate_system . axis . push ( this . convertAxis ( node ) ) ;
287+ break ;
288+
289+ case "LENGTHUNIT" :
290+ const unit = this . convertUnit ( node , 'LinearUnit' ) ;
291+ if ( result . coordinate_system && result . coordinate_system . axis ) {
292+ result . coordinate_system . axis . forEach ( ( axis ) => {
293+ if ( ! axis . unit ) {
294+ axis . unit = unit ;
295+ }
296+ } ) ;
297+ }
298+ if ( unit . conversion_factor && unit . conversion_factor !== 1 ) {
299+ if ( result . semi_major_axis ) {
300+ result . semi_major_axis = {
301+ value : result . semi_major_axis ,
302+ unit,
303+ }
304+ }
305+ }
306+ break ;
307+
308+ default :
309+ result . keyword = node [ 0 ] ;
310+ break ;
311+ }
312+
313+ return result ;
314+ }
315+ }
316+
317+ export default PROJJSONBuilderBase ;
0 commit comments