forked from MapComplete/MapComplete
Merge branch 'develop' into Robin-patch-1
This commit is contained in:
commit
ad3a0a10cb
40 changed files with 798 additions and 262 deletions
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -985,10 +985,10 @@ export class TagUtils {
|
|||
return ["", "## `" + mode + "` " + doc.name, "", doc.docs, "", ""].join("\n")
|
||||
}),
|
||||
"## " +
|
||||
TagUtils.comparators.map((comparator) => "`" + comparator[0] + "`").join(" ") +
|
||||
" Logical comparators",
|
||||
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,29 +134,49 @@ 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
|
||||
if (ps.menu.data) {
|
||||
ps.menu.set(false)
|
||||
return true
|
||||
}
|
||||
try {
|
||||
|
||||
if (MenuState.previewedImage.data !== undefined) {
|
||||
MenuState.previewedImage.setData(undefined)
|
||||
return true
|
||||
}
|
||||
|
||||
for (const key in ps) {
|
||||
const toggle = ps[key]
|
||||
const wasOpen = toggle.data
|
||||
toggle.setData(false)
|
||||
if (wasOpen) {
|
||||
if (ps.menu.data) {
|
||||
ps.menu.set(false)
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (this._selectedElement.data) {
|
||||
this._selectedElement.setData(undefined)
|
||||
return true
|
||||
|
||||
if (MenuState.previewedImage.data !== undefined) {
|
||||
MenuState.previewedImage.setData(undefined)
|
||||
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
|
||||
toggle.setData(false)
|
||||
if (wasOpen) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (this._selectedElement.data) {
|
||||
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()
|
||||
}
|
||||
perPostfix[this.freeform.postfixDistinguished] = freeformValue
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -888,11 +886,11 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
const msgs: string[] = [
|
||||
icon +
|
||||
" " +
|
||||
"*" +
|
||||
m.then.textFor(lang) +
|
||||
"* is shown if with " +
|
||||
m.if.asHumanString(true, false, {}),
|
||||
" " +
|
||||
"*" +
|
||||
m.then.textFor(lang) +
|
||||
"* is shown if with " +
|
||||
m.if.asHumanString(true, false, {})
|
||||
]
|
||||
|
||||
if (m.hideInAnswer === true) {
|
||||
|
@ -901,7 +899,7 @@ export default class TagRenderingConfig {
|
|||
if (m.ifnot !== undefined) {
|
||||
msgs.push(
|
||||
"Unselecting this answer will add " +
|
||||
m.ifnot.asHumanString(true, false, {})
|
||||
m.ifnot.asHumanString(true, false, {})
|
||||
)
|
||||
}
|
||||
return msgs.join(". ")
|
||||
|
@ -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)
|
||||
|
@ -44,14 +44,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
readonly lastClickLocation: Store<
|
||||
| undefined
|
||||
| {
|
||||
lon: number
|
||||
lat: number
|
||||
mode: "left" | "right" | "middle"
|
||||
/**
|
||||
* The nearest feature from a MapComplete layer
|
||||
*/
|
||||
nearestFeature?: Feature
|
||||
}
|
||||
lon: number
|
||||
lat: number
|
||||
mode: "left" | "right" | "middle"
|
||||
/**
|
||||
* The nearest feature from a MapComplete layer
|
||||
*/
|
||||
nearestFeature?: Feature
|
||||
}
|
||||
>
|
||||
readonly minzoom: UIEventSource<number>
|
||||
readonly maxzoom: UIEventSource<number>
|
||||
|
@ -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) {
|
||||
map?.setMaxBounds(bbox.toLngLat())
|
||||
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(() => {
|
||||
map.resize()
|
||||
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>
|
||||
|
||||
<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} />
|
||||
{:else}
|
||||
<Tr t={embedIn(tagsExplanation)} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if tags?.length > 0}
|
||||
{#each tags as tag}
|
||||
<div class="break-words" style="word-break: break-word">
|
||||
{#if tag["value"] === ""}
|
||||
<del>
|
||||
{tag["key"]}
|
||||
</del>
|
||||
{:else}
|
||||
<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
Add a link
Reference in a new issue