├── LICENSE ├── README.md ├── bundle.js ├── index.html ├── index.js ├── mapbox-gl-live.js ├── package.json └── script.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 OSM Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wikidata-osm 2 | Apps, tools and scripts to work with Wikidata items from OpenStreetMap 3 | 4 | ## Distance Visualizer 5 | 6 | ![new3](https://cloud.githubusercontent.com/assets/126868/22975383/90cadae6-f3ac-11e6-99aa-7c3b1254129b.gif) 7 | 8 | Use [the distance visualizer](https://osmlab.github.io/wikidata-osm/) to visualize the distance between the OpenStreetMap feature with the Wikidata tag, and the corresponsing coordinate on the Wikidata database. Larger markers indicate a larger distance between the matched features. 9 | 10 | ### Interpreting large match distances 11 | 12 | A large match distance between the mapped feature and WIkidata location could be due to one of the following cases: 13 | 14 | **Valid** 15 | - Large natural features like oceans or continents that do not have a definied point location can be 1000kms apart. [Example](https://osmlab.github.io/wikidata-osm/#3.83/27.72/-172.05) 16 | - Large administrative area features like countries, states and provinces hat do not have a definied point location can be 100kms apart. [Example](https://osmlab.github.io/wikidata-osm/#6.74/15.736/45.964) 17 | 18 | **Inappropriately tagged on OpenStreetMap** 19 | - A Wikidata tag was added to multiple OSM features that describe a single object like ways of a river instead of the river relation. [Example](https://osmlab.github.io/wikidata-osm/#9.34/40.6163/-123.6054) 20 | 21 | **Incorrect Wikidata tag on OpenStreetMap** 22 | - An incorrect Wikidata tag of a similiarly named feature has been added to OSM. [Example](https://osmlab.github.io/wikidata-osm/#12.74/16.8309/75.7144) 23 | 24 | **Incorrect location on Wikidata** 25 | - The Wikidata entry of the feature has an incorrect location. [Example](https://osmlab.github.io/wikidata-osm/#12.85/30.2919/73.0486) 26 | -------------------------------------------------------------------------------- /bundle.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0.1) { 193 | modified = true; 194 | feature.properties.distance = distance; 195 | } 196 | 197 | var popupHTML = populateTable(feature, modified); 198 | var data = { 199 | "type": "FeatureCollection", 200 | "features": [{ 201 | "type": "Feature", 202 | "geometry": { 203 | "type": "Point", 204 | "coordinates": [longitude, latitude] 205 | }, 206 | "properties": { 207 | "icon": "marker" 208 | } 209 | }, { 210 | "type": "Feature", 211 | "geometry": { 212 | "type": "Point", 213 | "coordinates": [feature.geometry.coordinates[0], feature.geometry.coordinates[1]] 214 | }, 215 | "properties": { 216 | "icon": "marker" 217 | } 218 | }] 219 | }; 220 | map.setLayoutProperty('wikidata-layer', 'visibility', 'none'); 221 | map.setLayoutProperty('points-layer', 'visibility', 'visible'); 222 | 223 | map.getSource('points').setData(data); 224 | var bounds = turf_bbox(data); 225 | var buffer; 226 | switch (true) 227 | { 228 | case (distance < 1): buffer = 0.005; 229 | break; 230 | case (distance < 20): buffer = 0.2; 231 | break; 232 | case (distance < 50): buffer = 1; 233 | break; 234 | case (distance < 100): buffer = 2; 235 | break; 236 | case (distance < 500): buffer = 3; 237 | break; 238 | default: buffer = 4; 239 | break; 240 | } 241 | bounds[0] -= buffer; 242 | bounds[1] -= buffer; 243 | bounds[2] += buffer; 244 | bounds[3] += buffer; 245 | map.fitBounds(bounds); 246 | $(location).attr('href', '#sidebar'); 247 | $('#sidebar').html(popupHTML); 248 | document.getElementById('close').onclick = function(){ 249 | map.setLayoutProperty('wikidata-layer', 'visibility','visible'); 250 | map.setLayoutProperty('points-layer', 'visibility', 'none'); 251 | map.getSource('points').setData({}); 252 | window.location = '#container'; 253 | }; 254 | } 255 | }); 256 | 257 | }); 258 | 259 | // Change the mouse to a pointer on hovering over inspectable features 260 | map.on('mousemove', function(e) { 261 | var features = map.queryRenderedFeatures(e.point, {layers: opts.layers}); 262 | map.getCanvas().style.cursor = (features.length) 263 | ? 'pointer' 264 | : ''; 265 | }); 266 | 267 | } 268 | } 269 | 270 | function populateTable(feature, modified) { 271 | // Populate the popup and set its coordinates 272 | // based on the feature found. 273 | 274 | var popupHTML = ""; 275 | 276 | popupHTML += "

" + feature.properties.name + "

"; 277 | popupHTML += "Wikidata
"; 278 | popupHTML += "OSM Search
"; 279 | 280 | popupHTML += ""; 281 | 282 | if (modified) { 283 | popupHTML += "

Modified on wikidata

"; 284 | } 285 | 286 | for (property in feature.properties) { 287 | if (property == 'distance') { 288 | var distance = feature.properties[property]; 289 | popupHTML += ""; 290 | } else { 291 | popupHTML += ""; 292 | } 293 | } 294 | popupHTML += "
" + property + "" + parseFloat(distance.toFixed(3)) + "
" + property + "" + feature.properties[property] + "
"; 295 | 296 | return popupHTML; 297 | } 298 | 299 | function getDistance(lnglat1, lnglat2) { 300 | // Uses spherical law of cosines approximation. 301 | const R = 6371000; 302 | 303 | const rad = Math.PI / 180, 304 | lat1 = lnglat1.lat * rad, 305 | lat2 = lnglat2.lat * rad, 306 | a = Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1) * Math.cos(lat2) * Math.cos((lnglat2.lng - lnglat1.lng) * rad); 307 | 308 | const maxMeters = R * Math.acos(Math.min(a, 1)); 309 | return maxMeters / 1000; 310 | } 311 | 312 | // Generate a nominatim search link for a feature name 313 | function nominatimLink(name, coordinates) { 314 | 315 | const NOMINATIM_BASE = 'http://nominatim.openstreetmap.org/search.php?q='; 316 | 317 | // Limit search to the vicinity of the given coordinates 318 | try { 319 | 320 | var left = coordinates[0] - 1; 321 | var top = coordinates[1] - 1; 322 | var right = coordinates[0] + 1; 323 | var bottom = coordinates[1] + 1; 324 | 325 | var NOMINATIM_OPTS = name + "&polygon=1&bounded=1&viewbox=" + left + "%2C" + top + "%2C" + right + "%2C" + bottom 326 | 327 | } catch (e) { 328 | var NOMINATIM_OPTS = name 329 | } 330 | 331 | return NOMINATIM_BASE + NOMINATIM_OPTS; 332 | 333 | } 334 | 335 | // Export module 336 | module.exports = Live; 337 | 338 | },{"turf-bbox":3,"turf-centroid":5,"xtend":8}],3:[function(require,module,exports){ 339 | var each = require('turf-meta').coordEach; 340 | 341 | /** 342 | * Takes a set of features, calculates the bbox of all input features, and returns a bounding box. 343 | * 344 | * @name bbox 345 | * @param {(Feature|FeatureCollection)} geojson input features 346 | * @return {Array} the bounding box of `input` given 347 | * as an array in WSEN order (west, south, east, north) 348 | * @example 349 | * var input = { 350 | * "type": "FeatureCollection", 351 | * "features": [ 352 | * { 353 | * "type": "Feature", 354 | * "properties": {}, 355 | * "geometry": { 356 | * "type": "Point", 357 | * "coordinates": [114.175329, 22.2524] 358 | * } 359 | * }, { 360 | * "type": "Feature", 361 | * "properties": {}, 362 | * "geometry": { 363 | * "type": "Point", 364 | * "coordinates": [114.170007, 22.267969] 365 | * } 366 | * }, { 367 | * "type": "Feature", 368 | * "properties": {}, 369 | * "geometry": { 370 | * "type": "Point", 371 | * "coordinates": [114.200649, 22.274641] 372 | * } 373 | * }, { 374 | * "type": "Feature", 375 | * "properties": {}, 376 | * "geometry": { 377 | * "type": "Point", 378 | * "coordinates": [114.186744, 22.265745] 379 | * } 380 | * } 381 | * ] 382 | * }; 383 | * 384 | * var bbox = turf.bbox(input); 385 | * 386 | * var bboxPolygon = turf.bboxPolygon(bbox); 387 | * 388 | * var resultFeatures = input.features.concat(bboxPolygon); 389 | * var result = { 390 | * "type": "FeatureCollection", 391 | * "features": resultFeatures 392 | * }; 393 | * 394 | * //=result 395 | */ 396 | module.exports = function (geojson) { 397 | var bbox = [Infinity, Infinity, -Infinity, -Infinity]; 398 | each(geojson, function (coord) { 399 | if (bbox[0] > coord[0]) bbox[0] = coord[0]; 400 | if (bbox[1] > coord[1]) bbox[1] = coord[1]; 401 | if (bbox[2] < coord[0]) bbox[2] = coord[0]; 402 | if (bbox[3] < coord[1]) bbox[3] = coord[1]; 403 | }); 404 | return bbox; 405 | }; 406 | 407 | },{"turf-meta":4}],4:[function(require,module,exports){ 408 | /** 409 | * Iterate over coordinates in any GeoJSON object, similar to 410 | * Array.forEach. 411 | * 412 | * @param {Object} layer any GeoJSON object 413 | * @param {Function} callback a method that takes (value) 414 | * @param {boolean=} excludeWrapCoord whether or not to include 415 | * the final coordinate of LinearRings that wraps the ring in its iteration. 416 | * @example 417 | * var point = { type: 'Point', coordinates: [0, 0] }; 418 | * coordEach(point, function(coords) { 419 | * // coords is equal to [0, 0] 420 | * }); 421 | */ 422 | function coordEach(layer, callback, excludeWrapCoord) { 423 | var i, j, k, g, l, geometry, stopG, coords, 424 | geometryMaybeCollection, 425 | wrapShrink = 0, 426 | isGeometryCollection, 427 | isFeatureCollection = layer.type === 'FeatureCollection', 428 | isFeature = layer.type === 'Feature', 429 | stop = isFeatureCollection ? layer.features.length : 1; 430 | 431 | // This logic may look a little weird. The reason why it is that way 432 | // is because it's trying to be fast. GeoJSON supports multiple kinds 433 | // of objects at its root: FeatureCollection, Features, Geometries. 434 | // This function has the responsibility of handling all of them, and that 435 | // means that some of the `for` loops you see below actually just don't apply 436 | // to certain inputs. For instance, if you give this just a 437 | // Point geometry, then both loops are short-circuited and all we do 438 | // is gradually rename the input until it's called 'geometry'. 439 | // 440 | // This also aims to allocate as few resources as possible: just a 441 | // few numbers and booleans, rather than any temporary arrays as would 442 | // be required with the normalization approach. 443 | for (i = 0; i < stop; i++) { 444 | 445 | geometryMaybeCollection = (isFeatureCollection ? layer.features[i].geometry : 446 | (isFeature ? layer.geometry : layer)); 447 | isGeometryCollection = geometryMaybeCollection.type === 'GeometryCollection'; 448 | stopG = isGeometryCollection ? geometryMaybeCollection.geometries.length : 1; 449 | 450 | for (g = 0; g < stopG; g++) { 451 | geometry = isGeometryCollection ? 452 | geometryMaybeCollection.geometries[g] : geometryMaybeCollection; 453 | coords = geometry.coordinates; 454 | 455 | wrapShrink = (excludeWrapCoord && 456 | (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon')) ? 457 | 1 : 0; 458 | 459 | if (geometry.type === 'Point') { 460 | callback(coords); 461 | } else if (geometry.type === 'LineString' || geometry.type === 'MultiPoint') { 462 | for (j = 0; j < coords.length; j++) callback(coords[j]); 463 | } else if (geometry.type === 'Polygon' || geometry.type === 'MultiLineString') { 464 | for (j = 0; j < coords.length; j++) 465 | for (k = 0; k < coords[j].length - wrapShrink; k++) 466 | callback(coords[j][k]); 467 | } else if (geometry.type === 'MultiPolygon') { 468 | for (j = 0; j < coords.length; j++) 469 | for (k = 0; k < coords[j].length; k++) 470 | for (l = 0; l < coords[j][k].length - wrapShrink; l++) 471 | callback(coords[j][k][l]); 472 | } else { 473 | throw new Error('Unknown Geometry Type'); 474 | } 475 | } 476 | } 477 | } 478 | module.exports.coordEach = coordEach; 479 | 480 | /** 481 | * Reduce coordinates in any GeoJSON object into a single value, 482 | * similar to how Array.reduce works. However, in this case we lazily run 483 | * the reduction, so an array of all coordinates is unnecessary. 484 | * 485 | * @param {Object} layer any GeoJSON object 486 | * @param {Function} callback a method that takes (memo, value) and returns 487 | * a new memo 488 | * @param {*} memo the starting value of memo: can be any type. 489 | * @param {boolean=} excludeWrapCoord whether or not to include 490 | * the final coordinate of LinearRings that wraps the ring in its iteration. 491 | * @return {*} combined value 492 | */ 493 | function coordReduce(layer, callback, memo, excludeWrapCoord) { 494 | coordEach(layer, function (coord) { 495 | memo = callback(memo, coord); 496 | }, excludeWrapCoord); 497 | return memo; 498 | } 499 | module.exports.coordReduce = coordReduce; 500 | 501 | /** 502 | * Iterate over property objects in any GeoJSON object, similar to 503 | * Array.forEach. 504 | * 505 | * @param {Object} layer any GeoJSON object 506 | * @param {Function} callback a method that takes (value) 507 | * @example 508 | * var point = { type: 'Feature', geometry: null, properties: { foo: 1 } }; 509 | * propEach(point, function(props) { 510 | * // props is equal to { foo: 1} 511 | * }); 512 | */ 513 | function propEach(layer, callback) { 514 | var i; 515 | switch (layer.type) { 516 | case 'FeatureCollection': 517 | for (i = 0; i < layer.features.length; i++) { 518 | callback(layer.features[i].properties); 519 | } 520 | break; 521 | case 'Feature': 522 | callback(layer.properties); 523 | break; 524 | } 525 | } 526 | module.exports.propEach = propEach; 527 | 528 | /** 529 | * Reduce properties in any GeoJSON object into a single value, 530 | * similar to how Array.reduce works. However, in this case we lazily run 531 | * the reduction, so an array of all properties is unnecessary. 532 | * 533 | * @param {Object} layer any GeoJSON object 534 | * @param {Function} callback a method that takes (memo, coord) and returns 535 | * a new memo 536 | * @param {*} memo the starting value of memo: can be any type. 537 | * @return {*} combined value 538 | */ 539 | function propReduce(layer, callback, memo) { 540 | propEach(layer, function (prop) { 541 | memo = callback(memo, prop); 542 | }); 543 | return memo; 544 | } 545 | module.exports.propReduce = propReduce; 546 | 547 | /** 548 | * Iterate over features in any GeoJSON object, similar to 549 | * Array.forEach. 550 | * 551 | * @param {Object} layer any GeoJSON object 552 | * @param {Function} callback a method that takes (value) 553 | * @example 554 | * var feature = { type: 'Feature', geometry: null, properties: {} }; 555 | * featureEach(feature, function(feature) { 556 | * // feature == feature 557 | * }); 558 | */ 559 | function featureEach(layer, callback) { 560 | if (layer.type === 'Feature') { 561 | callback(layer); 562 | } else if (layer.type === 'FeatureCollection') { 563 | for (var i = 0; i < layer.features.length; i++) { 564 | callback(layer.features[i]); 565 | } 566 | } 567 | } 568 | module.exports.featureEach = featureEach; 569 | 570 | /** 571 | * Get all coordinates from any GeoJSON object, returning an array of coordinate 572 | * arrays. 573 | * @param {Object} layer any GeoJSON object 574 | * @return {Array>} coordinate position array 575 | */ 576 | function coordAll(layer) { 577 | var coords = []; 578 | coordEach(layer, function (coord) { 579 | coords.push(coord); 580 | }); 581 | return coords; 582 | } 583 | module.exports.coordAll = coordAll; 584 | 585 | },{}],5:[function(require,module,exports){ 586 | var each = require('turf-meta').coordEach; 587 | var point = require('turf-helpers').point; 588 | 589 | /** 590 | * Takes one or more features and calculates the centroid using 591 | * the mean of all vertices. 592 | * This lessens the effect of small islands and artifacts when calculating 593 | * the centroid of a set of polygons. 594 | * 595 | * @name centroid 596 | * @param {(Feature|FeatureCollection)} features input features 597 | * @return {Feature} the centroid of the input features 598 | * @example 599 | * var poly = { 600 | * "type": "Feature", 601 | * "properties": {}, 602 | * "geometry": { 603 | * "type": "Polygon", 604 | * "coordinates": [[ 605 | * [105.818939,21.004714], 606 | * [105.818939,21.061754], 607 | * [105.890007,21.061754], 608 | * [105.890007,21.004714], 609 | * [105.818939,21.004714] 610 | * ]] 611 | * } 612 | * }; 613 | * 614 | * var centroidPt = turf.centroid(poly); 615 | * 616 | * var result = { 617 | * "type": "FeatureCollection", 618 | * "features": [poly, centroidPt] 619 | * }; 620 | * 621 | * //=result 622 | */ 623 | module.exports = function (features) { 624 | var xSum = 0, ySum = 0, len = 0; 625 | each(features, function (coord) { 626 | xSum += coord[0]; 627 | ySum += coord[1]; 628 | len++; 629 | }, true); 630 | return point([xSum / len, ySum / len]); 631 | }; 632 | 633 | },{"turf-helpers":6,"turf-meta":7}],6:[function(require,module,exports){ 634 | /** 635 | * Wraps a GeoJSON {@link Geometry} in a GeoJSON {@link Feature}. 636 | * 637 | * @name feature 638 | * @param {Geometry} geometry input geometry 639 | * @param {Object} properties properties 640 | * @returns {FeatureCollection} a FeatureCollection of input features 641 | * @example 642 | * var geometry = { 643 | * "type": "Point", 644 | * "coordinates": [ 645 | * 67.5, 646 | * 32.84267363195431 647 | * ] 648 | * } 649 | * 650 | * var feature = turf.feature(geometry); 651 | * 652 | * //=feature 653 | */ 654 | function feature(geometry, properties) { 655 | return { 656 | type: 'Feature', 657 | properties: properties || {}, 658 | geometry: geometry 659 | }; 660 | } 661 | 662 | module.exports.feature = feature; 663 | 664 | /** 665 | * Takes coordinates and properties (optional) and returns a new {@link Point} feature. 666 | * 667 | * @name point 668 | * @param {number[]} coordinates longitude, latitude position (each in decimal degrees) 669 | * @param {Object=} properties an Object that is used as the {@link Feature}'s 670 | * properties 671 | * @returns {Feature} a Point feature 672 | * @example 673 | * var pt1 = turf.point([-75.343, 39.984]); 674 | * 675 | * //=pt1 676 | */ 677 | module.exports.point = function (coordinates, properties) { 678 | if (!Array.isArray(coordinates)) throw new Error('Coordinates must be an array'); 679 | if (coordinates.length < 2) throw new Error('Coordinates must be at least 2 numbers long'); 680 | return feature({ 681 | type: 'Point', 682 | coordinates: coordinates.slice() 683 | }, properties); 684 | }; 685 | 686 | /** 687 | * Takes an array of LinearRings and optionally an {@link Object} with properties and returns a {@link Polygon} feature. 688 | * 689 | * @name polygon 690 | * @param {Array>>} coordinates an array of LinearRings 691 | * @param {Object=} properties a properties object 692 | * @returns {Feature} a Polygon feature 693 | * @throws {Error} throw an error if a LinearRing of the polygon has too few positions 694 | * or if a LinearRing of the Polygon does not have matching Positions at the 695 | * beginning & end. 696 | * @example 697 | * var polygon = turf.polygon([[ 698 | * [-2.275543, 53.464547], 699 | * [-2.275543, 53.489271], 700 | * [-2.215118, 53.489271], 701 | * [-2.215118, 53.464547], 702 | * [-2.275543, 53.464547] 703 | * ]], { name: 'poly1', population: 400}); 704 | * 705 | * //=polygon 706 | */ 707 | module.exports.polygon = function (coordinates, properties) { 708 | 709 | if (!coordinates) throw new Error('No coordinates passed'); 710 | 711 | for (var i = 0; i < coordinates.length; i++) { 712 | var ring = coordinates[i]; 713 | if (ring.length < 4) { 714 | throw new Error('Each LinearRing of a Polygon must have 4 or more Positions.'); 715 | } 716 | for (var j = 0; j < ring[ring.length - 1].length; j++) { 717 | if (ring[ring.length - 1][j] !== ring[0][j]) { 718 | throw new Error('First and last Position are not equivalent.'); 719 | } 720 | } 721 | } 722 | 723 | return feature({ 724 | type: 'Polygon', 725 | coordinates: coordinates 726 | }, properties); 727 | }; 728 | 729 | /** 730 | * Creates a {@link LineString} based on a 731 | * coordinate array. Properties can be added optionally. 732 | * 733 | * @name lineString 734 | * @param {Array>} coordinates an array of Positions 735 | * @param {Object=} properties an Object of key-value pairs to add as properties 736 | * @returns {Feature} a LineString feature 737 | * @throws {Error} if no coordinates are passed 738 | * @example 739 | * var linestring1 = turf.lineString([ 740 | * [-21.964416, 64.148203], 741 | * [-21.956176, 64.141316], 742 | * [-21.93901, 64.135924], 743 | * [-21.927337, 64.136673] 744 | * ]); 745 | * var linestring2 = turf.lineString([ 746 | * [-21.929054, 64.127985], 747 | * [-21.912918, 64.134726], 748 | * [-21.916007, 64.141016], 749 | * [-21.930084, 64.14446] 750 | * ], {name: 'line 1', distance: 145}); 751 | * 752 | * //=linestring1 753 | * 754 | * //=linestring2 755 | */ 756 | module.exports.lineString = function (coordinates, properties) { 757 | if (!coordinates) { 758 | throw new Error('No coordinates passed'); 759 | } 760 | return feature({ 761 | type: 'LineString', 762 | coordinates: coordinates 763 | }, properties); 764 | }; 765 | 766 | /** 767 | * Takes one or more {@link Feature|Features} and creates a {@link FeatureCollection}. 768 | * 769 | * @name featureCollection 770 | * @param {Feature[]} features input features 771 | * @returns {FeatureCollection} a FeatureCollection of input features 772 | * @example 773 | * var features = [ 774 | * turf.point([-75.343, 39.984], {name: 'Location A'}), 775 | * turf.point([-75.833, 39.284], {name: 'Location B'}), 776 | * turf.point([-75.534, 39.123], {name: 'Location C'}) 777 | * ]; 778 | * 779 | * var fc = turf.featureCollection(features); 780 | * 781 | * //=fc 782 | */ 783 | module.exports.featureCollection = function (features) { 784 | return { 785 | type: 'FeatureCollection', 786 | features: features 787 | }; 788 | }; 789 | 790 | /** 791 | * Creates a {@link Feature} based on a 792 | * coordinate array. Properties can be added optionally. 793 | * 794 | * @name multiLineString 795 | * @param {Array>>} coordinates an array of LineStrings 796 | * @param {Object=} properties an Object of key-value pairs to add as properties 797 | * @returns {Feature} a MultiLineString feature 798 | * @throws {Error} if no coordinates are passed 799 | * @example 800 | * var multiLine = turf.multiLineString([[[0,0],[10,10]]]); 801 | * 802 | * //=multiLine 803 | * 804 | */ 805 | module.exports.multiLineString = function (coordinates, properties) { 806 | if (!coordinates) { 807 | throw new Error('No coordinates passed'); 808 | } 809 | return feature({ 810 | type: 'MultiLineString', 811 | coordinates: coordinates 812 | }, properties); 813 | }; 814 | 815 | /** 816 | * Creates a {@link Feature} based on a 817 | * coordinate array. Properties can be added optionally. 818 | * 819 | * @name multiPoint 820 | * @param {Array>} coordinates an array of Positions 821 | * @param {Object=} properties an Object of key-value pairs to add as properties 822 | * @returns {Feature} a MultiPoint feature 823 | * @throws {Error} if no coordinates are passed 824 | * @example 825 | * var multiPt = turf.multiPoint([[0,0],[10,10]]); 826 | * 827 | * //=multiPt 828 | * 829 | */ 830 | module.exports.multiPoint = function (coordinates, properties) { 831 | if (!coordinates) { 832 | throw new Error('No coordinates passed'); 833 | } 834 | return feature({ 835 | type: 'MultiPoint', 836 | coordinates: coordinates 837 | }, properties); 838 | }; 839 | 840 | 841 | /** 842 | * Creates a {@link Feature} based on a 843 | * coordinate array. Properties can be added optionally. 844 | * 845 | * @name multiPolygon 846 | * @param {Array>>>} coordinates an array of Polygons 847 | * @param {Object=} properties an Object of key-value pairs to add as properties 848 | * @returns {Feature} a multipolygon feature 849 | * @throws {Error} if no coordinates are passed 850 | * @example 851 | * var multiPoly = turf.multiPolygon([[[[0,0],[0,10],[10,10],[10,0],[0,0]]]); 852 | * 853 | * //=multiPoly 854 | * 855 | */ 856 | module.exports.multiPolygon = function (coordinates, properties) { 857 | if (!coordinates) { 858 | throw new Error('No coordinates passed'); 859 | } 860 | return feature({ 861 | type: 'MultiPolygon', 862 | coordinates: coordinates 863 | }, properties); 864 | }; 865 | 866 | /** 867 | * Creates a {@link Feature} based on a 868 | * coordinate array. Properties can be added optionally. 869 | * 870 | * @name geometryCollection 871 | * @param {Array<{Geometry}>} geometries an array of GeoJSON Geometries 872 | * @param {Object=} properties an Object of key-value pairs to add as properties 873 | * @returns {Feature} a geometrycollection feature 874 | * @example 875 | * var pt = { 876 | * "type": "Point", 877 | * "coordinates": [100, 0] 878 | * }; 879 | * var line = { 880 | * "type": "LineString", 881 | * "coordinates": [ [101, 0], [102, 1] ] 882 | * }; 883 | * var collection = turf.geometrycollection([[0,0],[10,10]]); 884 | * 885 | * //=collection 886 | */ 887 | module.exports.geometryCollection = function (geometries, properties) { 888 | return feature({ 889 | type: 'GeometryCollection', 890 | geometries: geometries 891 | }, properties); 892 | }; 893 | 894 | var factors = { 895 | miles: 3960, 896 | nauticalmiles: 3441.145, 897 | degrees: 57.2957795, 898 | radians: 1, 899 | inches: 250905600, 900 | yards: 6969600, 901 | meters: 6373000, 902 | metres: 6373000, 903 | kilometers: 6373, 904 | kilometres: 6373 905 | }; 906 | 907 | /* 908 | * Convert a distance measurement from radians to a more friendly unit. 909 | * 910 | * @name radiansToDistance 911 | * @param {number} distance in radians across the sphere 912 | * @param {string=kilometers} units: one of miles, nauticalmiles, degrees, radians, 913 | * inches, yards, metres, meters, kilometres, kilometers. 914 | * @returns {number} distance 915 | */ 916 | module.exports.radiansToDistance = function (radians, units) { 917 | var factor = factors[units || 'kilometers']; 918 | if (factor === undefined) { 919 | throw new Error('Invalid unit'); 920 | } 921 | return radians * factor; 922 | }; 923 | 924 | /* 925 | * Convert a distance measurement from a real-world unit into radians 926 | * 927 | * @name distanceToRadians 928 | * @param {number} distance in real units 929 | * @param {string=kilometers} units: one of miles, nauticalmiles, degrees, radians, 930 | * inches, yards, metres, meters, kilometres, kilometers. 931 | * @returns {number} radians 932 | */ 933 | module.exports.distanceToRadians = function (distance, units) { 934 | var factor = factors[units || 'kilometers']; 935 | if (factor === undefined) { 936 | throw new Error('Invalid unit'); 937 | } 938 | return distance / factor; 939 | }; 940 | 941 | /* 942 | * Convert a distance measurement from a real-world unit into degrees 943 | * 944 | * @name distanceToRadians 945 | * @param {number} distance in real units 946 | * @param {string=kilometers} units: one of miles, nauticalmiles, degrees, radians, 947 | * inches, yards, metres, meters, kilometres, kilometers. 948 | * @returns {number} degrees 949 | */ 950 | module.exports.distanceToDegrees = function (distance, units) { 951 | var factor = factors[units || 'kilometers']; 952 | if (factor === undefined) { 953 | throw new Error('Invalid unit'); 954 | } 955 | return (distance / factor) * 57.2958; 956 | }; 957 | 958 | },{}],7:[function(require,module,exports){ 959 | arguments[4][4][0].apply(exports,arguments) 960 | },{"dup":4}],8:[function(require,module,exports){ 961 | module.exports = extend 962 | 963 | var hasOwnProperty = Object.prototype.hasOwnProperty; 964 | 965 | function extend() { 966 | var target = {} 967 | 968 | for (var i = 0; i < arguments.length; i++) { 969 | var source = arguments[i] 970 | 971 | for (var key in source) { 972 | if (hasOwnProperty.call(source, key)) { 973 | target[key] = source[key] 974 | } 975 | } 976 | } 977 | 978 | return target 979 | } 980 | 981 | },{}]},{},[1]); 982 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Wikidata-OSM Distance Visualizer 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 45 | 46 | 47 | 48 |
49 |
50 |
51 |
52 |

Wikidata-OSM Distance Visualizer

53 |

Use this tool to explore the distance between an OpenStreetMap feature and the location of its linked Wikidata entry. Please see the instructions on how to use this tool.

54 |

Larger markers represent a larger distance between the OpenStreetMap and corresponding Wikidata feature. It is normal for larger features like Countries to have a larger match distance. You can manually adjust the distance treshold to highlight to explore the data

55 |

Distance threshold

56 | 57 | 58 |
59 | 61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global App */ 4 | var mapboxglLive = require('./mapbox-gl-live'); 5 | 6 | var threshold = 1000; 7 | mapboxgl.accessToken = 'pk.eyJ1IjoicGxhbmVtYWQiLCJhIjoiY2l2dzVxbzA3MDAwNDJzbDUzMzVzbXc5dSJ9.WZ4_UtVvuVmOw4ofNMkiJw'; 8 | var map = new mapboxgl.Map({ 9 | container: 'map', 10 | style: 'mapbox://styles/mapbox/light-v9', 11 | zoom: 3, 12 | center: [ 13 | 21.6, 7.6 14 | ], 15 | hash: true 16 | }); 17 | 18 | map.addControl(new MapboxGeocoder({accessToken: mapboxgl.accessToken})); 19 | 20 | map.on('load', function() { 21 | map.addSource('wikidata-source', { 22 | type: 'vector', 23 | url: 'mapbox://amisha.wikidata_planet_geojson' 24 | }); 25 | map.addSource('points', { 26 | type: 'geojson', 27 | data: { 28 | "type": "FeatureCollection", 29 | "features": [ 30 | { 31 | "type": "Feature", 32 | "geometry": { 33 | "type": "Point", 34 | "coordinates": [] 35 | } 36 | }, { 37 | "type": "Feature", 38 | "geometry": { 39 | "type": "Point", 40 | "coordinates": [] 41 | } 42 | } 43 | ] 44 | } 45 | }); 46 | map.addLayer({ 47 | 'id': 'points-layer', 48 | 'type': 'circle', 49 | 'source': 'points', 50 | 'type': 'symbol', 51 | 'layout': { 52 | "icon-image": "{icon}-15", 53 | 'icon-size': 3 54 | } 55 | }); 56 | map.addLayer({ 57 | "id": "wikidata-layer", 58 | "type": "circle", 59 | "source": "wikidata-source", 60 | "source-layer": "wikidata_planet_geojson", 61 | "paint": { 62 | "circle-radius": { 63 | "property": 'distance', 64 | 'stops': [ 65 | [ 66 | 0, 0 67 | ], 68 | [ 69 | threshold / 20, 70 | 4 71 | ], 72 | [ 73 | threshold / 10, 74 | 8 75 | ], 76 | [threshold, 20] 77 | ] 78 | }, 79 | "circle-color": { 80 | "property": 'distance', 81 | "stops": [ 82 | [ 83 | 0, '#ffffff' 84 | ], 85 | [ 86 | threshold / 20, 87 | '#00ff00' 88 | ], 89 | [ 90 | threshold / 10, 91 | '#ffff00' 92 | ], 93 | [threshold, '#ff0000'] 94 | ] 95 | } 96 | } 97 | }); 98 | document.getElementById('slider').addEventListener('input', function(e) { 99 | var distance = parseInt(e.target.value, 10); 100 | threshold = distance; 101 | repaintLayer(distance); 102 | }); 103 | repaintLayer(1000); 104 | // Inspect wikidata layer on click and show popup information 105 | mapboxglLive.inspector(map, {layers: ['wikidata-layer']}); 106 | 107 | }); 108 | 109 | function repaintLayer(threshold) { 110 | map.setPaintProperty('wikidata-layer', "circle-radius", { 111 | "property": 'distance', 112 | 'stops': [ 113 | [ 114 | 0, 0 115 | ], 116 | [ 117 | threshold / 20, 118 | 4 119 | ], 120 | [ 121 | threshold / 10, 122 | 8 123 | ], 124 | [threshold, 20] 125 | ] 126 | }); 127 | map.setPaintProperty('wikidata-layer', "circle-color", { 128 | "property": 'distance', 129 | "stops": [ 130 | [ 131 | 0, '#ffffff' 132 | ], 133 | [ 134 | threshold / 20, 135 | '#00ff00' 136 | ], 137 | [ 138 | threshold / 10, 139 | '#ffff00' 140 | ], 141 | [threshold, '#ff0000'] 142 | ] 143 | }); 144 | document.getElementById('Distance').textContent = threshold + ' km'; 145 | 146 | } 147 | -------------------------------------------------------------------------------- /mapbox-gl-live.js: -------------------------------------------------------------------------------- 1 | // mapbox-gl-live: Live tools to add interactivity to your Mapbox GL map 2 | // inspector: Explore the map data by inspecting features with the mouse 3 | 4 | var xtend = require('xtend'); 5 | var centroid = require('turf-centroid'); 6 | var turf_bbox = require('turf-bbox'); 7 | 8 | defaultOpts = { 9 | layers: ['building'], 10 | on: 'click' 11 | } 12 | 13 | var Live = { 14 | 15 | // Inspect map layers on mouse interactivity 16 | inspector: function(map, opts) { 17 | 18 | opts = xtend(defaultOpts, opts); 19 | 20 | // Query features on interaction with the layers 21 | map.on(opts.on, function(e) { 22 | var features = map.queryRenderedFeatures(e.point, {layers: opts.layers}); 23 | 24 | if (!features.length) { 25 | return; 26 | } 27 | 28 | var feature = features[0]; 29 | 30 | var lngLat1, 31 | lngLat2; 32 | 33 | // Fetch the Wikidata entity for the latest properties 34 | $.getJSON("https://www.wikidata.org/w/api.php?action=wbgetentities&ids=" + feature.properties.wikidata + "&format=json&callback=?", function(data) { 35 | if (data["entities"]) { 36 | 37 | var latitude = data["entities"][feature.properties.wikidata]["claims"]["P625"][0]["mainsnak"]["datavalue"]["value"]["latitude"]; 38 | var longitude = data["entities"][feature.properties.wikidata]["claims"]["P625"][0]["mainsnak"]["datavalue"]["value"]["longitude"]; 39 | lngLat1 = new mapboxgl.LngLat(longitude, latitude); 40 | lngLat2 = new mapboxgl.LngLat(feature.geometry.coordinates[0], feature.geometry.coordinates[1]); 41 | var distance = getDistance(lngLat1, lngLat2); 42 | var modified = false; 43 | if (distance !== feature.properties.distance && Math.abs(distance - feature.properties.distance) > 0.1) { 44 | modified = true; 45 | feature.properties.distance = distance; 46 | } 47 | 48 | var popupHTML = populateTable(feature, modified); 49 | var data = { 50 | "type": "FeatureCollection", 51 | "features": [{ 52 | "type": "Feature", 53 | "geometry": { 54 | "type": "Point", 55 | "coordinates": [longitude, latitude] 56 | }, 57 | "properties": { 58 | "icon": "marker" 59 | } 60 | }, { 61 | "type": "Feature", 62 | "geometry": { 63 | "type": "Point", 64 | "coordinates": [feature.geometry.coordinates[0], feature.geometry.coordinates[1]] 65 | }, 66 | "properties": { 67 | "icon": "marker" 68 | } 69 | }] 70 | }; 71 | map.setLayoutProperty('wikidata-layer', 'visibility', 'none'); 72 | map.setLayoutProperty('points-layer', 'visibility', 'visible'); 73 | 74 | map.getSource('points').setData(data); 75 | var bounds = turf_bbox(data); 76 | var buffer; 77 | switch (true) 78 | { 79 | case (distance < 1): buffer = 0.005; 80 | break; 81 | case (distance < 20): buffer = 0.2; 82 | break; 83 | case (distance < 50): buffer = 1; 84 | break; 85 | case (distance < 100): buffer = 2; 86 | break; 87 | case (distance < 500): buffer = 3; 88 | break; 89 | default: buffer = 4; 90 | break; 91 | } 92 | bounds[0] -= buffer; 93 | bounds[1] -= buffer; 94 | bounds[2] += buffer; 95 | bounds[3] += buffer; 96 | map.fitBounds(bounds); 97 | $(location).attr('href', '#sidebar'); 98 | $('#sidebar').html(popupHTML); 99 | document.getElementById('close').onclick = function(){ 100 | map.setLayoutProperty('wikidata-layer', 'visibility','visible'); 101 | map.setLayoutProperty('points-layer', 'visibility', 'none'); 102 | map.getSource('points').setData({}); 103 | window.location = '#container'; 104 | }; 105 | } 106 | }); 107 | 108 | }); 109 | 110 | // Change the mouse to a pointer on hovering over inspectable features 111 | map.on('mousemove', function(e) { 112 | var features = map.queryRenderedFeatures(e.point, {layers: opts.layers}); 113 | map.getCanvas().style.cursor = (features.length) 114 | ? 'pointer' 115 | : ''; 116 | }); 117 | 118 | } 119 | } 120 | 121 | function populateTable(feature, modified) { 122 | // Populate the popup and set its coordinates 123 | // based on the feature found. 124 | 125 | var popupHTML = ""; 126 | 127 | popupHTML += "

" + feature.properties.name + "

"; 128 | popupHTML += "Wikidata
"; 129 | popupHTML += "OSM Search
"; 130 | 131 | popupHTML += ""; 132 | 133 | if (modified) { 134 | popupHTML += "

Modified on wikidata

"; 135 | } 136 | 137 | for (property in feature.properties) { 138 | if (property == 'distance') { 139 | var distance = feature.properties[property]; 140 | popupHTML += ""; 141 | } else { 142 | popupHTML += ""; 143 | } 144 | } 145 | popupHTML += "
" + property + "" + parseFloat(distance.toFixed(3)) + "
" + property + "" + feature.properties[property] + "
"; 146 | 147 | return popupHTML; 148 | } 149 | 150 | function getDistance(lnglat1, lnglat2) { 151 | // Uses spherical law of cosines approximation. 152 | const R = 6371000; 153 | 154 | const rad = Math.PI / 180, 155 | lat1 = lnglat1.lat * rad, 156 | lat2 = lnglat2.lat * rad, 157 | a = Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1) * Math.cos(lat2) * Math.cos((lnglat2.lng - lnglat1.lng) * rad); 158 | 159 | const maxMeters = R * Math.acos(Math.min(a, 1)); 160 | return maxMeters / 1000; 161 | } 162 | 163 | // Generate a nominatim search link for a feature name 164 | function nominatimLink(name, coordinates) { 165 | 166 | const NOMINATIM_BASE = 'http://nominatim.openstreetmap.org/search.php?q='; 167 | 168 | // Limit search to the vicinity of the given coordinates 169 | try { 170 | 171 | var left = coordinates[0] - 1; 172 | var top = coordinates[1] - 1; 173 | var right = coordinates[0] + 1; 174 | var bottom = coordinates[1] + 1; 175 | 176 | var NOMINATIM_OPTS = name + "&polygon=1&bounded=1&viewbox=" + left + "%2C" + top + "%2C" + right + "%2C" + bottom 177 | 178 | } catch (e) { 179 | var NOMINATIM_OPTS = name 180 | } 181 | 182 | return NOMINATIM_BASE + NOMINATIM_OPTS; 183 | 184 | } 185 | 186 | // Export module 187 | module.exports = Live; 188 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wikidata-osm", 3 | "version": "0.0.1", 4 | "description": "Explore Wikidata connected features on OpenStreetMap", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run build && budo index.js --serve=bundle.js --live -d", 8 | "build": "browserify index.js > bundle.js" 9 | }, 10 | "author": "Amisha S", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "browserify": "^13.1.0", 14 | "budo": "^9.2.1" 15 | }, 16 | "dependencies": { 17 | "turf-bbox": "^3.0.12", 18 | "turf-centroid": "^3.0.12", 19 | "xtend": "^4.0.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /script.py: -------------------------------------------------------------------------------- 1 | ### This script compares the location of the features having wikidata tags in OSM 2 | ### with that of records in wikidata.org database and find out the corresponding 3 | ### distance for each one of the feature. The distance can be used to validate the 4 | ### osm features as well as wikidata tag location. The greater the distance, higher 5 | ### the probability of incorrectness of either one of them. 6 | 7 | import json 8 | import shapely.geometry 9 | import psycopg2 10 | from geopy.distance import vincenty 11 | 12 | 13 | ### The database having the wikidata entries with the coordinates 14 | conn = psycopg2.connect("dbname=wikidata user=sanjaybhangar") 15 | 16 | cur = conn.cursor() 17 | 18 | ### The json dump of features having wikidata tags from osm planet 19 | fr = open('wikidata_planet.json') 20 | 21 | fw = open('distance_wikidata_planet.geojson', 'w') 22 | 23 | for line in fr: 24 | wikidata = json.loads(line) 25 | wiki_id = wikidata['properties']['wikidata'] 26 | cur.execute("SELECT ST_AsGeojson(ST_Transform(geom, 4326)) from mb_wikidata WHERE qid = '%s'" % (wiki_id)) 27 | x = cur.fetchone() 28 | if x is not None: 29 | geom_geojson = shapely.geometry.shape(wikidata["geometry"]) 30 | geom_db = shapely.geometry.shape(json.loads(x[0])) 31 | centroid_geojson = geom_geojson.centroid 32 | centroid_db = geom_db.centroid 33 | distance = vincenty((centroid_geojson.x,centroid_geojson.y),(centroid_db.x, centroid_db.y)).km 34 | wikidata['properties']['distance'] = distance 35 | wikidata['geometry']['type'] = 'Point' 36 | wikidata['geometry']['coordinates'] = [centroid_geojson.x, centroid_geojson.y] 37 | fw.write(json.dumps(wikidata) + '\n') 38 | 39 | fr.close() 40 | fw.close() 41 | cur.close() 42 | conn.close() --------------------------------------------------------------------------------