UNPKG

28.6 kBJavaScriptView Raw
1import mapboxgl from 'mapbox-gl';
2
3import { getBoundingBox, getBounds } from './helpers';
4import { cmap } from './render';
5import { propsDiff } from './propsDiff';
6
7export class Map {
8 constructor() {
9 this.map = null;
10 this.queue = [];
11 this.filterLayers = this.filterLayers.bind(this);
12 }
13 filterLayers() {
14 var layersKey = {
15 'added-line': { added: true, ways: true },
16 'added-point-tagged': { added: true, nodes: true },
17 'added-point-untagged': { added: true, nodes: true },
18 'added-relation': { added: true, relations: true },
19 'modified-old-line': { modified: true, ways: true },
20 'modified-old-point-tagged': { modified: true, nodes: true },
21 'modified-old-point-untagged': { modified: true, nodes: true },
22 'modified-old-point-on-way': { modified: true, nodes: true },
23 'modified-new-line': { modified: true, ways: true },
24 'modified-old-relation': { modified: true, relations: true },
25 'modified-new-point-tagged': { modified: true, nodes: true },
26 'modified-new-point-untagged': { modified: true, nodes: true },
27 'modified-new-point-on-way': { modified: true, nodes: true },
28 'modified-new-relation': { modified: true, relations: true },
29 'deleted-line': { deleted: true, ways: true },
30 'deleted-point-tagged': { deleted: true, nodes: true },
31 'deleted-point-untagged': { deleted: true, nodes: true },
32 'deleted-relation': { deleted: true, relations: true }
33 };
34
35 var selectedActions = [];
36 var selectedTypes = [];
37 document
38 .querySelectorAll('.cmap-filter-action-section input:checked')
39 .forEach(function(checkedElement) {
40 selectedActions.push(checkedElement.value);
41 });
42
43 document
44 .querySelectorAll('.cmap-filter-type-section input:checked')
45 .forEach(function(checkedElement) {
46 selectedTypes.push(checkedElement.value);
47 });
48
49 var layers = Object.keys(layersKey);
50
51 layers.forEach(layer => {
52 var isSelectedAction = selectedActions.reduce(function(accum, action) {
53 return layersKey[layer][action] || accum;
54 }, false);
55 var isSelectedType = selectedTypes.reduce(function(accum, type) {
56 return layersKey[layer][type] || accum;
57 }, false);
58
59 if (isSelectedAction && isSelectedType) {
60 this.map.setLayoutProperty(layer, 'visibility', 'visible');
61 } else {
62 this.map.setLayoutProperty(layer, 'visibility', 'none');
63 }
64
65 if (selectedActions.length === 0 || selectedTypes.length === 0) {
66 this.map.setLayoutProperty('bg-point', 'visibility', 'none');
67 this.map.setLayoutProperty('bg-line', 'visibility', 'none');
68 } else {
69 this.map.setLayoutProperty('bg-point', 'visibility', 'visible');
70 this.map.setLayoutProperty('bg-line', 'visibility', 'visible');
71 }
72 });
73 }
74 getMapInstance() {
75 return this.map;
76 }
77 getResult() {
78 return this.result;
79 }
80 remove() {
81 if (this.map) {
82 this.map.remove();
83 this.mapLoaded = false;
84 this.map = undefined;
85 }
86 }
87 addMapSource(result, bounds) {
88 if (this.map.getSource('changeset')) {
89 this.map.getSource('changeset').setData(result.geojson);
90 } else {
91 this.map.addSource('changeset', {
92 type: 'geojson',
93 data: result.geojson
94 });
95 }
96
97 if (this.map.getSource('bbox')) {
98 this.map.getSource('bbox').setData(getBoundingBox(bounds));
99 } else {
100 this.map.addSource('bbox', {
101 type: 'geojson',
102 data: getBoundingBox(bounds)
103 });
104 }
105 }
106 addMapLayers() {
107 this.map.addLayer({
108 id: 'bbox-line',
109 type: 'line',
110 source: 'bbox',
111 paint: {
112 'line-color': '#A58CF2',
113 'line-opacity': 0.75,
114 'line-width': 2
115 }
116 });
117
118 this.map.addLayer({
119 id: 'bg-line',
120 source: 'changeset',
121 type: 'line',
122 layout: {
123 'line-cap': 'round',
124 'line-join': 'round'
125 },
126 paint: {
127 'line-color': 'hsl(0, 0%, 15%)',
128 'line-width': 12,
129 'line-blur': 0.2,
130 'line-opacity': {
131 base: 1.5,
132 stops: [[12, 0.5], [18, 0.2]]
133 }
134 },
135 filter: ['all', ['==', 'type', 'way']]
136 });
137
138 this.map.addLayer({
139 id: 'bg-point',
140 source: 'changeset',
141 type: 'circle',
142 paint: {
143 'circle-color': 'hsl(0, 0%, 15%)',
144 'circle-blur': 0.2,
145 'circle-opacity': {
146 base: 1.5,
147 stops: [[12, 0.5], [18, 0.2]]
148 },
149 'circle-radius': {
150 base: 1.5,
151 stops: [[10, 12], [16, 10]]
152 }
153 },
154 filter: ['all', ['==', '$type', 'Point']]
155 });
156
157 this.map.addLayer({
158 id: 'highlight-line',
159 source: 'changeset',
160 type: 'line',
161 layout: {
162 'line-join': 'round',
163 'line-cap': 'round'
164 },
165 paint: {
166 'line-color': 'hsl(0, 0%, 75%)',
167 'line-width': {
168 base: 1,
169 stops: [[10, 15], [16, 10]]
170 },
171 'line-opacity': {
172 base: 1.5,
173 stops: [[12, 0.75], [18, 0.75]]
174 }
175 },
176 filter: ['all', ['==', 'id', ''], ['==', '$type', 'LineString']]
177 });
178
179 this.map.addLayer({
180 id: 'highlight-point',
181 source: 'changeset',
182 type: 'circle',
183 paint: {
184 'circle-color': 'hsl(0, 0%, 75%)',
185 'circle-radius': {
186 base: 1,
187 stops: [[10, 10], [16, 11]]
188 },
189 'circle-opacity': 0.8
190 },
191 filter: ['all', ['==', 'id', ''], ['==', '$type', 'Point']]
192 });
193
194 // Relations
195
196 this.map.addLayer({
197 id: 'deleted-relation',
198 source: 'changeset',
199 type: 'line',
200 paint: {
201 'line-color': '#CC2C47',
202 'line-width': {
203 base: 1,
204 stops: [[8, 1.5], [12, 1.5]]
205 },
206 'line-dasharray': [0.1, 0.1],
207 'line-opacity': 0.8
208 },
209 filter: [
210 'all',
211 ['==', 'type', 'relation'],
212 ['==', 'changeType', 'deletedNew']
213 ]
214 });
215
216 this.map.addLayer({
217 id: 'modified-old-relation',
218 source: 'changeset',
219 type: 'line',
220 layout: {
221 'line-join': 'round',
222 'line-cap': 'round'
223 },
224 paint: {
225 'line-color': '#DB950A',
226 'line-width': {
227 base: 1,
228 stops: [[8, 1.75], [12, 1.75]]
229 },
230 'line-blur': 0.25,
231 'line-opacity': 0.8
232 },
233 filter: [
234 'all',
235 ['==', 'type', 'relation'],
236 ['==', 'changeType', 'modifiedOld']
237 ]
238 });
239
240 this.map.addLayer({
241 id: 'modified-new-relation',
242 source: 'changeset',
243 type: 'line',
244 layout: {
245 'line-join': 'round',
246 'line-cap': 'round'
247 },
248 paint: {
249 'line-color': '#E8E845',
250 'line-width': {
251 base: 1,
252 stops: [[8, 1.25], [12, 1.25]]
253 },
254 'line-opacity': 0.8
255 },
256 filter: [
257 'all',
258 ['==', 'type', 'relation'],
259 ['==', 'changeType', 'modifiedNew']
260 ]
261 });
262
263 this.map.addLayer({
264 id: 'added-relation',
265 source: 'changeset',
266 type: 'line',
267 interactive: true,
268 layout: {
269 'line-join': 'round',
270 'line-cap': 'round'
271 },
272 paint: {
273 'line-color': '#39DBC0',
274 'line-width': {
275 base: 1,
276 stops: [[8, 1], [12, 1]]
277 },
278 'line-opacity': 0.8
279 },
280 filter: ['all', ['==', 'type', 'relation'], ['==', 'changeType', 'added']]
281 });
282
283 this.map.addLayer({
284 id: 'deleted-line',
285 source: 'changeset',
286 type: 'line',
287 paint: {
288 'line-color': '#CC2C47',
289 'line-width': {
290 base: 1,
291 stops: [[8, 3], [12, 5]]
292 },
293 'line-dasharray': [0.1, 0.25],
294 'line-opacity': 0.8
295 },
296 filter: ['all', ['==', 'type', 'way'], ['==', 'changeType', 'deletedNew']]
297 });
298
299 this.map.addLayer({
300 id: 'modified-old-point-on-way',
301 source: 'changeset',
302 type: 'circle',
303 paint: {
304 'circle-color': '#DB950A',
305 'circle-opacity': {
306 base: 1.5,
307 stops: [[10, 0.25], [14, 0.5]]
308 },
309 'circle-blur': 0.25,
310 'circle-radius': {
311 base: 1.5,
312 stops: [[10, 2.5], [16, 3.5]]
313 }
314 },
315 filter: [
316 'all',
317 ['==', '$type', 'LineString'],
318 ['==', 'changeType', 'modifiedOld']
319 ]
320 });
321
322 this.map.addLayer({
323 id: 'modified-old-line',
324 source: 'changeset',
325 type: 'line',
326 layout: {
327 'line-join': 'round',
328 'line-cap': 'round'
329 },
330 paint: {
331 'line-color': '#DB950A',
332 'line-width': {
333 base: 1,
334 stops: [[8, 3], [12, 6]]
335 },
336 'line-blur': {
337 base: 1,
338 stops: [[8, 0.25], [12, 0.5]]
339 },
340 'line-opacity': 0.6
341 },
342 filter: [
343 'all',
344 ['==', 'type', 'way'],
345 ['==', 'changeType', 'modifiedOld']
346 ]
347 });
348
349 this.map.addLayer({
350 id: 'modified-new-point-on-way',
351 source: 'changeset',
352 type: 'circle',
353 paint: {
354 'circle-color': '#E8E845',
355 'circle-opacity': {
356 base: 1.5,
357 stops: [[10, 0.25], [14, 0.25]]
358 },
359 'circle-radius': {
360 base: 1.5,
361 stops: [[10, 1.25], [16, 2.25]]
362 }
363 },
364 filter: [
365 'all',
366 ['==', '$type', 'LineString'],
367 ['==', 'changeType', 'modifiedNew']
368 ]
369 });
370
371 this.map.addLayer({
372 id: 'modified-new-line',
373 source: 'changeset',
374 type: 'line',
375 layout: {
376 'line-join': 'round',
377 'line-cap': 'round'
378 },
379 paint: {
380 'line-color': '#E8E845',
381 'line-width': {
382 base: 1,
383 stops: [[8, 1], [12, 2]]
384 },
385 'line-opacity': 0.6
386 },
387 filter: [
388 'all',
389 ['==', 'type', 'way'],
390 ['==', 'changeType', 'modifiedNew']
391 ]
392 });
393
394 this.map.addLayer({
395 id: 'added-line',
396 source: 'changeset',
397 type: 'line',
398 interactive: true,
399 layout: {
400 'line-join': 'round',
401 'line-cap': 'round'
402 },
403 paint: {
404 'line-color': '#39DBC0',
405 'line-width': {
406 base: 1,
407 stops: [[8, 1], [12, 1.5]]
408 },
409 'line-opacity': 0.8
410 },
411 filter: ['all', ['==', 'type', 'way'], ['==', 'changeType', 'added']]
412 });
413
414 this.map.addLayer({
415 id: 'deleted-point-untagged',
416 source: 'changeset',
417 type: 'circle',
418 paint: {
419 'circle-color': '#CC2C47',
420 'circle-radius': {
421 base: 1.5,
422 stops: [[10, 2], [16, 3]]
423 },
424 'circle-opacity': {
425 base: 1.5,
426 stops: [[10, 0.25], [14, 0.5]]
427 }
428 },
429 filter: [
430 'all',
431 ['==', 'changeType', 'deletedOld'],
432 ['any', ['==', 'tagsCount', 0], ['==', '$type', 'LineString']]
433 ]
434 });
435
436 this.map.addLayer({
437 id: 'modified-old-point-untagged',
438 source: 'changeset',
439 type: 'circle',
440 paint: {
441 'circle-color': '#DB950A',
442 'circle-opacity': {
443 base: 1.5,
444 stops: [[10, 0.25], [14, 0.5]]
445 },
446 'circle-radius': {
447 base: 1.5,
448 stops: [[10, 1.75], [16, 3]]
449 },
450 'circle-stroke-width': 1,
451 'circle-stroke-opacity': 0.9,
452 'circle-stroke-color': '#DB950A'
453 },
454 filter: [
455 'all',
456 ['==', 'type', 'node'],
457 ['==', 'changeType', 'modifiedOld'],
458 ['==', 'tagsCount', 0]
459 ]
460 });
461
462 this.map.addLayer({
463 id: 'modified-new-point-untagged',
464 source: 'changeset',
465 type: 'circle',
466 paint: {
467 'circle-color': '#E8E845',
468 'circle-opacity': {
469 base: 1.5,
470 stops: [[10, 0.25], [14, 0.5]]
471 },
472 'circle-radius': {
473 base: 1.5,
474 stops: [[10, 0.75], [16, 2]]
475 },
476 'circle-stroke-width': 1,
477 'circle-stroke-opacity': 0.9,
478 'circle-stroke-color': '#E8E845'
479 },
480 filter: [
481 'all',
482 ['==', 'type', 'node'],
483 ['==', 'changeType', 'modifiedNew'],
484 ['==', 'tagsCount', 0]
485 ]
486 });
487
488 this.map.addLayer({
489 id: 'added-point-untagged',
490 source: 'changeset',
491 type: 'circle',
492 paint: {
493 'circle-color': '#39DBC0',
494 'circle-opacity': {
495 base: 1.5,
496 stops: [[10, 0.3], [14, 0.75]]
497 },
498 'circle-radius': {
499 base: 1.5,
500 stops: [[10, 1.25], [16, 1.9]]
501 }
502 },
503 filter: [
504 'all',
505 ['==', 'type', 'node'],
506 ['==', 'changeType', 'added'],
507 ['==', 'tagsCount', 0]
508 ]
509 });
510
511 this.map.addLayer({
512 id: 'deleted-point-tagged',
513 source: 'changeset',
514 type: 'circle',
515 paint: {
516 'circle-color': '#CC2C47',
517 'circle-radius': {
518 base: 1.5,
519 stops: [[10, 4], [16, 7]]
520 },
521 'circle-opacity': {
522 base: 1.5,
523 stops: [[10, 0.25], [14, 0.5]]
524 },
525 'circle-stroke-width': 1,
526 'circle-stroke-opacity': 0.75,
527 'circle-stroke-color': '#CC2C47'
528 },
529 filter: [
530 'all',
531 ['==', 'type', 'node'],
532 ['==', 'changeType', 'deletedOld'],
533 ['!=', 'tagsCount', 0]
534 ]
535 });
536
537 this.map.addLayer({
538 id: 'modified-old-point-tagged',
539 source: 'changeset',
540 type: 'circle',
541 paint: {
542 'circle-color': '#DB950A',
543 'circle-opacity': {
544 base: 1.5,
545 stops: [[10, 0.25], [14, 0.75]]
546 },
547 'circle-radius': {
548 base: 1.5,
549 stops: [[10, 2.5], [16, 9]]
550 },
551 'circle-stroke-width': 1,
552 'circle-stroke-opacity': 0.9,
553 'circle-stroke-color': '#DB950A'
554 },
555 filter: [
556 'all',
557 ['==', 'type', 'node'],
558 ['==', 'changeType', 'modifiedOld'],
559 ['!=', 'tagsCount', 0]
560 ]
561 });
562
563 this.map.addLayer({
564 id: 'modified-new-point-tagged',
565 source: 'changeset',
566 type: 'circle',
567 paint: {
568 'circle-color': '#E8E845',
569 'circle-opacity': {
570 base: 1.5,
571 stops: [[10, 0.25], [14, 0.75]]
572 },
573 'circle-radius': {
574 base: 1.5,
575 stops: [[10, 2], [16, 7]]
576 },
577 'circle-stroke-width': 1,
578 'circle-stroke-opacity': 0.9,
579 'circle-stroke-color': '#E8E845'
580 },
581 filter: [
582 'all',
583 ['==', 'type', 'node'],
584 ['==', 'changeType', 'modifiedNew'],
585 ['!=', 'tagsCount', 0]
586 ]
587 });
588
589 this.map.addLayer({
590 id: 'added-point-tagged',
591 source: 'changeset',
592 type: 'circle',
593 paint: {
594 'circle-color': '#39DBC0',
595 'circle-opacity': {
596 base: 1.5,
597 stops: [[10, 0.3], [14, 0.75]]
598 },
599 'circle-radius': {
600 base: 1.5,
601 stops: [[10, 1], [16, 5]]
602 },
603 'circle-stroke-width': 1,
604 'circle-stroke-opacity': 0.9,
605 'circle-stroke-color': '#39DBC0'
606 },
607 filter: [
608 'all',
609 ['==', 'type', 'node'],
610 ['==', 'changeType', 'added'],
611 ['!=', 'tagsCount', 0]
612 ]
613 });
614 }
615
616 renderMap(baseLayer, result) {
617 if (!result) {
618 if (!this.result) return;
619 result = this.result;
620 } else {
621 this.result = result;
622 }
623 var bounds = getBounds(result.changeset.bbox);
624 if (this.map) {
625 if (!this.mapLoaded) {
626 this.queue.push([result, bounds]); // TOFIX use variable instead of array
627 this.result = result;
628 return;
629 }
630
631 if (baseLayer && this.oldBaseLayer !== baseLayer) {
632 this.map.setStyle(baseLayer);
633 this.baseLayerData = [result, bounds];
634 this.oldBaseLayer = baseLayer;
635 } else {
636 this.oldBaseLayer = baseLayer;
637 this.addMapSource(result, bounds);
638 this.map.fitBounds(bounds, { linear: true, padding: 200 });
639 this.result = result;
640 clearDiff();
641 }
642
643 // why not re attach on('click')
644 // if the map is still mounted
645 // it will automatically take the latest
646 // result thanks to this.result
647 return;
648 }
649
650 this.map = new mapboxgl.Map({
651 container: document.querySelector('.cmap-map'),
652 style: baseLayer || 'mapbox://styles/rasagy/cizp6lsah00ct2snu6gi3p16q',
653 center: bounds.getCenter(),
654 zoom: 14,
655 dragRotate: false,
656 touchZoomRotate: false
657 });
658 this.map.on('styledata', () => {
659 if (!this.baseLayerData) return;
660 console.log('style event fired 2');
661 var bounds = this.baseLayerData[1];
662 var result = this.baseLayerData[0];
663 this.baseLayerData = null;
664 this.map.fitBounds(bounds, { linear: true, padding: 200 });
665 this.addMapSource(result, bounds);
666 this.addMapLayers();
667 cmap.emit('load');
668 });
669 this.map.on('load', () => {
670 this.mapLoaded = true;
671 if (this.queue.length > 0) {
672 const index = this.queue.length - 1;
673 result = this.queue[index][0];
674 bounds = this.queue[index][1];
675 this.queue = [];
676 }
677 this.map.fitBounds(bounds, { linear: true, padding: 200 });
678 this.addMapSource(result, bounds);
679 this.addMapLayers();
680 cmap.emit('load');
681 });
682
683 this.map.on('click', e => {
684 var x1y1 = [e.point.x - 5, e.point.y - 5];
685 var x2y2 = [e.point.x + 5, e.point.y + 5];
686 var features = this.map.queryRenderedFeatures([x1y1, x2y2], {
687 layers: [
688 'added-line',
689 'added-point-tagged',
690 'modified-old-line',
691 'modified-old-point-tagged',
692 'modified-old-point-untagged',
693 'modified-new-line',
694 'modified-new-point-tagged',
695 'modified-new-point-untagged',
696 'deleted-line',
697 'deleted-point-tagged',
698 'added-relation',
699 'modified-old-relation',
700 'modified-new-relation',
701 'deleted-relation'
702 ]
703 });
704
705 if (features.length) {
706 this.selectFeature(features[0]);
707 } else {
708 this.clearFeature();
709 }
710 });
711 }
712 selectFeature(feature) {
713 var featureMap = this.result.featureMap;
714 var featureId = feature.properties.id;
715 var osmType = feature.properties.type;
716
717 this.highlightFeature(featureId);
718 displayDiff(featureId, featureMap);
719 cmap.emit('featureChange', osmType, featureId);
720 }
721 highlightFeature(featureId) {
722 this.map.setFilter('highlight-line', ['==', 'id', featureId]);
723 this.map.setFilter('highlight-point', ['==', 'id', featureId]);
724 }
725 clearHighlight() {
726 this.map.setFilter('highlight-line', ['==', 'id', '']);
727 this.map.setFilter('highlight-point', ['==', 'id', '']);
728 }
729 clearFeature() {
730 this.clearHighlight();
731 clearDiff();
732 cmap.emit('featureChange', null, null);
733 }
734}
735
736//Calculates the difference in the selected features
737
738function displayDiff(id, featureMap) {
739 var featuresWithId = featureMap[id];
740 var metadataProps = featuresWithId.map(function(f) {
741 var props = Object.assign({}, f.properties);
742 delete props.tags;
743 delete props.tagsCount;
744 delete props.relations;
745 delete props.action;
746 return props;
747 });
748 var tagProps = featuresWithId.map(function(f) {
749 var props = Object.assign({}, f.properties.tags);
750 props.changeType = f.properties.changeType;
751 return props;
752 });
753
754 // Sets headers for two tables
755
756 var type = featuresWithId[0].properties.type;
757 var metadataHeader = elt(
758 'div',
759 {},
760 elt('span', { class: 'cmap-inline-block' }, type.toUpperCase() + ': ' + id),
761 elt(
762 'ul',
763 { class: 'cmap-hlist cmap-inline-block cmap-fr' },
764 elt(
765 'li',
766 {},
767 elt(
768 'a',
769 {
770 target: '_blank',
771 class: 'cmap-hlist-item cmap-pointer cmap-noselect',
772 href: '//www.openstreetmap.org/' + type + '/' + id + '/history'
773 },
774 'OSM'
775 )
776 ),
777 elt(
778 'li',
779 {},
780 elt(
781 'a',
782 {
783 target: '_blank',
784 class: 'cmap-hlist-item cmap-pointer cmap-noselect',
785 href: '//osmlab.github.io/osm-deep-history/#/' + type + '/' + id
786 },
787 'Deep History'
788 )
789 )
790 )
791 );
792 var metadataHTML = getDiffHTML(
793 propsDiff(metadataProps),
794 ['id', 'type', 'changeType'],
795 metadataHeader
796 );
797 var tagHeader = elt(
798 'span',
799 { class: 'cmap-inline-block' },
800 'Tag details'.toUpperCase()
801 );
802 var tagHTML = getDiffHTML(
803 propsDiff(tagProps),
804 ['id', 'changeType'],
805 tagHeader
806 );
807
808 document.querySelector('.cmap-diff').style.display = 'block';
809
810 document.querySelector('.cmap-diff-metadata').innerHTML = '';
811 document.querySelector('.cmap-diff-metadata').appendChild(metadataHTML);
812 document.querySelector('.cmap-diff-metadata').style.display = 'block';
813
814 document.querySelector('.cmap-diff-tags').innerHTML = '';
815 document.querySelector('.cmap-diff-tags').appendChild(tagHTML);
816 document.querySelector('.cmap-diff-tags').style.display = 'block';
817}
818
819function clearDiff() {
820 document.querySelector('.cmap-diff').style.display = 'none';
821
822 document.querySelector('.cmap-diff-metadata').innerHTML = '';
823 document.querySelector('.cmap-diff-metadata').style.display = 'none';
824
825 document.querySelector('.cmap-diff-tags').innerHTML = '';
826 document.querySelector('.cmap-diff-tags').style.display = 'none';
827}
828
829//Renders the markup for a table
830function getDiffHTML(diff, ignoreList, header) {
831 var isAddedFeature = diff['changeType'].added === 'added';
832
833 var root = elt('table', { class: 'cmap-diff-table' });
834 if (isAddedFeature) {
835 root.style.width = '300px';
836 }
837
838 if (header) {
839 root.appendChild(
840 elt(
841 'thead',
842 {},
843 elt(
844 'tr',
845 {},
846 elt(
847 'td',
848 {
849 colspan: isAddedFeature ? '2' : '3',
850 class: 'cmap-table-head'
851 },
852 header
853 )
854 )
855 )
856 );
857 }
858
859 var tbody = elt('tbody');
860
861 var types = ['added', 'deleted', 'modifiedOld', 'modifiedNew', 'unchanged'];
862 var sortedProps = Object.keys(diff).sort(function(keyA, keyB) {
863 var indexA = types.indexOf(Object.keys(diff[keyA])[0]);
864 var indexB = types.indexOf(Object.keys(diff[keyB])[0]);
865 return indexA - indexB;
866 });
867
868 sortedProps.forEach(function(prop) {
869 if (ignoreList.indexOf(prop) === -1) {
870 var tr = elt('tr');
871
872 var th = elt('th', { title: prop, class: 'cmap-strong' }, prop);
873 tr.appendChild(th);
874
875 types.forEach(function(type) {
876 if (diff[prop].hasOwnProperty(type)) {
877 var propClass = 'diff-property cmap-scroll-styled props-diff-' + type;
878 if (type == 'added' && !isAddedFeature) {
879 var empty = elt('td', { class: propClass });
880 tr.appendChild(empty);
881 }
882
883 var td = elt('td', { class: propClass }, diff[prop][type]);
884 tr.appendChild(td);
885
886 if (type == 'deleted') {
887 var empty = elt('td', { class: propClass });
888 tr.appendChild(empty);
889 }
890
891 if (type == 'unchanged') {
892 tr.appendChild(td.cloneNode(true));
893 }
894 }
895 });
896
897 tbody.appendChild(tr);
898 }
899 });
900
901 root.appendChild(tbody);
902
903 return root;
904}
905
906// Recursively adds html elements
907function elt(name, attributes) {
908 var node = document.createElement(name);
909 if (attributes) {
910 for (var attr in attributes)
911 if (attributes.hasOwnProperty(attr))
912 node.setAttribute(attr, attributes[attr]);
913 }
914 for (var i = 2; i < arguments.length; i++) {
915 var child = arguments[i];
916 if (typeof child == 'string') child = document.createTextNode(child);
917 node.appendChild(child);
918 }
919 return node;
920}