Merge branch 'develop' into Robin-patch-1
This commit is contained in:
commit
ad3a0a10cb
40 changed files with 798 additions and 262 deletions
assets
layers
scouting_group
wayside_shrines
themes
src
Logic
FeatureSource/Actors
Search
CombinedSearcher.tsGeocodingFeatureSource.tsGeocodingProvider.tsLocalElementSearch.tsNominatimGeocoding.ts
SimpleMetaTagger.tsState
Tags
Web
Models
UI
|
@ -115,7 +115,23 @@
|
|||
"mastodon"
|
||||
],
|
||||
"filter": [
|
||||
"nsi_brand.brand"
|
||||
"nsi_brand.brand",
|
||||
{
|
||||
"id": "brand_search",
|
||||
"options": [
|
||||
{
|
||||
"osmTags": "brand~i~.*{search}.*",
|
||||
"fields": [
|
||||
{
|
||||
"name": "search"
|
||||
}
|
||||
],
|
||||
"question": {
|
||||
"en": "Search for brand: {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"allowMove": true,
|
||||
"credits": "Osmwithspace",
|
||||
|
|
31
assets/layers/wayside_shrines/shrine.svg
Normal file
31
assets/layers/wayside_shrines/shrine.svg
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
id="svg109"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14">
|
||||
<metadata
|
||||
id="metadata115">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs113" />
|
||||
<path
|
||||
id="rect816"
|
||||
d="M 7 0 L 5 3 L 5 12 L 9 12 L 9 3 L 7 0 z M 7 3.9980469 A 1 1 0 0 1 8 4.9980469 A 1 1 0 0 0 8 5 L 8 8 L 6 8 L 6 5 A 1 1 0 0 0 6 4.9980469 A 1 1 0 0 1 7 3.9980469 z M 4 13 L 4 14 L 10 14 L 10 13 L 4 13 z "
|
||||
style="opacity:1;fill:#555;fill-opacity:1;stroke:none;stroke-width:0.21650636;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" />
|
||||
</svg>
|
After (image error) Size: 1.1 KiB |
2
assets/layers/wayside_shrines/shrine.svg.license
Normal file
2
assets/layers/wayside_shrines/shrine.svg.license
Normal file
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: Wanderwütiger
|
||||
SPDX-License-Identifier: CC0-1.0
|
BIN
assets/layers/wayside_shrines/shrine_example1.jpg
Normal file
BIN
assets/layers/wayside_shrines/shrine_example1.jpg
Normal file
Binary file not shown.
After ![]() (image error) Size: 42 KiB |
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: Bastian Greshake Tzovaras
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
BIN
assets/layers/wayside_shrines/shrine_example2.jpg
Normal file
BIN
assets/layers/wayside_shrines/shrine_example2.jpg
Normal file
Binary file not shown.
After ![]() (image error) Size: 42 KiB |
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: Bastian Greshake Tzovaras
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
BIN
assets/layers/wayside_shrines/shrine_example3.jpg
Normal file
BIN
assets/layers/wayside_shrines/shrine_example3.jpg
Normal file
Binary file not shown.
After ![]() (image error) Size: 43 KiB |
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: Bastian Greshake Tzovaras
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
335
assets/layers/wayside_shrines/wayside_shrines.json
Normal file
335
assets/layers/wayside_shrines/wayside_shrines.json
Normal file
|
@ -0,0 +1,335 @@
|
|||
{
|
||||
"id": "wayside_shrine",
|
||||
"name": {
|
||||
"en": "Wayside Shrines"
|
||||
},
|
||||
"description": {
|
||||
"en": "Shrines are religious places that are dedicated to specific deities, saints and other figures of religious importance. Typically, the contain religious depictions and people frequently leave offerings at those places. Wayside shrines are small small shrines that can be found next to a road or pathway and are frequented by travellers passing by."
|
||||
},
|
||||
"source": {
|
||||
"osmTags": "historic=wayside_shrine"
|
||||
},
|
||||
"minzoom": 12,
|
||||
"title": {
|
||||
"render": {
|
||||
"en": "Wayside Shrine {name}"
|
||||
}
|
||||
},
|
||||
"pointRendering": [
|
||||
{
|
||||
"iconSize": "40,40",
|
||||
"location": [
|
||||
"point"
|
||||
],
|
||||
"anchor": "center",
|
||||
"marker": [
|
||||
{
|
||||
"icon": "circle",
|
||||
"color": "white"
|
||||
},
|
||||
{
|
||||
"icon": "./assets/layers/wayside_shrines/shrine.svg"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"lineRendering": [
|
||||
{
|
||||
"color": "#00f",
|
||||
"width": "8"
|
||||
}
|
||||
],
|
||||
"presets": [
|
||||
{
|
||||
"tags": [
|
||||
"historic=wayside_shrine"
|
||||
],
|
||||
"title": {
|
||||
"en": "a wayside shrine"
|
||||
},
|
||||
"description": {
|
||||
"en": "A wayside shrine typically shows a religious depiction, usually placed by a road or pathway. "
|
||||
},
|
||||
"exampleImages": [
|
||||
"./assets/layers/wayside_shrines/shrine_example1.jpg",
|
||||
"./assets/layers/wayside_shrines/shrine_example2.jpg",
|
||||
"./assets/layers/wayside_shrines/shrine_example3.jpg"
|
||||
]
|
||||
}
|
||||
],
|
||||
"tagRenderings": [
|
||||
"images",
|
||||
{
|
||||
"question": {
|
||||
"en": "What's the name of this shrine?"
|
||||
},
|
||||
"id": "shrine_name",
|
||||
"freeform": {
|
||||
"key": "name",
|
||||
"type": "string"
|
||||
},
|
||||
"render": {
|
||||
"en": "The name of this shrine is <b>{name}</b>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"question": {
|
||||
"en": "To which religion is this shrine dedicated?"
|
||||
},
|
||||
"id": "religion",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "religion=christian",
|
||||
"then": {
|
||||
"en": "This is a Christian shrine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "religion=buddhist",
|
||||
"then": {
|
||||
"en": "This is a Buddhist shrine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "religion=hindu",
|
||||
"then": {
|
||||
"en": "This is a Hindu shrine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "religion=jain",
|
||||
"then": {
|
||||
"en": "This is a Jain shrine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "religion=jewish",
|
||||
"then": {
|
||||
"en": "This is a Jewish shrine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "religion=muslim",
|
||||
"then": {
|
||||
"en": "This is an Islamic shrine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "religion=pagan",
|
||||
"then": {
|
||||
"en": "This is a Pagan shrine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "religion=shinto",
|
||||
"then": {
|
||||
"en": "This is a Shinto shrine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "religion=sikh",
|
||||
"then": {
|
||||
"en": "This is a Sikh shrine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "religion=taoist",
|
||||
"then": {
|
||||
"en": "This is a Taoist shrine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "religion=zoroastrian",
|
||||
"then": {
|
||||
"en": "This is a Zoroastrian shrine"
|
||||
}
|
||||
}
|
||||
],
|
||||
"multiAnswer": false,
|
||||
"freeform": {
|
||||
"key": "religion",
|
||||
"type": "string"
|
||||
},
|
||||
"render": {
|
||||
"en": "This shrine is {religion}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"question": {
|
||||
"en": "What's the Christian denomination of the shrine?"
|
||||
},
|
||||
"id": "denomination_christian",
|
||||
"condition": "religion=christian",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "denomination=catholic",
|
||||
"then": {
|
||||
"en": "It's denomination is: Catholic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "denomination=roman_catholic",
|
||||
"then": {
|
||||
"en": "It's denomination is: Roman Catholic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "denomination=orthodox",
|
||||
"then": {
|
||||
"en": "It's denomination is Orthodox"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "denomination=greek_orthodox",
|
||||
"then": {
|
||||
"en": "It's denomination is Greek-Orthodox"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "denomination=russian_orthodox",
|
||||
"then": {
|
||||
"en": "It's denomination is Russian-Orthodox"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "denomination=serbian_orthodox",
|
||||
"then": {
|
||||
"en": "It's denomination is Serbian Orthodox"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "denomination=protestant",
|
||||
"then": {
|
||||
"en": "It's denomination is Protestant"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "denomination=anglican",
|
||||
"then": {
|
||||
"en": "It's denomination is Anglican"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "denomination=adventist",
|
||||
"then": {
|
||||
"en": "It's denomination is Adventist"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "denomination=evangelical",
|
||||
"then": {
|
||||
"en": "It's denomination is evangelical"
|
||||
}
|
||||
}
|
||||
],
|
||||
"render": {
|
||||
"en": "Other denomination: {denomination}"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "denomination"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mappings": [
|
||||
{
|
||||
"if": "denomination=shia",
|
||||
"then": {
|
||||
"en": "It's denomination is Shia"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "denomination=sunni",
|
||||
"then": {
|
||||
"en": "It's denomination is Sunni"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "denomination=sufi",
|
||||
"then": {
|
||||
"en": "It's denomination is Sufi"
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": "denomination_muslim",
|
||||
"freeform": {
|
||||
"key": "denomination"
|
||||
},
|
||||
"render": {
|
||||
"en": "It's denomination is {denomination}"
|
||||
},
|
||||
"question": {
|
||||
"en": "What's the Muslim denomination of this shrine?"
|
||||
},
|
||||
"condition": "religion=muslim"
|
||||
},
|
||||
{
|
||||
"mappings": [
|
||||
{
|
||||
"if": "denomination=conservative",
|
||||
"then": {
|
||||
"en": "It's denomination is Conservative"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "denomination=orthodox",
|
||||
"then": {
|
||||
"en": "It's denomination is Orthodox"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "denomination=hasidic",
|
||||
"then": {
|
||||
"en": "It's denomination is Hasidic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "denomination=reform",
|
||||
"then": {
|
||||
"en": "It's denomination is Reform"
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": "denomination_jewish",
|
||||
"freeform": {
|
||||
"key": "denomination"
|
||||
},
|
||||
"render": {
|
||||
"en": "It's denomination is {denomination}"
|
||||
},
|
||||
"question": {
|
||||
"en": "What's the Jewish denomination of this shrine?"
|
||||
},
|
||||
"condition": "religion=jewish"
|
||||
},
|
||||
{
|
||||
"question": {
|
||||
"en": "What's the denomination of this shrine?"
|
||||
},
|
||||
"id": "denomination_other",
|
||||
"render": {
|
||||
"en": "The denomination of this shrine is: {denomination}"
|
||||
},
|
||||
"freeform": {
|
||||
"key": "denomination"
|
||||
},
|
||||
"condition": {
|
||||
"and": [
|
||||
"religion!=christian",
|
||||
"religion!=muslim",
|
||||
"religion!=jewish"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"deletion": {
|
||||
"softDeletionTags": {
|
||||
"and": [
|
||||
"disused:historic:={historic}",
|
||||
"historic="
|
||||
]
|
||||
},
|
||||
"neededChangesets": 1
|
||||
},
|
||||
"credits": "Bastian Greshake Tzovaras",
|
||||
"credits:uid": 20617622
|
||||
}
|
|
@ -26,9 +26,13 @@
|
|||
"=osmTags": {
|
||||
"and": [
|
||||
"tourism=camp_site",
|
||||
"scout!=no",
|
||||
"group!=no",
|
||||
{
|
||||
"or": [
|
||||
"scout=yes",
|
||||
"scout=only",
|
||||
"group=yes",
|
||||
"group_only=yes"
|
||||
]
|
||||
}
|
||||
|
@ -62,9 +66,13 @@
|
|||
"=osmTags": {
|
||||
"and": [
|
||||
"tourism=hostel",
|
||||
"scout!=no",
|
||||
"group!=no",
|
||||
{
|
||||
"or": [
|
||||
"scout=yes",
|
||||
"scout=only",
|
||||
"group=yes",
|
||||
"group_only=yes"
|
||||
]
|
||||
}
|
||||
|
|
31
assets/themes/wayside_shrines/shrine.svg
Normal file
31
assets/themes/wayside_shrines/shrine.svg
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
id="svg109"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14">
|
||||
<metadata
|
||||
id="metadata115">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs113" />
|
||||
<path
|
||||
id="rect816"
|
||||
d="M 7 0 L 5 3 L 5 12 L 9 12 L 9 3 L 7 0 z M 7 3.9980469 A 1 1 0 0 1 8 4.9980469 A 1 1 0 0 0 8 5 L 8 8 L 6 8 L 6 5 A 1 1 0 0 0 6 4.9980469 A 1 1 0 0 1 7 3.9980469 z M 4 13 L 4 14 L 10 14 L 10 13 L 4 13 z "
|
||||
style="opacity:1;fill:#555;fill-opacity:1;stroke:none;stroke-width:0.21650636;stroke-linecap:square;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill" />
|
||||
</svg>
|
After (image error) Size: 1.1 KiB |
2
assets/themes/wayside_shrines/shrine.svg.license
Normal file
2
assets/themes/wayside_shrines/shrine.svg.license
Normal file
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: Wanderwütiger
|
||||
SPDX-License-Identifier: CC0-1.0
|
13
assets/themes/wayside_shrines/wayside_shrines.json
Normal file
13
assets/themes/wayside_shrines/wayside_shrines.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "wayside_shrines",
|
||||
"title": {
|
||||
"en": "Wayside shrines"
|
||||
},
|
||||
"description": {
|
||||
"en": "This map shows shrines found on the side of roads and paths, and allows adding new ones"
|
||||
},
|
||||
"icon": "./assets/themes/wayside_shrines/shrine.svg",
|
||||
"layers": [
|
||||
"wayside_shrines"
|
||||
]
|
||||
}
|
|
@ -76,7 +76,7 @@ export default class SaveFeatureSourceToLocalStorage {
|
|||
const storage = TileLocalStorage.construct<Feature[]>(backend, layername, maxCacheAge)
|
||||
this.storage = storage
|
||||
const singleTileSavers: Map<number, SingleTileSaver> = new Map<number, SingleTileSaver>()
|
||||
features.features.addCallbackAndRunD((features) => {
|
||||
features.features.stabilized(5000).addCallbackAndRunD((features) => {
|
||||
if (
|
||||
features.some((f) => {
|
||||
let totalPoints = 0
|
||||
|
@ -116,7 +116,7 @@ export default class SaveFeatureSourceToLocalStorage {
|
|||
tileSaver = new SingleTileSaver(src, featureProperties)
|
||||
singleTileSavers.set(tileIndex, tileSaver)
|
||||
}
|
||||
// Don't cache not-uploaded features yet - they'll be cached when the receive their id
|
||||
// Don't cache not-uploaded features yet - they'll be cached when they receive their id
|
||||
features = features.filter((f) => !f.properties.id.match(/(node|way)\/-[0-9]+/))
|
||||
tileSaver.saveFeatures(features)
|
||||
})
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
import GeocodingProvider, {
|
||||
GeocodeResult,
|
||||
GeocodingOptions,
|
||||
SearchResult,
|
||||
} from "./GeocodingProvider"
|
||||
import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Store, Stores } from "../UIEventSource"
|
||||
|
||||
|
@ -44,12 +40,12 @@ export default class CombinedSearcher implements GeocodingProvider {
|
|||
return results
|
||||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> {
|
||||
const results = await Promise.all(this._providers.map((pr) => pr.search(query, options)))
|
||||
return CombinedSearcher.merge(results)
|
||||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
|
||||
suggest(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> {
|
||||
return Stores.concat(
|
||||
this._providersWithSuggest.map((pr) => pr.suggest(query, options))
|
||||
).map((gcrss) => CombinedSearcher.merge(gcrss))
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { SearchResult } from "./GeocodingProvider"
|
||||
import { GeocodeResult } from "./GeocodingProvider"
|
||||
import { Store } from "../UIEventSource"
|
||||
import { FeatureSource } from "../FeatureSource/FeatureSource"
|
||||
import { Feature, Geometry } from "geojson"
|
||||
|
@ -6,7 +6,7 @@ import { Feature, Geometry } from "geojson"
|
|||
export default class GeocodingFeatureSource implements FeatureSource {
|
||||
public features: Store<Feature<Geometry, Record<string, string>>[]>
|
||||
|
||||
constructor(provider: Store<SearchResult[]>) {
|
||||
constructor(provider: Store<GeocodeResult[]>) {
|
||||
this.features = provider.map((geocoded) => {
|
||||
if (geocoded === undefined) {
|
||||
return []
|
||||
|
|
|
@ -42,7 +42,6 @@ export type GeocodeResult = {
|
|||
payload?: object
|
||||
source?: string
|
||||
}
|
||||
export type SearchResult = GeocodeResult
|
||||
|
||||
export interface GeocodingOptions {
|
||||
bbox?: BBox
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider"
|
||||
import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Feature } from "geojson"
|
||||
|
@ -26,7 +26,7 @@ export default class LocalElementSearch implements GeocodingProvider {
|
|||
this._limit = limit
|
||||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> {
|
||||
return this.searchEntries(query, options, false).data
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,7 @@ export default class LocalElementSearch implements GeocodingProvider {
|
|||
query: string,
|
||||
options?: GeocodingOptions,
|
||||
matchStart?: boolean
|
||||
): Store<SearchResult[]> {
|
||||
): Store<GeocodeResult[]> {
|
||||
if (query.length < 3) {
|
||||
return new ImmutableStore([])
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ export default class LocalElementSearch implements GeocodingProvider {
|
|||
}
|
||||
return results.map((entry) => {
|
||||
const [osm_type, osm_id] = entry.feature.properties.id.split("/")
|
||||
return <SearchResult>{
|
||||
return <GeocodeResult>{
|
||||
lon: entry.center[0],
|
||||
lat: entry.center[1],
|
||||
osm_type,
|
||||
|
@ -141,7 +141,7 @@ export default class LocalElementSearch implements GeocodingProvider {
|
|||
})
|
||||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
|
||||
suggest(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> {
|
||||
return this.searchEntries(query, options, true)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { BBox } from "../BBox"
|
|||
import Constants from "../../Models/Constants"
|
||||
import { FeatureCollection } from "geojson"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider"
|
||||
import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||
|
||||
export class NominatimGeocoding implements GeocodingProvider {
|
||||
private readonly _host
|
||||
|
@ -15,7 +15,7 @@ export class NominatimGeocoding implements GeocodingProvider {
|
|||
this._host = host
|
||||
}
|
||||
|
||||
public search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
|
||||
public search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> {
|
||||
const b = options?.bbox ?? BBox.global
|
||||
const url = `${this._host}search?format=json&limit=${
|
||||
this.limit
|
||||
|
|
|
@ -11,6 +11,8 @@ import { UIEventSource } from "./UIEventSource"
|
|||
import ThemeConfig from "../Models/ThemeConfig/ThemeConfig"
|
||||
import OsmObjectDownloader from "./Osm/OsmObjectDownloader"
|
||||
import countryToCurrency from "country-to-currency"
|
||||
import { Unit } from "../Models/Unit"
|
||||
import { Denomination } from "../Models/Denomination"
|
||||
|
||||
/**
|
||||
* All elements that are needed to perform metatagging
|
||||
|
@ -476,10 +478,8 @@ export default class SimpleMetaTaggers {
|
|||
doc: "If 'units' is defined in the layoutConfig, then this metatagger will rewrite the specified keys to have the canonical form (e.g. `1meter` will be rewritten to `1m`; `1` will be rewritten to `1m` as well)",
|
||||
keys: ["Theme-defined keys"],
|
||||
},
|
||||
(feature, _, __, state) => {
|
||||
const units = Utils.NoNull(
|
||||
[].concat(...(state?.theme?.layers?.map((layer) => layer.units) ?? []))
|
||||
)
|
||||
(feature, layer, __, state) => {
|
||||
const units: Unit[] = layer.units
|
||||
if (units.length == 0) {
|
||||
return
|
||||
}
|
||||
|
@ -497,7 +497,7 @@ export default class SimpleMetaTaggers {
|
|||
continue
|
||||
}
|
||||
const value = feature.properties[key]
|
||||
const denom = unit.findDenomination(value, () => feature.properties["_country"])
|
||||
const denom: [string, Denomination] = unit.findDenomination(value, () => feature.properties["_country"])
|
||||
if (denom === undefined) {
|
||||
// no valid value found
|
||||
break
|
||||
|
@ -515,7 +515,7 @@ export default class SimpleMetaTaggers {
|
|||
if (canonical === value) {
|
||||
break
|
||||
}
|
||||
console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`)
|
||||
console.log("Rewritten ", key, ` from '${value}' into '${canonical}' due to denomination`, denomination)
|
||||
if (canonical === undefined && !unit.eraseInvalid) {
|
||||
break
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import GeocodingProvider, { type SearchResult } from "../Search/GeocodingProvider"
|
||||
import GeocodingProvider, { GeocodeResult, GeocodingUtils } from "../Search/GeocodingProvider"
|
||||
import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource"
|
||||
import CombinedSearcher from "../Search/CombinedSearcher"
|
||||
import FilterSearch, { FilterSearchResult } from "../Search/FilterSearch"
|
||||
|
@ -16,12 +16,13 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
|||
import { FeatureSource } from "../FeatureSource/FeatureSource"
|
||||
import { Feature } from "geojson"
|
||||
import OpenLocationCodeSearch from "../Search/OpenLocationCodeSearch"
|
||||
import { BBox } from "../BBox"
|
||||
|
||||
export default class SearchState {
|
||||
public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
|
||||
public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("")
|
||||
public readonly searchIsFocused = new UIEventSource(false)
|
||||
public readonly suggestions: Store<SearchResult[]>
|
||||
public readonly suggestions: Store<GeocodeResult[]>
|
||||
public readonly filterSuggestions: Store<FilterSearchResult[]>
|
||||
public readonly themeSuggestions: Store<MinimalThemeInformation[]>
|
||||
public readonly layerSuggestions: Store<LayerConfig[]>
|
||||
|
@ -60,7 +61,7 @@ export default class SearchState {
|
|||
return new ImmutableStore(true)
|
||||
}
|
||||
return Stores.concat(suggestions).map((suggestions) =>
|
||||
suggestions.some((list, i) => list === undefined)
|
||||
suggestions.some(list => list === undefined)
|
||||
)
|
||||
})
|
||||
this.suggestions = suggestionsList.bindD((suggestions) =>
|
||||
|
@ -100,7 +101,7 @@ export default class SearchState {
|
|||
|
||||
this.showSearchDrawer = new UIEventSource(false)
|
||||
|
||||
this.searchIsFocused.addCallbackAndRunD((sugg) => {
|
||||
this.searchIsFocused.addCallbackAndRunD(sugg => {
|
||||
if (sugg) {
|
||||
this.showSearchDrawer.set(true)
|
||||
}
|
||||
|
@ -124,7 +125,6 @@ export default class SearchState {
|
|||
const state = this.state
|
||||
|
||||
const layersToShow = payload.map((fsr) => fsr.layer.id)
|
||||
console.log("Layers to show are", layersToShow)
|
||||
for (const otherLayer of state.layerState.filteredLayers.values()) {
|
||||
const layer = otherLayer.layerDef
|
||||
if (!layer.isNormal()) {
|
||||
|
@ -167,4 +167,45 @@ export default class SearchState {
|
|||
this.state.featureProperties.trackFeature(f)
|
||||
this.state.selectedElement.set(f)
|
||||
}
|
||||
|
||||
public moveToBestMatch() {
|
||||
const suggestion = this.suggestions.data?.[0]
|
||||
if (suggestion) {
|
||||
this.applyGeocodeResult(suggestion)
|
||||
}
|
||||
if (this.suggestionsSearchRunning.data) {
|
||||
this.suggestionsSearchRunning.addCallback(() => {
|
||||
this.applyGeocodeResult(this.suggestions.data?.[0])
|
||||
return true // unregister
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
applyGeocodeResult(entry: GeocodeResult) {
|
||||
if (!entry) {
|
||||
console.error("ApplyGeocodeResult got undefined/null")
|
||||
}
|
||||
console.log("Moving to", entry.description)
|
||||
const state = this.state
|
||||
if (entry.boundingbox) {
|
||||
const [lat0, lat1, lon0, lon1] = entry.boundingbox
|
||||
state.mapProperties.bounds.set(
|
||||
new BBox([
|
||||
[lon0, lat0],
|
||||
[lon1, lat1]
|
||||
]).pad(0.01)
|
||||
)
|
||||
} else {
|
||||
state.mapProperties.flyTo(
|
||||
entry.lon,
|
||||
entry.lat,
|
||||
GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17
|
||||
)
|
||||
}
|
||||
if (entry.feature?.properties?.id) {
|
||||
state.selectedElement.set(entry.feature)
|
||||
}
|
||||
state.userRelatedState.recentlyVisitedSearch.add(entry)
|
||||
this.closeIfFullscreen()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,11 @@ export class TagTypes {
|
|||
return <any>and.and
|
||||
}
|
||||
|
||||
static uploadableAnd(and: And & UploadableTag): UploadableTag[] {
|
||||
return <any>and.and
|
||||
}
|
||||
|
||||
|
||||
static safeOr(or: Or & OptimizedTag): ((FlatTag | (And & OptimizedTag)) & OptimizedTag)[] {
|
||||
return <any>or.or
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export class TagUtils {
|
|||
["<=", (a, b) => a <= b],
|
||||
[">=", (a, b) => a >= b],
|
||||
["<", (a, b) => a < b],
|
||||
[">", (a, b) => a > b],
|
||||
[">", (a, b) => a > b]
|
||||
]
|
||||
public static modeDocumentation: Record<
|
||||
string,
|
||||
|
@ -48,7 +48,7 @@ export class TagUtils {
|
|||
"### Removing a key\n" +
|
||||
"\n" +
|
||||
"If a key should be deleted in the OpenStreetMap-database, specify `key=` as well. This can be used e.g. to remove a\n" +
|
||||
"fixme or value from another mapping if another field is filled out.",
|
||||
"fixme or value from another mapping if another field is filled out."
|
||||
},
|
||||
"!=": {
|
||||
name: "strict not equals",
|
||||
|
@ -62,7 +62,7 @@ export class TagUtils {
|
|||
"### If key is present\n" +
|
||||
"\n" +
|
||||
"This implies that, to check if a key is present, `key!=` can be used. This will only match if the key is present and not\n" +
|
||||
"empty.",
|
||||
"empty."
|
||||
},
|
||||
"~": {
|
||||
name: "Value matches regex",
|
||||
|
@ -73,12 +73,12 @@ export class TagUtils {
|
|||
"The regex is put within braces as to prevent runaway values.\n" +
|
||||
"\nUse `key~*` to indicate that any value is allowed. This is effectively the check that the attribute is present (defined _and_ not empty)." +
|
||||
"\n" +
|
||||
"Regexes will match the newline character with `.` too - the `s`-flag is enabled by default.",
|
||||
"Regexes will match the newline character with `.` too - the `s`-flag is enabled by default."
|
||||
},
|
||||
"~i~": {
|
||||
name: "Value matches case-invariant regex",
|
||||
overpassSupport: true,
|
||||
docs: "A tag can also be tested against a regex with `key~i~regex`, where the case of the value will be ignored. The regex is still matched against the _entire_ value",
|
||||
docs: "A tag can also be tested against a regex with `key~i~regex`, where the case of the value will be ignored. The regex is still matched against the _entire_ value"
|
||||
},
|
||||
"!~": {
|
||||
name: "Value should _not_ match regex",
|
||||
|
@ -87,27 +87,27 @@ export class TagUtils {
|
|||
"A tag can also be tested against a regex with `key!~regex`. This filter will match if the value does *not* match the regex. " +
|
||||
"\n If the\n" +
|
||||
"value is allowed to appear anywhere as substring, use `key~.*regex.*`.\n" +
|
||||
"The regex is put within braces as to prevent runaway values.\n",
|
||||
"The regex is put within braces as to prevent runaway values.\n"
|
||||
},
|
||||
"!~i~": {
|
||||
name: "Value does *not* match case-invariant regex",
|
||||
overpassSupport: true,
|
||||
docs: "A tag can also be tested against a regex with `key~i~regex`, where the case of the value will be ignored. The regex is still matched against the _entire_ value. This filter returns true if the value does *not* match",
|
||||
docs: "A tag can also be tested against a regex with `key~i~regex`, where the case of the value will be ignored. The regex is still matched against the _entire_ value. This filter returns true if the value does *not* match"
|
||||
},
|
||||
"~~": {
|
||||
name: "Key and value should match given regex",
|
||||
overpassSupport: true,
|
||||
docs: "Both the `key` and `value` part of this specification are interpreted as regexes, both the key and value musth completely match their respective regexes",
|
||||
docs: "Both the `key` and `value` part of this specification are interpreted as regexes, both the key and value musth completely match their respective regexes"
|
||||
},
|
||||
"~i~~": {
|
||||
name: "Key and value should match a given regex; value is case-invariant",
|
||||
overpassSupport: true,
|
||||
docs: "Similar to ~~, except that the value is case-invariant",
|
||||
docs: "Similar to ~~, except that the value is case-invariant"
|
||||
},
|
||||
"!~i~~": {
|
||||
name: "Key and value should match a given regex; value is case-invariant",
|
||||
overpassSupport: true,
|
||||
docs: "Similar to !~~, except that the value is case-invariant",
|
||||
docs: "Similar to !~~, except that the value is case-invariant"
|
||||
},
|
||||
":=": {
|
||||
name: "Substitute `... {some_key} ...` and match `key`",
|
||||
|
@ -133,24 +133,24 @@ export class TagUtils {
|
|||
"\n" +
|
||||
"```json\n" +
|
||||
"{\n" +
|
||||
' "mappings": [\n' +
|
||||
" \"mappings\": [\n" +
|
||||
" {\n" +
|
||||
' "if":"key:={some_other_key}",\n' +
|
||||
' "then": "...",\n' +
|
||||
' "hideInAnswer": "some_other_key="\n' +
|
||||
" \"if\":\"key:={some_other_key}\",\n" +
|
||||
" \"then\": \"...\",\n" +
|
||||
" \"hideInAnswer\": \"some_other_key=\"\n" +
|
||||
" }\n" +
|
||||
" ]\n" +
|
||||
"}\n" +
|
||||
"```\n" +
|
||||
"\n" +
|
||||
"One can use `key!:=prefix-{other_key}-postfix` as well, to match if `key` is _not_ the same\n" +
|
||||
"as `prefix-{other_key}-postfix` (with `other_key` substituted by the value)",
|
||||
"as `prefix-{other_key}-postfix` (with `other_key` substituted by the value)"
|
||||
},
|
||||
"!:=": {
|
||||
name: "Substitute `{some_key}` should not match `key`",
|
||||
overpassSupport: false,
|
||||
docs: "See `:=`, except that this filter is inverted",
|
||||
},
|
||||
docs: "See `:=`, except that this filter is inverted"
|
||||
}
|
||||
}
|
||||
private static keyCounts: { keys: any; tags: any } = key_counts
|
||||
public static readonly numberAndDateComparisonDocs =
|
||||
|
@ -175,10 +175,10 @@ export class TagUtils {
|
|||
"\n" +
|
||||
"```json\n" +
|
||||
"{\n" +
|
||||
' "osmTags": {\n' +
|
||||
' "or": [\n' +
|
||||
' "amenity=school",\n' +
|
||||
' "amenity=kindergarten"\n' +
|
||||
" \"osmTags\": {\n" +
|
||||
" \"or\": [\n" +
|
||||
" \"amenity=school\",\n" +
|
||||
" \"amenity=kindergarten\"\n" +
|
||||
" ]\n" +
|
||||
" }\n" +
|
||||
"}\n" +
|
||||
|
@ -194,7 +194,7 @@ export class TagUtils {
|
|||
"If the schema-files note a type [`TagConfigJson`](https://github.com/pietervdvn/MapComplete/blob/develop/src/Models/ThemeConfig/Json/TagConfigJson.ts), you can use one of these values.\n" +
|
||||
"\n" +
|
||||
"In some cases, not every type of tags-filter can be used. For example, _rendering_ an option with a regex is\n" +
|
||||
'fine (`"if": "brand~[Bb]randname", "then":" The brand is Brandname"`); but this regex can not be used to write a value\n' +
|
||||
"fine (`\"if\": \"brand~[Bb]randname\", \"then\":\" The brand is Brandname\"`); but this regex can not be used to write a value\n" +
|
||||
"into the database. The theme loader will however refuse to work with such inconsistencies and notify you of this while\n" +
|
||||
"you are building your theme.\n" +
|
||||
"\n" +
|
||||
|
@ -205,18 +205,18 @@ export class TagUtils {
|
|||
"\n" +
|
||||
"```json\n" +
|
||||
"{\n" +
|
||||
' "and": [\n' +
|
||||
' "key=value",\n' +
|
||||
" \"and\": [\n" +
|
||||
" \"key=value\",\n" +
|
||||
" {\n" +
|
||||
' "or": [\n' +
|
||||
' "other_key=value",\n' +
|
||||
' "other_key=some_other_value"\n' +
|
||||
" \"or\": [\n" +
|
||||
" \"other_key=value\",\n" +
|
||||
" \"other_key=some_other_value\"\n" +
|
||||
" ]\n" +
|
||||
" },\n" +
|
||||
' "key_which_should_be_missing=",\n' +
|
||||
' "key_which_should_have_a_value~*",\n' +
|
||||
' "key~.*some_regex_a*_b+_[a-z]?",\n' +
|
||||
' "height<1"\n' +
|
||||
" \"key_which_should_be_missing=\",\n" +
|
||||
" \"key_which_should_have_a_value~*\",\n" +
|
||||
" \"key~.*some_regex_a*_b+_[a-z]?\",\n" +
|
||||
" \"height<1\"\n" +
|
||||
" ]\n" +
|
||||
"}\n" +
|
||||
"```\n" +
|
||||
|
@ -374,9 +374,9 @@ export class TagUtils {
|
|||
* TagUtils.FlattenMultiAnswer(([new Tag("x","y"), new Tag("a","b")])) // => new And([new Tag("x","y"), new Tag("a","b")])
|
||||
* TagUtils.FlattenMultiAnswer(([new Tag("x","")])) // => new And([new Tag("x","")])
|
||||
*/
|
||||
static FlattenMultiAnswer(tagsFilters: UploadableTag[]): And {
|
||||
static FlattenMultiAnswer(tagsFilters: UploadableTag[]): UploadableTag[] {
|
||||
if (tagsFilters === undefined) {
|
||||
return new And([])
|
||||
return []
|
||||
}
|
||||
|
||||
const keyValues = TagUtils.SplitKeys(tagsFilters)
|
||||
|
@ -386,7 +386,7 @@ export class TagUtils {
|
|||
values.sort()
|
||||
and.push(new Tag(key, values.join(";")))
|
||||
}
|
||||
return new And(and)
|
||||
return and
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -988,7 +988,7 @@ export class TagUtils {
|
|||
TagUtils.comparators.map((comparator) => "`" + comparator[0] + "`").join(" ") +
|
||||
" Logical comparators",
|
||||
TagUtils.numberAndDateComparisonDocs,
|
||||
TagUtils.logicalOperator,
|
||||
TagUtils.logicalOperator
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
|
|
|
@ -177,7 +177,6 @@ export default class FeatureReviews {
|
|||
testmode?: Store<boolean>,
|
||||
loadingAllowed?: UIEventSource<boolean | null>
|
||||
) {
|
||||
console.trace(">>> Creating FeatureReviews", options)
|
||||
this.loadingAllowed = loadingAllowed
|
||||
const centerLonLat = GeoOperations.centerpointCoordinates(feature)
|
||||
;[this._lon, this._lat] = centerLonLat
|
||||
|
@ -217,12 +216,10 @@ export default class FeatureReviews {
|
|||
}
|
||||
this._name = tagsSource.map((tags) => {
|
||||
const defaultName = tags[nameKey]
|
||||
console.trace(">>>", options, defaultName)
|
||||
if (defaultName && defaultName !== "") {
|
||||
console.log("Using default name:", defaultName, "fallback:", options.fallbackName)
|
||||
return defaultName
|
||||
}
|
||||
console.trace("Using fallback name", options?.fallbackName, options)
|
||||
return options?.fallbackName
|
||||
})
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import UserRelatedState from "../Logic/State/UserRelatedState"
|
|||
import { Utils } from "../Utils"
|
||||
import Zoomcontrol from "../UI/Zoomcontrol"
|
||||
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
|
||||
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
|
||||
|
||||
export type PageType = (typeof MenuState.pageNames)[number]
|
||||
|
||||
|
@ -27,7 +28,7 @@ export class MenuState {
|
|||
"favourites",
|
||||
"usersettings",
|
||||
"share",
|
||||
"menu",
|
||||
"menu"
|
||||
] as const
|
||||
|
||||
/**
|
||||
|
@ -38,6 +39,9 @@ export class MenuState {
|
|||
undefined
|
||||
)
|
||||
|
||||
public static readonly nearbyImagesFeature: UIEventSource<object> = new UIEventSource<object>(
|
||||
undefined
|
||||
)
|
||||
public readonly pageStates: Record<PageType, UIEventSource<boolean>>
|
||||
|
||||
public readonly highlightedLayerInFilters: UIEventSource<string> = new UIEventSource<string>(
|
||||
|
@ -45,6 +49,7 @@ export class MenuState {
|
|||
)
|
||||
public highlightedUserSetting: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||
private readonly _selectedElement: UIEventSource<any> | undefined
|
||||
private isClosingAll = false
|
||||
|
||||
constructor(selectedElement: UIEventSource<any> | undefined) {
|
||||
this._selectedElement = selectedElement
|
||||
|
@ -129,8 +134,13 @@ export class MenuState {
|
|||
* Returns 'true' if at least one menu was opened
|
||||
*/
|
||||
public closeAll(): boolean {
|
||||
console.log("Closing all")
|
||||
if (this.isClosingAll) {
|
||||
return true
|
||||
}
|
||||
this.isClosingAll = true
|
||||
const ps = this.pageStates
|
||||
try {
|
||||
|
||||
if (ps.menu.data) {
|
||||
ps.menu.set(false)
|
||||
return true
|
||||
|
@ -141,6 +151,10 @@ export class MenuState {
|
|||
return true
|
||||
}
|
||||
|
||||
if (MenuState.nearbyImagesFeature.data !== undefined) {
|
||||
MenuState.nearbyImagesFeature.setData(undefined)
|
||||
return true
|
||||
}
|
||||
for (const key in ps) {
|
||||
const toggle = ps[key]
|
||||
const wasOpen = toggle.data
|
||||
|
@ -153,5 +167,16 @@ export class MenuState {
|
|||
this._selectedElement.setData(undefined)
|
||||
return true
|
||||
}
|
||||
} finally {
|
||||
this.isClosingAll = false
|
||||
}
|
||||
}
|
||||
|
||||
public setPreviewedImage(img?: Partial<ProvidedImage>) {
|
||||
if (img === undefined && !this.isClosingAll) {
|
||||
return
|
||||
}
|
||||
MenuState.previewedImage.setData(img)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@ export class AvailableRasterLayers {
|
|||
availableLayersBboxes.map(
|
||||
(eliPolygons) => {
|
||||
const loc = location.data
|
||||
const lonlat: [number, number] = [loc.lon, loc.lat]
|
||||
const lonlat: [number, number] = [loc?.lon ?? 0, loc?.lat ?? 0]
|
||||
const matching: RasterLayerPolygon[] = eliPolygons.filter((eliPolygon) => {
|
||||
if (eliPolygon.geometry === null) {
|
||||
return true // global ELI-layer
|
||||
|
|
|
@ -223,7 +223,7 @@ export default class TagRenderingConfig {
|
|||
inline: json.freeform.inline ?? false,
|
||||
default: json.freeform.default,
|
||||
postfixDistinguished: json.freeform.postfixDistinguished?.trim(),
|
||||
args: json.freeform.helperArgs,
|
||||
args: json.freeform.helperArgs
|
||||
}
|
||||
if (json.freeform["extraTags"] !== undefined) {
|
||||
throw `Freeform.extraTags is defined. This should probably be 'freeform.addExtraTag' (at ${context})`
|
||||
|
@ -447,7 +447,7 @@ export default class TagRenderingConfig {
|
|||
iconClass,
|
||||
addExtraTags,
|
||||
searchTerms: mapping.searchTerms,
|
||||
priorityIf: prioritySearch,
|
||||
priorityIf: prioritySearch
|
||||
}
|
||||
if (isQuestionable) {
|
||||
if (hideInAnswer !== true && mp.if !== undefined && !mp.if.isUsableAsAnswer()) {
|
||||
|
@ -554,7 +554,7 @@ export default class TagRenderingConfig {
|
|||
then: new TypedTranslation<object>(
|
||||
this.render.replace("{" + this.freeform.key + "}", leftover).translations,
|
||||
this.render.context
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -607,7 +607,7 @@ export default class TagRenderingConfig {
|
|||
return {
|
||||
then: this.render.PartialSubs({ [this.freeform.key]: v.trim() }),
|
||||
icon: this.renderIcon,
|
||||
iconClass: this.renderIconClass,
|
||||
iconClass: this.renderIconClass
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -662,7 +662,7 @@ export default class TagRenderingConfig {
|
|||
key: commonKey,
|
||||
values: Utils.NoNull(
|
||||
values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v)
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -677,7 +677,7 @@ export default class TagRenderingConfig {
|
|||
return {
|
||||
key,
|
||||
type: this.freeform.type,
|
||||
values,
|
||||
values
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not create FreeformValues for tagrendering", this.id)
|
||||
|
@ -734,7 +734,7 @@ export default class TagRenderingConfig {
|
|||
singleSelectedMapping: number,
|
||||
multiSelectedMapping: boolean[] | undefined,
|
||||
currentProperties: Record<string, string>
|
||||
): UploadableTag {
|
||||
): UploadableTag[] {
|
||||
if (typeof freeformValue === "string") {
|
||||
freeformValue = freeformValue?.trim()
|
||||
}
|
||||
|
@ -746,14 +746,18 @@ export default class TagRenderingConfig {
|
|||
if (freeformValue === "") {
|
||||
freeformValue = undefined
|
||||
}
|
||||
if (this.freeform?.postfixDistinguished && freeformValue !== undefined) {
|
||||
if (this.freeform?.postfixDistinguished) {
|
||||
const allValues = currentProperties[this.freeform.key].split(";").map((s) => s.trim())
|
||||
const perPostfix: Record<string, string> = {}
|
||||
for (const value of allValues) {
|
||||
const [v, postfix] = value.split("/")
|
||||
perPostfix[postfix.trim()] = v.trim()
|
||||
}
|
||||
if (freeformValue === "" || freeformValue === undefined) {
|
||||
delete perPostfix[this.freeform.postfixDistinguished]
|
||||
} else {
|
||||
perPostfix[this.freeform.postfixDistinguished] = freeformValue
|
||||
}
|
||||
const keys = Object.keys(perPostfix)
|
||||
keys.sort()
|
||||
freeformValue = keys.map((k) => perPostfix[k] + "/" + k).join("; ")
|
||||
|
@ -778,14 +782,14 @@ export default class TagRenderingConfig {
|
|||
const freeformOnly = { [this.freeform.key]: freeformValue }
|
||||
const matchingMapping = this.mappings?.find((m) => m.if.matchesProperties(freeformOnly))
|
||||
if (matchingMapping) {
|
||||
return new And([matchingMapping.if, ...(matchingMapping.addExtraTags ?? [])])
|
||||
return [matchingMapping.if, ...(matchingMapping.addExtraTags ?? [])]
|
||||
}
|
||||
// Either no mappings, or this is a radio-button selected freeform value
|
||||
const tag = new And([
|
||||
const tag = [
|
||||
new Tag(this.freeform.key, freeformValue),
|
||||
...(this.freeform.addExtraTags ?? []),
|
||||
])
|
||||
const newProperties = tag.applyOn(currentProperties)
|
||||
...(this.freeform.addExtraTags ?? [])
|
||||
]
|
||||
const newProperties = new And(tag).applyOn(currentProperties)
|
||||
if (this.invalidValues?.matchesProperties(newProperties)) {
|
||||
return undefined
|
||||
}
|
||||
|
@ -807,19 +811,14 @@ export default class TagRenderingConfig {
|
|||
selectedMappings.push(
|
||||
new And([
|
||||
new Tag(this.freeform.key, freeformValue),
|
||||
...(this.freeform.addExtraTags ?? []),
|
||||
...(this.freeform.addExtraTags ?? [])
|
||||
])
|
||||
)
|
||||
}
|
||||
const and = TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings])
|
||||
if (and.and.length === 0) {
|
||||
if (and.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
console.log(
|
||||
">>> New properties",
|
||||
TagUtils.asProperties(and, currentProperties),
|
||||
this.invalidValues
|
||||
)
|
||||
if (
|
||||
this.invalidValues?.matchesProperties(TagUtils.asProperties(and, currentProperties))
|
||||
) {
|
||||
|
@ -843,24 +842,23 @@ export default class TagRenderingConfig {
|
|||
!someMappingIsShown ||
|
||||
singleSelectedMapping === undefined)
|
||||
if (useFreeform) {
|
||||
return new And([
|
||||
return [
|
||||
new Tag(this.freeform.key, freeformValue),
|
||||
...(this.freeform.addExtraTags ?? []),
|
||||
])
|
||||
...(this.freeform.addExtraTags ?? [])
|
||||
]
|
||||
} else if (singleSelectedMapping !== undefined) {
|
||||
return new And([
|
||||
return [
|
||||
this.mappings[singleSelectedMapping].if,
|
||||
...(this.mappings[singleSelectedMapping].addExtraTags ?? []),
|
||||
])
|
||||
...(this.mappings[singleSelectedMapping].addExtraTags ?? [])
|
||||
]
|
||||
} else {
|
||||
console.error("TagRenderingConfig.ConstructSpecification has a weird fallback for", {
|
||||
freeformValue,
|
||||
singleSelectedMapping,
|
||||
multiSelectedMapping,
|
||||
currentProperties,
|
||||
useFreeform,
|
||||
useFreeform
|
||||
})
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
@ -892,7 +890,7 @@ export default class TagRenderingConfig {
|
|||
"*" +
|
||||
m.then.textFor(lang) +
|
||||
"* is shown if with " +
|
||||
m.if.asHumanString(true, false, {}),
|
||||
m.if.asHumanString(true, false, {})
|
||||
]
|
||||
|
||||
if (m.hideInAnswer === true) {
|
||||
|
@ -925,7 +923,7 @@ export default class TagRenderingConfig {
|
|||
if (this.labels?.length > 0) {
|
||||
labels = [
|
||||
"This tagrendering has labels ",
|
||||
...this.labels.map((label) => "`" + label + "`"),
|
||||
...this.labels.map((label) => "`" + label + "`")
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
|
@ -938,7 +936,7 @@ export default class TagRenderingConfig {
|
|||
freeform,
|
||||
mappings,
|
||||
condition,
|
||||
labels,
|
||||
labels
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
|
@ -964,11 +962,37 @@ export default class TagRenderingConfig {
|
|||
return Utils.NoNull(tags)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the freeform value that should be initially shown in the question
|
||||
* @param properties
|
||||
*/
|
||||
public initialFreeformValue(properties: Record<string, string>): string {
|
||||
const value = properties[this.freeform.key]
|
||||
if (!value) {
|
||||
return ""
|
||||
}
|
||||
const distinguish = this.freeform.postfixDistinguished
|
||||
if (!distinguish) {
|
||||
return value
|
||||
}
|
||||
const parts = value.split(";")
|
||||
for (const part of parts) {
|
||||
if (part.indexOf("/") < 0) {
|
||||
continue
|
||||
}
|
||||
const [v, denom] = part.split("/").map(s => s.trim())
|
||||
if (denom === distinguish) {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* The keys that should be erased if one has to revert to 'unknown'.
|
||||
* Might give undefined if setting to unknown is not possible
|
||||
*/
|
||||
public removeToSetUnknown(
|
||||
private removeToSetUnknown(
|
||||
partOfLayer: LayerConfig,
|
||||
currentTags: Record<string, string>
|
||||
): string[] | undefined {
|
||||
|
@ -1012,6 +1036,23 @@ export default class TagRenderingConfig {
|
|||
|
||||
return Array.from(toDelete)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives all the tags that should be applied to "reset" the freeform key to an "unknown" state
|
||||
*/
|
||||
public markUnknown(layer: LayerConfig, currentProperties: Record<string, string>): UploadableTag[] {
|
||||
if (this.freeform?.postfixDistinguished) {
|
||||
const allValues = currentProperties[this.freeform.key].split(";").filter(
|
||||
part => part.split("/")[1]?.trim() !== this.freeform.postfixDistinguished
|
||||
)
|
||||
return [new Tag(this.freeform.key, allValues.join(";"))]
|
||||
}
|
||||
|
||||
const keys = this.removeToSetUnknown(layer, currentProperties)
|
||||
|
||||
|
||||
return keys?.map(k => new Tag(k, ""))
|
||||
}
|
||||
}
|
||||
|
||||
export class TagRenderingConfigUtils {
|
||||
|
@ -1050,7 +1091,7 @@ export class TagRenderingConfigUtils {
|
|||
clone.mappings?.map((m) => {
|
||||
const mapping = {
|
||||
...m,
|
||||
priorityIf: m.priorityIf ?? TagUtils.Tag("id~*"),
|
||||
priorityIf: m.priorityIf ?? TagUtils.Tag("id~*")
|
||||
}
|
||||
if (m.if.usedKeys().indexOf("nobrand") < 0) {
|
||||
// Erase 'nobrand=yes', unless this option explicitly sets it
|
||||
|
|
|
@ -2,7 +2,6 @@ import BaseUIElement from "../UI/BaseUIElement"
|
|||
import { Denomination } from "./Denomination"
|
||||
import UnitConfigJson from "./ThemeConfig/Json/UnitConfigJson"
|
||||
import unit from "../../assets/layers/unit/unit.json"
|
||||
import { QuestionableTagRenderingConfigJson } from "./ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||
import TagRenderingConfig from "./ThemeConfig/TagRenderingConfig"
|
||||
import Validators, { ValidatorType } from "../UI/InputElement/Validators"
|
||||
import { Validator } from "../UI/InputElement/Validator"
|
||||
|
@ -14,7 +13,6 @@ export class Unit {
|
|||
public readonly denominationsSorted: Denomination[]
|
||||
public readonly eraseInvalid: boolean
|
||||
public readonly quantity: string
|
||||
private readonly _validator: Validator
|
||||
public readonly inverted: boolean
|
||||
|
||||
constructor(
|
||||
|
@ -26,7 +24,6 @@ export class Unit {
|
|||
inverted: boolean = false
|
||||
) {
|
||||
this.quantity = quantity
|
||||
this._validator = validator
|
||||
if (
|
||||
!inverted &&
|
||||
["string", "text", "key", "icon", "translation", "fediverse", "id"].indexOf(
|
||||
|
@ -97,7 +94,7 @@ export class Unit {
|
|||
tagRenderings: TagRenderingConfig[],
|
||||
ctx: string
|
||||
): Unit[] {
|
||||
let types: Record<string, ValidatorType> = {}
|
||||
const types: Record<string, ValidatorType> = {}
|
||||
for (const tagRendering of tagRenderings) {
|
||||
if (tagRendering.freeform?.type) {
|
||||
types[tagRendering.freeform.key] = tagRendering.freeform.type
|
||||
|
@ -185,7 +182,7 @@ export class Unit {
|
|||
): Unit[] {
|
||||
const appliesTo = json.appliesToKey
|
||||
for (let i = 0; i < (appliesTo ?? []).length; i++) {
|
||||
let key = appliesTo[i]
|
||||
const key = appliesTo[i]
|
||||
if (key.trim() !== key) {
|
||||
throw `${ctx}.appliesToKey[${i}] is invalid: it starts or ends with whitespace`
|
||||
}
|
||||
|
@ -265,7 +262,7 @@ export class Unit {
|
|||
const loaded = this.getFromLibrary(toLoad.quantity, ctx)
|
||||
const quantity = toLoad.quantity
|
||||
|
||||
function fetchDenom(d: string): Denomination {
|
||||
const fetchDenom = (d: string): Denomination => {
|
||||
const found = loaded.denominations.find(
|
||||
(denom) => denom.canonical.toLowerCase() === d
|
||||
)
|
||||
|
|
|
@ -14,6 +14,8 @@ export default class Hotkeys {
|
|||
}[]
|
||||
> = new UIEventSource([])
|
||||
|
||||
private static readonly seenKeys: Set<string> = new Set()
|
||||
|
||||
/**
|
||||
* Register a hotkey
|
||||
* @param key
|
||||
|
@ -51,6 +53,9 @@ export default class Hotkeys {
|
|||
}
|
||||
}
|
||||
|
||||
const keyString = JSON.stringify(key)
|
||||
this.seenKeys.add(keyString)
|
||||
|
||||
this._docs.data.push({ key, documentation, alsoTriggeredBy })
|
||||
this._docs.ping()
|
||||
if (Utils.runningFromConsole) {
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
<slot name="header" />
|
||||
</h1>
|
||||
{/if}
|
||||
<slot name="closebutton" />
|
||||
</svelte:fragment>
|
||||
<slot />
|
||||
{#if $$slots.footer}
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
import ImageOperations from "./ImageOperations.svelte"
|
||||
import Popup from "../Base/Popup.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import type { Feature, Point } from "geojson"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
@ -19,8 +18,9 @@
|
|||
import DotMenu from "../Base/DotMenu.svelte"
|
||||
import LoadingPlaceholder from "../Base/LoadingPlaceholder.svelte"
|
||||
import { MenuState } from "../../Models/MenuState"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
|
||||
export let image: Partial<ProvidedImage>
|
||||
export let image: Partial<ProvidedImage> & { id: string; url: string }
|
||||
let fallbackImage: string = undefined
|
||||
if (image.provider === Mapillary.singleton) {
|
||||
fallbackImage = "./assets/svg/blocked.svg"
|
||||
|
@ -28,7 +28,7 @@
|
|||
|
||||
let imgEl: HTMLImageElement
|
||||
export let imgClass: string = undefined
|
||||
export let state: SpecialVisualizationState = undefined
|
||||
export let state: ThemeViewState = undefined
|
||||
export let attributionFormat: "minimal" | "medium" | "large" = "medium"
|
||||
let previewedImage: UIEventSource<Partial<ProvidedImage>> = MenuState.previewedImage
|
||||
export let canZoom = previewedImage !== undefined
|
||||
|
@ -36,9 +36,7 @@
|
|||
let showBigPreview = new UIEventSource(false)
|
||||
onDestroy(
|
||||
showBigPreview.addCallbackAndRun((shown) => {
|
||||
if (!shown) {
|
||||
previewedImage?.set(undefined)
|
||||
}
|
||||
state.guistate.setPreviewedImage(shown ? image : undefined)
|
||||
})
|
||||
)
|
||||
if (previewedImage) {
|
||||
|
|
|
@ -89,17 +89,6 @@
|
|||
imgClass="max-h-64 w-auto sm:h-32 md:h-64"
|
||||
attributionFormat="minimal"
|
||||
>
|
||||
<!--
|
||||
<div slot="preview-action" class="self-center" >
|
||||
<LoginToggle {state} silentFail={true}>
|
||||
{#if linkable}
|
||||
<label class="normal-background p-2 rounded-full pointer-events-auto">
|
||||
<input bind:checked={$isLinked} type="checkbox" />
|
||||
<SpecialTranslation t={t.link} {tags} {state} {layer} {feature} />
|
||||
</label>
|
||||
{/if}
|
||||
</LoginToggle>
|
||||
</div>-->
|
||||
</AttributedImage>
|
||||
<LoginToggle {state} silentFail={true}>
|
||||
{#if linkable}
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { OsmTags } from "../../Models/OsmFeature"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import NearbyImages from "./NearbyImages.svelte"
|
||||
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import Camera_plus from "../../assets/svg/Camera_plus.svelte"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||
import { Accordion, AccordionItem, Modal } from "flowbite-svelte"
|
||||
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
|
||||
import Popup from "../Base/Popup.svelte"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import { onDestroy } from "svelte"
|
||||
import { MenuState } from "../../Models/MenuState"
|
||||
import { CloseButton } from "flowbite-svelte"
|
||||
|
||||
export let tags: UIEventSource<OsmTags>
|
||||
export let state: SpecialVisualizationState
|
||||
export let state: ThemeViewState
|
||||
export let lon: number
|
||||
export let lat: number
|
||||
export let feature: Feature
|
||||
|
@ -27,6 +24,16 @@
|
|||
|
||||
let enableLogin = state.featureSwitches.featureSwitchEnableLogin
|
||||
export let shown = new UIEventSource(false)
|
||||
onDestroy(MenuState.nearbyImagesFeature.addCallback(something => {
|
||||
if (something !== feature) {
|
||||
shown.set(false)
|
||||
}
|
||||
}))
|
||||
onDestroy(shown.addCallbackAndRun(isShown => {
|
||||
if (isShown) {
|
||||
MenuState.nearbyImagesFeature.set(feature)
|
||||
}
|
||||
}))
|
||||
</script>
|
||||
|
||||
{#if enableLogin.data}
|
||||
|
@ -37,10 +44,9 @@
|
|||
>
|
||||
<Tr t={t.seeNearby} />
|
||||
</button>
|
||||
<Popup {shown} bodyPadding="p-4">
|
||||
<span slot="header">
|
||||
<Tr t={t.seeNearby} />
|
||||
</span>
|
||||
<Popup {shown} bodyPadding="p-4" dismissable={false}>
|
||||
<Tr slot="header" t={t.seeNearby} />
|
||||
<CloseButton slot="closebutton" on:click={() => shown?.set(false)} />
|
||||
<NearbyImages {tags} {state} {lon} {lat} {feature} {linkable} {layer} />
|
||||
</Popup>
|
||||
{/if}
|
||||
|
|
|
@ -24,13 +24,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
"dragRotate",
|
||||
"dragPan",
|
||||
"keyboard",
|
||||
"touchZoomRotate",
|
||||
"touchZoomRotate"
|
||||
]
|
||||
private static maplibre_zoom_handlers = [
|
||||
"scrollZoom",
|
||||
"boxZoom",
|
||||
"doubleClickZoom",
|
||||
"touchZoomRotate",
|
||||
"touchZoomRotate"
|
||||
]
|
||||
readonly location: UIEventSource<{ lon: number; lat: number }>
|
||||
private readonly isFlying = new UIEventSource(false)
|
||||
|
@ -141,7 +141,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
const features = map
|
||||
.queryRenderedFeatures([
|
||||
[point.x - buffer, point.y - buffer],
|
||||
[point.x + buffer, point.y + buffer],
|
||||
[point.x + buffer, point.y + buffer]
|
||||
])
|
||||
.filter((f) => f.source.startsWith("mapcomplete_"))
|
||||
if (features.length === 1) {
|
||||
|
@ -281,9 +281,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
return {
|
||||
map: mlmap,
|
||||
ui: new SvelteUIElement(MaplibreMap, {
|
||||
map: mlmap,
|
||||
map: mlmap
|
||||
}),
|
||||
mapproperties: new MapLibreAdaptor(mlmap),
|
||||
mapproperties: new MapLibreAdaptor(mlmap)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -347,7 +347,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
) {
|
||||
const event = {
|
||||
date: new Date(),
|
||||
key: key,
|
||||
key: key
|
||||
}
|
||||
|
||||
for (let i = 0; i < this._onKeyNavigation.length; i++) {
|
||||
|
@ -536,7 +536,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
const bounds = map.getBounds()
|
||||
const bbox = new BBox([
|
||||
[bounds.getEast(), bounds.getNorth()],
|
||||
[bounds.getWest(), bounds.getSouth()],
|
||||
[bounds.getWest(), bounds.getSouth()]
|
||||
])
|
||||
if (this.bounds.data === undefined || !isSetup) {
|
||||
this.bounds.setData(bbox)
|
||||
|
@ -611,8 +611,11 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
if (!map) {
|
||||
return
|
||||
}
|
||||
console.log("Bounds are", bbox?.asGeometry())
|
||||
if (bbox) {
|
||||
if (GeoOperations.surfaceAreaInSqMeters(bbox.asGeojsonCached()) > 1) {
|
||||
map?.setMaxBounds(bbox.toLngLat())
|
||||
}
|
||||
} else {
|
||||
map?.setMaxBounds(null)
|
||||
}
|
||||
|
@ -730,14 +733,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
type: "raster-dem",
|
||||
url:
|
||||
"https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=" +
|
||||
Constants.maptilerApiKey,
|
||||
Constants.maptilerApiKey
|
||||
})
|
||||
try {
|
||||
while (!map?.isStyleLoaded()) {
|
||||
await Utils.waitFor(250)
|
||||
}
|
||||
map.setTerrain({
|
||||
source: id,
|
||||
source: id
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
@ -762,7 +765,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
if (this.scaleControl === undefined) {
|
||||
this.scaleControl = new ScaleControl({
|
||||
maxWidth: 100,
|
||||
unit: "metric",
|
||||
unit: "metric"
|
||||
})
|
||||
}
|
||||
if (!map.hasControl(this.scaleControl)) {
|
||||
|
@ -775,7 +778,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
window.requestAnimationFrame(() => {
|
||||
this._maplibreMap.data?.flyTo({
|
||||
zoom,
|
||||
center: [lon, lat],
|
||||
center: [lon, lat]
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -590,7 +590,11 @@ export default class ShowDataLayer {
|
|||
}
|
||||
const bbox = BBox.bboxAroundAll(features.map(BBox.get))
|
||||
window.requestAnimationFrame(() => {
|
||||
try {
|
||||
map.resize()
|
||||
} catch (e) {
|
||||
console.error("Could not resize the map in preparation of zoomToCurrentFeatures; the error is:", e)
|
||||
}
|
||||
map.fitBounds(bbox.toLngLat(), {
|
||||
padding: { top: 10, bottom: 10, left: 10, right: 10 },
|
||||
animate: false,
|
||||
|
|
|
@ -3,29 +3,30 @@
|
|||
import FromHtml from "../Base/FromHtml.svelte"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
/**
|
||||
* A 'TagHint' will show the given tags in a human readable form.
|
||||
* Depending on the options, it'll link through to the wiki or might be completely hidden
|
||||
*/
|
||||
export let tags: TagsFilter
|
||||
export let currentProperties: Record<string, string | any> = {}
|
||||
/**
|
||||
* If given, this function will be called to embed the given tags hint into this translation
|
||||
*/
|
||||
export let embedIn: ((string: string) => Translation) | undefined = undefined
|
||||
let tagsExplanation = ""
|
||||
$: tagsExplanation = tags?.asHumanString(true, false, currentProperties)
|
||||
export let tags: TagsFilter[]
|
||||
export let currentProperties: Record<string, string> = {}
|
||||
</script>
|
||||
|
||||
{#if tags?.length > 0}
|
||||
{#each tags as tag}
|
||||
<div class="break-words" style="word-break: break-word">
|
||||
{#if tags === undefined}
|
||||
<slot name="no-tags"><Tr cls="subtle" t={Translations.t.general.noTagsSelected} /></slot>
|
||||
{:else if embedIn === undefined}
|
||||
<FromHtml src={tagsExplanation} />
|
||||
{#if tag["value"] === ""}
|
||||
<del>
|
||||
{tag["key"]}
|
||||
</del>
|
||||
{:else}
|
||||
<Tr t={embedIn(tagsExplanation)} />
|
||||
<FromHtml src={tag.asHumanString(true, false, currentProperties)} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<slot name="no-tags">
|
||||
<Tr cls="subtle" t={Translations.t.general.noTagsSelected} />
|
||||
</slot>
|
||||
{/if}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { ImmutableStore, UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import type { Feature } from "geojson"
|
||||
|
@ -31,7 +31,9 @@
|
|||
import { get } from "svelte/store"
|
||||
import Markdown from "../../Base/Markdown.svelte"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { TagTypes } from "../../../Logic/Tags/TagTypes"
|
||||
import type { UploadableTag } from "../../../Logic/Tags/TagTypes"
|
||||
|
||||
import Popup from "../../Base/Popup.svelte"
|
||||
import If from "../../Base/If.svelte"
|
||||
import DotMenu from "../../Base/DotMenu.svelte"
|
||||
|
@ -43,7 +45,7 @@
|
|||
export let selectedElement: Feature
|
||||
export let state: SpecialVisualizationState
|
||||
export let layer: LayerConfig | undefined
|
||||
export let selectedTags: UploadableTag = undefined
|
||||
export let selectedTags: UploadableTag[] = undefined
|
||||
export let extraTags: UIEventSource<Record<string, string>> = new UIEventSource({})
|
||||
|
||||
export let clss = "interactive border-interactive"
|
||||
|
@ -65,9 +67,9 @@
|
|||
let checkedMappings: boolean[]
|
||||
|
||||
/**
|
||||
* IF set: we can remove the current answer by deleting all those keys
|
||||
* The tags to apply to mark this answer as "unknown"
|
||||
*/
|
||||
let settableKeys = tags.mapD((tags) => config.removeToSetUnknown(layer, tags))
|
||||
let onMarkUnknown: Store<UploadableTag[] | undefined> = tags.mapD((tags) => config.markUnknown(layer, tags))
|
||||
let unknownModal = new UIEventSource(false)
|
||||
|
||||
let searchTerm: UIEventSource<string> = new UIEventSource("")
|
||||
|
@ -118,7 +120,7 @@
|
|||
seenFreeforms.push(newProps[confg.freeform.key])
|
||||
}
|
||||
return matches
|
||||
}),
|
||||
})
|
||||
]
|
||||
|
||||
if (tgs !== undefined && confg.freeform) {
|
||||
|
@ -143,7 +145,7 @@
|
|||
if (confg.freeform?.key) {
|
||||
if (!confg.multiAnswer) {
|
||||
// Somehow, setting multi-answer freeform values is broken if this is not set
|
||||
freeformInput.set(tgs[confg.freeform.key])
|
||||
freeformInput.set(confg.initialFreeformValue(tgs))
|
||||
}
|
||||
} else {
|
||||
freeformInput.set(undefined)
|
||||
|
@ -208,9 +210,10 @@
|
|||
!$freeformInput &&
|
||||
!$freeformInputUnvalidated &&
|
||||
!checkedMappings?.some((m) => m) &&
|
||||
!config.freeform.postfixDistinguished &&
|
||||
$tags[config.freeform.key] // We need to have a current value in order to delete it
|
||||
) {
|
||||
selectedTags = new Tag(config.freeform.key, "")
|
||||
selectedTags = [new Tag(config.freeform.key, "")]
|
||||
} else {
|
||||
try {
|
||||
selectedTags = config?.constructChangeSpecification(
|
||||
|
@ -226,7 +229,7 @@
|
|||
freeform: $freeformInput,
|
||||
selectedMapping,
|
||||
checkedMappings,
|
||||
currentTags: tags.data,
|
||||
currentTags: tags.data
|
||||
},
|
||||
" --> ",
|
||||
selectedTags
|
||||
|
@ -245,10 +248,10 @@
|
|||
// Check the type of selectedTags
|
||||
if (selectedTags instanceof Tag) {
|
||||
// Re-define selectedTags as an And
|
||||
selectedTags = new And([selectedTags, ...extraTagsArray])
|
||||
selectedTags = [selectedTags, ...extraTagsArray]
|
||||
} else if (selectedTags instanceof And) {
|
||||
// Add the extraTags to the existing And
|
||||
selectedTags = new And([...selectedTags.and, ...extraTagsArray])
|
||||
selectedTags = [...TagTypes.uploadableAnd(selectedTags), ...extraTagsArray]
|
||||
} else {
|
||||
console.error(
|
||||
"selectedTags is not of type Tag or And, it is a " + JSON.stringify(selectedTags)
|
||||
|
@ -257,17 +260,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
function onSave(_ = undefined) {
|
||||
function onSave() {
|
||||
if (selectedTags === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const selectedTagsJoined = new And(selectedTags)
|
||||
if (layer === undefined || (layer?.source === null && layer.id !== "favourite")) {
|
||||
/**
|
||||
* This is a special, privileged layer.
|
||||
* We simply apply the tags onto the records
|
||||
*/
|
||||
const kv = selectedTags.asChange(tags.data)
|
||||
const kv = selectedTagsJoined.asChange(tags.data)
|
||||
for (const { k, v } of kv) {
|
||||
if (v === undefined) {
|
||||
// Note: we _only_ delete if it is undefined. We _leave_ the empty string and assign it, so that data consumers get correct information
|
||||
|
@ -278,13 +282,13 @@
|
|||
feedback.setData(undefined)
|
||||
}
|
||||
tags.ping()
|
||||
dispatch("saved", { config, applied: selectedTags })
|
||||
dispatch("saved", { config, applied: selectedTagsJoined })
|
||||
return
|
||||
}
|
||||
dispatch("saved", { config, applied: selectedTags })
|
||||
const change = new ChangeTagAction(tags.data.id, selectedTags, tags.data, {
|
||||
dispatch("saved", { config, applied: selectedTagsJoined })
|
||||
const change = new ChangeTagAction(tags.data.id, selectedTagsJoined, tags.data, {
|
||||
theme: tags.data["_orig_theme"] ?? state.theme?.id,
|
||||
changeType: "answer",
|
||||
changeType: "answer"
|
||||
})
|
||||
freeformInput.set(undefined)
|
||||
selectedMapping = undefined
|
||||
|
@ -325,10 +329,10 @@
|
|||
}
|
||||
|
||||
function clearAnswer() {
|
||||
const tagsToSet = settableKeys.data.map((k) => new Tag(k, ""))
|
||||
const tagsToSet: UploadableTag[] = onMarkUnknown.data
|
||||
const change = new ChangeTagAction(tags.data.id, new And(tagsToSet), tags.data, {
|
||||
theme: tags.data["_orig_theme"] ?? state.theme.id,
|
||||
changeType: "answer",
|
||||
changeType: "answer"
|
||||
})
|
||||
freeformInput.set(undefined)
|
||||
selectedMapping = undefined
|
||||
|
@ -575,13 +579,7 @@
|
|||
>
|
||||
<div class="subtle">
|
||||
<Tr t={Translations.t.unknown.removedKeys} />
|
||||
{#each $settableKeys as key}
|
||||
<code>
|
||||
<del>
|
||||
{key}
|
||||
</del>
|
||||
</code>
|
||||
{/each}
|
||||
<TagHint tags={$onMarkUnknown}></TagHint>
|
||||
</div>
|
||||
</If>
|
||||
<div class="flex w-full justify-end" slot="footer">
|
||||
|
@ -601,7 +599,7 @@
|
|||
</Popup>
|
||||
|
||||
<div class="sticky bottom-0 flex flex-wrap justify-between" style="z-index: 11">
|
||||
{#if $settableKeys && $isKnown && !matchesEmpty}
|
||||
{#if $onMarkUnknown?.length > 0 && $isKnown && !matchesEmpty}
|
||||
<button class="as-link small text-sm" on:click={() => unknownModal.set(true)}>
|
||||
<Tr t={Translations.t.unknown.markUnknown} />
|
||||
</button>
|
||||
|
@ -613,7 +611,13 @@
|
|||
<!-- TagRenderingQuestion-buttons -->
|
||||
<slot name="cancel" />
|
||||
<slot name="save-button" {selectedTags}>
|
||||
{#if config.freeform?.key && !checkedMappings?.some((m) => m) && !$freeformInput && !$freeformInputUnvalidated && $tags[config.freeform.key]}
|
||||
|
||||
<!-- Save-button / delete button -->
|
||||
{#if config.freeform?.key &&
|
||||
!checkedMappings?.some((m) => m) &&
|
||||
!$freeformInput && !$freeformInputUnvalidated
|
||||
&& $tags[config.freeform.key]
|
||||
&& $isKnown}
|
||||
<button
|
||||
class="primary flex"
|
||||
on:click|stopPropagation|preventDefault={() => onSave()}
|
||||
|
@ -635,9 +639,10 @@
|
|||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Taghint + debug info -->
|
||||
{#if UserRelatedState.SHOW_TAGS_VALUES.indexOf($showTags) >= 0 || ($showTags === "" && numberOfCs >= Constants.userJourney.tagsVisibleAt) || $featureSwitchIsTesting || $featureSwitchIsDebugging}
|
||||
<span class="flex flex-wrap justify-between">
|
||||
<TagHint {state} tags={selectedTags} currentProperties={$tags} />
|
||||
<TagHint tags={selectedTags} currentProperties={$tags} />
|
||||
<span class="flex flex-wrap">
|
||||
{#if $featureSwitchIsTesting}
|
||||
<div class="alert" style="padding: 0; margin: 0; margin-right: 0.5rem">
|
||||
|
@ -645,9 +650,9 @@
|
|||
</div>
|
||||
{/if}
|
||||
{#if $featureSwitchIsTesting || $featureSwitchIsDebugging}
|
||||
<a class="small" on:click={() => console.log("Configuration is ", config)}>
|
||||
<button class="small as-link" on:click={() => console.log("Configuration is ", config)}>
|
||||
{config.id}
|
||||
</a>
|
||||
</button>
|
||||
{/if}
|
||||
</span>
|
||||
</span>
|
||||
|
|
|
@ -5,17 +5,14 @@
|
|||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
||||
import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp"
|
||||
import DefaultIcon from "../Map/DefaultIcon.svelte"
|
||||
import { WithSearchState } from "../../Models/ThemeViewState/WithSearchState"
|
||||
|
||||
export let entry: GeocodeResult
|
||||
export let state: SpecialVisualizationState
|
||||
export let state: WithSearchState
|
||||
|
||||
let layer: LayerConfig
|
||||
let tags: UIEventSource<Record<string, string>>
|
||||
|
@ -36,34 +33,15 @@
|
|||
let inView = state.mapProperties.bounds.mapD((bounds) => bounds.contains([entry.lon, entry.lat]))
|
||||
|
||||
function select() {
|
||||
if (entry.boundingbox) {
|
||||
const [lat0, lat1, lon0, lon1] = entry.boundingbox
|
||||
state.mapProperties.bounds.set(
|
||||
new BBox([
|
||||
[lon0, lat0],
|
||||
[lon1, lat1],
|
||||
]).pad(0.01)
|
||||
)
|
||||
} else {
|
||||
state.mapProperties.flyTo(
|
||||
entry.lon,
|
||||
entry.lat,
|
||||
GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17
|
||||
)
|
||||
}
|
||||
if (entry.feature?.properties?.id) {
|
||||
state.selectedElement.set(entry.feature)
|
||||
}
|
||||
state.userRelatedState.recentlyVisitedSearch.add(entry)
|
||||
state.searchState.closeIfFullscreen()
|
||||
state.searchState.applyGeocodeResult(entry)
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class="unstyled link-no-underline searchresult w-full" on:click={() => select()}>
|
||||
<div class="flex w-full items-center gap-y-2 p-2">
|
||||
{#if layer}
|
||||
<div class="h-6">
|
||||
<DefaultIcon {layer} properties={entry.feature.properties} clss="w-6 h-6" />
|
||||
<div class="h-6 w-6">
|
||||
<DefaultIcon {layer} properties={entry.feature.properties} />
|
||||
</div>
|
||||
{:else if entry.category}
|
||||
<Icon
|
||||
|
|
|
@ -367,6 +367,7 @@
|
|||
<div class="flex flex-grow items-center justify-end">
|
||||
<div class="w-full sm:w-64">
|
||||
<Searchbar
|
||||
on:search={() => state.searchState.moveToBestMatch()}
|
||||
value={state.searchState.searchTerm}
|
||||
isFocused={state.searchState.searchIsFocused}
|
||||
/>
|
||||
|
|
Loading…
Add table
Reference in a new issue