var osm_geojson = {}; osm_geojson.geojson2osm = function(geo, changeset, osmChange) { function togeojson(geo, properties) { var nodes = '', ways = '', relations = ''; properties = properties || {}; switch (geo.type) { case 'Point': var coord = roundCoords([geo.coordinates]); nodes += ''; nodes += propertiesToTags(properties); nodes += ''; count--; break; case 'MultiPoint': break; case 'LineString': break; case 'MultiLineString': break; case 'Polygon': append(polygon(geo, properties)); break; case 'MultiPolygon': relations += ''; properties.type = 'multipolygon'; count--; for (var i = 0; i < geo.coordinates.length; i++){ poly = polygon({ 'coordinates': geo.coordinates[i] }, undefined, true); nodes += poly.nodes; ways += poly.ways; relations += poly.relations; } relations += propertiesToTags(properties); relations += ''; break; } function append(obj) { nodes += obj.nodes; ways += obj.ways; relations += obj.relations; } osm = '' + nodes + ways + relations + ''; if (osmChange) { osm = '' + nodes + ways + relations + ''; } return { nodes: nodes, ways: ways, relations: relations, osm: osm }; } function polygon(geo, properties, multipolygon) { var nodes = '', ways = '', relations = '', role = ''; properties = properties || {}; multipolygon = multipolygon || false; var coords = []; if (geo.coordinates.length > 1) { // polygon with holes -> multipolygon if (!multipolygon) relations += ''; count--; properties.type = 'multipolygon'; for (var i = 0; i < geo.coordinates.length; i++) { role = ((i === 0) ? 'outer' : 'inner'); relations += ''; ways += ''; count--; for (var a = 0; a < geo.coordinates[i].length-1; a++) { coords.push([geo.coordinates[i][a][1], geo.coordinates[i][a][0]]); } coords = createNodes(coords, true); nodes += coords.nodes; ways += coords.nds; ways += ''; coords = []; } if (!multipolygon) { relations += propertiesToTags(properties); relations += ''; } } else { // polygon -> way ways += ''; if (multipolygon) relations += ''; count--; for (var j = 0; j < geo.coordinates[0].length-1; j++) { coords.push([geo.coordinates[0][j][1], geo.coordinates[0][j][0]]); } coords = createNodes(coords, true); nodes += coords.nodes; ways += coords.nds; ways += propertiesToTags(properties); ways += ''; } return { nodes: nodes, ways: ways, relations: relations }; } function propertiesToTags(properties) { var tags = ''; for (var tag in properties) { if (properties[tag] !== null) { tags += ''; } } return tags; } function roundCoords(coords){ for (var a = 0; a < coords.length; a++) { coords[a][0] = Math.round(coords[a][0] * 1000000) / 1000000; coords[a][1] = Math.round(coords[a][1] * 1000000) / 1000000; } return coords; } function createNodes(coords, repeatLastND) { var nds = '', nodes = '', length = coords.length; repeatLastND = repeatLastND || false; // for polygons coords = roundCoords(coords); for (var a = 0; a < length; a++) { if (repeatLastND && a === 0) repeatLastND = count; nds += ''; nodes += ''; if (repeatLastND && a === length-1) nds += ''; count--; } return {'nds': nds, 'nodes': nodes}; } if (typeof geo === 'string') geo = JSON.parse(geo); var obj, count = -1; changeset = changeset || false; switch (geo.type) { case 'FeatureCollection': var temp = { nodes: '', ways: '', relations: '' }; obj = []; for (var i = 0; i < geo.features.length; i++){ obj.push(togeojson(geo.features[i].geometry, geo.features[i].properties)); } temp.osm = ''; if (osmChange) temp.osm = ''; for (var n = 0; n < obj.length; n++) { temp.nodes += obj[n].nodes; temp.ways += obj[n].ways; temp.relations += obj[n].relations; } temp.osm += temp.nodes + temp.ways + temp.relations; if (osmChange) { temp.osm += ''; } else { temp.osm += ''; } obj = temp.osm; break; case 'GeometryCollection': obj = []; for (var j = 0; j < geo.geometries.length; j++){ obj.push(togeojson(geo.geometries[j])); } break; case 'Feature': obj = togeojson(geo.geometry, geo.properties); obj = obj.osm; break; case 'Point': case 'MultiPoint': case 'LineString': case 'MultiLineString': case 'Polygon': case 'MultiPolygon': obj = togeojson(geo); obj = obj.osm; break; default: if (console) console.log('Invalid GeoJSON object: GeoJSON object must be one of \"Point\", \"LineString\", ' + '\"Polygon\", \"MultiPolygon\", \"Feature\", \"FeatureCollection\" or \"GeometryCollection\".'); } return obj; }; osm_geojson.osm2geojson = function(osm, metaProperties) { var xml = parse(osm), usedCoords = {}, nodeCache = cacheNodes(), wayCache = cacheWays(); return Bounds({ type : 'FeatureCollection', features : [] .concat(Ways(wayCache)) .concat(Ways(Relations)) .concat(Points(nodeCache)) }, xml); function parse(xml) { if (typeof xml !== 'string') return xml; return (new DOMParser()).parseFromString(xml, 'text/xml'); } function Bounds(geo, xml) { var bounds = getBy(xml, 'bounds'); if (!bounds.length) return geo; geo.bbox = [ attrf(bounds[0], 'minlon'), attrf(bounds[0], 'minlat'), attrf(bounds[0], 'maxlon'), attrf(bounds[0], 'maxlat') ]; return geo; } function setProperties(element) { if (!element) return {}; var props = {}, tags = element.getElementsByTagName('tag'), tags_length = tags.length; for (var t = 0; t < tags_length; t++) { props[attr(tags[t], 'k')] = isNumber(attr(tags[t], 'v')) ? parseFloat(attr(tags[t], 'v')) : attr(tags[t], 'v'); } if (metaProperties) { setIf(element, 'id', props, 'osm_id'); setIf(element, 'user', props, 'osm_lastEditor'); setIf(element, 'version', props, 'osm_version', true); setIf(element, 'changeset', props, 'osm_lastChangeset', true); setIf(element, 'timestamp', props, 'osm_lastEdited'); } return sortObject(props); } function getFeature(element, type, coordinates) { return { geometry: { type: type, coordinates: coordinates || [] }, type: 'Feature', properties: setProperties(element) }; } function cacheNodes() { var nodes = getBy(xml, 'node'), coords = {}; for (var n = 0; n < nodes.length; n++) { coords[attr(nodes[n], 'id')] = nodes[n]; } return coords; } function Points(nodeCache) { var points = nodeCache, features = []; for (var node in nodeCache) { var tags = getBy(nodeCache[node], 'tag'); if (!usedCoords[node] || tags.length) features.push(getFeature(nodeCache[node], 'Point', lonLat(nodeCache[node]))); } return features; } function cacheWays() { var ways = getBy(xml, 'way'), out = {}; for (var w = 0; w < ways.length; w++) { var feature = {}, nds = getBy(ways[w], 'nd'); if (attr(nds[0], 'ref') === attr(nds[nds.length - 1], 'ref')) { feature = getFeature(ways[w], 'Polygon', [[]]); } else { feature = getFeature(ways[w], 'LineString'); } for (var n = 0; n < nds.length; n++) { var node = nodeCache[attr(nds[n], 'ref')]; if (node) { var cords = lonLat(node); usedCoords[attr(nds[n], 'ref')] = true; if (feature.geometry.type === 'Polygon') { feature.geometry.coordinates[0].push(cords); } else { feature.geometry.coordinates.push(cords); } } } out[attr(ways[w], 'id')] = feature; } return out; } function Relations() { var relations = getBy(xml, 'relation'), relations_length = relations.length, features = []; for (var r = 0; r < relations_length; r++) { var feature = getFeature(relations[r], 'MultiPolygon'); if (feature.properties.type == 'multipolygon') { var members = getBy(relations[r], 'member'); // osm doesn't keep roles in order, so we do this twice for (var m = 0; m < members.length; m++) { if (attr(members[m], 'role') == 'outer') assignWay(members[m], feature); } for (var n = 0; n < members.length; n++) { if (attr(members[n], 'role') == 'inner') assignWay(members[n], feature); } delete feature.properties.type; } else { // http://taginfo.openstreetmap.us/relations } if (feature.geometry.coordinates.length) features.push(feature); } return features; function assignWay(member, feature) { var ref = attr(member, 'ref'), way = wayCache[ref]; if (way && way.geometry.type == 'Polygon') { if (way && attr(member, 'role') == 'outer') { feature.geometry.coordinates.push(way.geometry.coordinates); if (way.properties) { // exterior polygon properties can move to the multipolygon // but multipolygon (relation) tags take precedence for (var prop in way.properties) { if (!feature.properties[prop]) { feature.properties[prop] = prop; } } } } else if (way && attr(member, 'role') == 'inner'){ if (feature.geometry.coordinates.length > 1) { // do a point in polygon against each outer // this determines which outer the inner goes with for (var a = 0; a < feature.geometry.coordinates.length; a++) { if (pointInPolygon( way.geometry.coordinates[0][0], feature.geometry.coordinates[a][0])) { feature.geometry.coordinates[a].push(way.geometry.coordinates[0]); break; } } } else { if (feature.geometry.coordinates.length) { feature.geometry.coordinates[0].push(way.geometry.coordinates[0]); } } } } wayCache[ref] = false; } } function Ways(wayCache) { var features = []; for (var w in wayCache) if (wayCache[w]) features.push(wayCache[w]); return features; } // https://github.com/substack/point-in-polygon/blob/master/index.js function pointInPolygon(point, vs) { var x = point[0], y = point[1]; var inside = false; for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) { var xi = vs[i][0], yi = vs[i][1], xj = vs[j][0], yj = vs[j][1], intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); if (intersect) inside = !inside; } return inside; } // http://stackoverflow.com/a/1359808 function sortObject(o) { var sorted = {}, key, a = []; for (key in o) if (o.hasOwnProperty(key)) a.push(key); a.sort(); for (key = 0; key < a.length; key++) sorted[a[key]] = o[a[key]]; return sorted; } // http://stackoverflow.com/a/1830844 function isNumber(n) { return !isNaN(parseFloat(n)) && isFinite(n); } function attr(x, y) { return x.getAttribute(y); } function attrf(x, y) { return parseFloat(x.getAttribute(y)); } function getBy(x, y) { return x.getElementsByTagName(y); } function lonLat(elem) { return [attrf(elem, 'lon'), attrf(elem, 'lat')]; } function setIf(x, y, o, name, f) { if (attr(x, y)) o[name] = f ? parseFloat(attr(x, y)) : attr(x, y); } }; if (typeof module !== 'undefined') module.exports = osm_geojson;