UNPKG

osmtogeojson

Version:
1,066 lines (1,045 loc) 39.9 kB
var _ = require("./lodash.custom.js"); var rewind = require("@mapbox/geojson-rewind"); // see https://wiki.openstreetmap.org/wiki/Overpass_turbo/Polygon_Features var polygonFeatures = {}; require("osm-polygon-features").forEach(function(tags) { if (tags.polygon === "all") polygonFeatures[tags.key] = true; else { var list = (tags.polygon === "whitelist") ? "included_values" : "excluded_values", tagValuesObj = {}; tags.values.forEach(function(value) { tagValuesObj[value] = true; }); polygonFeatures[tags.key] = {}; polygonFeatures[tags.key][list] = tagValuesObj; } }); // default deduplication helper function function default_deduplicator(objectA, objectB) { // default deduplication handler: // if object versions differ, use highest available version if ((objectA.version || objectB.version) && (objectA.version !== objectB.version)) { return (+objectA.version || 0) > (+objectB.version || 0) ? objectA : objectB; } // otherwise: return merged obj properties return _.merge(objectA, objectB); } var osmtogeojson = {}; osmtogeojson = function( data, options, featureCallback ) { options = _.merge( { verbose: false, flatProperties: true, uninterestingTags: { "source": true, "source_ref": true, "source:ref": true, "history": true, "attribution": true, "created_by": true, "tiger:county": true, "tiger:tlid": true, "tiger:upload_uuid": true }, polygonFeatures: polygonFeatures, deduplicator: default_deduplicator }, options ); var result; if ( ((typeof XMLDocument !== "undefined") && data instanceof XMLDocument || (typeof XMLDocument === "undefined") && data.childNodes) ) result = _osmXML2geoJSON(data); else result = _overpassJSON2geoJSON(data); return result; function _overpassJSON2geoJSON(json) { // sort elements var nodes = new Array(); var ways = new Array(); var rels = new Array(); // helper functions function centerGeometry(object) { var pseudoNode = _.clone(object); pseudoNode.lat = object.center.lat; pseudoNode.lon = object.center.lon; pseudoNode.__is_center_placeholder = true; nodes.push(pseudoNode); } function boundsGeometry(object) { var pseudoWay = _.clone(object); pseudoWay.nodes = []; function addPseudoNode(lat,lon,i) { var pseudoNode = { type:"node", id: "_"+pseudoWay.type+"/"+pseudoWay.id+"bounds"+i, lat: lat, lon: lon } pseudoWay.nodes.push(pseudoNode.id); nodes.push(pseudoNode); } addPseudoNode(pseudoWay.bounds.minlat,pseudoWay.bounds.minlon,1); addPseudoNode(pseudoWay.bounds.maxlat,pseudoWay.bounds.minlon,2); addPseudoNode(pseudoWay.bounds.maxlat,pseudoWay.bounds.maxlon,3); addPseudoNode(pseudoWay.bounds.minlat,pseudoWay.bounds.maxlon,4); pseudoWay.nodes.push(pseudoWay.nodes[0]); pseudoWay.__is_bounds_placeholder = true; ways.push(pseudoWay); } function fullGeometryWay(way) { function addFullGeometryNode(lat,lon,id) { var geometryNode = { type:"node", id: id, lat: lat, lon: lon } nodes.push(geometryNode); } if (!_.isArray(way.nodes)) { way.nodes = way.geometry.map(function(nd) { if (nd !== null) // have to skip ref-less nodes return "_anonymous@"+nd.lat+"/"+nd.lon; else return "_anonymous@unknown_location"; }); } way.geometry.forEach(function(nd, i) { if (nd) { addFullGeometryNode( nd.lat, nd.lon, way.nodes[i] ); } }); } function fullGeometryRelation(rel) { function addFullGeometryNode(lat,lon,id) { var geometryNode = { type:"node", id: id, lat: lat, lon: lon } nodes.push(geometryNode); } function addFullGeometryWay(geometry,id) { // shared multipolygon ways cannot be defined multiple times with the same id. if (ways.some(function (way) { // todo: this is slow :( return way.type == "way" && way.id == id; })) return; var geometryWay = { type: "way", id: id, nodes:[] } function addFullGeometryWayPseudoNode(lat,lon) { // todo? do not save the same pseudo node multiple times var geometryPseudoNode = { type:"node", id: "_anonymous@"+lat+"/"+lon, lat: lat, lon: lon } geometryWay.nodes.push(geometryPseudoNode.id); nodes.push(geometryPseudoNode); } geometry.forEach(function(nd) { if (nd) { addFullGeometryWayPseudoNode( nd.lat, nd.lon ); } else { geometryWay.nodes.push(undefined); } }); ways.push(geometryWay); } rel.members.forEach(function(member, i) { if (member.type == "node") { if (member.lat) { addFullGeometryNode( member.lat, member.lon, member.ref ); } } else if (member.type == "way") { if (member.geometry) { member.ref = "_fullGeom"+member.ref; addFullGeometryWay( member.geometry, member.ref ); } } }); } // create copies of individual json objects to make sure the original data doesn't get altered // todo: cloning is slow: see if this can be done differently! for (var i=0;i<json.elements.length;i++) { switch (json.elements[i].type) { case "node": var node = json.elements[i]; nodes.push(node); break; case "way": var way = _.clone(json.elements[i]); way.nodes = _.clone(way.nodes); ways.push(way); if (way.center) centerGeometry(way); if (way.geometry) fullGeometryWay(way); else if (way.bounds) boundsGeometry(way); break; case "relation": var rel = _.clone(json.elements[i]); rel.members = _.clone(rel.members); rels.push(rel); var has_full_geometry = rel.members && rel.members.some(function (member) { return member.type == "node" && member.lat || member.type == "way" && member.geometry && member.geometry.length > 0 }); if (rel.center) centerGeometry(rel); if (has_full_geometry) fullGeometryRelation(rel); else if (rel.bounds) boundsGeometry(rel); break; default: // type=area (from coord-query) is an example for this case. } } return _convert2geoJSON(nodes,ways,rels); } function _osmXML2geoJSON(xml) { // sort elements var nodes = new Array(); var ways = new Array(); var rels = new Array(); // helper function function copy_attribute( x, o, attr ) { if (x.hasAttribute(attr)) o[attr] = x.getAttribute(attr); } function centerGeometry(object, centroid) { var pseudoNode = _.clone(object); copy_attribute(centroid, pseudoNode, 'lat'); copy_attribute(centroid, pseudoNode, 'lon'); pseudoNode.__is_center_placeholder = true; nodes.push(pseudoNode); } function boundsGeometry(object, bounds) { var pseudoWay = _.clone(object); pseudoWay.nodes = []; function addPseudoNode(lat,lon,i) { var pseudoNode = { type:"node", id: "_"+pseudoWay.type+"/"+pseudoWay.id+"bounds"+i, lat: lat, lon: lon } pseudoWay.nodes.push(pseudoNode.id); nodes.push(pseudoNode); } addPseudoNode(bounds.getAttribute('minlat'),bounds.getAttribute('minlon'),1); addPseudoNode(bounds.getAttribute('maxlat'),bounds.getAttribute('minlon'),2); addPseudoNode(bounds.getAttribute('maxlat'),bounds.getAttribute('maxlon'),3); addPseudoNode(bounds.getAttribute('minlat'),bounds.getAttribute('maxlon'),4); pseudoWay.nodes.push(pseudoWay.nodes[0]); pseudoWay.__is_bounds_placeholder = true; ways.push(pseudoWay); } function fullGeometryWay(way, nds) { function addFullGeometryNode(lat,lon,id) { var geometryNode = { type:"node", id: id, lat: lat, lon: lon } nodes.push(geometryNode); return geometryNode.id; } if (!_.isArray(way.nodes)) { way.nodes = []; _.each( nds, function( nd, i ) { way.nodes.push("_anonymous@"+nd.getAttribute('lat')+"/"+nd.getAttribute('lon')); }); } _.each( nds, function( nd, i ) { if (nd.getAttribute('lat')) { addFullGeometryNode( nd.getAttribute('lat'), nd.getAttribute('lon'), way.nodes[i] ); } }); } function fullGeometryRelation(rel, members) { function addFullGeometryNode(lat,lon,id) { var geometryNode = { type:"node", id: id, lat: lat, lon: lon } nodes.push(geometryNode); } function addFullGeometryWay(nds,id) { // shared multipolygon ways cannot be defined multiple times with the same id. if (ways.some(function (way) { // todo: this is slow :( return way.type == "way" && way.id == id; })) return; var geometryWay = { type: "way", id: id, nodes:[] } function addFullGeometryWayPseudoNode(lat,lon) { // todo? do not save the same pseudo node multiple times var geometryPseudoNode = { type:"node", id: "_anonymous@"+lat+"/"+lon, lat: lat, lon: lon } geometryWay.nodes.push(geometryPseudoNode.id); nodes.push(geometryPseudoNode); } _.each(nds, function(nd) { if (nd.getAttribute('lat')) { addFullGeometryWayPseudoNode( nd.getAttribute('lat'), nd.getAttribute('lon') ); } else { geometryWay.nodes.push(undefined); } }); ways.push(geometryWay); } _.each( members, function( member, i ) { if (rel.members[i].type == "node") { if (member.getAttribute('lat')) { addFullGeometryNode( member.getAttribute('lat'), member.getAttribute('lon'), rel.members[i].ref ); } } else if (rel.members[i].type == "way") { if (member.getElementsByTagName('nd').length > 0) { rel.members[i].ref = "_fullGeom"+rel.members[i].ref; addFullGeometryWay( member.getElementsByTagName('nd'), rel.members[i].ref ); } } }); } // nodes _.each( xml.getElementsByTagName('node'), function( node, i ) { var tags = {}; _.each( node.getElementsByTagName('tag'), function( tag ) { tags[tag.getAttribute('k')] = tag.getAttribute('v'); }); var nodeObject = { 'type': 'node' }; copy_attribute( node, nodeObject, 'id' ); copy_attribute( node, nodeObject, 'lat' ); copy_attribute( node, nodeObject, 'lon' ); copy_attribute( node, nodeObject, 'version' ); copy_attribute( node, nodeObject, 'timestamp' ); copy_attribute( node, nodeObject, 'changeset' ); copy_attribute( node, nodeObject, 'uid' ); copy_attribute( node, nodeObject, 'user' ); if (!_.isEmpty(tags)) nodeObject.tags = tags; nodes.push(nodeObject); }); // ways var centroid,bounds; _.each( xml.getElementsByTagName('way'), function( way, i ) { var tags = {}; var wnodes = []; _.each( way.getElementsByTagName('tag'), function( tag ) { tags[tag.getAttribute('k')] = tag.getAttribute('v'); }); var has_full_geometry = false; _.each( way.getElementsByTagName('nd'), function( nd, i ) { var id; if (id = nd.getAttribute('ref')) wnodes[i] = id; if (!has_full_geometry && nd.getAttribute('lat')) has_full_geometry = true; }); var wayObject = { "type": "way" }; copy_attribute( way, wayObject, 'id' ); copy_attribute( way, wayObject, 'version' ); copy_attribute( way, wayObject, 'timestamp' ); copy_attribute( way, wayObject, 'changeset' ); copy_attribute( way, wayObject, 'uid' ); copy_attribute( way, wayObject, 'user' ); if (wnodes.length > 0) wayObject.nodes = wnodes; if (!_.isEmpty(tags)) wayObject.tags = tags; if (centroid = way.getElementsByTagName('center')[0]) centerGeometry(wayObject,centroid); if (has_full_geometry) fullGeometryWay(wayObject, way.getElementsByTagName('nd')); else if (bounds = way.getElementsByTagName('bounds')[0]) boundsGeometry(wayObject,bounds); ways.push(wayObject); }); // relations _.each( xml.getElementsByTagName('relation'), function( relation, i ) { var tags = {}; var members = []; _.each( relation.getElementsByTagName('tag'), function( tag ) { tags[tag.getAttribute('k')] = tag.getAttribute('v'); }); var has_full_geometry = false; _.each( relation.getElementsByTagName('member'), function( member, i ) { members[i] = {}; copy_attribute( member, members[i], 'ref' ); copy_attribute( member, members[i], 'role' ); copy_attribute( member, members[i], 'type' ); if (!has_full_geometry && (members[i].type == 'node' && member.getAttribute('lat')) || (members[i].type == 'way' && member.getElementsByTagName('nd').length>0) ) has_full_geometry = true; }); var relObject = { "type": "relation" } copy_attribute( relation, relObject, 'id' ); copy_attribute( relation, relObject, 'version' ); copy_attribute( relation, relObject, 'timestamp' ); copy_attribute( relation, relObject, 'changeset' ); copy_attribute( relation, relObject, 'uid' ); copy_attribute( relation, relObject, 'user' ); if (members.length > 0) relObject.members = members; if (!_.isEmpty(tags)) relObject.tags = tags; if (centroid = relation.getElementsByTagName('center')[0]) centerGeometry(relObject,centroid); if (has_full_geometry) fullGeometryRelation(relObject, relation.getElementsByTagName('member')); else if (bounds = relation.getElementsByTagName('bounds')[0]) boundsGeometry(relObject,bounds); rels.push(relObject); }); return _convert2geoJSON(nodes,ways,rels); } function _convert2geoJSON(nodes,ways,rels) { // helper function that checks if there are any tags other than "created_by", "source", etc. or any tag provided in ignore_tags function has_interesting_tags(t, ignore_tags) { if (typeof ignore_tags !== "object") ignore_tags={}; if (typeof options.uninterestingTags === "function") return !options.uninterestingTags(t, ignore_tags); for (var k in t) if (!(options.uninterestingTags[k]===true) && !(ignore_tags[k]===true || ignore_tags[k]===t[k])) return true; return false; }; // helper function to extract meta information function build_meta_information(object) { var res = { "timestamp": object.timestamp, "version": object.version, "changeset": object.changeset, "user": object.user, "uid": object.uid }; for (var k in res) if (res[k] === undefined) delete res[k]; return res; } // some data processing (e.g. filter nodes only used for ways) var nodeids = new Object(); var poinids = new Object(); for (var i=0;i<nodes.length;i++) { var node = nodes[i]; if (nodeids[node.id] !== undefined) { // handle input data duplication node = options.deduplicator(node, nodeids[node.id]); } nodeids[node.id] = node; if (typeof node.tags != 'undefined' && has_interesting_tags(node.tags)) // this checks if the node has any tags other than "created_by" poinids[node.id] = true; } // todo -> after deduplication of relations?? for (var i=0;i<rels.length;i++) { if (_.isArray(rels[i].members)) { for (var j=0;j<rels[i].members.length;j++) { if (rels[i].members[j].type == "node") poinids[rels[i].members[j].ref] = true; } } } var wayids = new Object(); var waynids = new Object(); for (var i=0;i<ways.length;i++) { var way = ways[i]; if (wayids[way.id]) { // handle input data duplication way = options.deduplicator(way, wayids[way.id]); } wayids[way.id] = way; if (_.isArray(way.nodes)) { for (var j=0;j<way.nodes.length;j++) { if (typeof way.nodes[j] === "object") continue; // ignore already replaced way node objects waynids[way.nodes[j]] = true; way.nodes[j] = nodeids[way.nodes[j]]; } } } var pois = new Array(); for (var id in nodeids) { var node = nodeids[id]; if (!waynids[id] || poinids[id]) pois.push(node); } var relids = new Array(); for (var i=0;i<rels.length;i++) { var rel = rels[i]; if (relids[rel.id]) { // handle input data duplication rel = options.deduplicator(rel, relids[rel.id]); } relids[rel.id] = rel; } var relsmap = {node: {}, way: {}, relation: {}}; for (var id in relids) { var rel = relids[id]; if (!_.isArray(rel.members)) { if (options.verbose) console.warn('Relation',rel.type+'/'+rel.id,'ignored because it has no members'); continue; // ignore relations without members (e.g. returned by an ids_only query) } for (var j=0;j<rel.members.length;j++) { var m_type = rel.members[j].type; var m_ref = rel.members[j].ref; if (typeof m_ref !== "number") { // de-namespace full geometry content m_ref = m_ref.replace("_fullGeom", ""); } if (!relsmap[m_type]) { if (options.verbose) console.warn('Relation',rel.type+'/'+rel.id,'member',m_type+'/'+m_ref,'ignored because it has an invalid type'); continue; } if (typeof relsmap[m_type][m_ref] === "undefined") relsmap[m_type][m_ref] = []; relsmap[m_type][m_ref].push({ "role" : rel.members[j].role, "rel" : rel.id, "reltags" : rel.tags, }); } } // construct geojson var geojson; var geojsonnodes = []; for (i=0;i<pois.length;i++) { if (typeof pois[i].lon == "undefined" || typeof pois[i].lat == "undefined") { if (options.verbose) console.warn('POI',pois[i].type+'/'+pois[i].id,'ignored because it lacks coordinates'); continue; // lon and lat are required for showing a point } var feature = { "type" : "Feature", "id" : pois[i].type+"/"+pois[i].id, "properties" : { "type" : pois[i].type, "id" : pois[i].id, "tags" : pois[i].tags || {}, "relations" : relsmap["node"][pois[i].id] || [], "meta": build_meta_information(pois[i]) }, "geometry" : { "type" : "Point", "coordinates" : [+pois[i].lon, +pois[i].lat], } }; if (pois[i].__is_center_placeholder) feature.properties["geometry"] = "center"; if (!featureCallback) geojsonnodes.push(feature); else featureCallback(feature); } var geojsonlines = []; var geojsonpolygons = []; // process multipolygons for (var i=0;i<rels.length;i++) { // todo: refactor such that this loops over relids instead of rels? if (relids[rels[i].id] !== rels[i]) { // skip relation because it's a deduplication artifact continue; } if ((typeof rels[i].tags != "undefined") && (rels[i].tags["type"] == "route" || rels[i].tags["type"] == "waterway")) { if (!_.isArray(rels[i].members)) { if (options.verbose) console.warn('Route',rels[i].type+'/'+rels[i].id,'ignored because it has no members'); continue; // ignore relations without members (e.g. returned by an ids_only query) } rels[i].members.forEach(function(m) { if (wayids[m.ref] && !has_interesting_tags(wayids[m.ref].tags)) wayids[m.ref].is_skippablerelationmember = true; }); feature = construct_multilinestring(rels[i]); if (feature === false) { if (options.verbose) console.warn('Route relation',rels[i].type+'/'+rels[i].id,'ignored because it has invalid geometry'); continue; // abort if feature could not be constructed } if (!featureCallback) geojsonpolygons.push(feature); else featureCallback(rewind(feature)); function construct_multilinestring(rel) { var is_tainted = false; // prepare route members var members; members = rel.members.filter(function(m) {return m.type === "way";}); members = members.map(function(m) { var way = wayids[m.ref]; if (way === undefined || way.nodes === undefined) { // check for missing ways if (options.verbose) console.warn('Route '+rel.type+'/'+rel.id, 'tainted by a missing or incomplete way', m.type+'/'+m.ref); is_tainted = true; return; } return { // TODO: this is slow! :( id: m.ref, role: m.role, way: way, nodes: way.nodes.filter(function(n) { if (n !== undefined) return true; is_tainted = true; if (options.verbose) console.warn('Route', rel.type+'/'+rel.id, 'tainted by a way', m.type+'/'+m.ref, 'with a missing node'); return false; }) }; }); members = _.compact(members); // construct connected linestrings var linestrings; linestrings = join(members); // sanitize mp-coordinates (remove empty clusters or rings, {lat,lon,...} to [lon,lat] var coords = []; coords = _.compact(linestrings.map(function(linestring) { return _.compact(linestring.map(function(node) { return [+node.lon,+node.lat]; })); })); if (coords.length == 0) { if (options.verbose) console.warn('Route', rel.type+'/'+rel.id, 'contains no coordinates'); return false; // ignore routes without coordinates } // mp parsed, now construct the geoJSON var feature = { "type" : "Feature", "id" : rel.type+"/"+rel.id, "properties" : { "type" : rel.type, "id" : rel.id, "tags" : rel.tags || {}, "relations" : relsmap[rel.type][rel.id] || [], "meta": build_meta_information(rel) }, "geometry" : { "type" : coords.length === 1 ? "LineString" : "MultiLineString", "coordinates" : coords.length === 1 ? coords[0] : coords, } } if (is_tainted) { if (options.verbose) console.warn('Route', rel.type+'/'+rel.id, 'is tainted'); feature.properties["tainted"] = true; } return feature; } } // end construct multilinestring for route relations if ((typeof rels[i].tags != "undefined") && (rels[i].tags["type"] == "multipolygon" || rels[i].tags["type"] == "boundary")) { if (!_.isArray(rels[i].members)) { if (options.verbose) console.warn('Multipolygon',rels[i].type+'/'+rels[i].id,'ignored because it has no members'); continue; // ignore relations without members (e.g. returned by an ids_only query) } var outer_count = 0; for (var j=0;j<rels[i].members.length;j++) if (rels[i].members[j].role == "outer") outer_count++; else if (options.verbose && rels[i].members[j].role != "inner") console.warn('Multipolygon',rels[i].type+'/'+rels[i].id,'member',rels[i].members[j].type+'/'+rels[i].members[j].ref,'ignored because it has an invalid role: "' + rels[i].members[j].role + '"'); rels[i].members.forEach(function(m) { if (wayids[m.ref]) { // this even works in the following corner case: // a multipolygon amenity=xxx with outer line tagged amenity=yyy // see https://github.com/tyrasd/osmtogeojson/issues/7 if (m.role==="outer" && !has_interesting_tags(wayids[m.ref].tags,rels[i].tags)) wayids[m.ref].is_skippablerelationmember = true; if (m.role==="inner" && !has_interesting_tags(wayids[m.ref].tags)) wayids[m.ref].is_skippablerelationmember = true; } }); if (outer_count == 0) { if (options.verbose) console.warn('Multipolygon relation',rels[i].type+'/'+rels[i].id,'ignored because it has no outer ways'); continue; // ignore multipolygons without outer ways } var simple_mp = false; if (outer_count == 1 && !has_interesting_tags(rels[i].tags, {"type":true})) simple_mp = true; var feature = null; if (!simple_mp) { feature = construct_multipolygon(rels[i], rels[i]); } else { // simple multipolygon var outer_way = rels[i].members.filter(function(m) {return m.role === "outer";})[0]; outer_way = wayids[outer_way.ref]; if (outer_way === undefined) { if (options.verbose) console.warn('Multipolygon relation',rels[i].type+'/'+rels[i].id,'ignored because outer way', outer_way.type+'/'+outer_way.ref,'is missing'); continue; // abort if outer way object is not present } outer_way.is_skippablerelationmember = true; feature = construct_multipolygon(outer_way, rels[i]); } if (feature === false) { if (options.verbose) console.warn('Multipolygon relation',rels[i].type+'/'+rels[i].id,'ignored because it has invalid geometry'); continue; // abort if feature could not be constructed } if (!featureCallback) geojsonpolygons.push(feature); else featureCallback(rewind(feature)); function construct_multipolygon(tag_object, rel) { var is_tainted = false; var mp_geometry = simple_mp ? 'way' : 'relation', mp_id = typeof tag_object.id === "number" ? tag_object.id : +(tag_object.id.replace("_fullGeom", "")); // prepare mp members var members; members = rel.members.filter(function(m) {return m.type === "way";}); members = members.map(function(m) { var way = wayids[m.ref]; if (way === undefined || way.nodes === undefined) { // check for missing ways if (options.verbose) console.warn('Multipolygon', mp_geometry+'/'+mp_id, 'tainted by a missing or incomplete way', m.type+'/'+m.ref); is_tainted = true; return; } return { // TODO: this is slow! :( id: m.ref, role: m.role || "outer", way: way, nodes: way.nodes.filter(function(n) { if (n !== undefined) return true; is_tainted = true; if (options.verbose) console.warn('Multipolygon', mp_geometry+'/'+mp_id, 'tainted by a way', m.type+'/'+m.ref, 'with a missing node'); return false; }) }; }); members = _.compact(members); // construct outer and inner rings var outers, inners; outers = join(members.filter(function(m) {return m.role==="outer";})); inners = join(members.filter(function(m) {return m.role==="inner";})); // sort rings var mp; function findOuter(inner) { var polygonIntersectsPolygon = function(outer, inner) { for (var i=0; i<inner.length; i++) if (pointInPolygon(inner[i], outer)) return true; return false; } var mapCoordinates = function(from) { return from.map(function(n) { return [+n.lat,+n.lon]; }); } // stolen from iD/geo.js, // based on https://github.com/substack/point-in-polygon, // ray-casting algorithm based on http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html var pointInPolygon = function(point, polygon) { var x = point[0], y = point[1], inside = false; for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { var xi = polygon[i][0], yi = polygon[i][1]; var xj = polygon[j][0], yj = polygon[j][1]; var intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); if (intersect) inside = !inside; } return inside; }; // stolen from iD/relation.js var o, outer; // todo: all this coordinate mapping makes this unneccesarily slow. // see the "todo: this is slow! :(" above. inner = mapCoordinates(inner); /*for (o = 0; o < outers.length; o++) { outer = mapCoordinates(outers[o]); if (polygonContainsPolygon(outer, inner)) return o; }*/ for (o = 0; o < outers.length; o++) { outer = mapCoordinates(outers[o]); if (polygonIntersectsPolygon(outer, inner)) return o; } } mp = outers.map(function(o) {return [o];}); for (var j=0; j<inners.length; j++) { var o = findOuter(inners[j]); if (o !== undefined) mp[o].push(inners[j]); else if (options.verbose) console.warn('Multipolygon', mp_geometry+'/'+mp_id, 'contains an inner ring with no containing outer'); // so, no outer ring for this inner ring is found. // We're going to ignore holes in empty space. ; } // sanitize mp-coordinates (remove empty clusters or rings, {lat,lon,...} to [lon,lat] var mp_coords = []; mp_coords = _.compact(mp.map(function(cluster) { var cl = _.compact(cluster.map(function(ring) { if (ring.length < 4) { // todo: is this correct: ring.length < 4 ? if (options.verbose) console.warn('Multipolygon', mp_geometry+'/'+mp_id, 'contains a ring with less than four nodes'); return; } return _.compact(ring.map(function(node) { return [+node.lon,+node.lat]; })); })); if (cl.length == 0) { if (options.verbose) console.warn('Multipolygon', mp_geometry+'/'+mp_id, 'contains an empty ring cluster'); return; } return cl; })); if (mp_coords.length == 0) { if (options.verbose) console.warn('Multipolygon', mp_geometry+'/'+mp_id, 'contains no coordinates'); return false; // ignore multipolygons without coordinates } var mp_type = "MultiPolygon"; if (mp_coords.length === 1) { mp_type = "Polygon"; mp_coords = mp_coords[0]; } // mp parsed, now construct the geoJSON var feature = { "type" : "Feature", "id" : tag_object.type+"/"+mp_id, "properties" : { "type" : tag_object.type, "id" : mp_id, "tags" : tag_object.tags || {}, "relations" : relsmap[tag_object.type][tag_object.id] || [], "meta": build_meta_information(tag_object) }, "geometry" : { "type" : mp_type, "coordinates" : mp_coords, } } if (is_tainted) { if (options.verbose) console.warn('Multipolygon', mp_geometry+'/'+mp_id, 'is tainted'); feature.properties["tainted"] = true; } return feature; } } } // process lines and polygons for (var i=0;i<ways.length;i++) { // todo: refactor such that this loops over wayids instead of ways? if (wayids[ways[i].id] !== ways[i]) { // skip way because it's a deduplication artifact continue; } if (!_.isArray(ways[i].nodes)) { if (options.verbose) console.warn('Way',ways[i].type+'/'+ways[i].id,'ignored because it has no nodes'); continue; // ignore ways without nodes (e.g. returned by an ids_only query) } if (ways[i].is_skippablerelationmember) continue; // ignore ways which are already rendered as (part of) a multipolygon if (typeof ways[i].id !== "number") { // remove full geometry namespace for output ways[i].id = +ways[i].id.replace("_fullGeom", ""); } ways[i].tainted = false; ways[i].hidden = false; var coords = new Array(); for (j=0;j<ways[i].nodes.length;j++) { if (typeof ways[i].nodes[j] == "object") coords.push([+ways[i].nodes[j].lon, +ways[i].nodes[j].lat]); else { if (options.verbose) console.warn('Way',ways[i].type+'/'+ways[i].id,'is tainted by an invalid node'); ways[i].tainted = true; } } if (coords.length <= 1) { // invalid way geometry if (options.verbose) console.warn('Way',ways[i].type+'/'+ways[i].id,'ignored because it contains too few nodes'); continue; } var way_type = "LineString"; // default if (typeof ways[i].nodes[0] != "undefined" && typeof ways[i].nodes[ways[i].nodes.length-1] != "undefined" && // way has its start/end nodes loaded ways[i].nodes[0].id === ways[i].nodes[ways[i].nodes.length-1].id && // ... and forms a closed ring ( typeof ways[i].tags != "undefined" && // ... and has tags _isPolygonFeature(ways[i].tags) // ... and tags say it is a polygon || // or is a placeholder for a bounds geometry ways[i].__is_bounds_placeholder ) ) { way_type = "Polygon"; coords = [coords]; } var feature = { "type" : "Feature", "id" : ways[i].type+"/"+ways[i].id, "properties" : { "type" : ways[i].type, "id" : ways[i].id, "tags" : ways[i].tags || {}, "relations" : relsmap["way"][ways[i].id] || [], "meta": build_meta_information(ways[i]) }, "geometry" : { "type" : way_type, "coordinates" : coords, } } if (ways[i].tainted) { if (options.verbose) console.warn('Way',ways[i].type+'/'+ways[i].id,'is tainted'); feature.properties["tainted"] = true; } if (ways[i].__is_bounds_placeholder) feature.properties["geometry"] = "bounds"; if (!featureCallback) { if (way_type == "LineString") geojsonlines.push(feature); else geojsonpolygons.push(feature); } else { featureCallback(rewind(feature)); } } if (featureCallback) return true; geojson = { "type": "FeatureCollection", "features": [] }; geojson.features = geojson.features.concat(geojsonpolygons); geojson.features = geojson.features.concat(geojsonlines); geojson.features = geojson.features.concat(geojsonnodes); // optionally, flatten properties if (options.flatProperties) { geojson.features.forEach(function(f) { f.properties = _.merge( f.properties.meta, f.properties.tags, {id: f.properties.type+"/"+f.properties.id} ); }); } // fix polygon winding geojson = rewind(geojson); return geojson; } function _isPolygonFeature( tags ) { var polygonFeatures = options.polygonFeatures; if (typeof polygonFeatures === "function") return polygonFeatures(tags); // explicitely tagged non-areas if ( tags['area'] === 'no' ) return false; // assuming that a typical OSM way has in average less tags than // the polygonFeatures list, this way around should be faster for ( var key in tags ) { var val = tags[key]; var pfk = polygonFeatures[key]; // continue with next if tag is unknown or not "categorizing" if ( typeof pfk === 'undefined' ) continue; // continue with next if tag is explicitely un-set ("building=no") if ( val === 'no' ) continue; // check polygon features for: general acceptance, included or excluded values if ( pfk === true ) return true; if ( pfk.included_values && pfk.included_values[val] === true ) return true; if ( pfk.excluded_values && pfk.excluded_values[val] !== true ) return true; } // if no tags matched, this ain't no area. return false; } }; // helper that joins adjacent osm ways into linestrings or linear rings function join(ways) { var _first = function(arr) {return arr[0]}; var _last = function(arr) {return arr[arr.length-1]}; var _fitTogether = function(n1, n2) { return n1 !== undefined && n2 !== undefined && n1.id === n2.id; } // stolen from iD/relation.js var joined = [], current, first, last, i, how, what; while (ways.length) { current = ways.pop().nodes.slice(); joined.push(current); while (ways.length && !_fitTogether(_first(current), _last(current))) { first = _first(current); last = _last(current); for (i = 0; i < ways.length; i++) { what = ways[i].nodes; if (_fitTogether(last, _first(what))) { how = current.push; what = what.slice(1); break; } else if (_fitTogether(last, _last(what))) { how = current.push; what = what.slice(0, -1).reverse(); break; } else if (_fitTogether(first, _last(what))) { how = current.unshift; what = what.slice(0, -1); break; } else if (_fitTogether(first, _first(what))) { how = current.unshift; what = what.slice(1).reverse(); break; } else { what = how = null; } } if (!what) break; // Invalid geometry (dangling way, unclosed ring) ways.splice(i, 1); how.apply(current, what); } } return joined; } // for backwards compatibility osmtogeojson.toGeojson = osmtogeojson; module.exports = osmtogeojson;