Rewrite frontend using Vue
This commit is contained in:
parent
48ee6f9bd5
commit
503401ff57
723
frontend/App.vue
Normal file
723
frontend/App.vue
Normal file
|
@ -0,0 +1,723 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div id="map">
|
||||||
|
</div>
|
||||||
|
<button ref="btn" id="load-btn" type="button" class="btn btn-primary btn-lg" @click="load_wikidata_items">
|
||||||
|
<span v-if="!loading">
|
||||||
|
Load Wikidata items
|
||||||
|
</span>
|
||||||
|
<span v-if="loading">
|
||||||
|
<span class="spinner-border spinner-border-sm"></span>
|
||||||
|
Loading ...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div id="sidebar">
|
||||||
|
|
||||||
|
<div v-if="!current_item">
|
||||||
|
|
||||||
|
<div class="card m-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="search-form" class="row row-cols-lg-auto g-3 align-items-center" @submit.prevent="run_search">
|
||||||
|
<div class="col-12">
|
||||||
|
<input class="form-control" id="search-text" v-model.trim="search_text" placeholder="place">
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<button type="submit" id="search-btn" class="btn btn-primary">search</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="list-group">
|
||||||
|
<a class="list-group-item list-group-item-action"
|
||||||
|
:class="{ active: hit.identifier == this.active_hit }"
|
||||||
|
v-bind:key="hit.identifier"
|
||||||
|
v-for="hit in hits"
|
||||||
|
:href="hit_url(hit)"
|
||||||
|
@click.prevent="visit(hit)">
|
||||||
|
{{ hit.name }} ({{ hit.category }})
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card m-2" v-if="isa_list.length">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="h5 card-title">OSM/Wikidata link status</div>
|
||||||
|
<div class="list-group">
|
||||||
|
<label for="linked" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||||
|
<span>
|
||||||
|
<input class="form-check-input me-1" id="linked" type="checkbox" v-model="linked">
|
||||||
|
Wikidata items tagged in OSM
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-primary rounded-pill">{{ tagged_count }}</span>
|
||||||
|
</label><br>
|
||||||
|
<label for="not-linked" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||||
|
<span>
|
||||||
|
<input class="form-check-input me-1" id="not-linked" type="checkbox" v-model="not_linked">
|
||||||
|
Wikidata items not tagged in OSM
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-primary rounded-pill">{{ not_tagged_count }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card m-2" v-if="isa_list.length" id="isa-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="h5 card-title">item types</div>
|
||||||
|
<div><a href="#" @click.prevent="isa_tick_all">show all</a></div>
|
||||||
|
|
||||||
|
<div class="list-group" @mouseout="this.hover_isa=undefined">
|
||||||
|
<label class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" v-for="isa in isa_list" @mouseenter="this.hover_isa=isa">
|
||||||
|
<span>
|
||||||
|
<input class="form-check-input me-1" type="checkbox" :id="'isa-' + isa.qid" :value="isa.qid" v-model="isa_ticked">
|
||||||
|
{{ isa.label }} ({{ isa.qid }})
|
||||||
|
<a href="#" @click.stop="isa_ticked=[isa.qid]">only</a>
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-primary rounded-pill">{{ isa.count }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card m-2" id="detail-card" v-if="current_item">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="h4 card-title">
|
||||||
|
<span id="detail-header">item detail</span>
|
||||||
|
<button type="button" class="btn-close float-end" id="close-detail" @click="close_item()"></button>
|
||||||
|
</div>
|
||||||
|
<div id="detail">
|
||||||
|
<div class="row"><div class="col">
|
||||||
|
<strong>Wikidata item</strong><br>
|
||||||
|
<a :href="qid_url(wd_item.qid)" target="_blank">{{ wd_item.label }}</a> ({{ wd_item.qid }})
|
||||||
|
|
||||||
|
<span v-if="wd_item.description">
|
||||||
|
<br><strong>description</strong><br>{{ wd_item.description }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<br><strong>item type</strong>
|
||||||
|
<span v-bind:key="`isa-${wd_item.qid}-${isa_qid}`" v-for="isa_qid in wd_item.isa_list">
|
||||||
|
<br><a :href="qid_url(isa_qid)" target="_blank">{{isa_labels[isa_qid]}}</a> ({{isa_qid}})
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="wd_item.street_address.length">
|
||||||
|
<br><strong>street address</strong>
|
||||||
|
<br>{{wd_item.street_address[0]}}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div><div class="col">
|
||||||
|
|
||||||
|
<span v-if="current_item.tag_or_key_list && current_item.tag_or_key_list.length">
|
||||||
|
<strong>OSM tags/keys to search for</strong>
|
||||||
|
<span v-for="v in current_item.tag_or_key_list">
|
||||||
|
<br>{{ v }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="wd_item.image_list.length">
|
||||||
|
<img class="w-100" :src="api_base_url + '/commons/' + wd_item.image_list[0]">
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="current_item.nearby && current_item.nearby.length">
|
||||||
|
<strong>Possible OSM matches</strong><br>
|
||||||
|
<table class="table table-sm table-hover" @mouseout="this.current_osm = undefined">
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="osm in current_item.nearby"
|
||||||
|
class="osm-candidate"
|
||||||
|
@mouseenter="this.current_osm=osm">
|
||||||
|
<td class="text-end text-nowrap">
|
||||||
|
{{ osm.distance.toFixed(0) }}m
|
||||||
|
<a :href="'https://www.openstreetmap.org/' + osm.identifier" target="_blank"><i class="fa fa-map-o"></i></a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ osm.name || "no name" }}
|
||||||
|
<span v-for="(p, index) in osm.presets">
|
||||||
|
<span v-if="index != 0">, </span>
|
||||||
|
<a
|
||||||
|
:href="'http://wiki.openstreetmap.org/wiki/' + p.tag_or_key"
|
||||||
|
class="osm-wiki-link"
|
||||||
|
target="_blank">{{p.name}} <i class="fa fa-external-link"></i></a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="osm.address_list.length">
|
||||||
|
<br>address nodes: {{ osm.address_list.join("; ") }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import L from "leaflet";
|
||||||
|
import { ExtraMarkers } from "leaflet-extra-markers";
|
||||||
|
import axios from "redaxios";
|
||||||
|
|
||||||
|
var redMarker = ExtraMarkers.icon({
|
||||||
|
icon: "fa-wikidata",
|
||||||
|
markerColor: "red",
|
||||||
|
shape: "circle",
|
||||||
|
prefix: "fa",
|
||||||
|
});
|
||||||
|
|
||||||
|
var greenMarker = ExtraMarkers.icon({
|
||||||
|
icon: "fa-wikidata",
|
||||||
|
markerColor: "green",
|
||||||
|
shape: "circle",
|
||||||
|
prefix: "fa",
|
||||||
|
});
|
||||||
|
|
||||||
|
var blueMarker = ExtraMarkers.icon({
|
||||||
|
icon: "fa-wikidata",
|
||||||
|
markerColor: "blue",
|
||||||
|
shape: "circle",
|
||||||
|
prefix: "fa",
|
||||||
|
});
|
||||||
|
|
||||||
|
var osmYellowMarker = ExtraMarkers.icon({
|
||||||
|
icon: "fa-map",
|
||||||
|
markerColor: "yellow",
|
||||||
|
shape: "square",
|
||||||
|
prefix: "fa",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
startLat: Number,
|
||||||
|
startLon: Number,
|
||||||
|
startZoom: Number,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
api_base_url: "https://alpha.osm.wikidata.link",
|
||||||
|
tag_or_key_list: [],
|
||||||
|
search_text: "",
|
||||||
|
load_button_pressed: false,
|
||||||
|
hits: [],
|
||||||
|
center: undefined,
|
||||||
|
zoom: undefined,
|
||||||
|
isa_ticked: [],
|
||||||
|
isa_list: [],
|
||||||
|
isa_lookup: {},
|
||||||
|
items: {},
|
||||||
|
yellowMarker: osmYellowMarker,
|
||||||
|
osm_loaded: false,
|
||||||
|
wikidata_loaded: false,
|
||||||
|
osm_loading: false,
|
||||||
|
wikidata_loading: false,
|
||||||
|
current_item: undefined,
|
||||||
|
current_osm: undefined,
|
||||||
|
hover_qid: undefined,
|
||||||
|
isa_labels: {},
|
||||||
|
linked: true,
|
||||||
|
not_linked: true,
|
||||||
|
map: undefined,
|
||||||
|
hover_circles: [],
|
||||||
|
candidate_outline: undefined,
|
||||||
|
check_for_missing_done: false,
|
||||||
|
selected_circles: [],
|
||||||
|
hover_isa: undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
loading() {
|
||||||
|
return this.osm_loading || this.wikidata_loading;
|
||||||
|
},
|
||||||
|
wd_item() {
|
||||||
|
return this.current_item ? this.current_item.wikidata : undefined;
|
||||||
|
},
|
||||||
|
tagged_count() {
|
||||||
|
var count = 0;
|
||||||
|
for (const qid in this.items) {
|
||||||
|
var item = this.items[qid];
|
||||||
|
if (item.wikidata && item.osm) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
not_tagged_count() {
|
||||||
|
var count = 0;
|
||||||
|
for (const qid in this.items) {
|
||||||
|
var item = this.items[qid];
|
||||||
|
if (item.wikidata && !item.osm) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
selected_items() {
|
||||||
|
var ret = {};
|
||||||
|
for (const qid in this.items) {
|
||||||
|
var item = this.items[qid];
|
||||||
|
if (!item.wikidata) continue;
|
||||||
|
|
||||||
|
if (!this.linked && item.osm) continue;
|
||||||
|
if (!this.not_linked && !item.osm) continue;
|
||||||
|
|
||||||
|
if (item.wikidata.isa_list.some(isa => this.isa_ticked.includes(isa))) {
|
||||||
|
ret[qid] = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
selected_items(new_items, old_items) {
|
||||||
|
for (const qid of Object.keys(new_items)) {
|
||||||
|
if (!old_items[qid])
|
||||||
|
this.items[qid].group.addTo(this.map);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const qid of Object.keys(old_items)) {
|
||||||
|
if (!new_items[qid])
|
||||||
|
this.items[qid].group.removeFrom(this.map);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
current_osm(osm) {
|
||||||
|
if (this.candidate_outline !== undefined) {
|
||||||
|
this.candidate_outline.removeFrom(this.map);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (osm === undefined) return;
|
||||||
|
|
||||||
|
var mapStyle = { fillOpacity: 0, color: "red" };
|
||||||
|
var geojson = L.geoJSON(null, { style: mapStyle });
|
||||||
|
geojson.addData(osm.geojson);
|
||||||
|
geojson.addTo(this.map);
|
||||||
|
|
||||||
|
this.candidate_outline = geojson;
|
||||||
|
},
|
||||||
|
current_item(item, old_item) {
|
||||||
|
if (old_item) {
|
||||||
|
this.selected_circles.forEach((circle) => {
|
||||||
|
circle.removeFrom(this.map);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selected_circles = [];
|
||||||
|
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
item.markers.forEach((marker) => {
|
||||||
|
var coords = marker.getLatLng();
|
||||||
|
var circle = L.circle(coords, { radius: 20, color: "orange" }).addTo(this.map);
|
||||||
|
this.selected_circles.push(circle);
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
hover_isa(highlight_isa) {
|
||||||
|
this.drop_hover_circles();
|
||||||
|
|
||||||
|
for(const item of Object.values(this.selected_items)) {
|
||||||
|
var opacity = 0.9;
|
||||||
|
if (highlight_isa) {
|
||||||
|
var match = item.wikidata.isa_list.some(isa => isa == highlight_isa.qid);
|
||||||
|
opacity = match ? 1 : 0.2;
|
||||||
|
if (match) {
|
||||||
|
this.add_hover_circles(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.set_item_opacity(item, opacity);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isa_tick_all() {
|
||||||
|
this.isa_ticked = Object.keys(this.isa_labels);
|
||||||
|
},
|
||||||
|
build_map_path() {
|
||||||
|
var zoom = this.map.getZoom();
|
||||||
|
var c = this.map.getCenter();
|
||||||
|
var lat = c.lat.toFixed(5);
|
||||||
|
var lng = c.lng.toFixed(5);
|
||||||
|
var path = `/map/${zoom}/${lat}/${lng}`;
|
||||||
|
if (this.current_item) {
|
||||||
|
path += `?item=${this.wd_item.qid}`;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
},
|
||||||
|
|
||||||
|
mouse_events(marker, qid) {
|
||||||
|
marker.on("mouseover", () => { this.add_highlight(qid); });
|
||||||
|
marker.on("mouseout", () => { this.drop_highlight(qid); });
|
||||||
|
marker.on("click", () => { this.open_item(qid); });
|
||||||
|
|
||||||
|
var item = this.items[qid];
|
||||||
|
|
||||||
|
item.markers ||= [];
|
||||||
|
item.markers.push(marker);
|
||||||
|
},
|
||||||
|
|
||||||
|
set_item_opacity(item, opacity) {
|
||||||
|
if (item.outline) {
|
||||||
|
item.outline.setStyle({ opacity: opacity });
|
||||||
|
}
|
||||||
|
if (item.lines) {
|
||||||
|
item.lines.forEach((line) => {
|
||||||
|
line.setStyle({ opacity: opacity });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
item.markers.forEach((marker) => {
|
||||||
|
marker.setOpacity(opacity);
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
add_hover_circles(item) {
|
||||||
|
item.markers.forEach((marker) => {
|
||||||
|
var coords = marker.getLatLng();
|
||||||
|
var circle = L.circle(coords, { radius: 20 }).addTo(this.map);
|
||||||
|
this.hover_circles.push(circle);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
drop_hover_circles() {
|
||||||
|
this.hover_circles.forEach((circle) => {
|
||||||
|
circle.removeFrom(this.map);
|
||||||
|
});
|
||||||
|
this.hover_circles = [];
|
||||||
|
},
|
||||||
|
add_highlight(qid) {
|
||||||
|
var item = this.items[qid];
|
||||||
|
|
||||||
|
if (item.outline) {
|
||||||
|
item.outline.setStyle({ fillOpacity: 0.2, weight: 6 });
|
||||||
|
}
|
||||||
|
if (item.lines) {
|
||||||
|
item.lines.forEach((line) => {
|
||||||
|
line.setStyle({ weight: 6 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.add_hover_circles(item);
|
||||||
|
},
|
||||||
|
drop_highlight(qid) {
|
||||||
|
var item = this.items[qid];
|
||||||
|
|
||||||
|
if (item.outline) {
|
||||||
|
item.outline.setStyle({ fillOpacity: 0, weight: 3 });
|
||||||
|
}
|
||||||
|
if (item.lines) {
|
||||||
|
item.lines.forEach((line) => {
|
||||||
|
line.setStyle({ weight: 3 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.drop_hover_circles();
|
||||||
|
},
|
||||||
|
update_map_path() {
|
||||||
|
history.replaceState(null, null, this.build_map_path());
|
||||||
|
},
|
||||||
|
open_item(qid) {
|
||||||
|
var item = this.items[qid];
|
||||||
|
this.current_osm = undefined;
|
||||||
|
this.current_item = item;
|
||||||
|
this.update_map_path();
|
||||||
|
|
||||||
|
if (item.detail_requested !== undefined) return;
|
||||||
|
item.detail_requested = true;
|
||||||
|
|
||||||
|
var item_tags_url = `${this.api_base_url}/api/1/item/${qid}/tags`;
|
||||||
|
axios.get(item_tags_url).then((response) => {
|
||||||
|
var qid = response.data.qid;
|
||||||
|
this.items[qid].tag_or_key_list = response.data.tag_or_key_list;
|
||||||
|
});
|
||||||
|
|
||||||
|
var item_osm_candidates_url = `${this.api_base_url}/api/1/item/${qid}/candidates`;
|
||||||
|
var bounds = this.map.getBounds();
|
||||||
|
var params = { bounds: bounds.toBBoxString() };
|
||||||
|
|
||||||
|
axios.get(item_osm_candidates_url, { params: params }).then((response) => {
|
||||||
|
var qid = response.data.qid;
|
||||||
|
this.items[qid].nearby = response.data.nearby;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
close_item() {
|
||||||
|
this.current_osm = undefined;
|
||||||
|
this.current_item = undefined;
|
||||||
|
this.update_map_path();
|
||||||
|
},
|
||||||
|
qid_url(qid) {
|
||||||
|
return "https://www.wikidata.org/wiki/" + qid;
|
||||||
|
},
|
||||||
|
getMarker(item) {
|
||||||
|
if (!this.osm_loaded) return blueMarker;
|
||||||
|
return item.osm ? greenMarker : redMarker;
|
||||||
|
},
|
||||||
|
hit_url(hit) {
|
||||||
|
var lat = parseFloat(hit.lat).toFixed(5);
|
||||||
|
var lon = parseFloat(hit.lon).toFixed(5);
|
||||||
|
return `/map/16/${lat}/${lon}`
|
||||||
|
},
|
||||||
|
visit(hit) {
|
||||||
|
var lat = parseFloat(hit.lat).toFixed(5);
|
||||||
|
var lon = parseFloat(hit.lon).toFixed(5);
|
||||||
|
|
||||||
|
this.map.setView([lat, lon], 16);
|
||||||
|
},
|
||||||
|
|
||||||
|
process_wikidata_items(load_items) {
|
||||||
|
load_items.forEach(item => {
|
||||||
|
var qid = item.qid;
|
||||||
|
this.items[qid] ||= {};
|
||||||
|
if (this.items[qid].wikidata) return;
|
||||||
|
this.items[qid].wikidata = item;
|
||||||
|
var group = this.items[qid].group ||= L.featureGroup();
|
||||||
|
|
||||||
|
var icon = blueMarker;
|
||||||
|
var label = `${item.label} (${item.qid})`;
|
||||||
|
item.markers.forEach((marker_data) => {
|
||||||
|
var marker = L.marker(marker_data, { opacity: 0.9, icon: icon });
|
||||||
|
marker.addTo(group);
|
||||||
|
this.mouse_events(marker, qid);
|
||||||
|
marker_data.marker = marker;
|
||||||
|
});
|
||||||
|
// group.addTo(this.map);
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
clear_items() {
|
||||||
|
|
||||||
|
for (const qid of Object.keys(this.items)) {
|
||||||
|
this.items[qid].group.removeFrom(this.map);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.items = {};
|
||||||
|
this.isa_list = [];
|
||||||
|
this.isa_ticked = [];
|
||||||
|
this.isa_labels = {};
|
||||||
|
this.isa_lookup = {};
|
||||||
|
},
|
||||||
|
|
||||||
|
load_wikidata_items() {
|
||||||
|
this.load_button_pressed = true;
|
||||||
|
this.wikidata_loaded = false;
|
||||||
|
this.osm_loaded = false;
|
||||||
|
this.check_for_missing_done = false;
|
||||||
|
|
||||||
|
this.clear_items();
|
||||||
|
|
||||||
|
this.close_item();
|
||||||
|
|
||||||
|
this.wikidata_loading = true;
|
||||||
|
this.osm_loading = true;
|
||||||
|
|
||||||
|
var bounds = this.map.getBounds();
|
||||||
|
|
||||||
|
var items_url = this.api_base_url + "/api/1/items";
|
||||||
|
var osm_objects_url = this.api_base_url + "/api/1/osm";
|
||||||
|
|
||||||
|
var params = { bounds: bounds.toBBoxString() };
|
||||||
|
|
||||||
|
axios.get(items_url, { params: params }).then((response) => {
|
||||||
|
this.isa_list = response.data.isa_count;
|
||||||
|
this.isa_list.forEach(isa => {
|
||||||
|
this.isa_ticked.push(isa.qid);
|
||||||
|
this.isa_labels[isa.qid] = isa.label;
|
||||||
|
this.isa_lookup[isa.qid] = isa;
|
||||||
|
});
|
||||||
|
this.process_wikidata_items(response.data.items);
|
||||||
|
this.wikidata_loaded = true;
|
||||||
|
this.wikidata_loading = false;
|
||||||
|
|
||||||
|
this.check_for_missing();
|
||||||
|
});
|
||||||
|
|
||||||
|
axios.get(osm_objects_url, { params: params }).then((response) => {
|
||||||
|
response.data.objects.forEach((osm) => {
|
||||||
|
var qid = osm.wikidata;
|
||||||
|
this.items[qid] ||= {};
|
||||||
|
this.items[qid].osm ||= [];
|
||||||
|
this.items[qid].osm.push(osm);
|
||||||
|
var group = this.items[qid].group ||= L.featureGroup();
|
||||||
|
var icon = osmYellowMarker;
|
||||||
|
var marker = L.marker(osm.centroid, { opacity: 0.9, title: osm.name, icon: icon });
|
||||||
|
osm.marker = marker;
|
||||||
|
marker.addTo(group);
|
||||||
|
this.mouse_events(marker, qid);
|
||||||
|
|
||||||
|
if (osm.type != "node" && osm.geojson) {
|
||||||
|
var mapStyle = { fillOpacity: 0 };
|
||||||
|
var geojson = L.geoJSON(null, { style: mapStyle });
|
||||||
|
geojson.addData(osm.geojson);
|
||||||
|
geojson.addTo(group);
|
||||||
|
this.items[qid].outline = geojson;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.osm_loaded = true;
|
||||||
|
this.osm_loading = false;
|
||||||
|
|
||||||
|
this.check_for_missing();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
run_search() {
|
||||||
|
if (!this.search_text) return;
|
||||||
|
var params = { q: this.search_text };
|
||||||
|
var search_url = this.api_base_url + "/api/1/search";
|
||||||
|
axios.get(search_url, { params: params }).then((response) => {
|
||||||
|
this.hits = response.data.hits;
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
check_for_missing() {
|
||||||
|
if (this.check_for_missing_done) return;
|
||||||
|
if (!this.osm_loaded || !this.wikidata_loaded) return;
|
||||||
|
|
||||||
|
|
||||||
|
var missing_qids = [];
|
||||||
|
for (const [qid, item] of Object.entries(this.items)) {
|
||||||
|
if (!item.wikidata) missing_qids.push(qid);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('missing:', missing_qids);
|
||||||
|
if (missing_qids.length == 0) {
|
||||||
|
this.update_wikidata();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var c = this.map.getCenter();
|
||||||
|
var params = {
|
||||||
|
qids: missing_qids.join(","),
|
||||||
|
lat: c.lat.toFixed(5),
|
||||||
|
lon: c.lng.toFixed(5),
|
||||||
|
};
|
||||||
|
var missing_url = this.api_base_url + "/api/1/missing";
|
||||||
|
axios.get(missing_url, { params: params }).then((response) => {
|
||||||
|
response.data.isa_count.forEach((isa) => {
|
||||||
|
this.isa_labels[isa.qid] = isa.label;
|
||||||
|
if (this.isa_lookup[isa.qid] === undefined) {
|
||||||
|
this.isa_lookup[isa.qid] = isa;
|
||||||
|
this.isa_list.push(isa);
|
||||||
|
this.isa_ticked.push(isa.qid);
|
||||||
|
} else {
|
||||||
|
this.isa_lookup[isa.qid].count += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.process_wikidata_items(response.data.items);
|
||||||
|
this.update_wikidata();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
update_wikidata() {
|
||||||
|
for (const qid in this.items) {
|
||||||
|
var item = this.items[qid];
|
||||||
|
if (!item.osm) continue
|
||||||
|
|
||||||
|
var wd_item = item.wikidata;
|
||||||
|
|
||||||
|
item.osm.forEach((osm) => {
|
||||||
|
osm.marker.setIcon(wd_item ? osmYellowMarker : osmOrangeMarker);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!wd_item) continue;
|
||||||
|
|
||||||
|
wd_item.markers.forEach((marker_data) => {
|
||||||
|
marker_data.marker.setIcon(greenMarker);
|
||||||
|
item.lines ||= [];
|
||||||
|
item.osm.forEach((osm) => {
|
||||||
|
var path = [osm.centroid, marker_data];
|
||||||
|
var polyline = L.polyline(path, { color: "green" });
|
||||||
|
polyline.addTo(item.group)
|
||||||
|
this.items[qid].lines.push(polyline);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const qid in this.items) {
|
||||||
|
var item = this.items[qid];
|
||||||
|
if (item.osm) continue;
|
||||||
|
item.wikidata.markers.forEach((marker_data) => {
|
||||||
|
marker_data.marker.setIcon(redMarker);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
var lat = this.startLat ?? 52.19679;
|
||||||
|
var lon = this.startLon ?? 0.15224;
|
||||||
|
this.center = [lat, lon];
|
||||||
|
this.zoom = this.startZoom || 16;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$nextTick(function () {
|
||||||
|
var options = {
|
||||||
|
center: this.center,
|
||||||
|
zoom: this.zoom,
|
||||||
|
};
|
||||||
|
|
||||||
|
var map = L.map("map", options);
|
||||||
|
var osm_url = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
|
||||||
|
var tile_url = "https://tile-c.openstreetmap.fr/hot/{z}/{x}/{y}.png";
|
||||||
|
var osm = L.tileLayer(osm_url, {
|
||||||
|
maxZoom: 19,
|
||||||
|
});
|
||||||
|
osm.addTo(map);
|
||||||
|
|
||||||
|
map.on("moveend", this.update_map_path);
|
||||||
|
this.map = map;
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
#map {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 35%;
|
||||||
|
width: 65%;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#load-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 62.5%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#search {
|
||||||
|
position: absolute;
|
||||||
|
overflow: auto;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
bottom: 20px;
|
||||||
|
width: 25%;
|
||||||
|
background: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-highlight {
|
||||||
|
background: lightgray !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#load-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 62.5%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar {
|
||||||
|
position: absolute;
|
||||||
|
background: #eee;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
overflow: auto;
|
||||||
|
width: 35%;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
7
frontend/index.js
Normal file
7
frontend/index.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import {createApp} from 'vue';
|
||||||
|
import App from './App.vue';
|
||||||
|
|
||||||
|
export default function(props) {
|
||||||
|
const app = createApp(App, props).mount('#app');
|
||||||
|
return app;
|
||||||
|
}
|
22
package.json
Normal file
22
package.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "NODE_OPTIONS='--experimental-json-modules' snowpack dev",
|
||||||
|
"build": "NODE_OPTIONS='--experimental-json-modules' snowpack build",
|
||||||
|
"test": "echo \"This template does not include a test runner by default.\" && exit 1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bootstrap": "^5.0.1",
|
||||||
|
"fork-awesome": "^1.1.7",
|
||||||
|
"leaflet": "^1.7.1",
|
||||||
|
"leaflet-extra-markers": "^1.2.1",
|
||||||
|
"redaxios": "^0.4.1",
|
||||||
|
"vue": "^3.0.11"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@snowpack/plugin-dotenv": "^2.1.0",
|
||||||
|
"@snowpack/plugin-vue": "^2.4.0",
|
||||||
|
"snowpack": "^3.3.7",
|
||||||
|
"snowpack-plugin-cdn-import": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
35
snowpack.config.mjs
Normal file
35
snowpack.config.mjs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import pkg from './package.json';
|
||||||
|
|
||||||
|
/** @type {import("snowpack").SnowpackUserConfig } */
|
||||||
|
export default {
|
||||||
|
mount: {
|
||||||
|
// public: {url: '/', static: true},
|
||||||
|
frontend: {url: '/dist'},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
'@snowpack/plugin-vue',
|
||||||
|
'@snowpack/plugin-dotenv',
|
||||||
|
['snowpack-plugin-cdn-import', {
|
||||||
|
dependencies: pkg.dependencies,
|
||||||
|
enableInDevMode: true,
|
||||||
|
// baseUrl: 'https://unpkg.com',
|
||||||
|
}]
|
||||||
|
],
|
||||||
|
routes: [
|
||||||
|
/* Enable an SPA Fallback in development: */
|
||||||
|
// {"match": "routes", "src": ".*", "dest": "/index.html"},
|
||||||
|
],
|
||||||
|
optimize: {
|
||||||
|
/* Example: Bundle your final build: */
|
||||||
|
// "bundle": true,
|
||||||
|
},
|
||||||
|
packageOptions: {
|
||||||
|
/* ... */
|
||||||
|
},
|
||||||
|
devOptions: {
|
||||||
|
/* ... */
|
||||||
|
},
|
||||||
|
buildOptions: {
|
||||||
|
/* ... */
|
||||||
|
},
|
||||||
|
};
|
|
@ -2,87 +2,28 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Map showing Wikidata items linked to OSM</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='fork-awesome/css/fork-awesome.css')}}">
|
<title>Wikidata items linked to OSM</title>
|
||||||
<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/bootstrap@5.0.1/dist/css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css">
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='Leaflet.ExtraMarkers/leaflet.extra-markers.min.css') }}" type="text/css" media="all" />
|
<link rel="stylesheet" href="https://unpkg.com/fork-awesome@1.1.7/css/fork-awesome.min.css">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
<link rel="stylesheet" href="https://unpkg.com/leaflet-extra-markers@1.2.1/dist/css/leaflet.extra-markers.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="map">
|
<div id="app"></div>
|
||||||
</div>
|
|
||||||
<button id="load-btn" type="button" class="btn btn-primary btn-lg">
|
|
||||||
<span id="loading" class="d-none">
|
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
|
||||||
Loading ...
|
|
||||||
</span>
|
|
||||||
<span id="load-text">
|
|
||||||
Load Wikidata items
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div id="sidebar">
|
<script type="module">
|
||||||
<div id="search-and-isa">
|
import { createApp } from "https://cdn.skypack.dev/vue@^3.0.11";
|
||||||
<div class="card m-2" id="search-card">
|
import App from {{ url_for('static', filename='snowpack/App.vue.js') | tojson }};
|
||||||
<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 d-none m-2" id="link-status-card">
|
const props = {
|
||||||
<div class="card-body">
|
startLat: {{ lat }},
|
||||||
<div class="h5 card-title">OSM/Wikidata link status</div>
|
startLon: {{ lon }},
|
||||||
<div>
|
startZoom: {{ zoom }},
|
||||||
<input type="checkbox" id="linked" checked>
|
};
|
||||||
<label for="linked">Wikidata items tagged in OSM: <span id="linked-count"></span></label><br>
|
|
||||||
<input type="checkbox" id="not-linked" checked>
|
|
||||||
<label for="not-linked">Wikidata items not tagged in OSM: <span id="not-linked-count"></span></label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card d-none m-2" id="isa-card">
|
const app = createApp(App, props).mount('#app');
|
||||||
<div class="card-body">
|
|
||||||
<div class="h5 card-title">item types</div>
|
|
||||||
<div><a href="#" id="show-all-isa">show all</a></div>
|
|
||||||
<div id="isa-list">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card d-none m-2" id="detail-card">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="h4 card-title">
|
|
||||||
<span id="detail-header">item detail</span>
|
|
||||||
<button type="button" class="btn-close float-end" id="close-detail"></button>
|
|
||||||
</div>
|
|
||||||
<div id="detail"></div>
|
|
||||||
<div id="candidates"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var start_lat = {{ lat | tojson }};
|
|
||||||
var start_lng = {{ lng | tojson }};
|
|
||||||
var zoom = {{ zoom | tojson }};
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<script src="https://unpkg.com/axios@latest"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
|
||||||
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" 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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
88
templates/old_map.html
Normal file
88
templates/old_map.html
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<!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.7.1/dist/leaflet.css">
|
||||||
|
<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/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="map">
|
||||||
|
</div>
|
||||||
|
<button id="load-btn" type="button" class="btn btn-primary btn-lg">
|
||||||
|
<span id="loading" class="d-none">
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
|
Loading ...
|
||||||
|
</span>
|
||||||
|
<span id="load-text">
|
||||||
|
Load Wikidata items
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="sidebar">
|
||||||
|
<div id="search-and-isa">
|
||||||
|
<div class="card m-2" 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 d-none m-2" id="link-status-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="h5 card-title">OSM/Wikidata link status</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="linked" checked>
|
||||||
|
<label for="linked">Wikidata items tagged in OSM: <span id="linked-count"></span></label><br>
|
||||||
|
<input type="checkbox" id="not-linked" checked>
|
||||||
|
<label for="not-linked">Wikidata items not tagged in OSM: <span id="not-linked-count"></span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card d-none m-2" id="isa-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="h5 card-title">item types</div>
|
||||||
|
<div><a href="#" id="show-all-isa">show all</a></div>
|
||||||
|
<div id="isa-list">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card d-none m-2" id="detail-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="h4 card-title">
|
||||||
|
<span id="detail-header">item detail</span>
|
||||||
|
<button type="button" class="btn-close float-end" id="close-detail"></button>
|
||||||
|
</div>
|
||||||
|
<div id="detail"></div>
|
||||||
|
<div id="candidates"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var start_lat = {{ lat | tojson }};
|
||||||
|
var start_lng = {{ lng | tojson }};
|
||||||
|
var zoom = {{ zoom | tojson }};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<script src="https://unpkg.com/axios@latest"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" 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>
|
||||||
|
|
20
web_view.py
20
web_view.py
|
@ -273,14 +273,30 @@ def identifier_page(pid):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/map/<int:zoom>/<float(signed=True):lat>/<float(signed=True):lng>")
|
@app.route("/old_map/<int:zoom>/<float(signed=True):lat>/<float(signed=True):lng>")
|
||||||
def map_location(zoom, lat, lng):
|
def old_map_location(zoom, lat, lng):
|
||||||
t = int(time())
|
t = int(time())
|
||||||
return render_template("map.html", zoom=zoom, lat=lat, lng=lng, time=t)
|
return render_template("map.html", zoom=zoom, lat=lat, lng=lng, time=t)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/map")
|
@app.route("/map")
|
||||||
def map_start_page():
|
def map_start_page():
|
||||||
|
location = get_user_location()
|
||||||
|
lat, lon = location
|
||||||
|
return redirect(url_for(
|
||||||
|
'map_location',
|
||||||
|
zoom=16,
|
||||||
|
lat=f'{lat:.5f}',
|
||||||
|
lon=f'{lon:.5f}'
|
||||||
|
))
|
||||||
|
|
||||||
|
@app.route("/map/<int:zoom>/<float(signed=True):lat>/<float(signed=True):lon>")
|
||||||
|
def map_location(zoom, lat, lon):
|
||||||
|
return render_template("map.html", zoom=zoom, lat=lat, lon=lon)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/old_map")
|
||||||
|
def old_map_start_page():
|
||||||
t = int(time())
|
t = int(time())
|
||||||
location = get_user_location()
|
location = get_user_location()
|
||||||
if not location:
|
if not location:
|
||||||
|
|
Loading…
Reference in a new issue