UNPKG

overpass-frontend

Version:

A JavaScript (NodeJS/Browser) library to easily access data from OpenStreetMap via Overpass API or from an OSM File. The objects can directly be used with LeafletJS or exported to GeoJSON. Data will be cached in the browser memory.

477 lines (402 loc) 14.9 kB
/* global L:false */ const async = require('async') const BoundingBox = require('boundingbox') const osmtogeojson = require('osmtogeojson') const OverpassObject = require('./OverpassObject') const OverpassFrontend = require('./defines') const geojsonShiftWorld = require('./geojsonShiftWorld') const turf = require('./turf') /** * A relation * @property {string} id ID of this object, starting with 'r'. * @property {number} osm_id Numeric id. * @property {string} type Type: 'relation'. * @property {object} tags OpenStreetMap tags. * @property {object} meta OpenStreetMap meta information. * @property {GeoJSON} geometry of the object * @property {object} data Data as loaded from Overpass API. * @property {bit_array} properties Which information about this object is known? * @property {object[]} memberOf List of relations where this object is member of. * @property {string} memberOf.id ID of the relation where this object is member of. * @property {string} memberOf.role Role of this object in the relation. * @property {number} memberOf.sequence This object is the nth member in the relation. * @property {null|string} memberOf.connectedPrev null (unknown), 'no' (connected), 'forward' (connected at the front end of this way), 'backward' (connected at the back end of this way) * @property {null|string} memberOf.connectedNext null (unknown), 'no' (connected), 'forward' (connected at the back end of this way), 'backward' (connected at the front end of this way) * @property {null|string} members.dir null (unknown), 'forward', 'backward' * @property {BoundingBox} bounds Bounding box of this object. * @property {Point} center Centroid of the bounding box. * @property {object[]} members Nodes of the way. * @property {string} members.id ID of the member. * @property {number} members.ref Numeric ID of the member. * @property {string} members.type 'node'. * @property {string} members.role Role of the member. * @property {null|string} members.connectedPrev null (unknown), 'no' (connected), 'forward' (connected at the front end of this way), 'backward' (connected at the back end of this way) * @property {null|string} members.connectedNext null (unknown), 'no' (connected), 'forward' (connected at the back end of this way), 'backward' (connected at the fornt end of this way) * @property {null|string} members.dir null (unknown), 'forward', 'backward', 'loop' */ class OverpassRelation extends OverpassObject { updateData (data, options) { super.updateData(data, options) if (data.bounds) { this.bounds = new BoundingBox(data.bounds) this.center = this.bounds.getCenter() this.properties |= OverpassFrontend.BBOX | OverpassFrontend.CENTER } if (data.center) { this.center = data.center this.properties |= OverpassFrontend.CENTER } if (data.members) { this.members = [] this.properties |= OverpassFrontend.MEMBERS const membersKnown = !!this.memberFeatures this.memberFeatures = data.members.map( (member, sequence) => { this.members.push(member) // fix referenced ways from 'out geom' output if (member.type === 'way' && typeof member.ref === 'string') { const m = member.ref.match(/^_fullGeom([0-9]+)$/) if (m) { member.ref = parseInt(m[1]) } } member.id = member.type.substr(0, 1) + member.ref const ob = JSON.parse(JSON.stringify(member)) ob.id = ob.ref delete ob.ref delete ob.role let memberProperties = OverpassFrontend.ID_ONLY if ((member.type === 'node' && 'lat' in member) || (member.type === 'way' && 'geometry' in member)) { memberProperties |= OverpassFrontend.GEOM } const memberOb = this.overpass.createOrUpdateOSMObject(ob, { properties: memberProperties }) // call notifyMemberOf only once per member if (!membersKnown) { memberOb.notifyMemberOf(this, member.role, sequence) } return memberOb } ) this.updateGeometry() } } updateGeometry () { if (!this.members) { return } let allKnown = true const elements = [{ type: 'relation', id: this.osm_id, tags: this.tags, members: this.members.map(member => { const data = { ref: member.ref, type: member.type, role: member.role } if (!(member.id in this.overpass.cacheElements)) { allKnown = false return data } const ob = this.overpass.cacheElements[member.id] if ((ob.properties & OverpassFrontend.GEOM) === 0) { allKnown = false } if (ob.type === 'node') { if (ob.geometry) { data.lat = ob.geometry.lat data.lon = ob.geometry.lon } } else if (ob.type === 'way') { data.geometry = ob.geometry } return data }) }] this.geometry = osmtogeojson({ elements }) if (allKnown) { this.properties = this.properties | OverpassFrontend.GEOM } this.members.forEach( (member, index) => { if (member.type !== 'way') { return } const memberOb = this.overpass.cacheElements[member.id] if (!memberOb.members || member.type !== 'way') { return } const firstMemberId = memberOb.members[0].id const lastMemberId = memberOb.members[memberOb.members.length - 1].id const revMemberOf = memberOb.memberOf.filter(memberOf => memberOf.sequence === index && memberOf.id === this.id)[0] if (index > 0) { const prevMember = this.overpass.cacheElements[this.members[index - 1].id] if (prevMember.type === 'way' && prevMember.members) { if (firstMemberId === prevMember.members[0].id || firstMemberId === prevMember.members[prevMember.members.length - 1].id) { member.connectedPrev = 'forward' } else if (lastMemberId === prevMember.members[0].id || lastMemberId === prevMember.members[prevMember.members.length - 1].id) { member.connectedPrev = 'backward' } else { member.connectedPrev = 'no' } } } if (index < this.members.length - 1) { const nextMember = this.overpass.cacheElements[this.members[index + 1].id] if (nextMember.type === 'way' && nextMember.members) { if (firstMemberId === nextMember.members[0].id || firstMemberId === nextMember.members[nextMember.members.length - 1].id) { member.connectedNext = 'backward' } else if (lastMemberId === nextMember.members[0].id || lastMemberId === nextMember.members[nextMember.members.length - 1].id) { member.connectedNext = 'forward' } else { member.connectedNext = 'no' } } } if (!member.connectedPrev || !member.connectedNext) { member.dir = member.connectedPrev || member.connectedNext || null } else if (member.connectedPrev === member.connectedNext) { member.dir = member.connectedPrev || member.connectedNext || null } else { member.dir = null } if (revMemberOf) { if ('dir' in member) { revMemberOf.dir = member.dir } if ('connectedPrev' in member) { revMemberOf.connectedPrev = member.connectedPrev } if ('connectedNext' in member) { revMemberOf.connectedNext = member.connectedNext } } else { console.log('Warning: memberOf reference ' + member.id + ' -> ' + this.id + ' (#' + index + ') does not exist.') } } ) if (!(this.properties & OverpassFrontend.BBOX)) { this.members.forEach(member => { const ob = this.overpass.cacheElements[member.id] if (ob.bounds) { if (this.bounds) { this.bounds.extend(ob.bounds) } else { this.bounds = new BoundingBox(ob.bounds) } } if (this.bounds) { this.center = this.bounds.getCenter() } }) if (this.bounds && allKnown) { this.properties = this.properties | OverpassFrontend.BBOX | OverpassFrontend.CENTER } } } notifyMemberUpdate (memberObs) { super.notifyMemberUpdate(memberObs) if (!this.members) { return } this.updateGeometry() } /** * Return list of member ids. * @return {string[]} */ memberIds () { if (this._memberIds) { return this._memberIds } if (typeof this.data.members === 'undefined') { return null } this._memberIds = [] for (let i = 0; i < this.data.members.length; i++) { const member = this.data.members[i] this._memberIds.push(member.type.substr(0, 1) + member.ref) } return this._memberIds } member_ids () { // eslint-disable-line console.log('called deprecated OverpassRelation.member_ids() function - replace by memberIds()') return this.memberIds() } /** * return a leaflet feature for this object. * @param {object} [options] options Options will be passed to the leaflet function * @param {number[]} [options.shiftWorld=[0, 0]] Shift western (negative) longitudes by shiftWorld[0], eastern (positive) longitudes by shiftWorld[1] (e.g. by 360, 0 to show objects around lon=180) * @return {L.layer} */ leafletFeature (options = {}) { if (!this.data.members) { return null } if (!('shiftWorld' in options)) { options.shiftWorld = [0, 0] } // no geometry? use the member features instead if (!this.geometry) { const feature = L.featureGroup() feature._updateCallbacks = [] return feature } const feature = L.geoJSON(geojsonShiftWorld(this.geometry, options.shiftWorld), { pointToLayer: function (options, geoJsonPoint, member) { let feature switch (options.nodeFeature) { case 'Marker': feature = L.marker(member, options) break case 'Circle': feature = L.circle(member, options.radius, options) break case 'CircleMarker': default: feature = L.circleMarker(member, options) } return feature }.bind(this, options) }) feature.setStyle(options) // create an event handler on the 'update' event, so that loading member // features will update geometry this.memberFeatures.forEach( (member, index) => { if (!(member.properties & OverpassFrontend.GEOM)) { const updFun = member => { feature.clearLayers() feature.addData(this.geometry) feature.setStyle(options) } member.once('update', updFun) } } ) return feature } GeoJSON () { const ret = { type: 'Feature', id: this.type + '/' + this.osm_id, properties: this.GeoJSONProperties() } if (this.members) { if (this.geometry.features.length === 1) { ret.geometry = this.geometry.features[0].geometry } else { ret.geometry = { type: 'GeometryCollection', geometries: this.memberFeatures .map(member => member.GeoJSON().geometry) // .geometry may be undefined .filter(member => member) .filter(member => member.type !== 'GeometryCollection' || member.geometries.length) } } } return ret } exportOSMXML (options, parentNode, callback) { super.exportOSMXML(options, parentNode, (err, result) => { if (err) { return callback(err) } if (!result) { // already included return callback(null) } if (this.members) { async.each(this.members, (member, done) => { const memberOb = this.overpass.cacheElements[member.id] const nd = parentNode.ownerDocument.createElement('member') nd.setAttribute('ref', memberOb.osm_id) nd.setAttribute('type', memberOb.type) nd.setAttribute('role', member.role) result.appendChild(nd) memberOb.exportOSMXML(options, parentNode, done) }, (err) => { callback(err, result) } ) } else { callback(null, result) } } ) } exportOSMJSON (conf, elements, callback) { super.exportOSMJSON(conf, elements, (err, result) => { if (err) { return callback(err) } if (!result) { // already included return callback(null) } if (this.members) { result.members = [] async.each(this.members, (member, done) => { const memberOb = this.overpass.cacheElements[member.id] result.members.push({ ref: memberOb.osm_id, type: memberOb.type, role: member.role }) memberOb.exportOSMJSON(conf, elements, done) }, (err) => { callback(err, result) } ) } else { callback(null, result) } } ) } intersects (bbox) { const result = super.intersects(bbox) if (result === 0 || result === 2) { return result } let i if (this.geometry) { let geometry = this.geometry let bboxShifted = bbox.toGeoJSON ? bbox.toGeoJSON() : bbox if (this.bounds && this.bounds.minlon > this.bounds.maxlon) { geometry = geojsonShiftWorld(geometry, [360, 0]) bboxShifted = geojsonShiftWorld(bboxShifted, [360, 0]) } if (turf.booleanIntersects(geometry, bboxShifted)) { return 2 } // if there's a relation member (where Overpass does not return the // geometry) we can't know if the geometry intersects -> return 1 for (i = 0; i < this.data.members.length; i++) { if (this.data.members[i].type === 'relation') { return 1 } } // if there's no relation member and the geometry is complete we can be sure there's no intersection return this.properties & OverpassFrontend.GEOM ? 0 : 1 } else if (this.members) { for (i in this.members) { const memberId = this.members[i].id const member = this.overpass.cacheElements[memberId] if (member) { if (member.intersects(bbox) === 2) { return 2 } } } } return 1 } } module.exports = OverpassRelation