var _ = require("./lodash.custom.js"); var rewind = require("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; } }); var osmtogeojson = {}; osmtogeojson = function( data, options ) { options = _.merge( { verbose: false, flatProperties: false, 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, }, 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, __is_uninteresting: true } 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, __is_uninteresting: true } 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 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, __is_uninteresting: true } 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, __is_uninteresting: true } 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(); for (var i=0;i 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; j0.1.0*/); 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; } }; // for backwards compatibility osmtogeojson.toGeojson = osmtogeojson; module.exports = osmtogeojson;