Skip to content

Commit

Permalink
create (multi)line features for route relations. closes #1 πŸŽ‰πŸŽ‰
Browse files Browse the repository at this point in the history
  • Loading branch information
tyrasd committed Feb 19, 2017
1 parent bd508f1 commit 39898b5
Show file tree
Hide file tree
Showing 2 changed files with 243 additions and 47 deletions.
181 changes: 134 additions & 47 deletions index.js
Expand Up @@ -604,6 +604,92 @@ osmtogeojson = function( data, options, featureCallback ) {
// skip relation because it's a deduplication artifact
continue;
}
if ((typeof rels[i].tags != "undefined") &&
(rels[i].tags["type"] == "route")) {
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) { // check for missing ways
if (options.verbose) console.warn('Route '+rel.type+'/'+rel.id, 'tainted by a missing 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)) {
Expand All @@ -622,9 +708,9 @@ osmtogeojson = function( data, options, featureCallback ) {
// 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_multipolygon_outline = true;
wayids[m.ref].is_skippablerelationmember = true;
if (m.role==="inner" && !has_interesting_tags(wayids[m.ref].tags))
wayids[m.ref].is_multipolygon_outline = true;
wayids[m.ref].is_skippablerelationmember = true;
}
});
if (outer_count == 0) {
Expand All @@ -645,7 +731,7 @@ osmtogeojson = function( data, options, featureCallback ) {
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_multipolygon_outline = true;
outer_way.is_skippablerelationmember = true;
feature = construct_multipolygon(outer_way, rels[i]);
}
if (feature === false) {
Expand All @@ -656,6 +742,7 @@ osmtogeojson = function( data, options, 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',
Expand Down Expand Up @@ -686,49 +773,6 @@ osmtogeojson = function( data, options, featureCallback ) {
members = _.compact(members);
// construct outer and inner rings
var outers, inners;
function join(ways) {
var _first = function(arr) {return arr[0]};
var _last = function(arr) {return arr[arr.length-1]};
// 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 && _first(current) !== _last(current)) {
first = _first(current);
last = _last(current);
for (i = 0; i < ways.length; i++) {
what = ways[i].nodes;
if (last === _first(what)) {
how = current.push;
what = what.slice(1);
break;
} else if (last === _last(what)) {
how = current.push;
what = what.slice(0, -1).reverse();
break;
} else if (first == _last(what)) {
how = current.unshift;
what = what.slice(0, -1);
break;
} else if (first == _first(what)) {
how = current.unshift;
what = what.slice(1).reverse();
break;
} else {
what = how = null;
}
}
if (!what) {
if (options.verbose) console.warn('Multipolygon', mp_geometry+'/'+mp_id, 'contains unclosed ring geometry');
break; // Invalid geometry (dangling way, unclosed ring)
}
ways.splice(i, 1);
how.apply(current, what);
}
}
return joined;
}
outers = join(members.filter(function(m) {return m.role==="outer";}));
inners = join(members.filter(function(m) {return m.role==="inner";}));
// sort rings
Expand Down Expand Up @@ -849,7 +893,7 @@ osmtogeojson = function( data, options, featureCallback ) {
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_multipolygon_outline)
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
Expand Down Expand Up @@ -969,6 +1013,49 @@ osmtogeojson = function( data, options, featureCallback ) {
}
};

// 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]};
// 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 && _first(current) !== _last(current)) {
first = _first(current);
last = _last(current);
for (i = 0; i < ways.length; i++) {
what = ways[i].nodes;
if (last === _first(what)) {
how = current.push;
what = what.slice(1);
break;
} else if (last === _last(what)) {
how = current.push;
what = what.slice(0, -1).reverse();
break;
} else if (first == _last(what)) {
how = current.unshift;
what = what.slice(0, -1);
break;
} else if (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;

Expand Down
109 changes: 109 additions & 0 deletions test/osm.test.js
Expand Up @@ -696,6 +696,115 @@ describe("osm (json)", function () {
result = osmtogeojson(json, {flatProperties: false});
expect(result).to.eql(geojson);
});
it("route relation", function () {
var json, geojson;
// valid multipolygon
json = {
elements: [
{
type: "relation",
id: 1,
tags: {"type":"route"},
members: [
{
type: "way",
ref: 2,
role: "forward"
},
{
type: "way",
ref: 3,
role: "backward"
},
{
type: "way",
ref: 4,
role: "forward"
}
]
},
{
type: "way",
id: 2,
nodes: [4,5]
},
{
type: "way",
id: 3,
nodes: [5,6]
},
{
type: "way",
id: 4,
nodes: [7,8]
},
{
type: "node",
id: 4,
lat: -1.0,
lon: -1.0
},
{
type: "node",
id: 5,
lat: 0.0,
lon: 0.0
},
{
type: "node",
id: 6,
lat: 1.0,
lon: 1.0
},
{
type: "node",
id: 7,
lat: 10.0,
lon: 10.0
},
{
type: "node",
id: 8,
lat: 20.0,
lon: 20.0
}
]
};
geojson = {
type: "FeatureCollection",
features: [
{
type: "Feature",
id: "relation/1",
properties: {
type: "relation",
id: 1,
tags: {"type":"route"},
relations: [],
meta: {}
},
geometry: {
type: "MultiLineString",
coordinates: [[
[-1.0,-1.0],
[ 0.0, 0.0],
[ 1.0, 1.0]
],[
[10.0,10.0],
[20.0,20.0]
]]
}
}
]
};
var result = osmtogeojson(json, {flatProperties: false});
function _sorter(a,b) {
return a.length - b.length;
}
result.features[0].geometry.coordinates.sort(_sorter);
geojson.features[0].geometry.coordinates.sort(_sorter);
expect(result).to.eql(geojson);
});
// tags & pois
it("tags: ways and nodes / pois", function () {
var json, geojson;
Expand Down

0 comments on commit 39898b5

Please sign in to comment.