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.
290 lines (238 loc) • 8.05 kB
JavaScript
/* global L:false */
const async = require('async')
const BoundingBox = require('boundingbox')
const OverpassObject = require('./OverpassObject')
const OverpassFrontend = require('./defines')
const turf = require('./turf')
/**
* A way
* @property {string} id ID of this object, starting with 'w'.
* @property {number} osm_id Numeric id.
* @property {string} type Type: 'way'.
* @property {object} tags OpenStreetMap tags.
* @property {object} meta OpenStreetMap meta information.
* @property {Point[]} 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 way 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 {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'.
*/
class OverpassWay extends OverpassObject {
updateData (data, options) {
if (data.nodes) {
this.nodes = data.nodes
}
if (data.geometry) {
this.geometry = data.geometry
this.properties |= OverpassFrontend.GEOM
}
super.updateData(data, options)
if (typeof this.data.nodes !== 'undefined') {
this.members = []
this.properties |= OverpassFrontend.MEMBERS
for (let i = 0; i < this.data.nodes.length; i++) {
this.members.push({
id: 'n' + this.data.nodes[i],
ref: this.data.nodes[i],
type: 'node'
})
let obProperties = OverpassFrontend.ID_ONLY
const ob = {
id: this.data.nodes[i],
type: 'node'
}
if (data.geometry && data.geometry[i]) {
obProperties = obProperties | OverpassFrontend.GEOM
ob.lat = data.geometry[i].lat
ob.lon = data.geometry[i].lon
}
const memberOb = this.overpass.createOrUpdateOSMObject(ob, {
properties: obProperties
})
memberOb.notifyMemberOf(this, null, i)
}
}
this.checkGeometry()
}
notifyMemberUpdate (memberObs) {
super.notifyMemberUpdate(memberObs)
this.checkGeometry()
}
checkGeometry () {
if (this.members && (this.properties & OverpassFrontend.GEOM) === 0) {
this.geometry = this.members.map(
member => {
const node = this.overpass.cacheElements[member.id]
return node ? node.geometry : null
}
).filter(geom => geom)
if (this.geometry.length === 0) {
delete this.geometry
return
}
if (this.geometry.length === this.members.length) {
this.properties = this.properties | OverpassFrontend.GEOM
}
}
if (this.geometry && (this.properties & OverpassFrontend.BBOX) === 0) {
this.bounds = new BoundingBox(this.geometry[0])
this.geometry.slice(1).forEach(geom => this.bounds.extend(geom))
}
if (this.bounds && (this.properties & OverpassFrontend.CENTER) === 0) {
this.center = this.bounds.getCenter()
}
if ((this.properties & OverpassFrontend.GEOM) === OverpassFrontend.GEOM) {
this.properties = this.properties | OverpassFrontend.BBOX | OverpassFrontend.CENTER
}
}
memberIds () {
if (this._memberIds) {
return this._memberIds
}
if (!this.nodes) {
return null
}
this._memberIds = []
for (let i = 0; i < this.nodes.length; i++) {
const member = this.nodes[i]
this._memberIds.push('n' + member)
}
return this._memberIds
}
member_ids () { // eslint-disable-line
console.log('called deprecated OverpassWay.member_ids() function - replace by memberIds()')
return this.memberIds()
}
GeoJSON () {
const result = {
type: 'Feature',
id: this.type + '/' + this.osm_id,
properties: this.GeoJSONProperties()
}
if (this.geometry) {
const coordinates = this.geometry
.filter(point => point) // discard non-loaded points
.map(point => [point.lon, point.lat])
const isClosed = coordinates.length > 1 && this.members && this.members[0].id === this.members[this.members.length - 1].id
if (isClosed) {
result.geometry = {
type: 'Polygon',
coordinates: [coordinates]
}
} else {
result.geometry = {
type: 'LineString',
coordinates: coordinates
}
}
}
return result
}
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('nd')
nd.setAttribute('ref', memberOb.osm_id)
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.nodes = []
async.each(this.members,
(member, done) => {
const memberOb = this.overpass.cacheElements[member.id]
result.nodes.push(memberOb.osm_id)
memberOb.exportOSMJSON(conf, elements, done)
},
(err) => {
callback(err, result)
}
)
} else {
callback(null, result)
}
}
)
}
/**
* return a leaflet feature for this object. If the ways is closed, a L.polygon will be returned, otherwise a L.polyline.
* @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.geometry) {
return null
}
if (!('shiftWorld' in options)) {
options.shiftWorld = [0, 0]
}
const geom = this.geometry
.filter(g => g)
.map(g => {
return { lat: g.lat, lon: g.lon + options.shiftWorld[g.lon < 0 ? 0 : 1] }
})
if (this.geometry[this.geometry.length - 1] && this.geometry[0] &&
this.geometry[this.geometry.length - 1].lat === this.geometry[0].lat &&
this.geometry[this.geometry.length - 1].lon === this.geometry[0].lon) {
return L.polygon(geom, options)
}
return L.polyline(geom, options)
}
intersects (bbox) {
const result = super.intersects(bbox)
if (result === 0 || result === 2) {
return result
}
if (this.geometry) {
let intersects
if (bbox.toGeoJSON) {
// bbox is BoundingBox
intersects = turf.booleanIntersects(this.GeoJSON(), bbox.toGeoJSON())
} else {
// bbox is GeoJSON
intersects = turf.booleanIntersects(this.GeoJSON(), bbox)
}
return intersects ? 2 : 0
}
return 1
}
}
module.exports = OverpassWay