-
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathgeoConverter.ts
400 lines (375 loc) ยท 12.7 KB
/
geoConverter.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
/* eslint-disable @typescript-eslint/naming-convention */
/**
* Loosely based on https://github.com/eugeneYWang/GeoJSON.ts
*/
export class GeoConverter {
// supported geometry object types
// see GeoJSON spec: https://www.rfc-editor.org/rfc/rfc7946.html#section-1.4
private geometryTypes = [
'Point',
'MultiPoint',
'LineString',
'MultiLineString',
'Polygon',
'MultiPolygon',
'GeoJSON'
];
// geometry object properties
private geometryProperties: Array<any> = [];
// default geo data conversion options
defaultOptions: object = {
doThrows: {
invalidGeometry: false
}
};
/**
* Geo data conversion errors.
*/
errors: object = {
invalidGeometryError: this.invalidGeometryError
};
/**
* Creates new Geo data converter instance.
*/
constructor() {
}
/**
* Converts array or data object to GeoJSON object.
* @param objects Data objects to convert.
* @param options Geo data conversion options.
* @param callback Optional callback for data conversion.
* @returns GeoJSON data object.
*/
public toGeo(objects: [] | object, options: object, callback?: Function): any {
let geoJson: any;
// apply geo data conversion default settings
let settings = this.applyDefaults(options, this.defaultOptions);
// reset geometry fields
this.geometryProperties.length = 0;
this.setGeometry(settings);
if (Array.isArray(objects)) {
// create geo features collection
geoJson = {type: 'FeatureCollection', features: []};
objects.forEach(item => {
const feature: any = this.getFeature(item, settings);
if (feature.geometry?.type !== undefined) { // has geometry type and coordinates
geoJson.features.push(feature);
}
});
this.addOptionalProperties(geoJson, settings);
}
else {
// create geo data object from a single data object
geoJson = this.getFeature(objects, settings);
this.addOptionalProperties(geoJson, settings);
}
if (callback && typeof callback === 'function') {
callback(geoJson);
}
else {
return geoJson;
}
}
/**
* Adds default settings to geo data parameters.
* Does not overwrite any data properties.
* Only adds additional defaults.
* @param params Geo data parameters.
* @param defaults Default settings.
* @returns
*/
private applyDefaults(params: any, defaults: any): any {
let settings: any = params || {};
for (let setting in settings) {
if (defaults.hasOwnProperty(setting) && !settings[setting]) {
// add default setting
settings[setting] = defaults[setting];
}
}
return settings;
}
/**
* Adds optional crs and bbox GeoJSON properties, if present.
* @param geoJson Geo data object to update.
* @param settings Geo data setttings.
*/
private addOptionalProperties(geoJson: any, settings: any) {
if (settings.crs && this.isValidCrs(settings.crs)) {
if (settings.isPostgres) {
geoJson.geometry.crs = settings.crs;
} else {
geoJson.crs = settings.crs;
}
}
if (settings.bbox) {
geoJson.bbox = settings.bbox;
}
if (settings.extraGlobal) {
geoJson.properties = {};
for (let key in settings.extraGlobal) {
geoJson.properties[key] = settings.extraGlobal[key];
}
}
}
/**
* Validates geo data CRS config structure.
* @param crs Crs to validate.
* @returns
*/
private isValidCrs(crs: any): boolean {
if (crs.type === 'name') {
if (crs.properties && crs.properties.name) {
return true;
}
else {
throw new Error('Invalid CRS. Properties must contain "name" key.');
}
}
else if (crs.type === 'link') {
if (crs.properties && crs.properties.href && crs.properties.type) {
return true;
}
else {
throw new Error('Invalid CRS. Properties must contain "href" and "type" key.');
}
}
else {
throw new Error('Invald CRS. Type attribute must be "name" or "link".');
}
}
/**
* Moves geometry settings to the `geometry` key for easier access.
* @param settings Geometry data settings.
*/
private setGeometry(settings: any): void {
settings.geometry = {};
for (let propertyName in settings) {
if (settings.hasOwnProperty(propertyName) &&
this.geometryTypes.indexOf(propertyName) >= 0) {
settings.geometry[propertyName] = settings[propertyName];
delete settings[propertyName];
}
}
this.setGeometryProperties(settings.geometry);
}
/**
* Adds fields with geometry data to geometry object properties.
* Geometry properties are used when adding properties to geo features,
* so that no geometry fields are added to the geo properties collection.
* @param geoSettings Geometry data settings.
*/
private setGeometryProperties(geoSettings: any): void {
for (let propertyName in geoSettings) {
if (geoSettings.hasOwnProperty(propertyName)) {
if (typeof geoSettings[propertyName] === 'string') {
this.geometryProperties.push(geoSettings[propertyName]);
}
else if (typeof geoSettings[propertyName] === 'object') {
// array of coordinates for Point object
this.geometryProperties.push(geoSettings[propertyName][0]);
this.geometryProperties.push(geoSettings[propertyName][1]);
}
}
}
if (this.geometryProperties.length === 0) {
throw new Error("No geometry attributes specified.");
}
}
/**
* Creates a Feature object for the GeoJSON features collection.
* @param item Data item object.
* @param settings Geo data conversion settings.
* @returns Feature object with geometry and data properties.
*/
private getFeature(item: any, settings: any): object {
let feature: any = {type: 'Feature'};
feature['geometry'] = this.buildGeometry(item, settings);
feature['properties'] = this.getDataProperties(item, settings);
return feature;
}
/**
* Creates data properties collection for the GeoJSON Feature object.
* @param item Data item object.
* @param settings Geo data conversion settings.
* @returns Feature object with geometry and data properties.
*/
private getDataProperties(item: any, settings: any): object {
let data: any = {};
// TODO: add include and extra data props support
// from: https://github.com/eugeneYWang/GeoJSON.ts/blob/master/geojson.ts#L343
for (let propertyName in item) {
if (item.hasOwnProperty(propertyName) &&
this.geometryProperties.indexOf(propertyName) === -1 &&
settings.exclude.indexOf(propertyName) === -1) {
// add it to geometry feature data properties
data[propertyName] = item[propertyName];
}
}
return data;
}
/**
* Checks for nested objects.
* @param value Object value.
* @returns
*/
private isNested(value: any) {
return /^.+\..+$/.test(value);
}
/**
* Creates geometry object for the geo data feature.
* @param item Data item.
* @param settings Geo data settings.
* @returns Geometry data object.
*/
private buildGeometry(item: any, settings: any): any {
let geometry: any = {};
for (let geometryType in settings.geometry) {
let geometryProperty = settings.geometry[geometryType];
if (typeof geometryProperty === 'string' && item.hasOwnProperty(geometryProperty)) {
// string point: {Point: 'coords'}
if (geometryType === 'GeoJSON') {
geometry = item[geometryProperty];
}
else {
geometry['type'] = geometryType;
geometry['coordinates'] = item[geometryProperty];
}
}
else if (typeof geometryProperty === 'object' && !Array.isArray(geometryProperty)) {
/* polygons of form
Polygon: {
northeast: ['lat', 'lng'],
southwest: ['lat', 'lng']
}
*/
let points: any = Object.keys(geometryProperty).map((key: string) => {
let order = geometryProperty[key];
let newItem = item[key];
return this.buildGeometry(newItem, {geometry: {Point: order}});
});
geometry['type'] = geometryType;
geometry['coordinates'] = [].concat(
points.map((point: any) => point.coordinates)
);
}
else if (Array.isArray(geometryProperty) &&
item.hasOwnProperty(geometryProperty[0]) &&
item.hasOwnProperty(geometryProperty[1]) &&
item.hasOwnProperty(geometryProperty[2])) {
// point coordinates with alt: {Point: ['lat', 'lng', 'alt']}
geometry['type'] = geometryType;
geometry['coordinates'] = [
Number(item[geometryProperty[1]]),
Number(item[geometryProperty[0]]),
Number(item[geometryProperty[2]])
];
}
else if (Array.isArray(geometryProperty) &&
item.hasOwnProperty(geometryProperty[0]) &&
item.hasOwnProperty(geometryProperty[1])) {
// point coordinates: {Point: ['lat', 'lng']}
geometry['type'] = geometryType;
geometry['coordinates'] = [Number(item[geometryProperty[1]]), Number(item[geometryProperty[0]])];
}
else if (Array.isArray(geometryProperty) &&
this.isNested(geometryProperty[0]) &&
this.isNested(geometryProperty[1]) &&
this.isNested(geometryProperty[2])) {
// nested point coordinates with alt: {Point: ['container.lat', 'container.lng', 'container.alt']}
let coordinates = [];
for (let i = 0; i < geometryProperty.length; i++) {
// i.e. 0 and 1
var paths = geometryProperty[i].split('.');
var itemClone = item;
for (var j = 0; j < paths.length; j++) {
if (!itemClone.hasOwnProperty(paths[j])) {
return false;
}
// iterate deeper into the object
itemClone = itemClone[paths[j]];
}
coordinates[i] = itemClone;
}
geometry['type'] = geometryType;
geometry['coordinates'] = [
Number(coordinates[1]),
Number(coordinates[0]),
Number(coordinates[2])
];
}
else if (Array.isArray(geometryProperty) &&
this.isNested(geometryProperty[0]) &&
this.isNested(geometryProperty[1])) {
// nested point coordinates: {Point: ['container.lat', 'container.lng']}
let coordinates = [];
for (let i = 0; i < geometryProperty.length; i++) {
// i.e. 0 and 1
let paths = geometryProperty[i].split(".");
let itemClone = item;
for (let j = 0; j < paths.length; j++) {
if (!itemClone.hasOwnProperty(paths[j])) {
return false;
}
// iterate deeper into the object
itemClone = itemClone[paths[j]];
}
coordinates[i] = itemClone;
}
geometry['type'] = geometryType;
geometry['coordinates'] = [Number(coordinates[1]), Number(coordinates[0])];
}
else if (Array.isArray(geometryProperty) &&
geometryProperty[0].constructor.name === 'Object' &&
Object.keys(geometryProperty[0])[0] === 'coordinates') {
// coordinates point: {Point: [{coordinates: [lat, lng]}]}
geometry['type'] = geometryType;
geometry['coordinates'] = [
Number(item.coordinates[geometryProperty[0].coordinates.indexOf('lng')]),
Number(item.coordinates[geometryProperty[0].coordinates.indexOf('lat')])
];
}
}
if (settings.doThrows &&
settings.doThrows.invalidGeometry &&
!this.isValidGeometry(geometry)) {
throw this.invalidGeometryError(item, settings);
}
return geometry;
}
/**
* Generates invalid geometry error.
* @param args Geometry data arguments.
*/
invalidGeometryError(...args: any[]): Error {
let errorArgs = (1 <= args.length) ? [].slice.call(args, 0) : [];
let item = errorArgs.shift();
let params = errorArgs.shift();
throw Error(`Invalid Geometry: item: ${JSON.stringify(item, null, 2)}
\n params: ${JSON.stringify(params, null, 2)}`);
}
/**
* Validates geometry object.
* @param geometry Geometry object to validate.
* @returns
*/
isValidGeometry(geometry: any): boolean {
if (!geometry || !Object.keys(geometry).length) {
return false;
}
return true;
};
/**
* Adds data contained in the `extra` parameter to geo data properties.
* @param properties Geo data properties to update.
* @param extra Extra properties to add.
* @returns Updated geo data properties.
*/
private addExtraProperties(properties: any, extra: any) {
for (var key in extra) {
if (extra.hasOwnProperty(key)) {
properties[key] = extra[key];
}
}
return properties;
}
}