osmfeatures 0.1.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/dist/index.js ADDED
@@ -0,0 +1,497 @@
1
+ import createHttpError from 'http-errors';
2
+ function resultFromFeatures(features, meta) {
3
+ return {
4
+ data: { type: 'FeatureCollection', features },
5
+ meta,
6
+ };
7
+ }
8
+ function metaFromHeaders(headers, featureCount) {
9
+ const returnedRaw = headers.get('X-Returned');
10
+ const parsed = returnedRaw != null && returnedRaw !== '' ? Number.parseInt(returnedRaw, 10) : Number.NaN;
11
+ const nextCursor = headers.get('X-Next-Cursor');
12
+ const unitsRaw = headers.get('X-Usage-Units-Charged');
13
+ const unitsParsed = unitsRaw != null && unitsRaw !== '' ? Number(unitsRaw) : Number.NaN;
14
+ const meta = {
15
+ returned: Number.isFinite(parsed) ? parsed : featureCount,
16
+ has_more: (headers.get('X-Has-More') || 'false').toLowerCase() === 'true',
17
+ next_cursor: nextCursor && nextCursor.length > 0 ? nextCursor : null,
18
+ };
19
+ if (Number.isFinite(unitsParsed)) {
20
+ meta.units_charged = unitsParsed;
21
+ }
22
+ return meta;
23
+ }
24
+ const DEFAULT_BASE_URL = 'https://api.maplark.com';
25
+ const DEFAULT_LIMIT = 1000;
26
+ const MAX_LIMIT = 6000;
27
+ const GEOJSON_ACCEPT = 'application/geo+json';
28
+ function isGeojsonAccept(accept) {
29
+ if (accept == null || accept.trim() === '') {
30
+ return true;
31
+ }
32
+ const media = accept.split(',', 1)[0].split(';', 1)[0].trim().toLowerCase();
33
+ return media === '*/*' || media === '*' || media === GEOJSON_ACCEPT;
34
+ }
35
+ function appError(status, code, detail, subtype) {
36
+ const err = createHttpError(status, detail);
37
+ err.code = code;
38
+ err.subtype = subtype;
39
+ return err;
40
+ }
41
+ function isPowerOfTwo(n) {
42
+ return Number.isInteger(n) && n >= 1 && (n & (n - 1)) === 0;
43
+ }
44
+ function optionalString(query, key) {
45
+ const raw = query[key];
46
+ if (raw == null || raw === '') {
47
+ return undefined;
48
+ }
49
+ if (Array.isArray(raw)) {
50
+ const first = raw[0];
51
+ if (first == null || first === '') {
52
+ return undefined;
53
+ }
54
+ return String(first);
55
+ }
56
+ return String(raw);
57
+ }
58
+ function optionalNumber(query, key) {
59
+ const raw = query[key];
60
+ if (raw == null || raw === '') {
61
+ return undefined;
62
+ }
63
+ if (typeof raw === 'number') {
64
+ return Number.isFinite(raw) ? raw : undefined;
65
+ }
66
+ const text = optionalString(query, key);
67
+ if (text == null) {
68
+ return undefined;
69
+ }
70
+ const parsed = Number.parseFloat(text);
71
+ return Number.isFinite(parsed) ? parsed : undefined;
72
+ }
73
+ function optionalBoolean(query, key) {
74
+ const raw = query[key];
75
+ if (raw == null || raw === '') {
76
+ return undefined;
77
+ }
78
+ if (typeof raw === 'boolean') {
79
+ return raw;
80
+ }
81
+ const text = String(Array.isArray(raw) ? raw[0] : raw).trim().toLowerCase();
82
+ if (text === 'true' || text === '1') {
83
+ return true;
84
+ }
85
+ if (text === 'false' || text === '0') {
86
+ return false;
87
+ }
88
+ return undefined;
89
+ }
90
+ function parseLimit(query, fallback = DEFAULT_LIMIT) {
91
+ const raw = optionalString(query, 'limit');
92
+ if (raw == null) {
93
+ return fallback;
94
+ }
95
+ const parsed = Number.parseInt(raw, 10);
96
+ if (!Number.isFinite(parsed)) {
97
+ return fallback;
98
+ }
99
+ if (parsed > MAX_LIMIT) {
100
+ throw appError(400, 'invalid_limit', `limit must be <= ${MAX_LIMIT}.`);
101
+ }
102
+ return parsed;
103
+ }
104
+ /** Parse `bbox_tiles` from a query map for `query_all` (client-side only). */
105
+ export function resolveBboxTiles(query = {}, fallback = 2) {
106
+ const raw = optionalString(query, 'bbox_tiles');
107
+ if (raw == null) {
108
+ return fallback;
109
+ }
110
+ const parsed = Number.parseInt(raw, 10);
111
+ if (!Number.isFinite(parsed) || !isPowerOfTwo(parsed)) {
112
+ throw appError(400, 'invalid_bbox_tiles', 'bbox_tiles must be a power of 2 (1, 2, 4, 8, …).');
113
+ }
114
+ return parsed;
115
+ }
116
+ /** Split bbox into `tileCount` tiles by repeated longest-side bisection. */
117
+ export function splitBbox(bbox, tileCount) {
118
+ if (!isPowerOfTwo(tileCount)) {
119
+ throw appError(400, 'invalid_bbox_tiles', 'bbox_tiles must be a power of 2 (1, 2, 4, 8, …).');
120
+ }
121
+ const parts = bbox.split(',').map((value) => Number.parseFloat(value.trim()));
122
+ if (parts.length !== 4 || parts.some((value) => !Number.isFinite(value))) {
123
+ throw appError(400, 'invalid_bbox', 'bbox must be min_lon,min_lat,max_lon,max_lat.');
124
+ }
125
+ let tiles = [
126
+ [parts[0], parts[1], parts[2], parts[3]],
127
+ ];
128
+ while (tiles.length < tileCount) {
129
+ const next = [];
130
+ for (const [minLon, minLat, maxLon, maxLat] of tiles) {
131
+ const lonSpan = maxLon - minLon;
132
+ const latSpan = maxLat - minLat;
133
+ if (lonSpan >= latSpan) {
134
+ const midLon = minLon + lonSpan / 2;
135
+ next.push([minLon, minLat, midLon, maxLat], [midLon, minLat, maxLon, maxLat]);
136
+ }
137
+ else {
138
+ const midLat = minLat + latSpan / 2;
139
+ next.push([minLon, minLat, maxLon, midLat], [minLon, midLat, maxLon, maxLat]);
140
+ }
141
+ }
142
+ tiles = next;
143
+ }
144
+ return tiles.map(([minLon, minLat, maxLon, maxLat]) => `${minLon},${minLat},${maxLon},${maxLat}`);
145
+ }
146
+ function buildFeaturesQuery(params) {
147
+ const query = new URLSearchParams();
148
+ query.set('bbox', params.bbox);
149
+ query.set('limit', String(params.limit));
150
+ if (params.cursor) {
151
+ query.set('cursor', params.cursor);
152
+ }
153
+ if (params.zoom != null) {
154
+ query.set('zoom', String(params.zoom));
155
+ }
156
+ if (params.around) {
157
+ query.set('around', params.around);
158
+ }
159
+ if (params.osmIds) {
160
+ query.set('osm_ids', params.osmIds);
161
+ }
162
+ if (params.minLengthM != null) {
163
+ query.set('min_length_m', String(params.minLengthM));
164
+ }
165
+ if (params.maxLengthM != null) {
166
+ query.set('max_length_m', String(params.maxLengthM));
167
+ }
168
+ if (params.minAreaM2 != null) {
169
+ query.set('min_area_m2', String(params.minAreaM2));
170
+ }
171
+ if (params.maxAreaM2 != null) {
172
+ query.set('max_area_m2', String(params.maxAreaM2));
173
+ }
174
+ if (params.disableBudgetWarning != null) {
175
+ query.set('disable_budget_warning', String(params.disableBudgetWarning));
176
+ }
177
+ if (params.centroid != null) {
178
+ query.set('centroid', String(params.centroid));
179
+ }
180
+ if (params.type) {
181
+ query.set('type', params.type);
182
+ }
183
+ if (params.shape) {
184
+ query.set('shape', params.shape);
185
+ }
186
+ for (const tag of params.tags ?? []) {
187
+ query.append('tags', tag);
188
+ }
189
+ for (const tag of params.orTags ?? []) {
190
+ query.append('or_tags', tag);
191
+ }
192
+ for (const tag of params.notTags ?? []) {
193
+ query.append('not_tags', tag);
194
+ }
195
+ return query;
196
+ }
197
+ function sleep(ms) {
198
+ return new Promise((resolve) => {
199
+ setTimeout(resolve, ms);
200
+ });
201
+ }
202
+ function featureKey(feature, index) {
203
+ if (feature && typeof feature === 'object') {
204
+ const record = feature;
205
+ if (record['id'] != null) {
206
+ return String(record['id']);
207
+ }
208
+ const props = record['properties'];
209
+ if (props && typeof props === 'object') {
210
+ const p = props;
211
+ if (p['osm_type'] != null && p['osm_id'] != null) {
212
+ return `${String(p['osm_type'])}/${String(p['osm_id'])}`;
213
+ }
214
+ }
215
+ }
216
+ return `fallback-${index}`;
217
+ }
218
+ function dedupeFeatures(features) {
219
+ const seen = new Set();
220
+ const deduped = [];
221
+ for (let index = 0; index < features.length; index += 1) {
222
+ const key = featureKey(features[index], index);
223
+ if (seen.has(key)) {
224
+ continue;
225
+ }
226
+ seen.add(key);
227
+ deduped.push(features[index]);
228
+ }
229
+ return deduped;
230
+ }
231
+ async function readUpstreamErrorDetail(upstream) {
232
+ try {
233
+ const rawBody = await upstream.text();
234
+ if (rawBody !== '') {
235
+ return rawBody;
236
+ }
237
+ }
238
+ catch {
239
+ // Fall through to status text.
240
+ }
241
+ return upstream.statusText || undefined;
242
+ }
243
+ export class OSMFeatures {
244
+ apiKey;
245
+ apiBaseUrl;
246
+ timeoutMs;
247
+ retryAttempts;
248
+ retryBaseMs;
249
+ retryMaxMs;
250
+ constructor(apiKey, { apiBaseUrl = DEFAULT_BASE_URL, timeoutMs = 30_000, retryAttempts = 3, retryBaseMs = 750, retryMaxMs = 15_000, } = {}) {
251
+ this.apiKey = apiKey;
252
+ this.apiBaseUrl = apiBaseUrl;
253
+ this.timeoutMs = timeoutMs;
254
+ this.retryAttempts = retryAttempts;
255
+ this.retryBaseMs = retryBaseMs;
256
+ this.retryMaxMs = retryMaxMs;
257
+ }
258
+ /** Map Express/query params + resolved layer into flat `query` / `query_all` params. */
259
+ resolveRequest(query, layer) {
260
+ return {
261
+ ...layer,
262
+ limit: parseLimit(query),
263
+ cursor: optionalString(query, 'cursor'),
264
+ zoom: optionalNumber(query, 'zoom'),
265
+ around: optionalString(query, 'around'),
266
+ osmIds: optionalString(query, 'osm_ids'),
267
+ minLengthM: optionalNumber(query, 'min_length_m'),
268
+ maxLengthM: optionalNumber(query, 'max_length_m'),
269
+ minAreaM2: optionalNumber(query, 'min_area_m2'),
270
+ maxAreaM2: optionalNumber(query, 'max_area_m2'),
271
+ disableBudgetWarning: optionalBoolean(query, 'disable_budget_warning'),
272
+ centroid: optionalBoolean(query, 'centroid'),
273
+ };
274
+ }
275
+ async throwUpstreamError(upstream) {
276
+ const subtype = upstream.status === 429 ? 'upstream_rate_limit' : undefined;
277
+ const upstreamDetail = await readUpstreamErrorDetail(upstream);
278
+ const detail = upstreamDetail
279
+ ? `Server returned status ${upstream.status}. Details: ${upstreamDetail}`
280
+ : `Server returned status ${upstream.status}.`;
281
+ const err = appError(upstream.status, 'upstream_status', detail, subtype);
282
+ err.upstreamStatus = upstream.status;
283
+ err.upstreamDetail = upstreamDetail;
284
+ throw err;
285
+ }
286
+ /** Single HTTP request with retry. Throws on non-OK (same role as Python `_raw_query`). */
287
+ async _rawQuery(params, fetchFn, sleepFn, nowFn) {
288
+ const query = buildFeaturesQuery(params);
289
+ const upstreamUrl = new URL(`${this.apiBaseUrl}/v2/osm_features`);
290
+ for (const [key, value] of query.entries()) {
291
+ upstreamUrl.searchParams.append(key, value);
292
+ }
293
+ let upstream;
294
+ let retryAttempt = 0;
295
+ while (true) {
296
+ try {
297
+ upstream = await fetchFn(upstreamUrl.toString(), {
298
+ method: 'GET',
299
+ headers: {
300
+ Authorization: `Bearer ${this.apiKey}`,
301
+ Accept: params.accept || GEOJSON_ACCEPT,
302
+ 'User-Agent': 'osmfeatures',
303
+ },
304
+ signal: AbortSignal.timeout(this.timeoutMs),
305
+ });
306
+ }
307
+ catch (error) {
308
+ const subtype = error instanceof DOMException && error.name === 'TimeoutError'
309
+ ? 'upstream_timeout'
310
+ : 'upstream_network';
311
+ const err = appError(502, 'upstream_error', 'OSM features upstream request failed.', subtype);
312
+ err.cause = error;
313
+ throw err;
314
+ }
315
+ if (upstream.ok) {
316
+ break;
317
+ }
318
+ if (upstream.status === 429 && retryAttempt < this.retryAttempts) {
319
+ retryAttempt += 1;
320
+ const retryAfterHeader = upstream.headers.get('retry-after');
321
+ const fallbackMs = Math.min(this.retryMaxMs, this.retryBaseMs * (2 ** Math.max(0, retryAttempt - 1)));
322
+ let waitMs = fallbackMs;
323
+ if (retryAfterHeader) {
324
+ const asSeconds = Number.parseFloat(retryAfterHeader);
325
+ if (Number.isFinite(asSeconds) && asSeconds >= 0) {
326
+ waitMs = Math.min(this.retryMaxMs, Math.max(0, Math.round(asSeconds * 1000)));
327
+ }
328
+ else {
329
+ const asDateMs = Date.parse(retryAfterHeader);
330
+ if (!Number.isNaN(asDateMs)) {
331
+ const deltaMs = asDateMs - nowFn();
332
+ if (deltaMs > 0) {
333
+ waitMs = Math.min(this.retryMaxMs, deltaMs);
334
+ }
335
+ }
336
+ }
337
+ }
338
+ await sleepFn(waitMs);
339
+ continue;
340
+ }
341
+ await this.throwUpstreamError(upstream);
342
+ }
343
+ if (isGeojsonAccept(params.accept)) {
344
+ const body = (await upstream.json());
345
+ const features = Array.isArray(body.features) ? body.features : [];
346
+ return resultFromFeatures(features, metaFromHeaders(upstream.headers, features.length));
347
+ }
348
+ const bytes = await upstream.arrayBuffer();
349
+ return {
350
+ data: bytes,
351
+ meta: metaFromHeaders(upstream.headers, 0),
352
+ };
353
+ }
354
+ /** Single upstream page. Params map 1:1 to server query string (no tiling). */
355
+ async query({ bbox, tags, orTags, notTags, type, shape, limit = DEFAULT_LIMIT, cursor, zoom, around, osmIds, minLengthM, maxLengthM, minAreaM2, maxAreaM2, disableBudgetWarning, centroid, accept, }, dependencies = {}) {
356
+ const payload = await this._rawQuery({
357
+ bbox,
358
+ tags,
359
+ orTags,
360
+ notTags,
361
+ type,
362
+ shape,
363
+ limit,
364
+ cursor,
365
+ zoom,
366
+ around,
367
+ osmIds,
368
+ minLengthM,
369
+ maxLengthM,
370
+ minAreaM2,
371
+ maxAreaM2,
372
+ disableBudgetWarning,
373
+ centroid,
374
+ accept,
375
+ }, dependencies.fetchFn ?? fetch, dependencies.sleepFn ?? sleep, dependencies.nowFn ?? Date.now);
376
+ return payload;
377
+ }
378
+ /** Auto-paginate (and optionally tile) until complete. Each page uses `_rawQuery`. */
379
+ async query_all({ bbox, tags, orTags, notTags, type, shape, zoom, around, osmIds, minLengthM, maxLengthM, minAreaM2, maxAreaM2, disableBudgetWarning, centroid, accept, limitPerPage = DEFAULT_LIMIT, bboxTiles = 2, maxPages = 15, maxFeatures = 55_000, }, dependencies = {}) {
380
+ if (!isGeojsonAccept(accept)) {
381
+ throw appError(400, 'invalid_accept', 'query_all only supports GeoJSON; use query({ accept }) for binary encodings.');
382
+ }
383
+ if (!isPowerOfTwo(bboxTiles)) {
384
+ throw appError(400, 'invalid_bbox_tiles', 'bbox_tiles must be a power of 2 (1, 2, 4, 8, …).');
385
+ }
386
+ const fetchFn = dependencies.fetchFn ?? fetch;
387
+ const sleepFn = dependencies.sleepFn ?? sleep;
388
+ const nowFn = dependencies.nowFn ?? Date.now;
389
+ const featureCap = maxFeatures == null ? Number.POSITIVE_INFINITY : maxFeatures;
390
+ const tileBboxes = splitBbox(bbox, bboxTiles);
391
+ const allFeatures = [];
392
+ let pageCount = 0;
393
+ let lastPage = null;
394
+ let relayPartialReason = null;
395
+ let lastCursor = null;
396
+ let unitsCharged = 0;
397
+ let sawUnitsCharged = false;
398
+ // Cap / maxPages stop is intentional; this flag only marks result incomplete.
399
+ let stoppedEarly = false;
400
+ const baseParams = {
401
+ tags,
402
+ orTags,
403
+ notTags,
404
+ type,
405
+ shape,
406
+ limit: limitPerPage,
407
+ zoom,
408
+ around,
409
+ osmIds,
410
+ minLengthM,
411
+ maxLengthM,
412
+ minAreaM2,
413
+ maxAreaM2,
414
+ disableBudgetWarning,
415
+ centroid,
416
+ };
417
+ for (const tileBbox of tileBboxes) {
418
+ if (allFeatures.length >= featureCap) {
419
+ stoppedEarly = true;
420
+ break;
421
+ }
422
+ if (relayPartialReason !== null) {
423
+ break;
424
+ }
425
+ let cursor;
426
+ let tilePages = 0;
427
+ let tileExhausted = false;
428
+ while (tilePages < maxPages && allFeatures.length < featureCap) {
429
+ let page;
430
+ try {
431
+ const raw = await this._rawQuery({ ...baseParams, bbox: tileBbox, cursor }, fetchFn, sleepFn, nowFn);
432
+ if (raw.data instanceof ArrayBuffer) {
433
+ throw appError(400, 'invalid_accept', 'query_all only supports GeoJSON; use query({ accept }) for binary encodings.');
434
+ }
435
+ page = { data: raw.data, meta: raw.meta };
436
+ }
437
+ catch (error) {
438
+ const status = error.status;
439
+ if (pageCount > 0 && (status === 400 || status === 429)) {
440
+ relayPartialReason = status === 400
441
+ ? 'upstream_rejected_cursor'
442
+ : 'upstream_rate_limited_after_retries';
443
+ break;
444
+ }
445
+ throw error;
446
+ }
447
+ const pageFeatures = Array.isArray(page.data.features)
448
+ ? page.data.features
449
+ : [];
450
+ allFeatures.push(...pageFeatures);
451
+ lastPage = page;
452
+ pageCount += 1;
453
+ tilePages += 1;
454
+ if (page.meta.units_charged != null) {
455
+ unitsCharged += page.meta.units_charged;
456
+ sawUnitsCharged = true;
457
+ }
458
+ const hasMore = page.meta.has_more;
459
+ const nextCursor = page.meta.next_cursor;
460
+ if (!hasMore || typeof nextCursor !== 'string' || nextCursor === '') {
461
+ lastCursor = typeof nextCursor === 'string' ? nextCursor : null;
462
+ tileExhausted = true;
463
+ break;
464
+ }
465
+ cursor = nextCursor;
466
+ lastCursor = nextCursor;
467
+ }
468
+ if (relayPartialReason !== null) {
469
+ break;
470
+ }
471
+ if (!tileExhausted) {
472
+ stoppedEarly = true;
473
+ if (allFeatures.length >= featureCap) {
474
+ break;
475
+ }
476
+ }
477
+ }
478
+ const uniqueFeatures = dedupeFeatures(allFeatures);
479
+ const truncated = Number.isFinite(featureCap) && uniqueFeatures.length > featureCap;
480
+ const features = truncated ? uniqueFeatures.slice(0, featureCap) : uniqueFeatures;
481
+ const incomplete = truncated || stoppedEarly;
482
+ const meta = {
483
+ returned: features.length,
484
+ page_count: pageCount,
485
+ has_more: incomplete || Boolean(lastPage?.meta.has_more) || relayPartialReason !== null,
486
+ next_cursor: incomplete ? lastCursor : (lastPage?.meta.next_cursor ?? null),
487
+ relay_partial: relayPartialReason !== null,
488
+ relay_partial_reason: relayPartialReason ?? undefined,
489
+ };
490
+ if (sawUnitsCharged) {
491
+ meta.units_charged = unitsCharged;
492
+ }
493
+ return resultFromFeatures(features, meta);
494
+ }
495
+ }
496
+ export * from './geojson-feature.js';
497
+ export * from './preset/index.js';
@@ -0,0 +1,3 @@
1
+ import type { OSMFeaturesPresetId } from './catalog.js';
2
+ /** Opt-in: min_area_m2 so low-zoom building queries skip invisible footprints. Omit zoom for full detail. */
3
+ export declare function resolveBuildingsMinAreaM2(presetId: OSMFeaturesPresetId, bbox: string, zoom: number | undefined): number | undefined;
@@ -0,0 +1,30 @@
1
+ // Buildings area floors by zoom (Web Mercator px size at bbox mid-lat):
2
+ // z < 11.5 — drop tiny houses / small sheds (~0.5×0.5 px)
3
+ // z < 12.5 — drop only true specks (~0.25×0.25 px)
4
+ const BUILDINGS_SMALL_AREA_ZOOM = 11.5;
5
+ const BUILDINGS_SPECK_AREA_ZOOM = 12.5;
6
+ const BUILDINGS_SMALL_PIXEL_EDGE = 0.5;
7
+ const BUILDINGS_SPECK_PIXEL_EDGE = 0.25;
8
+ const WEB_MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0 = 156543.03392;
9
+ function bboxMidLatitude(bbox) {
10
+ const parts = bbox.split(',').map((value) => Number.parseFloat(value));
11
+ const minLat = parts[1];
12
+ const maxLat = parts[3];
13
+ if (!Number.isFinite(minLat) || !Number.isFinite(maxLat)) {
14
+ return 0;
15
+ }
16
+ return (minLat + maxLat) / 2;
17
+ }
18
+ /** Opt-in: min_area_m2 so low-zoom building queries skip invisible footprints. Omit zoom for full detail. */
19
+ export function resolveBuildingsMinAreaM2(presetId, bbox, zoom) {
20
+ if (presetId !== 'buildings' || zoom == null || zoom >= BUILDINGS_SPECK_AREA_ZOOM) {
21
+ return undefined;
22
+ }
23
+ const latitude = bboxMidLatitude(bbox);
24
+ const metersPerPixel = (WEB_MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0 * Math.cos((latitude * Math.PI) / 180)) / 2 ** zoom;
25
+ // ponytail: z<11.5 uses 0.5px (tiny houses); z<12.5 uses 0.25px (specks only). Raise if still noisy.
26
+ const pixelEdge = zoom < BUILDINGS_SMALL_AREA_ZOOM
27
+ ? BUILDINGS_SMALL_PIXEL_EDGE
28
+ : BUILDINGS_SPECK_PIXEL_EDGE;
29
+ return (metersPerPixel * pixelEdge) ** 2;
30
+ }
@@ -0,0 +1,13 @@
1
+ export type OSMFeaturesPresetId = 'buildings' | 'roads_paths' | 'parks_green_space' | 'food_dining' | 'shops_commerce' | 'public_transport' | 'leisure_sports' | 'natural_features' | 'waterways';
2
+ /** Catalog entry: tags only. Pass bbox at resolve time. */
3
+ export type OSMFeaturesLayerPreset = {
4
+ id: OSMFeaturesPresetId;
5
+ label: string;
6
+ tags?: string[];
7
+ orTags?: string[];
8
+ notTags?: string[];
9
+ type?: string;
10
+ shape?: 'line' | 'polygon';
11
+ };
12
+ export declare const OSM_FEATURES_LAYER_PRESETS: Record<OSMFeaturesPresetId, OSMFeaturesLayerPreset>;
13
+ export declare const OSM_FEATURES_LAYER_PRESET_ORDER: OSMFeaturesPresetId[];
@@ -0,0 +1,127 @@
1
+ export const OSM_FEATURES_LAYER_PRESETS = {
2
+ buildings: {
3
+ id: 'buildings',
4
+ label: 'Buildings',
5
+ tags: ['building'],
6
+ type: 'way,relation',
7
+ shape: 'polygon',
8
+ },
9
+ roads_paths: {
10
+ id: 'roads_paths',
11
+ label: 'Roads & paths',
12
+ tags: ['highway'],
13
+ type: 'way,relation',
14
+ shape: 'line',
15
+ },
16
+ parks_green_space: {
17
+ id: 'parks_green_space',
18
+ label: 'Parks & green space',
19
+ orTags: [
20
+ 'leisure=park',
21
+ 'leisure=garden',
22
+ 'landuse=grass',
23
+ 'landuse=forest',
24
+ 'natural=wood',
25
+ 'boundary=national_park',
26
+ ],
27
+ type: 'way,relation',
28
+ },
29
+ food_dining: {
30
+ id: 'food_dining',
31
+ label: 'Food & dining',
32
+ orTags: [
33
+ 'amenity=restaurant',
34
+ 'amenity=cafe',
35
+ 'amenity=fast_food',
36
+ 'amenity=bar',
37
+ 'amenity=pub',
38
+ 'amenity=biergarten',
39
+ ],
40
+ },
41
+ shops_commerce: {
42
+ id: 'shops_commerce',
43
+ label: 'Shops & commerce',
44
+ orTags: [
45
+ 'shop',
46
+ 'amenity=marketplace',
47
+ 'amenity=fuel',
48
+ 'office',
49
+ ],
50
+ },
51
+ public_transport: {
52
+ id: 'public_transport',
53
+ label: 'Public transport',
54
+ orTags: [
55
+ 'highway=bus_stop',
56
+ 'railway=station',
57
+ 'railway=tram_stop',
58
+ 'railway=halt',
59
+ 'railway=subway_entrance',
60
+ 'railway=rail',
61
+ 'railway=subway',
62
+ 'railway=light_rail',
63
+ 'railway=tram',
64
+ 'railway=narrow_gauge',
65
+ 'public_transport=platform',
66
+ 'public_transport=stop_position',
67
+ 'amenity=bus_station',
68
+ 'route=train',
69
+ 'route=subway',
70
+ 'route=light_rail',
71
+ 'route=tram',
72
+ 'route=bus',
73
+ ],
74
+ },
75
+ leisure_sports: {
76
+ id: 'leisure_sports',
77
+ label: 'Leisure & sports',
78
+ orTags: [
79
+ 'leisure=pitch',
80
+ 'leisure=sports_centre',
81
+ 'leisure=stadium',
82
+ 'leisure=playground',
83
+ 'leisure=swimming_pool',
84
+ 'leisure=fitness_centre',
85
+ 'leisure=track',
86
+ ],
87
+ },
88
+ natural_features: {
89
+ id: 'natural_features',
90
+ label: 'Natural features',
91
+ orTags: [
92
+ 'natural=water',
93
+ 'natural=wood',
94
+ 'natural=scrub',
95
+ 'natural=wetland',
96
+ 'natural=peak',
97
+ 'natural=cliff',
98
+ 'natural=saddle',
99
+ 'natural=beach',
100
+ ],
101
+ },
102
+ // OMT waterway classes: river / canal / stream / drain / ditch (no subclass).
103
+ waterways: {
104
+ id: 'waterways',
105
+ label: 'Waterways',
106
+ orTags: [
107
+ 'waterway=river',
108
+ 'waterway=canal',
109
+ 'waterway=stream',
110
+ 'waterway=drain',
111
+ 'waterway=ditch',
112
+ ],
113
+ type: 'way,relation',
114
+ shape: 'line',
115
+ },
116
+ };
117
+ export const OSM_FEATURES_LAYER_PRESET_ORDER = [
118
+ 'buildings',
119
+ 'roads_paths',
120
+ 'parks_green_space',
121
+ 'food_dining',
122
+ 'shops_commerce',
123
+ 'public_transport',
124
+ 'leisure_sports',
125
+ 'natural_features',
126
+ 'waterways',
127
+ ];