diff --git a/README.md b/README.md index 79abc79..4caba63 100644 --- a/README.md +++ b/README.md @@ -4,186 +4,79 @@ ## Instructions -This extension is an interaction type in OpenLayers which requires a few setup steps: - -### 1. Vector layer for OSM ways +```bash +npm install ol-osmwaysnap +``` -To begin with, it is necessary to setup a vector layer that contains OSM ways. Using builtin `OSMWaySource` class that automatically fetches ways from OpenStreetMap using OverpassAPI, or alternatively, it can also be used with other linestring feature layer. +Create an instance of class `OSMWaySnap` and add it to map. (Default snapping to OSM roads) ```ts import VectorLayer from 'ol/layer/Vector'; -import { OSMWaySource } from 'ol-osmwaysnap'; +import VectorSource from 'ol/source/Vector'; +import { OSMWaySnap } from 'ol-osmwaysnap'; -// Default: Snap to roads (OSM highway) -const osmWayLayer = new VectorLayer({ - source: new OSMWaySource({ - maximumResolution: 5, - fetchBufferSize: 250, - overpassEndpointURL: 'https://...' // Choose one instance from https://wiki.openstreetmap.org/wiki/Overpass_API#Public_Overpass_API_instances - }), - style: OSMWaySource.getDefaultStyle() +const targetLayer = new VectorLayer>>({ + source: new VectorSource>() }); +map.addLayer(targetLayer); -// Snap to railways -const osmWayLayer = new VectorLayer({ - source: new OSMWaySource({ - maximumResolution: 5, - fetchBufferSize: 250, - overpassQuery: '(way["railway"];>;);', - overpassEndpointURL: 'https://...' // Choose one instance from https://wiki.openstreetmap.org/wiki/Overpass_API#Public_Overpass_API_instances - }), - style: OSMWaySource.getDefaultStyle() +// Default: Snap to roads (OSM highway) +const interaction = new OSMWaySnap({ + source: targetLayer.getSource(), + maximumResolution: 5, + fetchBufferSize: 250, + overpassEndpointURL: 'https://...' // Choose one instance from https://wiki.openstreetmap.org/wiki/Overpass_API#Public_Overpass_API_instances }); +map.addInteraction(interaction); ``` -### 2. Add interactions - -There are at least 2 interactions to be added in order for the extension to work 1) `OSMWaySnap` and 2) `Snap` interaction from default OpenLayers interactions. +Or specify a custom OverpassQL for different way elements, for example to railways. ```ts -import { OSMWaySnap } from 'ol-osmwaysnap'; -import Snap from 'ol/interaction/Snap'; - -map.addInteraction(new OSMWaySnap({ - source: targetFeatureLayer.getSource(), - waySource: osmWayLayer.getSource() -})); -map.addInteraction(new Snap({ - source: osmWayLayer.getSource() -})); +// Snap to railways +const interaction = new OSMWaySnap({ + source: targetLayer.getSource(), + maximumResolution: 5, + fetchBufferSize: 250, + overpassQuery: '(way["railway"];>;);', + overpassEndpointURL: 'https://...' // Choose one instance from https://wiki.openstreetmap.org/wiki/Overpass_API#Public_Overpass_API_instances +}); +map.addInteraction(interaction); ``` -## Examples - -### Using as module +Or use a custom vector source (not OSM) for snapping. ```ts -import Map from 'ol/Map'; -import View from 'ol/View'; -import TileLayer from 'ol/layer/Tile'; -import VectorLayer from 'ol/layer/Vector'; -import VectorSource from 'ol/source/Vector'; -import OSM from 'ol/source/OSM'; -import Snap from 'ol/interaction/Snap'; -import LineString from 'ol/geom/LineString'; -import Feature from 'ol/Feature'; -import { OSMWaySource, OSMWaySnap } from 'ol-osmwaysnap'; - -const basemap = new TileLayer({ source: new OSM() }); - -const osmWaySource = new OSMWaySource({ +const interaction = new OSMWaySnap({ + source: targetLayer.getSource(), + waySource: someVectorSource maximumResolution: 5, - fetchBufferSize: 250, - overpassEndpointURL: 'https://...' // Choose one instance from https://wiki.openstreetmap.org/wiki/Overpass_API#Public_Overpass_API_instances + fetchBufferSize: 250 }); +map.addInteraction(interaction); +``` -const targetFeaturesLayer = new VectorLayer>>({ - source: new VectorSource>() -}); -const osmWayLayer = new VectorLayer({source: osmWaySource, style: OSMWaySource.getDefaultStyle()}); - -const map = new Map({ - target: 'map', - layers: [basemap, osmWayLayer, targetFeaturesLayer], - view: new View({ - center: [11018989, 2130015], - zoom: 16 - }) -}); +## Examples +TODO -map.addInteraction(new OSMWaySnap({ - source: targetFeaturesLayer.getSource()!, - waySource: osmWayLayer.getSource()! -})); -map.addInteraction(new Snap({ - source: osmWayLayer.getSource()! -})); -``` +## Constructor Options -### Using as CDN - -```html - - - - - - - - -
-
- - - - - -``` +### For `OSMWaySnap` -## Options +- `autoFocus?: boolean` - True to automatically fit map view to next candidantes. (default: true) +- `focusPadding?: number` - Used with autoFocus, specify number to add padding to view fitting. (default: 50 !PROJECTION SENSITIVE!) +- `sketchStyle?: StyleLike` - Style of sketch features (default is predefined, overwrite if necessary) +- `source: VectorSource>` - Target source of edition +- `waySource?: VectorSource>` - Ways source for snapping (default to a new instance of OSMOverpassWaySource) +- `createAndAddWayLayer?: boolean` - Create a new way layer from way source (if provided) and add to map (default: true) +- `wrapX?: boolean` - WrapX -### `OSMWaySource` constructor options +### extended options if `waySource` is not provided - `cachedFeaturesCount: number` - The number of features to store before getting cleared. This is to prevent heavy memory consumption. - `fetchBufferSize: number` - Buffer size to apply to the extent of fetching OverpassAPI. This is to prevent excessive call despite slight map view panning. **USE THE SAME PROJECTION WITH THE LAYER**. - `maximumResolution: number` - Map view resolution to start fetching OverpassAPI. This is to prevent fetching elements in too big extent. **USE THE SAME PROJECTION WITH THE LAYER** +- `overpassQuery: string` - OverpassQL statement(s) to fetch, excluding settings and out statements. - `overpassEndpointURL?: string` - OverpassAPI endpoint URL (https://wiki.openstreetmap.org/wiki/Overpass_API#Public_Overpass_API_instances) -- `overpassQuery: string` - OverpassQL statement for ways to fetch, default to OSM highways. - -### `OSMWaySnap` constructor options - -- `autoFocus?: boolean` - True to automatically fit map view to next candidantes. -- `focusPadding?: number` - Used with autoFocus, specify number to add padding to view fitting. -- `sketchStyle?: StyleLike` - Style of sketch features. -- `source: VectorSource>` - Target source of edition. -- `waySource: VectorSource>` - Source to OSMWays for snapping. -- `wrapX?: boolean` --- diff --git a/examples/index.html b/examples/index.html index b2c286d..2f062b4 100644 --- a/examples/index.html +++ b/examples/index.html @@ -41,7 +41,8 @@
- + + diff --git a/package-lock.json b/package-lock.json index 86638d2..8e19b9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,16 @@ { - "name": "ol-osm-way-snap", - "version": "0.0.0", + "name": "ol-osmwaysnap", + "version": "0.0.3", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "ol-osm-way-snap", - "version": "0.0.0", + "name": "ol-osmwaysnap", + "version": "0.0.3", "license": "ISC", + "dependencies": { + "ol-osmoverpass": "^0.1.0" + }, "devDependencies": { "ol": "^8.2.0", "typescript": "^5.3.2", @@ -921,6 +924,11 @@ "url": "https://opencollective.com/openlayers" } }, + "node_modules/ol-osmoverpass": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ol-osmoverpass/-/ol-osmoverpass-0.1.0.tgz", + "integrity": "sha512-AnF2XT15BpF6Ab5JlbF0FKv0KiTrNFJMgXkKq/o/W7bnfJTaGMCk9IKfDFN+iKha3KZ7CwuSjeBQEmE67vsDdA==" + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -2280,6 +2288,11 @@ "rbush": "^3.0.1" } }, + "ol-osmoverpass": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ol-osmoverpass/-/ol-osmoverpass-0.1.0.tgz", + "integrity": "sha512-AnF2XT15BpF6Ab5JlbF0FKv0KiTrNFJMgXkKq/o/W7bnfJTaGMCk9IKfDFN+iKha3KZ7CwuSjeBQEmE67vsDdA==" + }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", diff --git a/package.json b/package.json index 8d43796..d8af100 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,14 @@ { "name": "ol-osmwaysnap", - "version": "0.0.3", + "version": "0.1.0", "description": "OpenLayers Interaction Extension for Snapping Ways from OSM using Overpass API", "keywords": [ - "osm", "openstreetmap", "openlayers", "gis", "map", "road-snapping" + "osm", + "openstreetmap", + "openlayers", + "gis", + "map", + "road-snapping" ], "main": "dist/index.js", "scripts": { @@ -30,5 +35,8 @@ "typescript": "^5.3.2", "webpack": "^5.74.0", "webpack-cli": "^4.10.0" + }, + "dependencies": { + "ol-osmoverpass": "^0.1.0" } } diff --git a/src/index.ts b/src/index.ts index 32009bb..9291f2f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ -export { default as OSMWaySnap } from './interaction'; -export { default as OSMWaySource } from './source'; +export { default as OSMWaySnap, type OSMWaySnapOptions } from './interaction'; +export { default as LineStringUtils } from './line-string-utils'; diff --git a/src/interaction.ts b/src/interaction.ts index 94fa502..f787470 100644 --- a/src/interaction.ts +++ b/src/interaction.ts @@ -1,76 +1,47 @@ -import Map from 'ol/Map'; +import { Map } from 'ol'; +import { boundingExtent } from 'ol/extent'; import Feature from 'ol/Feature'; -import VectorLayer from 'ol/layer/Vector'; -import Point from 'ol/geom/Point'; -import LineString from 'ol/geom/LineString'; -import MultiPoint from 'ol/geom/MultiPoint'; -import VectorSource from 'ol/source/Vector'; -import PointerInteraction from 'ol/interaction/Pointer'; -import Circle from 'ol/style/Circle'; -import Fill from 'ol/style/Fill'; -import Stroke from 'ol/style/Stroke'; +import { Point, LineString } from 'ol/geom'; +import { Snap, Pointer as PointerInteraction } from 'ol/interaction'; +import { Vector as VectorLayer } from 'ol/layer'; +import { Vector as VectorSource } from 'ol/source'; +import { Circle, Fill, Stroke } from 'ol/style'; import Style, { createEditingStyle } from 'ol/style/Style'; -import LineStringUtils from './utils/line-string'; +import { OSMOverpassWaySource, type OSMOverpassSourceOptions } from 'ol-osmoverpass'; +import LineStringUtils from './line-string-utils'; import type { Coordinate } from 'ol/coordinate'; import type { StyleLike } from 'ol/style/Style'; import type { MapBrowserEvent } from 'ol'; /** Options for OSMWaySnap interaction */ export type OSMWaySnapOptions = { - /** True to automatically fit map view to next candidantes. */ + /** True to automatically fit map view to next candidantes. (default: true) */ autoFocus?: boolean, - /** Used with autoFocus, specify number to add padding to view fitting. */ + /** Used with autoFocus, specify number to add padding to view fitting. (default: 50 !PROJECTION SENSITIVE!) */ focusPadding?: number, - /** Style of sketch features */ + /** Style of sketch features (default is predefined, overwrite if necessary) */ sketchStyle?: StyleLike, /** Target source of edition */ source: VectorSource>, - /** Source to OSMWays for snapping */ - waySource: VectorSource>, + /** Ways source for snapping (default to a new instance of OSMOverpassWaySource) */ + waySource?: VectorSource>, + + /** Create a new way layer from way source (if provided) and add to map (default: true) */ + createAndAddWayLayer?: boolean, /** WrapX */ wrapX?: boolean -}; - -const DEFAULT_SKETCH_STYLE: StyleLike = feature => { - if (feature.getProperties().candidate) { - return feature.getGeometry()!.getType() === 'Point' ? - new Style({ - image: new Circle({ - fill: new Fill({ color: '#00ffff' }), - stroke: new Stroke({ color: '#ff0000', width: 1 }), - radius: 2 - }) - }) - : new Style({ - stroke: new Stroke({ - width: 2, - color: 'rgba(245,128,2,0.5)' - }) - }); - } else if (feature.getGeometry()!.getType() === 'Point') { - return createEditingStyle()['Point']; - } - return new Style({ - stroke: new Stroke({ - width: 4, - color: '#02c0f5' - }) - }); -}; +} & OSMOverpassSourceOptions; /** * Interaction for snapping linestring to way elements from OSM * This is designed to use with Snap interaction. */ export default class OSMWaySnap extends PointerInteraction { - /** Options */ - private options: OSMWaySnapOptions; - /** Coordinates of active linestring */ private coordinates: Coordinate[] = []; /** Feature that is being edited */ @@ -87,23 +58,57 @@ export default class OSMWaySnap extends PointerInteraction { /** Candidate lines for selection on map */ private candidateLines: Feature[] = []; + /** True to automatically fit map view to next candidantes. */ + private autoFocus: boolean; + /** Used with autoFocus, specify number to add padding to view fitting. */ + private focusPadding: number; + /** Target source of edition */ + private source: VectorSource>; + /** Ways source for snapping */ + private waySource: VectorSource>; + /** Create a new way layer from way source (if provided) and add to map */ + private createAndAddWayLayer: boolean; + /** WrapX */ + private wrapX: boolean|undefined = undefined; + + /** Map */ + private map: Map|undefined = undefined; + + /** Layer of snapping ways */ + private wayLayer: VectorLayer>>|undefined = undefined; + /** Snap interaction */ + private snapInteraction: Snap; + /** * Constructor * @param options Options */ public constructor(options: OSMWaySnapOptions) { super(); - this.options = { - ...options, - autoFocus: options.autoFocus === undefined ? true : options.autoFocus, - focusPadding: options.focusPadding ?? 50, - }; + this.autoFocus = options.autoFocus === undefined ? true: options.autoFocus; + this.focusPadding = options.focusPadding ?? 50; + this.source = options.source; + this.waySource = options.waySource ?? new OSMOverpassWaySource({ + cachedFeaturesCount: options.cachedFeaturesCount ?? undefined, + fetchBufferSize: options.fetchBufferSize ?? undefined, + maximumResolution: options.maximumResolution ?? undefined, + overpassQuery: options.overpassQuery ?? '(way["highway"];>;);', + overpassEndpointURL: options.overpassEndpointURL ?? undefined + }); + this.createAndAddWayLayer = options.createAndAddWayLayer === undefined ? true : options.createAndAddWayLayer; + this.wrapX = options.wrapX; + this.snapInteraction = new Snap({ source: this.waySource }); + + if (this.createAndAddWayLayer) { + this.wayLayer = new VectorLayer({ source: this.waySource, style: OSMOverpassWaySource.getDefaultStyle() }); + } this.overlayLayer = new VectorLayer({ - source: new VectorSource({ useSpatialIndex: false, wrapX: this.options.wrapX }), + source: new VectorSource({ useSpatialIndex: false, wrapX: this.wrapX }), updateWhileInteracting: true, - style: this.options.sketchStyle ?? DEFAULT_SKETCH_STYLE + style: options.sketchStyle ?? OSMWaySnap.getDefaultSketchStyle() }); - this.addChangeListener('active', this.updateState); + + this.addChangeListener('active', this.activeChanged.bind(this)); } /** @@ -113,8 +118,34 @@ export default class OSMWaySnap extends PointerInteraction { * @param map Map. */ public setMap(map: Map|null) { + if (this.map) { + this.waySource.un('featuresloadend', this.waysFeaturesLoadEnded.bind(this)); + this.wayLayer && this.map.removeLayer(this.wayLayer); + this.map.removeLayer(this.overlayLayer); + this.map.removeInteraction(this.snapInteraction); + } super.setMap(map); - this.updateState(); + this.map = map ?? undefined; + if (this.map) { + this.waySource.on('featuresloadend', this.waysFeaturesLoadEnded.bind(this)); + this.wayLayer && this.map.getAllLayers().indexOf(this.wayLayer) < 0 && this.map.addLayer(this.wayLayer); + this.map.addLayer(this.overlayLayer); + this.map.getInteractions().getArray().indexOf(this.snapInteraction) < 0 && this.map.addInteraction(this.snapInteraction); + } + } + + /** + * Handler on active property changed. + */ + public activeChanged() { + this.setMap(this.getActive() ? (this.map ?? null) : null); + } + + /** + * Get way vector source + */ + public getWaySource(): VectorSource> { + return this.waySource; } /** @@ -164,8 +195,8 @@ export default class OSMWaySnap extends PointerInteraction { this.activeFeature = new Feature(new LineString(this.coordinates)); } else { this.activeFeature.getGeometry()!.setCoordinates(this.coordinates); - if (this.coordinates.length > 1 && !this.options.source.hasFeature(this.activeFeature)) { - this.options.source.addFeature(this.activeFeature); + if (this.coordinates.length > 1 && !this.source.hasFeature(this.activeFeature)) { + this.source.addFeature(this.activeFeature); } } this.calculateCandidates(); @@ -238,8 +269,8 @@ export default class OSMWaySnap extends PointerInteraction { || !this.activeFeature!.getGeometry()!.intersectsCoordinate(p) ); if (fitCoordinates.length > 1) { - map.getView().fit(new MultiPoint(fitCoordinates), { - padding: Array(4).fill(this.options.focusPadding) + map.getView().fit(boundingExtent(fitCoordinates), { + padding: Array(4).fill(this.focusPadding) }); } } @@ -252,7 +283,7 @@ export default class OSMWaySnap extends PointerInteraction { */ private calculateCandidates(fit: boolean = true) { const lastFeatureCoors = this.activeFeature!.getGeometry()!.getLastCoordinate(); - this.candidateLines = this.options.waySource.getFeatures().filter( + this.candidateLines = this.waySource.getFeatures().filter( feature => feature.getGeometry()!.containsXY(lastFeatureCoors[0], lastFeatureCoors[1]) ).map(f => { f = new Feature(f.getGeometry()); @@ -261,7 +292,7 @@ export default class OSMWaySnap extends PointerInteraction { }); this.candidatePoints = this.candidateLines.map(l => l.getGeometry()!.getCoordinates()).flat() .map(c => new Feature({ candidate: true, geometry: new Point(c) })); - if (fit && this.options.autoFocus) { + if (fit && this.autoFocus) { this.fitCandidatesToMapView(); } this.createOrUpdateSketchLine(lastFeatureCoors); @@ -353,17 +384,32 @@ export default class OSMWaySnap extends PointerInteraction { this.calculateCandidates(false); } - /** - * When the interaction state is changed (assigned or unassigned to maps). - */ - private updateState() { - const map = this.getMap(); - const active = this.getActive(); - this.overlayLayer.setMap(active ? map : null); - if (map) { - this.options.waySource.on('featuresloadend', this.waysFeaturesLoadEnded.bind(this)); - } else { - this.options.waySource.un('featuresloadend', this.waysFeaturesLoadEnded.bind(this)); - } + private static getDefaultSketchStyle(): StyleLike { + return feature => { + if (feature.getProperties().candidate) { + return feature.getGeometry()!.getType() === 'Point' ? + new Style({ + image: new Circle({ + fill: new Fill({ color: '#00ffff' }), + stroke: new Stroke({ color: '#ff0000', width: 1 }), + radius: 2 + }) + }) + : new Style({ + stroke: new Stroke({ + width: 2, + color: 'rgba(245,128,2,0.5)' + }) + }); + } else if (feature.getGeometry()!.getType() === 'Point') { + return createEditingStyle()['Point']; + } + return new Style({ + stroke: new Stroke({ + width: 4, + color: '#02c0f5' + }) + }); + }; } }; diff --git a/src/utils/line-string.ts b/src/line-string-utils.ts similarity index 95% rename from src/utils/line-string.ts rename to src/line-string-utils.ts index 1956724..de2db7c 100644 --- a/src/utils/line-string.ts +++ b/src/line-string-utils.ts @@ -1,4 +1,4 @@ -import LineString from 'ol/geom/LineString'; +import { LineString } from 'ol/geom'; import type { Coordinate } from 'ol/coordinate'; /** diff --git a/src/osm/osm-ways.ts b/src/osm/osm-ways.ts deleted file mode 100644 index 9d5248c..0000000 --- a/src/osm/osm-ways.ts +++ /dev/null @@ -1,50 +0,0 @@ -import Feature from 'ol/Feature'; -import LineString from 'ol/geom/LineString'; -import OverpassAPI from './overpass-api'; -import { transform, transformExtent } from 'ol/proj'; -import type { Extent } from 'ol/extent'; -import type { OSMNode, OSMWay } from './response'; -import type { Projection } from 'ol/proj'; - -/** - * OSM way elements fetcher from Overpass API -*/ -export default class OSMWays { - private constructor() {} - - /** - * Fetch ways from OSM in the specified extent and query, transform to geometric objects of linestring and return the array of them - * @param extent fetch extent - * @param query OverpassQL for querying ways (excluding settings and out statements) - * @param projection Projection - * @param endpoint URL endpoint, if specify it will overwrite the static settings in OverpassAPI class - * @returns Promise of linestring features array - */ - public static async fetch(extent: Extent, query: string, projection: Projection, endpoint?: string): Promise[]> { - if (projection.getCode() !== 'EPSG:4326') { - extent = transformExtent(extent, projection, 'EPSG:4326'); - } - - const response = await OverpassAPI.fetchInExtent(extent, query, 'out;', endpoint); - - const nodes: { [id: number]: OSMNode } = (response.elements.filter(x => x.type === 'node') as OSMNode[]) - .reduce((dict, node: OSMNode) => { - dict[node.id] = node; - return dict; - }, {} as { [id: number]: OSMNode }); - - return (response.elements.filter(x => x.type === 'way') as OSMWay[]) - .map(x => { - const coordinates = projection.getCode() === 'EPSG:4326' ? - x.nodes.map(id => [nodes[id].lon, nodes[id].lat]) - : x.nodes.map(id => transform([nodes[id].lon, nodes[id].lat], 'EPSG:4326', projection)); - const feature = new Feature(new LineString(coordinates)); - feature.setProperties({ - osmid: x.id, - name: x.tags.name ?? undefined - }); - feature.setId(x.id); - return feature; - }); - } -}; diff --git a/src/osm/overpass-api.ts b/src/osm/overpass-api.ts deleted file mode 100644 index 5e0723e..0000000 --- a/src/osm/overpass-api.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { Extent } from 'ol/extent'; -import type { OverpassResponse } from './response'; - -/** - * Exception for not specifying OverpassAPI endpoint - */ -class OverpassAPIEndpointURLUnsetError extends Error { - public constructor() { - const msg = 'No endpoint URL for Overpass API specified for static property endpointURL of class OverpassAPI.' - + ' Please specify one from https://wiki.openstreetmap.org/wiki/Overpass_API#Public_Overpass_API_instances'; - console.error(msg); - super(msg); - } -} - -/** - * OverpassAPI fetcher - */ -export default class OverpassAPI { - /** - * Default endpoint URL for all queries if not specified in each call. - * Public instances can be found in https://wiki.openstreetmap.org/wiki/Overpass_API#Public_Overpass_API_instances - */ - public static endpointURL: string|undefined = undefined; - - private constructor() {} - - /** - * Fetch data from OverpassAPI - * @param settings OverpassQL settings statement - * @param query OverpassQL query statement(s) - * @param out OverpassQL out statement - * @param endpoint OverpassAPI endpoint URL - * @returns Promise of OverpassAPI response object - */ - public static async fetch( - settings: string, - query: string, - out?: string, - endpoint?: string - ): Promise { - if (!endpoint && !OverpassAPI.endpointURL) { - throw new OverpassAPIEndpointURLUnsetError(); - } - - out = out ?? 'out;'; - - const body = new URLSearchParams({ data: settings + query + out }).toString(); - const result = await fetch( - endpoint ?? OverpassAPI.endpointURL!, - { method: 'POST', body } - ); - - return (await result.json()); - } - - /** - * Convert OpenLayers extent into OverpassQL bbox setting statement - * @param extent OpenLayers extent - * @returns OverpassQL bbox setting statement - */ - public static extentToBBox(extent: Extent): string { - return '[bbox:' + [ - Math.min(extent[1], extent[3]), - Math.min(extent[0], extent[2]), - Math.max(extent[1], extent[3]), - Math.max(extent[0], extent[2]) - ].join(',') + ']'; - } - - /** - * Create setting statements from OpenLayers extent and fetch data specified in query from OverpassAPI - * @param extent OpenLayers Extent - * @param query OverpassQL query statement - * @param out OverpassQL out statement - * @param endpoint OverpassAPI endpoint URL - * @returns Promise of OverpassAPI response object - */ - public static async fetchInExtent( - extent: Extent, - query: string, - out: string = 'out;', - endpoint?: string - ): Promise { - const settings = OverpassAPI.extentToBBox(extent) + '[out:json];'; - return OverpassAPI.fetch(settings, query, out, endpoint); - } -}; diff --git a/src/osm/response.ts b/src/osm/response.ts deleted file mode 100644 index 0318081..0000000 --- a/src/osm/response.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Type definition of OverpassAPI response with out:json settings - -export type OSMElementBase = { - type: 'node'|'way'|'relation', - id: number, - tags: { name?: string } & Record -}; - -export type OSMNode = OSMElementBase & { - type: 'node', - lat: number, - lon: number, -}; - -export type OSMWay = OSMElementBase & { - type: 'way', - nodes: number[] -}; - -export type OSMElement = OSMNode | OSMWay; - -export type OverpassResponse = { - version: number, - generator: string, - elements: OSMElement[], - osm3s: { - timestamp_osm_base: string, - copyright: string - } -}; diff --git a/src/source.ts b/src/source.ts deleted file mode 100644 index 8b10594..0000000 --- a/src/source.ts +++ /dev/null @@ -1,119 +0,0 @@ -import OSMWays from './osm/osm-ways'; -import VectorSource from 'ol/source/Vector'; -import Style from 'ol/style/Style'; -import { bbox } from 'ol/loadingstrategy'; -import { buffer, containsExtent } from 'ol/extent'; -import type { Feature } from 'ol'; -import type RBush from 'ol/structs/RBush'; -import type { LineString } from 'ol/geom'; -import type { Projection } from 'ol/proj'; -import type { Extent } from 'ol/extent'; - -export type LoaderSuccessFn = (features: Feature[]) => void; -export type LoaderFailedFn = () => void; - -export type OSMWaySourceOptions = { - /** The number of features to store before getting cleared. This is to prevent heavy memory consumption. (Default: 20000) */ - cachedFeaturesCount: number, - - /** Buffer size to apply to the extent of fetching OverpassAPI. This is to prevent excessive call despite slight map view panning. (Default: 0) */ - fetchBufferSize: number, - - /** Map view resolution to start fetching OverpassAPI. This is to prevent fetching elements in too big extent. */ - maximumResolution: number, - - /** OverpassAPI endpoint URL (https://wiki.openstreetmap.org/wiki/Overpass_API#Public_Overpass_API_instances) */ - overpassEndpointURL?: string, - - /** OverpassQL statement for ways to fetch, default to OSM highways. */ - overpassQuery: string -}; - -/** - * VectorSource that automatically fetching OSM way elements from OverpassAPI - */ -export default class OSMWaySource extends VectorSource> { - /** Source options */ - public options: OSMWaySourceOptions; - - /** Indicating if the source is busy fetching data. This is to prevent excessive call to the API. */ - private _busy: boolean = false; - - /** Extents in which data were fetched. Inherited from VectorSource class. */ - private get loadedExtents(): RBush<{ extent: Extent }> { - return (this as any).loadedExtentsRtree_; - } - - /** - * Constructor - * @param options Options - */ - public constructor(options?: Partial) { - super({ strategy: bbox }); - this.options = { - cachedFeaturesCount: 20000, - maximumResolution: 0, - fetchBufferSize: 0, - overpassQuery: '(way["highway"];>;);', - ...(options ?? {}) - }; - this.setLoader(this.fetchFeatures); - } - - /** - * Load features in the specified extents - * @param extent Extent to load features - * @param resolution Resolution - * @param projection Projection - */ - public loadFeatures(extent: Extent, resolution: number, projection: Projection) { - if (this._busy) return; - if (resolution > this.options.maximumResolution) return; - if (this.loadedExtents.getAll().some(e => containsExtent(e.extent, extent))) return; - super.loadFeatures(extent, resolution, projection); - this.removeLoadedExtent(extent); - } - - /** - * Fetch features from OverpassAPI - * @param extent Extent to fetch (before buffering) - * @param resolution Resolution - * @param projection Projection - * @param success Callback success function - * @param failure Callback failure function - */ - private async fetchFeatures( - extent: Extent, - resolution: number, - projection: Projection, - success?: LoaderSuccessFn, - failure?: LoaderFailedFn - ) { - const fetchExtent = this.options.fetchBufferSize ? buffer(extent, this.options.fetchBufferSize) : extent; - try { - this._busy = true; - if (this.getFeatures().length > this.options.cachedFeaturesCount) { - this.clear(true); - this.loadedExtents.clear(); - } - const features = await OSMWays.fetch(fetchExtent, this.options.overpassQuery, projection, this.options.overpassEndpointURL); - this.addFeatures(features.filter(x => !this.getFeatureById(x.getId()!))); - this.loadedExtents.insert(fetchExtent, { extent: fetchExtent }); - return success && success(features); - } catch { - return failure && failure(); - } finally { - this._busy = false; - } - } - - /** - * Get default style of OSM source to the layer, default to be invisible as it is used for snapping only. - * @returns Style - */ - public static getDefaultStyle(): Style { - return new Style({ - stroke: undefined - }); - } -} diff --git a/webpack.config.js b/webpack.config.js index f309047..1cfae82 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -8,22 +8,17 @@ module.exports = { }, externalsType: 'var', externals: { + 'ol': 'ol', 'ol/extent': 'ol.extent', 'ol/Feature': 'ol.Feature', - 'ol/geom/LineString': 'ol.geom.LineString', - 'ol/geom/MultiPoint': 'ol.geom.MultiPoint', - 'ol/geom/Point': 'ol.geom.Point', - 'ol/interaction/Pointer': 'ol.interaction.Pointer', - 'ol/interaction/Property': 'ol.interaction.Property', - 'ol/layer/Vector': 'ol.layer.Vector', + 'ol/geom': 'ol.geom', + 'ol/interaction': 'ol.interaction', + 'ol/layer': 'ol.layer', 'ol/loadingstrategy': 'ol.loadingstrategy', - 'ol/Map': 'ol.Map', 'ol/proj': 'ol.proj', - 'ol/source/Vector': 'ol.source.Vector', - 'ol/style/Circle': 'ol.style.Circle', - 'ol/style/Fill': 'ol.style.Fill', - 'ol/style/Stroke': 'ol.style.Stroke', - 'ol/style/Style': 'ol.style.Style' + 'ol/source': 'ol.source', + 'ol/style': 'ol.style', + 'ol/style/Style': 'ol.style.Style', }, output: { path: path.resolve(__dirname, 'dist', 'webpack'),