forked from MapComplete/MapComplete
Merge feature branch
This commit is contained in:
commit
a6c752037b
28 changed files with 931 additions and 149 deletions
55
.github/workflows/deploy_hosted.yml
vendored
Normal file
55
.github/workflows/deploy_hosted.yml
vendored
Normal file
|
@ -0,0 +1,55 @@
|
|||
name: Deploy develop on dev.mapcomplete.org
|
||||
on:
|
||||
push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: "npm"
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: install deps
|
||||
run: npm ci
|
||||
shell: bash
|
||||
|
||||
- name: create generated dir
|
||||
run: mkdir ./assets/generated
|
||||
shell: bash
|
||||
|
||||
- name: create dependencies
|
||||
run: npm run generate:licenses; npm run generate:images; npm run generate:charging-stations; npm run generate:service-worker; npm run download:editor-layer-index
|
||||
shell: bash
|
||||
|
||||
- name: sync translations
|
||||
run: npm run generate:translations
|
||||
shell: bash
|
||||
|
||||
- name: run tests
|
||||
run: |
|
||||
pwd
|
||||
ls
|
||||
npm run test
|
||||
shell: bash
|
||||
|
||||
- name: Prepare deploy
|
||||
run: npm run prepare-deploy
|
||||
shell: bash
|
||||
|
||||
|
||||
|
||||
- name: Upload artefact
|
||||
env:
|
||||
SSH_KEY: ${{ secrets.HETZNER_KEY }}
|
||||
run: |
|
||||
mkdir .ssh
|
||||
echo $SSH_KEY > .ssh/id_ed25519
|
||||
scp dist/* pietervdvn@hosted.mapcomplete.org:/root/public/${{ github.ref_name }}
|
||||
|
||||
|
|
@ -3232,6 +3232,18 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "name",
|
||||
"question":{
|
||||
"en": "What is the name of this place?"
|
||||
},
|
||||
"render": {
|
||||
"*": "<b>{name}</b>"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "name"
|
||||
}
|
||||
}
|
||||
],
|
||||
"allowMove": false,
|
||||
|
|
70
assets/layers/usertouched/usertouched.json
Normal file
70
assets/layers/usertouched/usertouched.json
Normal file
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"id": "usertouched",
|
||||
"description": {
|
||||
"en": "Special layer showing all items which were changed by a certain user"
|
||||
},
|
||||
"name": {
|
||||
"en": "Changed by user"
|
||||
},
|
||||
"title": {
|
||||
"render": {
|
||||
"en": "Changed by user"
|
||||
}
|
||||
},
|
||||
"source": "special",
|
||||
"tagRenderings": [
|
||||
{
|
||||
"id": "test",
|
||||
"render": {
|
||||
"en": "Changed by user"
|
||||
}
|
||||
},
|
||||
"all_tags"
|
||||
],
|
||||
"pointRendering": [
|
||||
{
|
||||
"location": [
|
||||
"point",
|
||||
"centroid"
|
||||
],
|
||||
"iconSize": "15,15",
|
||||
"marker": [
|
||||
{
|
||||
"icon": "circle",
|
||||
"color": "#aaa"
|
||||
},
|
||||
{
|
||||
"icon": "ring",
|
||||
"color": "#000"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"lineRendering": [
|
||||
{
|
||||
"color": "black",
|
||||
"width": 3,
|
||||
"fillColor": "#00000000"
|
||||
},
|
||||
{
|
||||
"color": "#cccccccc",
|
||||
"width": {
|
||||
"render": 0,
|
||||
"mappings": [
|
||||
{
|
||||
"if": {
|
||||
"or": [
|
||||
"_geometry:type=Polygon",
|
||||
"_geometry:type=MultiPolygon"
|
||||
]
|
||||
},
|
||||
"then": 20
|
||||
}
|
||||
]
|
||||
},
|
||||
"offset": 15,
|
||||
"fillColor": "#00000000"
|
||||
}
|
||||
],
|
||||
"allowMove": false
|
||||
}
|
13
assets/themes/inspector/inspector.json
Normal file
13
assets/themes/inspector/inspector.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "inspector",
|
||||
"title": {
|
||||
"en": "Inspect changes from a single user"
|
||||
},
|
||||
"description": {
|
||||
"en": "A theme to inspect what a single user did in the past"
|
||||
},
|
||||
"icon": "./assets/svg/add.svg",
|
||||
"layers": [
|
||||
"usertouched"
|
||||
]
|
||||
}
|
|
@ -4,26 +4,20 @@
|
|||
"en": "Changes made with MapComplete",
|
||||
"de": "Änderungen mit MapComplete",
|
||||
"cs": "Změny provedené pomocí MapComplete",
|
||||
"es": "Cambios realizados con MapComplete",
|
||||
"fr": "Modifications faites avec MapComplete",
|
||||
"nl": "Wijzigingen gemaakt met MapComplete"
|
||||
"es": "Cambios realizados con MapComplete"
|
||||
},
|
||||
"shortDescription": {
|
||||
"en": "Shows changes made by MapComplete",
|
||||
"de": "Zeigt die von MapComplete vorgenommenen Änderungen an",
|
||||
"cs": "Zobrazuje změny provedené nástrojem MapComplete",
|
||||
"es": "Muestra los cambios realizados por MapComplete",
|
||||
"fr": "Afficher les modifications faites avec MapComplete",
|
||||
"nl": "Toont wijzigingen gemaakt met MapComplete"
|
||||
"es": "Muestra los cambios realizados por MapComplete"
|
||||
},
|
||||
"description": {
|
||||
"en": "This maps shows all the changes made with MapComplete",
|
||||
"de": "Diese Karte zeigt alle mit MapComplete vorgenommenen Änderungen",
|
||||
"es": "Este mapa muestra todos los cambios realizados con MapComplete",
|
||||
"pl": "Ta mapa pokazuje wszystkie zmiany wprowadzone za pomocą MapComplete",
|
||||
"cs": "Tyto mapy zobrazují všechny změny provedené pomocí MapComplete",
|
||||
"fr": "Cette carte montre tous les changements effectués avec MapComplete",
|
||||
"nl": "Deze kaarten tonen alle wijzigingen die zijn gemaakt met MapComplete"
|
||||
"cs": "Tyto mapy zobrazují všechny změny provedené pomocí MapComplete"
|
||||
},
|
||||
"icon": "./assets/svg/logo.svg",
|
||||
"hideFromOverview": true,
|
||||
|
@ -36,10 +30,7 @@
|
|||
"name": {
|
||||
"en": "Changeset centers",
|
||||
"de": "Changeset-Zentren",
|
||||
"es": "Centros de conjuntos de cambios",
|
||||
"fr": "Centre du groupe de modifications",
|
||||
"nl": "Changeset centra",
|
||||
"cs": "Changeset centra"
|
||||
"es": "Centros de conjuntos de cambios"
|
||||
},
|
||||
"minzoom": 0,
|
||||
"source": {
|
||||
|
@ -52,16 +43,14 @@
|
|||
"en": "Changeset for {theme}",
|
||||
"de": "Änderungssatz für {theme}",
|
||||
"cs": "Sada změn pro {theme}",
|
||||
"es": "Conjunto de cambios para {theme}",
|
||||
"nl": "Changeset voor {theme}"
|
||||
"es": "Conjunto de cambios para {theme}"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"en": "Shows all MapComplete changes",
|
||||
"de": "Zeigt alle MapComplete-Änderungen",
|
||||
"es": "Muestra todos los cambios de MapComplete",
|
||||
"cs": "Zobrazí všechny změny MapComplete",
|
||||
"nl": "Toon alle MapComplete-wijzigingen"
|
||||
"cs": "Zobrazí všechny změny MapComplete"
|
||||
},
|
||||
"tagRenderings": [
|
||||
{
|
||||
|
@ -70,8 +59,7 @@
|
|||
"en": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
|
||||
"de": "Änderungssatz <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
|
||||
"cs": "Sada změn <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
|
||||
"es": "Conjunto de cambios <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>",
|
||||
"nl": "Changeset <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>"
|
||||
"es": "Conjunto de cambios <a href='https://openstreetmap.org/changeset/{id}' target='_blank'>{id}</a>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -80,8 +68,7 @@
|
|||
"en": "What contributor did make this change?",
|
||||
"de": "Wer hat zu dieser Änderung beigetragen?",
|
||||
"cs": "Který přispěvatel provedl tuto změnu?",
|
||||
"es": "¿Qué colaborador realizó este cambio?",
|
||||
"nl": "Welke bijdrager maakte deze verandering?"
|
||||
"es": "¿Qué colaborador realizó este cambio?"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "user"
|
||||
|
@ -90,9 +77,7 @@
|
|||
"en": "Change made by <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
|
||||
"de": "Änderung vorgenommen von <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
|
||||
"cs": "Změna provedena uživatelem <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
|
||||
"es": "Cambio realizado por <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
|
||||
"fr": "Modification faite par <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>",
|
||||
"nl": "Wijziging aangebracht door <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>"
|
||||
"es": "Cambio realizado por <a href='https://openstreetmap.org/user/{user}' target='_blank'>{user}</a>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -101,8 +86,7 @@
|
|||
"en": "What theme was used to make this change?",
|
||||
"de": "Welches Thema wurde für diese Änderung verwendet?",
|
||||
"cs": "Jaký motiv byl použit k provedení této změny?",
|
||||
"es": "¿Qué tema se utilizó para realizar este cambio?",
|
||||
"nl": "Welk thema werd gebruikt voor deze wijziging?"
|
||||
"es": "¿Qué tema se utilizó para realizar este cambio?"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "theme"
|
||||
|
@ -110,9 +94,7 @@
|
|||
"render": {
|
||||
"en": "Change with theme <a href='https://mapcomplete.org/{theme}'>{theme}</a>",
|
||||
"de": "Änderung mit Thema <a href='https://mapcomplete.org/{theme}'>{theme}</a>",
|
||||
"es": "Cambio con el tema <a href='https://mapcomplete.org/{theme}'>{theme}</a>",
|
||||
"nl": "Verander met thema <a href='https://mapcomplete.org/{theme}'>{theme}</a>",
|
||||
"cs": "Změna pomocí tématu <a href='https://mapcomplete.org/{theme}'>{theme}</a>"
|
||||
"es": "Cambio con el tema <a href='https://mapcomplete.org/{theme}'>{theme}</a>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -124,15 +106,13 @@
|
|||
"en": "What locale (language) was this change made in?",
|
||||
"de": "In welcher Sprache (Locale) wurde diese Änderung vorgenommen?",
|
||||
"cs": "V jakém prostředí (jazyce) byla tato změna provedena?",
|
||||
"es": "¿En qué configuración regional (idioma) se realizó este cambio?",
|
||||
"nl": "In welke 'locale' (taal) is deze wijziging gemaakt?"
|
||||
"es": "¿En qué configuración regional (idioma) se realizó este cambio?"
|
||||
},
|
||||
"render": {
|
||||
"en": "User locale is {locale}",
|
||||
"de": "Die Benutzersprache ist {locale}",
|
||||
"cs": "Uživatelské prostředí je {locale}",
|
||||
"es": "Configuración regional del usuario es {locale}",
|
||||
"nl": "De gebruikerstaal (locale) is {locale}"
|
||||
"es": "Configuración regional del usuario es {locale}"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -141,15 +121,13 @@
|
|||
"en": "Change with with <a href='{host}'>{host}</a>",
|
||||
"de": "Änderung mit <a href='{host}'>{host}</a>",
|
||||
"cs": "Změnit pomocí <a href='{host}'>{host}</a>",
|
||||
"es": "Cambio realizado con <a href='{host}'>{host}</a>",
|
||||
"nl": "Gewijzigd met <a href='{host}'>{host}</a>"
|
||||
"es": "Cambio realizado con <a href='{host}'>{host}</a>"
|
||||
},
|
||||
"question": {
|
||||
"en": "What host (website) was this change made with?",
|
||||
"de": "Bei welchem Host (Website) wurde diese Änderung vorgenommen?",
|
||||
"cs": "U jakého hostitele (webové stránky) byla tato změna provedena?",
|
||||
"es": "¿Con qué anfitrión (sitio web) se realizó este cambio?",
|
||||
"nl": "Met welke host (website) is deze wijziging gemaakt?"
|
||||
"es": "¿Con qué anfitrión (sitio web) se realizó este cambio?"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "host"
|
||||
|
@ -173,17 +151,13 @@
|
|||
"en": "What version of MapComplete was used to make this change?",
|
||||
"de": "Welche Version von MapComplete wurde verwendet, um diese Änderung vorzunehmen?",
|
||||
"cs": "Jaká verze aplikace MapComplete byla použita k provedení této změny?",
|
||||
"es": "¿Qué versión de MapComplete se utilizó para realizar este cambio?",
|
||||
"fr": "Quelle version de MapCompletee a été utilisée pour faire cette modification ?",
|
||||
"nl": "Welke versie van MapComplete is gebruikt voor deze wijziging?"
|
||||
"es": "¿Qué versión de MapComplete se utilizó para realizar este cambio?"
|
||||
},
|
||||
"render": {
|
||||
"en": "Made with {editor}",
|
||||
"de": "Erstellt mit {editor}",
|
||||
"cs": "Vytvořeno pomocí {editor}",
|
||||
"es": "Hecho con {editor}",
|
||||
"fr": "Fait avec {editor}",
|
||||
"nl": "Gemaakt met {editor}"
|
||||
"es": "Hecho con {editor}"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "editor"
|
||||
|
@ -378,8 +352,8 @@
|
|||
"then": "./assets/layers/entrance/entrance.svg"
|
||||
},
|
||||
{
|
||||
"if": "theme=insects",
|
||||
"then": "./assets/layers/insect_hotel/insect_hotel.svg"
|
||||
"if": "theme=inspector",
|
||||
"then": "./assets/svg/add.svg"
|
||||
},
|
||||
{
|
||||
"if": "theme=items_with_image",
|
||||
|
@ -589,9 +563,7 @@
|
|||
"de": "Themenname enthält {search}",
|
||||
"es": "El nombre del tema contiene {search}",
|
||||
"pl": "Nazwa tematu zawiera {search}",
|
||||
"cs": "Název obsahuje {search}",
|
||||
"fr": "Le nom du thème contient {search}",
|
||||
"nl": "Themanaam bevat {search}"
|
||||
"cs": "Název obsahuje {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -610,9 +582,7 @@
|
|||
"en": "Themename does <b>not</b> contain {search}",
|
||||
"de": "Themename enthält <b>nicht</b> {search}",
|
||||
"es": "El nombre del tema <b>no</b> contiene {search}",
|
||||
"cs": "Název motivu <b>neobsahuje</b> {search}",
|
||||
"fr": "Le nom du thème <b>ne contient pas</b> {search}",
|
||||
"nl": "Themanaam bevat <b>geen</b> {search}"
|
||||
"cs": "Název motivu <b>neobsahuje</b> {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -631,9 +601,7 @@
|
|||
"en": "Made by contributor {search}",
|
||||
"de": "Erstellt von Mitwirkendem {search}",
|
||||
"es": "Hecho por el colaborador {search}",
|
||||
"cs": "Vytvořeno přispěvatelem {search}",
|
||||
"fr": "Fait par le·a contributeur·trice {search}",
|
||||
"nl": "Toegevoegd door {search}"
|
||||
"cs": "Vytvořeno přispěvatelem {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -652,9 +620,7 @@
|
|||
"en": "<b>Not</b> made by contributor {search}",
|
||||
"de": "<b>Nicht</b> erstellt von Mitwirkendem {search}",
|
||||
"es": "<b>No</b> hecho por el colaborador {search}",
|
||||
"cs": "<b>Nevytvořeno</b> přispěvatelem {search}",
|
||||
"fr": "<b>Pas</b> fait par le·a contributeur·trice {search}",
|
||||
"nl": "<b>Niet</b> toegevoegd door {search}"
|
||||
"cs": "<b>Nevytvořeno</b> přispěvatelem {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -674,9 +640,7 @@
|
|||
"en": "Made before {search}",
|
||||
"de": "Erstellt vor {search}",
|
||||
"es": "Hecho antes de {search}",
|
||||
"cs": "Vytvořeno před {search}",
|
||||
"fr": "Fait avant {search}",
|
||||
"nl": "Toegevoegd vóór {search}"
|
||||
"cs": "Vytvořeno před {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -696,9 +660,7 @@
|
|||
"en": "Made after {search}",
|
||||
"de": "Erstellt nach {search}",
|
||||
"es": "Hecho después de {search}",
|
||||
"cs": "Vytvořeno po {search}",
|
||||
"fr": "Fait après {search}",
|
||||
"nl": "Toegevoegd na {search}"
|
||||
"cs": "Vytvořeno po {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -717,9 +679,7 @@
|
|||
"en": "User language (iso-code) {search}",
|
||||
"de": "Benutzersprache (ISO-Code) {search}",
|
||||
"es": "Idioma del usuario (código ISO) {search}",
|
||||
"cs": "Jazyk uživatele (iso-kód) {search}",
|
||||
"fr": "Langage utilisateur (code iso) {search}",
|
||||
"nl": "Gebruikerstaal (iso-code) {search}"
|
||||
"cs": "Jazyk uživatele (iso-kód) {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -738,8 +698,7 @@
|
|||
"en": "Made with host {search}",
|
||||
"de": "Erstellt mit Host {search}",
|
||||
"cs": "Vytvořeno pomocí hostitele {search}",
|
||||
"es": "Hecho con el anfitrión {search}",
|
||||
"nl": "Gemaakt met {search}"
|
||||
"es": "Hecho con el anfitrión {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -753,8 +712,7 @@
|
|||
"en": "Changeset added at least one image",
|
||||
"de": "Changeset hat mindestens ein Bild hinzugefügt",
|
||||
"cs": "Sada změn přidala alespoň jeden obrázek",
|
||||
"es": "El conjunto de cambios agregó al menos una imagen",
|
||||
"nl": "Changeset voegde minstens één afbeelding toe"
|
||||
"es": "El conjunto de cambios agregó al menos una imagen"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -768,8 +726,7 @@
|
|||
"en": "Exclude GRB theme",
|
||||
"de": "GRB-Thema ausschließen",
|
||||
"cs": "Vyloučit motiv GRB",
|
||||
"es": "Excluir el tema GRB",
|
||||
"nl": "GRB-thema uitsluiten"
|
||||
"es": "Excluir el tema GRB"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -783,8 +740,7 @@
|
|||
"en": "Exclude etymology theme",
|
||||
"de": "Etymologie-Thema ausschließen",
|
||||
"es": "Excluir el tema de etimología",
|
||||
"cs": "Vyloučit etymologii tématu",
|
||||
"nl": "Thema etymologie uitsluiten"
|
||||
"cs": "Vyloučit etymologii tématu"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -802,9 +758,7 @@
|
|||
"en": "More statistics can be found <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>here</a>",
|
||||
"de": "Weitere Statistiken findest du <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>hier</a>",
|
||||
"cs": "Další statistiky najdete <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>zde</a>",
|
||||
"es": "Puedes encontrar más estadísticas <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>aquí</a>",
|
||||
"fr": "Plus de statistiques peuvent être trouvées <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>ici</a>",
|
||||
"nl": "Meer statistieken vind je <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>hier</a>"
|
||||
"es": "Puedes encontrar más estadísticas <a href='https://github.com/pietervdvn/MapComplete/tree/develop/Docs/Tools/graphs' target='_blank'>aquí</a>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -675,6 +675,9 @@
|
|||
"pickTheme": "Pick a theme below to get started.",
|
||||
"title": "MapComplete"
|
||||
},
|
||||
"inspector": {
|
||||
"menu": "Inspect a contributor"
|
||||
},
|
||||
"move": {
|
||||
"cancel": "Select a different reason",
|
||||
"cannotBeMoved": "This feature cannot be moved.",
|
||||
|
|
|
@ -643,11 +643,15 @@ class LayerOverviewUtils extends Script {
|
|||
LayerOverviewUtils.layerPath +
|
||||
sharedLayerPath.substring(sharedLayerPath.lastIndexOf("/"))
|
||||
if (!forceReload && !this.shouldBeUpdated(sharedLayerPath, targetPath)) {
|
||||
const sharedLayer = JSON.parse(readFileSync(targetPath, "utf8"))
|
||||
sharedLayers.set(sharedLayer.id, sharedLayer)
|
||||
skippedLayers.push(sharedLayer.id)
|
||||
ScriptUtils.erasableLog("Loaded " + sharedLayer.id)
|
||||
continue
|
||||
try{
|
||||
const sharedLayer = JSON.parse(readFileSync(targetPath, "utf8"))
|
||||
sharedLayers.set(sharedLayer.id, sharedLayer)
|
||||
skippedLayers.push(sharedLayer.id)
|
||||
ScriptUtils.erasableLog("Loaded " + sharedLayer.id)
|
||||
continue
|
||||
}catch (e) {
|
||||
throw "Could not parse "+targetPath+" : "+e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import Panoramax_bw from "../../assets/svg/Panoramax_bw.svelte"
|
|||
import Link from "../../UI/Base/Link"
|
||||
|
||||
export default class PanoramaxImageProvider extends ImageProvider {
|
||||
public static readonly singleton = new PanoramaxImageProvider()
|
||||
public static readonly singleton: PanoramaxImageProvider = new PanoramaxImageProvider()
|
||||
private static readonly xyz = new PanoramaxXYZ()
|
||||
private static defaultPanoramax = new AuthorizedPanoramax(
|
||||
Constants.panoramax.url,
|
||||
|
@ -126,7 +126,11 @@ export default class PanoramaxImageProvider extends ImageProvider {
|
|||
if (!Panoramax.isId(value)) {
|
||||
return undefined
|
||||
}
|
||||
return [await this.getInfoFor(value).then((r) => this.featureToImage(<any>r))]
|
||||
return [await this.getInfo(value)]
|
||||
}
|
||||
|
||||
public async getInfo(hash: string): Promise<ProvidedImage> {
|
||||
return await this.getInfoFor(hash).then((r) => this.featureToImage(<any>r))
|
||||
}
|
||||
|
||||
getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> {
|
||||
|
|
|
@ -14,7 +14,7 @@ export default class OsmObjectDownloader {
|
|||
readonly isUploading: Store<boolean>
|
||||
}
|
||||
private readonly backend: string
|
||||
private historyCache = new Map<string, UIEventSource<OsmObject[]>>()
|
||||
private historyCache = new Map<string, Promise<OsmObject[]>>()
|
||||
|
||||
constructor(
|
||||
backend: string = "https://api.openstreetmap.org",
|
||||
|
@ -75,49 +75,51 @@ export default class OsmObjectDownloader {
|
|||
return await this.applyPendingChanges(obj)
|
||||
}
|
||||
|
||||
public DownloadHistory(id: NodeId): UIEventSource<OsmNode[]>
|
||||
|
||||
public DownloadHistory(id: WayId): UIEventSource<OsmWay[]>
|
||||
|
||||
public DownloadHistory(id: RelationId): UIEventSource<OsmRelation[]>
|
||||
|
||||
public DownloadHistory(id: OsmId): UIEventSource<OsmObject[]>
|
||||
|
||||
public DownloadHistory(id: string): UIEventSource<OsmObject[]> {
|
||||
if (this.historyCache.has(id)) {
|
||||
return this.historyCache.get(id)
|
||||
}
|
||||
private async _downloadHistoryUncached(id: string): Promise<OsmObject[]> {
|
||||
const splitted = id.split("/")
|
||||
const type = splitted[0]
|
||||
const idN = Number(splitted[1])
|
||||
const src = new UIEventSource<OsmObject[]>([])
|
||||
this.historyCache.set(id, src)
|
||||
Utils.downloadJsonCached(
|
||||
const data = await Utils.downloadJsonCached(
|
||||
`${this.backend}api/0.6/${type}/${idN}/history`,
|
||||
10 * 60 * 1000
|
||||
).then((data) => {
|
||||
const elements: any[] = data.elements
|
||||
const osmObjects: OsmObject[] = []
|
||||
for (const element of elements) {
|
||||
let osmObject: OsmObject = null
|
||||
element.nodes = []
|
||||
switch (type) {
|
||||
case "node":
|
||||
osmObject = new OsmNode(idN, element)
|
||||
break
|
||||
case "way":
|
||||
osmObject = new OsmWay(idN, element)
|
||||
break
|
||||
case "relation":
|
||||
osmObject = new OsmRelation(idN, element)
|
||||
break
|
||||
}
|
||||
osmObject?.SaveExtraData(element, [])
|
||||
osmObjects.push(osmObject)
|
||||
)
|
||||
const elements: [] = data["elements"]
|
||||
const osmObjects: OsmObject[] = []
|
||||
for (const element of elements) {
|
||||
let osmObject: OsmObject = null
|
||||
element["nodes"] = []
|
||||
switch (type) {
|
||||
case "node":
|
||||
osmObject = new OsmNode(idN, element)
|
||||
break
|
||||
case "way":
|
||||
osmObject = new OsmWay(idN, element)
|
||||
break
|
||||
case "relation":
|
||||
osmObject = new OsmRelation(idN, element)
|
||||
break
|
||||
}
|
||||
src.setData(osmObjects)
|
||||
})
|
||||
return src
|
||||
osmObject?.SaveExtraData(element, [])
|
||||
osmObjects.push(osmObject)
|
||||
}
|
||||
return osmObjects
|
||||
}
|
||||
|
||||
public downloadHistory(id: NodeId): Promise<OsmNode[]>
|
||||
|
||||
public downloadHistory(id: WayId): Promise<OsmWay[]>
|
||||
|
||||
public downloadHistory(id: RelationId): Promise<OsmRelation[]>
|
||||
|
||||
public downloadHistory(id: OsmId): Promise<OsmObject[]>
|
||||
|
||||
public async downloadHistory(id: string): Promise<OsmObject[]> {
|
||||
if (this.historyCache.has(id)) {
|
||||
return this.historyCache.get(id)
|
||||
}
|
||||
const promise = this._downloadHistoryUncached(id)
|
||||
this.historyCache.set(id, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -26,7 +26,10 @@ export class Overpass {
|
|||
) {
|
||||
this._timeout = timeout ?? new ImmutableStore<number>(90)
|
||||
this._interpreterUrl = interpreterUrl
|
||||
const optimized = filter.optimize()
|
||||
if (filter === undefined && !extraScripts) {
|
||||
throw "Filter is undefined. This is probably a bug. Alternatively, pass an 'extraScript'"
|
||||
}
|
||||
const optimized = filter?.optimize()
|
||||
if (optimized === true || optimized === false) {
|
||||
throw "Invalid filter: optimizes to true of false"
|
||||
}
|
||||
|
@ -85,7 +88,7 @@ export class Overpass {
|
|||
* new Overpass(new Tag("key","value"), [], "").buildScript("{{bbox}}") // => `[out:json][timeout:90]{{bbox}};(nwr["key"="value"];);out body;out meta;>;out skel qt;`
|
||||
*/
|
||||
public buildScript(bbox: string, postCall: string = "", pretty = false): string {
|
||||
const filters = this._filter.asOverpass()
|
||||
const filters = this._filter?.asOverpass() ?? []
|
||||
let filter = ""
|
||||
for (const filterOr of filters) {
|
||||
if (pretty) {
|
||||
|
@ -97,12 +100,13 @@ export class Overpass {
|
|||
}
|
||||
}
|
||||
for (const extraScript of this._extraScripts) {
|
||||
filter += "(" + extraScript + ");"
|
||||
filter += extraScript
|
||||
}
|
||||
return `[out:json][timeout:${this._timeout.data}]${bbox};(${filter});out body;${
|
||||
this._includeMeta ? "out meta;" : ""
|
||||
}>;out skel qt;`
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the actual script to execute on Overpass with geocoding
|
||||
* 'PostCall' can be used to set an extra range, see 'AsOverpassTurboLink'
|
||||
|
|
|
@ -727,6 +727,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Parse the number and round to the nearest int
|
||||
*
|
||||
* @param source
|
||||
* UIEventSource.asInt(new UIEventSource("123")).data // => 123
|
||||
|
|
|
@ -41,6 +41,7 @@ export default class Constants {
|
|||
"usersettings",
|
||||
"icons",
|
||||
"filters",
|
||||
"usertouched"
|
||||
] as const
|
||||
/**
|
||||
* Layer IDs of layers which have special properties through built-in hooks
|
||||
|
|
|
@ -306,7 +306,7 @@ export default class ThemeConfig implements ThemeInformation {
|
|||
return { untranslated, total }
|
||||
}
|
||||
|
||||
public getMatchingLayer(tags: Record<string, string>): LayerConfig | undefined {
|
||||
public getMatchingLayer(tags: Record<string, string>, blacklistLayers?: Set<string>): LayerConfig | undefined {
|
||||
if (tags === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
@ -314,6 +314,9 @@ export default class ThemeConfig implements ThemeInformation {
|
|||
return this.getLayer("current_view")
|
||||
}
|
||||
for (const layer of this.layers) {
|
||||
if(blacklistLayers?.has(layer.id)){
|
||||
continue
|
||||
}
|
||||
if (!layer.source) {
|
||||
if (layer.isShown?.matchesProperties(tags)) {
|
||||
return layer
|
||||
|
|
|
@ -1061,7 +1061,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
/**
|
||||
* Searches the appropriate layer - will first try if a special layer matches; if not, a normal layer will be used by delegating to the theme
|
||||
*/
|
||||
public getMatchingLayer(properties: Record<string, string>) {
|
||||
public getMatchingLayer(properties: Record<string, string>): LayerConfig | undefined {
|
||||
const id = properties.id
|
||||
|
||||
if (id.startsWith("summary_")) {
|
||||
|
|
|
@ -3,24 +3,15 @@
|
|||
import type { Feature } from "geojson"
|
||||
import SelectedElementView from "../BigComponents/SelectedElementView.svelte"
|
||||
import SelectedElementTitle from "../BigComponents/SelectedElementTitle.svelte"
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import { LastClickFeatureSource } from "../../Logic/FeatureSource/Sources/LastClickFeatureSource"
|
||||
import Loading from "./Loading.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { GeocodingUtils } from "../../Logic/Search/GeocodingProvider"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let selected: Feature
|
||||
let tags = state.featureProperties.getStore(selected.properties.id)
|
||||
|
||||
export let absolute = true
|
||||
function getLayer(properties: Record<string, string>): LayerConfig {
|
||||
return state.getMatchingLayer(properties)
|
||||
}
|
||||
|
||||
let layer = getLayer(selected.properties)
|
||||
let layer = state.getMatchingLayer(selected.properties)
|
||||
|
||||
let stillMatches = tags.map(
|
||||
(tags) => !layer?.source?.osmTags || layer?.source?.osmTags?.matchesProperties(tags)
|
||||
|
|
|
@ -279,6 +279,11 @@
|
|||
</Page>
|
||||
</div>
|
||||
|
||||
<a class="flex" href={window.location.protocol + "//" + window.location.host + "/inspector.html"}>
|
||||
<MagnifyingGlassCircle class="mr-2 h-6 w-6" />
|
||||
<Tr t={Translations.t.inspector.menu} />
|
||||
</a>
|
||||
|
||||
<a class="flex" href="https://github.com/pietervdvn/MapComplete/" target="_blank">
|
||||
<Github class="h-6 w-6" />
|
||||
<Tr t={Translations.t.general.attribution.gotoSourceCode} />
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
<script lang="ts">
|
||||
import type { Feature } from "geojson"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||
import { CloseButton } from "flowbite-svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let layer: LayerConfig
|
||||
export let selectedElement: Feature
|
||||
let tags: UIEventSource<Record<string, string>> = state.featureProperties.getStore(
|
||||
let tags: UIEventSource<Record<string, string>> = state?.featureProperties?.getStore(
|
||||
selectedElement.properties.id
|
||||
)
|
||||
$: {
|
||||
tags = state.featureProperties.getStore(selectedElement.properties.id)
|
||||
tags = state?.featureProperties?.getStore(selectedElement.properties.id)
|
||||
}
|
||||
|
||||
let isTesting = state.featureSwitchIsTesting
|
||||
|
|
46
src/UI/History/AggregateImages.svelte
Normal file
46
src/UI/History/AggregateImages.svelte
Normal file
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts">
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { HistoryUtils } from "./HistoryUtils"
|
||||
import type { Feature } from "geojson"
|
||||
import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader"
|
||||
import { OsmObject } from "../../Logic/Osm/OsmObject"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import AttributedPanoramaxImage from "./AttributedPanoramaxImage.svelte"
|
||||
|
||||
export let onlyShowUsername: string[]
|
||||
export let features: Feature[]
|
||||
|
||||
const downloader = new OsmObjectDownloader()
|
||||
let allHistories: UIEventSource<OsmObject[][]> = UIEventSource.FromPromise(
|
||||
Promise.all(features.map(f => downloader.downloadHistory(f.properties.id)))
|
||||
)
|
||||
let imageKeys = new Set(...["panoramax", "image:streetsign", "image:menu"].map(k => {
|
||||
const result: string[] = [k]
|
||||
for (let i = 0; i < 10; i++) {
|
||||
result.push(k + ":" + i)
|
||||
}
|
||||
return result
|
||||
}))
|
||||
let usernamesSet = new Set(onlyShowUsername)
|
||||
let allDiffs: Store<{
|
||||
key: string;
|
||||
value?: string;
|
||||
oldValue?: string
|
||||
}[]> = allHistories.mapD(histories => HistoryUtils.fullHistoryDiff(histories, usernamesSet))
|
||||
|
||||
let addedImages = allDiffs.mapD(diffs => [].concat(...diffs.filter(({ key }) => imageKeys.has(key))))
|
||||
|
||||
</script>
|
||||
{#if $allDiffs === undefined}
|
||||
<Loading />
|
||||
{:else if $addedImages.length === 0}
|
||||
No images added by this contributor
|
||||
{:else}
|
||||
<div class="flex">
|
||||
{#each $addedImages as imgDiff}
|
||||
<div class="w-48 h-48">
|
||||
<AttributedPanoramaxImage hash={imgDiff.value} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
105
src/UI/History/AggregateView.svelte
Normal file
105
src/UI/History/AggregateView.svelte
Normal file
|
@ -0,0 +1,105 @@
|
|||
<script lang="ts">
|
||||
import type { Feature } from "geojson"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader"
|
||||
import { OsmObject } from "../../Logic/Osm/OsmObject"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import { HistoryUtils } from "./HistoryUtils"
|
||||
import * as shared_questions from "../../assets/generated/layers/questions.json"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
export let onlyShowUsername: string[]
|
||||
export let features: Feature[]
|
||||
|
||||
let usernames = new Set(onlyShowUsername)
|
||||
|
||||
const downloader = new OsmObjectDownloader()
|
||||
let allHistories: UIEventSource<OsmObject[][]> = UIEventSource.FromPromise(
|
||||
Promise.all(features.map(f => downloader.downloadHistory(f.properties.id)))
|
||||
)
|
||||
let allDiffs: Store<{
|
||||
key: string;
|
||||
value?: string;
|
||||
oldValue?: string
|
||||
}[]> = allHistories.mapD(histories => HistoryUtils.fullHistoryDiff(histories, usernames))
|
||||
|
||||
const trs = shared_questions.tagRenderings.map(tr => new TagRenderingConfig(tr))
|
||||
|
||||
function detectQuestion(key: string): TagRenderingConfig {
|
||||
return trs.find(tr => tr.freeform?.key === key)
|
||||
}
|
||||
|
||||
const mergedCount: Store<{
|
||||
key: string;
|
||||
tr: TagRenderingConfig;
|
||||
count: number;
|
||||
values: { value: string; count: number }[]
|
||||
}[]> = allDiffs.mapD(allDiffs => {
|
||||
const keyCounts = new Map<string, Map<string, number>>()
|
||||
for (const diff of allDiffs) {
|
||||
const k = diff.key
|
||||
if (!keyCounts.has(k)) {
|
||||
keyCounts.set(k, new Map<string, number>())
|
||||
}
|
||||
const valueCounts = keyCounts.get(k)
|
||||
const v = diff.value ?? ""
|
||||
valueCounts.set(v, 1 + (valueCounts.get(v) ?? 0))
|
||||
}
|
||||
|
||||
const perKey: {
|
||||
key: string, tr: TagRenderingConfig, count: number, values:
|
||||
{ value: string, count: number }[]
|
||||
}[] = []
|
||||
keyCounts.forEach((values, key) => {
|
||||
const keyTotal: { value: string, count: number }[] = []
|
||||
values.forEach((count, value) => {
|
||||
keyTotal.push({ value, count })
|
||||
})
|
||||
let countForKey = 0
|
||||
for (const { count } of keyTotal) {
|
||||
countForKey += count
|
||||
}
|
||||
keyTotal.sort((a, b) => b.count - a.count)
|
||||
const tr = detectQuestion(key)
|
||||
perKey.push({ count: countForKey, tr, key, values: keyTotal })
|
||||
})
|
||||
perKey.sort((a, b) => b.count - a.count)
|
||||
|
||||
return perKey
|
||||
})
|
||||
|
||||
const t = Translations.t.inspector
|
||||
|
||||
</script>
|
||||
|
||||
{#if allHistories === undefined}
|
||||
<Loading />
|
||||
{:else if $allDiffs !== undefined}
|
||||
{#each $mergedCount as diff}
|
||||
<h3>
|
||||
{#if diff.tr}
|
||||
<Tr t={diff.tr.question} />
|
||||
{:else}
|
||||
{diff.key}
|
||||
{/if}
|
||||
</h3>
|
||||
<AccordionSingle>
|
||||
<span slot="header">
|
||||
<Tr t={t.answeredCountTimes.Subs(diff)} />
|
||||
</span>
|
||||
<ul>
|
||||
{#each diff.values as value}
|
||||
<li>
|
||||
<b>{value.value}</b>
|
||||
{#if value.count > 1}
|
||||
- {value.count}
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</AccordionSingle>
|
||||
{/each}
|
||||
{/if}
|
13
src/UI/History/AttributedPanoramaxImage.svelte
Normal file
13
src/UI/History/AttributedPanoramaxImage.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import AttributedImage from "../Image/AttributedImage.svelte"
|
||||
import PanoramaxImageProvider from "../../Logic/ImageProviders/Panoramax"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
|
||||
|
||||
export let hash: string
|
||||
let image: UIEventSource<ProvidedImage> = UIEventSource.FromPromise(PanoramaxImageProvider.singleton.getInfo(hash))
|
||||
</script>
|
||||
|
||||
{#if $image !== undefined}
|
||||
<AttributedImage image={$image}></AttributedImage>
|
||||
{/if}
|
106
src/UI/History/History.svelte
Normal file
106
src/UI/History/History.svelte
Normal file
|
@ -0,0 +1,106 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Shows a history of the object which focuses on changes made by a certain username
|
||||
*/
|
||||
import type { OsmId } from "../../Models/OsmFeature"
|
||||
import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import { HistoryUtils } from "./HistoryUtils"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
export let onlyShowChangesBy: string[]
|
||||
export let id: OsmId
|
||||
|
||||
let usernames = new Set(onlyShowChangesBy)
|
||||
let fullHistory = UIEventSource.FromPromise(new OsmObjectDownloader().downloadHistory(id))
|
||||
|
||||
let partOfLayer = fullHistory.mapD(history => history.map(step => ({
|
||||
step,
|
||||
layer: HistoryUtils.determineLayer(step.tags)
|
||||
})))
|
||||
let filteredHistory = partOfLayer.mapD(history =>
|
||||
history.filter(({ step }) => {
|
||||
if (usernames.size == 0) {
|
||||
return true
|
||||
}
|
||||
console.log("Checking if ", step.tags["_last_edit:contributor"],"is contained in", onlyShowChangesBy)
|
||||
return usernames.has(step.tags["_last_edit:contributor"])
|
||||
|
||||
}).map(({ step, layer }) => {
|
||||
const diff = HistoryUtils.tagHistoryDiff(step, fullHistory.data)
|
||||
return { step, layer, diff }
|
||||
}))
|
||||
|
||||
let lastStep = filteredHistory.mapD(history => history.at(-1))
|
||||
let allGeometry = filteredHistory.mapD(all => !all.some(x => x.diff.length > 0))
|
||||
/**
|
||||
* These layers are only shown if there are tag changes as well
|
||||
*/
|
||||
const ignoreLayersIfNoChanges: ReadonlySet<string> = new Set(["walls_and_buildings"])
|
||||
const t = Translations.t.inspector.previousContributors
|
||||
|
||||
</script>
|
||||
|
||||
{#if !$allGeometry || !ignoreLayersIfNoChanges.has($lastStep?.layer?.id)}
|
||||
{#if $lastStep?.layer}
|
||||
<a href={"https://openstreetmap.org/" + $lastStep.step.tags.id} target="_blank">
|
||||
<h3 class="flex items-center gap-x-2">
|
||||
<div class="w-8 h-8 shrink-0 inline-block">
|
||||
<ToSvelte construct={$lastStep.layer?.defaultIcon($lastStep.step.tags)} />
|
||||
</div>
|
||||
<Tr t={$lastStep.layer?.title?.GetRenderValue($lastStep.step.tags)?.Subs($lastStep.step.tags)} />
|
||||
</h3>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#if !$filteredHistory}
|
||||
<Loading>Loading history...</Loading>
|
||||
{:else if $filteredHistory.length === 0}
|
||||
<Tr t={t.onlyGeometry} />
|
||||
{:else}
|
||||
<table class="w-full m-1">
|
||||
{#each $filteredHistory as { step, layer }}
|
||||
|
||||
{#if step.version === 1}
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
<h3>
|
||||
<Tr t={t.createdBy.Subs({contributor: step.tags["_last_edit:contributor"]})} />
|
||||
</h3>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if HistoryUtils.tagHistoryDiff(step, $fullHistory).length === 0}
|
||||
<tr>
|
||||
<td class="font-bold justify-center flex w-full" colspan="3">
|
||||
<Tr t={t.onlyGeometry} />
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
{#each HistoryUtils.tagHistoryDiff(step, $fullHistory) as diff}
|
||||
<tr>
|
||||
<td><a href={"https://osm.org/changeset/"+step.tags["_last_edit:changeset"]}
|
||||
target="_blank">{step.version}</a></td>
|
||||
<td>{layer?.id ?? "Unknown layer"}</td>
|
||||
{#if diff.oldValue === undefined}
|
||||
<td>{diff.key}</td>
|
||||
<td>{diff.value}</td>
|
||||
{:else if diff.value === undefined }
|
||||
<td>{diff.key}</td>
|
||||
<td class="line-through"> {diff.value}</td>
|
||||
{:else}
|
||||
<td>{diff.key}</td>
|
||||
<td><span class="line-through"> {diff.oldValue}</span> → {diff.value}</td>
|
||||
{/if}
|
||||
|
||||
|
||||
</tr>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
</table>
|
||||
{/if}
|
||||
{/if}
|
51
src/UI/History/HistoryUtils.ts
Normal file
51
src/UI/History/HistoryUtils.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import * as all_layers from "../../assets/generated/themes/personal.json"
|
||||
import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
|
||||
import { OsmObject } from "../../Logic/Osm/OsmObject"
|
||||
|
||||
export class HistoryUtils {
|
||||
|
||||
public static readonly personalTheme = new ThemeConfig(<any> all_layers, true)
|
||||
private static ignoredLayers = new Set<string>(["fixme"])
|
||||
public static determineLayer(properties: Record<string, string>){
|
||||
return this.personalTheme.getMatchingLayer(properties, this.ignoredLayers)
|
||||
}
|
||||
|
||||
public static tagHistoryDiff(step: OsmObject, history: OsmObject[]): {
|
||||
key: string,
|
||||
value?: string,
|
||||
oldValue?: string,
|
||||
step: OsmObject
|
||||
}[] {
|
||||
const previous = history[step.version - 2]
|
||||
if (!previous) {
|
||||
return Object.keys(step.tags).filter(key => !key.startsWith("_") && key !== "id").map(key => ({
|
||||
key, value: step.tags[key], step
|
||||
}))
|
||||
}
|
||||
const previousTags = previous.tags
|
||||
return Object.keys(step.tags).filter(key => !key.startsWith("_") )
|
||||
.map(key => {
|
||||
const value = step.tags[key]
|
||||
const oldValue = previousTags[key]
|
||||
return {
|
||||
key, value, oldValue, step
|
||||
}
|
||||
}).filter(ch => ch.oldValue !== ch.value)
|
||||
}
|
||||
|
||||
public static fullHistoryDiff(histories: OsmObject[][], onlyShowUsername?: Set<string>){
|
||||
const allDiffs: {key: string, oldValue?: string, value?: string}[] = [].concat(...histories.map(
|
||||
history => {
|
||||
const filtered = history.filter(step => !onlyShowUsername || onlyShowUsername?.has(step.tags["_last_edit:contributor"] ))
|
||||
const diffs: {
|
||||
key: string;
|
||||
value?: string;
|
||||
oldValue?: string
|
||||
}[][] = filtered.map(step => HistoryUtils.tagHistoryDiff(step, history))
|
||||
return [].concat(...diffs)
|
||||
}
|
||||
))
|
||||
return allDiffs
|
||||
}
|
||||
|
||||
}
|
107
src/UI/History/PreviouslySpiedUsers.svelte
Normal file
107
src/UI/History/PreviouslySpiedUsers.svelte
Normal file
|
@ -0,0 +1,107 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
|
||||
import Dropdown from "../Base/Dropdown.svelte"
|
||||
|
||||
export let osmConnection: OsmConnection
|
||||
export let inspectedContributors: UIEventSource<{
|
||||
name: string,
|
||||
visitedTime: string,
|
||||
label: string
|
||||
}[]>
|
||||
let dispatch = createEventDispatcher<{ selectUser: string }>()
|
||||
|
||||
let labels = UIEventSource.asObject<string[]>(osmConnection.getPreference("previously-spied-labels"), [])
|
||||
let labelField = ""
|
||||
|
||||
function remove(user: string) {
|
||||
inspectedContributors.set(inspectedContributors.data.filter(entry => entry.name !== user))
|
||||
}
|
||||
|
||||
function addLabel() {
|
||||
if (labels.data.indexOf(labelField) >= 0) {
|
||||
return
|
||||
}
|
||||
labels.data.push(labelField)
|
||||
labels.ping()
|
||||
labelField = ""
|
||||
}
|
||||
|
||||
function sort(key: string) {
|
||||
console.log("Sorting on", key)
|
||||
inspectedContributors.data.sort((a, b) => (a[key] ?? "").localeCompare(b[key] ?? ""))
|
||||
inspectedContributors.ping()
|
||||
}
|
||||
</script>
|
||||
|
||||
<LoginToggle ignoreLoading state={{osmConnection}}>
|
||||
<table class="w-full">
|
||||
<tr>
|
||||
<td>
|
||||
<button class="as-link cursor-pointer" on:click={() => sort("name")}>
|
||||
Contributor
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<button class="as-link cursor-pointer" on:click={() => sort("visitedTime")}>
|
||||
Visited time
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<button class="as-link cursor-pointer" on:click={() => sort("label")}>Label</button>
|
||||
</td>
|
||||
<td>Remove</td>
|
||||
</tr>
|
||||
{#each $inspectedContributors as c}
|
||||
<tr>
|
||||
<td>
|
||||
<button class="as-link" on:click={() => dispatch("selectUser", c.name)}>{c.name}</button>
|
||||
</td>
|
||||
<td>
|
||||
{c.visitedTime}
|
||||
</td>
|
||||
<td>
|
||||
<select bind:value={c.label} on:change={() => inspectedContributors.ping()}>
|
||||
<option value={undefined}><i>No label</i></option>
|
||||
{#each $labels as l}
|
||||
<option value={l}>{l}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<XCircleIcon class="w-6 h-6" on:click={() => remove(c.name)} />
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
|
||||
<AccordionSingle>
|
||||
|
||||
<div slot="header">Labels</div>
|
||||
{#if $labels.length === 0}
|
||||
No labels
|
||||
{:else}
|
||||
{#each $labels as label}
|
||||
<div class="mx-2">{label}
|
||||
<button class:disabled={!$inspectedContributors.some(c => c.label === label)} on:click={() => {dispatch("selectUser",
|
||||
inspectedContributors.data.filter(c =>c.label === label).map(c => c .name).join(";")
|
||||
)}}>See all changes for these users
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
<div class="interactive flex m-2 items-center gap-x-2 rounded-lg p-2">
|
||||
<div class="shrink-0">Create a new label</div>
|
||||
<input bind:value={labelField} type="text" />
|
||||
<button on:click={() => addLabel()} class:disabled={!(labelField?.length > 0) } class="disabled shrink-0">Add
|
||||
label
|
||||
</button>
|
||||
</div>
|
||||
</AccordionSingle>
|
||||
</LoginToggle>
|
|
@ -28,22 +28,24 @@
|
|||
export let imgClass: string = undefined
|
||||
export let state: SpecialVisualizationState = undefined
|
||||
export let attributionFormat: "minimal" | "medium" | "large" = "medium"
|
||||
export let previewedImage: UIEventSource<ProvidedImage>
|
||||
export let previewedImage: UIEventSource<ProvidedImage> = undefined
|
||||
export let canZoom = previewedImage !== undefined
|
||||
let loaded = false
|
||||
let showBigPreview = new UIEventSource(false)
|
||||
onDestroy(
|
||||
showBigPreview.addCallbackAndRun((shown) => {
|
||||
if (!shown) {
|
||||
previewedImage.set(undefined)
|
||||
previewedImage?.set(undefined)
|
||||
}
|
||||
})
|
||||
)
|
||||
if(previewedImage){
|
||||
onDestroy(
|
||||
previewedImage.addCallbackAndRun((previewedImage) => {
|
||||
showBigPreview.set(previewedImage?.id === image.id)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function highlight(entered: boolean = true) {
|
||||
if (!entered) {
|
||||
|
@ -82,7 +84,7 @@
|
|||
class="normal-background"
|
||||
on:click={() => {
|
||||
console.log("Closing")
|
||||
previewedImage.set(undefined)
|
||||
previewedImage?.set(undefined)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -124,7 +126,7 @@
|
|||
{#if canZoom && loaded}
|
||||
<div
|
||||
class="bg-black-transparent absolute right-0 top-0 rounded-bl-full"
|
||||
on:click={() => previewedImage.set(image)}
|
||||
on:click={() => previewedImage?.set(image)}
|
||||
>
|
||||
<MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" />
|
||||
</div>
|
||||
|
|
227
src/UI/InspectorGUI.svelte
Normal file
227
src/UI/InspectorGUI.svelte
Normal file
|
@ -0,0 +1,227 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../Logic/UIEventSource"
|
||||
import { QueryParameters } from "../Logic/Web/QueryParameters"
|
||||
import ValidatedInput from "./InputElement/ValidatedInput.svelte"
|
||||
import { Overpass } from "../Logic/Osm/Overpass"
|
||||
import Constants from "../Models/Constants"
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte"
|
||||
import { MapLibreAdaptor } from "./Map/MapLibreAdaptor"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import ShowDataLayer from "./Map/ShowDataLayer"
|
||||
import * as inspector_theme from "../assets/generated/themes/inspector.json"
|
||||
|
||||
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import type { Feature } from "geojson"
|
||||
import Loading from "./Base/Loading.svelte"
|
||||
import { linear } from "svelte/easing"
|
||||
import { Drawer } from "flowbite-svelte"
|
||||
import ThemeConfig from "../Models/ThemeConfig/ThemeConfig"
|
||||
import History from "./History/History.svelte"
|
||||
import TitledPanel from "./Base/TitledPanel.svelte"
|
||||
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import { Utils } from "../Utils"
|
||||
import AggregateView from "./History/AggregateView.svelte"
|
||||
import { HistoryUtils } from "./History/HistoryUtils"
|
||||
import AggregateImages from "./History/AggregateImages.svelte"
|
||||
import Page from "./Base/Page.svelte"
|
||||
import PreviouslySpiedUsers from "./History/PreviouslySpiedUsers.svelte"
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||
import MagnifyingGlassCircle from "@babeard/svelte-heroicons/outline/MagnifyingGlassCircle"
|
||||
import Translations from "./i18n/Translations"
|
||||
import Tr from "./Base/Tr.svelte"
|
||||
|
||||
let username = QueryParameters.GetQueryParameter("user", undefined, "Inspect this user")
|
||||
let step = new UIEventSource<"waiting" | "loading" | "done">("waiting")
|
||||
let map = new UIEventSource<MlMap>(undefined)
|
||||
let zoom = UIEventSource.asFloat(QueryParameters.GetQueryParameter("z", "0"))
|
||||
let lat = UIEventSource.asFloat(QueryParameters.GetQueryParameter("lat", "0"))
|
||||
let lon = UIEventSource.asFloat(QueryParameters.GetQueryParameter("lon", "0"))
|
||||
let theme = new ThemeConfig(<any>inspector_theme, true)
|
||||
let layer = theme.layers.find(l => l.id === "usertouched")
|
||||
// Is this a dirty hack? Yes it is!
|
||||
theme.getMatchingLayer = () => {
|
||||
return layer
|
||||
}
|
||||
let loadingData = false
|
||||
let selectedElement = new UIEventSource<Feature>(undefined)
|
||||
|
||||
let maplibremap: MapLibreAdaptor = new MapLibreAdaptor(map, {
|
||||
zoom,
|
||||
location: new UIEventSource<{ lon: number; lat: number }>({ lat: lat.data, lon: lon.data })
|
||||
})
|
||||
maplibremap.location.stabilized(500).addCallbackAndRunD(l => {
|
||||
lat.set(l.lat)
|
||||
lon.set(l.lon)
|
||||
})
|
||||
|
||||
let allLayers = HistoryUtils.personalTheme.layers
|
||||
let layersNoFixme = allLayers.filter(l => l.id !== "fixme")
|
||||
let fixme = allLayers.find(l => l.id === "fixme")
|
||||
let featuresStore = new UIEventSource<Feature[]>([])
|
||||
let features = new StaticFeatureSource(featuresStore)
|
||||
ShowDataLayer.showMultipleLayers(map, features, [...layersNoFixme, fixme] , {
|
||||
zoomToFeatures: true,
|
||||
onClick: (f: Feature) => {
|
||||
selectedElement.set(undefined)
|
||||
Utils.waitFor(200).then(() => {
|
||||
selectedElement.set(f)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
let osmConnection = new OsmConnection()
|
||||
let inspectedContributors: UIEventSource<{
|
||||
name: string,
|
||||
visitedTime: string,
|
||||
label: string
|
||||
}[]> = UIEventSource.asObject(
|
||||
osmConnection.getPreference("spied-upon-users"), [])
|
||||
|
||||
async function load() {
|
||||
const user = username.data
|
||||
if(user.indexOf(";")<0){
|
||||
|
||||
const inspectedData = inspectedContributors.data
|
||||
const previousEntry = inspectedData.find(e => e.name === user)
|
||||
if (previousEntry) {
|
||||
previousEntry.visitedTime = new Date().toISOString()
|
||||
} else {
|
||||
inspectedData.push({
|
||||
label: undefined,
|
||||
visitedTime: new Date().toISOString(),
|
||||
name: user
|
||||
})
|
||||
}
|
||||
inspectedContributors.ping()
|
||||
}
|
||||
|
||||
step.setData("loading")
|
||||
featuresStore.set([])
|
||||
const overpass = new Overpass(undefined, user.split(";").map(user => "nw(user_touched:\"" + user + "\");"), Constants.defaultOverpassUrls[0])
|
||||
if (!maplibremap.bounds.data) {
|
||||
return
|
||||
}
|
||||
loadingData = true
|
||||
const [data, date] = await overpass.queryGeoJson(maplibremap.bounds.data)
|
||||
console.log("Overpass result:", data)
|
||||
loadingData = false
|
||||
console.log(data, date)
|
||||
featuresStore.set(data.features)
|
||||
console.log("Loaded", data.features.length)
|
||||
}
|
||||
|
||||
map.addCallbackAndRunD(() => {
|
||||
// when the map is loaded: attempt to load the user given via Queryparams
|
||||
if (username.data) {
|
||||
console.log("Current username is", username.data)
|
||||
load()
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
let mode: "map" | "table" | "aggregate" | "images" = "map"
|
||||
|
||||
let showPreviouslyVisited = new UIEventSource(true)
|
||||
const t = Translations.t.inspector
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col w-full h-full">
|
||||
|
||||
<div class="flex gap-x-2 items-center low-interaction p-2">
|
||||
<MagnifyingGlassCircle class="w-12 h-12"/>
|
||||
<h1 class="flex-shrink-0 m-0 mx-2">
|
||||
<Tr t={t.title}/>
|
||||
</h1>
|
||||
<ValidatedInput type="string" value={username} on:submit={() => load()} />
|
||||
{#if loadingData}
|
||||
<Loading />
|
||||
{:else}
|
||||
<button class="primary" on:click={() => load()}>
|
||||
<Tr t={t.load}/>
|
||||
</button>
|
||||
{/if}
|
||||
<button on:click={() => showPreviouslyVisited.setData(true)}>
|
||||
<Tr t={t.earlierInspected}/>
|
||||
</button>
|
||||
<a href="./index.html" class="button">
|
||||
<Tr t={t.backToIndex}/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<button class:primary={mode === "map"} on:click={() => mode = "map"}>
|
||||
<Tr t={t.mapView}/>
|
||||
</button>
|
||||
<button class:primary={mode === "table"} on:click={() => mode = "table"}>
|
||||
<Tr t={t.tableView}/>
|
||||
</button>
|
||||
<button class:primary={mode === "aggregate"} on:click={() => mode = "aggregate"}>
|
||||
<Tr t={t.aggregateView}/>
|
||||
</button>
|
||||
<button class:primary={mode === "images"} on:click={() => mode = "images"}>
|
||||
<Tr t={t.images}/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if mode === "map"}
|
||||
{#if $selectedElement !== undefined}
|
||||
<!-- right modal with the selected element view -->
|
||||
<Drawer
|
||||
placement="right"
|
||||
transitionType="fly"
|
||||
activateClickOutside={false}
|
||||
backdrop={false}
|
||||
id="drawer-right"
|
||||
width="w-full md:w-6/12 lg:w-5/12 xl:w-4/12"
|
||||
rightOffset="inset-y-0 right-0"
|
||||
transitionParams={{
|
||||
x: 640,
|
||||
duration: 0,
|
||||
easing: linear,
|
||||
}}
|
||||
divClass="overflow-y-auto z-50 bg-white"
|
||||
hidden={$selectedElement === undefined}
|
||||
on:close={() => {
|
||||
selectedElement.setData(undefined)
|
||||
}}
|
||||
>
|
||||
|
||||
<TitledPanel>
|
||||
<div slot="title" class="flex justify-between">
|
||||
|
||||
<a target="_blank" rel="noopener"
|
||||
href={"https://osm.org/"+$selectedElement.properties.id}>{$selectedElement.properties.id}</a>
|
||||
<XCircleIcon class="w-6 h-6" on:click={() => selectedElement.set(undefined)} />
|
||||
</div>
|
||||
|
||||
<History onlyShowChangesBy={$username} id={$selectedElement.properties.id}></History>
|
||||
</TitledPanel>
|
||||
</Drawer>
|
||||
{/if}
|
||||
|
||||
<div class="flex-grow overflow-hidden m-1 rounded-xl">
|
||||
<MaplibreMap map={map} mapProperties={maplibremap} autorecovery={true} />
|
||||
</div>
|
||||
{:else if mode === "table"}
|
||||
<div class="m-2 h-full overflow-y-auto">
|
||||
{#each $featuresStore as f}
|
||||
<History onlyShowChangesBy={$username?.split(";")} id={f.properties.id} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if mode === "aggregate"}
|
||||
<div class="m-2 h-full overflow-y-auto">
|
||||
<AggregateView features={$featuresStore} onlyShowUsername={$username?.split(";")} />
|
||||
</div>
|
||||
{:else if mode === "images"}
|
||||
<div class="m-2 h-full overflow-y-auto">
|
||||
<AggregateImages features={$featuresStore} onlyShowUsername={$username?.split(";")} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Page shown={showPreviouslyVisited}>
|
||||
<div slot="header">Earlier inspected constributors</div>
|
||||
<PreviouslySpiedUsers {osmConnection} {inspectedContributors} on:selectUser={(e) => {
|
||||
username.set(e.detail); load();showPreviouslyVisited.set(false)
|
||||
}} />
|
||||
</Page>
|
5
src/UI/InspectorGUI.ts
Normal file
5
src/UI/InspectorGUI.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import InspectorGUI from "./InspectorGUI.svelte"
|
||||
|
||||
new InspectorGUI({
|
||||
target: document.getElementById("main"),
|
||||
})
|
|
@ -97,7 +97,7 @@ export class DeleteFlowState {
|
|||
if (allByMyself.data === null && useTheInternet) {
|
||||
// We kickoff the download here as it hasn't yet been downloaded. Note that this is mapped onto 'all by myself' above
|
||||
const hist = this.objectDownloader
|
||||
.DownloadHistory(id)
|
||||
.downloadHistory(id)
|
||||
.map((versions) =>
|
||||
versions.map((version) =>
|
||||
Number(version.tags["_last_edit:contributor:uid"])
|
||||
|
|
|
@ -87,7 +87,7 @@ export interface SpecialVisualizationState {
|
|||
readonly geocodedImages: UIEventSource<Feature[]>
|
||||
readonly searchState: SearchState
|
||||
|
||||
getMatchingLayer(properties: Record<string, string>)
|
||||
getMatchingLayer(properties: Record<string, string>): LayerConfig | undefined
|
||||
|
||||
showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer
|
||||
reportError(message: string | Error | XMLHttpRequest, extramessage?: string): Promise<void>
|
||||
|
|
Loading…
Reference in a new issue