-
Notifications
You must be signed in to change notification settings - Fork 21
/
index.ts
172 lines (141 loc) · 5.92 KB
/
index.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
import Geometry from './lib/geometry.js';
import Discovery from './lib/discovery.js';
import Fetch from './lib/fetch.js';
import EventEmitter from 'node:events';
import { Feature } from 'geojson';
import Err from '@openaddresses/batch-error';
import rewind from './lib/rewind.js';
import Schema from './lib/schema.js';
import {
JSONSchema6,
} from 'json-schema';
const SUPPORTED = ['FeatureServer', 'MapServer'];
export enum EsriDumpConfigApproach {
BBOX = 'bbox',
ITER = 'iter'
}
export enum EsriResourceType {
FeatureServer = 'FeatureServer',
MapServer = 'MapServer'
}
export interface EsriDumpConfigInput {
approach?: EsriDumpConfigApproach;
headers?: { [k: string]: string; };
params?: { [k: string]: string; };
}
export interface EsriDumpConfig {
approach: EsriDumpConfigApproach;
headers: { [k: string]: string; };
params: { [k: string]: string; };
}
export default class EsriDump extends EventEmitter {
url: URL;
config: EsriDumpConfig;
geomType: null | string;
resourceType: EsriResourceType;
constructor(url: string, config: EsriDumpConfigInput = {}) {
super();
this.url = new URL(url);
this.config = {
approach: config.approach || EsriDumpConfigApproach.BBOX,
headers: config.headers || {},
params: config.params || {}
};
// Validate URL is a "/rest/services/" endpoint
if (!this.url.pathname.includes('/rest/services/')) throw new Err(400, null, 'Did not recognize ' + url + ' as an ArcGIS /rest/services/ endpoint.');
this.geomType = null;
const occurrence = SUPPORTED.map((d) => { return url.lastIndexOf(d); });
const known = SUPPORTED[occurrence.indexOf(Math.max.apply(null, occurrence))];
if (known === 'MapServer') this.resourceType = EsriResourceType.MapServer;
else if (known === 'FeatureServer') this.resourceType = EsriResourceType.FeatureServer;
else throw new Err(400, null, 'Unknown or unsupported ESRI URL Format');
this.emit('type', this.resourceType);
}
async schema(): Promise<JSONSchema6> {
const metadata = await this.#fetchMeta();
return Schema(metadata);
}
async discover() {
try {
const discover = new Discovery(this.url);
discover.fetch(this.config);
discover.on('layer', (layer: any) => {
this.emit('layer', layer);
}).on('schema', (schema: JSONSchema6) => {
this.emit('schema', schema);
}).on('error', (error: Err) => {
this.emit('error', error);
}).on('done', () => {
this.emit('done');
});
} catch (err) {
this.emit('error', err);
}
}
async fetch(config?: {
map?: (g: Geometry, f: Feature) => Feature
}) {
if (!config) config = {};
const metadata = await this.#fetchMeta();
try {
const geom = new Geometry(this.url, metadata);
geom.fetch(this.config);
geom.on('feature', (feature: Feature) => {
feature = rewind(feature) as Feature;
if (config.map) feature = config.map(geom, feature);
this.emit('feature', feature);
}).on('error', (error: Err) => {
this.emit('error', error);
}).on('done', () => {
this.emit('done');
});
} catch (err) {
this.emit('error', err);
}
}
async #fetchMeta() {
const url = new URL(this.url);
if (process.env.DEBUG) console.error(String(url));
const res = await Fetch(this.config, url);
if (!res.ok) this.emit('error', await res.text());
// TODO: Type Defs
const metadata: any = await res.json();
if (metadata.error) {
return this.emit('error', new Err(400, null, 'Server metadata error: ' + metadata.error.message));
} else if (metadata.capabilities && metadata.capabilities.indexOf('Query') === -1 ) {
return this.emit('error', new Err(400, null, 'Layer doesn\'t support query operation.'));
} else if (metadata.folders || metadata.services) {
let errorMessage = 'Endpoint provided is not a Server resource.\n';
if (metadata.folders.length > 0) {
errorMessage += '\nChoose a Layer from a Service in one of these Folders: \n '
+ metadata.folders.join('\n ') + '\n';
}
if (metadata.services.length > 0 && Array.isArray(metadata.services)) {
errorMessage += '\nChoose a Layer from one of these Services: \n '
+ metadata.services.map((d: any) => { return d.name; }).join('\n ') + '\n';
}
return this.emit('error', new Err(400, null, errorMessage));
} else if (metadata.layers) {
let errorMessage = 'Endpoint provided is not a Server resource.\n';
if (metadata.layers.length > 0 && Array.isArray(metadata.layers)) {
errorMessage += '\nChoose one of these Layers: \n '
+ metadata.layers.map((d: any) => { return d.name; }).join('\n ') + '\n';
}
return this.emit('error', new Err(400, null, errorMessage));
} else if (!this.resourceType) {
return this.emit('error', new Err(400, null, 'Could not determine server type of ' + url));
}
this.geomType = metadata.geometryType;
if (!this.geomType) {
return this.emit('error', new Err(400, null, 'no geometry'));
} else if (!metadata.extent) {
return this.emit('error', new Err(400, null, 'Layer doesn\'t list an extent.'));
} else if ('subLayers' in metadata && metadata.subLayers.length > 0) {
return this.emit('error', new Err(400, null, 'Specified layer has sublayers.'));
}
return metadata;
}
}
export {
Geometry
}