-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathadiff-to-geojson.js
155 lines (130 loc) · 5.51 KB
/
adiff-to-geojson.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import bbox from "@turf/bbox";
import bboxPolygon from "@turf/bbox-polygon";
import { isArea } from "id-area-keys";
import deepEqual from "deep-equal";
const EPSILON = 0.00005; // in degrees (about 5m at the equator)
function deg2rad(degrees) {
return degrees * (Math.PI / 180);
}
function coordsAreEqual(a, b) {
return a[0] === b[0] && a[1] === b[1];
}
function isClosed(coordsArray) {
return coordsAreEqual(coordsArray[0], coordsArray[coordsArray.length - 1]);
}
function elementToGeoJSON(element) {
let properties = { ...element };
let geometry = null;
switch (element.type) {
case "node":
{
let { lon, lat } = element;
// FIXME: bug-compatibility with real-changesets-parser
let coordinates = lon !== undefined && lat !== undefined ? [+lon, +lat] : [];
geometry = { type: "Point", coordinates };
}
break;
case "way":
{
let { nodes } = element;
let coordinates = nodes
.filter((node) => node.lon !== undefined && node.lat !== undefined)
.map(({ lon, lat }) => [+lon, +lat]);
if (coordinates.length > 0 && isClosed(coordinates) && isArea(element.tags)) {
geometry = { type: "Polygon", coordinates: [coordinates] };
} else {
geometry = { type: "LineString", coordinates };
}
}
break;
case "relation":
{
let members = element.members || [];
let features = members
.map(({ ref, role, ...member }) => elementToGeoJSON({ id: ref, ...member }))
.map(f => { f.properties.action = "noop"; f.properties.side = undefined; return f; });
properties.relations = features;
// NOTE: members might not all have geometries, because child relations' members aren't
// included in adiffs from adiffs.osmcha.org right now. bbox() will return an infinite
// bounding box if these geometry-less members are included, so filter them out first.
let bounds = bbox({ type: "FeatureCollection", features: features.filter((f) => f.geometry) });
// Make sure the resulting bbox is finite before attaching it as a geometry for this
// relation (it can be infinite if there were no features left after filtering above)
if (bounds.every(v => Number.isFinite(v))) {
// Check if the bbox is degenerate (has zero area). This happens when creating a bbox
// of a single point. To make sure these bboxes can be rendered on the map, we'll
// bump their dimensions by a small amount, making their size nonzero.
if (bounds[0] === bounds[2] || bounds[1] === bounds[3]) {
let phi = deg2rad(bounds[1]); // latitude in radians
let cosphi = Math.cos(phi); // Mercator y-scale factor
// Scaling the y-axis epsilon by cosphi above ensures the resulting box is square
bounds[0] -= EPSILON;
bounds[1] -= EPSILON * cosphi;
bounds[2] += EPSILON;
bounds[3] += EPSILON * cosphi;
}
geometry = bboxPolygon(bounds).geometry;
}
}
break;
}
// erase unwanted properties
delete properties.lon;
delete properties.lat;
delete properties.nodes;
delete properties.members;
return { type: "Feature", properties, geometry };
}
function adiffToGeoJSON({ actions }) {
// let features = [];
// this maps string IDs like "way/123456" to GeoJSON features.
// each value is an array because in the case of modify actions
// there are two versions of the feature (old and new)
let features = new Map();
for (let idx in actions) {
let action = actions[idx];
let oldFeature = null;
let newFeature = null;
let id = action.new.type + "/" + action.new.id;
if (action.type === "create" || action.type === "modify") {
newFeature = elementToGeoJSON(action.new);
newFeature.properties.action = action.type;
newFeature.properties.side = "new";
newFeature.properties.num_tags = Object.keys(newFeature.properties.tags).length;
}
if (action.type === "modify" || action.type === "delete") {
oldFeature = elementToGeoJSON(action.old);
oldFeature.properties.action = action.type;
oldFeature.properties.side = "old";
oldFeature.properties.num_tags = Object.keys(oldFeature.properties.tags).length;
}
if (action.type === "modify") {
if (action.new.type === "relation" && action.new.version === action.old.version) {
oldFeature.properties.action = "noop";
newFeature.properties.action = "noop";
};
let tags_changed = !deepEqual(action.old.tags, action.new.tags, { strict: true });
oldFeature.properties.tags_changed = tags_changed;
newFeature.properties.tags_changed = tags_changed;
}
// if (oldFeature) features.push(oldFeature);
// if (newFeature) features.push(newFeature);
features.set(id, [oldFeature, newFeature].filter(Boolean));
// HACK: (WIP, not working) need to add relation members, but
// omit them if they're already in the diff as standalone
// added/modified/deleted elements
if (newFeature && newFeature.properties.relations) {
for (let related of newFeature.properties.relations) {
let id = related.properties.type + "/" + related.properties.id;
if (!features.has(id)) {
features.set(id, [related]);
}
}
}
}
features = [...features.values()]
.map((fs, idx) => fs.map(f => { f.id = idx; return f; }))
.flat();
return { type: "FeatureCollection", features };
}
export default adiffToGeoJSON;