Source: OverpassRelation.js

  1. /* global L:false */
  2. const async = require('async')
  3. const BoundingBox = require('boundingbox')
  4. const osmtogeojson = require('osmtogeojson')
  5. const OverpassObject = require('./OverpassObject')
  6. const OverpassFrontend = require('./defines')
  7. const geojsonShiftWorld = require('./geojsonShiftWorld')
  8. const turf = require('./turf')
  9. /**
  10. * A relation
  11. * @property {string} id ID of this object, starting with 'r'.
  12. * @property {number} osm_id Numeric id.
  13. * @property {string} type Type: 'relation'.
  14. * @property {object} tags OpenStreetMap tags.
  15. * @property {object} meta OpenStreetMap meta information.
  16. * @property {GeoJSON} geometry of the object
  17. * @property {object} data Data as loaded from Overpass API.
  18. * @property {bit_array} properties Which information about this object is known?
  19. * @property {object[]} memberOf List of relations where this object is member of.
  20. * @property {string} memberOf.id ID of the relation where this object is member of.
  21. * @property {string} memberOf.role Role of this object in the relation.
  22. * @property {number} memberOf.sequence This object is the nth member in the relation.
  23. * @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)
  24. * @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)
  25. * @property {null|string} members.dir null (unknown), 'forward', 'backward'
  26. * @property {BoundingBox} bounds Bounding box of this object.
  27. * @property {Point} center Centroid of the bounding box.
  28. * @property {object[]} members Nodes of the way.
  29. * @property {string} members.id ID of the member.
  30. * @property {number} members.ref Numeric ID of the member.
  31. * @property {string} members.type 'node'.
  32. * @property {string} members.role Role of the member.
  33. * @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)
  34. * @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)
  35. * @property {null|string} members.dir null (unknown), 'forward', 'backward', 'loop'
  36. */
  37. class OverpassRelation extends OverpassObject {
  38. updateData (data, options) {
  39. super.updateData(data, options)
  40. if (data.bounds) {
  41. this.bounds = new BoundingBox(data.bounds)
  42. this.center = this.bounds.getCenter()
  43. this.properties |= OverpassFrontend.BBOX | OverpassFrontend.CENTER
  44. }
  45. if (data.center) {
  46. this.center = data.center
  47. this.properties |= OverpassFrontend.CENTER
  48. }
  49. if (data.members) {
  50. this.members = []
  51. this.properties |= OverpassFrontend.MEMBERS
  52. const membersKnown = !!this.memberFeatures
  53. this.memberFeatures = data.members.map(
  54. (member, sequence) => {
  55. this.members.push(member)
  56. // fix referenced ways from 'out geom' output
  57. if (member.type === 'way' && typeof member.ref === 'string') {
  58. const m = member.ref.match(/^_fullGeom([0-9]+)$/)
  59. if (m) {
  60. member.ref = parseInt(m[1])
  61. }
  62. }
  63. member.id = member.type.substr(0, 1) + member.ref
  64. const ob = JSON.parse(JSON.stringify(member))
  65. ob.id = ob.ref
  66. delete ob.ref
  67. delete ob.role
  68. let memberProperties = OverpassFrontend.ID_ONLY
  69. if ((member.type === 'node' && 'lat' in member) ||
  70. (member.type === 'way' && 'geometry' in member)) {
  71. memberProperties |= OverpassFrontend.GEOM
  72. }
  73. const memberOb = this.overpass.createOrUpdateOSMObject(ob, { properties: memberProperties })
  74. // call notifyMemberOf only once per member
  75. if (!membersKnown) {
  76. memberOb.notifyMemberOf(this, member.role, sequence)
  77. }
  78. return memberOb
  79. }
  80. )
  81. this.updateGeometry()
  82. }
  83. }
  84. updateGeometry () {
  85. if (!this.members) {
  86. return
  87. }
  88. let allKnown = true
  89. const elements = [{
  90. type: 'relation',
  91. id: this.osm_id,
  92. tags: this.tags,
  93. members: this.members.map(member => {
  94. const data = {
  95. ref: member.ref,
  96. type: member.type,
  97. role: member.role
  98. }
  99. if (!(member.id in this.overpass.cacheElements)) {
  100. allKnown = false
  101. return data
  102. }
  103. const ob = this.overpass.cacheElements[member.id]
  104. if ((ob.properties & OverpassFrontend.GEOM) === 0) {
  105. allKnown = false
  106. }
  107. if (ob.type === 'node') {
  108. if (ob.geometry) {
  109. data.lat = ob.geometry.lat
  110. data.lon = ob.geometry.lon
  111. }
  112. } else if (ob.type === 'way') {
  113. data.geometry = ob.geometry
  114. }
  115. return data
  116. })
  117. }]
  118. this.geometry = osmtogeojson({ elements })
  119. if (allKnown) {
  120. this.properties = this.properties | OverpassFrontend.GEOM
  121. }
  122. this.members.forEach(
  123. (member, index) => {
  124. if (member.type !== 'way') {
  125. return
  126. }
  127. const memberOb = this.overpass.cacheElements[member.id]
  128. if (!memberOb.members || member.type !== 'way') {
  129. return
  130. }
  131. const firstMemberId = memberOb.members[0].id
  132. const lastMemberId = memberOb.members[memberOb.members.length - 1].id
  133. const revMemberOf = memberOb.memberOf.filter(memberOf => memberOf.sequence === index && memberOf.id === this.id)[0]
  134. if (index > 0) {
  135. const prevMember = this.overpass.cacheElements[this.members[index - 1].id]
  136. if (prevMember.type === 'way' && prevMember.members) {
  137. if (firstMemberId === prevMember.members[0].id || firstMemberId === prevMember.members[prevMember.members.length - 1].id) {
  138. member.connectedPrev = 'forward'
  139. } else if (lastMemberId === prevMember.members[0].id || lastMemberId === prevMember.members[prevMember.members.length - 1].id) {
  140. member.connectedPrev = 'backward'
  141. } else {
  142. member.connectedPrev = 'no'
  143. }
  144. }
  145. }
  146. if (index < this.members.length - 1) {
  147. const nextMember = this.overpass.cacheElements[this.members[index + 1].id]
  148. if (nextMember.type === 'way' && nextMember.members) {
  149. if (firstMemberId === nextMember.members[0].id || firstMemberId === nextMember.members[nextMember.members.length - 1].id) {
  150. member.connectedNext = 'backward'
  151. } else if (lastMemberId === nextMember.members[0].id || lastMemberId === nextMember.members[nextMember.members.length - 1].id) {
  152. member.connectedNext = 'forward'
  153. } else {
  154. member.connectedNext = 'no'
  155. }
  156. }
  157. }
  158. if (!member.connectedPrev || !member.connectedNext) {
  159. member.dir = member.connectedPrev || member.connectedNext || null
  160. } else if (member.connectedPrev === member.connectedNext) {
  161. member.dir = member.connectedPrev || member.connectedNext || null
  162. } else {
  163. member.dir = null
  164. }
  165. if (revMemberOf) {
  166. if ('dir' in member) {
  167. revMemberOf.dir = member.dir
  168. }
  169. if ('connectedPrev' in member) {
  170. revMemberOf.connectedPrev = member.connectedPrev
  171. }
  172. if ('connectedNext' in member) {
  173. revMemberOf.connectedNext = member.connectedNext
  174. }
  175. } else {
  176. console.log('Warning: memberOf reference ' + member.id + ' -> ' + this.id + ' (#' + index + ') does not exist.')
  177. }
  178. }
  179. )
  180. if (!(this.properties & OverpassFrontend.BBOX)) {
  181. this.members.forEach(member => {
  182. const ob = this.overpass.cacheElements[member.id]
  183. if (ob.bounds) {
  184. if (this.bounds) {
  185. this.bounds.extend(ob.bounds)
  186. } else {
  187. this.bounds = new BoundingBox(ob.bounds)
  188. }
  189. }
  190. if (this.bounds) {
  191. this.center = this.bounds.getCenter()
  192. }
  193. })
  194. if (this.bounds && allKnown) {
  195. this.properties = this.properties | OverpassFrontend.BBOX | OverpassFrontend.CENTER
  196. }
  197. }
  198. }
  199. notifyMemberUpdate (memberObs) {
  200. super.notifyMemberUpdate(memberObs)
  201. if (!this.members) {
  202. return
  203. }
  204. this.updateGeometry()
  205. }
  206. /**
  207. * Return list of member ids.
  208. * @return {string[]}
  209. */
  210. memberIds () {
  211. if (this._memberIds) {
  212. return this._memberIds
  213. }
  214. if (typeof this.data.members === 'undefined') {
  215. return null
  216. }
  217. this._memberIds = []
  218. for (let i = 0; i < this.data.members.length; i++) {
  219. const member = this.data.members[i]
  220. this._memberIds.push(member.type.substr(0, 1) + member.ref)
  221. }
  222. return this._memberIds
  223. }
  224. member_ids () { // eslint-disable-line
  225. console.log('called deprecated OverpassRelation.member_ids() function - replace by memberIds()')
  226. return this.memberIds()
  227. }
  228. /**
  229. * return a leaflet feature for this object.
  230. * @param {object} [options] options Options will be passed to the leaflet function
  231. * @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)
  232. * @return {L.layer}
  233. */
  234. leafletFeature (options = {}) {
  235. if (!this.data.members) {
  236. return null
  237. }
  238. if (!('shiftWorld' in options)) {
  239. options.shiftWorld = [0, 0]
  240. }
  241. // no geometry? use the member features instead
  242. if (!this.geometry) {
  243. const feature = L.featureGroup()
  244. feature._updateCallbacks = []
  245. return feature
  246. }
  247. const feature = L.geoJSON(geojsonShiftWorld(this.geometry, options.shiftWorld), {
  248. pointToLayer: function (options, geoJsonPoint, member) {
  249. let feature
  250. switch (options.nodeFeature) {
  251. case 'Marker':
  252. feature = L.marker(member, options)
  253. break
  254. case 'Circle':
  255. feature = L.circle(member, options.radius, options)
  256. break
  257. case 'CircleMarker':
  258. default:
  259. feature = L.circleMarker(member, options)
  260. }
  261. return feature
  262. }.bind(this, options)
  263. })
  264. feature.setStyle(options)
  265. // create an event handler on the 'update' event, so that loading member
  266. // features will update geometry
  267. this.memberFeatures.forEach(
  268. (member, index) => {
  269. if (!(member.properties & OverpassFrontend.GEOM)) {
  270. const updFun = member => {
  271. feature.clearLayers()
  272. feature.addData(this.geometry)
  273. feature.setStyle(options)
  274. }
  275. member.once('update', updFun)
  276. }
  277. }
  278. )
  279. return feature
  280. }
  281. GeoJSON () {
  282. const ret = {
  283. type: 'Feature',
  284. id: this.type + '/' + this.osm_id,
  285. properties: this.GeoJSONProperties()
  286. }
  287. if (this.members) {
  288. if (this.geometry.features.length === 1) {
  289. ret.geometry = this.geometry.features[0].geometry
  290. } else {
  291. ret.geometry = {
  292. type: 'GeometryCollection',
  293. geometries: this.memberFeatures
  294. .map(member => member.GeoJSON().geometry) // .geometry may be undefined
  295. .filter(member => member)
  296. .filter(member => member.type !== 'GeometryCollection' || member.geometries.length)
  297. }
  298. }
  299. }
  300. return ret
  301. }
  302. exportOSMXML (options, parentNode, callback) {
  303. super.exportOSMXML(options, parentNode,
  304. (err, result) => {
  305. if (err) {
  306. return callback(err)
  307. }
  308. if (!result) { // already included
  309. return callback(null)
  310. }
  311. if (this.members) {
  312. async.each(this.members,
  313. (member, done) => {
  314. const memberOb = this.overpass.cacheElements[member.id]
  315. const nd = parentNode.ownerDocument.createElement('member')
  316. nd.setAttribute('ref', memberOb.osm_id)
  317. nd.setAttribute('type', memberOb.type)
  318. nd.setAttribute('role', member.role)
  319. result.appendChild(nd)
  320. memberOb.exportOSMXML(options, parentNode, done)
  321. },
  322. (err) => {
  323. callback(err, result)
  324. }
  325. )
  326. } else {
  327. callback(null, result)
  328. }
  329. }
  330. )
  331. }
  332. exportOSMJSON (conf, elements, callback) {
  333. super.exportOSMJSON(conf, elements,
  334. (err, result) => {
  335. if (err) {
  336. return callback(err)
  337. }
  338. if (!result) { // already included
  339. return callback(null)
  340. }
  341. if (this.members) {
  342. result.members = []
  343. async.each(this.members,
  344. (member, done) => {
  345. const memberOb = this.overpass.cacheElements[member.id]
  346. result.members.push({
  347. ref: memberOb.osm_id,
  348. type: memberOb.type,
  349. role: member.role
  350. })
  351. memberOb.exportOSMJSON(conf, elements, done)
  352. },
  353. (err) => {
  354. callback(err, result)
  355. }
  356. )
  357. } else {
  358. callback(null, result)
  359. }
  360. }
  361. )
  362. }
  363. intersects (bbox) {
  364. const result = super.intersects(bbox)
  365. if (result === 0 || result === 2) {
  366. return result
  367. }
  368. let i
  369. if (this.geometry) {
  370. let geometry = this.geometry
  371. let bboxShifted = bbox.toGeoJSON ? bbox.toGeoJSON() : bbox
  372. if (this.bounds && this.bounds.minlon > this.bounds.maxlon) {
  373. geometry = geojsonShiftWorld(geometry, [360, 0])
  374. bboxShifted = geojsonShiftWorld(bboxShifted, [360, 0])
  375. }
  376. if (turf.booleanIntersects(geometry, bboxShifted)) {
  377. return 2
  378. }
  379. // if there's a relation member (where Overpass does not return the
  380. // geometry) we can't know if the geometry intersects -> return 1
  381. for (i = 0; i < this.data.members.length; i++) {
  382. if (this.data.members[i].type === 'relation') {
  383. return 1
  384. }
  385. }
  386. // if there's no relation member and the geometry is complete we can be sure there's no intersection
  387. return this.properties & OverpassFrontend.GEOM ? 0 : 1
  388. } else if (this.members) {
  389. for (i in this.members) {
  390. const memberId = this.members[i].id
  391. const member = this.overpass.cacheElements[memberId]
  392. if (member) {
  393. if (member.intersects(bbox) === 2) {
  394. return 2
  395. }
  396. }
  397. }
  398. }
  399. return 1
  400. }
  401. }
  402. module.exports = OverpassRelation