├── .gitignore ├── LICENSE ├── css └── oscviewer.css ├── img ├── layer-switcher-maximize.png ├── layer-switcher-minimize.png └── loadinggif-4.gif ├── index.html ├── js ├── FormatRegistry.js ├── Loader.js ├── OSMChangeStyle.js ├── OverpassAPI.js ├── PopupHandler.js ├── XHRDebug.js ├── control │ ├── Diff.js │ ├── Live.js │ ├── Loading.js │ ├── Player.js │ └── Status.js ├── map.js ├── oscviewer.js └── tooltips.js ├── lib ├── olex.js └── overpass.js ├── licenses ├── momentjs-LICENSE ├── openlayers-authors.txt ├── openlayers-license.txt ├── openlayers_themes-LICENSE.txt └── underscorejs-LICENSE └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /nbproject/ 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Norbert Renner and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /css/oscviewer.css: -------------------------------------------------------------------------------- 1 | html,body,#map_div { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | body { 7 | margin: 0; 8 | /*background-color: #f1eee8;*/ 9 | background-color: #000000; 10 | } 11 | 12 | body,#map_div,.info,.info td,#info,#info td,#legend,#about,.text_content { 13 | font-family: Arial, sans-serif; 14 | } 15 | 16 | .info,.info td,#info,#info td,#legend,#legend td,#about,.text_content,.control_input { 17 | font-size: small; 18 | } 19 | 20 | #map_div,a { 21 | color: grey; 22 | } 23 | 24 | .pale { 25 | filter: saturate(30%) brightness(0.3); 26 | } 27 | .gray { 28 | filter: grayscale(100%) brightness(0.4); 29 | } 30 | .inverted { 31 | filter: grayscale(100%) invert(80%) brightness(0.5); 32 | } 33 | 34 | .hidden { 35 | display: none; 36 | } 37 | 38 | .minimize { 39 | /* 40 | position: relative; 41 | */ 42 | position: absolute; 43 | right: 0px; 44 | top: 0px; 45 | margin: 3px; 46 | border-radius: 4px; 47 | cursor: pointer; 48 | } 49 | .minimize_icon { 50 | width: 10px; 51 | height: 10px; 52 | margin: 6px; 53 | background-image: url(../img/layer-switcher-minimize.png); 54 | } 55 | /* 56 | #bbox_button, #live_button { 57 | cursor: pointer; 58 | float: left; 59 | } 60 | */ 61 | 62 | #bottom_buttons { 63 | left: 0px; 64 | bottom: 0px; 65 | } 66 | .bottom_buttons_cell { 67 | vertical-align: bottom; 68 | display: table-cell; 69 | } 70 | .bottom_buttons_cell > div { 71 | position: relative; 72 | vertical-align: bottom; 73 | } 74 | #about_border { 75 | /* 76 | left: 217px; 77 | bottom: 0px; 78 | */ 79 | max-width: 750px; 80 | } 81 | /* TODO text_content (bottom buttons) scrollable 82 | #help_content { 83 | overflow:auto; 84 | } 85 | */ 86 | .help_section { 87 | margin-top: 6px; 88 | margin-bottom: 3px; 89 | font-weight: bold; 90 | } 91 | .help_row { 92 | display: block; 93 | } 94 | .help_row > div { 95 | display: table-cell; 96 | } 97 | .label_col { 98 | padding-left: 5px; 99 | padding-right: 10px; 100 | text-align: right; 101 | width: 3em; 102 | } 103 | #fileinput_border { 104 | left: 38px; 105 | top: 0px; 106 | 107 | /* TODO file input only for Osmosis edition */ 108 | display: none; 109 | } 110 | 111 | /* #fileinput_border,#top_buttons_border,*/ 112 | #bottom_buttons,#top_control_row,.olControlLayerSwitcher { 113 | position: absolute; 114 | z-index: 9999; 115 | margin: 8px; 116 | } 117 | .border,.olControlLayerSwitcher { 118 | border-radius: 4px; 119 | padding: 3px; 120 | background-color: rgba(255, 255, 255, 0.4); 121 | } 122 | .inner_border { 123 | margin-left: 8px; 124 | } 125 | #top_control_row > .border { 126 | float: left; 127 | } 128 | 129 | .button,.text_content,.info,.button_disabled { 130 | padding: 5px; 131 | background-color: rgba(0, 0, 0, 0.7); 132 | } 133 | .button,.info { 134 | color: white; 135 | } 136 | .button_disabled { 137 | color: #888; 138 | cursor: default; 139 | pointer-events: none; 140 | } 141 | .text_content,.text_content a { 142 | color: #BBB; 143 | } 144 | #status_bar, #status_bar > span { 145 | float: left; 146 | } 147 | .status_field { 148 | padding-left: 4px; 149 | padding-right: 7px; 150 | } 151 | .status_label { 152 | color: #888; 153 | vertical-align:top; 154 | font-size: 70%; 155 | } 156 | #status_countdown { 157 | width: 1em; 158 | text-align: center; 159 | } 160 | 161 | #spinner { 162 | height: 11px; 163 | width: 16px; 164 | } 165 | .spinner { 166 | background-image: url(../img/loadinggif-4.gif); 167 | background-repeat: no-repeat; 168 | background-position: center center; 169 | } 170 | #legend_content,.info,#info,#about_content,#fileinput_div,.olControlLayerSwitcher .layersDiv,.olControlLayerSwitcher .maximizeDiv,.olControlLayerSwitcher .minimizeDiv,.button,.text_content { 171 | border-radius: 4px; 172 | } 173 | /* #top_buttons_border */ 174 | #top_control_row { 175 | position: absolute; 176 | left: 38px; 177 | top: 0px; 178 | } 179 | .button,#status_bar { 180 | padding: 3px 10px 3px 10px; 181 | } 182 | .button, .button_disabled { 183 | -webkit-touch-callout: none; 184 | -webkit-user-select: none; 185 | -khtml-user-select: none; 186 | -moz-user-select: none; 187 | -ms-user-select: none; 188 | user-select: none; 189 | } 190 | .left_button { 191 | float: left; 192 | border-radius: 4px 0px 0px 4px; 193 | margin-right: 1px; 194 | } 195 | .right_button { 196 | float: left; 197 | border-radius: 0px 4px 4px 0px; 198 | } 199 | .inner_button { 200 | float: left; 201 | margin-right: 1px; 202 | border-radius: 0px; 203 | } 204 | 205 | .datetime_buttons { 206 | margin-right: 0px; 207 | } 208 | .datetime_buttons > div { 209 | padding: 0px 3px 0px 10px; 210 | border-radius: 0px; 211 | font-size: 55%; 212 | line-height: 1; 213 | text-align: right; 214 | /* 215 | color: #888; 216 | background-color: rgba(0, 0, 0, 0.7); 217 | */ 218 | } 219 | .datetime_buttons > div:first-child { 220 | padding-top: 2px; 221 | } 222 | .datetime_buttons > div:last-child { 223 | padding-bottom: 2px; 224 | } 225 | /* 226 | .datetime_buttons > div:hover { 227 | cursor: pointer; 228 | background-color: rgba(0, 0, 0, 0.5) !important; 229 | } 230 | */ 231 | 232 | .control_input,.control_checkbox { 233 | height: 21px; 234 | } 235 | .control_input { 236 | margin-right: 0px; 237 | } 238 | .control_input > input,.control_input > textarea { 239 | width: 8em; 240 | height: 15px; 241 | font-size: 13px; 242 | margin: 0px; 243 | border: 3px solid rgba(0, 0, 0, 0.7); 244 | padding: 0px 5px 0px 5px; 245 | outline: none; 246 | color: black; 247 | background-color: rgba(0, 0, 0, 0); 248 | } 249 | .control_input > textarea { 250 | width: 24em; 251 | overflow:hidden; 252 | } 253 | .control_checkbox { 254 | margin-right: 0px; 255 | padding: 0px 1px 0px 3px; 256 | background-color: rgba(0, 0, 0, 0.7); 257 | color: #888; 258 | font-size: small; 259 | } 260 | .control_checkbox > input[type=checkbox] { 261 | /* normalize Firefox (14px) and Chrome (13px) + 1 for alignment */ 262 | height: 15px; 263 | /* Firefox now 18px, not resizable, so downscale for now */ 264 | -moz-transform: scale(0.8); 265 | } 266 | .inner_label { 267 | float: left; 268 | border-radius: 0px; 269 | margin-left: 0px; 270 | padding: 3px 5px 3px 0px; 271 | margin-right: 1px; 272 | } 273 | 274 | #load_button { 275 | padding-left: 7px; 276 | } 277 | 278 | .info,#info { 279 | overflow: auto; 280 | } 281 | 282 | .info a,#info a { 283 | color: white; 284 | } 285 | 286 | .info td,#info td { 287 | padding: 1px 5px 1px 5px; 288 | } 289 | 290 | #legend td { 291 | padding-left: 5px; 292 | border-radius: 4px; 293 | } 294 | .legend_symbol { 295 | padding-top: 0px; 296 | padding-bottom: 0px; 297 | } 298 | 299 | .title { 300 | font-weight: bold; 301 | margin-bottom: 10px; 302 | } 303 | 304 | .button { 305 | font-weight: normal; 306 | font-size: small; 307 | cursor: pointer; 308 | } 309 | 310 | .info .title,#info .title { 311 | margin-top: 5px; 312 | padding-left: 5px; 313 | } 314 | 315 | .info .footer { 316 | position:relative; 317 | text-align: center; 318 | margin-top: 5px; 319 | } 320 | 321 | .warning { 322 | background-color: rgba(255, 215, 0, 0.8); 323 | padding: 5px; 324 | font-weight: bold; 325 | } 326 | 327 | td.default { 328 | background-color: #323232; 329 | } 330 | 331 | td.keydefault { 332 | } 333 | 334 | td.undefined { 335 | background-color: none; 336 | } 337 | 338 | .delete { 339 | color: rgba(255, 51, 51, 0.8); 340 | } 341 | 342 | .create { 343 | color: rgba(250, 247, 151, 0.6); 344 | } 345 | 346 | .modify_geometry { 347 | color: rgba(144, 238, 144, 0.6); 348 | } 349 | 350 | .modify { 351 | color: rgba(135, 206, 250, 0.6); 352 | } 353 | 354 | td.deleted { 355 | background-color: rgba(255, 51, 51, 0.8); /* #FF3333 */ 356 | } 357 | 358 | td.created { 359 | /*background-color: rgba(144, 238, 144, 0.6); lightgreen; #90EE90 */ 360 | background-color: rgba(250, 247, 151, 0.6); /* yellow #FAF797 */ 361 | } 362 | 363 | td.modified { 364 | background-color: rgba(135, 206, 250, 0.6); /*lightskyblue; #87CEFA */ 365 | } 366 | 367 | td.legend_created { 368 | background-color: #FAF797; 369 | } 370 | 371 | td.legend_deleted { 372 | background-color: #FF3333; 373 | } 374 | 375 | td.legend_geom_new { 376 | background-color: lightgreen; 377 | } 378 | 379 | td.legend_geom_old { 380 | background-color: darkred; 381 | } 382 | 383 | .tagkey { 384 | background-color: #323232; 385 | /* font-weight: bold; */ 386 | text-align: left; 387 | padding-right: 5px; 388 | } 389 | 390 | .tagsep { 391 | height: 6px; 392 | } 393 | 394 | .tagsep hr { 395 | display: none; 396 | /* 397 | height: 1px; 398 | border-style: none; 399 | background-color: grey; 400 | */ 401 | } 402 | 403 | /* OpenLayers Zoom Control */ 404 | div.olControlZoom a,.olControlLayerSwitcher .layersDiv,.olControlLayerSwitcher .maximizeDiv { 405 | background: #000000 !important; 406 | /* fallback for IE - IE6 requires background shorthand*/ 407 | background: rgba(0, 0, 0, 0.7) !important; 408 | filter: alpha(opacity = 70) !important; 409 | } 410 | div.olControlZoom a:hover,.maximizeDiv.olButton:hover,.button:hover,.button_active { 411 | background: #000000 !important; /* fallback for IE */ 412 | background-color: rgba(0, 0, 0, 0.5) !important; 413 | filter: alpha(opacity = 50) !important; 414 | } 415 | 416 | @media only screen and (max-width: 600px) { 417 | div.olControlZoom a:hover { 418 | background: rgba(0, 0, 0, 0.5) !important; 419 | } 420 | } 421 | 422 | /* OpenLayers LayerSwitcher Control */ 423 | .minimizeDiv.olButton:hover,.minimize:hover { 424 | background: #000000 !important; /* fallback for IE */ 425 | background-color: rgba(158, 158, 158, 0.2) !important; 426 | filter: alpha(opacity = 50) !important; 427 | } 428 | 429 | .olControlAttribution,.olControlPermalink { 430 | font-size: 70% !important; 431 | text-align: right; 432 | } 433 | .olControlAttribution { 434 | bottom: 0.5em !important; 435 | } 436 | div.olControlMousePosition { 437 | font-size: 70% !important; 438 | bottom: 2em !important; 439 | } 440 | .olControlPermalink { 441 | bottom: 3.5em !important; 442 | } 443 | .olControlLayerSwitcher { 444 | top: 0px; 445 | right: 0px; 446 | width: auto !important; 447 | } 448 | .layerDiv { 449 | width: 20em; 450 | } 451 | .olControlLayerSwitcher .maximizeDiv,.olControlLayerSwitcher .minimizeDiv { 452 | top: 0px !important; 453 | } 454 | 455 | .olControlLayerSwitcher .minimizeDiv { 456 | position: relative; 457 | top: 0px; 458 | right: 0px; 459 | margin: 3px; 460 | } 461 | 462 | .maximizeDiv.olButton,.minimizeDiv.olButton { 463 | width: 23px; 464 | height: 23px; 465 | } 466 | 467 | .maximizeDiv .olAlphaImg { 468 | margin: 2px; 469 | width: 19px; 470 | height: 19px; 471 | } 472 | 473 | .minimizeDiv .olAlphaImg { 474 | /* 10x10 */ 475 | margin: 7px 7px 6px 6px; 476 | } -------------------------------------------------------------------------------- /img/layer-switcher-maximize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrenner/achavi/9934871777b6e744d21bb2f22b112d386bcd9d30/img/layer-switcher-maximize.png -------------------------------------------------------------------------------- /img/layer-switcher-minimize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrenner/achavi/9934871777b6e744d21bb2f22b112d386bcd9d30/img/layer-switcher-minimize.png -------------------------------------------------------------------------------- /img/loadinggif-4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrenner/achavi/9934871777b6e744d21bb2f22b112d386bcd9d30/img/loadinggif-4.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | achavi - Augmented OSM Change Viewer  [attic] 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 | bbox 22 |
23 | 26 |
27 | clear 28 |
29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 | 37 |
38 | 41 |
42 | 43 |
44 |
relations
45 |
46 | load 47 |
48 |
49 | 50 | 57 | 58 | 82 | 100 |
101 | 102 |
103 |
104 |
105 |
106 |
legend
107 | 131 |
132 |
133 |
134 |
135 |
136 |
137 |
help
138 | 172 |
173 |
174 |
175 |
176 |
177 |
178 |
about
179 | 226 |
227 |
228 |
229 |
230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | -------------------------------------------------------------------------------- /js/FormatRegistry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * class FormatRegistry 3 | */ 4 | function FormatRegistry(formatOptions) { 5 | this.formatOptions = formatOptions; 6 | 7 | /* Maps XML root tag name to corresponding OpenLayers.Format class, custom name when multiple with same root */ 8 | this.formats = {}; 9 | this.formats.osmChange = OpenLayers.Format.OSCAugmented; 10 | this.formats.osm = OpenLayers.Format.OSMExt; 11 | this.formats.osmAugmentedDiff = OpenLayers.Format.OSCAugmentedDiff; 12 | this.formats.osmAugmentedDiff_IDSorted = OpenLayers.Format.OSCAugmentedDiffIDSorted; 13 | this.formats.osmChangeset = OpenLayers.Format.OSMChangeset; 14 | this.formats.osmDiff = OSMDiffFormat; // overpass.js, new augmented diff format 15 | } 16 | 17 | FormatRegistry.prototype.getFormat = function(doc) { 18 | var format = null; 19 | var formatType = this.getFormatType(doc); 20 | var formatClass = this.formats[formatType]; 21 | if (formatClass) { 22 | format = new formatClass(this.formatOptions); 23 | } else { 24 | console.error('unknown format "' + formatType + '"'); 25 | } 26 | 27 | return { 28 | type : formatType, 29 | format : format 30 | }; 31 | }; 32 | 33 | FormatRegistry.prototype.getFormatType = function(doc) { 34 | var type = doc.documentElement.nodeName; 35 | 36 | // special cases with common root node name but different content 37 | if (type === 'osm') { 38 | var generatorAttribute = doc.documentElement.getAttribute('generator'); 39 | if (generatorAttribute && generatorAttribute.indexOf('Overpass API') === 0) { 40 | type = 'osmDiff'; 41 | } else { 42 | var node = doc.documentElement.firstChild; 43 | while (node) { 44 | // changeset info file 45 | if (node.nodeName === 'changeset') { 46 | type = 'osmChangeset'; 47 | break; 48 | } 49 | node = node.nextSibling; 50 | } 51 | } 52 | } else if (type === 'osmAugmentedDiff') { 53 | var formatAttribute = doc.documentElement.getAttribute('format'); 54 | if (formatAttribute && formatAttribute === 'id-sorted') { 55 | type = 'osmAugmentedDiff_IDSorted'; 56 | } 57 | } 58 | 59 | return type; 60 | }; -------------------------------------------------------------------------------- /js/Loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * class Loader 3 | */ 4 | function Loader(map, layers, status) { 5 | this.map = map; 6 | this.layers = layers; 7 | this.status = status; 8 | 9 | // Maximum length of XHR responseText. Larger responses are likely to crash the browser, 10 | // either immediately or on subsequent requests with the same size. 11 | // Approximate limit determined by trial and error. 12 | this.responseSizeLimit = 50000000; 13 | 14 | var formatOptions = { 15 | internalProjection : map.getProjectionObject() 16 | }; 17 | this.formatRegistry = new FormatRegistry(formatOptions); 18 | 19 | // register global events to get handlers called with options parameter, 20 | // containing config and requestUrl instead of just request 21 | /* 22 | OpenLayers.Request.events.on({ 23 | success : success, 24 | failure : failure 25 | }); 26 | */ 27 | 28 | // Reuse XHR instance, seems to reduce memory overhead for subsequent, large requests. 29 | // Can handle only one request at a time. For now, reuse one instance only and 30 | // create new one when needed (see GET), later include library with instance pool. 31 | this.xhr = new XMLHttpRequest(); 32 | } 33 | 34 | Loader.prototype.handleLoad = function(doc, fileNameOrUrl, options) { 35 | 36 | if (typeof doc == "string") { 37 | doc = OpenLayers.Format.XML.prototype.read.apply(this, [ doc ]); 38 | } 39 | 40 | var desc = this.formatRegistry.getFormat(doc); 41 | var format = desc.format; 42 | var oscFeatures = []; 43 | var osmFeatures = []; 44 | var features = []; 45 | var feature; 46 | var state; 47 | var i = 0; 48 | var old = null; 49 | var filtered; 50 | var changesByFid = {} 51 | var changeset = options && options.changeset; 52 | 53 | console.timeEnd("xml"); 54 | if (format) { 55 | if (desc.type === 'osmChangeset') { 56 | var features = format.read(doc), 57 | changesetFeature = features[0]; 58 | cs = changesetFeature.attributes; 59 | 60 | // line instead of polygon, popup only on changeset boundary 61 | changesetFeature.geometry = new OpenLayers.Geometry.LineString( 62 | changesetFeature.geometry.components[0].components); 63 | this.layers.changesets.addFeatures(features); 64 | 65 | this.map.zoomToExtent(changesets.getDataExtent()); 66 | } else { 67 | console.time("read"); 68 | if (desc.type === 'osm') { 69 | osmFeatures = format.read(doc); 70 | 71 | // TODO sync separate file loading 72 | // oscFeatures = oscFormat.read(oscResponse, osmXml); 73 | } else if (desc.type === 'osmAugmentedDiff_IDSorted') { 74 | var obj = format.readAugmenting(doc); 75 | osmFeatures = obj.old; 76 | oscFeatures = obj.change; 77 | // YYYY-MM-DDTHH\:mm\:ssZ 78 | this.status.timestamp = null; 79 | if (obj.timestamp) { 80 | this.status.timestamp = moment(obj.timestamp, 'YYYY-MM-DDTHH[\\]:mm[\\]:ssZ').valueOf(); 81 | } 82 | console.log(obj.timestamp); 83 | } else if (desc.type === 'osmDiff') { 84 | var remarks = doc.getElementsByTagName('remark') 85 | if (remarks && remarks.length) { 86 | var remark = remarks[0].textContent; 87 | if (remark.match(/runtime error/)) { 88 | this.failure({ 89 | config: {}, 90 | requestUrl: desc.type, 91 | request: { 92 | status: 'error', 93 | statusText: remark 94 | } 95 | }); 96 | alert(desc.type + ': '+ remark); 97 | } 98 | } 99 | format.extent = null; 100 | features = format.read(doc); 101 | 102 | // fid hash, build first because of feature order (nodes after ways) 103 | for (i = 0; i < features.length; i++) { 104 | feature = features[i]; 105 | if (feature.attributes.state === 'new') { 106 | changesByFid[feature.fid] = feature; 107 | } 108 | } 109 | 110 | for (i = 0; i < features.length; i++) { 111 | feature = features[i]; 112 | state = feature.attributes.state; 113 | 114 | // adjust new format to old, for now 115 | feature.attributes = feature.tags; 116 | feature.attributes.state = state; 117 | feature.attributes.action = feature.action; 118 | 119 | // pair old and new feature for filtering 120 | // assume features ordered in old/new pairs (or old or new only) 121 | 122 | if (state === 'old') { 123 | // don't add now, keep to pair with new for filtering 124 | old = feature; 125 | } else { 126 | filtered = true; 127 | 128 | // omit entities with no changes in scope, e.g. ways and relations reported 129 | // as modified but geometry unchanged inside bbox (node/member changed outside) 130 | if (old && !oscviewer.isChanged(old, feature)) { 131 | filtered = false; 132 | } 133 | 134 | // filter by changeset when loading a changeset based on time range and bbox 135 | if (filtered && changeset && !( 136 | feature.attributes.changeset === changeset 137 | // modified way/relation where only node or member geometry changed 138 | || (old && old.attributes.version === feature.attributes.version 139 | && (feature.type === 'way' && this.isWayChangeInChangeset(changesByFid, feature, changeset) 140 | || feature.type === 'relation' /* TODO this.isRelationChangeInChangeset */)) 141 | )) { 142 | filtered = false; 143 | //console.log('===== changeset filtered out: ' + feature.fid + ' (' + feature.attributes.changeset + ' != ' + changeset + ') ====='); 144 | } 145 | 146 | if (filtered) { 147 | if (old) { 148 | osmFeatures.push(old); 149 | } 150 | oscFeatures.push(feature); 151 | } 152 | old = null; 153 | } 154 | } 155 | console.log('osmDiff old: ' + osmFeatures.length + ', new: ' + oscFeatures.length); 156 | this.status.timestamp = this.getOsmBaseTimestamp(doc); 157 | } else { 158 | console.warn('deprecated diff format returned'); 159 | if (format.isAugmented(doc)) { 160 | osmFeatures = format.readAugmenting(doc); 161 | } 162 | oscFeatures = format.read(doc); 163 | } 164 | console.timeEnd("read"); 165 | 166 | console.time("setActions"); 167 | oscviewer.setActions(oscFeatures, osmFeatures); 168 | console.timeEnd("setActions"); 169 | 170 | console.time("addFeatures"); 171 | this.layers.old.addFeatures(osmFeatures); 172 | this.layers.changes.addFeatures(oscFeatures); 173 | console.timeEnd("addFeatures"); 174 | 175 | this.status.addChanges(oscFeatures.length); 176 | console.log('features added: changes = ' + oscFeatures.length + ', old = ' + osmFeatures.length 177 | + ' - total: changes = ' + this.layers.changes.features.length + ', old = ' + this.layers.old.features.length); 178 | 179 | // Chrome memory debugging, requires "--enable-memory-info" command-line argument 180 | if (performance && performance.memory) { 181 | var m = performance.memory; 182 | console.log('memory: used=' + m.usedJSHeapSize + ', total=' + m.totalJSHeapSize + ', limit=' + m.jsHeapSizeLimit); 183 | } 184 | 185 | if (!(options && options.zoomToExtent === false)) { 186 | this.map.zoomToExtent(this.layers.changes.getDataExtent()); 187 | } 188 | } 189 | } 190 | }; 191 | 192 | Loader.prototype.isWayChangeInChangeset = function(changesByFid, change, changeset) { 193 | var result = false, 194 | points = change.geometry.components, 195 | i, 196 | node; 197 | 198 | for (i = 0; i < points.length; i++) { 199 | node = changesByFid['node.' + points[i].ref]; 200 | if (node && node.tags.changeset === changeset) { 201 | result = true; 202 | break; 203 | } 204 | } 205 | /* 206 | if (!result) { 207 | console.log('----- ' + change.fid + ' change not in changeset -----'); 208 | } 209 | */ 210 | return result; 211 | } 212 | 213 | Loader.prototype.getOsmBaseTimestamp = function(doc) { 214 | var timestamp = null, 215 | osmBase; 216 | var metaList = doc.getElementsByTagName("meta"); 217 | if (metaList && metaList.length > 0) { 218 | osmBase = metaList[0].getAttribute('osm_base'); 219 | if (osmBase) { 220 | timestamp = moment(osmBase, 'YYYY-MM-DDTHH[\\]:mm[\\]:ssZ').valueOf(); 221 | } 222 | } 223 | return timestamp; 224 | }, 225 | 226 | // options - {Object} Hash containing request, config and requestUrl keys 227 | Loader.prototype.success = function(options) { 228 | var response; 229 | // only handle when no success handler defined for this request 230 | if (!options.config.success) { 231 | console.timeEnd("request"); 232 | console.time("xml"); 233 | var request = options.request; 234 | var requestUrl = options.requestUrl; 235 | // console.log('headers: ' request.getAllResponseHeaders()); 236 | var len = request.responseText.length; 237 | if (len > 302) { 238 | console.log('size: ' + len); 239 | } 240 | response = request.responseXML || request.responseText; 241 | if (response) { 242 | this.handleLoad(response, requestUrl, options.config); 243 | } else { 244 | console.error('empty response for "' + requestUrl + '" (' + request.status + ' ' + request.statusText 245 | + ')'); 246 | } 247 | } 248 | }; 249 | 250 | // options - {Object} Hash containing request, config and requestUrl keys 251 | Loader.prototype.failure = function(options) { 252 | // only handle when no failure handler defined for this request 253 | if (!options.config.failure) { 254 | var request = options.request; 255 | var requestUrl = options.requestUrl; 256 | console.error('error loading "' + requestUrl + '" (' + request.status + ' ' + request.statusText + ')'); 257 | this.status.errors++; 258 | } 259 | }; 260 | 261 | //options - {Object} Hash containing request, config and requestUrl keys 262 | Loader.prototype.postLoad = function(options) { 263 | if (options.config.postLoadCallback) { 264 | options.config.postLoadCallback(); 265 | } 266 | }; 267 | 268 | Loader.prototype.GET = function(config) { 269 | console.log("requesting " + config.url); 270 | console.time("request"); 271 | 272 | //OpenLayers.Util.applyDefaults(config, {success: success, failure: failure}); 273 | //OpenLayers.Request.GET(config); 274 | 275 | var url = config.url; 276 | 277 | /* 278 | //xhr.onprogress = (function () { 279 | xhr.onreadystatechange = (function () { 280 | var xhrDebug = new XHRDebug(); 281 | return function(evt) { 282 | xhrDebug.log(this); 283 | }; 284 | })(); 285 | */ 286 | 287 | var xhr = this.xhr; 288 | 289 | // If request instance is active (state not UNSENT or DONE) 290 | if (xhr.readyState != 0 && xhr.readyState != 4) { 291 | // create an additional temp. instance (reusing active instance would abort running request). 292 | // Needed when both live and forward/backward controls are active. 293 | xhr = new XMLHttpRequest(); 294 | } 295 | 296 | xhr.open('GET', url); 297 | 298 | var self = this; 299 | xhr.onload = function(evt) { 300 | self.success({request: evt.target, config: config, requestUrl: config.url}); 301 | }; 302 | xhr.onprogress = function(evt) { 303 | if (this.readyState == 3) { 304 | // Abort loading of Request response when length of responseText > limit, in order 305 | // to avoid Browser crash. evt.total not set in our case (Overpass API). 306 | if (evt.loaded > self.responseSizeLimit) { 307 | console.error('abort response loading, size limit exceeded (' + self.responseSizeLimit + '), url = ' + config.url); 308 | self.status.errors++; 309 | this.abort(); 310 | } 311 | } 312 | }; 313 | xhr.onerror = function(evt) { 314 | self.failure({request: evt.target, config: config, requestUrl: config.url}); 315 | }; 316 | xhr.onloadend = function(evt) { 317 | self.postLoad({request: evt.target, config: config, requestUrl: config.url}); 318 | }; 319 | 320 | // text only, XML parsing done later for better control over memory issues 321 | xhr.overrideMimeType("text/plain"); 322 | xhr.send(); 323 | return xhr; 324 | }; 325 | -------------------------------------------------------------------------------- /js/OSMChangeStyle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * class OSMChangeStyle 3 | */ 4 | function OSMChangeStyle(map) { 5 | this.map = map; 6 | } 7 | 8 | OSMChangeStyle.prototype.getStyleMaps = function() { 9 | var styleMaps = {}; 10 | 11 | var context = { 12 | getZIndex : function(feature) { 13 | var geometryType = feature.geometry.CLASS_NAME.substr(20); 14 | switch (geometryType) { 15 | case 'Polygon': 16 | if (feature.attributes.landuse) 17 | return 10; 18 | else 19 | return 20; 20 | case 'LineString': 21 | return 30; 22 | case 'Point': 23 | return 40; 24 | default: 25 | return 0; 26 | } 27 | } 28 | }; 29 | 30 | // default style 31 | var defaultStyle = new OpenLayers.Style({ 32 | graphicZIndex: '${getZIndex}', 33 | cursor: 'pointer', 34 | 35 | //dummy values to test for unmatched features 36 | strokeColor : "#FF00FF", 37 | strokeWidth : 4, 38 | strokeOpacity : 1.0, 39 | fillColor : 'orange' 40 | }, { 41 | context : context 42 | }); 43 | var selectStyle = new OpenLayers.Style({ 44 | graphicZIndex: '${getZIndex}', 45 | 46 | //dummy values to test for unmatched features 47 | strokeColor : "#FF00FF", 48 | strokeWidth: 4, 49 | strokeOpacity : 1.0 50 | }, { 51 | context : context 52 | }); 53 | styleMaps.changes = new OpenLayers.StyleMap({ 54 | "default" : defaultStyle, 55 | "select" : selectStyle 56 | }); 57 | styleMaps.old = new OpenLayers.StyleMap({ 58 | "default" : defaultStyle.clone(), 59 | "select" : selectStyle.clone() 60 | }); 61 | 62 | // context 63 | var ruleContext = function(feature) { 64 | var ctx = OpenLayers.Util.applyDefaults({}, feature.attributes); 65 | OpenLayers.Util.applyDefaults(ctx , { 66 | hasTags: (feature.hasTags ? 'true' : 'false'), 67 | osmType: oscviewer.getOsmType(feature), 68 | used: (feature.used ? 'true' : 'false') 69 | }); 70 | return ctx; 71 | }; 72 | 73 | // util 74 | var cqlFormat = new OpenLayers.Format.CQL(); 75 | var rule = function(cqlFilter, options) { 76 | OpenLayers.Util.applyDefaults(options, { 77 | filter: cqlFormat.read(cqlFilter), 78 | context: ruleContext 79 | }); 80 | return new OpenLayers.Rule(options); 81 | }; 82 | 83 | var ruleEqualTo = function(property, value, options) { 84 | OpenLayers.Util.applyDefaults(options, { 85 | filter: new OpenLayers.Filter.Comparison({ 86 | type: OpenLayers.Filter.Comparison.EQUAL_TO, 87 | property: property, 88 | value: value 89 | }), 90 | context: ruleContext 91 | }); 92 | return new OpenLayers.Rule(options); 93 | }; 94 | 95 | // var 96 | getScaleFromZoom = function(zoom) { 97 | var res = map.getResolutionForZoom(zoom); 98 | var units = map.getUnits(); 99 | var scale = OpenLayers.Util.getScaleFromResolution(res, units); 100 | return scale; 101 | }; 102 | 103 | // rules 104 | var rules = []; 105 | 106 | rules.push(rule("osmType = 'changeset'", { 107 | symbolizer: { 108 | strokeColor: '#ee9900', 109 | strokeWidth: 4, 110 | strokeOpacity : 0.3 111 | } 112 | })); 113 | 114 | rules.push(rule("osmType = 'relation'", { 115 | symbolizer: { 116 | strokeWidth: 10, 117 | strokeOpacity : 0.4, 118 | fillOpacity : 0.1 119 | } 120 | })); 121 | rules.push(rule("osmType = 'relation'", { 122 | symbolizer: { 123 | strokeWidth: 6, 124 | strokeOpacity : 0.3 125 | }, 126 | minScaleDenominator: getScaleFromZoom(18) 127 | })); 128 | rules.push(rule("osmType = 'relation'", { 129 | symbolizer: { 130 | strokeWidth: 4, 131 | strokeOpacity : 0.2 132 | }, 133 | minScaleDenominator: getScaleFromZoom(15) 134 | })); 135 | 136 | rules.push(rule("osmType = 'way'", { 137 | symbolizer: { 138 | strokeWidth: 2, 139 | strokeOpacity : 1.0, 140 | fillOpacity : 0.2 141 | } 142 | })); 143 | rules.push(rule("osmType = 'way'", { 144 | symbolizer: { 145 | strokeWidth: 1, 146 | fillOpacity : 0.1 147 | }, 148 | minScaleDenominator: getScaleFromZoom(15) 149 | })); 150 | rules.push(rule("osmType = 'way'", { 151 | symbolizer: { 152 | strokeWidth: 1.5, 153 | fillOpacity : 0.1 154 | }, 155 | minScaleDenominator: getScaleFromZoom(10) 156 | })); 157 | 158 | var wayNodeSymbolizer = { 159 | graphicName: "square", 160 | pointRadius: 3, 161 | fillOpacity: 1, 162 | strokeWidth: 1, 163 | strokeColor: 'black' // '#666' 'rgba(255, 255, 255, 0.4)' 164 | }; 165 | rules.push(rule("osmType = 'node' AND hasTags = 'false'", { 166 | symbolizer: wayNodeSymbolizer 167 | })); 168 | rules.push(rule("osmType = 'node' AND hasTags = 'false'", { 169 | symbolizer: { 170 | pointRadius: 2, 171 | strokeWidth: 0.5 172 | }, 173 | minScaleDenominator: getScaleFromZoom(17) 174 | })); 175 | rules.push(rule("osmType = 'node' AND hasTags = 'false' AND used = 'false'", { 176 | symbolizer: { 177 | pointRadius: 1, 178 | strokeWidth: 0 179 | }, 180 | minScaleDenominator: getScaleFromZoom(15) 181 | })); 182 | // TODO Format.OSM does not set 'used' flag! 183 | rules.push(rule("osmType = 'node' AND hasTags = 'false' AND used = 'true'", { 184 | symbolizer: { 185 | display: 'none' 186 | }, 187 | minScaleDenominator: getScaleFromZoom(15) 188 | })); 189 | 190 | var oldModifyLowZoom = ruleEqualTo("action", "modify:geometry", { 191 | symbolizer: { 192 | display: 'none' 193 | }, 194 | minScaleDenominator: getScaleFromZoom(13) 195 | }); 196 | 197 | rules.push(rule("osmType = 'node' AND hasTags = 'true'", { 198 | symbolizer: { 199 | graphicName: "circle", 200 | pointRadius: 4, 201 | fillOpacity: 1, 202 | strokeWidth: 1, 203 | strokeColor: 'black' 204 | } 205 | })); 206 | rules.push(rule("osmType = 'node' AND hasTags = 'true'", { 207 | symbolizer: { 208 | pointRadius: 3, 209 | strokeWidth: 0.5 210 | }, 211 | minScaleDenominator: getScaleFromZoom(15) 212 | })); 213 | rules.push(rule("osmType = 'node' AND hasTags = 'true'", { 214 | symbolizer: { 215 | pointRadius: 2 216 | }, 217 | minScaleDenominator: getScaleFromZoom(12) 218 | })); 219 | rules.push(rule("osmType = 'node' AND hasTags = 'true'", { 220 | symbolizer: { 221 | pointRadius: 1 222 | }, 223 | minScaleDenominator: getScaleFromZoom(8) 224 | })); 225 | 226 | // select rules 227 | var rulesSelect = []; 228 | 229 | rulesSelect.push(rule("osmType = 'changeset'", { 230 | symbolizer: { 231 | strokeOpacity : 0.8 232 | } 233 | })); 234 | 235 | rulesSelect.push(rule("osmType = 'relation'", { 236 | symbolizer: { 237 | strokeOpacity : 0.8, 238 | strokeColor: 'white' 239 | } 240 | })); 241 | 242 | rulesSelect.push(rule("osmType = 'way'", { 243 | symbolizer: { 244 | strokeWidth: 3, 245 | strokeColor: 'white' 246 | } 247 | })); 248 | rulesSelect.push(rule("osmType = 'way'", { 249 | symbolizer: { 250 | strokeWidth: 2 251 | }, 252 | minScaleDenominator: getScaleFromZoom(15) 253 | })); 254 | 255 | rulesSelect.push(rule("osmType = 'node'", { 256 | symbolizer: { 257 | strokeWidth: 1.5, 258 | strokeColor: 'white' 259 | } 260 | })); 261 | 262 | // unique value rules (action) 263 | 264 | // changes 265 | var actionRules = { 266 | "create" : { 267 | strokeColor : '#FAF797', 268 | fillColor : '#FAF797' 269 | }, 270 | "modify" : { 271 | strokeColor : 'lightskyblue', 272 | fillColor : 'lightskyblue' 273 | }, 274 | "modify:geometry" : { 275 | strokeColor : 'lightgreen', 276 | fillColor : 'lightgreen' 277 | }, 278 | "delete" : { 279 | display : 'none' 280 | } 281 | }; 282 | styleMaps.changes.addUniqueValueRules("default", "action", actionRules); 283 | 284 | // old 285 | var osmActionRules = { 286 | "create" : { 287 | display : 'none' 288 | }, 289 | "modify" : { 290 | display : 'none' 291 | }, 292 | "modify:geometry" : { 293 | strokeColor : 'darkred', 294 | fillColor : 'darkred' 295 | //pointRadius : '${getGraphicRadiusOld}' 296 | }, 297 | "delete" : { 298 | strokeColor : '#FF3333', 299 | fillColor : '#FF3333' 300 | }, 301 | "augment" : { 302 | display : 'none' 303 | } 304 | }; 305 | styleMaps.old.addUniqueValueRules("default", "action", osmActionRules); 306 | 307 | // after UniqueValueRules because of strokeColor nodes 308 | styleMaps.changes.styles['default'].addRules(rules); 309 | styleMaps.old.styles['default'].addRules(rules); 310 | styleMaps.old.styles['default'].addRules([oldModifyLowZoom]); 311 | styleMaps.changes.styles['select'].addRules(rules); 312 | styleMaps.old.styles['select'].addRules(rules); 313 | styleMaps.changes.styles['select'].addRules(rulesSelect); 314 | styleMaps.old.styles['select'].addRules(rulesSelect); 315 | 316 | return styleMaps; 317 | }; 318 | -------------------------------------------------------------------------------- /js/OverpassAPI.js: -------------------------------------------------------------------------------- 1 | /** 2 | * class OverpassAPI 3 | */ 4 | function OverpassAPI(loader, bboxControl) { 5 | this.loader = loader; 6 | this.bboxControl = bboxControl; 7 | this.bbox = null; 8 | 9 | // http://www.overpass-api.de/augmented_diffs/000/008/066.osc.gz 10 | // http://www.overpass-api.de/augmented_diffs/id_sorted/000/028/706.osc.gz 11 | this.sequenceUrlRegex = /.*overpass-api\.de\/augmented_diffs(?:\/id_sorted|)\/([0-9]{3})\/([0-9]{3})\/([0-9]{3}).osc.gz/; 12 | } 13 | 14 | OverpassAPI.prototype.getSequenceUrl = function(sequence) { 15 | var s = sequence.toString(); 16 | s = "000000000".substring(0, 9 - s.length) + s; 17 | var path = { 18 | a : s.substring(0, 3), 19 | b : s.substring(3, 6), 20 | c : s.substring(6, 9) 21 | }; 22 | //var urlFormat = 'http://overpass-api.de/augmented_diffs/${a}/${b}/${c}.osc.gz'; 23 | var urlFormat = 'https://overpass-api.de/augmented_diffs/id_sorted/${a}/${b}/${c}.osc.gz'; 24 | 25 | var url = OpenLayers.String.format(urlFormat, path); 26 | return url; 27 | }; 28 | 29 | OverpassAPI.prototype.parseSequence = function (request, url) { 30 | var sequence = -1; 31 | var response = request.responseText; 32 | if (response) { 33 | sequence = parseInt(response); 34 | } else { 35 | console.error('empty response for "' + url + '" (' + request.status + ' ' 36 | + request.statusText + ')'); 37 | } 38 | return sequence; 39 | }; 40 | 41 | OverpassAPI.prototype.getCurrentSequence = function () { 42 | var sequence = -1; 43 | var url = "https://overpass-api.de/augmented_diffs/state.txt"; 44 | 45 | OpenLayers.Request.GET({ 46 | url: url, 47 | async: false, 48 | // do not send X-Requested-With header (option added by olex.Request-patch) 49 | disableXRequestedWith: true, 50 | success: _.bind(function(request) { 51 | sequence = this.parseSequence(request, url); 52 | }, this) 53 | }); 54 | return sequence; 55 | }; 56 | 57 | OverpassAPI.prototype.getSequenceByTime = function (timestamp, callback) { 58 | var osmBase = moment.utc(timestamp).format('YYYY-MM-DDTHH[\\]:mm[\\]:ss\\Z'); 59 | console.log('load time: ' + osmBase); 60 | var url = 'https://overpass-api.de/api/augmented_state_by_date?osm_base=' + osmBase; 61 | console.log('requesting state ' + url); 62 | OpenLayers.Request.GET({ 63 | url: url, 64 | async: true, 65 | disableXRequestedWith: true, 66 | success: _.bind(function(request) { 67 | var sequence = this.parseSequence(request, url); 68 | callback(sequence); 69 | }, this) 70 | }); 71 | }; 72 | 73 | OverpassAPI.prototype.getSequenceFromUrl = function (url) { 74 | return parseFloat(url.replace(this.sequenceUrlRegex, "$1$2$3")); 75 | }; 76 | 77 | OverpassAPI.prototype.loadByUrl = function(url) { 78 | var sequence = this.getSequenceFromUrl(url); 79 | this.load(sequence); 80 | }; 81 | 82 | OverpassAPI.prototype.load = function(sequence, postLoadCallback) { 83 | var bboxParam; 84 | if (sequence && sequence >= 0) { 85 | var url = "https://overpass-api.de/api/augmented_diff?id=" + sequence + "&info=no"; 86 | //var url = getSequenceUrl(sequence); 87 | if (!this.bbox) { 88 | this.bbox = this.bboxControl.addBBoxFromViewPort(); 89 | } 90 | bboxParam = OpenLayers.String.format('&bbox=${left},${bottom},${right},${top}', this.bbox); 91 | //console.log("box = " + bboxParam); 92 | url += bboxParam; 93 | this.loader.GET({ 94 | url: url, 95 | // do not zoom to data extent after load; option forwarded to load handler 96 | // (option only forwarded when using success event instead of callback) 97 | zoomToExtent: false, 98 | // do not send X-Requested-With header (option added by olex.Request-patch) 99 | disableXRequestedWith: true, 100 | postLoadCallback: postLoadCallback 101 | }); 102 | } else { 103 | console.log('invalid sequence: "' + sequence + '"'); 104 | } 105 | }; 106 | 107 | OverpassAPI.prototype.loadDiff = function(from, to, relations, query, postLoadCallback, changeset) { 108 | var mindate = moment.utc(from).format('YYYY-MM-DDTHH:mm:ss\\Z'), 109 | maxdate = to ? moment.utc(to).format('YYYY-MM-DDTHH:mm:ss\\Z') : '', 110 | bboxParam, 111 | url, 112 | xhr, 113 | dateRange; 114 | 115 | if (maxdate) { 116 | maxdate = ',"' + maxdate + '"'; 117 | } 118 | dateRange = '"' + mindate + '"' + maxdate; 119 | 120 | var data_url = 'https://overpass-api.de/api/interpreter'; 121 | if(!query) { 122 | query = '(node(bbox)(changed);way(bbox)(changed);' + (relations ? 'relation(bbox)(changed);' : '') + ');'; 123 | } 124 | query = query.replace(/(\r\n|\n|\r)/gm,""); 125 | query = query.replace(/\{\{bbox\}\}/gm,"bbox"); 126 | url = data_url + '?data=[adiff:' + dateRange 127 | + '];' + query + 'out meta geom(bbox);'; 128 | 129 | if (!this.bbox) { 130 | this.bbox = this.bboxControl.addBBoxFromViewPort(); 131 | } 132 | bboxParam = OpenLayers.String.format('&bbox=${left},${bottom},${right},${top}', this.bbox); 133 | url += bboxParam; 134 | 135 | xhr = this.loader.GET({ 136 | url: url, 137 | // do not zoom to data extent after load; option forwarded to load handler 138 | // (option only forwarded when using success event instead of callback) 139 | zoomToExtent: false, 140 | // do not send X-Requested-With header (option added by olex.Request-patch) 141 | disableXRequestedWith: true, 142 | postLoadCallback: postLoadCallback, 143 | changeset: changeset 144 | }); 145 | return xhr; 146 | }; 147 | -------------------------------------------------------------------------------- /js/PopupHandler.js: -------------------------------------------------------------------------------- 1 | function PopupHandler(map, old, changes) { 2 | 3 | this.selectedFeature = null; 4 | this.popup = null; 5 | 6 | this.onPopupClose = function(evt) { 7 | // TODO reference to HoverAndSelectFeature needed 8 | hover.unselect(selectedFeature); 9 | }; 10 | 11 | this.getOldFeature = function(changeFeature) { 12 | var oldFeature; 13 | if (changeFeature.hasOwnProperty('oldFeature')) { 14 | oldFeature = changeFeature.oldFeature; 15 | } else { 16 | oldFeature = old.getFeatureByFid(changeFeature.fid); 17 | } 18 | return oldFeature; 19 | }; 20 | 21 | this.getChangeFeature = function(oldFeature) { 22 | var changeFeature; 23 | if (oldFeature.hasOwnProperty('changeFeature')) { 24 | changeFeature = oldFeature.changeFeature; 25 | } else { 26 | changeFeature = changes.getFeatureByFid(oldFeature.fid); 27 | } 28 | return changeFeature; 29 | }; 30 | 31 | this.onFeatureSelect = function(feature, mouseXy, hover) { 32 | var oldFeature, changeFeature; 33 | var infoHtml; 34 | 35 | selectedFeature = feature; 36 | 37 | if (feature.layer === changes) { 38 | changeFeature = feature; 39 | oldFeature = this.getOldFeature(changeFeature); 40 | } else { 41 | oldFeature = feature; 42 | changeFeature = this.getChangeFeature(oldFeature); 43 | } 44 | 45 | infoHtml = oscviewer.getInfoHtml(oldFeature, changeFeature); 46 | 47 | //OpenLayers.Popup.prototype.displayClass = "border"; 48 | //OpenLayers.Popup.prototype.contentDisplayClass = "info"; 49 | // use mouse position to place popup; using geometry center does nor really work for long ways 50 | //feature.geometry.getBounds().getCenterLonLat() 51 | popup = new OpenLayers.Popup.Anchored("popup", map.getLonLatFromViewPortPx(mouseXy), null, infoHtml, 52 | { 53 | size : new OpenLayers.Size(0, 0), 54 | offset : new OpenLayers.Pixel(0, 0) 55 | }, false /*!hover*/, this.onPopupClose); 56 | popup.autoSize = true; 57 | 58 | // set CSS defined values, as OL keeps overwriting them 59 | popup.setBackgroundColor(popup.div.style.backgroundColor); 60 | popup.setOpacity(popup.div.style.opacity); 61 | popup.setBorder(popup.div.style.border); 62 | 63 | // prevent popup flickering when mouse is hovering both over feature and popup div 64 | if (hover) { 65 | popup.div.style['pointer-events'] = 'none'; 66 | } 67 | 68 | feature.popup = popup; 69 | map.addPopup(popup); 70 | 71 | oscviewer.attachInfoHtmlListeners(); 72 | }; 73 | 74 | this.onFeatureUnselect = function(feature) { 75 | map.removePopup(feature.popup); 76 | feature.popup.destroy(); 77 | feature.popup = null; 78 | }; 79 | } -------------------------------------------------------------------------------- /js/XHRDebug.js: -------------------------------------------------------------------------------- 1 | /** 2 | * class XHRDebug 3 | */ 4 | function XHRDebug() { 5 | this.oldReadyState = null; 6 | this.oldTime = +new Date(); 7 | } 8 | 9 | XHRDebug.prototype.log = function(request) { 10 | var states = ['UNSENT', 'OPENED', 'HEADERS_RECEIVED', 'LOADING', 'DONE']; 11 | var curTime, len; 12 | var readyState = request.readyState; 13 | 14 | if (readyState != this.oldReadyState) { 15 | console.log(states[readyState]); 16 | this.oldReadyState = readyState; 17 | } 18 | 19 | if (readyState == request.HEADERS_RECEIVED) { 20 | this.oldTime = +new Date(); 21 | //console.log('headers = ' + request.getAllResponseHeaders()); 22 | } else if (readyState == request.LOADING){ 23 | curTime = +new Date(); 24 | if ((curTime - this.oldTime) > 500) { 25 | len = request.responseText.length; 26 | console.log(len); 27 | this.oldTime = curTime; 28 | } 29 | } else if(readyState == request.DONE) { 30 | len = request.responseText.length; 31 | if (len > 302) { 32 | console.log(len); 33 | } 34 | } 35 | }; -------------------------------------------------------------------------------- /js/control/Diff.js: -------------------------------------------------------------------------------- 1 | function Diff(overpassAPI, loading, status) { 2 | this.overpassAPI = overpassAPI; 3 | this.loading = loading; 4 | this.status = status; 5 | 6 | this.element = null; 7 | 8 | this.eleFromDatetime = document.getElementById('fromDatetime'); 9 | this.eleToDatetime = document.getElementById('toDatetime'); 10 | this.eleRelations = document.getElementById('relations'); 11 | this.eleQuery = document.getElementById('query'); 12 | 13 | // disable feature on prod until issue #34 is resolved (Custom query #18 follow-up) 14 | if (document.location.hostname !== "overpass-api.de") { 15 | this.eleQuery.parentElement.hidden = false; 16 | } 17 | 18 | this.lastVisit = this.getLastVisit(); 19 | if (this.lastVisit) { 20 | this.setDateTimeToLastVisit(); 21 | //console.log('last visit: ' + moment(this.lastVisit).format("YYYY-MM-DD HH:mm:ss")); 22 | } else { 23 | this.setDateTime(moment().subtract('days', 1)); 24 | } 25 | 26 | this.loadButton = document.getElementById('load_button'); 27 | this.loadButton.onclick = _.bind(this.load, this); 28 | } 29 | 30 | Diff.prototype.getLastVisit = function() { 31 | var lastVisitItem, 32 | lastVisit = null; 33 | 34 | try { 35 | lastVisitItem = localStorage.getItem(Status.STORAGE_KEY_LAST_VISIT); 36 | } catch (err) { 37 | console.warn('Failed to read last visit from localStorage: ' + err.message); 38 | } 39 | 40 | if (lastVisitItem) { 41 | lastVisit = parseInt(lastVisitItem); 42 | } 43 | return lastVisit; 44 | }; 45 | 46 | Diff.prototype.setDateTime = function(dateTime) { 47 | this.eleFromDatetime.value = oscviewer.formatIsoDateTime(dateTime); 48 | }; 49 | 50 | Diff.prototype.setDateTimeToLastVisit = function() { 51 | this.setDateTime(this.lastVisit); 52 | }; 53 | 54 | Diff.prototype.setDateTimeToNow = function() { 55 | this.setDateTime(Date.now()); 56 | }; 57 | 58 | Diff.prototype.getTime = function(ele) { 59 | if (!ele.value) { 60 | return null; 61 | } 62 | return moment(ele.value, 'YYYY-MM-DD HH:mm').valueOf(); 63 | }; 64 | 65 | Diff.prototype.load = function() { 66 | var from = this.getTime(this.eleFromDatetime), 67 | to = this.getTime(this.eleToDatetime), 68 | query = this.eleQuery.value, 69 | relations = this.eleRelations.checked, 70 | xhr; 71 | xhr = this.overpassAPI.loadDiff(from, to, relations, query, _.bind(this.postLoad, this)); 72 | this.loading.loadStart(xhr); 73 | this.loadButton.classList.add('button_disabled'); 74 | }; 75 | 76 | Diff.prototype.postLoad = function() { 77 | this.loading.loadEnd(); 78 | this.loadButton.classList.remove('button_disabled'); 79 | 80 | // remember osm_base date as last visit when request was up to now (to empty) 81 | if (!this.eleToDatetime.value && this.status.timestamp) { 82 | try { 83 | localStorage.setItem(Status.STORAGE_KEY_LAST_VISIT, this.status.timestamp); 84 | } catch (err) { 85 | console.warn('Failed to write last visit to localStorage: ' + err.message); 86 | } 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /js/control/Live.js: -------------------------------------------------------------------------------- 1 | /** 2 | * class Live 3 | */ 4 | function Live(overpassAPI, status) { 5 | this.overpassAPI = overpassAPI; 6 | this.status = status; 7 | 8 | this.interval = null; 9 | this.sequence = -1; 10 | this.nextLoadTime = null; 11 | 12 | this.retryDelay = 30000; // 30 sec. 13 | this.catchUpDelay = 2000; // 2 sec. 14 | 15 | this.element = document.getElementById('live_button'); 16 | this.element.onclick = _.bind(this.toggle, this); 17 | } 18 | 19 | Live.prototype.calcNextLoadTime = function() { 20 | var m = moment(this.nextLoadTime).add('minutes', 1); 21 | return m.valueOf(); 22 | }; 23 | 24 | Live.prototype.load = function() { 25 | this.status.loadStart(); 26 | var currentSequence = this.overpassAPI.getCurrentSequence(); 27 | if (currentSequence && currentSequence >= 0) { 28 | if (this.sequence === -1) { 29 | this.nextLoadTime = this.calcNextLoadTime(); 30 | this.sequence = currentSequence; 31 | this.overpassAPI.load(this.sequence, _.bind(this.postLoad, this)); 32 | } else { 33 | if (currentSequence > this.sequence){ 34 | if (currentSequence - this.sequence > 1) { 35 | // shorter delay to catch up if more than one diff behind 36 | this.nextLoadTime = Date.now() + this.catchUpDelay; 37 | } else { 38 | this.nextLoadTime = this.calcNextLoadTime(); 39 | } 40 | this.sequence++; 41 | this.overpassAPI.load(this.sequence, _.bind(this.postLoad, this)); 42 | } else { 43 | this.status.loadEnd(); 44 | this.status.setCountdown('x'); 45 | this.nextLoadTime += this.retryDelay; 46 | console.log('skip refresh: sequence = ' + this.sequence + ', next retry: ' + moment(this.nextLoadTime).format("HH:mm:ss")); 47 | } 48 | } 49 | } 50 | }; 51 | 52 | Live.prototype.postLoad = function() { 53 | this.status.loadEnd(); 54 | this.status.sequence = this.sequence; 55 | this.status.count++; 56 | this.status.update(); 57 | }; 58 | 59 | Live.prototype.tick = function() { 60 | if (this.nextLoadTime <= Date.now()) { 61 | this.load(); 62 | } else { 63 | this.status.setCountdown(moment(this.nextLoadTime).diff(moment(), 'seconds') + 1); 64 | } 65 | }; 66 | 67 | Live.prototype.toggle = function(e) { 68 | this.element.classList.toggle('button_active'); 69 | if (!this.interval) { 70 | this.nextLoadTime = Date.now(); 71 | this.load(); 72 | this.interval = window.setInterval(_.bind(this.tick, this), 1000); 73 | } else { 74 | window.clearInterval(this.interval); 75 | this.interval = null; 76 | this.sequence = -1; 77 | this.status.setCountdown(null); 78 | console.log('live stopped'); 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /js/control/Loading.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Loading panel 3 | */ 4 | function Loading() { 5 | document.getElementById('cancel_button').onclick = _.bind(this.cancel, this); 6 | } 7 | 8 | Loading.prototype.cancel = function() { 9 | this.xhr.abort(); 10 | }; 11 | 12 | Loading.prototype.loadStart = function(xhr) { 13 | this.xhr = xhr; 14 | document.getElementById('loading').classList.remove('hidden'); 15 | document.getElementById('spinner').classList.add('spinner'); 16 | }; 17 | 18 | Loading.prototype.loadEnd = function() { 19 | document.getElementById('spinner').classList.remove('spinner'); 20 | document.getElementById('loading').classList.add('hidden'); 21 | this.xhr = null; 22 | }; 23 | -------------------------------------------------------------------------------- /js/control/Player.js: -------------------------------------------------------------------------------- 1 | /** 2 | * class Player 3 | */ 4 | function Player(overpassAPI, status) { 5 | this.overpassAPI = overpassAPI; 6 | this.status = status; 7 | 8 | this.interval = null; 9 | this.sequence = -1; 10 | this.active = false; 11 | 12 | // number of sequences (~ 1 per minute) 13 | this.limit = 60 * 24; // 24h 14 | 15 | this.stopSequence = null; 16 | this.currentSequence = null; 17 | 18 | this.element = null; 19 | this.mode = null; 20 | 21 | var add = function(a, b) { 22 | return a + b; 23 | }; 24 | var subtract = function(a, b) { 25 | return a - b; 26 | }; 27 | 28 | var modes = { 29 | 'fast_backward_button': { operation: subtract, limit: this.limit }, 30 | 'fast_forward_button': { operation: add, limit: this.limit }, 31 | 'backward_button': { operation: subtract, limit: 1 }, 32 | 'forward_button': { operation: add, limit: 1 } 33 | }; 34 | 35 | for (id in modes) { 36 | document.getElementById(id).onclick = _.bind(this.toggle, this, modes[id]); 37 | } 38 | 39 | this.eleDatetime = document.getElementById('datetime'); 40 | this.eleDatetime.onchange = _.bind(this.resetSequence, this); 41 | 42 | this.lastVisit = null; 43 | var lastVisitItem = localStorage.getItem(Status.STORAGE_KEY_LAST_VISIT); 44 | var eleLastVisitButton = document.getElementById('last_visit_button'); 45 | if (lastVisitItem) { 46 | this.lastVisit = parseInt(lastVisitItem); 47 | this.setDateTimeToLastVisit(); 48 | eleLastVisitButton.onclick = _.bind(this.setDateTimeToLastVisit, this); 49 | console.log('last visit: ' + moment(this.lastVisit).format("YYYY-MM-DD HH:mm:ss")); 50 | } else { 51 | this.setDateTimeToNow(); 52 | eleLastVisitButton.classList.remove('button'); 53 | eleLastVisitButton.classList.add('button_disabled'); 54 | } 55 | document.getElementById('now_button').onclick = _.bind(this.setDateTimeToNow, this); 56 | 57 | document.getElementById('load_button').onclick = _.bind(this.loadTime, this); 58 | } 59 | 60 | /** 61 | * First available sequence number for *id_sorted* diffs. 62 | * see http://overpass-api.de/augmented_diffs/id_sorted/ 63 | * api/augmented_state_by_date returns 6749 for "old" format, id_sorted format 64 | * starts with 8385, but api returns empty result for the first ones. 65 | */ 66 | Player.LOWER_LIMIT = 13000; 67 | 68 | Player.prototype.setDateTime = function(dateTime) { 69 | this.eleDatetime.value = oscviewer.formatIsoDateTime(dateTime); 70 | this.resetSequence(); 71 | }; 72 | 73 | Player.prototype.setDateTimeToLastVisit = function() { 74 | this.setDateTime(this.lastVisit); 75 | }; 76 | 77 | Player.prototype.setDateTimeToNow = function() { 78 | this.setDateTime(Date.now()); 79 | }; 80 | 81 | Player.prototype.resetSequence = function() { 82 | this.sequence = -1; 83 | }; 84 | 85 | Player.prototype.start = function(mode, element) { 86 | this.element = element; 87 | this.element.classList.add('button_active'); 88 | this.mode = mode; 89 | this.active = true; 90 | 91 | if (this.sequence === -1) { 92 | this.getSequenceByTime(_.bind(this.startWithSequence, this)); 93 | } else { 94 | this.startWithSequence(); 95 | } 96 | }; 97 | 98 | Player.prototype.loadCurrentSequence = function() { 99 | this.currentSequence = this.overpassAPI.getCurrentSequence(); 100 | if (this.currentSequence && this.currentSequence >= 0) { 101 | console.log('current sequence = ' + this.currentSequence); 102 | } else { 103 | console.error('invalid current sequence: "' + this.currentSequence + '"'); 104 | this.currentSequence = null; 105 | } 106 | }; 107 | 108 | Player.prototype.startWithSequence = function() { 109 | var limitSequence = this.mode.operation(this.sequence, this.mode.limit); 110 | if (!this.currentSequence || limitSequence >= this.currentSequence) { 111 | this.loadCurrentSequence(); 112 | } 113 | this.stopSequence = Math.min(limitSequence, this.currentSequence); 114 | this.stopSequence = Math.max(this.stopSequence, Player.LOWER_LIMIT); 115 | 116 | // skips this sequence, which is either loaded with live or loadTime 117 | this.loadNext(); 118 | }; 119 | 120 | Player.prototype.load = function() { 121 | this.status.loadStart(); 122 | this.overpassAPI.load(this.sequence, _.bind(this.postLoad, this)); 123 | }; 124 | 125 | Player.prototype.loadNext = function() { 126 | if (this.sequence !== this.stopSequence) { 127 | // ++ or -- 128 | this.sequence = this.mode.operation(this.sequence, 1); 129 | this.load(); 130 | } else { 131 | if (this.mode.limit > 1) { 132 | console.log('player stopped - limit reached'); 133 | } 134 | this.stop(); 135 | } 136 | }; 137 | 138 | Player.prototype.updateStatus = function() { 139 | this.status.sequence = this.sequence; 140 | this.status.count++; 141 | this.status.countdown = null; 142 | this.status.update(); 143 | 144 | if (this.status.timestamp) { 145 | this.eleDatetime.value = moment(this.status.timestamp).format('YYYY-MM-DD HH:mm'); 146 | } 147 | }; 148 | 149 | Player.prototype.postLoad = function() { 150 | this.status.loadEnd(); 151 | this.updateStatus(); 152 | 153 | if (this.active) { 154 | this.interval = window.setTimeout(_.bind(this.loadNext, this), 200); 155 | } 156 | }; 157 | 158 | Player.prototype.stop = function() { 159 | this.active = false; 160 | this.mode = null; 161 | window.clearTimeout(this.interval); 162 | this.interval = null; 163 | this.status.setCountdown(null); 164 | this.element.classList.remove('button_active'); 165 | }; 166 | 167 | Player.prototype.getSequenceByTime = function(callback) { 168 | var inputTime = moment(this.eleDatetime.value, 'YYYY-MM-DD HH:mm').seconds(59).valueOf(); 169 | 170 | this.status.loadStart(); 171 | this.overpassAPI.getSequenceByTime(inputTime, _.bind(function(sequence) { 172 | this.status.loadEnd(); 173 | console.log('sequence = ' + sequence); 174 | this.sequence = Math.max(sequence, Player.LOWER_LIMIT); 175 | callback(); 176 | }, this)); 177 | }; 178 | 179 | Player.prototype.loadTime = function(e) { 180 | this.getSequenceByTime(_.bind(this.load, this)); 181 | }; 182 | 183 | Player.prototype.toggle = function(mode, e) { 184 | if (!this.active) { 185 | this.start(mode, e.target || e.srcElement); 186 | } else { 187 | this.stop(); 188 | } 189 | }; 190 | -------------------------------------------------------------------------------- /js/control/Status.js: -------------------------------------------------------------------------------- 1 | /** 2 | * class Status 3 | */ 4 | function Status() { 5 | this.reset(); 6 | } 7 | 8 | /** key for local storage, max timestamp of last visit */ 9 | Status.STORAGE_KEY_LAST_VISIT = 'achavi.last_visit'; 10 | 11 | Status.prototype.update = function() { 12 | var unset = '-'; 13 | var sTimestamp = unset; 14 | if (this.timestamp) { 15 | sTimestamp = moment(this.timestamp).format('HH:mm'); 16 | 17 | // remember last visit as max timestamp of loaded diffs 18 | var lastVisit = localStorage.getItem(Status.STORAGE_KEY_LAST_VISIT); 19 | if (lastVisit < this.timestamp) { 20 | localStorage.setItem(Status.STORAGE_KEY_LAST_VISIT, this.timestamp); 21 | } 22 | } 23 | 24 | document.getElementById('status_countdown').innerHTML = this.nvl(this.countdown, unset); 25 | document.getElementById('status_time').innerHTML = sTimestamp; 26 | document.getElementById('status_count').innerHTML = this.count || unset; 27 | document.getElementById('status_sequence').innerHTML = this.sequence || unset; 28 | document.getElementById('status_new_changes').innerHTML = this.nvl(this.newChanges, unset); 29 | document.getElementById('status_total_changes').innerHTML = this.nvl(this.totalChanges, unset); 30 | document.getElementById('status_errors').innerHTML = this.errors || unset; 31 | }; 32 | 33 | Status.prototype.reset = function() { 34 | this.countdown = null; 35 | this.sequence = null; 36 | /** number in milliseconds */ 37 | this.timestamp = null; 38 | this.count = 0; 39 | this.newChanges = null; 40 | this.totalChanges = null; 41 | this.errors = 0; 42 | 43 | this.update(); 44 | }; 45 | 46 | Status.prototype.addChanges = function(changes) { 47 | this.newChanges = changes; 48 | if (this.totalChanges) { 49 | this.totalChanges += changes; 50 | } else { 51 | this.totalChanges = changes; 52 | } 53 | }; 54 | 55 | Status.prototype.nvl = function(val, s) { 56 | return (val !== null) ? val : s; 57 | }; 58 | 59 | Status.prototype.setCountdown = function(countdown) { 60 | this.countdown = countdown; 61 | this.update(); 62 | }; 63 | 64 | Status.prototype.loadStart = function() { 65 | this.setCountdown(' '); 66 | document.getElementById('status_countdown').classList.add('spinner'); 67 | }; 68 | 69 | Status.prototype.loadEnd = function() { 70 | document.getElementById('status_countdown').classList.remove('spinner'); 71 | }; 72 | -------------------------------------------------------------------------------- /js/map.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var displayProjection = "EPSG:4326"; 4 | 5 | var hover; 6 | var renderers = [ "SVG" ]; // Canvas 7 | 8 | var status, 9 | loader, 10 | loading, 11 | overpassAPI, 12 | changesetRegex; 13 | 14 | function addBaseLayers(map) { 15 | 16 | //OpenLayers.Layer.OSM.wrapDateLine = false; 17 | 18 | // reuse Bing resolutions 0-21 as client layer resolutions 19 | var resolutions = OpenLayers.Layer.Bing.prototype.serverResolutions; 20 | // OSM zoom levels: 0-19 21 | var serverResolutions = resolutions.slice(0, 20); 22 | 23 | var url = [ 24 | 'https://a.tile.openstreetmap.org/${z}/${x}/${y}.png', 25 | 'https://b.tile.openstreetmap.org/${z}/${x}/${y}.png', 26 | 'https://c.tile.openstreetmap.org/${z}/${x}/${y}.png' 27 | ]; 28 | var opts = { 29 | wrapDateLine: false, 30 | transitionEffect: 'map-resize', 31 | resolutions : resolutions, 32 | serverResolutions : serverResolutions, 33 | attribution: "tiles © OpenStreetMap contributors" 34 | }; 35 | map.addLayer(new OpenLayers.Layer.OSM('OSM dark', url, _.extend({}, opts, {opacity : 0.2}))); 36 | map.addLayer(new OpenLayers.Layer.OSM('OSM pale', url, _.extend({}, opts, {className: 'pale'}))); 37 | map.addLayer(new OpenLayers.Layer.OSM('OSM gray', url, _.extend({}, opts, {className: 'gray'}))); 38 | map.addLayer(new OpenLayers.Layer.OSM('OSM inverted', url, _.extend({}, opts, {className: 'inverted'}))); 39 | map.addLayer(new OpenLayers.Layer.OSM('OSM', url, opts)); 40 | 41 | map.addLayer(new OpenLayers.Layer.OSM('Carto Dark Matter', [ 42 | 'https://a.basemaps.cartocdn.com/rastertiles/dark_all/${z}/${x}/${y}.png', 43 | 'https://b.basemaps.cartocdn.com/rastertiles/dark_all/${z}/${x}/${y}.png', 44 | 'https://c.basemaps.cartocdn.com/rastertiles/dark_all/${z}/${x}/${y}.png' 45 | ], _.extend({}, opts, { 46 | attribution: 'tiles by Carto, under CC BY 3.0.' 47 | }))); 48 | 49 | // empty layer (~ no base layer) 50 | map.addLayer(new OpenLayers.Layer("blank", { 51 | isBaseLayer : true 52 | })); 53 | } 54 | 55 | function init() { 56 | OpenLayers.ImgPath = "img/"; 57 | 58 | var styleMaps; 59 | var options = { 60 | div : "map_div", 61 | // disable theme CSS auto-loading, causes error with OpenLayers.js.gz 62 | // (requires exact name match of OpenLayers.js script URL) 63 | theme : null, 64 | projection : "EPSG:900913", 65 | displayProjection : displayProjection, 66 | controls : [] 67 | }; 68 | 69 | map = new OpenLayers.Map(options); 70 | 71 | var cAttribution = new OpenLayers.Control.Attribution(); 72 | cAttribution.template = "achavi [attic.0.3]  -  ${layers}"; 73 | map.addControl(cAttribution); 74 | // map.addControl(new OpenLayers.Control.LayerSwitcher()); 75 | map.addControl(new OpenLayers.Control.LayerSwitcherBorder()); 76 | map.addControl(new OpenLayers.Control.Navigation()); 77 | map.addControl(new OpenLayers.Control.Zoom()); 78 | // map.addControl(new OpenLayers.Control.PanZoomBar()); 79 | map.addControl(new OpenLayers.Control.Permalink()); 80 | map.addControl(new OpenLayers.Control.MousePosition()); 81 | 82 | addBaseLayers(map); 83 | 84 | var styler = new OSMChangeStyle(map); 85 | styleMaps = styler.getStyleMaps(); 86 | 87 | changesets = new OpenLayers.Layer.Vector("changeset", { 88 | projection : map.displayProjection, 89 | visibility : true, 90 | styleMap : styleMaps.changes, 91 | renderers : renderers 92 | }); 93 | 94 | old = new OpenLayers.Layer.Vector("old", { 95 | projection : map.displayProjection, 96 | visibility : true, 97 | styleMap : styleMaps.old, 98 | renderers : renderers, 99 | rendererOptions: { zIndexing: true } 100 | }); 101 | 102 | changes = new OpenLayers.Layer.Vector("changes", { 103 | projection : map.displayProjection, 104 | visibility : true, 105 | styleMap : styleMaps.changes, 106 | renderers : renderers, 107 | rendererOptions: { zIndexing: true }, 108 | attribution: 'data © OpenStreetMap contributors, ' 109 | + 'licensed under ODbL' 110 | }); 111 | 112 | status = new Status(); 113 | loading = new Loading(); 114 | loader = new Loader(map, { changes: changes, old: old, changesets: changesets }, status); 115 | var parameters = OpenLayers.Control.ArgParser.prototype.getParameters(); 116 | if (parameters.relations) { 117 | document.getElementById('relations').checked = parameters.relations === 'true'; 118 | } 119 | if (parameters.url) { 120 | // load augmented change file passed as 'url' parameter; 121 | // optional changeset param for filtering by changeset 122 | loader.GET({url: parameters.url, zoomToExtent: true, changeset: parameters.changeset}); 123 | } else if (parameters.changeset) { 124 | loadChangeset(parameters.changeset); 125 | } 126 | 127 | map.addLayer(changesets); 128 | map.addLayer(old); 129 | map.addLayer(changes); 130 | 131 | var vectorLayers = [ old, changes, changesets ]; 132 | addControls(map, vectorLayers); 133 | 134 | // hover + select 135 | var handler = new PopupHandler(map, old, changes); 136 | var options = { 137 | onSelect : function(feature) { 138 | // also pass mouse position of the event 139 | // (note: OpenLayers.Handler.evt is not an API property) 140 | // see http://trac.osgeo.org/openlayers/ticket/2089 141 | handler.onFeatureSelect(feature, this.handlers.feature.evt.xy, this.hover); 142 | }, 143 | onUnselect : handler.onFeatureUnselect 144 | }; 145 | 146 | hover = new OpenLayers.Control.HoverAndSelectFeature(vectorLayers, options); 147 | map.addControl(hover); 148 | hover.activate(); 149 | 150 | if (!map.getCenter()) { 151 | if (old.features.length > 0) { 152 | map.zoomToExtent(old.getDataExtent()); 153 | } else if (changes.features.length > 0) { 154 | map.zoomToExtent(changes.getDataExtent()); 155 | } else { 156 | //map.zoomToMaxExtent(); 157 | map.setCenter(new OpenLayers.LonLat(0,0), 2); 158 | } 159 | 160 | // map.setCenter(new OpenLayers.LonLat(lon, 161 | // lat).transform("EPSG:4326", map.getProjectionObject()), zoom); 162 | } 163 | } 164 | 165 | function addBottomControls() { 166 | var toggle = function(e) { 167 | var prefix = this.id.substring(0, this.id.indexOf('_')) + '_'; 168 | document.getElementById(prefix + 'content').classList.toggle('hidden'); 169 | document.getElementById(prefix + 'button').classList.toggle('hidden'); 170 | }; 171 | 172 | var bottomButtons = ['legend', 'help', 'about']; 173 | bottomButtons.forEach(function(id) { 174 | document.getElementById(id + '_minimize').onclick = toggle; 175 | document.getElementById(id + '_button').onclick = toggle; 176 | }); 177 | } 178 | 179 | function addBBoxControl(map, bboxChangeCallback) { 180 | 181 | var col = 'rgba(255, 255, 255, 0.4)'; 182 | bbox.style['default'].strokeColor = col; 183 | bbox.style['transform'].strokeColor = col; 184 | bbox.style['temporary'].strokeColor = col; 185 | 186 | // bbox vector layer for drawing 187 | var bboxLayer = new OpenLayers.Layer.Vector("bbox", { 188 | styleMap: bbox.createStyleMap(), 189 | // for now, hide layer by default, because bbox select control disables/interferes with main select control 190 | visibility: false 191 | }); 192 | map.addLayer(bboxLayer); 193 | 194 | // bbox control 195 | bbox.addControls(map, bboxLayer, { 196 | update : bboxChangeCallback, 197 | activate : function() { 198 | // reset when new box is drawn (set bbox to null) 199 | bboxChangeCallback(null); 200 | document.getElementById('bbox_button').classList.add('button_active'); 201 | }, 202 | deactivate : function() { 203 | document.getElementById('bbox_button').classList.remove('button_active'); 204 | } 205 | }); 206 | 207 | var onBBoxClick = function(e) { 208 | bbox.switchActive(); 209 | }; 210 | document.getElementById('bbox_button').onclick = onBBoxClick; 211 | 212 | return bbox; 213 | } 214 | 215 | function addControls(map, layers) { 216 | 217 | addBottomControls(); 218 | 219 | var bboxChangeHandler = function(bbox) { 220 | overpassAPI.bbox = bbox; 221 | }; 222 | var bboxControl = addBBoxControl(map, bboxChangeHandler); 223 | 224 | overpassAPI = new OverpassAPI(loader, bboxControl); 225 | new Live(overpassAPI, status); 226 | new Diff(overpassAPI, loading, status); 227 | 228 | var onClearClick = function(e) { 229 | for (var i = 0; i < layers.length; i++) { 230 | layers[i].removeAllFeatures(); 231 | } 232 | status.reset(); 233 | }; 234 | document.getElementById('clear_button').onclick = onClearClick; 235 | 236 | var fileReaderControl = new FileReaderControl(_.bind(loader.handleLoad, loader)); 237 | fileReaderControl.addUrlHandler(overpassAPI.sequenceUrlRegex, _.bind(overpassAPI.loadByUrl, overpassAPI)); 238 | changesetRegex = /.*\/changeset\/([0-9]*)/; 239 | fileReaderControl.addUrlHandler(changesetRegex, loadChangesetByUrl); 240 | fileReaderControl.activate(); 241 | } 242 | 243 | function loadChangesetByUrl(url) { 244 | var id = url.match(changesetRegex)[1]; 245 | loadChangeset(id); 246 | } 247 | 248 | function loadChangeset(id) { 249 | 250 | function handleDiff() { 251 | loading.loadEnd(); 252 | } 253 | 254 | function handleChangeset() { 255 | var csFeature, cs, bbox, xhr, from, to, relations, query; 256 | 257 | csFeature = changesets.getFeatureByFid('changeset.' + id); 258 | if (csFeature) { 259 | cs = csFeature.attributes; 260 | from = cs.created_at; 261 | if (from) { 262 | // workaround: no result when same start and end date (e.g. changeset 23168078), not sure if needed generally 263 | // https://github.com/drolbr/Overpass-API/issues/113 264 | from = moment(cs.created_at, 'YYYY-MM-DDTHH:mm:ss\\Z').subtract('seconds', 1).format('YYYY-MM-DDTHH:mm:ss\\Z'); 265 | } 266 | to = cs.closed_at; 267 | bbox = new OpenLayers.Bounds(cs.min_lon, cs.min_lat, cs.max_lon, cs.max_lat); 268 | overpassAPI.bbox = bbox; 269 | 270 | relations = document.getElementById('relations').checked; 271 | xhr = overpassAPI.loadDiff(from, to, relations, query, handleDiff, id); 272 | loading.loadStart(xhr); 273 | } 274 | } 275 | 276 | loader.GET({ 277 | url: 'https://www.openstreetmap.org/api/0.6/changeset/' + id, 278 | zoomToExtent: true, 279 | postLoadCallback: handleChangeset 280 | }); 281 | } 282 | 283 | init(); 284 | })(); 285 | -------------------------------------------------------------------------------- /js/oscviewer.js: -------------------------------------------------------------------------------- 1 | var oscviewer = (function() { 2 | 3 | var ACTION_MODIFY_GEOMETRY = 'modify:geometry'; 4 | 5 | function buildFidMap(features) { 6 | var map = {}; 7 | for(var i=0, len=features.length; i < len; i++) { 8 | map[features[i].fid] = features[i]; 9 | } 10 | return map; 11 | } 12 | 13 | function setActions(oscFeatures, osmFeatures) { 14 | var i; 15 | var oscFeature, osmFeature; 16 | var action; 17 | //var oscFeatures = oscLayer.features; 18 | var versionDiff; 19 | //var osmFeatures = []; 20 | 21 | var oscFeaturesFidMap = buildFidMap(oscFeatures); 22 | var osmFeaturesFidMap = buildFidMap(osmFeatures); 23 | 24 | var oscOnly = osmFeatures.length === 0; 25 | 26 | // Overpass diffs: distinguish deletes from old versions (before setting actions from osc!) 27 | for (i = 0; i < osmFeatures.length; i++) { 28 | osmFeature = osmFeatures[i]; 29 | if (osmFeature.attributes['action'] === 'erase') { 30 | oscFeature = oscFeaturesFidMap[osmFeature.fid]; 31 | if (oscFeature) { 32 | // in 'insert' section = old version of modified object, remove 'erase' 33 | delete osmFeature.attributes['action']; 34 | } else { 35 | // replace action string 'erase' with 'delete' 36 | osmFeature.attributes['action'] = 'delete'; 37 | setHasTags(osmFeature, null); 38 | } 39 | } else if (osmFeature.attributes['action'] === 'keep') { 40 | // set common action for referenced objects (for styling) 41 | osmFeature.attributes['action'] = 'augment'; 42 | } 43 | } 44 | 45 | for (i = 0; i < oscFeatures.length; i++) { 46 | oscFeature = oscFeatures[i]; 47 | osmFeature = null; 48 | 49 | action = oscFeature.attributes['action']; 50 | 51 | if (!(oscOnly || action === 'create')) { 52 | osmFeature = osmFeaturesFidMap[oscFeature.fid]; 53 | 54 | if (action === 'modify' || action === 'delete') { 55 | if (osmFeature) { 56 | // set action to old object 57 | if (action === 'modify') { 58 | setModifyAction(osmFeature, oscFeature); 59 | } else { 60 | osmFeature.attributes['action'] = action; 61 | } 62 | } else { 63 | console.warn('Feature "' + oscFeature.fid + '" not found'); 64 | } 65 | } else if (action === 'insert') { 66 | // Overpass diffs 67 | if (osmFeature) { 68 | // modify 69 | setModifyAction(osmFeature, oscFeature); 70 | } else { 71 | // create 72 | oscFeature.attributes['action'] = 'create'; 73 | } 74 | } 75 | } 76 | 77 | // handle special cases when old or new entity is out of scope 78 | // (geometry entering or leaving the bounding box) 79 | /* disabled: falsly reports summarized changes as out of scope 80 | (e.g. create v1 + modify v2 = create v2). 81 | Can't distinguish these cases only by version number. 82 | if (action === 'create' && oscFeature.attributes.version > 1) { 83 | oscFeature.scopeAction = action; 84 | oscFeature.attributes.action = ACTION_MODIFY_GEOMETRY; 85 | } else 86 | */ 87 | if (action === 'delete' && oscFeature.attributes.visible === 'true') { 88 | oscFeature.scopeAction = action; 89 | osmFeature.attributes.action = ACTION_MODIFY_GEOMETRY; 90 | oscFeature.attributes.action = ACTION_MODIFY_GEOMETRY; 91 | } 92 | 93 | setHasTags(osmFeature, oscFeature); 94 | } 95 | } 96 | 97 | function isChanged(osmFeature, oscFeature) { 98 | var geometryChanged = !oscFeature.geometry.equals(osmFeature.geometry); 99 | var tagsChanged = isTagsChanged(osmFeature, oscFeature); 100 | var rolesChanged = oscFeature.type === 'relation' && isRolesChanged(osmFeature, oscFeature); 101 | 102 | return geometryChanged || tagsChanged || rolesChanged; 103 | } 104 | 105 | function isTagsChanged(osmFeature, oscFeature) { 106 | var osmTags = getTags(osmFeature); 107 | var oscTags = getTags(oscFeature); 108 | 109 | return !_.isEqual(osmTags, oscTags); 110 | } 111 | 112 | function isRolesChanged(osmFeature, oscFeature) { 113 | var osmRoles = getRoles(osmFeature); 114 | var oscRoles = getRoles(oscFeature); 115 | 116 | return !_.isEqual(osmRoles, oscRoles); 117 | } 118 | 119 | function setModifyAction(osmFeature, oscFeature) { 120 | var action = oscFeature.geometry.equals(osmFeature.geometry) ? 'modify' : ACTION_MODIFY_GEOMETRY; 121 | osmFeature.attributes['action'] = action; 122 | oscFeature.attributes['action'] = action; 123 | } 124 | 125 | function getTags(feature) { 126 | return _.omit(feature.attributes, OpenLayers.Format.OSC.prototype.metaAttributes); 127 | } 128 | 129 | function getRoles(feature) { 130 | return _.map(feature.geometry.components, function(member){ return member.role; }); 131 | } 132 | 133 | function getHasTags(feature) { 134 | var result = false; 135 | for (var attr in feature.attributes) { 136 | if (!isMetaAttribute(attr)) { 137 | result = true; 138 | break; 139 | } 140 | } 141 | return result; 142 | } 143 | 144 | function setHasTags(osmFeature, oscFeature) { 145 | var osmHasTags = false; 146 | if (osmFeature) { 147 | osmHasTags = getHasTags(osmFeature); 148 | osmFeature.hasTags = osmHasTags; 149 | } 150 | if (oscFeature) { 151 | // also indicate that the old feature had tags, when the changed feature has none 152 | oscFeature.hasTags = osmHasTags || getHasTags(oscFeature); 153 | } 154 | } 155 | 156 | // TODO move to OSMDiffFormat 157 | OpenLayers.Format.OSC.prototype.metaAttributes.push('action'); 158 | OpenLayers.Format.OSC.prototype.metaAttributes.push('state'); 159 | 160 | // 'visible' only set in attic format when entity moved out of bbox (-> action=delete) 161 | OpenLayers.Format.OSMMeta.prototype.metaAttributes.push('visible'); 162 | 163 | function isMetaAttribute(attr) { 164 | var metaAttrs = OpenLayers.Format.OSC.prototype.metaAttributes; 165 | return OpenLayers.Util.indexOf(metaAttrs, attr) !== -1; 166 | } 167 | 168 | function isChangesetMetaAttribute(attr) { 169 | var metaAttrs = OpenLayers.Format.OSMChangeset.prototype.metaAttributes; 170 | return OpenLayers.Util.indexOf(metaAttrs, attr) !== -1; 171 | } 172 | 173 | function getInfoHtml(oldFeature, changeFeature) { 174 | var keys = []; 175 | var oldTags = undefined; 176 | var changeTags = undefined; 177 | var feature = changeFeature || oldFeature; 178 | var osm = { 179 | id : feature.osm_id, 180 | type : getOsmType(feature) 181 | }; 182 | var action = feature.attributes['action'] || ''; 183 | var tagColoring = !feature.scopeAction; // no tag comparison when one feature out of scope 184 | 185 | var infoHtml = ''; 186 | infoHtml += '
'; 187 | infoHtml += '
'; 188 | infoHtml += '
'; 189 | // modify:geometry -> modify_geometry for CSS 190 | infoHtml += ''; 191 | infoHtml += action; 192 | infoHtml += ''; 193 | infoHtml += ' ' + osm.type; 194 | infoHtml += ' ' + formatOsmLink(osm.id, osm.type); 195 | infoHtml += '
'; 196 | 197 | if (feature.scopeAction) { 198 | var scopeText = feature.scopeAction === 'create' ? 'old' : 'new'; 199 | infoHtml += '
Note: ' + scopeText + ' entity out of scope
'; 200 | } else if (!oldFeature && action != 'create') { 201 | infoHtml += '
Warning: Old feature not found,
probably not included in OSM file
'; 202 | } 203 | 204 | infoHtml += ''; 205 | 206 | if (oldFeature) { 207 | oldTags = oldFeature.attributes; 208 | keys = _.keys(oldTags); 209 | } 210 | if (changeFeature) { 211 | changeTags = changeFeature.attributes; 212 | keys = keys.concat(_.keys(changeTags)); 213 | } 214 | keys = _.filter(keys, function(key) { 215 | if (osm.type === 'changeset') { 216 | return !isChangesetMetaAttribute(key); 217 | } else { 218 | return !isMetaAttribute(key); 219 | } 220 | }); 221 | keys.sort(); 222 | keys = _.unique(keys, true); 223 | 224 | // single column display (no comparison) when same object version 225 | // e.g. for modify:geometry on way when only node changed or 226 | // duplicated delete when change version is not available (Osmosis diff) 227 | if (oldTags && changeTags && oldTags.version === changeTags.version) { 228 | changeTags = undefined; 229 | } 230 | 231 | if (osm.type === 'changeset') { 232 | infoHtml += printChangesetMeta(oldTags); 233 | } else { 234 | infoHtml += printEntityMeta(oldTags, changeTags); 235 | } 236 | 237 | infoHtml += ''; 238 | infoHtml += printKeys(keys, oldTags, changeTags, action, tagColoring); 239 | 240 | infoHtml += '

'; 241 | 242 | if (osm.type !== 'changeset') { 243 | infoHtml += getEditFooter(feature, osm); 244 | } 245 | 246 | infoHtml += '
'; 247 | infoHtml += '
'; 248 | 249 | return infoHtml; 250 | } 251 | 252 | function getEditFooter(feature, osm){ 253 | var footerHtml = ''; 254 | if (feature.geometry.bounds && feature.geometry.bounds.right) { 255 | var bounds = feature.geometry.bounds.clone().transform(new OpenLayers.Projection("EPSG:900913"), new OpenLayers.Projection("EPSG:4326")); 256 | var boundsParams = OpenLayers.String.format('left=${left}&right=${right}&top=${top}&bottom=${bottom}', bounds); 257 | 258 | var josmBaseUrl = 'http://127.0.0.1:8111/'; 259 | var josmLoadUrl = josmBaseUrl + 'load_object?objects=' + osm.type.substr(0,1) + osm.id; 260 | var josmSelectUrl = josmBaseUrl + 'zoom?' + boundsParams + '&select=' + osm.type + osm.id; 261 | 262 | footerHtml += ''; 269 | } 270 | return footerHtml; 271 | } 272 | 273 | function attachJosmRemoteListener(id) { 274 | var josmRemote = document.getElementById(id); 275 | if (josmRemote) { 276 | var url = josmRemote.href; 277 | var clickHandler = function(evt) { 278 | evt.preventDefault(); 279 | 280 | var xhr = new XMLHttpRequest(); 281 | xhr.open('GET', url); 282 | xhr.onerror = function(e) { 283 | console.error('Error connecting to JOSM, url: ' + url); 284 | alert('Error connecting to JOSM:\n\nIs JOSM running and Remote Control enabled?'); 285 | }; 286 | xhr.send(); 287 | 288 | return false; 289 | }; 290 | 291 | josmRemote.onclick = clickHandler; 292 | } 293 | } 294 | 295 | function attachInfoHtmlListeners() { 296 | attachJosmRemoteListener('josmLoad'); 297 | attachJosmRemoteListener('josmSelect'); 298 | } 299 | 300 | function getOsmType(feature) { 301 | return feature.fid.substring(0, feature.fid.indexOf('.')); 302 | } 303 | 304 | function formatIsoDateTime(dateTimeString) { 305 | // locale-independent, ISO-like format, but in user's local time zone 306 | return moment(dateTimeString).format('YYYY-MM-DD HH:mm'); 307 | } 308 | 309 | function formatIsoDateTimeSec(dateTimeString) { 310 | // locale-independent, ISO-like format, but in user's local time zone 311 | if (!dateTimeString) { 312 | return ''; 313 | } 314 | return moment(dateTimeString).format('YYYY-MM-DD HH:mm:ss'); 315 | } 316 | 317 | function formatOsmLink(val, type) { 318 | return '' + xssFilters.inHTMLData(val) + ''; 320 | } 321 | 322 | function printChangesetMeta(tags) { 323 | var infoHtml = ''; 324 | 325 | infoHtml += printMeta('user', tags, null, formatOsmLink); 326 | infoHtml += printMeta('created_at', tags, null, formatIsoDateTimeSec); 327 | infoHtml += printMeta('closed_at', tags, null, formatIsoDateTimeSec); 328 | infoHtml += printMeta('open', tags); 329 | 330 | return infoHtml; 331 | } 332 | 333 | function printEntityMeta(oldTags, changeTags) { 334 | var infoHtml = ''; 335 | 336 | infoHtml += printMeta('timestamp', oldTags, changeTags, formatIsoDateTime); 337 | infoHtml += printMeta('user', oldTags, changeTags, formatOsmLink); 338 | infoHtml += printMeta('version', oldTags, changeTags); 339 | infoHtml += printMeta('changeset', oldTags, changeTags, formatOsmLink); 340 | 341 | return infoHtml; 342 | } 343 | 344 | function printMeta(key, oldTags, changeTags, format) { 345 | var infoHtml = ''; 346 | if (!format) { 347 | // dummy 348 | format = function(val) { 349 | return val; 350 | }; 351 | } 352 | infoHtml += '' + key + ''; 353 | if (oldTags) { 354 | infoHtml += '' + format(oldTags[key], key) + ''; 355 | } 356 | if (changeTags) { 357 | infoHtml += '' + format(changeTags[key], key) + ''; 358 | } 359 | infoHtml += ''; 360 | return infoHtml; 361 | } 362 | 363 | function printKeys(keys, oldTags, changeTags, action, coloring) { 364 | var i; 365 | var oldVal, changeVal; 366 | var classes; 367 | var infoHtml = ''; 368 | for (i = 0; i < keys.length; i++) { 369 | key = keys[i]; 370 | oldVal = oldTags && oldTags[key]; 371 | changeVal = changeTags && changeTags[key]; 372 | classes = (oldTags && changeTags && coloring) ? getClasses(oldVal, changeVal, action) : getDefaultClasses(); 373 | infoHtml += '' + xssFilters.inHTMLData(key) + ''; 374 | if (oldTags) { 375 | infoHtml += '' + val(oldVal) + ''; 376 | } 377 | if (changeTags) { 378 | infoHtml += '' + val(changeVal) + ''; 379 | } 380 | infoHtml += ''; 381 | } 382 | return infoHtml; 383 | } 384 | 385 | function getDefaultClasses() { 386 | var c = { 387 | key : 'keydefault', 388 | old : 'default', 389 | change : 'default' 390 | }; 391 | return c; 392 | } 393 | 394 | function getClasses(oldVal, changeVal, action) { 395 | var c = getDefaultClasses(); 396 | 397 | if (_.isUndefined(oldVal)) { 398 | c.key = 'created'; 399 | c.old = 'undefined'; 400 | c.change = 'created'; 401 | } else if (_.isUndefined(changeVal)) { 402 | // default on delete, because tags themselves are unchanged 403 | if (action !== 'delete') { 404 | c.key = 'deleted'; 405 | c.old = 'deleted'; 406 | } 407 | c.change = 'undefined'; 408 | } else if (oldVal !== changeVal) { 409 | c.old = 'modified'; 410 | c.change = 'modified'; 411 | } 412 | 413 | return c; 414 | } 415 | 416 | function val(value) { 417 | return _.isUndefined(value) ? '' : xssFilters.inHTMLData(value); 418 | } 419 | 420 | return { 421 | setActions : setActions, 422 | getInfoHtml: getInfoHtml, 423 | attachInfoHtmlListeners: attachInfoHtmlListeners, 424 | getOsmType: getOsmType, 425 | formatIsoDateTime: formatIsoDateTime, 426 | isChanged: isChanged 427 | }; 428 | })(); 429 | -------------------------------------------------------------------------------- /js/tooltips.js: -------------------------------------------------------------------------------- 1 | var tooltips = (function() { 2 | 3 | // tag id > tooltip text (title attribute) 4 | 5 | var main = { 6 | bbox_button: "click and drag to draw bounding box (optional, else current map view is used), to display and edit activate 'bbox' layer", 7 | /*live_button: "load latest diff and activate minutely refreshing",*/ 8 | clear_button: "remove all loaded changes" 9 | }; 10 | 11 | var diff = { 12 | fromDatetime: "Enter start date & time (local time) of time range to load (Format: YYYY-MM-DD HH:mm). Defaults to -24h or last visit.", 13 | toDatetime: "Enter end date & time (local time) of time range to load (Format: YYYY-MM-DD HH:mm), or leave empty. Defaults to current time.", 14 | relations: "Check to also load changed relations, only basic support for relations right now.", 15 | load_button: "Load changes in the specified time range" 16 | }; 17 | 18 | var player = { 19 | fast_backward_button: "fast backward - starts loading all augmented diffs previous to date & time entered, stop by pushing again, stops after limit of 24h", 20 | backward_button: "backward - load previous augmented diff to date & time entered", 21 | last_visit_button: "last visit - sets date & time to latest diff loaded in last session", 22 | now_button: "now - sets date & time to current time", 23 | datetime: "Enter date & time (local time) of augmented diff to load (Format: YYYY-MM-DD HH:mm), or set with last/now. Defaults to last visit.", 24 | load_button: "load augmented diff of date & time entered", 25 | forward_button: "forward - load next augmented diff to date & time entered", 26 | fast_forward_button: "fast forward - starts loading all augmented diffs after date & time entered, stop by pushing again, stops after limit of 24h", 27 | }; 28 | 29 | var status = { 30 | status_countdown: "next refresh countdown - seconds until checking for new augmented diff", 31 | status_time: "base time - OSM base time when the last changes were published (usually lag of 1-2 minutes)", 32 | status_count: "diff count - number of loaded diffs", 33 | status_sequence: "sequence number - number of last loaded augmented diff", 34 | status_total_changes: "total changes - number of changes loaded", 35 | status_new_changes: "new changes - number of changes loaded with last diff", 36 | status_errors: "loading errors - number of too large diffs that were rejected" 37 | }; 38 | 39 | /** 40 | * Add texts as tooltip to button and status bars, and also to help dialog. 41 | * Note: title attribute does not work with Firefox for Windows! 42 | */ 43 | var setTitlesAndHelp = function(map, name) { 44 | //var html = name + '
'; 45 | var html = '
' + name + '
'; 46 | for (var id in map) { 47 | var label; 48 | var ele = document.getElementById(id); 49 | ele.setAttribute('title', map[id]); 50 | if (id.substr(0,6) === 'status') { 51 | // for the status bar, set tooltip on labels as well 52 | ele.previousElementSibling.setAttribute('title', map[id]); 53 | label = ele.previousElementSibling.textContent; 54 | } else { 55 | label = ele.textContent; 56 | } 57 | //html += ''; 58 | html += '
' + label + '
' + map[id] + '
'; 59 | } 60 | //html += '
' + label + '' + map[id] + '
'; 61 | var eleHelp = document.getElementById('help_tooltips'); 62 | eleHelp.innerHTML += html; 63 | }; 64 | 65 | setTitlesAndHelp(main, 'Main buttons'); 66 | setTitlesAndHelp(diff, 'Time range'); 67 | //setTitlesAndHelp(player, 'Player'); 68 | //setTitlesAndHelp(status, 'Status bar'); 69 | })(); -------------------------------------------------------------------------------- /lib/olex.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for 2 | * full list of contributors). Published under the 2-clause BSD license. 3 | * See license.txt in the OpenLayers distribution or repository for the 4 | * full text of the license. */ 5 | 6 | /** 7 | * @requires OpenLayers/Format/OSM.js 8 | */ 9 | 10 | /** 11 | * Class: OpenLayers.Format.OSMMeta 12 | * Extended OSM parser. Adds meta attributes as tags. 13 | * Create a new instance with the constructor. 14 | * 15 | * Inherits from: 16 | * - 17 | */ 18 | OpenLayers.Format.OSMMeta = OpenLayers.Class(OpenLayers.Format.OSM, { 19 | 20 | // without id, which is already added as osm_id property to feature 21 | metaAttributes: ['version', 'timestamp', 'uid', 'user', 'changeset'], 22 | 23 | initialize: function(options) { 24 | OpenLayers.Format.OSM.prototype.initialize.apply(this, [options]); 25 | }, 26 | 27 | getTags: function(dom_node, interesting_tags) { 28 | var tags = OpenLayers.Format.OSM.prototype.getTags.apply(this, arguments); 29 | var meta = this.getMetaAttributes(dom_node); 30 | tags = OpenLayers.Util.extend(tags, meta); 31 | return tags; 32 | }, 33 | 34 | getMetaAttributes: function(dom_node) { 35 | var meta = {}, name; 36 | for (var i = 0; i < this.metaAttributes.length; i++) { 37 | name = this.metaAttributes[i]; 38 | meta[name] = dom_node.getAttribute(name); 39 | } 40 | return meta; 41 | }, 42 | 43 | CLASS_NAME: "OpenLayers.Format.OSMMeta" 44 | }); 45 | /* Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for 46 | * full list of contributors). Published under the 2-clause BSD license. 47 | * See license.txt in the OpenLayers distribution or repository for the 48 | * full text of the license. */ 49 | 50 | /** 51 | * @requires OSMMeta.js 52 | */ 53 | 54 | /** 55 | * Class: OpenLayers.Format.OSMExt 56 | * Extended OSM parser. Returns all nodes (including way nodes) and all tags, 57 | * without the overhead of checking interestingTagsExclude, which is ignored. 58 | * Create a new instance with the constructor. 59 | * 60 | * Inherits from: 61 | * - 62 | */ 63 | OpenLayers.Format.OSMExt = OpenLayers.Class(OpenLayers.Format.OSMMeta, { 64 | 65 | initialize: function(options) { 66 | OpenLayers.Format.OSMMeta.prototype.initialize.apply(this, [options]); 67 | 68 | // return used nodes (way nodes) as separate entities (check is ignored) 69 | this.checkTags = true; 70 | }, 71 | 72 | getTags: function(dom_node, interesting_tags) { 73 | // ignore interesting_tags parameter, pass false to avoid check 74 | var tags = OpenLayers.Format.OSMMeta.prototype.getTags.apply(this, [dom_node, false]); 75 | // all tags are interesting 76 | return interesting_tags ? [tags, true] : tags; 77 | }, 78 | 79 | CLASS_NAME: "OpenLayers.Format.OSMExt" 80 | }); 81 | /* Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for 82 | * full list of contributors). Published under the 2-clause BSD license. 83 | * See license.txt in the OpenLayers distribution or repository for the 84 | * full text of the license. */ 85 | 86 | /** 87 | * @requires OSMExt.js 88 | */ 89 | 90 | /** 91 | * Class: OpenLayers.Format.OSC 92 | * OSC (OSM change file) parser. Create a new instance with the 93 | * constructor. 94 | * 95 | * Inherits from: 96 | * - 97 | */ 98 | OpenLayers.Format.OSC = OpenLayers.Class(OpenLayers.Format.OSMExt, { 99 | 100 | initialize: function(options) { 101 | OpenLayers.Format.OSMExt.prototype.initialize.apply(this, [options]); 102 | 103 | this.metaAttributes.push('action'); 104 | }, 105 | 106 | // osmDoc optional 107 | read: function(doc, osmDoc) { 108 | // copied and modified version of OpenLayers.Format.OSM.read 109 | 110 | if (typeof doc == "string") { 111 | doc = OpenLayers.Format.XML.prototype.read.apply(this, [doc]); 112 | } 113 | 114 | // OSM XML 115 | var osmNodes = {}; 116 | if (osmDoc) { 117 | if (typeof osmDoc == "string") { 118 | osmDoc = OpenLayers.Format.XML.prototype.read.apply(this, [osmDoc]); 119 | } 120 | osmNodes = this.getNodes(osmDoc); 121 | } 122 | 123 | // OSC 124 | var nodes = this.getNodes(doc); 125 | var ways = this.getWays(doc); 126 | 127 | // Geoms will contain at least ways.length entries. 128 | var feat_list = new Array(ways.length); 129 | 130 | for (var i = 0; i < ways.length; i++) { 131 | // no fixed length, nodes might be missing 132 | var point_list = []; 133 | 134 | var poly = this.isWayArea(ways[i]) ? 1 : 0; 135 | for (var j = 0; j < ways[i].nodes.length; j++) { 136 | var node = nodes[ways[i].nodes[j]]; 137 | 138 | // if not in OSC get referenced node from augmenting file (OSM XML) 139 | if (!node) { 140 | node = osmNodes[ways[i].nodes[j]]; 141 | } 142 | if (node) { 143 | var point = new OpenLayers.Geometry.Point(node.lon, node.lat); 144 | 145 | // Since OSM is topological, we stash the node ID internally. 146 | point.osm_id = parseInt(ways[i].nodes[j]); 147 | //point_list[j] = point; 148 | point_list.push(point); 149 | 150 | // We don't display nodes if they're used inside other 151 | // elements. 152 | node.used = true; 153 | } else if (osmDoc) { 154 | console.warn('node "' + ways[i].nodes[j] + '" referenced by way "' + ways[i].id + '" not found'); 155 | } 156 | } 157 | if (point_list.length === 0 && ways[i].tags['action'] !== 'delete') { 158 | console.warn('no nodes for way "' + ways[i].id + '" found - way will not appear on map'); 159 | } 160 | var geometry = null; 161 | if (poly) { 162 | geometry = new OpenLayers.Geometry.Polygon( 163 | new OpenLayers.Geometry.LinearRing(point_list)); 164 | } else { 165 | geometry = new OpenLayers.Geometry.LineString(point_list); 166 | } 167 | if (this.internalProjection && this.externalProjection) { 168 | geometry.transform(this.externalProjection, 169 | this.internalProjection); 170 | } 171 | var feat = new OpenLayers.Feature.Vector(geometry, 172 | ways[i].tags); 173 | feat.osm_id = parseInt(ways[i].id); 174 | feat.fid = "way." + feat.osm_id; 175 | feat_list[i] = feat; 176 | } 177 | for (var node_id in nodes) { 178 | var node = nodes[node_id]; 179 | if (!node.used || this.checkTags) { 180 | var tags = null; 181 | 182 | if (this.checkTags) { 183 | var result = this.getTags(node.node, true); 184 | if (node.used && !result[1]) { 185 | continue; 186 | } 187 | tags = result[0]; 188 | } else { 189 | tags = this.getTags(node.node); 190 | } 191 | 192 | var feat = new OpenLayers.Feature.Vector( 193 | new OpenLayers.Geometry.Point(node['lon'], node['lat']), 194 | tags); 195 | if (this.internalProjection && this.externalProjection) { 196 | feat.geometry.transform(this.externalProjection, 197 | this.internalProjection); 198 | } 199 | feat.osm_id = parseInt(node_id); 200 | feat.fid = "node." + feat.osm_id; 201 | feat.used = node.used; 202 | feat_list.push(feat); 203 | } 204 | // Memory cleanup 205 | node.node = null; 206 | } 207 | return feat_list; 208 | }, 209 | 210 | getMetaAttributes: function(dom_node) { 211 | var meta = OpenLayers.Format.OSMMeta.prototype.getMetaAttributes.apply(this, [dom_node]); 212 | meta['action'] = this.getActionString(dom_node); 213 | return meta; 214 | }, 215 | 216 | getActionString: function(dom_node) { 217 | var action = dom_node.parentNode.tagName; 218 | return action; 219 | }, 220 | 221 | CLASS_NAME: "OpenLayers.Format.OSC" 222 | }); 223 | /* Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for 224 | * full list of contributors). Published under the 2-clause BSD license. 225 | * See license.txt in the OpenLayers distribution or repository for the 226 | * full text of the license. */ 227 | 228 | /** 229 | * @requires OSC.js 230 | */ 231 | 232 | /** 233 | * Class: OpenLayers.Format.OSCAugmented 234 | * Augmented OSC (OSM change file) parser. Create a new instance with the 235 | * constructor. 236 | * 237 | * Inherits from: 238 | * - 239 | */ 240 | OpenLayers.Format.OSCAugmented = OpenLayers.Class(OpenLayers.Format.OSC, { 241 | 242 | initialize: function(options) { 243 | OpenLayers.Format.OSC.prototype.initialize.apply(this, [options]); 244 | }, 245 | 246 | readAugmenting: function(augOscDoc) { 247 | var result = []; 248 | 249 | augOscDoc = this.toDocument(augOscDoc); 250 | 251 | // extract 'augment' node tree and pass to OSM.read (treat as OSM XML doc) 252 | var augmentNode = this.getAugmentNode(augOscDoc); 253 | if (augmentNode) { 254 | result = OpenLayers.Format.OSMExt.prototype.read.apply(this, [augmentNode]); 255 | } 256 | 257 | return result; 258 | }, 259 | 260 | /** 261 | * NOTE: modifies the passed document (removes the augment node)! 262 | */ 263 | read: function(augOscDoc) { 264 | var result = []; 265 | 266 | augOscDoc = this.toDocument(augOscDoc); 267 | 268 | // extract and delete 'augment' node tree and pass both to OSC.read 269 | // (treat 'augment' node as separate OSM XML doc) 270 | var augmentNode = this.getAugmentNode(augOscDoc); 271 | if (augmentNode) { 272 | augmentNode.parentNode.removeChild(augmentNode); 273 | result = OpenLayers.Format.OSC.prototype.read.apply(this, [augOscDoc, augmentNode]); 274 | } else { 275 | result = OpenLayers.Format.OSC.prototype.read.apply(this, [augOscDoc]); 276 | } 277 | 278 | return result; 279 | }, 280 | 281 | isAugmented: function(augOscDoc) { 282 | augOscDoc = this.toDocument(augOscDoc); 283 | 284 | var augment_list = augOscDoc.getElementsByTagName("augment"); 285 | return augment_list.length > 0; 286 | }, 287 | 288 | toDocument: function(stringOrDoc) { 289 | if (typeof stringOrDoc == "string") { 290 | stringOrDoc = OpenLayers.Format.XML.prototype.read.apply(this, [stringOrDoc]); 291 | } 292 | return stringOrDoc; 293 | }, 294 | 295 | getAugmentNode: function(doc) { 296 | var result = null; 297 | var augment_list = doc.getElementsByTagName("augment"); 298 | if (augment_list.length === 1) { 299 | result = augment_list[0]; 300 | } else { 301 | console.warn('Exactly one "augment" section expected in OSC, found: ' + (augment_list.length)); 302 | } 303 | return result; 304 | }, 305 | 306 | CLASS_NAME: "OpenLayers.Format.OSCAugmented" 307 | }); 308 | /* Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for 309 | * full list of contributors). Published under the 2-clause BSD license. 310 | * See license.txt in the OpenLayers distribution or repository for the 311 | * full text of the license. */ 312 | 313 | /** 314 | * @requires OSC.js 315 | */ 316 | 317 | /** 318 | * Class: OpenLayers.Format.OSCAugmented 319 | * Overpass API augmented diff parser. Create a new instance with the 320 | * constructor. 321 | * 322 | * Inherits from: 323 | * - 324 | */ 325 | OpenLayers.Format.OSCAugmentedDiff = OpenLayers.Class(OpenLayers.Format.OSC, { 326 | 327 | initialize: function(options) { 328 | OpenLayers.Format.OSC.prototype.initialize.apply(this, [options]); 329 | }, 330 | 331 | readAugmenting: function(doc) { 332 | var result = []; 333 | 334 | doc = this.toDocument(doc); 335 | 336 | var osm = doc.createElement("osm"); 337 | this.appendChildren(osm, doc.getElementsByTagName("erase")); 338 | this.appendChildren(osm, doc.getElementsByTagName("keep")); 339 | 340 | result = OpenLayers.Format.OSMExt.prototype.read.apply(this, [osm]); 341 | 342 | return result; 343 | }, 344 | 345 | read: function(doc) { 346 | var result = []; 347 | 348 | doc = this.toDocument(doc); 349 | 350 | var osm = doc.createElement("osm"); 351 | this.appendChildren(osm, doc.getElementsByTagName("erase")); 352 | this.appendChildren(osm, doc.getElementsByTagName("keep")); 353 | 354 | var osc = doc.createElement("osmChange"); 355 | this.appendChildren(osc, doc.getElementsByTagName("insert")); 356 | 357 | result = OpenLayers.Format.OSC.prototype.read.apply(this, [osc, osm]); 358 | 359 | return result; 360 | }, 361 | 362 | appendChildren: function(node, children) { 363 | var clone, i; 364 | for (i = 0; i < children.length; i++) { 365 | clone = children[i].cloneNode(true); 366 | node.appendChild(clone); 367 | } 368 | }, 369 | 370 | isAugmented: function () { 371 | return true; 372 | }, 373 | 374 | toDocument: function(stringOrDoc) { 375 | if (typeof stringOrDoc == "string") { 376 | stringOrDoc = OpenLayers.Format.XML.prototype.read.apply(this, [stringOrDoc]); 377 | } 378 | return stringOrDoc; 379 | }, 380 | 381 | CLASS_NAME: "OpenLayers.Format.OSCAugmentedDiff" 382 | }); 383 | /* Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for 384 | * full list of contributors). Published under the 2-clause BSD license. 385 | * See license.txt in the OpenLayers distribution or repository for the 386 | * full text of the license. */ 387 | 388 | /** 389 | * @requires OSC.js 390 | */ 391 | 392 | /** 393 | * Class: OpenLayers.Format.OSCAugmentedDiffIDSorted Overpass API augmented diff 394 | * parser. Create a new instance with the 395 | * constructor. 396 | * 397 | * Inherits from: - 398 | */ 399 | OpenLayers.Format.OSCAugmentedDiffIDSorted = OpenLayers.Class(OpenLayers.Format.OSC, { 400 | 401 | initialize : function(options) { 402 | OpenLayers.Format.OSC.prototype.initialize.apply(this, [ options ]); 403 | }, 404 | 405 | readAugmenting : function(doc) { 406 | var object; 407 | var old = []; 408 | var change = []; 409 | 410 | doc = this.toDocument(doc); 411 | 412 | var actionElementList = doc.getElementsByTagName("action"); 413 | for ( var i = 0; i < actionElementList.length; i++) { 414 | var actionNode = actionElementList[i]; 415 | var actionType = actionNode.getAttribute("type"); 416 | switch (actionType) { 417 | case 'create': 418 | object = actionNode.firstElementChild; 419 | this.addFeature(change, object, actionNode, actionType); 420 | break; 421 | case 'delete': 422 | case 'modify': 423 | // old 424 | object = actionNode.firstElementChild.firstElementChild; 425 | var oldFeature = this.addFeature(old, object, actionNode, actionType); 426 | // new 427 | object = actionNode.lastElementChild.firstElementChild; 428 | var changeFeature = this.addFeature(change, object, actionNode, actionType); 429 | this.linkFeatures(changeFeature, oldFeature); 430 | break; 431 | case 'info': 432 | // only needed for relations (not handled yet) 433 | break; 434 | default: 435 | console.warn('unhandled action type "' + actionType + '"'); 436 | } 437 | } 438 | 439 | return { 440 | old : old, 441 | change : change, 442 | timestamp : this.getTimestamp(doc) 443 | }; 444 | }, 445 | 446 | getTimestamp: function(doc) { 447 | var timestamp = null; 448 | var metaList = doc.getElementsByTagName("meta"); 449 | if (metaList && metaList.length > 0) { 450 | timestamp = metaList[0].getAttribute('osm_base'); 451 | } 452 | return timestamp; 453 | }, 454 | 455 | read : function(doc) { 456 | var obj = readAugmenting(doc); 457 | return obj.change.concat(obj.old); 458 | }, 459 | 460 | addFeature: function(featureList, object, actionNode, actionType) { 461 | var feature = this.parseFeature(object); 462 | if (feature) { 463 | feature.attributes['action'] = this.getActionString(actionNode, actionType); 464 | if (feature.osm_type === 'node') { 465 | var waymember = actionNode.getAttribute("waymember"); 466 | feature.used = (waymember && waymember === "yes"); 467 | } 468 | featureList.push(feature); 469 | } 470 | return feature; 471 | }, 472 | 473 | parseFeature: function(object) { 474 | var feature = null, 475 | tags; 476 | var type = object.tagName.toLowerCase(); 477 | 478 | tags = this.getTags(object); 479 | 480 | var geometry = this.parseGeometry[type].apply(this, [object, tags]); 481 | if (geometry) { 482 | if (this.internalProjection && this.externalProjection) { 483 | geometry.transform(this.externalProjection, 484 | this.internalProjection); 485 | } 486 | feature = new OpenLayers.Feature.Vector(geometry, tags); 487 | 488 | feature.osm_id = parseInt(object.getAttribute("id")); 489 | feature.osm_type = type; 490 | feature.fid = type + "." + feature.osm_id; 491 | } 492 | 493 | return feature; 494 | }, 495 | 496 | getActionString: function(actionNode, actionType) { 497 | var actionString = actionType; 498 | var geometryAttr; 499 | if (actionType === 'modify') { 500 | geometryAttr = actionNode.getAttribute("geometry"); 501 | if (geometryAttr && geometryAttr === "changed") { 502 | actionString = 'modify:geometry'; 503 | } 504 | } 505 | return actionString; 506 | }, 507 | 508 | /** 509 | * Property: parseGeometry 510 | * Properties of this object are the functions that parse geometries based 511 | * on their type. 512 | */ 513 | parseGeometry: { 514 | node: function(objectNode, tags) { 515 | var geometry = new OpenLayers.Geometry.Point( 516 | objectNode.getAttribute("lon"), 517 | objectNode.getAttribute("lat")); 518 | return geometry; 519 | }, 520 | 521 | way: function(object, tags) { 522 | var geometry, node, point; 523 | var nodeList = object.getElementsByTagName("nd"); 524 | 525 | // We know the minimal of this one ahead of time. (Could be -1 526 | // due to areas/polygons) 527 | var pointList = new Array(nodeList.length); 528 | for (var j = 0; j < nodeList.length; j++) { 529 | node = nodeList[j]; 530 | 531 | point = new OpenLayers.Geometry.Point( 532 | node.getAttribute("lon"), 533 | node.getAttribute("lat")); 534 | 535 | // Since OSM is topological, we stash the node ID internally. 536 | point.osm_id = parseInt(node.getAttribute("ref")); 537 | pointList[j] = point; 538 | } 539 | 540 | if (this.isWayArea(pointList, tags)) { 541 | geometry = new OpenLayers.Geometry.Polygon( 542 | new OpenLayers.Geometry.LinearRing(pointList)); 543 | } else { 544 | geometry = new OpenLayers.Geometry.LineString(pointList); 545 | } 546 | return geometry; 547 | }, 548 | 549 | relation: function(objectNode) { 550 | // not handled yet 551 | }, 552 | }, 553 | 554 | linkFeatures: function(changeFeature, oldFeature) { 555 | if (changeFeature) { 556 | changeFeature.oldFeature = oldFeature; 557 | } 558 | if (oldFeature) { 559 | oldFeature.changeFeature = changeFeature; 560 | } 561 | }, 562 | 563 | // use original, not super method, because action is determined from 564 | // action not object node 565 | getMetaAttributes: OpenLayers.Format.OSMMeta.prototype.getMetaAttributes, 566 | 567 | /** 568 | * Method: isWayArea 569 | * Check whether the tags and geometry indicate something is an area. 570 | * 571 | * Parameters: 572 | * pointList - {Array()} Way nodes 573 | * tags - {Object} Way tags 574 | * 575 | * Returns: 576 | * {Boolean} 577 | */ 578 | isWayArea: function(pointList, tags) { 579 | var poly_shaped = false; 580 | var poly_tags = false; 581 | 582 | if (pointList.length > 2 583 | && pointList[0].osm_id === pointList[pointList.length - 1].osm_id) { 584 | poly_shaped = true; 585 | } 586 | 587 | for(var key in tags) { 588 | if (this.areaTags[key]) { 589 | poly_tags = true; 590 | break; 591 | } 592 | } 593 | 594 | return poly_shaped && poly_tags; 595 | }, 596 | 597 | appendChildren : function(node, children) { 598 | var clone, i; 599 | for (i = 0; i < children.length; i++) { 600 | clone = children[i].cloneNode(true); 601 | node.appendChild(clone); 602 | } 603 | }, 604 | 605 | isAugmented : function() { 606 | return true; 607 | }, 608 | 609 | toDocument : function(stringOrDoc) { 610 | if (typeof stringOrDoc == "string") { 611 | stringOrDoc = OpenLayers.Format.XML.prototype.read.apply(this, [ stringOrDoc ]); 612 | } 613 | return stringOrDoc; 614 | }, 615 | 616 | CLASS_NAME : "OpenLayers.Format.OSCAugmentedDiffIDSorted" 617 | }); 618 | /* Copyright (c) 2006-2012 by OpenLayers Contributors (see authors.txt for 619 | * full list of contributors). Published under the 2-clause BSD license. 620 | * See license.txt in the OpenLayers distribution or repository for the 621 | * full text of the license. */ 622 | 623 | /** 624 | * @requires OpenLayers/Format/XML.js 625 | * @requires OpenLayers/Feature/Vector.js 626 | * @requires OpenLayers/Geometry/Point.js 627 | * @requires OpenLayers/Geometry/LineString.js 628 | * @requires OpenLayers/Geometry/Polygon.js 629 | * @requires OpenLayers/Projection.js 630 | */ 631 | 632 | /** 633 | * Class: OpenLayers.Format.OSM 634 | * OSM parser. Create a new instance with the 635 | * constructor. 636 | * 637 | * Inherits from: 638 | * - 639 | */ 640 | OpenLayers.Format.OSMChangeset = OpenLayers.Class(OpenLayers.Format.XML, { 641 | 642 | metaAttributes: ['user', 'uid', 'created_at', 'closed_at', 'open', 'min_lat', 'min_lon', 'max_lat', 'max_lon'], 643 | 644 | /** 645 | * Constructor: OpenLayers.Format.OSM 646 | * Create a new parser for OSM. 647 | * 648 | * Parameters: 649 | * options - {Object} An optional object whose properties will be set on 650 | * this instance. 651 | */ 652 | initialize: function(options) { 653 | 654 | // OSM coordinates are always in longlat WGS84 655 | this.externalProjection = new OpenLayers.Projection("EPSG:4326"); 656 | 657 | OpenLayers.Format.XML.prototype.initialize.apply(this, [options]); 658 | }, 659 | 660 | /** 661 | * APIMethod: read 662 | * Return changeset from a OSM changeset doc 663 | 664 | * Parameters: 665 | * doc - {Element} 666 | * 667 | * Returns: 668 | * Array({}) 669 | */ 670 | read: function(doc) { 671 | if (typeof doc == "string") { 672 | doc = OpenLayers.Format.XML.prototype.read.apply(this, [doc]); 673 | } 674 | 675 | var feat_list = []; 676 | var changesetNode = null; 677 | var nodeList = doc.getElementsByTagName("changeset"); 678 | if (nodeList.length > 0) { 679 | changesetNode = nodeList[0]; 680 | var tags = this.getTags(changesetNode); 681 | 682 | // left, bottom, right, top 683 | var bounds = new OpenLayers.Bounds(tags.min_lon, tags.min_lat, tags.max_lon, tags.max_lat); 684 | var geometry = bounds.toGeometry(); 685 | if (this.internalProjection && this.externalProjection) { 686 | geometry.transform(this.externalProjection, 687 | this.internalProjection); 688 | } 689 | var feat = new OpenLayers.Feature.Vector(geometry, tags); 690 | feat.osm_id = parseInt(changesetNode.getAttribute("id")); 691 | feat.fid = "changeset." + feat.osm_id; 692 | feat_list.push(feat); 693 | } 694 | 695 | return feat_list; 696 | }, 697 | 698 | getTags: OpenLayers.Format.OSMMeta.prototype.getTags, 699 | 700 | getMetaAttributes: OpenLayers.Format.OSMMeta.prototype.getMetaAttributes, 701 | 702 | CLASS_NAME: "OpenLayers.Format.OSMChangeset" 703 | }); 704 | OpenLayers.Control.LayerSwitcherBorder = OpenLayers.Class(OpenLayers.Control.LayerSwitcher, { 705 | 706 | borderDiv: null, 707 | 708 | initialize: function(options) { 709 | OpenLayers.Control.LayerSwitcher.prototype.initialize.apply(this, arguments); 710 | }, 711 | 712 | draw: function() { 713 | this.borderDiv = OpenLayers.Control.prototype.draw.apply(this); 714 | this.div = null; 715 | /* 716 | this.borderDiv = document.createElement("div"); 717 | //OpenLayers.Element.addClass(this.borderDiv, "border"); 718 | OpenLayers.Element.addClass(this.borderDiv, this.displayClass); 719 | if (!this.allowSelection) { 720 | this.borderDiv.className += " olControlNoSelect"; 721 | this.borderDiv.setAttribute("unselectable", "on", 0); 722 | this.borderDiv.onselectstart = OpenLayers.Function.False; 723 | } 724 | */ 725 | 726 | OpenLayers.Control.LayerSwitcher.prototype.draw.apply(this); 727 | 728 | //this.div.style.width = this.borderDiv.style.width; 729 | //this.borderDiv.style.width = "auto"; 730 | this.div.className = "layerSwitcherDiv"; 731 | this.div.style.position = ""; 732 | //OpenLayers.Element.addClass(this.div, "layerSwitcherDiv"); 733 | //this.borderDiv.id = this.div.id; 734 | this.div.id = this.div.id + "_layerSwitcherDiv"; 735 | 736 | this.maximizeDiv.style.position = ""; 737 | 738 | this.borderDiv.appendChild(this.div); 739 | 740 | // OpenLayers.Util.modifyAlphaImageDiv(this.maximizeDiv, null, null, {w: 22, h: 22}); 741 | 742 | return this.borderDiv; 743 | }, 744 | 745 | maximizeControl: function(e) { 746 | 747 | // set the div's width and height to empty values, so 748 | // the div dimensions can be controlled by CSS 749 | // this.div.style.width = ""; 750 | // this.div.style.height = ""; 751 | this.layersDiv.style.display = ""; 752 | 753 | this.showControls(false); 754 | 755 | if (e != null) { 756 | OpenLayers.Event.stop(e); 757 | } 758 | }, 759 | 760 | minimizeControl: function(e) { 761 | 762 | // to minimize the control we set its div's width 763 | // and height to 0px, we cannot just set "display" 764 | // to "none" because it would hide the maximize 765 | // div 766 | // this.div.style.width = "0px"; 767 | // this.div.style.height = "0px"; 768 | this.layersDiv.style.display = "none"; 769 | 770 | this.showControls(true); 771 | 772 | if (e != null) { 773 | OpenLayers.Event.stop(e); 774 | } 775 | } 776 | 777 | // CLASS_NAME: keep parent name because CSS classes are named after this 778 | }); 779 | /** 780 | * Reads files using HTML5 file API 781 | * 782 | * derived from http://www.html5rocks.com/en/tutorials/file/dndfiles/ 783 | */ 784 | function FileReaderControl(onLoadCallback) { 785 | this.onLoadCallback = onLoadCallback; 786 | 787 | // {urlRegex:, handler:} objects 788 | this.urlHandlers = []; 789 | } 790 | 791 | FileReaderControl.prototype.addUrlHandler = function(urlRegex, handler) { 792 | this.urlHandlers.push({urlRegex: urlRegex, handler: handler}); 793 | }; 794 | 795 | FileReaderControl.prototype.activate = function() { 796 | if (window.File && window.FileReader && window.FileList) { 797 | document.getElementById('fileinput').addEventListener('change', _.bind(this.handleFileSelect, this), false); 798 | 799 | var dropZone = document.getElementById('map_div'); 800 | dropZone.addEventListener('dragover', this.handleDragOver, false); 801 | dropZone.addEventListener('drop', _.bind(this.handleFileSelect, this), false); 802 | } else { 803 | // File API not supported 804 | document.getElementById('fileinput').disabled = true; 805 | console.warn('Browser does not support the HTML5 File API!'); 806 | } 807 | }; 808 | 809 | FileReaderControl.prototype.handleFileSelect = function(evt) { 810 | var files, file; 811 | 812 | evt.stopPropagation(); 813 | evt.preventDefault(); 814 | 815 | // FileList from file input or drag'n'drop 816 | files = evt.target.files || evt.dataTransfer.files; 817 | if (files.length > 0) { 818 | file = files[0]; 819 | console.log('handleFileSelect: ' + file.name); 820 | 821 | if (file.type === 'application/xml' || file.type === 'text/xml' || !file.type) { 822 | var fileReader = new FileReader(); 823 | var handleLoad = _.bind(this.onLoadCallback, this); 824 | fileReader.onload = function(evt) { 825 | var text = evt.target.result; 826 | handleLoad(text, file.name); 827 | }; 828 | fileReader.onerror = function(evt) { 829 | console.error('Error: ' + evt.target.error.code); 830 | }; 831 | fileReader.readAsText(file); 832 | } else { 833 | console.error("File type '" + file.type + "' not recognized as XML for " + file.name); 834 | } 835 | } else if (evt.dataTransfer ) { 836 | var url = evt.dataTransfer.getData("URL"); 837 | if (url) { 838 | var handled = false; 839 | for (var i = 0; i < this.urlHandlers.length; i++) { 840 | var obj = this.urlHandlers[i]; 841 | if (obj.urlRegex.test(url)) { 842 | obj.handler(url); 843 | handled = true; 844 | break; 845 | } 846 | } 847 | if (!handled) { 848 | console.warn("no handler found for url: " + url); 849 | } 850 | } else { 851 | console.warn("unhandled event dataTransfer: " + evt.dataTransfer); 852 | } 853 | } else { 854 | console.warn("unhandled event: " + evt); 855 | } 856 | }; 857 | 858 | FileReaderControl.prototype.handleDragOver = function(evt) { 859 | evt.stopPropagation(); 860 | evt.preventDefault(); 861 | evt.dataTransfer.dropEffect = 'copy'; 862 | }; 863 | OpenLayers.Control.HoverAndSelectFeature = OpenLayers.Class(OpenLayers.Control.SelectFeature, { 864 | initialize : function(layers, options) { 865 | this.hover = true; 866 | OpenLayers.Control.SelectFeature.prototype.initialize.apply(this, [ layers, options ]); 867 | 868 | // allow map panning while feature hovered or selected 869 | this.handlers['feature'].stopDown = false; 870 | this.handlers['feature'].stopUp = false; 871 | }, 872 | 873 | clickFeature : function(feature) { 874 | if (this.hover) { 875 | this.hover = false; 876 | if (!this.highlightOnly) { 877 | // feature already selected by hover, unselect before calling super, 878 | // which is done to allow select handler to distinguish between hover and click 879 | this.unselect(feature); 880 | } 881 | } 882 | OpenLayers.Control.SelectFeature.prototype.clickFeature.apply(this, [ feature ]); 883 | }, 884 | 885 | clickoutFeature : function(feature) { 886 | OpenLayers.Control.SelectFeature.prototype.clickoutFeature.apply(this, [ feature ]); 887 | this.hover = true; 888 | }, 889 | 890 | CLASS_NAME : "OpenLayers.Control.HoverAndSelectFeature" 891 | }); 892 | var bbox = (function() { 893 | 894 | var drawFeature, transform; 895 | var map, bboxLayer; 896 | 897 | /** 898 | * update, activate, deactivate 899 | */ 900 | var callbacks; 901 | 902 | var style = { 903 | "default" : { 904 | fillColor : "#FFD119", 905 | fillOpacity : 0.1, 906 | strokeWidth : 2, 907 | strokeColor : "#333", 908 | strokeDashstyle : "solid" 909 | }, 910 | "select" : { 911 | fillOpacity : 0.2, 912 | strokeWidth : 2.5, 913 | }, 914 | "temporary" : { 915 | fillColor : "#FFD119", 916 | fillOpacity : 0.1, 917 | strokeDashstyle : "longdash" 918 | }, 919 | "transform" : { 920 | display : "${getDisplay}", 921 | cursor : "${role}", 922 | pointRadius : 6, 923 | fillColor : "rgb(158, 158, 158)", 924 | fillOpacity : 1, 925 | strokeColor : "#333", 926 | strokeWidth : 2, 927 | strokeOpacity : 1 928 | } 929 | }; 930 | 931 | function createStyleMap() { 932 | 933 | var styleMap = new OpenLayers.StyleMap({ 934 | //"default" : new OpenLayers.Style(defaultStyle), 935 | "default" : new OpenLayers.Style(style["default"]), 936 | "select" : new OpenLayers.Style(style["select"]), 937 | "temporary" : new OpenLayers.Style(style["temporary"]), 938 | // render intent for TransformFeature control 939 | "transform" : new OpenLayers.Style(style["transform"], { 940 | context : { 941 | getDisplay : function(feature) { 942 | // Hide transform box, as it's styling is limited because of underlying bbox feature. 943 | // Instead, the render intent of the bbox feature is assigned separately. 944 | return feature.geometry.CLASS_NAME === "OpenLayers.Geometry.LineString" ? "none" : ""; 945 | }, 946 | } 947 | }) 948 | }); 949 | /* debug 950 | var orig = OpenLayers.StyleMap.prototype.createSymbolizer; 951 | OpenLayers.StyleMap.prototype.createSymbolizer = function(feature, intent) { 952 | var ret = orig.apply(this, arguments); 953 | console.log(intent + '( ' + this.extendDefault + '): ' + JSON.stringify(ret)); 954 | return ret; 955 | }; 956 | */ 957 | 958 | return styleMap; 959 | } 960 | 961 | function featureInsert(feature) { 962 | drawFeatureDeactivate(); 963 | callbacks.update(getBBox(feature)); 964 | } 965 | 966 | function onTransformComplete(evt) { 967 | callbacks.update(getBBox(evt.feature)); 968 | } 969 | 970 | function drawFeatureActivate() { 971 | drawFeature.activate(); 972 | if (transform.active) { 973 | transform.deactivate(); 974 | } 975 | bboxLayer.destroyFeatures(); 976 | 977 | // crosshair cursor 978 | OpenLayers.Element.addClass(map.viewPortDiv, "olDrawBox"); 979 | 980 | callbacks.activate(); 981 | } 982 | 983 | function drawFeatureDeactivate() { 984 | drawFeature.deactivate(); 985 | 986 | // default cursor (remove crosshair cursor) 987 | OpenLayers.Element.removeClass(map.viewPortDiv, "olDrawBox"); 988 | 989 | callbacks.deactivate(); 990 | } 991 | 992 | function switchActive() { 993 | if (!drawFeature.active) { 994 | drawFeatureActivate(); 995 | } else { 996 | drawFeatureDeactivate(); 997 | } 998 | } 999 | 1000 | function addControls(pMap, pBboxLayer, pCallbacks) { 1001 | 1002 | callbacks = pCallbacks; 1003 | bboxLayer = pBboxLayer; 1004 | map = pMap; 1005 | 1006 | // draw control 1007 | /* TODO: use feature label or popup to update coordinates while drawing 1008 | var onMove = function(geometry) { 1009 | updateInfo(new OpenLayers.Feature.Vector(geometry)); 1010 | }; 1011 | */ 1012 | var polyOptions = { 1013 | irregular : true, 1014 | // allow dragging beyond map viewport 1015 | documentDrag : true 1016 | }; 1017 | drawFeature = new OpenLayers.Control.DrawFeature(bboxLayer, OpenLayers.Handler.RegularPolygon, { 1018 | handlerOptions : polyOptions 1019 | /* 1020 | ,callbacks : { 1021 | move : onMove 1022 | } 1023 | */ 1024 | }); 1025 | drawFeature.featureAdded = featureInsert; 1026 | map.addControl(drawFeature); 1027 | 1028 | // feature edit control (move and resize), activated by select control 1029 | transform = new OpenLayers.Control.TransformFeature(bboxLayer, { 1030 | renderIntent : "transform", 1031 | rotate : false, 1032 | irregular : true 1033 | }); 1034 | transform.events.register("transformcomplete", transform, onTransformComplete); 1035 | map.addControl(transform); 1036 | 1037 | // select control 1038 | // - highlight feature on hover to indicate that it is clickable 1039 | // - activate editing on click (select), deactivate editing on click on map (unselect) 1040 | var select = new OpenLayers.Control.HoverAndSelectFeature(bboxLayer, { 1041 | hover : true, 1042 | highlightOnly : true, 1043 | onSelect : function(feature) { 1044 | select.unhighlight(feature); 1045 | transform.setFeature(feature); 1046 | feature.renderIntent = "temporary"; 1047 | bboxLayer.drawFeature(feature); 1048 | }, 1049 | onUnselect : function(feature) { 1050 | transform.unsetFeature(); 1051 | feature.renderIntent = "default"; 1052 | bboxLayer.drawFeature(feature); 1053 | } 1054 | }); 1055 | 1056 | map.addControl(select); 1057 | select.activate(); 1058 | } 1059 | 1060 | function getBBox(feature) { 1061 | return roundAndTransform(feature.geometry.getBounds()); 1062 | } 1063 | 1064 | function roundAndTransform(aBounds) { 1065 | var bounds = aBounds.clone().transform(map.getProjectionObject(), map.displayProjection); 1066 | 1067 | var decimals = Math.floor(map.getZoom() / 3); 1068 | var multiplier = Math.pow(10, decimals); 1069 | 1070 | // custom float.toFixed function that rounds to integer when .0 1071 | // see OpenLayers.Bounds.toBBOX 1072 | var toFixed = function(num) { 1073 | return Math.round(num * multiplier) / multiplier; 1074 | }; 1075 | 1076 | // (left, bottom, right, top) 1077 | var box = new OpenLayers.Bounds( 1078 | toFixed(bounds.left), 1079 | toFixed(bounds.bottom), 1080 | toFixed(bounds.right), 1081 | toFixed(bounds.top) 1082 | ); 1083 | 1084 | return box; 1085 | } 1086 | 1087 | function addBBoxFromViewPort() { 1088 | var bounds = map.getExtent(); 1089 | bboxLayer.addFeatures([new OpenLayers.Feature.Vector(bounds.toGeometry())]); 1090 | 1091 | return roundAndTransform(bounds); 1092 | } 1093 | 1094 | return { 1095 | style: style, 1096 | createStyleMap : createStyleMap, 1097 | addControls : addControls, 1098 | switchActive : switchActive, 1099 | addBBoxFromViewPort : addBBoxFromViewPort 1100 | }; 1101 | })();(function() { 1102 | /* 1103 | * Patches OpenLayers.Request.issue. 1104 | * Adds config option "disableXRequestedWith" to disable setting X-Requested-With header. 1105 | * (Error in Chrome: "Request header field X-Requested-With is not allowed by Access-Control-Allow-Headers") 1106 | * see https://github.com/openlayers/openlayers/issues/188 1107 | */ 1108 | 1109 | var funcOldStr = OpenLayers.Request.issue.toString(); 1110 | var replacement = "customRequestedWithHeader === false && !(config.disableXRequestedWith === true)"; 1111 | 1112 | // support both compressed and uncompressed 1113 | var funcNewStr = funcOldStr.replace("customRequestedWithHeader===false", replacement); 1114 | funcNewStr = funcNewStr.replace("customRequestedWithHeader === false", replacement); 1115 | 1116 | eval('OpenLayers.Request.issue = ' + funcNewStr); 1117 | console.warn('patched OpenLayers.Request.issue'); 1118 | //console.debug(OpenLayers.Request.issue); 1119 | })(); 1120 | -------------------------------------------------------------------------------- /lib/overpass.js: -------------------------------------------------------------------------------- 1 | // https://github.com/drolbr/Overpass-API/blob/master/html/overpass.js 2 | // with modifications 3 | 4 | OpenLayers.Control.Click = OpenLayers.Class(OpenLayers.Control, { 5 | 6 | genericUrl: "", 7 | tolerance: 0.0, 8 | map: "", 9 | 10 | defaultHandlerOptions: { 11 | 'single': true, 12 | 'double': false, 13 | 'pixelTolerance': 0, 14 | 'stopSingle': false, 15 | 'stopDouble': false 16 | }, 17 | 18 | initialize: function(url, tolerance, map) { 19 | this.genericUrl = url; 20 | this.tolerance = tolerance; 21 | this.map = map; 22 | 23 | this.handlerOptions = OpenLayers.Util.extend( 24 | {}, this.defaultHandlerOptions 25 | ); 26 | OpenLayers.Control.prototype.initialize.apply( 27 | this, arguments 28 | ); 29 | this.handler = new OpenLayers.Handler.Click( 30 | this, { 31 | 'click': this.trigger 32 | }, this.handlerOptions 33 | ); 34 | }, 35 | 36 | trigger: function(evt) { 37 | var lonlat = map.getLonLatFromPixel(evt.xy) 38 | .transform(new OpenLayers.Projection("EPSG:900913"), 39 | new OpenLayers.Projection("EPSG:4326")); 40 | 41 | var popup = new OpenLayers.Popup("location_info", 42 | new OpenLayers.LonLat(lonlat.lon, lonlat.lat) 43 | .transform(new OpenLayers.Projection("EPSG:4326"), new OpenLayers.Projection("EPSG:900913")), 44 | null, 45 | "Loading ...", 46 | true); 47 | 48 | popup.contentSize = new OpenLayers.Size(400, 400); 49 | popup.closeOnMove = true; 50 | map.addPopup(popup); 51 | 52 | var rel_tolerance = this.tolerance * map.getScale(); 53 | if (rel_tolerance > 0.01) 54 | rel_tolerance = 0.01; 55 | 56 | var request = OpenLayers.Request.GET({ 57 | url: this.genericUrl + "&bbox=" 58 | + (lonlat.lon - rel_tolerance) + "," + (lonlat.lat - rel_tolerance) + "," 59 | + (lonlat.lon + rel_tolerance) + "," + (lonlat.lat + rel_tolerance), 60 | async: false 61 | }); 62 | 63 | map.removePopup(popup); 64 | popup.contentHTML = request.responseText; 65 | map.addPopup(popup); 66 | } 67 | 68 | }); 69 | 70 | // ---------------------------------------------------------------------------- 71 | 72 | function setStatusText(text) 73 | { 74 | var html_node = document.getElementById("statusline"); 75 | if (html_node != null) 76 | { 77 | var div = html_node.firstChild; 78 | div.deleteData(0, div.nodeValue.length); 79 | div.appendData(text); 80 | } 81 | } 82 | 83 | var zoom_valid = true; 84 | var load_counter = 0; 85 | 86 | function make_features_added_closure(layer) { 87 | var layer_ = layer; 88 | return function(evt) { 89 | setStatusText("Displaying " + layer_.features.length + " features ..."); 90 | }; 91 | } 92 | 93 | ZoomLimitedBBOXStrategy = OpenLayers.Class(OpenLayers.Strategy.BBOX, { 94 | 95 | zoom_data_limit: 13, 96 | 97 | initialize: function(zoom_data_limit) { 98 | this.zoom_data_limit = zoom_data_limit; 99 | }, 100 | 101 | update: function(options) { 102 | this.ratio = this.layer.ratio; 103 | var mapBounds = this.getMapBounds(); 104 | if (this.layer && this.layer.map && this.layer.map.getZoom() < this.zoom_data_limit) { 105 | setStatusText("Please zoom in to view data."); 106 | zoom_valid = false; 107 | 108 | this.bounds = null; 109 | } 110 | else if (mapBounds !== null && ((options && options.force) || 111 | this.invalidBounds(mapBounds))) { 112 | ++load_counter; 113 | setStatusText("Loading data ..."); 114 | zoom_valid = true; 115 | 116 | this.calculateBounds(mapBounds); 117 | this.resolution = this.layer.map.getResolution(); 118 | this.triggerRead(options); 119 | } 120 | }, 121 | 122 | CLASS_NAME: "ZoomLimitedBBOXStrategy" 123 | }); 124 | 125 | OSMTimeoutFormat = OpenLayers.Class(OpenLayers.Format.OSM, { 126 | 127 | initialize: function(strategy) { 128 | this.strategy = strategy; 129 | }, 130 | 131 | read: function(doc) 132 | { 133 | if (typeof doc == "string") { 134 | doc = OpenLayers.Format.XML.prototype.read.apply(this, [doc]); 135 | } 136 | 137 | var feat_list = OpenLayers.Format.OSM.prototype.read.apply(this, [doc]); 138 | 139 | if (this.strategy) 140 | { 141 | var node_list = doc.getElementsByTagName("remark"); 142 | if (node_list.length > 0) 143 | { 144 | setStatusText("Please zoom in to view data."); 145 | this.strategy.bounds = null; 146 | } 147 | } 148 | 149 | return feat_list; 150 | }, 151 | 152 | strategy: null, 153 | 154 | CLASS_NAME: "OSMTimeoutFormat" 155 | }); 156 | 157 | //----------------------------------------------------------------------------- 158 | 159 | var OSMDiffFormat = OpenLayers.Class(OpenLayers.Format.OSMMeta, { 160 | 161 | extent: {}, 162 | 163 | setStatus: function(status) {}, 164 | 165 | pushTextualResult: function(feature) {}, 166 | 167 | initialize: function(options) 168 | { 169 | OpenLayers.Format.OSMMeta.prototype.initialize.apply(this, [ options ]); 170 | 171 | if (options && options.extent) 172 | this.extent = options.extent; 173 | if (options && options.setStatus) 174 | this.setStatus = options.setStatus; 175 | if (options && options.pushTextualResult) 176 | this.pushTextualResult = options.pushTextualResult; 177 | }, 178 | 179 | read: function(doc) 180 | { 181 | this.setStatus("Processing data"); 182 | 183 | if (typeof doc == "string") 184 | doc = OpenLayers.Format.XML.prototype.read.apply(this, [doc]); 185 | 186 | var feat_list = []; 187 | 188 | var relations = this.getRelations(doc); 189 | for (var relation_id in relations) 190 | feat_list.push(relations[relation_id]); 191 | 192 | var ways = this.getWays(doc); 193 | for (var way_id in ways) 194 | feat_list.push(ways[way_id]); 195 | 196 | var nodes = this.getNodes(doc); 197 | for (var node_id in nodes) 198 | feat_list.push(nodes[node_id]); 199 | 200 | this.setStatus("Ready"); 201 | return feat_list; 202 | }, 203 | 204 | isInExtent: function(node) 205 | { 206 | if (!(node.getAttribute("lon") && node.getAttribute("lat"))) 207 | if (node.nodeName === "node") 208 | return true; // keep deleted nodes for popup info 209 | else 210 | return false; // filter out way/relation node refs without geometry (outside bbox, because of geom(bbox)) 211 | 212 | if (!this.extent) 213 | return true; 214 | 215 | var geom = new OpenLayers.Geometry.Point( 216 | node.getAttribute("lon"), node.getAttribute("lat")); 217 | return (geom.x <= this.extent.right && geom.x >= this.extent.left && 218 | geom.y <= this.extent.top && geom.y >= this.extent.bottom); 219 | }, 220 | 221 | stateOf: function(element) 222 | { 223 | var state = {}; 224 | if (element.parentNode.nodeName == "old") 225 | state = { state: "old" }; 226 | else 227 | state = { state: "new" }; 228 | 229 | return state; 230 | }, 231 | 232 | getAction: function(element) 233 | { 234 | var actionElement = element.parentNode; 235 | 236 | if (actionElement.nodeName !== 'action') { 237 | actionElement = actionElement.parentNode; 238 | } 239 | return actionElement.getAttribute('type'); 240 | }, 241 | 242 | pointGeomFromNode: function(node) 243 | { 244 | var geom = new OpenLayers.Geometry.Point( 245 | node.getAttribute("lon"), node.getAttribute("lat")); 246 | if (this.internalProjection && this.externalProjection) 247 | geom.transform(this.externalProjection, this.internalProjection); 248 | geom.ref = node.getAttribute('ref'); 249 | return geom; 250 | }, 251 | 252 | pointListFromWay: function(node_list) 253 | { 254 | var lower = 0; 255 | while (lower < node_list.length && !this.isInExtent(node_list[lower])) 256 | ++lower; 257 | if (lower > 0 && node_list[lower-1].getAttribute("lat") && node_list[lower-1].getAttribute("lon")) 258 | --lower; 259 | 260 | var upper = node_list.length; 261 | while (upper > 0 && !this.isInExtent(node_list[upper-1])) 262 | --upper; 263 | if (upper < node_list.length 264 | && node_list[upper].getAttribute("lat") && node_list[upper].getAttribute("lon")) 265 | ++upper; 266 | 267 | if (upper < lower) 268 | return new Array(); 269 | 270 | var point_list = new Array(upper - lower); 271 | var pos = 0; 272 | for (var j = lower; j < upper; ++j) 273 | { 274 | if (node_list[j].getAttribute("lat") && node_list[j].getAttribute("lon")) 275 | point_list[pos++] = this.pointGeomFromNode(node_list[j]); 276 | } 277 | point_list = point_list.slice(0, pos); 278 | 279 | return point_list; 280 | }, 281 | 282 | getNodes: function(doc) 283 | { 284 | var node_list = doc.getElementsByTagName("node"); 285 | var usedNodesList = doc.querySelectorAll('nd[ref]'); 286 | var usedNodes = {}; 287 | var nodes = {}; 288 | var i; 289 | 290 | for (i = 0; i < usedNodesList.length; ++i) { 291 | usedNodes[usedNodesList[i].getAttribute('ref')] = true; 292 | } 293 | 294 | for (i = 0; i < node_list.length; ++i) 295 | { 296 | var node = node_list[i]; 297 | var id = node.getAttribute("id"); 298 | 299 | var state = this.stateOf(node); 300 | var feat = new OpenLayers.Feature.Vector(this.pointGeomFromNode(node), state); 301 | feat.tags = this.getTags(node); 302 | feat.osm_id = parseInt(id); 303 | feat.osm_version = parseInt(node.getAttribute("version")); 304 | feat.type = "node"; 305 | feat.fid = "node." + feat.osm_id; 306 | feat.geometry.osm_id = feat.osm_id; 307 | feat.action = this.getAction(node); 308 | feat.used = !!usedNodes[feat.osm_id]; 309 | 310 | if (this.isInExtent(node)) 311 | { 312 | nodes[id + "." + state.state] = feat; 313 | this.pushTextualResult(feat); 314 | } 315 | } 316 | return nodes; 317 | }, 318 | 319 | getWays: function(doc) { 320 | var way_list = doc.getElementsByTagName("way"); 321 | var ways = {}; 322 | for (var i = 0; i < way_list.length; ++i) 323 | { 324 | var way = way_list[i]; 325 | var id = way.getAttribute("id"); 326 | 327 | var state = this.stateOf(way); 328 | var way_nodes = this.pointListFromWay(way.getElementsByTagName("nd")); 329 | var feat = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.LineString(way_nodes), state); 330 | feat.tags = this.getTags(way); 331 | feat.osm_id = parseInt(id); 332 | feat.osm_version = parseInt(way.getAttribute("version")); 333 | feat.type = "way"; 334 | feat.fid = "way." + feat.osm_id; 335 | feat.action = this.getAction(way); 336 | 337 | // do add (deleted) ways without geometry for popup info 338 | // if (way_nodes.length >= 2) 339 | // { 340 | ways[id + "." + state.state] = feat; 341 | this.pushTextualResult(feat); 342 | // } 343 | } 344 | return ways; 345 | }, 346 | 347 | getRelations: function(doc) { 348 | var relation_list = doc.getElementsByTagName("relation"); 349 | var return_relations = {}; 350 | for (var i = 0; i < relation_list.length; ++i) 351 | { 352 | var relation = relation_list[i]; 353 | var id = relation.getAttribute("id"); 354 | 355 | var member_list = relation.getElementsByTagName("member"); 356 | var geom = new OpenLayers.Geometry.Collection(); 357 | var component; 358 | for (var j = 0; j < member_list.length; ++j) 359 | { 360 | component = null; 361 | if (member_list[j].getAttribute("type") == "node" && this.isInExtent(member_list[j])) 362 | component = this.pointGeomFromNode(member_list[j]); 363 | else if (member_list[j].getAttribute("type") == "way") 364 | { 365 | var way_nodes = this.pointListFromWay(member_list[j].getElementsByTagName("nd")); 366 | if (way_nodes.length >= 2) 367 | component = new OpenLayers.Geometry.LineString(way_nodes); 368 | } 369 | if (component) { 370 | component.ref = member_list[j].getAttribute("ref"); 371 | component.role = member_list[j].getAttribute("role"); 372 | geom.addComponent(component); 373 | } 374 | } 375 | 376 | var state = this.stateOf(relation); 377 | var feat = new OpenLayers.Feature.Vector(geom, state); 378 | feat.tags = this.getTags(relation); 379 | feat.osm_id = parseInt(id); 380 | feat.osm_version = parseInt(relation.getAttribute("version")); 381 | feat.type = "relation"; 382 | feat.fid = "relation." + feat.osm_id; 383 | feat.action = this.getAction(relation); 384 | 385 | // do add (deleted) relations without geometry for popup info 386 | // if (geom.components.length > 0) 387 | // { 388 | return_relations[id + "." + state.state] = feat; 389 | this.pushTextualResult(feat); 390 | // } 391 | } 392 | return return_relations; 393 | }, 394 | 395 | strategy: null, 396 | 397 | CLASS_NAME: "OSMDiffFormat" 398 | }); 399 | 400 | //----------------------------------------------------------------------------- 401 | 402 | function make_large_layer(data_url, color, zoom) { 403 | 404 | var styleMap = new OpenLayers.StyleMap({ 405 | strokeColor: color, 406 | strokeOpacity: 0.5, 407 | strokeWidth: 6, 408 | pointRadius: 10, 409 | fillColor: color, 410 | fillOpacity: 0.25 411 | }); 412 | 413 | var strategy = new ZoomLimitedBBOXStrategy(zoom); 414 | var layer = new OpenLayers.Layer.Vector(color, { 415 | strategies: [strategy], 416 | protocol: new OpenLayers.Protocol.HTTP({ 417 | url: data_url, 418 | format: new OSMTimeoutFormat(strategy) 419 | }), 420 | styleMap: styleMap, 421 | projection: new OpenLayers.Projection("EPSG:4326"), 422 | ratio: 1.0 423 | }); 424 | 425 | layer.events.register("featuresadded", layer, make_features_added_closure(layer)); 426 | 427 | return layer; 428 | } 429 | 430 | function make_layer(data_url, color) { 431 | return make_large_layer(data_url, color, 8); 432 | } 433 | -------------------------------------------------------------------------------- /licenses/momentjs-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2012 Tim Wood 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /licenses/openlayers-authors.txt: -------------------------------------------------------------------------------- 1 | OpenLayers contributors: 2 | 3 | Antoine Abt 4 | Mike Adair 5 | Jeff Adams 6 | Seb Benthall 7 | Bruno Binet 8 | Stéphane Brunner 9 | Howard Butler 10 | Bertil Chaupis 11 | John Cole 12 | Tim Coulter 13 | Robert Coup 14 | Jeff Dege 15 | Roald de Wit 16 | Schuyler Erle 17 | Christian López Espínola 18 | John Frank 19 | Sean Gilles 20 | Pierre Giraud 21 | Ivan Grcic 22 | Andreas Hocevar 23 | Marc Jansen 24 | Ian Johnson 25 | Frédéric Junod 26 | Eric Lemoine 27 | Philip Lindsay 28 | Martijn van Oosterhout 29 | David Overstrom 30 | Corey Puffault 31 | Peter William Robins 32 | Gregers Rygg 33 | Tim Schaub 34 | Christopher Schmidt 35 | Cameron Shorter 36 | Pedro Simonetti 37 | Paul Spencer 38 | Paul Smith 39 | Glen Stampoultzis 40 | James Stembridge 41 | Erik Uzureau 42 | Bart van den Eijnden 43 | Ivan Willig 44 | Thomas Wood 45 | Bill Woodall 46 | Steve Woodbridge 47 | David Zwarg 48 | 49 | Some portions of OpenLayers are used under the Apache 2.0 license, available 50 | in doc/licenses/APACHE-2.0.txt. 51 | 52 | Some portions of OpenLayers are used under the MIT license, availabie in 53 | doc/licenses/MIT-LICENSE.txt. 54 | 55 | Some portions of OpenLayers are Copyright 2001 Robert Penner, and are used 56 | under the BSD license, available in doc/licenses/BSD-LICENSE.txt 57 | -------------------------------------------------------------------------------- /licenses/openlayers-license.txt: -------------------------------------------------------------------------------- 1 | Copyright 2005-2012 OpenLayers Contributors. All rights reserved. See 2 | authors.txt for full list. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY OPENLAYERS CONTRIBUTORS ``AS IS'' AND ANY EXPRESS 15 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 16 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 17 | SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 19 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 20 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 21 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 22 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 23 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | The views and conclusions contained in the software and documentation are those 26 | of the authors and should not be interpreted as representing official policies, 27 | either expressed or implied, of OpenLayers Contributors. 28 | -------------------------------------------------------------------------------- /licenses/openlayers_themes-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Development Seed, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | - Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | - Neither the name of the Development Seed, Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived from this 14 | software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /licenses/underscorejs-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2012 Jeremy Ashkenas, DocumentCloud 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # achavi - Augmented OSM Change Viewer 2 | 3 | Displays [OpenStreetMap](openstreetmap.org) changes based on the [Overpass API](https://overpass-api.de/) [Augmented Delta (adiff)](https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#Augmented_Delta_between_two_dates_.28.22adiff.22.29) query and the 4 | [Augmented Diff](https://wiki.openstreetmap.org/wiki/Overpass_API/Augmented_Diffs#Contained_data) format. 5 | 6 | https://overpass-api.de/achavi/ - production (master) 7 | https://nrenner.github.io/achavi/ - development, test (gh-pages) 8 | 9 | *work in progress* 10 | 11 | See [olex](https://github.com/nrenner/olex) repository for OpenLayers-related sources (included as lib/olex.js). 12 | 13 | ## License 14 | 15 | Copyright (c) 2018 Norbert Renner and [contributors](https://github.com/nrenner/achavi/graphs/contributors), licensed under the [MIT License (MIT)](LICENSE) 16 | 17 | ## Install 18 | 19 | Achavi is a pure JavaScript Browser app. It relies on the Overpass API for serving Augmented Diffs. 20 | 21 | The project layout is already the runnable web app, there currently is no build. All files and directories are required at runtime, 22 | except readme.md. 23 | 24 | ## Licenses 25 | 26 | * [OpenLayers](http://www.openlayers.org/): Copyright (c) 2005-2012 OpenLayers [Contributors](licenses/openlayers-authors.txt), [2-clause BSD License](licenses/openlayers-license.txt) 27 | * [Underscore.js](http://underscorejs.org/): Copyright (c) 2009-2012 Jeremy Ashkenas, DocumentCloud, [MIT License](licenses/underscorejs-LICENSE) 28 | * [Moment.js](http://momentjs.com/): Copyright (c) 2011-2012 Tim Wood, [MIT License](licenses/momentjs-LICENSE) 29 | * [Yahoo! Secure XSS Filters](https://github.com/yahoo/xss-filters): Copyright (c) 2015, Yahoo! Inc., [Yahoo BSD license](https://github.com/yahoo/xss-filters/blob/master/LICENSE) 30 | 31 | * loadinggif-4.gif by http://loadinggif.com/ 32 | * [layer-switcher-minimize.png](https://github.com/nrenner/openlayers_themes): Copyright (c) 2010, Development Seed, Inc., [3-clause BSD License](licenses/openlayers_themes-LICENSE.txt) 33 | --------------------------------------------------------------------------------