Source: OverpassRelation.js

/* 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