forked from MapComplete/MapComplete
More search functionality
This commit is contained in:
parent
5d0de8520b
commit
1c46a65c84
25 changed files with 962 additions and 846 deletions
|
@ -90,11 +90,11 @@
|
||||||
"minzoom": 6,
|
"minzoom": 6,
|
||||||
"title": {
|
"title": {
|
||||||
"render": {
|
"render": {
|
||||||
"en": "Cycle highway",
|
"en": "cycle highway",
|
||||||
"de": "Radschnellweg",
|
"de": "Radschnellweg",
|
||||||
"ca": "Via ciclista",
|
"ca": "via ciclista",
|
||||||
"fr": "Aménagement cyclable",
|
"fr": "Aménagement cyclable",
|
||||||
"nl": "Fietssnelweg",
|
"nl": "fietssnelweg",
|
||||||
"es": "autovía ciclista",
|
"es": "autovía ciclista",
|
||||||
"nb_NO": "sykkelmotorvei",
|
"nb_NO": "sykkelmotorvei",
|
||||||
"da": "cykelmotorvej",
|
"da": "cykelmotorvej",
|
||||||
|
|
|
@ -257,6 +257,9 @@
|
||||||
{
|
{
|
||||||
"builtin": "id_presets.shop_types",
|
"builtin": "id_presets.shop_types",
|
||||||
"override": {
|
"override": {
|
||||||
|
"labels": [
|
||||||
|
"description"
|
||||||
|
],
|
||||||
"question": {
|
"question": {
|
||||||
"en": "What kind of shop is this?",
|
"en": "What kind of shop is this?",
|
||||||
"nl": "Wat voor soort winkel is dit?",
|
"nl": "Wat voor soort winkel is dit?",
|
||||||
|
|
|
@ -74,7 +74,6 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"cycle_highways"
|
"cycle_highways"
|
||||||
],
|
],
|
||||||
"overpassTimeout": 60,
|
"overpassTimeout": 60,
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -35,23 +35,6 @@
|
||||||
"1": {
|
"1": {
|
||||||
"title": "un mupi"
|
"title": "un mupi"
|
||||||
},
|
},
|
||||||
"10": {
|
|
||||||
"description": "Una pieza de tela impermeable con un mensaje impreso, anclada permanentemente en una pared",
|
|
||||||
"title": "una lona"
|
|
||||||
},
|
|
||||||
"11": {
|
|
||||||
"title": "un tótem"
|
|
||||||
},
|
|
||||||
"12": {
|
|
||||||
"description": "Se utiliza para carteles publicitarios, letreros de neón, logotipos y carteles en entradas institucionales",
|
|
||||||
"title": "un señal"
|
|
||||||
},
|
|
||||||
"13": {
|
|
||||||
"title": "una escultura"
|
|
||||||
},
|
|
||||||
"14": {
|
|
||||||
"title": "una pared pintada"
|
|
||||||
},
|
|
||||||
"2": {
|
"2": {
|
||||||
"title": "un mupi sobre la pared"
|
"title": "un mupi sobre la pared"
|
||||||
},
|
},
|
||||||
|
@ -77,6 +60,23 @@
|
||||||
},
|
},
|
||||||
"9": {
|
"9": {
|
||||||
"title": "una pantalla montada en una marquesina de tránsito"
|
"title": "una pantalla montada en una marquesina de tránsito"
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"description": "Una pieza de tela impermeable con un mensaje impreso, anclada permanentemente en una pared",
|
||||||
|
"title": "una lona"
|
||||||
|
},
|
||||||
|
"11": {
|
||||||
|
"title": "un tótem"
|
||||||
|
},
|
||||||
|
"12": {
|
||||||
|
"description": "Se utiliza para carteles publicitarios, letreros de neón, logotipos y carteles en entradas institucionales",
|
||||||
|
"title": "un señal"
|
||||||
|
},
|
||||||
|
"13": {
|
||||||
|
"title": "una escultura"
|
||||||
|
},
|
||||||
|
"14": {
|
||||||
|
"title": "una pared pintada"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
|
@ -171,9 +171,6 @@
|
||||||
"1": {
|
"1": {
|
||||||
"then": "Esto es un tablón de anuncios"
|
"then": "Esto es un tablón de anuncios"
|
||||||
},
|
},
|
||||||
"10": {
|
|
||||||
"then": "Esto es una pared pintada"
|
|
||||||
},
|
|
||||||
"2": {
|
"2": {
|
||||||
"then": "Esto es una columna"
|
"then": "Esto es una columna"
|
||||||
},
|
},
|
||||||
|
@ -197,6 +194,9 @@
|
||||||
},
|
},
|
||||||
"9": {
|
"9": {
|
||||||
"then": "Esto es un tótem"
|
"then": "Esto es un tótem"
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"then": "Esto es una pared pintada"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"question": "¿Qué tipo de elemento publicitario es?",
|
"question": "¿Qué tipo de elemento publicitario es?",
|
||||||
|
@ -211,9 +211,6 @@
|
||||||
"1": {
|
"1": {
|
||||||
"then": "Tablon de anuncios"
|
"then": "Tablon de anuncios"
|
||||||
},
|
},
|
||||||
"10": {
|
|
||||||
"then": "Pared Pintada"
|
|
||||||
},
|
|
||||||
"2": {
|
"2": {
|
||||||
"then": "Mupi"
|
"then": "Mupi"
|
||||||
},
|
},
|
||||||
|
@ -237,6 +234,9 @@
|
||||||
},
|
},
|
||||||
"9": {
|
"9": {
|
||||||
"then": "Tótem"
|
"then": "Tótem"
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"then": "Pared Pintada"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -375,15 +375,6 @@
|
||||||
"1": {
|
"1": {
|
||||||
"then": "Mural"
|
"then": "Mural"
|
||||||
},
|
},
|
||||||
"10": {
|
|
||||||
"then": "Azulejo (Baldosas decorativas Españolas y Portuguesas)"
|
|
||||||
},
|
|
||||||
"11": {
|
|
||||||
"then": "Cerámica"
|
|
||||||
},
|
|
||||||
"12": {
|
|
||||||
"then": "Tallado en madera"
|
|
||||||
},
|
|
||||||
"2": {
|
"2": {
|
||||||
"then": "Pintura"
|
"then": "Pintura"
|
||||||
},
|
},
|
||||||
|
@ -407,6 +398,15 @@
|
||||||
},
|
},
|
||||||
"9": {
|
"9": {
|
||||||
"then": "Relieve"
|
"then": "Relieve"
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"then": "Azulejo (Baldosas decorativas Españolas y Portuguesas)"
|
||||||
|
},
|
||||||
|
"11": {
|
||||||
|
"then": "Cerámica"
|
||||||
|
},
|
||||||
|
"12": {
|
||||||
|
"then": "Tallado en madera"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"question": "¿Qué tipo de obra es esta pieza?",
|
"question": "¿Qué tipo de obra es esta pieza?",
|
||||||
|
@ -1788,12 +1788,6 @@
|
||||||
"1": {
|
"1": {
|
||||||
"then": "Este carril bici está pavimentado"
|
"then": "Este carril bici está pavimentado"
|
||||||
},
|
},
|
||||||
"10": {
|
|
||||||
"then": "Este carril bici está hecho de gravilla"
|
|
||||||
},
|
|
||||||
"12": {
|
|
||||||
"then": "Este carril bici está hecho de tierra natural"
|
|
||||||
},
|
|
||||||
"2": {
|
"2": {
|
||||||
"then": "Este carril bici está hecho de asfalto"
|
"then": "Este carril bici está hecho de asfalto"
|
||||||
},
|
},
|
||||||
|
@ -1808,6 +1802,12 @@
|
||||||
},
|
},
|
||||||
"9": {
|
"9": {
|
||||||
"then": "Este carril bici está hecho de grava"
|
"then": "Este carril bici está hecho de grava"
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"then": "Este carril bici está hecho de gravilla"
|
||||||
|
},
|
||||||
|
"12": {
|
||||||
|
"then": "Este carril bici está hecho de tierra natural"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"question": "¿De qué superficie está hecho este carril bici?",
|
"question": "¿De qué superficie está hecho este carril bici?",
|
||||||
|
@ -1853,9 +1853,6 @@
|
||||||
"1": {
|
"1": {
|
||||||
"then": "Este carril bici está pavimentado"
|
"then": "Este carril bici está pavimentado"
|
||||||
},
|
},
|
||||||
"10": {
|
|
||||||
"then": "Este carril bici está hecho de gravilla"
|
|
||||||
},
|
|
||||||
"2": {
|
"2": {
|
||||||
"then": "Este carril bici está hecho de asfalto"
|
"then": "Este carril bici está hecho de asfalto"
|
||||||
},
|
},
|
||||||
|
@ -1867,6 +1864,9 @@
|
||||||
},
|
},
|
||||||
"9": {
|
"9": {
|
||||||
"then": "Este carril bici está hecho de grava"
|
"then": "Este carril bici está hecho de grava"
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"then": "Este carril bici está hecho de gravilla"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"question": "¿De qué esta hecha la superficie de esta calle?",
|
"question": "¿De qué esta hecha la superficie de esta calle?",
|
||||||
|
@ -2508,18 +2508,6 @@
|
||||||
"0": {
|
"0": {
|
||||||
"then": "Esto es una pizzería"
|
"then": "Esto es una pizzería"
|
||||||
},
|
},
|
||||||
"10": {
|
|
||||||
"then": "Aquí se sirven platos Chinos"
|
|
||||||
},
|
|
||||||
"11": {
|
|
||||||
"then": "Aquí se sirven platos Griegos"
|
|
||||||
},
|
|
||||||
"12": {
|
|
||||||
"then": "Aquí se sirven platos Indios"
|
|
||||||
},
|
|
||||||
"13": {
|
|
||||||
"then": "Aquí se sirven platos Turcos"
|
|
||||||
},
|
|
||||||
"2": {
|
"2": {
|
||||||
"then": "Principalmente sirve pasta"
|
"then": "Principalmente sirve pasta"
|
||||||
},
|
},
|
||||||
|
@ -2540,6 +2528,18 @@
|
||||||
},
|
},
|
||||||
"9": {
|
"9": {
|
||||||
"then": "Aquí se sirven platos Franceses"
|
"then": "Aquí se sirven platos Franceses"
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"then": "Aquí se sirven platos Chinos"
|
||||||
|
},
|
||||||
|
"11": {
|
||||||
|
"then": "Aquí se sirven platos Griegos"
|
||||||
|
},
|
||||||
|
"12": {
|
||||||
|
"then": "Aquí se sirven platos Indios"
|
||||||
|
},
|
||||||
|
"13": {
|
||||||
|
"then": "Aquí se sirven platos Turcos"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"question": "¿Qué tipo de comida sirven aquí?",
|
"question": "¿Qué tipo de comida sirven aquí?",
|
||||||
|
@ -2850,11 +2850,11 @@
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
"memorial-type": {
|
"memorial-type": {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
"10": {
|
|
||||||
"then": "Es una cruz"
|
|
||||||
},
|
|
||||||
"9": {
|
"9": {
|
||||||
"then": "Es un obelisco"
|
"then": "Es un obelisco"
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"then": "Es una cruz"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2945,19 +2945,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"10": {
|
|
||||||
"options": {
|
|
||||||
"0": {
|
|
||||||
"question": "Todas las notas"
|
|
||||||
},
|
|
||||||
"1": {
|
|
||||||
"question": "Ocultar las notas de importación"
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"question": "Solo mostrar las notas de importación"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"2": {
|
"2": {
|
||||||
"options": {
|
"options": {
|
||||||
"0": {
|
"0": {
|
||||||
|
@ -3013,6 +3000,19 @@
|
||||||
"question": "Solo mostrar las notas abiertas"
|
"question": "Solo mostrar las notas abiertas"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"options": {
|
||||||
|
"0": {
|
||||||
|
"question": "Todas las notas"
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"question": "Ocultar las notas de importación"
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"question": "Solo mostrar las notas de importación"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Notas de OpenStreetMap",
|
"name": "Notas de OpenStreetMap",
|
||||||
|
@ -3615,6 +3615,24 @@
|
||||||
"1": {
|
"1": {
|
||||||
"question": "Reciclaje de baterías"
|
"question": "Reciclaje de baterías"
|
||||||
},
|
},
|
||||||
|
"3": {
|
||||||
|
"question": "Reciclaje de latas"
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"question": "Reciclaje de ropa"
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"question": "Reciclaje de aceite de cocina"
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"question": "Reciclaje de aceite de motor"
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"question": "Reciclaje de residuos orgánicos"
|
||||||
|
},
|
||||||
|
"9": {
|
||||||
|
"question": "Reciclaje de botellas de cristal"
|
||||||
|
},
|
||||||
"10": {
|
"10": {
|
||||||
"question": "Reciclaje de cristal"
|
"question": "Reciclaje de cristal"
|
||||||
},
|
},
|
||||||
|
@ -3641,24 +3659,6 @@
|
||||||
},
|
},
|
||||||
"18": {
|
"18": {
|
||||||
"question": "Reciclaje de pequeños electrodomésticos"
|
"question": "Reciclaje de pequeños electrodomésticos"
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"question": "Reciclaje de latas"
|
|
||||||
},
|
|
||||||
"4": {
|
|
||||||
"question": "Reciclaje de ropa"
|
|
||||||
},
|
|
||||||
"5": {
|
|
||||||
"question": "Reciclaje de aceite de cocina"
|
|
||||||
},
|
|
||||||
"6": {
|
|
||||||
"question": "Reciclaje de aceite de motor"
|
|
||||||
},
|
|
||||||
"8": {
|
|
||||||
"question": "Reciclaje de residuos orgánicos"
|
|
||||||
},
|
|
||||||
"9": {
|
|
||||||
"question": "Reciclaje de botellas de cristal"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3701,6 +3701,27 @@
|
||||||
"0": {
|
"0": {
|
||||||
"then": "Aquí se pueden reciclar baterías"
|
"then": "Aquí se pueden reciclar baterías"
|
||||||
},
|
},
|
||||||
|
"2": {
|
||||||
|
"then": "Aquí se pueden reciclar latas"
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"then": "Aquí se puede reciclar ropa"
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"then": "Aquí se puede reciclar aceite de cocina"
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"then": "Aquí se puede reciclar aceite de motor"
|
||||||
|
},
|
||||||
|
"7": {
|
||||||
|
"then": "Los residuos orgánicos pueden reciclarse aquí"
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"then": "Aquí se pueden reciclar residuos orgánicos"
|
||||||
|
},
|
||||||
|
"9": {
|
||||||
|
"then": "Aquí se pueden reciclar botellas de cristal"
|
||||||
|
},
|
||||||
"10": {
|
"10": {
|
||||||
"then": "Aquí se puede reciclar cristal"
|
"then": "Aquí se puede reciclar cristal"
|
||||||
},
|
},
|
||||||
|
@ -3725,29 +3746,8 @@
|
||||||
"19": {
|
"19": {
|
||||||
"then": "Aquí se puede reciclar chatarra"
|
"then": "Aquí se puede reciclar chatarra"
|
||||||
},
|
},
|
||||||
"2": {
|
|
||||||
"then": "Aquí se pueden reciclar latas"
|
|
||||||
},
|
|
||||||
"20": {
|
"20": {
|
||||||
"then": "El calzado se puede reciclar aquí"
|
"then": "El calzado se puede reciclar aquí"
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"then": "Aquí se puede reciclar ropa"
|
|
||||||
},
|
|
||||||
"4": {
|
|
||||||
"then": "Aquí se puede reciclar aceite de cocina"
|
|
||||||
},
|
|
||||||
"5": {
|
|
||||||
"then": "Aquí se puede reciclar aceite de motor"
|
|
||||||
},
|
|
||||||
"7": {
|
|
||||||
"then": "Los residuos orgánicos pueden reciclarse aquí"
|
|
||||||
},
|
|
||||||
"8": {
|
|
||||||
"then": "Aquí se pueden reciclar residuos orgánicos"
|
|
||||||
},
|
|
||||||
"9": {
|
|
||||||
"then": "Aquí se pueden reciclar botellas de cristal"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"question": "¿Qué se puede reciclar aquí?"
|
"question": "¿Qué se puede reciclar aquí?"
|
||||||
|
@ -4168,12 +4168,6 @@
|
||||||
"1": {
|
"1": {
|
||||||
"then": "Esta lámpara utiliza LEDs"
|
"then": "Esta lámpara utiliza LEDs"
|
||||||
},
|
},
|
||||||
"10": {
|
|
||||||
"then": "Esta lámpara utiliza lámparas de sodio de alta presión (naranja con blanco)"
|
|
||||||
},
|
|
||||||
"11": {
|
|
||||||
"then": "Esta lampara se ilumina con gas"
|
|
||||||
},
|
|
||||||
"2": {
|
"2": {
|
||||||
"then": "Esta lámpara utiliza iluminación incandescente"
|
"then": "Esta lámpara utiliza iluminación incandescente"
|
||||||
},
|
},
|
||||||
|
@ -4194,6 +4188,12 @@
|
||||||
},
|
},
|
||||||
"9": {
|
"9": {
|
||||||
"then": "Esta lámpara utiliza lámparas de sodio de baja presión (naranja monocromo)"
|
"then": "Esta lámpara utiliza lámparas de sodio de baja presión (naranja monocromo)"
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"then": "Esta lámpara utiliza lámparas de sodio de alta presión (naranja con blanco)"
|
||||||
|
},
|
||||||
|
"11": {
|
||||||
|
"then": "Esta lampara se ilumina con gas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"question": "¿Qué tipo de iluminación utiliza esta lámpara?"
|
"question": "¿Qué tipo de iluminación utiliza esta lámpara?"
|
||||||
|
@ -4774,6 +4774,9 @@
|
||||||
},
|
},
|
||||||
"vending": {
|
"vending": {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
|
"8": {
|
||||||
|
"then": "Aquí se venden cámaras de aire para bicicletas"
|
||||||
|
},
|
||||||
"22": {
|
"22": {
|
||||||
"then": "Las luces para bicicletas se venden aquí"
|
"then": "Las luces para bicicletas se venden aquí"
|
||||||
},
|
},
|
||||||
|
@ -4788,9 +4791,6 @@
|
||||||
},
|
},
|
||||||
"26": {
|
"26": {
|
||||||
"then": "Aquí se venden candados para bicicletas"
|
"then": "Aquí se venden candados para bicicletas"
|
||||||
},
|
|
||||||
"8": {
|
|
||||||
"then": "Aquí se venden cámaras de aire para bicicletas"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -711,14 +711,6 @@ video {
|
||||||
top: 2.5rem;
|
top: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-2 {
|
|
||||||
top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-2 {
|
|
||||||
right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-1\/4 {
|
.left-1\/4 {
|
||||||
left: 25%;
|
left: 25%;
|
||||||
}
|
}
|
||||||
|
@ -787,6 +779,10 @@ video {
|
||||||
top: 0.25rem;
|
top: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.top-2 {
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.top-\[calc\(100\%\+1rem\)\] {
|
.top-\[calc\(100\%\+1rem\)\] {
|
||||||
top: calc(100% + 1rem);
|
top: calc(100% + 1rem);
|
||||||
}
|
}
|
||||||
|
@ -1077,10 +1073,6 @@ video {
|
||||||
margin-left: -1.5rem;
|
margin-left: -1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mt-12 {
|
|
||||||
margin-top: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-3 {
|
.mb-3 {
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
@ -1448,6 +1440,10 @@ video {
|
||||||
max-height: 3rem;
|
max-height: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.max-h-96 {
|
||||||
|
max-height: 24rem;
|
||||||
|
}
|
||||||
|
|
||||||
.max-h-24 {
|
.max-h-24 {
|
||||||
max-height: 6rem;
|
max-height: 6rem;
|
||||||
}
|
}
|
||||||
|
@ -5298,11 +5294,6 @@ svg.apply-fill path {
|
||||||
border-color: rgb(209 213 219 / var(--tw-border-opacity));
|
border-color: rgb(209 213 219 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover\:bg-stone-200:hover {
|
|
||||||
--tw-bg-opacity: 1;
|
|
||||||
background-color: rgb(231 229 228 / var(--tw-bg-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.hover\:bg-indigo-200:hover {
|
.hover\:bg-indigo-200:hover {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(199 210 254 / var(--tw-bg-opacity));
|
background-color: rgb(199 210 254 / var(--tw-bg-opacity));
|
||||||
|
@ -7243,6 +7234,10 @@ svg.apply-fill path {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sm\:w-96 {
|
||||||
|
width: 24rem;
|
||||||
|
}
|
||||||
|
|
||||||
.sm\:w-11 {
|
.sm\:w-11 {
|
||||||
width: 2.75rem;
|
width: 2.75rem;
|
||||||
}
|
}
|
||||||
|
@ -7259,10 +7254,6 @@ svg.apply-fill path {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm\:w-96 {
|
|
||||||
width: 24rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm\:grid-cols-2 {
|
.sm\:grid-cols-2 {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
|
@ -909,6 +909,7 @@ export class GeoOperations {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GeoOperations.distanceToHuman(52.3) // => "50m"
|
* GeoOperations.distanceToHuman(52.3) // => "50m"
|
||||||
|
* GeoOperations.distanceToHuman(999) // => "1.0km"
|
||||||
* GeoOperations.distanceToHuman(2800) // => "2.8km"
|
* GeoOperations.distanceToHuman(2800) // => "2.8km"
|
||||||
* GeoOperations.distanceToHuman(12800) // => "13km"
|
* GeoOperations.distanceToHuman(12800) // => "13km"
|
||||||
* GeoOperations.distanceToHuman(128000) // => "130km"
|
* GeoOperations.distanceToHuman(128000) // => "130km"
|
||||||
|
@ -920,7 +921,7 @@ export class GeoOperations {
|
||||||
if (meters === undefined) {
|
if (meters === undefined) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
meters = Math.round(meters)
|
meters = Utils.roundHuman( Math.round(meters))
|
||||||
if (meters < 1000) {
|
if (meters < 1000) {
|
||||||
return Utils.roundHuman(meters) + "m"
|
return Utils.roundHuman(meters) + "m"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
|
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
|
import { Store, Stores } from "../UIEventSource"
|
||||||
|
|
||||||
export default class CombinedSearcher implements GeocodingProvider {
|
export default class CombinedSearcher implements GeocodingProvider {
|
||||||
private _providers: ReadonlyArray<GeocodingProvider>
|
private _providers: ReadonlyArray<GeocodingProvider>
|
||||||
|
@ -16,13 +17,13 @@ export default class CombinedSearcher implements GeocodingProvider {
|
||||||
* @param geocoded
|
* @param geocoded
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private merge(geocoded: GeoCodeResult[][]): GeoCodeResult[]{
|
private merge(geocoded: GeoCodeResult[][]): GeoCodeResult[] {
|
||||||
const results : GeoCodeResult[] = []
|
const results: GeoCodeResult[] = []
|
||||||
const seenIds = new Set<string>()
|
const seenIds = new Set<string>()
|
||||||
for (const geocodedElement of geocoded) {
|
for (const geocodedElement of geocoded) {
|
||||||
for (const entry of geocodedElement) {
|
for (const entry of geocodedElement) {
|
||||||
const id = entry.osm_type+ entry.osm_id
|
const id = entry.osm_type + entry.osm_id
|
||||||
if(seenIds.has(id)){
|
if (seenIds.has(id)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seenIds.add(id)
|
seenIds.add(id)
|
||||||
|
@ -33,12 +34,12 @@ export default class CombinedSearcher implements GeocodingProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||||
const results = await Promise.all(this._providers.map(pr => pr.search(query, options)))
|
const results = (await Promise.all(this._providers.map(pr => pr.search(query, options))))
|
||||||
return this.merge(results)
|
return results.flatMap(x => x)
|
||||||
}
|
}
|
||||||
|
|
||||||
async suggest(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
|
||||||
const results = await Promise.all(this._providersWithSuggest.map(pr => pr.suggest(query, options)))
|
return Stores.concat(this._providersWithSuggest.map(pr => pr.suggest(query, options)))
|
||||||
return this.merge(results)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
|
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
|
import { ImmutableStore, Store } from "../UIEventSource"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple search-class which interprets possible locations
|
* A simple search-class which interprets possible locations
|
||||||
|
@ -17,28 +18,25 @@ export default class CoordinateSearch implements GeocodingProvider {
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param query
|
|
||||||
* @param options
|
|
||||||
*
|
*
|
||||||
* const ls = new CoordinateSearch()
|
* const ls = new CoordinateSearch()
|
||||||
* const results = await ls.search("https://www.openstreetmap.org/search?query=Brugge#map=11/51.2611/3.2217")
|
* const results = ls.directSearch("https://www.openstreetmap.org/search?query=Brugge#map=11/51.2611/3.2217")
|
||||||
* results.length // => 1
|
* results.length // => 1
|
||||||
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate","source": "coordinateSearch"}
|
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate","source": "coordinateSearch"}
|
||||||
*
|
*
|
||||||
* const ls = new CoordinateSearch()
|
* const ls = new CoordinateSearch()
|
||||||
* const results = await ls.search("https://www.openstreetmap.org/#map=11/51.2611/3.2217")
|
* const results = ls.directSearch("https://www.openstreetmap.org/#map=11/51.2611/3.2217")
|
||||||
* results.length // => 1
|
* results.length // => 1
|
||||||
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate","source": "coordinateSearch"}
|
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate","source": "coordinateSearch"}
|
||||||
*
|
*
|
||||||
* const ls = new CoordinateSearch()
|
* const ls = new CoordinateSearch()
|
||||||
* const results = await ls.search("51.2611 3.2217")
|
* const results = ls.directSearch("51.2611 3.2217")
|
||||||
* results.length // => 2
|
* results.length // => 2
|
||||||
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate", "source": "coordinateSearch"}
|
* results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611", "category": "coordinate", "source": "coordinateSearch"}
|
||||||
* results[1] // => {lon: 51.2611, lat: 3.2217, display_name: "lon: 51.2611, lat: 3.2217", "category": "coordinate", "source": "coordinateSearch"}
|
* results[1] // => {lon: 51.2611, lat: 3.2217, display_name: "lon: 51.2611, lat: 3.2217", "category": "coordinate", "source": "coordinateSearch"}
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
private directSearch(query: string): GeoCodeResult[] {
|
||||||
|
|
||||||
const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => <GeoCodeResult>{
|
const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => <GeoCodeResult>{
|
||||||
lat: Number(m[1]),
|
lat: Number(m[1]),
|
||||||
|
@ -49,7 +47,6 @@ export default class CoordinateSearch implements GeocodingProvider {
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r)))
|
const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r)))
|
||||||
.map(m => <GeoCodeResult>{
|
.map(m => <GeoCodeResult>{
|
||||||
lat: Number(m[2]),
|
lat: Number(m[2]),
|
||||||
|
@ -58,12 +55,15 @@ export default class CoordinateSearch implements GeocodingProvider {
|
||||||
source: "coordinateSearch",
|
source: "coordinateSearch",
|
||||||
category: "coordinate"
|
category: "coordinate"
|
||||||
})
|
})
|
||||||
|
|
||||||
return matches.concat(matchesLonLat)
|
return matches.concat(matchesLonLat)
|
||||||
}
|
}
|
||||||
|
|
||||||
suggest(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
suggest(query: string): Store<GeoCodeResult[]> {
|
||||||
return this.search(query, options)
|
return new ImmutableStore(this.directSearch(query))
|
||||||
|
}
|
||||||
|
|
||||||
|
async search (query: string): Promise<GeoCodeResult[]> {
|
||||||
|
return this.directSearch(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { BBox } from "../BBox"
|
import { BBox } from "../BBox"
|
||||||
import { Feature, Geometry } from "geojson"
|
import { Feature, Geometry } from "geojson"
|
||||||
import { DefaultPinIcon } from "../../Models/Constants"
|
import { DefaultPinIcon } from "../../Models/Constants"
|
||||||
|
import { Store } from "../UIEventSource"
|
||||||
|
|
||||||
export type GeocodingCategory = "coordinate" | "city" | "house" | "street" | "locality" | "country" | "train_station" | "county" | "airport"
|
export type GeocodingCategory = "coordinate" | "city" | "house" | "street" | "locality" | "country" | "train_station" | "county" | "airport"
|
||||||
|
|
||||||
|
@ -42,7 +43,7 @@ export default interface GeocodingProvider {
|
||||||
* @param query
|
* @param query
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
suggest?(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]>
|
suggest?(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReverseGeocodingResult = Feature<Geometry,{
|
export type ReverseGeocodingResult = Feature<Geometry,{
|
||||||
|
|
|
@ -3,7 +3,19 @@ import ThemeViewState from "../../Models/ThemeViewState"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import { GeoOperations } from "../GeoOperations"
|
import { GeoOperations } from "../GeoOperations"
|
||||||
|
import { ImmutableStore, Store, Stores } from "../UIEventSource"
|
||||||
|
|
||||||
|
type IntermediateResult = {
|
||||||
|
feature: Feature,
|
||||||
|
/**
|
||||||
|
* Lon, lat
|
||||||
|
*/
|
||||||
|
center: [number, number],
|
||||||
|
levehnsteinD: number,
|
||||||
|
physicalDistance: number,
|
||||||
|
searchTerms: string[],
|
||||||
|
description: string
|
||||||
|
}
|
||||||
export default class LocalElementSearch implements GeocodingProvider {
|
export default class LocalElementSearch implements GeocodingProvider {
|
||||||
private readonly _state: ThemeViewState
|
private readonly _state: ThemeViewState
|
||||||
private readonly _limit: number
|
private readonly _limit: number
|
||||||
|
@ -15,30 +27,13 @@ export default class LocalElementSearch implements GeocodingProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||||
return this.searchEntries(query, options, false)
|
return this.searchEntries(query, options, false).data
|
||||||
}
|
}
|
||||||
|
|
||||||
searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): GeoCodeResult[] {
|
private getPartialResult(query: string, matchStart: boolean, centerpoint: [number, number], features: Feature[]): IntermediateResult[] {
|
||||||
if (query.length < 3) {
|
const results: IntermediateResult [] = []
|
||||||
return []
|
|
||||||
}
|
for (const feature of features) {
|
||||||
const center: { lon: number; lat: number } = this._state.mapProperties.location.data
|
|
||||||
const centerPoint: [number, number] = [center.lon, center.lat]
|
|
||||||
let results: {
|
|
||||||
feature: Feature,
|
|
||||||
/**
|
|
||||||
* Lon, lat
|
|
||||||
*/
|
|
||||||
center: [number, number],
|
|
||||||
levehnsteinD: number,
|
|
||||||
physicalDistance: number,
|
|
||||||
searchTerms: string[],
|
|
||||||
description: string
|
|
||||||
}[] = []
|
|
||||||
const properties = this._state.perLayer
|
|
||||||
query = Utils.simplifyStringForSearch(query)
|
|
||||||
for (const [_, geoIndexedStore] of properties) {
|
|
||||||
for (const feature of geoIndexedStore.features.data) {
|
|
||||||
const props = feature.properties
|
const props = feature.properties
|
||||||
const searchTerms: string[] = Utils.NoNull([props.name, props.alt_name, props.local_name,
|
const searchTerms: string[] = Utils.NoNull([props.name, props.alt_name, props.local_name,
|
||||||
(props["addr:street"] && props["addr:number"]) ?
|
(props["addr:street"] && props["addr:number"]) ?
|
||||||
|
@ -56,24 +51,43 @@ export default class LocalElementSearch implements GeocodingProvider {
|
||||||
if (levehnsteinD <= 2) {
|
if (levehnsteinD <= 2) {
|
||||||
|
|
||||||
let description = ""
|
let description = ""
|
||||||
function ifDef(prefix: string, key: string){
|
if (feature.properties["addr:street"]) {
|
||||||
if(feature.properties[key]){
|
description += "" + feature.properties["addr:street"]
|
||||||
description += prefix+ feature.properties[key]
|
|
||||||
}
|
}
|
||||||
|
if (feature.properties["addr:housenumber"]) {
|
||||||
|
description += " " + feature.properties["addr:housenumber"]
|
||||||
}
|
}
|
||||||
ifDef("", "addr:street")
|
|
||||||
ifDef(" ", "addr:housenumber")
|
|
||||||
results.push({
|
results.push({
|
||||||
feature,
|
feature,
|
||||||
center,
|
center,
|
||||||
physicalDistance: GeoOperations.distanceBetween(centerPoint, center),
|
physicalDistance: GeoOperations.distanceBetween(centerpoint, center),
|
||||||
levehnsteinD,
|
levehnsteinD,
|
||||||
searchTerms,
|
searchTerms,
|
||||||
description: description !== "" ? description : undefined
|
description: description !== "" ? description : undefined
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): Store<GeoCodeResult[]> {
|
||||||
|
if (query.length < 3) {
|
||||||
|
return new ImmutableStore([])
|
||||||
|
}
|
||||||
|
const center: { lon: number; lat: number } = this._state.mapProperties.location.data
|
||||||
|
const centerPoint: [number, number] = [center.lon, center.lat]
|
||||||
|
const properties = this._state.perLayer
|
||||||
|
query = Utils.simplifyStringForSearch(query)
|
||||||
|
|
||||||
|
const partials: Store<IntermediateResult[]>[] = []
|
||||||
|
|
||||||
|
for (const [_, geoIndexedStore] of properties) {
|
||||||
|
const partialResult = geoIndexedStore.features.map(features => this.getPartialResult(query, matchStart, centerPoint, features))
|
||||||
|
partials.push(partialResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
const listed: Store<IntermediateResult[]> = Stores.concat(partials)
|
||||||
|
return listed.mapD(results => {
|
||||||
results.sort((a, b) => (a.physicalDistance + a.levehnsteinD * 25) - (b.physicalDistance + b.levehnsteinD * 25))
|
results.sort((a, b) => (a.physicalDistance + a.levehnsteinD * 25) - (b.physicalDistance + b.levehnsteinD * 25))
|
||||||
if (this._limit || options?.limit) {
|
if (this._limit || options?.limit) {
|
||||||
results = results.slice(0, Math.min(this._limit ?? options?.limit, options?.limit ?? this._limit))
|
results = results.slice(0, Math.min(this._limit ?? options?.limit, options?.limit ?? this._limit))
|
||||||
|
@ -92,9 +106,12 @@ export default class LocalElementSearch implements GeocodingProvider {
|
||||||
description: entry.description
|
description: entry.description
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async suggest(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
|
||||||
return this.searchEntries(query, options, true)
|
return this.searchEntries(query, options, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,10 @@ import { BBox } from "../BBox"
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants"
|
||||||
import { FeatureCollection } from "geojson"
|
import { FeatureCollection } from "geojson"
|
||||||
import Locale from "../../UI/i18n/Locale"
|
import Locale from "../../UI/i18n/Locale"
|
||||||
import GeocodingProvider, { GeoCodeResult, ReverseGeocodingProvider } from "./GeocodingProvider"
|
import GeocodingProvider, { GeoCodeResult } from "./GeocodingProvider"
|
||||||
|
import { Store, UIEventSource } from "../UIEventSource"
|
||||||
|
|
||||||
export class NominatimGeocoding implements GeocodingProvider, ReverseGeocodingProvider {
|
export class NominatimGeocoding implements GeocodingProvider {
|
||||||
|
|
||||||
private readonly _host ;
|
private readonly _host ;
|
||||||
|
|
||||||
|
@ -13,14 +14,14 @@ export class NominatimGeocoding implements GeocodingProvider, ReverseGeocodingPr
|
||||||
this._host = host
|
this._host = host
|
||||||
}
|
}
|
||||||
|
|
||||||
public async search(query: string, options?: { bbox?: BBox; limit?: number }): Promise<GeoCodeResult[]> {
|
public search(query: string, options?: { bbox?: BBox; limit?: number }): Promise<GeoCodeResult[]> {
|
||||||
const b = options?.bbox ?? BBox.global
|
const b = options?.bbox ?? BBox.global
|
||||||
const url = `${
|
const url = `${
|
||||||
this._host
|
this._host
|
||||||
}search?format=json&limit=${options?.limit ?? 1}&viewbox=${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}&accept-language=${
|
}search?format=json&limit=${options?.limit ?? 1}&viewbox=${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}&accept-language=${
|
||||||
Locale.language.data
|
Locale.language.data
|
||||||
}&q=${query}`
|
}&q=${query}`
|
||||||
return await Utils.downloadJson(url)
|
return Utils.downloadJson(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants"
|
||||||
import GeocodingProvider, {
|
import GeocodingProvider, {
|
||||||
GeoCodeResult, GeocodingCategory,
|
GeoCodeResult,
|
||||||
|
GeocodingCategory,
|
||||||
GeocodingOptions,
|
GeocodingOptions,
|
||||||
ReverseGeocodingProvider,
|
ReverseGeocodingProvider,
|
||||||
ReverseGeocodingResult
|
ReverseGeocodingResult
|
||||||
|
@ -9,6 +10,7 @@ import { Utils } from "../../Utils"
|
||||||
import { Feature, FeatureCollection } from "geojson"
|
import { Feature, FeatureCollection } from "geojson"
|
||||||
import Locale from "../../UI/i18n/Locale"
|
import Locale from "../../UI/i18n/Locale"
|
||||||
import { GeoOperations } from "../GeoOperations"
|
import { GeoOperations } from "../GeoOperations"
|
||||||
|
import { Store, Stores } from "../UIEventSource"
|
||||||
|
|
||||||
export default class PhotonSearch implements GeocodingProvider, ReverseGeocodingProvider {
|
export default class PhotonSearch implements GeocodingProvider, ReverseGeocodingProvider {
|
||||||
private _endpoint: string
|
private _endpoint: string
|
||||||
|
@ -52,8 +54,8 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
|
||||||
return this.suggest(query, options)
|
return Stores.FromPromise(this.search(query, options))
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildDescription(entry: Feature) {
|
private buildDescription(entry: Feature) {
|
||||||
|
@ -71,7 +73,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
||||||
case "house": {
|
case "house": {
|
||||||
|
|
||||||
const addr = ifdef("", p.street) + ifdef(" ", p.housenumber)
|
const addr = ifdef("", p.street) + ifdef(" ", p.housenumber)
|
||||||
if(!addr){
|
if (!addr) {
|
||||||
return p.city
|
return p.city
|
||||||
}
|
}
|
||||||
return addr + ifdef(", ", p.city)
|
return addr + ifdef(", ", p.city)
|
||||||
|
@ -81,7 +83,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
||||||
return p.city ?? p.country
|
return p.city ?? p.country
|
||||||
case "city":
|
case "city":
|
||||||
case "locality":
|
case "locality":
|
||||||
if(p.state){
|
if (p.state) {
|
||||||
return p.state + ifdef(", ", p.country)
|
return p.state + ifdef(", ", p.country)
|
||||||
}
|
}
|
||||||
return p.country
|
return p.country
|
||||||
|
@ -91,18 +93,18 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCategory(entry: Feature){
|
private getCategory(entry: Feature) {
|
||||||
const p = entry.properties
|
const p = entry.properties
|
||||||
if(p.osm_value === "train_station" || p.osm_key === "railway"){
|
if (p.osm_value === "train_station" || p.osm_key === "railway") {
|
||||||
return "train_station"
|
return "train_station"
|
||||||
}
|
}
|
||||||
if(p.osm_value === "aerodrome" || p.osm_key === "aeroway"){
|
if (p.osm_value === "aerodrome" || p.osm_key === "aeroway") {
|
||||||
return "airport"
|
return "airport"
|
||||||
}
|
}
|
||||||
return p.type
|
return p.type
|
||||||
}
|
}
|
||||||
|
|
||||||
async suggest?(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||||
if (query.length < 3) {
|
if (query.length < 3) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,12 +18,15 @@ export class RecentSearch {
|
||||||
|
|
||||||
|
|
||||||
state.selectedElement.addCallbackAndRunD(selected => {
|
state.selectedElement.addCallbackAndRunD(selected => {
|
||||||
|
|
||||||
const [osm_type, osm_id] = selected.properties.id.split("/")
|
const [osm_type, osm_id] = selected.properties.id.split("/")
|
||||||
|
if(!osm_id){
|
||||||
|
return
|
||||||
|
}
|
||||||
const [lon, lat] = GeoOperations.centerpointCoordinates(selected)
|
const [lon, lat] = GeoOperations.centerpointCoordinates(selected)
|
||||||
const entry = <GeoCodeResult>{
|
const entry = <GeoCodeResult>{
|
||||||
feature: selected,
|
feature: selected,
|
||||||
osm_id, osm_type,
|
osm_id, osm_type,
|
||||||
description: "Viewed recently",
|
|
||||||
lon, lat
|
lon, lat
|
||||||
}
|
}
|
||||||
this.addSelected(entry)
|
this.addSelected(entry)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||||
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
|
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils"
|
||||||
import MoreScreen from "../../UI/BigComponents/MoreScreen"
|
import MoreScreen from "../../UI/BigComponents/MoreScreen"
|
||||||
import { Store } from "../UIEventSource"
|
import { ImmutableStore, Store } from "../UIEventSource"
|
||||||
|
|
||||||
export default class ThemeSearch implements GeocodingProvider {
|
export default class ThemeSearch implements GeocodingProvider {
|
||||||
|
|
||||||
|
@ -17,11 +17,15 @@ export default class ThemeSearch implements GeocodingProvider {
|
||||||
this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection)
|
this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection)
|
||||||
}
|
}
|
||||||
|
|
||||||
search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||||
return this.suggest(query, options)
|
return this.searchDirect(query, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
async suggest?(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
|
||||||
|
return new ImmutableStore(this.searchDirect(query, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
private searchDirect(query: string, options?: GeocodingOptions): GeoCodeResult[] {
|
||||||
if(query.length < 1){
|
if(query.length < 1){
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -33,10 +37,10 @@ export default class ThemeSearch implements GeocodingProvider {
|
||||||
.filter(th => MoreScreen.MatchesLayout(th, query))
|
.filter(th => MoreScreen.MatchesLayout(th, query))
|
||||||
.slice(0, limit + 1)
|
.slice(0, limit + 1)
|
||||||
|
|
||||||
return withMatch.map(match => (<GeoCodeResult> {
|
return withMatch.map(match => <GeoCodeResult> {
|
||||||
payload: match,
|
payload: match,
|
||||||
osm_id: match.id
|
osm_id: match.id
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,40 +3,12 @@ import { Utils } from "../../Utils"
|
||||||
export class ThemeMetaTagging {
|
export class ThemeMetaTagging {
|
||||||
public static readonly themeName = "usersettings"
|
public static readonly themeName = "usersettings"
|
||||||
|
|
||||||
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) {
|
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
|
||||||
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
|
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
|
||||||
feat.properties._description
|
Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' )
|
||||||
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
|
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
|
||||||
?.at(1)
|
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
|
||||||
)
|
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
|
||||||
Utils.AddLazyProperty(
|
feat.properties['__current_backgroun'] = 'initial_value'
|
||||||
feat.properties,
|
|
||||||
"_d",
|
|
||||||
() => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? ""
|
|
||||||
)
|
|
||||||
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
|
|
||||||
((feat) => {
|
|
||||||
const e = document.createElement("div")
|
|
||||||
e.innerHTML = feat.properties._d
|
|
||||||
return Array.from(e.getElementsByTagName("a")).filter(
|
|
||||||
(a) => a.href.match(/mastodon|en.osm.town/) !== null
|
|
||||||
)[0]?.href
|
|
||||||
})(feat)
|
|
||||||
)
|
|
||||||
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
|
|
||||||
((feat) => {
|
|
||||||
const e = document.createElement("div")
|
|
||||||
e.innerHTML = feat.properties._d
|
|
||||||
return Array.from(e.getElementsByTagName("a")).filter(
|
|
||||||
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
|
|
||||||
)[0]?.href
|
|
||||||
})(feat)
|
|
||||||
)
|
|
||||||
Utils.AddLazyProperty(
|
|
||||||
feat.properties,
|
|
||||||
"_mastodon_candidate",
|
|
||||||
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
|
|
||||||
)
|
|
||||||
feat.properties["__current_backgroun"] = "initial_value"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -41,8 +41,26 @@ export class Stores {
|
||||||
return src
|
return src
|
||||||
}
|
}
|
||||||
|
|
||||||
public static flatten<X>(source: Store<Store<X>>, possibleSources?: Store<any>[]): Store<X> {
|
public static concat<T>(stores: Store<T[]>[]): Store<T[]> {
|
||||||
return UIEventSource.flatten(source, possibleSources)
|
const newStore = new UIEventSource<T[]>([])
|
||||||
|
function update(){
|
||||||
|
if(newStore._callbacks.isDestroyed){
|
||||||
|
return true // unregister
|
||||||
|
}
|
||||||
|
const results: T[] = []
|
||||||
|
for (const store of stores) {
|
||||||
|
if(store.data){
|
||||||
|
results.push(...store.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newStore.setData(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const store of stores) {
|
||||||
|
store.addCallback(() => update())
|
||||||
|
}
|
||||||
|
update()
|
||||||
|
return newStore
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -105,8 +123,6 @@ export abstract class Store<T> implements Readable<T> {
|
||||||
callbackDestroyFunction: (f: () => void) => void
|
callbackDestroyFunction: (f: () => void) => void
|
||||||
): Store<J>
|
): Store<J>
|
||||||
|
|
||||||
M
|
|
||||||
|
|
||||||
public mapD<J>(
|
public mapD<J>(
|
||||||
f: (t: Exclude<T, undefined | null>) => J,
|
f: (t: Exclude<T, undefined | null>) => J,
|
||||||
extraStoresToWatch?: Store<any>[],
|
extraStoresToWatch?: Store<any>[],
|
||||||
|
@ -120,7 +136,7 @@ export abstract class Store<T> implements Readable<T> {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return f(<Exclude<T, undefined | null>>t)
|
return f(<Exclude<T, undefined | null>>t)
|
||||||
}, extraStoresToWatch)
|
}, extraStoresToWatch, callbackDestroyFunction)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -231,6 +247,9 @@ export abstract class Store<T> implements Readable<T> {
|
||||||
if (mapped.data === newEventSource) {
|
if (mapped.data === newEventSource) {
|
||||||
sink.setData(resultData)
|
sink.setData(resultData)
|
||||||
}
|
}
|
||||||
|
if(sink._callbacks.isDestroyed){
|
||||||
|
return true // unregister
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -308,6 +327,8 @@ export abstract class Store<T> implements Readable<T> {
|
||||||
run(v)
|
run(v)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ImmutableStore<T> extends Store<T> {
|
export class ImmutableStore<T> extends Store<T> {
|
||||||
|
@ -361,6 +382,10 @@ export class ImmutableStore<T> extends Store<T> {
|
||||||
bind<X>(f: (t: T) => Store<X>): Store<X> {
|
bind<X>(f: (t: T) => Store<X>): Store<X> {
|
||||||
return f(this.data)
|
return f(this.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -369,7 +394,7 @@ export class ImmutableStore<T> extends Store<T> {
|
||||||
class ListenerTracker<T> {
|
class ListenerTracker<T> {
|
||||||
public pingCount = 0
|
public pingCount = 0
|
||||||
private readonly _callbacks: ((t: T) => boolean | void | any)[] = []
|
private readonly _callbacks: ((t: T) => boolean | void | any)[] = []
|
||||||
|
public isDestroyed = false
|
||||||
/**
|
/**
|
||||||
* Adds a callback which can be called; a function to unregister is returned
|
* Adds a callback which can be called; a function to unregister is returned
|
||||||
*/
|
*/
|
||||||
|
@ -429,6 +454,11 @@ class ListenerTracker<T> {
|
||||||
length() {
|
length() {
|
||||||
return this._callbacks.length
|
return this._callbacks.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public destroy(){
|
||||||
|
this.isDestroyed= true
|
||||||
|
this._callbacks.splice(0, this._callbacks.length)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -584,10 +614,14 @@ class MappedStore<TIn, T> extends Store<T> {
|
||||||
this._data = newData
|
this._data = newData
|
||||||
this._callbacks.ping(this._data)
|
this._callbacks.ping(this._data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.unregisterFromUpstream()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
||||||
private static readonly pass: () => {}
|
private static readonly pass: (() => void) = () => {};
|
||||||
public data: T
|
public data: T
|
||||||
_callbacks: ListenerTracker<T> = new ListenerTracker<T>()
|
_callbacks: ListenerTracker<T> = new ListenerTracker<T>()
|
||||||
|
|
||||||
|
@ -596,9 +630,13 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
||||||
this.data = data
|
this.data = data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public destroy(){
|
||||||
|
this._callbacks.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
public static flatten<X>(
|
public static flatten<X>(
|
||||||
source: Store<Store<X>>,
|
source: Store<Store<X>>,
|
||||||
possibleSources?: Store<any>[]
|
possibleSources?: Store<object>[]
|
||||||
): UIEventSource<X> {
|
): UIEventSource<X> {
|
||||||
const sink = new UIEventSource<X>(source.data?.data)
|
const sink = new UIEventSource<X>(source.data?.data)
|
||||||
|
|
||||||
|
@ -627,7 +665,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
||||||
*/
|
*/
|
||||||
public static FromPromise<T>(
|
public static FromPromise<T>(
|
||||||
promise: Promise<T>,
|
promise: Promise<T>,
|
||||||
onError: (e: any) => void = undefined
|
onError: (e) => void = undefined
|
||||||
): UIEventSource<T> {
|
): UIEventSource<T> {
|
||||||
const src = new UIEventSource<T>(undefined)
|
const src = new UIEventSource<T>(undefined)
|
||||||
promise?.then((d) => src.setData(d))
|
promise?.then((d) => src.setData(d))
|
||||||
|
@ -671,7 +709,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
||||||
public static asInt(source: UIEventSource<string>): UIEventSource<number> {
|
public static asInt(source: UIEventSource<string>): UIEventSource<number> {
|
||||||
return source.sync(
|
return source.sync(
|
||||||
(str) => {
|
(str) => {
|
||||||
let parsed = parseInt(str)
|
const parsed = parseInt(str)
|
||||||
return isNaN(parsed) ? undefined : parsed
|
return isNaN(parsed) ? undefined : parsed
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
|
@ -702,7 +740,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
||||||
public static asFloat(source: UIEventSource<string>): UIEventSource<number> {
|
public static asFloat(source: UIEventSource<string>): UIEventSource<number> {
|
||||||
return source.sync(
|
return source.sync(
|
||||||
(str) => {
|
(str) => {
|
||||||
let parsed = parseFloat(str)
|
const parsed = parseFloat(str)
|
||||||
return isNaN(parsed) ? undefined : parsed
|
return isNaN(parsed) ? undefined : parsed
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
|
|
|
@ -601,6 +601,9 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if(document.activeElement.tagName === "button" || document.activeElement.tagName === "input"){
|
||||||
|
return
|
||||||
|
}
|
||||||
this.selectClosestAtCenter(0)
|
this.selectClosestAtCenter(0)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
autofocus
|
autofocus
|
||||||
class="normal-background absolute top-0 right-0 flex h-screen w-full flex-col overflow-y-auto drop-shadow-2xl md:w-6/12 lg:w-5/12 xl:w-4/12"
|
class="normal-background absolute top-0 right-0 flex h-screen w-full flex-col overflow-y-auto drop-shadow-2xl md:w-6/12 lg:w-5/12 xl:w-4/12"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
style="max-width: 100vw; max-height: 100vh"
|
style="max-width: 100vw; max-height: 100vh; z-index: 11"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
id="modal-right"
|
id="modal-right"
|
||||||
>
|
>
|
||||||
|
|
|
@ -12,11 +12,15 @@
|
||||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||||
import { GeoLocationState } from "../../Logic/State/GeoLocationState"
|
import { GeoLocationState } from "../../Logic/State/GeoLocationState"
|
||||||
import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding"
|
import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding"
|
||||||
import type GeocodingProvider from "../../Logic/Geocoding/GeocodingProvider"
|
import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider"
|
||||||
import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider"
|
import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider"
|
||||||
|
import type GeocodingProvider from "../../Logic/Geocoding/GeocodingProvider"
|
||||||
|
|
||||||
import SearchResults from "./SearchResults.svelte"
|
import SearchResults from "./SearchResults.svelte"
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
|
import MoreScreen from "./MoreScreen"
|
||||||
|
import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||||
|
import { focusWithArrows } from "../../Utils/focusWithArrows"
|
||||||
|
|
||||||
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
|
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
|
||||||
export let bounds: UIEventSource<BBox>
|
export let bounds: UIEventSource<BBox>
|
||||||
|
@ -82,6 +86,17 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const poi = result[0]
|
const poi = result[0]
|
||||||
|
if (poi.payload !== undefined) {
|
||||||
|
// This is a theme
|
||||||
|
const theme = <MinimalLayoutInformation>poi.payload
|
||||||
|
const url = MoreScreen.createUrlFor(theme, false)
|
||||||
|
console.log("Found a theme, going to", url)
|
||||||
|
// @ts-ignore
|
||||||
|
window.location = url
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (poi.boundingbox) {
|
||||||
|
|
||||||
const [lat0, lat1, lon0, lon1] = poi.boundingbox
|
const [lat0, lat1, lon0, lon1] = poi.boundingbox
|
||||||
bounds.set(
|
bounds.set(
|
||||||
new BBox([
|
new BBox([
|
||||||
|
@ -89,6 +104,9 @@
|
||||||
[lon1, lat1]
|
[lon1, lat1]
|
||||||
]).pad(0.01)
|
]).pad(0.01)
|
||||||
)
|
)
|
||||||
|
} else if (poi.lon && poi.lat) {
|
||||||
|
state.mapProperties.flyTo(poi.lon, poi.lat, GeocodingUtils.categoryToZoomLevel[poi.category] ?? 16)
|
||||||
|
}
|
||||||
if (perLayer !== undefined) {
|
if (perLayer !== undefined) {
|
||||||
const id = poi.osm_type + "/" + poi.osm_id
|
const id = poi.osm_type + "/" + poi.osm_id
|
||||||
const layers = Array.from(perLayer?.values() ?? [])
|
const layers = Array.from(perLayer?.values() ?? [])
|
||||||
|
@ -107,6 +125,7 @@
|
||||||
}
|
}
|
||||||
dispatch("searchIsValid", false)
|
dispatch("searchIsValid", false)
|
||||||
dispatch("searchCompleted")
|
dispatch("searchCompleted")
|
||||||
|
isFocused.setData(false)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
feedback = Translations.t.general.search.error.txt
|
feedback = Translations.t.general.search.error.txt
|
||||||
|
@ -116,13 +135,36 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let suggestions: Store<{success: GeoCodeResult[]} | {error}> = searchContents.stabilized(250).bindD(search =>
|
let suggestions: Store<GeoCodeResult[]> = searchContents.stabilized(250).bindD(search => {
|
||||||
UIEventSource.FromPromiseWithErr(searcher.suggest(search))
|
if (search.length === 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return searcher.suggest(search, { bbox: bounds.data })
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let geosearch: HTMLDivElement
|
||||||
|
|
||||||
|
function checkFocus() {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
if (geosearch.contains(document.activeElement)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isFocused.setData(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("focus",() => {
|
||||||
|
checkFocus()
|
||||||
|
}, true /* use 'capturing' instead of bubbling, needed for focus-events*/)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="normal-background flex justify-between rounded-full pl-2 w-full">
|
<div bind:this={geosearch} use:focusWithArrows={"searchresult"}>
|
||||||
|
|
||||||
|
<div class="normal-background flex justify-between rounded-full pl-2 w-full">
|
||||||
<form class="flex w-full flex-wrap">
|
<form class="flex w-full flex-wrap">
|
||||||
{#if isRunning}
|
{#if isRunning}
|
||||||
<Loading>{Translations.t.general.search.searching}</Loading>
|
<Loading>{Translations.t.general.search.searching}</Loading>
|
||||||
|
@ -133,10 +175,14 @@
|
||||||
bind:this={inputElement}
|
bind:this={inputElement}
|
||||||
on:keypress={(keypr) => {
|
on:keypress={(keypr) => {
|
||||||
feedback = undefined
|
feedback = undefined
|
||||||
return keypr.key === "Enter" ? performSearch() : undefined
|
if(keypr.key === "Enter"){
|
||||||
|
performSearch()
|
||||||
|
keypr.preventDefault()
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
}}
|
}}
|
||||||
on:focus={() => {isFocused.setData(true)}}
|
on:focus={() => {isFocused.setData(true)}}
|
||||||
on:blur={() => {isFocused.setData(false)}}
|
on:blur={() => {checkFocus()}}
|
||||||
bind:value={$searchContents}
|
bind:value={$searchContents}
|
||||||
use:placeholder={Translations.t.general.search.search}
|
use:placeholder={Translations.t.general.search.search}
|
||||||
use:ariaLabel={Translations.t.general.search.search}
|
use:ariaLabel={Translations.t.general.search.search}
|
||||||
|
@ -150,11 +196,12 @@
|
||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
<SearchIcon aria-hidden="true" class="h-6 w-6 self-end" on:click={performSearch} />
|
<SearchIcon aria-hidden="true" class="h-6 w-6 self-end" on:click={performSearch} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative h-0" style="z-index: 10">
|
<div class="relative h-0" style="z-index: 10">
|
||||||
|
<div class="absolute right-0 w-full sm:w-96 h-fit max-h-96">
|
||||||
<div class="absolute right-0" style="width: 25rem; max-width: 98vw">
|
<SearchResults {isFocused} {state} results={$suggestions} searchTerm={searchContents}
|
||||||
<SearchResults {isFocused} {state} results={$suggestions} searchTerm={searchContents} on:select={() => {searchContents.set("")}}/>
|
on:select={() => {searchContents.set(""); isFocused.setData(false)}} />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,14 +16,17 @@
|
||||||
import Tr from "../Base/Tr.svelte"
|
import Tr from "../Base/Tr.svelte"
|
||||||
import { Translation } from "../i18n/Translation"
|
import { Translation } from "../i18n/Translation"
|
||||||
import MoreScreen from "./MoreScreen"
|
import MoreScreen from "./MoreScreen"
|
||||||
|
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||||
|
|
||||||
export let entry: GeoCodeResult
|
export let entry: GeoCodeResult
|
||||||
export let state: SpecialVisualizationState
|
export let state: SpecialVisualizationState
|
||||||
let layer: LayerConfig
|
let layer: LayerConfig
|
||||||
let tags: UIEventSource<Record<string, string>>
|
let tags: UIEventSource<Record<string, string>>
|
||||||
|
let descriptionTr: TagRenderingConfig = undefined
|
||||||
if (entry.feature?.properties?.id) {
|
if (entry.feature?.properties?.id) {
|
||||||
layer = state.layout.getMatchingLayer(entry.feature.properties)
|
layer = state.layout.getMatchingLayer(entry.feature.properties)
|
||||||
tags = state.featureProperties.getStore(entry.feature.properties.id)
|
tags = state.featureProperties.getStore(entry.feature.properties.id)
|
||||||
|
descriptionTr = layer.tagRenderings.find(tr => tr.labels.indexOf("description") >= 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
let dispatch = createEventDispatcher<{ select }>()
|
let dispatch = createEventDispatcher<{ select }>()
|
||||||
|
@ -35,7 +38,6 @@
|
||||||
let otherTheme: MinimalLayoutInformation | undefined = <MinimalLayoutInformation>entry.payload
|
let otherTheme: MinimalLayoutInformation | undefined = <MinimalLayoutInformation>entry.payload
|
||||||
|
|
||||||
function select() {
|
function select() {
|
||||||
console.log("Selected search entry", entry)
|
|
||||||
if (entry.boundingbox) {
|
if (entry.boundingbox) {
|
||||||
const [lat0, lat1, lon0, lon1] = entry.boundingbox
|
const [lat0, lat1, lon0, lon1] = entry.boundingbox
|
||||||
state.mapProperties.bounds.set(
|
state.mapProperties.bounds.set(
|
||||||
|
@ -56,7 +58,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if otherTheme}
|
{#if otherTheme}
|
||||||
<a href={ MoreScreen.createUrlFor(otherTheme, false)} class="flex items-center p-2 w-full gap-y-2 rounded-xl" >
|
<a href={ MoreScreen.createUrlFor(otherTheme, false)}
|
||||||
|
class="flex items-center p-2 w-full gap-y-2 rounded-xl searchresult">
|
||||||
|
|
||||||
<Icon icon={otherTheme.icon} clss="w-6 h-6 m-1" />
|
<Icon icon={otherTheme.icon} clss="w-6 h-6 m-1" />
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
|
@ -68,7 +71,7 @@
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{:else}
|
{:else}
|
||||||
<button class="unstyled w-full link-no-underline" on:click={() => select() }>
|
<button class="unstyled w-full link-no-underline searchresult" on:click={() => select() }>
|
||||||
<div class="p-2 flex items-center w-full gap-y-2">
|
<div class="p-2 flex items-center w-full gap-y-2">
|
||||||
{#if layer}
|
{#if layer}
|
||||||
<ToSvelte construct={() => layer.defaultIcon(entry.feature.properties).SetClass("w-6 h-6")} />
|
<ToSvelte construct={() => layer.defaultIcon(entry.feature.properties).SetClass("w-6 h-6")} />
|
||||||
|
@ -84,6 +87,7 @@
|
||||||
{entry.display_name ?? entry.osm_id}
|
{entry.display_name ?? entry.osm_id}
|
||||||
{/if}
|
{/if}
|
||||||
</b>
|
</b>
|
||||||
|
{#if $distance > 50}
|
||||||
<div class="flex gap-x-1 items-center">
|
<div class="flex gap-x-1 items-center">
|
||||||
{#if $bearing && !$inView}
|
{#if $bearing && !$inView}
|
||||||
<ArrowUp class="w-4 h-4 shrink-0" style={`transform: rotate(${$bearing - $mapRotation}deg)`} />
|
<ArrowUp class="w-4 h-4 shrink-0" style={`transform: rotate(${$bearing - $mapRotation}deg)`} />
|
||||||
|
@ -92,12 +96,23 @@
|
||||||
{GeoOperations.distanceToHuman($distance)}
|
{GeoOperations.distanceToHuman($distance)}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-x-2">
|
||||||
|
|
||||||
|
{#if descriptionTr}
|
||||||
|
<TagRenderingAnswer defaultSize="subtle" noIcons={true} config={descriptionTr} {tags} {state}
|
||||||
|
selectedElement={entry.feature} {layer} />
|
||||||
|
{/if}
|
||||||
|
{#if descriptionTr && entry.description}
|
||||||
|
–
|
||||||
|
{/if}
|
||||||
{#if entry.description}
|
{#if entry.description}
|
||||||
<div class="subtle flex justify-between w-full">
|
<div class="subtle flex justify-between w-full">
|
||||||
{entry.description}
|
{entry.description}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,15 +2,15 @@
|
||||||
import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider"
|
import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider"
|
||||||
import SearchResult from "./SearchResult.svelte"
|
import SearchResult from "./SearchResult.svelte"
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
import { XMarkIcon } from "@babeard/svelte-heroicons/solid"
|
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import Loading from "../Base/Loading.svelte"
|
import Loading from "../Base/Loading.svelte"
|
||||||
import Tr from "../Base/Tr.svelte"
|
import Tr from "../Base/Tr.svelte"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
import MoreScreen from "./MoreScreen"
|
import MoreScreen from "./MoreScreen"
|
||||||
|
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
|
||||||
|
|
||||||
export let state: SpecialVisualizationState
|
export let state: SpecialVisualizationState
|
||||||
export let results: { success: GeoCodeResult[] } | { error }
|
export let results: GeoCodeResult[]
|
||||||
export let searchTerm: Store<string>
|
export let searchTerm: Store<string>
|
||||||
export let isFocused: UIEventSource<boolean>
|
export let isFocused: UIEventSource<boolean>
|
||||||
|
|
||||||
|
@ -19,36 +19,26 @@
|
||||||
let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview
|
let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full collapsable" style="height: 50rem;" class:collapsed={!$isFocused}>
|
<div class="relative w-full h-full collapsable " class:collapsed={!$isFocused}>
|
||||||
{#if results?.["error"] !== undefined}
|
<button class="absolute right-0 top-0 border-none p-0" on:click={() => isFocused.setData(false)} tabindex="-1">
|
||||||
<div class="searchbox normal-background items-center">
|
<XCircleIcon class="w-6 h-6" />
|
||||||
An error occured
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
{:else if $searchTerm.length > 0 && results === undefined}
|
<div class="searchbox normal-background">
|
||||||
<div class="searchbox normal-background items-center">
|
|
||||||
|
{#if $searchTerm.length > 0 && results === undefined}
|
||||||
|
<div class="flex justify-center m-4 my-8">
|
||||||
<Loading />
|
<Loading />
|
||||||
</div>
|
</div>
|
||||||
{:else if results?.["success"]?.length > 0}
|
{:else if results?.length > 0}
|
||||||
<div class="relative w-full h-full">
|
<div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto p-2" tabindex="-1">
|
||||||
<div class="absolute top-0 right-0 searchbox normal-background"
|
|
||||||
style="width: 25rem">
|
|
||||||
<div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto">
|
|
||||||
|
|
||||||
{#each results["success"] as entry (entry)}
|
{#each results as entry (entry)}
|
||||||
<SearchResult on:select {entry} {state} />
|
<SearchResult on:select {entry} {state} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="absolute top-2 right-2 cursor-pointer" on:click={() => isFocused.setData(false)}>
|
|
||||||
<XMarkIcon class="w-4 h-4 hover:bg-stone-200 rounded-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else if $searchTerm.length > 0 || $recentlySeen?.length > 0 || $recentThemes?.length > 0}
|
{:else if $searchTerm.length > 0 || $recentlySeen?.length > 0 || $recentThemes?.length > 0}
|
||||||
|
<div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto p-2 flex flex-col gap-y-8" tabindex="-1">
|
||||||
<div class="searchbox normal-background overflow-y-auto h-full">
|
|
||||||
{#if $searchTerm.length > 0}
|
{#if $searchTerm.length > 0}
|
||||||
<b class="flex justify-center p-4">
|
<b class="flex justify-center p-4">
|
||||||
<Tr t={Translations.t.general.search.nothingFor.Subs({term: $searchTerm})} />
|
<Tr t={Translations.t.general.search.nothingFor.Subs({term: $searchTerm})} />
|
||||||
|
@ -56,26 +46,32 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $recentlySeen?.length > 0}
|
{#if $recentlySeen?.length > 0}
|
||||||
<h3 class="mx-2">
|
<div>
|
||||||
|
<h3 class="m-2">
|
||||||
<Tr t={Translations.t.general.search.recents} />
|
<Tr t={Translations.t.general.search.recents} />
|
||||||
</h3>
|
</h3>
|
||||||
{#each $recentlySeen as entry}
|
{#each $recentlySeen as entry}
|
||||||
<SearchResult {entry} {state} on:select />
|
<SearchResult {entry} {state} on:select />
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $recentThemes?.length > 0 && $allowOtherThemes}
|
{#if $recentThemes?.length > 0 && $allowOtherThemes}
|
||||||
<h3 class="mx-2">
|
<div>
|
||||||
|
<h3 class="m-2">
|
||||||
<Tr t={Translations.t.general.search.recentThemes} />
|
<Tr t={Translations.t.general.search.recentThemes} />
|
||||||
</h3>
|
</h3>
|
||||||
{#each $recentThemes as themeId (themeId)}
|
{#each $recentThemes as themeId (themeId)}
|
||||||
<SearchResult
|
<SearchResult
|
||||||
entry={{payload: MoreScreen.officialThemesById.get(themeId), display_name: themeId, lat: 0, lon: 0}} {state}
|
entry={{payload: MoreScreen.officialThemesById.get(themeId), display_name: themeId, lat: 0, lon: 0}}
|
||||||
|
{state}
|
||||||
on:select />
|
on:select />
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -91,7 +87,7 @@
|
||||||
.collapsable {
|
.collapsable {
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
transition: max-height 400ms linear;
|
transition: max-height 400ms linear;
|
||||||
transition-delay: 500ms;
|
transition-delay: 100ms;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
export let layer: LayerConfig
|
export let layer: LayerConfig
|
||||||
export let config: TagRenderingConfig
|
export let config: TagRenderingConfig
|
||||||
export let extraClasses: string | undefined = undefined
|
export let extraClasses: string | undefined = undefined
|
||||||
|
export let defaultSize = "w-full"
|
||||||
|
export let noIcons = false
|
||||||
|
|
||||||
export let id: string = undefined
|
export let id: string = undefined
|
||||||
|
|
||||||
|
@ -28,7 +30,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties($tags))}
|
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties($tags))}
|
||||||
<div {id} class={twMerge("link-underline flex h-full w-full flex-col", extraClasses)}>
|
<div {id} class={twMerge("link-underline flex h-full flex-col", defaultSize, extraClasses)}>
|
||||||
{#if $trs.length === 1}
|
{#if $trs.length === 1}
|
||||||
<TagRenderingMapping
|
<TagRenderingMapping
|
||||||
mapping={$trs[0]}
|
mapping={$trs[0]}
|
||||||
|
@ -37,13 +39,14 @@
|
||||||
{selectedElement}
|
{selectedElement}
|
||||||
{layer}
|
{layer}
|
||||||
clss={config?.classes?.join(" ") ?? ""}
|
clss={config?.classes?.join(" ") ?? ""}
|
||||||
|
{noIcons}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $trs.length > 1}
|
{#if $trs.length > 1}
|
||||||
<ul>
|
<ul>
|
||||||
{#each $trs as mapping}
|
{#each $trs as mapping}
|
||||||
<li>
|
<li>
|
||||||
<TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer} />
|
<TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer} {noIcons}/>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
export let tags: UIEventSource<Record<string, string>>
|
export let tags: UIEventSource<Record<string, string>>
|
||||||
export let state: SpecialVisualizationState
|
export let state: SpecialVisualizationState
|
||||||
export let layer: LayerConfig
|
export let layer: LayerConfig
|
||||||
|
export let noIcons = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Css classes to apply
|
* Css classes to apply
|
||||||
|
@ -32,7 +33,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if mapping.icon !== undefined}
|
{#if mapping.icon !== undefined && !noIcons}
|
||||||
<div class="inline-flex items-center">
|
<div class="inline-flex items-center">
|
||||||
<Marker
|
<Marker
|
||||||
icons={mapping.icon}
|
icons={mapping.icon}
|
||||||
|
|
21
src/Utils/focusWithArrows.ts
Normal file
21
src/Utils/focusWithArrows.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
export function focusWithArrows(htmlElement: HTMLDivElement, classname: string) {
|
||||||
|
const destroy: () => void = undefined
|
||||||
|
htmlElement.addEventListener("keyup", e => {
|
||||||
|
const currentElement = document.activeElement
|
||||||
|
const canBeSelected = <HTMLElement[]> Array.from(htmlElement.getElementsByClassName(classname))
|
||||||
|
|
||||||
|
const i = canBeSelected.findIndex(el => el.contains(currentElement) || el === currentElement)
|
||||||
|
const l = canBeSelected.length
|
||||||
|
if (e.key === "ArrowDown") {
|
||||||
|
canBeSelected.at((i + 1) % l).focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === "ArrowUp") {
|
||||||
|
canBeSelected.at((i - 1) ).focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return { destroy }
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue