1 | import React from 'react';
|
2 | import moment from 'moment';
|
3 | import { getBounds } from './helpers';
|
4 | import { cmap } from './render';
|
5 |
|
6 | export function getFeatures(features) {
|
7 | var keys = Object.keys(features);
|
8 | return keys.map(item => features[item]);
|
9 | }
|
10 |
|
11 | export function processFeatures(features) {
|
12 | const finalReport = new Map();
|
13 | const analyzedFeatures = features.map(feature =>
|
14 | analyzeFeature(feature[0], feature[1])
|
15 | );
|
16 | const keys = ['addedTags', 'changedValues', 'deletedTags'];
|
17 | analyzedFeatures.map(item =>
|
18 | keys.map(key =>
|
19 | item.get(key).forEach(tag => {
|
20 | if (finalReport.get(tag)) {
|
21 | finalReport.set(tag, finalReport.get(tag).concat([item.get('id')]));
|
22 | } else {
|
23 | finalReport.set(tag, [item.get('id')]);
|
24 | }
|
25 | })
|
26 | )
|
27 | );
|
28 | return finalReport;
|
29 | }
|
30 |
|
31 | export function analyzeFeature(newVersion, oldVersion) {
|
32 | var oldVersionKeys = Object.keys(oldVersion.properties.tags);
|
33 | var newVersionKeys = Object.keys(newVersion.properties.tags);
|
34 | var addedTags = newVersionKeys.filter(
|
35 | tag => oldVersionKeys.indexOf(tag) === -1
|
36 | );
|
37 | var deletedTags = oldVersionKeys.filter(
|
38 | tag => newVersionKeys.indexOf(tag) === -1
|
39 | );
|
40 | var changedValues = newVersionKeys
|
41 | .filter(
|
42 | tag => addedTags.indexOf(tag) === -1 && deletedTags.indexOf(tag) === -1
|
43 | )
|
44 | .filter(
|
45 | tag => newVersion.properties.tags[tag] !== oldVersion.properties.tags[tag]
|
46 | );
|
47 | var result = new Map();
|
48 | result
|
49 | .set('id', newVersion.properties.id)
|
50 | .set('addedTags', addedTags.map(tag => `Added tag ${tag}`))
|
51 | .set('deletedTags', deletedTags.map(tag => `Deleted tag ${tag}`))
|
52 | .set(
|
53 | 'changedValues',
|
54 | changedValues.map(tag => `Changed value of tag ${tag}`)
|
55 | );
|
56 | return result;
|
57 | }
|
58 |
|
59 | export function selectFeature(id: number) {
|
60 | if (!id || !cmap) return;
|
61 | cmap.emit('selectFeature', 'node|way', id);
|
62 | }
|
63 |
|
64 | function FeatureListItem(props) {
|
65 | return (
|
66 | <li>
|
67 | <span className="cmap-pointer " onClick={() => selectFeature(props.id)}>
|
68 | {props.id}
|
69 | </span>
|
70 | </li>
|
71 | );
|
72 | }
|
73 |
|
74 | export class ChangeItem extends React.PureComponent {
|
75 | constructor(props) {
|
76 | super(props);
|
77 | this.state = {
|
78 | opened: false
|
79 | };
|
80 | this.tag = props.change[0];
|
81 | this.value = props.change[1];
|
82 | this.handleChange = this.handleChange.bind(this);
|
83 | }
|
84 | handleChange() {
|
85 | this.setState({ opened: !this.state.opened });
|
86 | }
|
87 | render() {
|
88 | return (
|
89 | <div>
|
90 | <h7
|
91 | className="cmap-sub-heading cmap-pointer"
|
92 | onClick={this.handleChange}
|
93 | >
|
94 | {this.state.opened ? '▼' : '▶'}
|
95 | {this.tag}
|
96 | </h7>
|
97 | <ul
|
98 | className="cmap-vlist"
|
99 | style={{
|
100 | display: this.state.opened ? 'block' : 'none'
|
101 | }}
|
102 | >
|
103 | {this.value.map((id, k) => <FeatureListItem id={id} key={k} />)}
|
104 | </ul>
|
105 | </div>
|
106 | );
|
107 | }
|
108 | }
|
109 |
|
110 | export class Sidebar extends React.PureComponent {
|
111 | constructor(props) {
|
112 | super(props);
|
113 | this.state = {
|
114 | actions: true,
|
115 | type: false,
|
116 | changes: false,
|
117 | mapStyle: false,
|
118 | user: false
|
119 | };
|
120 | this.changeReport = [];
|
121 | this.changedFeatures = processFeatures(
|
122 | getFeatures(this.props.result.featureMap).filter(
|
123 | item => item.length === 2 && item[0].properties.action === 'modify'
|
124 | )
|
125 | ).forEach((featureIDs, tag) => this.changeReport.push([tag, featureIDs]));
|
126 |
|
127 | this.toggleUser = this.toggleUser.bind(this);
|
128 | this.toggleActions = this.toggleActions.bind(this);
|
129 | this.toggleType = this.toggleType.bind(this);
|
130 | this.toggleChanges = this.toggleChanges.bind(this);
|
131 | this.toggleMapStyle = this.toggleMapStyle.bind(this);
|
132 | }
|
133 | toggleUser() {
|
134 | this.setState({
|
135 | user: !this.state.user
|
136 | });
|
137 | }
|
138 | toggleActions() {
|
139 | this.setState({
|
140 | actions: !this.state.actions
|
141 | });
|
142 | }
|
143 | toggleType() {
|
144 | this.setState({
|
145 | type: !this.state.type
|
146 | });
|
147 | }
|
148 | toggleChanges() {
|
149 | this.setState({
|
150 | changes: !this.state.changes
|
151 | });
|
152 | }
|
153 | toggleMapStyle() {
|
154 | this.setState({
|
155 | mapStyle: !this.state.mapStyle
|
156 | });
|
157 | }
|
158 | render() {
|
159 | const result = this.props.result;
|
160 | const changesetId = this.props.changesetId;
|
161 | const filterLayers = this.props.filterLayers;
|
162 | var date = new Date(
|
163 | result.changeset.to ? result.changeset.to : result.changeset.from
|
164 | );
|
165 |
|
166 | var bbox = result.changeset.bbox;
|
167 | var bounds = getBounds(bbox);
|
168 | var center = bounds.getCenter();
|
169 | var userName = result.changeset.user;
|
170 | var userId = result.changeset.uid;
|
171 | return (
|
172 | <div className="cmap-sidebar">
|
173 | <section className="cmap-changeset-section cmap-fill-light cmap-pt3">
|
174 | <h6 className="cmap-heading">
|
175 | Changeset:
|
176 | <em className="cmap-changeset-id">{changesetId}</em>
|
177 | <small className="cmap-time" title={date}>
|
178 | ({moment(date).fromNow()})
|
179 | </small>
|
180 | </h6>
|
181 | <ul className="cmap-hlist">
|
182 | <li>
|
183 | <a
|
184 | target="_blank"
|
185 | className="cmap-hlist-item cmap-noselect cmap-pointer cmap-c-link-osm"
|
186 | href={'https://openstreetmap.org/changeset/' + changesetId}
|
187 | >
|
188 | OSM
|
189 | </a>
|
190 | </li>
|
191 | <li>
|
192 | <a
|
193 | target="_blank"
|
194 | className="cmap-hlist-item cmap-noselect cmap-pointer cmap-c-link-osmcha"
|
195 | href={'https://osmcha.org/changesets/' + changesetId + '/'}
|
196 | >
|
197 | OSMCha
|
198 | </a>
|
199 | </li>
|
200 | <li>
|
201 | <a
|
202 | target="_blank"
|
203 | className="cmap-hlist-item cmap-noselect cmap-pointer cmap-c-link-achavi"
|
204 | href={
|
205 | 'https://overpass-api.de/achavi/?changeset=' + changesetId
|
206 | }
|
207 | >
|
208 | Achavi
|
209 | </a>
|
210 | </li>
|
211 | <li>
|
212 | <a
|
213 | target="_blank"
|
214 | className="cmap-hlist-item cmap-noselect cmap-pointer cmap-c-link-osmhv"
|
215 | href={
|
216 | 'http://osmhv.openstreetmap.de/changeset.jsp?id=' +
|
217 | changesetId
|
218 | }
|
219 | >
|
220 | OSM HV
|
221 | </a>
|
222 | </li>
|
223 | <li>
|
224 | <a
|
225 | target="_blank"
|
226 | className="cmap-hlist-item cmap-noselect cmap-pointer cmap-c-link-josm"
|
227 | href={
|
228 | 'http://127.0.0.1:8111/import?url=http://www.openstreetmap.org/api/0.6/changeset/' +
|
229 | changesetId +
|
230 | '/download'
|
231 | }
|
232 | >
|
233 | JOSM
|
234 | </a>
|
235 | </li>
|
236 | <li>
|
237 | <a
|
238 | target="_blank"
|
239 | className="cmap-hlist-item cmap-noselect cmap-pointer cmap-c-link-id"
|
240 | href={
|
241 | 'http://preview.ideditor.com/release#map=15/' +
|
242 | center.lat +
|
243 | '/' +
|
244 | center.lng
|
245 | }
|
246 | >
|
247 | iD
|
248 | </a>
|
249 | </li>
|
250 | </ul>
|
251 | </section>
|
252 | <section className="cmap-user-section cmap-fill-light cmap-pb3">
|
253 | <h6 className="cmap-heading" onClick={this.toggleUser}>
|
254 | {this.state.user ? '▼' : '▶'}
|
255 | User: <em className="cmap-user-id">{userName}</em>
|
256 | </h6>
|
257 |
|
258 | <ul
|
259 | className="cmap-hlist"
|
260 | style={{
|
261 | display: this.state.user ? 'block' : 'none'
|
262 | }}
|
263 | >
|
264 | <li>
|
265 | <a
|
266 | target="_blank"
|
267 | className="cmap-hlist-item cmap-noselect cmap-pointer cmap-u-link-osm"
|
268 | href={'https://openstreetmap.org/user/' + userName}
|
269 | >
|
270 | OSM
|
271 | </a>
|
272 | </li>
|
273 | <li>
|
274 | <a
|
275 | target="_blank"
|
276 | className="cmap-hlist-item cmap-noselect cmap-pointer cmap-u-link-hdyc"
|
277 | href={'http://hdyc.neis-one.org/?' + userName}
|
278 | >
|
279 | HDYC
|
280 | </a>
|
281 | </li>
|
282 | <li>
|
283 | <a
|
284 | target="_blank"
|
285 | className="cmap-hlist-item cmap-noselect cmap-pointer cmap-u-link-disc"
|
286 | href={
|
287 | 'http://resultmaps.neis-one.org/osm-discussion-comments?uid=' +
|
288 | userId
|
289 | }
|
290 | >
|
291 | Discussions
|
292 | </a>
|
293 | </li>
|
294 | <li>
|
295 | <a
|
296 | target="_blank"
|
297 | className="cmap-hlist-item cmap-noselect cmap-pointer cmap-u-link-comm"
|
298 | href={
|
299 | 'http://resultmaps.neis-one.org/osm-discussion-comments?uid=115894' +
|
300 | userId +
|
301 | '&commented'
|
302 | }
|
303 | >
|
304 | Comments
|
305 | </a>
|
306 | </li>
|
307 | </ul>
|
308 | </section>
|
309 | <section className="cmap-filter-action-section cmap-pt3">
|
310 | <h6 className="cmap-heading pointer" onClick={this.toggleActions}>
|
311 | {this.state.actions ? '▼' : '▶'}Filter by actions
|
312 | </h6>
|
313 |
|
314 | <ul
|
315 | style={{
|
316 | display: this.state.actions ? 'block' : 'none'
|
317 | }}
|
318 | className="cmap-hlist"
|
319 | >
|
320 | <li>
|
321 | <label className="cmap-hlist-item cmap-noselect cmap-pointer">
|
322 | <input
|
323 | type="checkbox"
|
324 | value="added"
|
325 | defaultChecked="true"
|
326 | id="cmap-layer-selector-added"
|
327 | onChange={filterLayers}
|
328 | />
|
329 | <span className="cmap-label-text">Added</span>
|
330 | <span className="cmap-color-box cmap-color-added" />
|
331 | </label>
|
332 | </li>
|
333 | <li>
|
334 | <label className="cmap-hlist-item cmap-noselect cmap-pointer">
|
335 | <input
|
336 | type="checkbox"
|
337 | value="modified"
|
338 | defaultChecked="true"
|
339 | id="cmap-layer-selector-modified"
|
340 | onChange={filterLayers}
|
341 | />
|
342 | <span className="cmap-label-text">Modified</span>
|
343 | <span className="cmap-color-box cmap-color-modified-old" />
|
344 | <span className="cmap-unicode">→</span>
|
345 | <span className="cmap-color-box cmap-color-modified-new" />
|
346 | </label>
|
347 | </li>
|
348 | <li>
|
349 | <label className="cmap-hlist-item cmap-noselect cmap-pointer">
|
350 | <input
|
351 | type="checkbox"
|
352 | value="deleted"
|
353 | defaultChecked="true"
|
354 | id="cmap-layer-selector-deleted"
|
355 | onChange={filterLayers}
|
356 | />
|
357 | <span className="cmap-label-text">Deleted</span>
|
358 | <span className="cmap-color-box cmap-color-deleted" />
|
359 | </label>
|
360 | </li>
|
361 | </ul>
|
362 | </section>
|
363 | <section className="cmap-filter-type-section">
|
364 | <h6 className="cmap-heading pointer" onClick={this.toggleType}>
|
365 | {this.state.type ? '▼' : '▶'}Filter by type
|
366 | </h6>
|
367 | <ul
|
368 | className="cmap-hlist"
|
369 | style={{
|
370 | display: this.state.type ? 'block' : 'none'
|
371 | }}
|
372 | >
|
373 | <li>
|
374 | <label className="cmap-hlist-item cmap-noselect cmap-pointer">
|
375 | <input
|
376 | type="checkbox"
|
377 | value="nodes"
|
378 | defaultChecked="true"
|
379 | id="cmap-type-selector-nodes"
|
380 | onClick={filterLayers}
|
381 | />
|
382 | <span className="cmap-label-text">Nodes</span>
|
383 | </label>
|
384 | </li>
|
385 | <li>
|
386 | <label className="cmap-hlist-item cmap-noselect cmap-pointer">
|
387 | <input
|
388 | type="checkbox"
|
389 | value="ways"
|
390 | defaultChecked="true"
|
391 | id="cmap-type-selector-ways"
|
392 | onChange={filterLayers}
|
393 | />
|
394 | <span className="cmap-label-text">Ways</span>
|
395 | </label>
|
396 | </li>
|
397 | <li>
|
398 | <label className="cmap-hlist-item cmap-noselect cmap-pointer">
|
399 | <input
|
400 | type="checkbox"
|
401 | value="relations"
|
402 | defaultChecked="true"
|
403 | id="cmap-type-selector-relations"
|
404 | onChange={filterLayers}
|
405 | />
|
406 | <span className="cmap-label-text">Relations</span>
|
407 | </label>
|
408 | </li>
|
409 | </ul>
|
410 | </section>
|
411 | <section className="cmap-filter-changes-section cmap-pb3">
|
412 | <h6 className="cmap-heading pointer" onClick={this.toggleChanges}>
|
413 | {this.state.changes ? '▼' : '▶'}Tags added / updated / deleted
|
414 | </h6>
|
415 | <ul
|
416 | className="cmap-sub-hlist"
|
417 | style={{ display: this.state.changes ? 'block' : 'none' }}
|
418 | >
|
419 | {this.changeReport
|
420 | .sort()
|
421 | .map((change, k) => <ChangeItem key={k} change={change} />)}
|
422 | </ul>
|
423 | </section>
|
424 | <section className="cmap-map-style-section cmap-pb3">
|
425 | <h6 className="cmap-heading pointer" onClick={this.toggleMapStyle}>
|
426 | {this.state.mapStyle ? '▼' : '▶'}Map style
|
427 | </h6>
|
428 |
|
429 | <ul
|
430 | className="cmap-hlist"
|
431 | style={{
|
432 | display: this.state.mapStyle ? 'block' : 'none'
|
433 | }}
|
434 | >
|
435 | <li>
|
436 | <label className="cmap-hlist-item cmap-noselect cmap-pointer">
|
437 | <input
|
438 | type="radio"
|
439 | value="satellite"
|
440 | defaultChecked="true"
|
441 | name="baselayer"
|
442 | id="cmap-baselayer-satellite"
|
443 | onChange={this.props.toggleLayer}
|
444 | />
|
445 | <span className="cmap-label-text">Satellite</span>
|
446 | </label>
|
447 | </li>
|
448 | <li>
|
449 | <label className="cmap-hlist-item cmap-noselect cmap-pointer">
|
450 | <input
|
451 | type="radio"
|
452 | value="streets"
|
453 | name="baselayer"
|
454 | id="cmap-baselayer-streets"
|
455 | onChange={this.props.toggleLayer}
|
456 | />
|
457 | <span className="cmap-label-text">Streets</span>
|
458 | </label>
|
459 | </li>
|
460 | <li>
|
461 | <label className="cmap-hlist-item cmap-noselect cmap-pointer">
|
462 | <input
|
463 | type="radio"
|
464 | value="dark"
|
465 | name="baselayer"
|
466 | id="cmap-baselayer-dark"
|
467 | onChange={this.props.toggleLayer}
|
468 | />
|
469 | <span className="cmap-label-text">Dark</span>
|
470 | </label>
|
471 | </li>
|
472 | </ul>
|
473 | </section>
|
474 | </div>
|
475 | );
|
476 | }
|
477 | }
|