forked from edward/owl-map
Initial commit
This commit is contained in:
commit
c4aa27e8f4
33
static/css/map.css
Normal file
33
static/css/map.css
Normal file
|
@ -0,0 +1,33 @@
|
|||
#map {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#search {
|
||||
position: absolute;
|
||||
overflow: auto;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
bottom: 20px;
|
||||
width: 25%;
|
||||
background: lightgray;
|
||||
}
|
||||
|
||||
#load-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
bottom: 20px;
|
||||
overflow: auto;
|
||||
}
|
0
static/css/style.css
Normal file
0
static/css/style.css
Normal file
55
static/js/app.js
Normal file
55
static/js/app.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
'use strict';
|
||||
|
||||
var map = L.map('map');
|
||||
|
||||
var tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png');
|
||||
var group = L.featureGroup();
|
||||
map.addLayer(group);
|
||||
tiles.addTo(map);
|
||||
|
||||
var items = {};
|
||||
var duration_span = document.getElementById("duration");
|
||||
|
||||
map.on('moveend', function(e) {
|
||||
var bounds = map.getBounds();
|
||||
console.log("map moved", bounds.toBBoxString());
|
||||
|
||||
var markers_url = "/api/1/markers.json";
|
||||
var params = {bounds: bounds.toBBoxString()};
|
||||
axios.get(markers_url, {params: params}).then(response => {
|
||||
var items = response.data.items;
|
||||
items.forEach(item => {
|
||||
if (item.qid in items)
|
||||
return;
|
||||
item.markers.forEach(marker => {
|
||||
var marker = L.marker(marker, {"title": item.label });
|
||||
marker.addTo(group);
|
||||
});
|
||||
items[item.qid] = item;
|
||||
});
|
||||
|
||||
duration_span.innerText = response.data.duration;
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
var hit_links = document.getElementsByClassName("hit-link");
|
||||
|
||||
// console.log(hit_links);
|
||||
|
||||
for (const link of hit_links) {
|
||||
link.addEventListener('click', event => {
|
||||
event.preventDefault();
|
||||
var link = event.target;
|
||||
var id_string = event.target.id;
|
||||
var re_id = /^hit-link-(\d+)$/;
|
||||
var hit_index = re_id.exec(id_string)[1];
|
||||
var [south, north, west, east] = bbox_list[hit_index];
|
||||
var bounds = [[north, west], [south, east]];
|
||||
|
||||
console.log('click', bounds);
|
||||
map.fitBounds(bounds);
|
||||
});
|
||||
}
|
388
static/js/map.js
Normal file
388
static/js/map.js
Normal file
|
@ -0,0 +1,388 @@
|
|||
'use strict';
|
||||
|
||||
// Create a map
|
||||
|
||||
var options = {};
|
||||
if (start_lat || start_lng) {
|
||||
start_lat = start_lat.toFixed(5);
|
||||
start_lng = start_lng.toFixed(5);
|
||||
options = {
|
||||
center: [start_lat, start_lng],
|
||||
zoom: zoom,
|
||||
};
|
||||
history.replaceState(null, null, `/map/${zoom}/${start_lat}/${start_lng}`);
|
||||
}
|
||||
|
||||
var map = L.map("map", options);
|
||||
var group = L.featureGroup();
|
||||
var wikidata_items = {};
|
||||
var osm_objects = {};
|
||||
var wikidata_loaded = false;
|
||||
var osm_loaded = false;
|
||||
var loading = document.getElementById("loading");
|
||||
var load_text = document.getElementById("load-text");
|
||||
var isa_card = document.getElementById("isa-card");
|
||||
var isa_labels = {};
|
||||
var connections = {};
|
||||
map.addLayer(group);
|
||||
map.zoomControl.setPosition('topright');
|
||||
|
||||
var blueMarker = L.ExtraMarkers.icon({
|
||||
icon: 'fa-wikidata',
|
||||
markerColor: 'blue',
|
||||
shape: 'circle',
|
||||
prefix: 'fa',
|
||||
});
|
||||
|
||||
var greenMarker = L.ExtraMarkers.icon({
|
||||
icon: 'fa-wikidata',
|
||||
markerColor: 'green',
|
||||
shape: 'circle',
|
||||
prefix: 'fa',
|
||||
});
|
||||
|
||||
var redMarker = L.ExtraMarkers.icon({
|
||||
icon: 'fa-wikidata',
|
||||
markerColor: 'red',
|
||||
shape: 'circle',
|
||||
prefix: 'fa',
|
||||
});
|
||||
|
||||
var osmYellowMarker = L.ExtraMarkers.icon({
|
||||
icon: 'fa-map',
|
||||
markerColor: 'yellow',
|
||||
shape: 'square',
|
||||
prefix: 'fa',
|
||||
});
|
||||
|
||||
var osmOrangeMarker = L.ExtraMarkers.icon({
|
||||
icon: 'fa-map',
|
||||
markerColor: 'orange',
|
||||
shape: 'square',
|
||||
prefix: 'fa',
|
||||
});
|
||||
|
||||
|
||||
if (!start_lat || !start_lng) {
|
||||
map.fitBounds([[49.85,-10.5], [58.75, 1.9]]);
|
||||
}
|
||||
|
||||
// Add OpenStreetMap layer
|
||||
var osm = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19 });
|
||||
|
||||
osm.addTo(map);
|
||||
|
||||
function check_items(items) {
|
||||
if (items.length === 0)
|
||||
return;
|
||||
|
||||
var item = items.shift();
|
||||
var qid = item.qid;
|
||||
|
||||
var markers_url = `/api/1/item/${qid}`;
|
||||
axios.get(markers_url).then(response => {
|
||||
// console.log(response.data, item);
|
||||
response.data.info.forEach(osm => {
|
||||
var icon = osmYellowMarker;
|
||||
var marker = L.marker(osm.centroid, {title: osm.name, icon: icon});
|
||||
var popup = `
|
||||
<p>
|
||||
<a href="${osm.url}" target="_blank">${osm.identifier}</a>: ${osm.name}
|
||||
</p>`
|
||||
marker.bindPopup(popup);
|
||||
marker.addTo(group);
|
||||
|
||||
});
|
||||
item.markers.forEach(marker_data => {
|
||||
var marker = marker_data.marker;
|
||||
var icon = response.data.tagged ? greenMarker : redMarker;
|
||||
marker.setIcon(icon);
|
||||
|
||||
response.data.info.forEach(osm => {
|
||||
var path = [osm.centroid, marker_data];
|
||||
var polyline = L.polyline(path, {color: 'green'}).addTo(map);
|
||||
});
|
||||
|
||||
});
|
||||
if (items.length)
|
||||
check_items(items);
|
||||
});
|
||||
}
|
||||
|
||||
function update_wikidata() {
|
||||
if (Object.keys(wikidata_items).length === 0 || Object.keys(osm_objects).length === 0) {
|
||||
if (wikidata_loaded && osm_loaded) {
|
||||
loading.classList.add("visually-hidden");
|
||||
load_text.classList.remove("visually-hidden");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (const qid in osm_objects) {
|
||||
var osm_list = osm_objects[qid];
|
||||
|
||||
var item = wikidata_items[qid];
|
||||
if (!item) {
|
||||
osm_list.forEach(osm => {
|
||||
osm.marker.setIcon(osmOrangeMarker);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.lines === undefined)
|
||||
item.lines = [];
|
||||
item.markers.forEach(marker_data => {
|
||||
marker_data.marker.setIcon(greenMarker);
|
||||
osm_list.forEach(osm => {
|
||||
var path = [osm.centroid, marker_data];
|
||||
var polyline = L.polyline(path, {color: 'green'}).addTo(group);
|
||||
item.lines.push(polyline);
|
||||
});
|
||||
});
|
||||
}
|
||||
for (const qid in wikidata_items) {
|
||||
if (osm_objects[qid])
|
||||
continue;
|
||||
var item = wikidata_items[qid];
|
||||
item.markers.forEach(marker_data => {
|
||||
marker_data.marker.setIcon(redMarker);
|
||||
});
|
||||
}
|
||||
|
||||
loading.classList.add("visually-hidden");
|
||||
load_text.classList.remove("visually-hidden");
|
||||
}
|
||||
|
||||
function isa_only(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var this_id = e.target.parentNode.childNodes[0].id;
|
||||
|
||||
var checkbox_list = document.getElementsByClassName('isa-checkbox');
|
||||
|
||||
for (const checkbox of checkbox_list) {
|
||||
checkbox.checked = checkbox.id == this_id;
|
||||
}
|
||||
|
||||
checkbox_change();
|
||||
}
|
||||
|
||||
function checkbox_change() {
|
||||
var checkbox_list = document.getElementsByClassName('isa-checkbox');
|
||||
var ticked = [];
|
||||
for (const checkbox of checkbox_list) {
|
||||
if (checkbox.checked) {
|
||||
ticked.push(checkbox.id.substr(4));
|
||||
}
|
||||
}
|
||||
|
||||
for (const qid in wikidata_items) {
|
||||
var item = wikidata_items[qid];
|
||||
const item_isa_list = wikidata_items[qid]['isa_list'];
|
||||
const intersection = ticked.filter(isa_qid => item_isa_list.includes(isa_qid));
|
||||
if (item.lines) {
|
||||
item.lines.forEach(line => {
|
||||
if (intersection.length) {
|
||||
line.addTo(group);
|
||||
} else {
|
||||
line.removeFrom(group);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
item.markers.forEach(marker_data => {
|
||||
var marker = marker_data.marker;
|
||||
if (intersection.length) {
|
||||
marker.addTo(group);
|
||||
} else {
|
||||
marker.removeFrom(group);
|
||||
}
|
||||
});
|
||||
|
||||
if(osm_objects[qid]) {
|
||||
osm_objects[qid].forEach(osm => {
|
||||
if (intersection.length) {
|
||||
osm.marker.addTo(group);
|
||||
} else {
|
||||
osm.marker.removeFrom(group);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function set_isa_list(isa_count) {
|
||||
isa_card.classList.remove("visually-hidden");
|
||||
var isa_list = document.getElementById("isa-list");
|
||||
isa_list.innerHTML = '';
|
||||
isa_count.forEach(isa => {
|
||||
isa_labels[isa.qid] = isa.label;
|
||||
var isa_id = `isa-${isa.qid}`;
|
||||
var e = document.createElement('div');
|
||||
e.setAttribute('class', 'isa-item');
|
||||
|
||||
var checkbox = document.createElement('input');
|
||||
checkbox.setAttribute('type', 'checkbox');
|
||||
checkbox.setAttribute('checked', 'checked');
|
||||
checkbox.setAttribute('id', isa_id);
|
||||
checkbox.setAttribute('class', 'isa-checkbox');
|
||||
checkbox.onchange = checkbox_change;
|
||||
e.appendChild(checkbox);
|
||||
|
||||
e.appendChild(document.createTextNode(' '));
|
||||
|
||||
var label = document.createElement('label');
|
||||
label.setAttribute('for', isa_id);
|
||||
var label_text = document.createTextNode(` ${isa.label} (${isa.qid}): ${isa.count} `);
|
||||
label.appendChild(label_text);
|
||||
|
||||
e.appendChild(label);
|
||||
e.appendChild(document.createTextNode(' '));
|
||||
|
||||
var only = document.createElement('a');
|
||||
only.setAttribute('href', '#');
|
||||
var only_text = document.createTextNode('only');
|
||||
only.appendChild(only_text);
|
||||
only.onclick = isa_only;
|
||||
e.appendChild(only);
|
||||
|
||||
isa_list.appendChild(e);
|
||||
});
|
||||
}
|
||||
|
||||
function load_wikidata_items() {
|
||||
var checkbox_list = document.getElementsByClassName('isa-checkbox');
|
||||
|
||||
for (const checkbox of checkbox_list)
|
||||
checkbox.checked = true;
|
||||
|
||||
checkbox_change();
|
||||
|
||||
loading.classList.remove("visually-hidden");
|
||||
load_text.classList.add("visually-hidden");
|
||||
|
||||
var bounds = map.getBounds();
|
||||
console.log("map moved", bounds.toBBoxString());
|
||||
|
||||
// var items_to_check = [];
|
||||
var params = {bounds: bounds.toBBoxString()};
|
||||
var items_url = "/api/1/items";
|
||||
|
||||
axios.get(items_url, {params: params}).then(response => {
|
||||
set_isa_list(response.data.isa_count);
|
||||
var items = response.data.items;
|
||||
items.forEach(item => {
|
||||
if (item.qid in wikidata_items)
|
||||
return;
|
||||
item.markers.forEach(marker_data => {
|
||||
// var icon = marker.tagged ? greenMarker : blueMarker;
|
||||
var icon = blueMarker;
|
||||
var label = `${item.label} (${item.qid})`
|
||||
var marker = L.marker(marker_data, {title: label, icon: icon});
|
||||
var wd_url = 'https://www.wikidata.org/wiki/' + item.qid;
|
||||
var popup = '<p><strong>Wikidata item</strong><br>'
|
||||
popup += `<a href="${wd_url}" target="_blank">${item.label}</a> (${item.qid})`
|
||||
if (item.description) {
|
||||
popup += `<br>description: ${item.description}`
|
||||
}
|
||||
if (item.isa_list) {
|
||||
popup += '<br><strong>item type</strong>'
|
||||
for (const [index, isa_qid] of item.isa_list.entries()) {
|
||||
var isa_url = 'https://www.wikidata.org/wiki/' + isa_qid;
|
||||
var isa_label = isa_labels[isa_qid];
|
||||
popup += `<br><a href="${isa_url}">${isa_label}</a> (${isa_qid})`;
|
||||
}
|
||||
}
|
||||
popup += '</p>';
|
||||
marker.bindPopup(popup);
|
||||
marker.addTo(group);
|
||||
marker_data.marker = marker;
|
||||
});
|
||||
wikidata_items[item.qid] = item;
|
||||
|
||||
|
||||
// items_to_check.push(item);
|
||||
});
|
||||
|
||||
wikidata_loaded = true;
|
||||
isa_card.classList.remove("visually-hidden");
|
||||
update_wikidata();
|
||||
|
||||
// duration_span.innerText = response.data.duration;
|
||||
// check_items(items_to_check);
|
||||
|
||||
});
|
||||
var osm_objects_url = "/api/1/osm";
|
||||
axios.get(osm_objects_url, {params: params}).then(response => {
|
||||
console.log(`${response.data.duration} seconds`);
|
||||
response.data.objects.forEach(osm => {
|
||||
var qid = osm.wikidata;
|
||||
if (osm_objects[qid] === undefined)
|
||||
osm_objects[qid] = [];
|
||||
osm_objects[qid].push(osm);
|
||||
|
||||
var icon = osmYellowMarker;
|
||||
var marker = L.marker(osm.centroid, {title: osm.name, icon: icon});
|
||||
osm.marker = marker;
|
||||
var wd_url = 'https://www.wikidata.org/wiki/' + qid;
|
||||
var popup = `
|
||||
<p>
|
||||
${osm.name}:
|
||||
<a href="${osm.url}" target="_blank">${osm.identifier}</a>
|
||||
<br>
|
||||
Wikidata tag: <a href="${wd_url}">${qid}</a>
|
||||
</p>`
|
||||
marker.bindPopup(popup);
|
||||
marker.addTo(group);
|
||||
});
|
||||
|
||||
osm_loaded = true;
|
||||
update_wikidata();
|
||||
});
|
||||
|
||||
|
||||
};
|
||||
|
||||
var load_btn = document.getElementById('load-btn');
|
||||
load_btn.onclick = load_wikidata_items;
|
||||
|
||||
var search_btn = document.getElementById('search-btn');
|
||||
var search_form = document.getElementById('search-form');
|
||||
search_form.onsubmit = function(e) {
|
||||
e.preventDefault();
|
||||
var search_text = document.getElementById('search-text').value.trim();
|
||||
if (!search_text)
|
||||
return;
|
||||
var params = {q: search_text};
|
||||
var search_url = "/api/1/search";
|
||||
var search_results = document.getElementById('search-results');
|
||||
axios.get(search_url, {params: params}).then(response => {
|
||||
search_results.innerHTML = '';
|
||||
response.data.hits.forEach(hit => {
|
||||
var e = document.createElement('div');
|
||||
var category = document.createTextNode(hit.category + ' ');
|
||||
e.appendChild(category);
|
||||
var a = document.createElement('a');
|
||||
var lat = parseFloat(hit.lat).toFixed(5);
|
||||
var lon = parseFloat(hit.lon).toFixed(5);
|
||||
a.setAttribute('href', `/map/15/${lat}/${lon}`);
|
||||
var link_text = document.createTextNode(hit.name);
|
||||
a.appendChild(link_text);
|
||||
e.appendChild(a);
|
||||
search_results.appendChild(e);
|
||||
});
|
||||
console.log(response.data);
|
||||
});
|
||||
}
|
||||
|
||||
map.on('moveend', function (e) {
|
||||
var zoom = map.getZoom();
|
||||
var c = map.getCenter();
|
||||
var lat = c.lat.toFixed(5);
|
||||
var lng = c.lng.toFixed(5);
|
||||
history.replaceState(null, null, `/map/${zoom}/${lat}/${lng}`);
|
||||
});
|
40
static/js/search_map.js
Normal file
40
static/js/search_map.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
'use strict';
|
||||
|
||||
// Create a map
|
||||
|
||||
var options = {};
|
||||
if (user_lat && user_lon) {
|
||||
options = {
|
||||
center: [user_lat, user_lon],
|
||||
zoom: 15,
|
||||
};
|
||||
}
|
||||
|
||||
var map = L.map("map", options);
|
||||
map.zoomControl.setPosition('topright');
|
||||
|
||||
if (!user_lat || !user_lon) {
|
||||
map.fitBounds([[49.85,-10.5], [58.75, 1.9]]);
|
||||
}
|
||||
|
||||
// Add OpenStreetMap layer
|
||||
var osm = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 18 });
|
||||
|
||||
osm.addTo(map);
|
||||
|
||||
var hits = document.getElementsByClassName("hit-card");
|
||||
var current_hit = null;
|
||||
|
||||
for (const card of hits) {
|
||||
card.addEventListener('mouseover', event => {
|
||||
var id_string = card.id;
|
||||
if (current_hit == id_string) return;
|
||||
current_hit = id_string;
|
||||
var re_id = /^hit-card-(\d+)$/;
|
||||
var hit_index = re_id.exec(id_string)[1];
|
||||
var [south, north, west, east] = bbox_list[hit_index];
|
||||
var bounds = [[north, west], [south, east]];
|
||||
|
||||
map.fitBounds(bounds);
|
||||
});
|
||||
}
|
24
templates/base.html
Normal file
24
templates/base.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>
|
||||
{% block title %}{% endblock %} OSM ↔ Wikidata
|
||||
</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css')}}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap4/css/bootstrap.css')}}">
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='fork-awesome/css/fork-awesome.css')}}">
|
||||
|
||||
{% block style %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-fluid p-0">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block script %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
9
templates/empty_page.html
Normal file
9
templates/empty_page.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-2">
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
25
templates/identifier_index.html
Normal file
25
templates/identifier_index.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-2">
|
||||
|
||||
<table class="table">
|
||||
{% for pid, osm_keys, label in property_map %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('identifier_page', pid=pid) }}">{{ label }}</a>
|
||||
({{ pid }})
|
||||
<a href="https://wikidata.org/wiki/Property:{{ pid }}" target="_blank">
|
||||
<i class="fa fa-wikidata"></i>
|
||||
<i class="fa fa-external-link-square"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ '/'.join(osm_keys) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
29
templates/identifier_page.html
Normal file
29
templates/identifier_page.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid my-2">
|
||||
|
||||
<h1>{{ label }} ({{ pid }})</h1>
|
||||
<p><a href="{{ url_for('identifier_index') }}">Back to identifier index</a></p>
|
||||
|
||||
<p>Total number of items: {{ total }}</p>
|
||||
<p>OSM total: {{ osm_total }}</p>
|
||||
|
||||
<table class="table">
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>{{ item.label() }} ({{ item.qid }})</td>
|
||||
<td>{{ ' / '.join(item.get_claim(pid)) }}</td>
|
||||
<td>
|
||||
{% for point in osm_points[item.qid] %}
|
||||
{{ point.identifier }}: {{ point.tags.name }}<br />
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
16
templates/index.html
Normal file
16
templates/index.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid my-2">
|
||||
<h1>OSM/Wikidata</h1>
|
||||
<ul>
|
||||
<li><a href="{{ url_for('map_start_page') }}">map</a></li>
|
||||
<li><a href="{{ url_for('search_map_page') }}">map with search</a></li>
|
||||
<li><a href="{{ url_for('search_page') }}">search</a></li>
|
||||
<li><a href="{{ url_for('identifier_index') }}">identifies</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
62
templates/map.html
Normal file
62
templates/map.html
Normal file
|
@ -0,0 +1,62 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Map showing Wikidata items linked to OSM</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='fork-awesome/css/fork-awesome.css')}}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/map.css') }}?time={{time}}" type="text/css" media="all" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.0/dist/leaflet.css" integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ==" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='Leaflet.ExtraMarkers/leaflet.extra-markers.min.css') }}" type="text/css" media="all" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
|
||||
<div id="sidebar">
|
||||
<div class="card bg-white" id="search-card">
|
||||
<div class="card-body">
|
||||
<form id="search-form">
|
||||
<div>
|
||||
<input id="search-text" placeholder="place">
|
||||
<button type="submit" id="search-btn" class="btn btn-primary">search</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="search-results">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-white visually-hidden mt-3" id="isa-card">
|
||||
<div class="card-body">
|
||||
<div class="h5 card-title">item types</div>
|
||||
<div id="isa-list">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="load-btn" type="button" class="btn btn-primary btn-lg">
|
||||
<span id="loading" class="visually-hidden">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
Loading ...
|
||||
</span>
|
||||
<span id="load-text">
|
||||
Load Wikidata items
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<script>
|
||||
var start_lat = {{ lat | tojson }};
|
||||
var start_lng = {{ lng | tojson }};
|
||||
var zoom = {{ zoom | tojson }};
|
||||
var bbox_list = {{ (bbox_list or []) | tojson }};
|
||||
|
||||
</script>
|
||||
<script src="https://unpkg.com/axios@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js" integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/leaflet@1.3.0/dist/leaflet.js" integrity="sha512-C7BBF9irt5R7hqbUm2uxtODlUVs+IsNu2UULGuZN7gM+k/mmeG4xvIEac01BtQa4YIkUpp23zZC4wIwuXaPMQA==" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='Leaflet.ExtraMarkers/leaflet.extra-markers.min.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/map.js') }}?time={{time}}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
62
templates/search.html
Normal file
62
templates/search.html
Normal file
|
@ -0,0 +1,62 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{% endblock %}
|
||||
{% block style %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='leaflet/leaflet.css') }}">
|
||||
<style>
|
||||
#map {
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script src="https://unpkg.com/axios@latest"></script>
|
||||
<script src="{{ url_for('static', filename='leaflet/leaflet.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
||||
|
||||
<script>
|
||||
bbox_list = {{ bbox_list | tojson }};
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
|
||||
<form class="form-inline">
|
||||
<input class="form-control m-2" name="q" value="{{ request.args.q or "" }}">
|
||||
<button class="btn btn-primary m-2" type="submit">find</button>
|
||||
</form>
|
||||
|
||||
{% if hits %}
|
||||
{% for hit in hits %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p><a href="#" class="hit-link" id="hit-link-{{ loop.index0 }}" data-bounding-box="{{ hit.boundingbox }}">{{ hit.display_name }}</a></p>
|
||||
<p>
|
||||
{{ hit.category }}
|
||||
{{ hit.osm_type }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
|
||||
<div id="map" class="w-100 vh-100">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<p>Duration: <span id="duration"></span> seconds</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
51
templates/search_map.html
Normal file
51
templates/search_map.html
Normal file
|
@ -0,0 +1,51 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Map</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/map.css') }}" type="text/css" media="all" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.0/dist/leaflet.css" integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ==" crossorigin="anonymous" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
<div id="search">
|
||||
<div class="p-3">
|
||||
<h5>search</h5>
|
||||
<div>
|
||||
|
||||
<form class="form-inline">
|
||||
<input class="form-control m-2" name="q" value="{{ request.args.q or "" }}">
|
||||
<button class="btn btn-primary m-2" type="submit">find</button>
|
||||
</form>
|
||||
|
||||
{% if hits %}
|
||||
{% for hit in hits %}
|
||||
<div class="card mt-1">
|
||||
<div class="card-body hit-card" id="hit-card-{{ loop.index0 }}">
|
||||
<p><a href="#" class="hit-link" id="hit-link-{{ loop.index0 }}" data-bounding-box="{{ hit.boundingbox }}">{{ hit.display_name }}</a></p>
|
||||
<p>
|
||||
{{ hit.category }}
|
||||
{{ hit.osm_type }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="map"></div>
|
||||
|
||||
<script>
|
||||
var user_lat = {{ user_lat | tojson }};
|
||||
var user_lon = {{ user_lon | tojson }};
|
||||
var bbox_list = {{ (bbox_list or []) | tojson }};
|
||||
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/js/bootstrap.bundle.min.js" integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/leaflet@1.3.0/dist/leaflet.js" integrity="sha512-C7BBF9irt5R7hqbUm2uxtODlUVs+IsNu2UULGuZN7gM+k/mmeG4xvIEac01BtQa4YIkUpp23zZC4wIwuXaPMQA==" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" src="{{ url_for('static', filename='js/search_map.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
378
web_view.py
Executable file
378
web_view.py
Executable file
|
@ -0,0 +1,378 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
from flask import Flask, render_template, request, jsonify, redirect, url_for
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import selectinload
|
||||
from matcher import nominatim, model, database
|
||||
from collections import Counter
|
||||
from time import time
|
||||
import GeoIP
|
||||
|
||||
srid = 4326
|
||||
|
||||
app = Flask(__name__)
|
||||
app.debug = True
|
||||
|
||||
DB_URL = "postgresql:///matcher"
|
||||
database.init_db(DB_URL)
|
||||
|
||||
property_map = [
|
||||
("P238", ["iata"], "IATA airport code"),
|
||||
("P239", ["icao"], "ICAO airport code"),
|
||||
("P240", ["faa", "ref"], "FAA airport code"),
|
||||
# ('P281', ['addr:postcode', 'postal_code'], 'postal code'),
|
||||
("P296", ["ref", "ref:train", "railway:ref"], "station code"),
|
||||
("P300", ["ISO3166-2"], "ISO 3166-2 code"),
|
||||
("P359", ["ref:rce"], "Rijksmonument ID"),
|
||||
("P590", ["ref:gnis", "GNISID", "gnis:id", "gnis:feature_id"], "USGS GNIS ID"),
|
||||
("P649", ["ref:nrhp"], "NRHP reference number"),
|
||||
("P722", ["uic_ref"], "UIC station code"),
|
||||
("P782", ["ref"], "LAU (local administrative unit)"),
|
||||
("P836", ["ref:gss"], "UK Government Statistical Service code"),
|
||||
("P856", ["website", "contact:website", "url"], "website"),
|
||||
("P882", ["nist:fips_code"], "FIPS 6-4 (US counties)"),
|
||||
("P901", ["ref:fips"], "FIPS 10-4 (countries and regions)"),
|
||||
# A UIC id can be a IBNR, but not every IBNR is an UIC id
|
||||
("P954", ["uic_ref"], "IBNR ID"),
|
||||
("P981", ["ref:woonplaatscode"], "BAG code for Dutch residencies"),
|
||||
("P1216", ["HE_ref"], "National Heritage List for England number"),
|
||||
("P2253", ["ref:edubase"], "EDUBase URN"),
|
||||
("P2815", ["esr:user", "ref", "ref:train"], "ESR station code"),
|
||||
("P3425", ["ref", "ref:SIC"], "Natura 2000 site ID"),
|
||||
("P3562", ["seamark:light:reference"], "Admiralty number"),
|
||||
(
|
||||
"P4755",
|
||||
["ref", "ref:train", "ref:crs", "crs", "nat_ref"],
|
||||
"UK railway station code",
|
||||
),
|
||||
("P4803", ["ref", "ref:train"], "Amtrak station code"),
|
||||
("P6082", ["nycdoitt:bin"], "NYC Building Identification Number"),
|
||||
("P5086", ["ref"], "FIPS 5-2 alpha code (US states)"),
|
||||
("P5087", ["ref:fips"], "FIPS 5-2 numeric code (US states)"),
|
||||
("P5208", ["ref:bag"], "BAG building ID for Dutch buildings"),
|
||||
]
|
||||
|
||||
|
||||
@app.teardown_appcontext
|
||||
def shutdown_session(exception=None):
|
||||
database.session.remove()
|
||||
|
||||
|
||||
def check_for_tagged_qids(qids):
|
||||
tagged = set()
|
||||
for qid in qids:
|
||||
for cls in model.Point, model.Polygon, model.Line:
|
||||
q = cls.query.filter(cls.tags["wikidata"] == qid)
|
||||
print(q)
|
||||
if q.count():
|
||||
tagged.add(qid)
|
||||
break
|
||||
|
||||
return tagged
|
||||
|
||||
|
||||
def check_for_tagged_qid(qid):
|
||||
return any(
|
||||
database.session.query(
|
||||
cls.query.filter(
|
||||
cls.tags.has_key("wikidata"), cls.tags["wikidata"] == qid
|
||||
).exists()
|
||||
).scalar()
|
||||
for cls in (model.Point, model.Polygon, model.Line)
|
||||
)
|
||||
|
||||
|
||||
def get_tagged_qid(qid):
|
||||
tagged = []
|
||||
seen = set()
|
||||
for cls in (model.Point, model.Polygon, model.Line):
|
||||
q = cls.query.filter(cls.tags.has_key("wikidata"), cls.tags["wikidata"] == qid)
|
||||
for osm in q:
|
||||
if osm.identifier in seen:
|
||||
continue
|
||||
seen.add(osm.identifier)
|
||||
tagged.append(
|
||||
{
|
||||
"identifier": osm.identifier,
|
||||
"url": osm.osm_url,
|
||||
# 'geoson': osm.geojson(),
|
||||
"centroid": list(osm.get_centroid()),
|
||||
"name": osm.name or "[no label]",
|
||||
}
|
||||
)
|
||||
|
||||
return tagged
|
||||
|
||||
|
||||
def make_envelope(bbox):
|
||||
west, south, east, north = [float(i) for i in bbox.split(",")]
|
||||
return func.ST_MakeEnvelope(west, south, east, north, srid)
|
||||
|
||||
|
||||
def get_osm_with_wikidata_tag(bbox):
|
||||
db_bbox = make_envelope(bbox)
|
||||
|
||||
tagged = []
|
||||
|
||||
seen = set()
|
||||
for cls in (model.Point, model.Polygon, model.Line):
|
||||
q = cls.query.filter(
|
||||
cls.tags.has_key("wikidata"), func.ST_Covers(db_bbox, cls.way)
|
||||
)
|
||||
for osm in q:
|
||||
if osm.identifier in seen:
|
||||
continue
|
||||
seen.add(osm.identifier)
|
||||
name = osm.name
|
||||
if not name:
|
||||
if "addr:housename" in osm.tags:
|
||||
name = osm.tags["addr:housename"]
|
||||
else:
|
||||
name = "[no label]"
|
||||
|
||||
tagged.append(
|
||||
{
|
||||
"identifier": osm.identifier,
|
||||
"url": osm.osm_url,
|
||||
# 'geoson': osm.geojson(),
|
||||
"centroid": list(osm.get_centroid()),
|
||||
"name": name,
|
||||
"wikidata": osm.tags["wikidata"],
|
||||
}
|
||||
)
|
||||
|
||||
return tagged
|
||||
|
||||
|
||||
def get_items_in_bbox(bbox):
|
||||
db_bbox = make_envelope(bbox)
|
||||
|
||||
q = (
|
||||
model.Item.query.join(model.ItemLocation)
|
||||
.filter(func.ST_Covers(db_bbox, model.ItemLocation.location))
|
||||
.options(selectinload(model.Item.locations))
|
||||
)
|
||||
|
||||
return q
|
||||
|
||||
|
||||
def get_markers(all_items):
|
||||
items = []
|
||||
for item in all_items:
|
||||
if "en" not in item.labels:
|
||||
continue
|
||||
locations = [list(i.get_lat_lon()) for i in item.locations]
|
||||
item = {
|
||||
"qid": item.qid,
|
||||
"label": item.label(),
|
||||
"description": item.description(),
|
||||
"markers": locations,
|
||||
"isa_list": [v["id"] for v in item.get_claim("P31")],
|
||||
}
|
||||
items.append(item)
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def get_user_location():
|
||||
gi = GeoIP.open("/home/edward/lib/data/GeoIPCity.dat", GeoIP.GEOIP_STANDARD)
|
||||
|
||||
remote_ip = request.remote_addr
|
||||
gir = gi.record_by_addr(remote_ip)
|
||||
if not gir:
|
||||
return
|
||||
lat, lon = gir["latitude"], gir["longitude"]
|
||||
return (lat, lon)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def redirect_from_root():
|
||||
return redirect(url_for("map_start_page"))
|
||||
|
||||
|
||||
@app.route("/index")
|
||||
def index_page():
|
||||
return render_template("index.html")
|
||||
|
||||
|
||||
@app.route("/identifier")
|
||||
def identifier_index():
|
||||
return render_template("identifier_index.html", property_map=property_map)
|
||||
|
||||
|
||||
@app.route("/identifier/<pid>")
|
||||
def identifier_page(pid):
|
||||
per_page = 10
|
||||
page = int(request.args.get("page", 1))
|
||||
property_dict = {pid: (osm_keys, label) for pid, osm_keys, label in property_map}
|
||||
osm_keys, label = property_dict[pid]
|
||||
|
||||
wd = model.Item.query.filter(model.Item.claims.has_key(pid))
|
||||
total = wd.count()
|
||||
|
||||
start = per_page * (page - 1)
|
||||
items = wd.all()[start : per_page * page]
|
||||
|
||||
qids = [item.qid for item in items]
|
||||
print(qids)
|
||||
|
||||
# pred = None
|
||||
# values = set()
|
||||
# for item in items:
|
||||
# values |= set(item.get_claim(pid))
|
||||
#
|
||||
# for key in osm_keys:
|
||||
# if key == 'ref':
|
||||
# continue
|
||||
# if pred is None:
|
||||
# pred = model.Point.tags[key].in_(values)
|
||||
# else:
|
||||
# pred |= model.Point.tags[key].in_(values)
|
||||
#
|
||||
|
||||
osm_points = {}
|
||||
|
||||
for qid in qids:
|
||||
osm_points[qid] = model.Point.query.filter(
|
||||
model.Point.tags["wikidata"] == qid
|
||||
).all()
|
||||
|
||||
osm_total = len(osm_points)
|
||||
|
||||
return render_template(
|
||||
"identifier_page.html",
|
||||
pid=pid,
|
||||
osm_keys=osm_keys,
|
||||
label=label,
|
||||
items=items,
|
||||
total=total,
|
||||
osm_total=osm_total,
|
||||
osm_points=osm_points,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/map/<int:zoom>/<float(signed=True):lat>/<float(signed=True):lng>")
|
||||
def map_location(zoom, lat, lng):
|
||||
t = int(time())
|
||||
return render_template("map.html", zoom=zoom, lat=lat, lng=lng, time=t)
|
||||
|
||||
|
||||
@app.route("/map")
|
||||
def map_start_page():
|
||||
t = int(time())
|
||||
location = get_user_location()
|
||||
if not location:
|
||||
return render_template("map.html", zoom=16, lat=None, lng=None, time=t)
|
||||
|
||||
lat, lng = location
|
||||
return render_template("map.html", zoom=16, lat=lat, lng=lng, time=t)
|
||||
|
||||
|
||||
@app.route("/search/map")
|
||||
def search_map_page():
|
||||
user_lat, user_lon = get_user_location() or (None, None)
|
||||
|
||||
q = request.args.get("q")
|
||||
if not q:
|
||||
return render_template("map.html", user_lat=user_lat, user_lon=user_lon)
|
||||
|
||||
hits = nominatim.lookup(q)
|
||||
for hit in hits:
|
||||
if "geotext" in hit:
|
||||
del hit["geotext"]
|
||||
bbox = [hit["boundingbox"] for hit in hits]
|
||||
|
||||
return render_template(
|
||||
"search_map.html",
|
||||
hits=hits,
|
||||
bbox_list=bbox,
|
||||
user_lat=user_lat,
|
||||
user_lon=user_lon,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/search")
|
||||
def search_page():
|
||||
q = request.args.get("q")
|
||||
if not q:
|
||||
return render_template("search.html", hits=None, bbox_list=None)
|
||||
hits = nominatim.lookup(q)
|
||||
for hit in hits:
|
||||
if "geotext" in hit:
|
||||
del hit["geotext"]
|
||||
bbox = [hit["boundingbox"] for hit in hits]
|
||||
return render_template("search.html", hits=hits, bbox_list=bbox)
|
||||
|
||||
|
||||
def get_isa_count(items):
|
||||
isa_count = Counter()
|
||||
for item in items:
|
||||
isa_list = item.get_claim("P31")
|
||||
for isa in isa_list:
|
||||
isa_count[isa["id"]] += 1
|
||||
|
||||
return isa_count.most_common()
|
||||
|
||||
|
||||
@app.route("/api/1/items")
|
||||
def api_wikidata_items():
|
||||
bounds = request.args.get("bounds")
|
||||
t0 = time()
|
||||
q = get_items_in_bbox(bounds)
|
||||
db_items = q.all()
|
||||
items = get_markers(db_items)
|
||||
|
||||
counts = get_isa_count(db_items)
|
||||
isa_ids = [qid[1:] for qid, count in counts]
|
||||
isa_items = {
|
||||
isa.qid: isa for isa in model.Item.query.filter(model.Item.item_id.in_(isa_ids))
|
||||
}
|
||||
|
||||
isa_count = []
|
||||
for qid, count in counts:
|
||||
item = isa_items.get(qid)
|
||||
label = item.label() if item else "[missing]"
|
||||
isa = {
|
||||
"qid": qid,
|
||||
"count": count,
|
||||
"label": label,
|
||||
}
|
||||
isa_count.append(isa)
|
||||
|
||||
t1 = time() - t0
|
||||
print(f"wikidata: {t1} seconds")
|
||||
|
||||
return jsonify(success=True, items=items, isa_count=isa_count, duration=t1)
|
||||
|
||||
|
||||
@app.route("/api/1/osm")
|
||||
def api_osm_objects():
|
||||
bounds = request.args.get("bounds")
|
||||
t0 = time()
|
||||
objects = get_osm_with_wikidata_tag(bounds)
|
||||
t1 = time() - t0
|
||||
print(f"OSM: {t1} seconds")
|
||||
return jsonify(success=True, objects=objects, duration=t1)
|
||||
|
||||
|
||||
@app.route("/api/1/item/Q<int:item_id>")
|
||||
def api_item_detail(item_id):
|
||||
qid = f"Q{item_id}"
|
||||
tagged = get_tagged_qid(qid)
|
||||
return jsonify(qid=qid, tagged=bool(tagged), info=tagged)
|
||||
|
||||
|
||||
@app.route("/api/1/search")
|
||||
def api_search():
|
||||
q = request.args["q"]
|
||||
hits = nominatim.lookup(q)
|
||||
for hit in hits:
|
||||
if "geotext" in hit:
|
||||
del hit["geotext"]
|
||||
hit["name"] = nominatim.get_hit_name(hit)
|
||||
|
||||
return jsonify(hits=hits)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0")
|
Loading…
Reference in a new issue