Merge develop

This commit is contained in:
Pieter Vander Vennet 2025-01-21 21:14:11 +01:00
commit 4fdae55c97
206 changed files with 8760 additions and 3718 deletions

View file

@ -15,14 +15,14 @@ export class AllKnownLayoutsLazy {
private readonly dict: Map<string, ThemeConfig> = new Map()
constructor(includeFavouriteLayer = true) {
const paths = ScriptUtils.readDirRecSync("./public/assets/generated/themes/",1)
const paths = ScriptUtils.readDirRecSync("./public/assets/generated/themes/", 1)
for (const path of paths) {
const themeConfigJson = <ThemeConfigJson> JSON.parse(readFileSync(path, "utf8"))
const themeConfigJson = <ThemeConfigJson>JSON.parse(readFileSync(path, "utf8"))
for (const layerId of Constants.added_by_default) {
if (layerId === "favourite" && favourite.id) {
if (includeFavouriteLayer) {
themeConfigJson.layers.push(<LayerConfigJson> favourite)
themeConfigJson.layers.push(<LayerConfigJson>favourite)
}
continue
}

View file

@ -8,10 +8,10 @@ export class AllSharedLayers {
public static getSharedLayersConfigs(): Map<string, LayerConfigJson> {
const sharedLayers = new Map<string, LayerConfigJson>()
for (const layer of known_layers["layers"]) {
if(layer.id === undefined){
if (layer.id === undefined) {
continue
}
sharedLayers.set(layer.id, <any> layer)
sharedLayers.set(layer.id, <any>layer)
}
return sharedLayers

View file

@ -88,7 +88,7 @@ export default class InitialMapPositioning {
return
}
const targetLayer = layoutToUse.getMatchingLayer(osmObject.tags)
if(targetLayer){
if (targetLayer) {
this.zoom.setData(Math.max(this.zoom.data, targetLayer.minzoom))
}
const [lat, lon] = osmObject.centerpoint()

View file

@ -89,11 +89,11 @@ export default class DetermineTheme {
if (themes.length == 0) {
throw "Build failed or running, no layouts are known at all"
}
const themeInfo = themes.find(th => th.id === id)
const themeInfo = themes.find((th) => th.id === id)
if (themeInfo === undefined) {
const alternatives = Utils.sortedByLevenshteinDistance(
id,
themes.map(th => th.id),
themes.map((th) => th.id),
(i) => i
).slice(0, 3)
const msg = `No builtin map theme with name ${layoutId} exists. Perhaps you meant one of ${alternatives.join(
@ -103,7 +103,10 @@ export default class DetermineTheme {
}
// Actually fetch the theme
const config = await Utils.downloadJsonCached<ThemeConfigJson>("./assets/generated/themes/"+id+".json", 1000*60*60*60)
const config = await Utils.downloadJsonCached<ThemeConfigJson>(
"./assets/generated/themes/" + id + ".json",
1000 * 60 * 60 * 60
)
return new ThemeConfig(config, true)
}

View file

@ -22,7 +22,7 @@ export default class AllImageProviders {
...WikimediaImageProvider.commonsPrefixes,
...Mapillary.valuePrefixes,
...AllImageProviders.dontLoadFromPrefixes,
"Category:",
"Category:"
])
private static ImageAttributionSource: ImageProvider[] = [
@ -31,7 +31,7 @@ export default class AllImageProviders {
WikidataImageProvider.singleton,
WikimediaImageProvider.singleton,
Panoramax.singleton,
AllImageProviders.genericImageProvider,
AllImageProviders.genericImageProvider
]
public static apiUrls: string[] = [].concat(
...AllImageProviders.ImageAttributionSource.map((src) => src.apiUrls())
@ -44,7 +44,7 @@ export default class AllImageProviders {
mapillary: Mapillary.singleton,
wikidata: WikidataImageProvider.singleton,
wikimedia: WikimediaImageProvider.singleton,
panoramax: Panoramax.singleton,
panoramax: Panoramax.singleton
}
public static byName(name: string) {
@ -67,10 +67,33 @@ export default class AllImageProviders {
}
private static readonly _cachedImageStores: Record<string, Store<ProvidedImage[]>> = {}
/**
* Does a guess on the number of images that are probably there.
* Will simply count all image tags
*
* AllImageProviders.estimateNumberOfImages({image:"abc", "mapillary": "123", "panoramax:0": "xyz"}) // => 3
*
*/
public static estimateNumberOfImages(tags: Record<string, string>, prefixes: string[] = undefined): number {
let count = 0
const allPrefixes = Utils.Dedup(prefixes ?? [].concat(...AllImageProviders.ImageAttributionSource.map(s => s.defaultKeyPrefixes)))
for (const prefix of allPrefixes) {
for (const k in tags) {
if (k === prefix || k.startsWith(prefix + ":")) {
count++
continue
}
}
}
return count
}
/**
* Tries to extract all image data for this image. Cached on tags?.data?.id
*/
public static LoadImagesFor(
public static loadImagesFor(
tags: Store<Record<string, string>>,
tagKey?: string[]
): Store<ProvidedImage[]> {
@ -108,11 +131,11 @@ export default class AllImageProviders {
*/
public static loadImagesFrom(urls: string[]): Store<ProvidedImage[]> {
const tags = {
id: urls.join(";"),
id: urls.join(";")
}
for (let i = 0; i < urls.length; i++) {
tags["image:" + i] = urls[i]
}
return this.LoadImagesFor(new ImmutableStore(tags))
return this.loadImagesFor(new ImmutableStore(tags))
}
}

View file

@ -149,7 +149,7 @@ export default class PanoramaxImageProvider extends ImageProvider {
)
}
Stores.Chronic(1500, () => hasLoading(source.data)).addCallback(() => {
Stores.Chronic(5000, () => hasLoading(source.data)).addCallback(() => {
super.getRelevantUrlsFor(tags, prefixes).then((data) => {
source.set(data)
return !hasLoading(data)

View file

@ -610,8 +610,10 @@ export class OsmConnection {
if (!(this.apiIsOnline.data === "unreachable" || this.apiIsOnline.data === "offline")) {
return
}
if (!this.isLoggedIn.data) {
return
}
try {
console.log("Api is offline - trying to reconnect...")
this.AttemptLogin()
} catch (e) {
console.log("Could not login due to", e)

View file

@ -1,8 +1,8 @@
import { Store, UIEventSource } from "../UIEventSource"
import { OsmConnection } from "./OsmConnection"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import OSMAuthInstance = OSMAuth.osmAuth
import { Utils } from "../../Utils"
import OSMAuthInstance = OSMAuth.osmAuth
export class OsmPreferences {
/**

View file

@ -124,7 +124,7 @@ export class And extends TagsFilter {
* t0.shadows(t0) // => true
* t1.shadows(t1) // => true
* t2.shadows(t2) // => true
* t0.shadows(t1) // => false
* t0.shadows(t1) // => true
* t0.shadows(t2) // => false
* t1.shadows(t0) // => false
* t1.shadows(t2) // => false
@ -135,33 +135,34 @@ export class And extends TagsFilter {
* const t1 = new And([new Tag("shop","clothes"), new Or([new Tag("brand","XYZ"),new Tag("brand:wikidata","Q1234")])])
* const t2 = new And([new RegexTag("shop","mall",true), new Or([TagUtils.Tag("shop~*"), new Tag("craft","shoemaker")])])
* t1.shadows(t2) // => true
*
* const t1 = new Tag("a","b")
* const t2 = new And([new Tag("x","y"), new Tag("a","b")])
* t2.shadows(t1) // => true
* t1.shadows(t2) // => false
*/
shadows(other: TagsFilter): boolean {
const phrases: TagsFilter[] = other instanceof And ? other.and : [other];
// The phrases of the _other_ and
const phrases: readonly TagsFilter[] = other instanceof And ? other.and : [other]
// A phrase might be shadowed by a certain subsection. We keep track of this here
const shadowedOthers = phrases.map(() => false)
for (const selfTag of this.and) {
let shadowsSome = false;
let shadowsAll = true;
for (let i = 0; i < phrases.length; i++){
let shadowsAll = true
for (let i = 0; i < phrases.length; i++) {
const otherTag = phrases[i]
const doesShadow = selfTag.shadows(otherTag)
if(doesShadow){
shadowedOthers[i] = true;
if (doesShadow) {
shadowedOthers[i] = true
}
shadowsSome ||= doesShadow;
shadowsAll &&= doesShadow;
shadowsAll &&= doesShadow
}
// If A => X and A => Y, then
// A&B implies X&Y. We discovered an A that implies all needed values
if (shadowsAll) {
return true;
}
if (!shadowsSome) {
return false;
return true
}
}
return !shadowedOthers.some(v => !v);
return !shadowedOthers.some((v) => !v)
}
usedKeys(): string[] {

View file

@ -83,7 +83,6 @@ export class Or extends TagsFilter {
return false
}
shadows(other: TagsFilter): boolean {
if (other instanceof Or) {
for (const selfTag of this.or) {

View file

@ -150,11 +150,11 @@ export class Tag extends TagsFilter {
return other.matchesProperties({ [this.key]: this.value })
}
}
if(other instanceof Or){
return other.or.some(other => this.shadows(other))
if (other instanceof Or) {
return other.or.some((other) => this.shadows(other))
}
if(other instanceof And){
return !other.and.some(other => !this.shadows(other))
if (other instanceof And) {
return !other.and.some((other) => !this.shadows(other))
}
return false
}

View file

@ -133,11 +133,11 @@ 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" +
@ -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" +
@ -246,7 +246,7 @@ export class TagUtils {
static asProperties(
tags: TagsFilter | TagsFilter[],
baseproperties: Record<string, string> = {},
baseproperties: Record<string, string> = {}
) {
if (Array.isArray(tags)) {
tags = new And(tags)
@ -274,11 +274,11 @@ export class TagUtils {
static SplitKeysRegex(tagsFilters: UploadableTag[], allowRegex: false): Record<string, string[]>
static SplitKeysRegex(
tagsFilters: UploadableTag[],
allowRegex: boolean,
allowRegex: boolean
): Record<string, (string | RegexTag)[]>
static SplitKeysRegex(
tagsFilters: UploadableTag[],
allowRegex: boolean,
allowRegex: boolean
): Record<string, (string | RegexTag)[]> {
const keyValues: Record<string, (string | RegexTag)[]> = {}
tagsFilters = [...tagsFilters] // copy all, use as queue
@ -307,7 +307,7 @@ export class TagUtils {
if (typeof key !== "string") {
console.error(
"Invalid type to flatten the multiAnswer: key is a regex too",
tagsFilter,
tagsFilter
)
throw "Invalid type to FlattenMultiAnswer: key is a regex too"
}
@ -508,7 +508,7 @@ export class TagUtils {
public static Tag(json: TagConfigJson, context?: string | ConversionContext): TagsFilterClosed
public static Tag(
json: TagConfigJson,
context: string | ConversionContext = "",
context: string | ConversionContext = ""
): TagsFilterClosed {
try {
const ctx = typeof context === "string" ? context : context.path.join(".")
@ -540,7 +540,7 @@ export class TagUtils {
throw `Error at ${context}: detected a non-uploadable tag at a location where this is not supported: ${t.asHumanString(
false,
false,
{},
{}
)}`
})
@ -661,7 +661,7 @@ export class TagUtils {
*/
public static removeShadowedElementsFrom(
blacklist: TagsFilter[],
listToFilter: TagsFilter[],
listToFilter: TagsFilter[]
): TagsFilter[] {
return listToFilter.filter((tf) => !blacklist.some((guard) => guard.shadows(tf)))
}
@ -690,15 +690,22 @@ export class TagUtils {
return result
}
public static removeKnownParts(tag: TagsFilter, known: TagsFilter, valueOfKnown = true): TagsFilter | boolean{
/**
* TagUtils.removeKnownParts(TagUtils.Tag({and: ["vending=excrement_bag"}),TagUtils.Tag({and: ["amenity=waste_basket", "vending=excrement_bag"]}), true) // => true
*/
public static removeKnownParts(
tag: TagsFilter,
known: TagsFilter,
valueOfKnown = true
): TagsFilter | boolean {
const tagOrBool = And.construct([tag]).optimize()
if(tagOrBool === true || tagOrBool === false){
if (tagOrBool === true || tagOrBool === false) {
return tagOrBool
}
if(tagOrBool instanceof And){
if (tagOrBool instanceof And) {
return tagOrBool.removePhraseConsideredKnown(known, valueOfKnown)
}
return tagOrBool
return new And([tagOrBool]).removePhraseConsideredKnown(known, valueOfKnown)
}
/**
@ -710,7 +717,7 @@ export class TagUtils {
*/
public static containsEquivalents(
guards: ReadonlyArray<TagsFilter>,
listToFilter: ReadonlyArray<TagsFilter>,
listToFilter: ReadonlyArray<TagsFilter>
): boolean {
return listToFilter.some((tf) => guards.some((guard) => guard.shadows(tf)))
}
@ -754,7 +761,7 @@ export class TagUtils {
values.push(i + "")
}
return values
}),
})
)
return Utils.NoNull(spec)
}
@ -762,13 +769,13 @@ export class TagUtils {
private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilterClosed {
if (json === undefined) {
throw new Error(
`Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression`,
`Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression`
)
}
if (typeof json != "string") {
if (json["and"] !== undefined && json["or"] !== undefined) {
throw `${context}: Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined. Did you override a value? Perhaps use \`"=parent": { ... }\` instead of "parent": {...}\` to trigger a replacement and not a fuse of values. The value is ${JSON.stringify(
json,
json
)}`
}
if (json["and"] !== undefined) {
@ -850,13 +857,13 @@ export class TagUtils {
return new RegexTag(
withRegex.key,
new RegExp(".+", "si" + withRegex.modifier),
withRegex.invert,
withRegex.invert
)
}
return new RegexTag(
withRegex.key,
new RegExp("^(" + value + ")$", "s" + withRegex.modifier),
withRegex.invert,
withRegex.invert
)
}
@ -978,16 +985,15 @@ 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,
].join("\n")
}
static fromProperties(tags: Record<string, string>): TagConfigJson | boolean {
const opt = new And(Object.keys(tags).map(k => new Tag(k, tags[k]))).optimize()
const opt = new And(Object.keys(tags).map((k) => new Tag(k, tags[k]))).optimize()
if (opt === true || opt === false) {
return opt
}

View file

@ -49,12 +49,11 @@ export interface NSIItem {
include: string[]
exclude: string[]
}
readonly tags: Readonly<Record<string, string>>
readonly fromTemplate?: boolean
readonly tags: Readonly<Record<string, string>>
readonly fromTemplate?: boolean
}
export default class NameSuggestionIndex {
public static readonly supportedTypes = ["brand", "flag", "operator", "transit"] as const
private readonly nsiFile: Readonly<NSIFile>
private readonly nsiWdFile: Readonly<
@ -80,7 +79,7 @@ export default class NameSuggestionIndex {
}
>
>,
features: Readonly<FeatureCollection>,
features: Readonly<FeatureCollection>
) {
this.nsiFile = nsiFile
this.nsiWdFile = nsiWdFile
@ -94,11 +93,17 @@ export default class NameSuggestionIndex {
return NameSuggestionIndex.inited
}
const [nsi, nsiWd, features] = await Promise.all(
["./assets/data/nsi/nsi.min.json", "./assets/data/nsi/wikidata.min.json", "./assets/data/nsi/featureCollection.min.json"].map((url) =>
Utils.downloadJsonCached(url, 1000 * 60 * 60 * 24 * 30),
),
[
"./assets/data/nsi/nsi.min.json",
"./assets/data/nsi/wikidata.min.json",
"./assets/data/nsi/featureCollection.min.json",
].map((url) => Utils.downloadJsonCached(url, 1000 * 60 * 60 * 24 * 30))
)
NameSuggestionIndex.inited = new NameSuggestionIndex(
<any>nsi,
<any>nsiWd["wikidata"],
<any>features
)
NameSuggestionIndex.inited = new NameSuggestionIndex(<any>nsi, <any>nsiWd["wikidata"], <any>features)
return NameSuggestionIndex.inited
}
@ -129,13 +134,13 @@ export default class NameSuggestionIndex {
try {
return Utils.downloadJsonCached<Record<string, number>>(
`./assets/data/nsi/stats/${type}.${c.toUpperCase()}.json`,
24 * 60 * 60 * 1000,
24 * 60 * 60 * 1000
)
} catch (e) {
console.error("Could not fetch " + type + " statistics due to", e)
return undefined
}
}),
})
)
stats = Utils.NoNull(stats)
if (stats.length === 1) {
@ -183,10 +188,13 @@ export default class NameSuggestionIndex {
* If set, sort by frequency instead of alphabetically
*/
sortByFrequency: boolean
},
}
): Promise<Mapping[]> {
const mappings: (Mapping & { frequency: number })[] = []
const frequencies = country !== undefined ? await NameSuggestionIndex.fetchFrequenciesFor(type, country) : {}
const frequencies =
country !== undefined
? await NameSuggestionIndex.fetchFrequenciesFor(type, country)
: {}
for (const key in tags) {
if (key.startsWith("_")) {
continue
@ -197,7 +205,7 @@ export default class NameSuggestionIndex {
key,
value,
country.join(";"),
location,
location
)
if (!actualBrands) {
continue
@ -242,7 +250,7 @@ export default class NameSuggestionIndex {
}
public supportedTags(
type: "operator" | "brand" | "flag" | "transit" | string,
type: "operator" | "brand" | "flag" | "transit" | string
): Record<string, string[]> {
const tags: Record<string, string[]> = {}
const keys = Object.keys(this.nsiFile.nsi)
@ -287,10 +295,10 @@ export default class NameSuggestionIndex {
type: string,
tags: { key: string; value: string }[],
country: string = undefined,
location: [number, number] = undefined,
location: [number, number] = undefined
): NSIItem[] {
return tags.flatMap((tag) =>
this.getSuggestionsForKV(type, tag.key, tag.value, country, location),
this.getSuggestionsForKV(type, tag.key, tag.value, country, location)
)
}
@ -313,7 +321,7 @@ export default class NameSuggestionIndex {
key: string,
value: string,
country: string = undefined,
location: [number, number] = undefined,
location: [number, number] = undefined
): NSIItem[] {
const path = `${type}s/${key}/${value}`
const entry = this.nsiFile.nsi[path]
@ -353,8 +361,7 @@ export default class NameSuggestionIndex {
}
const key = i.locationSet.include?.join(";") + "-" + i.locationSet.exclude?.join(";")
const fromCache = NameSuggestionIndex.resolvedSets[key]
const resolvedSet =
fromCache ?? this.loco.resolveLocationSet(i.locationSet)
const resolvedSet = fromCache ?? this.loco.resolveLocationSet(i.locationSet)
if (!fromCache) {
NameSuggestionIndex.resolvedSets[key] = resolvedSet
}
@ -377,13 +384,12 @@ export default class NameSuggestionIndex {
center: [number, number],
options: {
sortByFrequency: boolean
},
}
): Promise<Mapping[]> {
const nsi = await NameSuggestionIndex.getNsiIndex()
return nsi.generateMappings(key, tags, country, center, options)
}
/**
* Where can we find the URL on the world wide web?
* Probably facebook! Don't use in the website, might expose people
@ -402,7 +408,7 @@ export default class NameSuggestionIndex {
}
return icon
}
private static readonly brandPrefix = ["name", "alt_name", "operator","brand"] as const
private static readonly brandPrefix = ["name", "alt_name", "operator", "brand"] as const
/**
* An NSI-item might have tags such as `name=X`, `alt_name=brand X`, `brand=X`, `brand:wikidata`, `shop=Y`, `service:abc=yes`
@ -412,12 +418,14 @@ export default class NameSuggestionIndex {
*
* (More of an extension method on NSIItem)
*/
static asFilterTags(item: NSIItem): string | { and: TagConfigJson[] } | { or: TagConfigJson[] } {
static asFilterTags(
item: NSIItem
): string | { and: TagConfigJson[] } | { or: TagConfigJson[] } {
let brandDetection: string[] = []
let required: string[] = []
const tags: Record<string, string> = item.tags
for (const k in tags) {
if (NameSuggestionIndex.brandPrefix.some(br => k === br || k.startsWith(br + ":"))) {
if (NameSuggestionIndex.brandPrefix.some((br) => k === br || k.startsWith(br + ":"))) {
brandDetection.push(k + "=" + tags[k])
} else {
required.push(k + "=" + tags[k])

View file

@ -40,7 +40,7 @@ export default class TagInfo {
let url: string
if (value) {
url = `${this._backend}api/4/tag/stats?key=${encodeURIComponent(
key,
key
)}&value=${encodeURIComponent(value)}`
} else {
url = `${this._backend}api/4/key/stats?key=${encodeURIComponent(key)}`
@ -70,10 +70,10 @@ export default class TagInfo {
}
const countriesFC: FeatureCollection = await Utils.downloadJsonCached<FeatureCollection>(
"https://download.geofabrik.de/index-v1-nogeom.json",
24 * 1000 * 60 * 60,
24 * 1000 * 60 * 60
)
TagInfo._geofabrikCountries = countriesFC.features.map(
(f) => <GeofabrikCountryProperties>f.properties,
(f) => <GeofabrikCountryProperties>f.properties
)
return TagInfo._geofabrikCountries
}
@ -99,7 +99,7 @@ export default class TagInfo {
private static async getDistributionsFor(
countryCode: string,
key: string,
value?: string,
value?: string
): Promise<TagInfoStats> {
if (!countryCode) {
return undefined
@ -111,7 +111,16 @@ export default class TagInfo {
try {
return await ti.getStats(key, value)
} catch (e) {
console.warn("Could not fetch info from taginfo for", countryCode, key, value, "due to", e, "Taginfo country specific instance is ", ti._backend)
console.warn(
"Could not fetch info from taginfo for",
countryCode,
key,
value,
"due to",
e,
"Taginfo country specific instance is ",
ti._backend
)
return undefined
}
}
@ -127,9 +136,9 @@ export default class TagInfo {
*/
public static async getGlobalDistributionsFor<T>(
writeInto: Record<string, T>,
f: ((stats: TagInfoStats) => T),
f: (stats: TagInfoStats) => T,
key: string,
value?: string,
value?: string
): Promise<number> {
const countriesAll = await this.geofabrikCountries()
const countries = countriesAll
@ -138,14 +147,14 @@ export default class TagInfo {
let downloaded = 0
for (const country of countries) {
if(writeInto[country] !== undefined){
if (writeInto[country] !== undefined) {
continue
}
const r = await TagInfo.getDistributionsFor(country, key, value)
if(r === undefined){
if (r === undefined) {
continue
}
downloaded ++
downloaded++
writeInto[country] = f(r)
}
return downloaded

View file

@ -13,49 +13,96 @@ import { Or } from "../../../Logic/Tags/Or"
import Translations from "../../../UI/i18n/Translations"
import { FlatTag, OptimizedTag, TagsFilterClosed } from "../../../Logic/Tags/TagTypes"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import { And } from "../../../Logic/Tags/And"
import { Translation } from "../../../UI/i18n/Translation"
export class PruneFilters extends DesugaringStep<LayerConfigJson>{
export class PruneFilters extends DesugaringStep<LayerConfigJson> {
constructor() {
super("Removes all filters which are impossible, e.g. because they conflict with the base tags", ["filter"],"PruneFilters")
super(
"Removes all filters which are impossible, e.g. because they conflict with the base tags",
["filter"],
"PruneFilters"
)
}
private prune(sourceTags:FlatTag, filter: FilterConfigJson, context: ConversionContext): FilterConfigJson{
if(!filter.strict){
/**
* Prunes a filter; returns null/undefined if keeping the filter is useless
*/
private prune(
sourceTags: FlatTag,
filter: FilterConfigJson,
context: ConversionContext
): FilterConfigJson {
if (filter.options.length === 1) {
const option = filter.options[0]
const tags = TagUtils.Tag(option.osmTags)
const optimized = TagUtils.removeKnownParts(tags, sourceTags, true)
if (optimized === true) {
context.warn("Removing filter as always known: ", new Translation(option.question).textFor("en"))
return undefined
}
if (optimized === false) {
context.warn("Removing filter as not possible: ", new Translation(option.question).textFor("en"))
return undefined
}
}
if (!filter.strict) {
return filter
}
const countBefore = filter.options.length
const newOptions: FilterConfigOptionJson[] = filter.options.filter(option => {
if(!option.osmTags){
return true
}
const condition = <OptimizedTag & TagsFilterClosed> TagUtils.Tag(option.osmTags).optimize()
return condition.shadows(sourceTags);
}).map(option => {
if(!option.osmTags){
return option
}
let basetags = TagUtils.Tag(option.osmTags)
return {...option, osmTags: (<TagsFilter>TagUtils.removeKnownParts(basetags ,sourceTags)).asJson()}
})
const newOptions: FilterConfigOptionJson[] = filter.options
.filter((option) => {
if (!option.osmTags) {
return true
}
const condition = <OptimizedTag & TagsFilterClosed>(
TagUtils.Tag(option.osmTags).optimize()
)
return condition.shadows(sourceTags)
})
.map((option) => {
if (!option.osmTags) {
return option
}
const basetags = TagUtils.Tag(option.osmTags)
return {
...option,
osmTags: (<TagsFilter>TagUtils.removeKnownParts(basetags, sourceTags)).asJson(),
}
})
const countAfter = newOptions.length
if(countAfter !== countBefore){
context.enters("filter", filter.id ).info("Pruned "+(countBefore-countAfter)+" options away from filter (out of "+countBefore+")")
if (countAfter !== countBefore) {
context
.enters("filter", filter.id)
.info(
"Pruned " +
(countBefore - countAfter) +
" options away from filter (out of " +
countBefore +
")"
)
}
return {...filter, options: newOptions, strict: undefined}
return { ...filter, options: newOptions, strict: undefined }
}
public convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
if(!Array.isArray(json.filter) || typeof json.source === "string"){
if (!Array.isArray(json.filter) || typeof json.source === "string") {
return json
}
if(!json.source["osmTags"]){
if (!json.source["osmTags"]) {
return json
}
const sourceTags = TagUtils.Tag(json.source["osmTags"])
return {...json, filter: json.filter?.map(obj => this.prune(sourceTags, <FilterConfigJson> obj, context))}
return {
...json,
filter: Utils.NoNull(json.filter?.map((obj) =>
this.prune(sourceTags, <FilterConfigJson>obj, context)
)),
}
}
}
export class ExpandFilter extends DesugaringStep<LayerConfigJson> {
@ -69,7 +116,7 @@ export class ExpandFilter extends DesugaringStep<LayerConfigJson> {
"If the string is formatted 'layername.filtername, it will be looked up into that layer instead. Note that pruning should still be done",
].join(" "),
["filter"],
"ExpandFilter",
"ExpandFilter"
)
this._state = state
}
@ -84,11 +131,11 @@ export class ExpandFilter extends DesugaringStep<LayerConfigJson> {
public static buildFilterFromTagRendering(
tr: TagRenderingConfigJson,
context: ConversionContext,
context: ConversionContext
): FilterConfigJson {
if (!(tr.mappings?.length >= 1)) {
context.err(
"Found a matching tagRendering to base a filter on, but this tagRendering does not contain any mappings",
"Found a matching tagRendering to base a filter on, but this tagRendering does not contain any mappings"
)
}
const qtr = <QuestionableTagRenderingConfigJson>tr
@ -103,7 +150,7 @@ export class ExpandFilter extends DesugaringStep<LayerConfigJson> {
if (qtr.multiAnswer && osmTags instanceof Tag) {
osmTags = new RegexTag(
osmTags.key,
new RegExp("^(.+;)?" + osmTags.value + "(;.+)$", "is"),
new RegExp("^(.+;)?" + osmTags.value + "(;.+)$", "is")
)
}
if (mapping.alsoShowIf) {
@ -161,7 +208,7 @@ export class ExpandFilter extends DesugaringStep<LayerConfigJson> {
if (matchingTr) {
const filter = ExpandFilter.buildFilterFromTagRendering(
matchingTr,
context.enters("filter", i),
context.enters("filter", i)
)
newFilters.push(filter)
continue
@ -175,7 +222,7 @@ export class ExpandFilter extends DesugaringStep<LayerConfigJson> {
const split = filter.split(".")
if (split.length > 2) {
context.err(
"invalid filter name: " + filter + ", expected `layername.filterid`",
"invalid filter name: " + filter + ", expected `layername.filterid`"
)
}
const layer = this._state.sharedLayers.get(split[0])
@ -184,7 +231,7 @@ export class ExpandFilter extends DesugaringStep<LayerConfigJson> {
}
const expectedId = split[1]
const expandedFilter = (<(FilterConfigJson | string)[]>layer.filter).find(
(f) => typeof f !== "string" && f.id === expectedId,
(f) => typeof f !== "string" && f.id === expectedId
)
if (expandedFilter === undefined) {
context.err("Did not find filter with name " + filter)
@ -199,15 +246,15 @@ export class ExpandFilter extends DesugaringStep<LayerConfigJson> {
const suggestions = Utils.sortedByLevenshteinDistance(
filter,
Array.from(ExpandFilter.predefinedFilters.keys()),
(t) => t,
(t) => t
)
context
.enter(filter)
.err(
"While searching for predefined filter " +
filter +
": this filter is not found. Perhaps you meant one of: " +
suggestions,
filter +
": this filter is not found. Perhaps you meant one of: " +
suggestions
)
}
newFilters.push(found)

View file

@ -11,9 +11,9 @@ export class ExpandTagRendering extends Conversion<
| string
| TagRenderingConfigJson
| {
builtin: string | string[]
override: any
},
builtin: string | string[]
override: any
},
TagRenderingConfigJson[]
> {
private readonly _state: DesugaringContext
@ -35,12 +35,12 @@ export class ExpandTagRendering extends Conversion<
noHardcodedStrings?: false | boolean
// If set, a question will be added to the 'sharedTagRenderings'. Should only be used for 'questions.json'
addToContext?: false | boolean
},
}
) {
super(
"Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question and reusing the builtins",
[],
"ExpandTagRendering",
"ExpandTagRendering"
)
this._state = state
this._self = self
@ -59,11 +59,13 @@ export class ExpandTagRendering extends Conversion<
}
public convert(
spec: string | any,
ctx: ConversionContext,
spec: string | { "builtin": string | string[] } | (TagRenderingConfigJson),
ctx: ConversionContext
): QuestionableTagRenderingConfigJson[] {
const trs = this.convertOnce(spec, ctx)
?.map(tr => this.pruneMappings<TagRenderingConfigJson & { id: string }>(tr, ctx))
const trs = this.convertOnce(<any>spec, ctx)?.map((tr) =>
this.pruneMappings<TagRenderingConfigJson & { id: string }>(tr, ctx)
)
if (!Array.isArray(trs)) {
ctx.err("Result of lookup for " + spec + " is not iterable; got " + trs)
return undefined
@ -71,8 +73,9 @@ export class ExpandTagRendering extends Conversion<
const result = []
for (const tr of trs) {
if (typeof tr === "string" || tr["builtin"] !== undefined) {
const stable = this.convert(tr, ctx.inOperation("recursive_resolve"))
.map(tr => this.pruneMappings(tr, ctx))
const stable = this.convert(tr, ctx.inOperation("recursive_resolve")).map((tr) =>
this.pruneMappings(tr, ctx)
)
result.push(...stable)
if (this._options?.addToContext) {
for (const tr of stable) {
@ -90,49 +93,60 @@ export class ExpandTagRendering extends Conversion<
return result
}
private pruneMappings<T extends (TagRenderingConfigJson & {
id: string
})>(tagRendering: T, ctx: ConversionContext): T {
private pruneMappings<
T extends TagRenderingConfigJson & {
id: string
}
>(tagRendering: T, ctx: ConversionContext): T {
if (!tagRendering["strict"]) {
return tagRendering
}
if(!this._self.source["osmTags"]){
if (!this._self.source["osmTags"]) {
return tagRendering
}
ctx.inOperation("expandTagRendering:pruning").enters(tagRendering.id)
.info(`PRUNING! Tagrendering to prune: ${tagRendering.id} in the context of layer ${this._self.id} Sourcetags: ${this._self.source["osmTags"]}`)
ctx.inOperation("expandTagRendering:pruning")
.enters(tagRendering.id)
.info(
`PRUNING! Tagrendering to prune: ${tagRendering.id} in the context of layer ${this._self.id} Sourcetags: ${this._self.source["osmTags"]}`
)
const before = tagRendering.mappings?.length ?? 0
const alwaysTags = TagUtils.Tag(this._self.source["osmTags"])
const newMappings = tagRendering.mappings?.filter(mapping => {
const condition = TagUtils.Tag(mapping.if)
return condition.shadows(alwaysTags);
}).map(mapping => {
const newIf = TagUtils.removeKnownParts(
TagUtils.Tag(mapping.if), alwaysTags)
if (typeof newIf === "boolean") {
throw "Invalid removeKnownParts"
}
return {
...mapping,
if: newIf.asJson(),
}
})
const newMappings = tagRendering.mappings
?.filter((mapping) => {
const condition = TagUtils.Tag(mapping.if)
return condition.shadows(alwaysTags)
})
.map((mapping) => {
const newIf = TagUtils.removeKnownParts(TagUtils.Tag(mapping.if), alwaysTags)
if (typeof newIf === "boolean") {
throw "Invalid removeKnownParts"
}
return {
...mapping,
if: newIf.asJson()
}
})
const after = newMappings?.length ?? 0
if (before - after > 0) {
ctx.info(`Pruned mappings for ${tagRendering.id}, from ${before} to ${after} (removed ${before - after})`)
ctx.info(
`Pruned mappings for ${tagRendering.id}, from ${before} to ${after} (removed ${
before - after
})`
)
}
const tr = {
...tagRendering,
mappings: newMappings,
mappings: newMappings
}
delete tr["strict"]
return tr
}
private lookup(name: string, ctx: ConversionContext): (TagRenderingConfigJson & { id: string })[] | undefined {
private lookup(
name: string,
ctx: ConversionContext
): (TagRenderingConfigJson & { id: string })[] | undefined {
const direct = this.directLookup(name)
if (direct === undefined) {
@ -142,17 +156,17 @@ export class ExpandTagRendering extends Conversion<
for (const tagRenderingConfigJson of direct) {
const nm: string | string[] | undefined = tagRenderingConfigJson["builtin"]
if (nm !== undefined) {
let indirect: TagRenderingConfigJson[]
let indirect: (TagRenderingConfigJson & { id: string })[]
if (typeof nm === "string") {
indirect = this.lookup(nm, ctx)
} else {
indirect = [].concat(...nm.map((n) => this.lookup(n, ctx)))
}
for (let foundTr of indirect) {
foundTr = Utils.Clone<any>(foundTr)
foundTr = Utils.Clone(foundTr)
ctx.MergeObjectsForOverride(tagRenderingConfigJson["override"] ?? {}, foundTr)
foundTr["id"] = tagRenderingConfigJson["id"] ?? foundTr["id"]
result.push(<any>foundTr)
result.push(foundTr)
}
} else {
result.push(tagRenderingConfigJson)
@ -202,9 +216,11 @@ export class ExpandTagRendering extends Conversion<
matchingTrs = layerTrs.filter((tr) => tr["id"] === id || tr["labels"]?.indexOf(id) >= 0)
}
const contextWriter = new AddContextToTranslations<TagRenderingConfigJson & { id: string }>("layers:")
const contextWriter = new AddContextToTranslations<TagRenderingConfigJson & { id: string }>(
"layers:"
)
for (let i = 0; i < matchingTrs.length; i++) {
let found: (TagRenderingConfigJson & { id: string }) = Utils.Clone(matchingTrs[i])
let found: TagRenderingConfigJson & { id: string } = Utils.Clone(matchingTrs[i])
if (this._options?.applyCondition) {
// The matched tagRenderings are 'stolen' from another layer. This means that they must match the layer condition before being shown
if (typeof layer.source !== "string") {
@ -220,8 +236,8 @@ export class ExpandTagRendering extends Conversion<
found,
ConversionContext.construct(
[layer.id, "tagRenderings", found["id"]],
["AddContextToTranslations"],
),
["AddContextToTranslations"]
)
)
matchingTrs[i] = found
}
@ -232,9 +248,16 @@ export class ExpandTagRendering extends Conversion<
return undefined
}
private convertOnce(tr: string | any, ctx: ConversionContext): (TagRenderingConfigJson & { id: string })[] {
private convertOnce(
tr: string | { "builtin": string } | TagRenderingConfigJson,
ctx: ConversionContext
): TagRenderingConfigJson[] {
const state = this._state
if (tr === undefined) {
return []
}
if (typeof tr === "string") {
if (this._state.tagRenderings !== null) {
const lookup = this.lookup(tr, ctx)
@ -250,7 +273,7 @@ export class ExpandTagRendering extends Conversion<
ctx.warn(
`A literal rendering was detected: ${tr}
Did you perhaps forgot to add a layer name as 'layername.${tr}'? ` +
Array.from(state.sharedLayers.keys()).join(", "),
Array.from(state.sharedLayers.keys()).join(", ")
)
}
@ -260,15 +283,15 @@ export class ExpandTagRendering extends Conversion<
tr +
" \n Did you perhaps forget to add the layer as prefix, such as `icons." +
tr +
"`? ",
"`? "
)
}
return [
<any>{
<TagRenderingConfigJson & { id: string }>{
render: tr,
id: tr.replace(/[^a-zA-Z0-9]/g, ""),
},
id: tr.replace(/[^a-zA-Z0-9]/g, "")
}
]
}
@ -295,7 +318,7 @@ export class ExpandTagRendering extends Conversion<
"An object calling a builtin can only have keys `builtin` or `override`, but a key with name `" +
key +
"` was found. This won't be picked up! The full object is: " +
JSON.stringify(tr),
JSON.stringify(tr)
)
}
@ -317,7 +340,7 @@ export class ExpandTagRendering extends Conversion<
const candidates = Utils.sortedByLevenshteinDistance(
layerName,
Utils.NoNull(Array.from(state.sharedLayers.keys())),
(s) => s,
(s) => s
)
if (state.sharedLayers.size === 0) {
ctx.warn(
@ -325,7 +348,7 @@ export class ExpandTagRendering extends Conversion<
name +
": layer " +
layerName +
" not found for now, but ignoring as this is a bootstrapping run. ",
" not found for now, but ignoring as this is a bootstrapping run. "
)
} else {
ctx.err(
@ -334,13 +357,13 @@ export class ExpandTagRendering extends Conversion<
": layer " +
layerName +
" not found. Maybe you meant one of " +
candidates.slice(0, 3).join(", "),
candidates.slice(0, 3).join(", ")
)
}
continue
}
candidates = Utils.NoNull(layer.tagRenderings.map((tr) => tr["id"])).map(
(id) => layerName + "." + id,
(id) => layerName + "." + id
)
}
candidates = Utils.sortedByLevenshteinDistance(name, candidates, (i) => i)
@ -349,12 +372,12 @@ export class ExpandTagRendering extends Conversion<
name +
" was not found.\n\tDid you mean one of " +
candidates.join(", ") +
"?\n(Hint: did you add a new label and are you trying to use this label at the same time? Run 'reset:layeroverview' first",
"?\n(Hint: did you add a new label and are you trying to use this label at the same time? Run 'reset:layeroverview' first"
)
continue
}
for (let foundTr of lookup) {
foundTr = Utils.Clone<any>(foundTr)
foundTr = Utils.Clone(foundTr)
ctx.MergeObjectsForOverride(tr["override"] ?? {}, foundTr)
if (names.length == 1) {
foundTr["id"] = tr["id"] ?? foundTr["id"]
@ -365,6 +388,6 @@ export class ExpandTagRendering extends Conversion<
return trs
}
return [tr]
return [<TagRenderingConfigJson & { id: string }>tr]
}
}

View file

@ -24,7 +24,7 @@ import { ExpandTagRendering } from "./ExpandTagRendering"
class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
constructor() {
super(
'Inspects all the tagRenderings. If some tagRenderings have the `filter` attribute set, introduce those filters. This step might introduce shorthand filter names, thus \'ExpandFilter\' should be run afterwards. Can be disabled with "#filter":"no-auto"',
"Inspects all the tagRenderings. If some tagRenderings have the `filter` attribute set, introduce those filters. This step might introduce shorthand filter names, thus 'ExpandFilter' should be run afterwards. Can be disabled with \"#filter\":\"no-auto\"",
["filter"],
"AddFiltersFromTagRenderings"
)
@ -127,7 +127,7 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
if (json.freeform.inline === true) {
context.err(
"'inline' is set, but the rendering contains a special visualisation...\n " +
spec[key]
spec[key]
)
}
json = JSON.parse(JSON.stringify(json))
@ -226,20 +226,20 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
if (blacklisted?.length > 0 && used?.length > 0) {
context.err(
"The {questions()}-special rendering only supports either a blacklist OR a whitelist, but not both." +
"\n Whitelisted: " +
used.join(", ") +
"\n Blacklisted: " +
blacklisted.join(", ")
"\n Whitelisted: " +
used.join(", ") +
"\n Blacklisted: " +
blacklisted.join(", ")
)
}
for (const usedLabel of used) {
if (!allLabels.has(usedLabel)) {
context.err(
"This layers specifies a special question element for label `" +
usedLabel +
"`, but this label doesn't exist.\n" +
" Available labels are " +
Array.from(allLabels).join(", ")
usedLabel +
"`, but this label doesn't exist.\n" +
" Available labels are " +
Array.from(allLabels).join(", ")
)
}
seen.add(usedLabel)
@ -253,8 +253,8 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
const question: QuestionableTagRenderingConfigJson = {
id: "leftover-questions",
render: {
"*": `{questions( ,${Array.from(seen).join(";")})}`,
},
"*": `{questions( ,${Array.from(seen).join(";")})}`
}
}
json.tagRenderings.push(question)
}
@ -336,13 +336,13 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
if (json.allowMove && !usedSpecialFunctions.has("move_button")) {
json.tagRenderings.push({
id: "move-button",
render: { "*": "{move_button()}" },
render: { "*": "{move_button()}" }
})
}
if (json.deletion && !usedSpecialFunctions.has("delete_button")) {
json.tagRenderings.push({
id: "delete-button",
render: { "*": "{delete_button()}" },
render: { "*": "{delete_button()}" }
})
}
@ -357,9 +357,9 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
or: [
"__featureSwitchIsDebugging=true",
"mapcomplete-show_tags=full",
"mapcomplete-show_debug=yes",
],
},
"mapcomplete-show_debug=yes"
]
}
}
json.tagRenderings?.push(trc)
}
@ -467,10 +467,10 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
private static convertIfNeeded(
input:
| (object & {
special: {
type: string
}
})
special: {
type: string
}
})
| any,
context: ConversionContext
): any {
@ -568,7 +568,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
.map((nm) => RewriteSpecial.escapeStr(special[nm] ?? "", context))
.join(",")
return {
"*": `{${type}(${args})${clss}}`,
"*": `{${type}(${args})${clss}}`
}
}
@ -666,11 +666,50 @@ class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
}[] = []
for (let i = 0; i < badgesJson.length; i++) {
const iconBadge: {
const iconBadge: string | ({
if: TagConfigJson
then: string | MinimalTagRenderingConfigJson
} = badgesJson[i]
const expanded = this._expand.convert(
}) = badgesJson[i]
if (typeof iconBadge === "string") {
const expanded: QuestionableTagRenderingConfigJson[] = this._expand.convert(
iconBadge,
context.enters("iconBadges", i)
)
for (const tr of expanded) {
const condition = tr.condition
for (const trElement of tr.mappings) {
const showIf = TagUtils.optimzeJson({
and: Utils.NoNull([condition,
{
or: Utils.NoNull([
trElement.alsoShowIf, trElement.if
])
}
])
})
if (showIf === true) {
context.warn("Dropping iconBadge that would be _always_ shown: " + (trElement.icon ?? trElement.then))
continue
}
if (showIf === false) {
continue
}
iconBadges.push({
if: showIf,
then: trElement.icon ?? trElement.then
})
}
}
continue
}
const expanded: QuestionableTagRenderingConfigJson[] = this._expand.convert(
<QuestionableTagRenderingConfigJson>iconBadge.then,
context.enters("iconBadges", i)
)
@ -682,7 +721,7 @@ class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
iconBadges.push(
...expanded.map((resolved) => ({
if: iconBadge.if,
then: <MinimalTagRenderingConfigJson>resolved,
then: <MinimalTagRenderingConfigJson>resolved
}))
)
}
@ -751,19 +790,21 @@ class ExpandMarkerRenderings extends DesugaringStep<IconConfigJson> {
}
convert(json: IconConfigJson, context: ConversionContext): IconConfigJson {
const expander = new ExpandTagRendering(this._state, this._layer, {applyCondition: false})
const expander = new ExpandTagRendering(this._state, this._layer, { applyCondition: false })
const result: IconConfigJson = { icon: undefined, color: undefined }
if (json.icon && json.icon["builtin"]) {
result.icon = <MinimalTagRenderingConfigJson>(
expander.convert(<any>json.icon, context.enter("icon"))[0]
) ?? json.icon
result.icon =
<MinimalTagRenderingConfigJson>(
expander.convert(<any>json.icon, context.enter("icon"))[0]
) ?? json.icon
} else {
result.icon = json.icon
}
if (json.color && json.color["builtin"]) {
result.color = <MinimalTagRenderingConfigJson>(
expander.convert(<any>json.color, context.enter("color"))[0]
) ?? json.color
result.color =
<MinimalTagRenderingConfigJson>(
expander.convert(<any>json.color, context.enter("color"))[0]
) ?? json.color
} else {
result.color = json.color
}
@ -820,7 +861,7 @@ export class AddRatingBadge extends DesugaringStep<LayerConfigJson> {
const specialVis: Exclude<RenderingSpecification, string>[] = <
Exclude<RenderingSpecification, string>[]
>ValidationUtils.getAllSpecialVisualisations(<any>json.tagRenderings).filter(
>ValidationUtils.getAllSpecialVisualisations(<any>json.tagRenderings).filter(
(rs) => typeof rs !== "string"
)
const funcs = new Set<string>(specialVis.map((rs) => rs.func.funcName))
@ -856,7 +897,7 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
}
return <TagRenderingConfigJson>{
id: "title_icon_auto_" + tr.id,
mappings,
mappings
}
}
@ -901,8 +942,8 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
.enters("titleIcons", i)
.warn(
"TagRendering with id " +
trId +
" does not have any icons, not generating an icon for this"
trId +
" does not have any icons, not generating an icon for this"
)
continue
}
@ -965,7 +1006,7 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
(layer) =>
new Concat(
new ExpandTagRendering(state, layer, {
addToContext: options?.addTagRenderingsToContext ?? false,
addToContext: options?.addTagRenderingsToContext ?? false
})
)
),
@ -1002,7 +1043,7 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
}
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
if(json === undefined || json === null){
if (json === undefined || json === null) {
throw "Error: prepareLayer got null"
}
return super.convert(json, context)

View file

@ -112,7 +112,7 @@ export class DoesImageExist extends DesugaringStep<string> {
if (!this._knownImagePaths.has(image)) {
if (this.doesPathExist === undefined || image.indexOf("nsi/logos/") >= 0) {
// pass
} else if (!this.doesPathExist(image) ) {
} else if (!this.doesPathExist(image)) {
context.err(
`Image with path ${image} does not exist.\n Check for typo's and missing directories in the path. `
)

View file

@ -13,14 +13,15 @@ export interface IconConfigJson {
*
*
* types: <span class="text-lg font-bold">Use a different icon depending on the value of some attributes</span> ; icon
* suggestions: return [ "nsi_brand.icon", "nsi_operator.icon", "id_presets.shop_rendering", ...Constants.defaultPinIcons.map(i => ({if: "value="+i, then: i, icon: i}))]
* suggestions: return [ {"if":"value=nsi_brand.icon", "then": "Use icons for brand from the Name Suggestion Index"}, {"if":"value=nsi_operator.icon", "then": "Use icons for operator from the Name Suggestion Index"}, {"if":"value=id_presets.shop_rendering", "then": "Use shop preset icons from iD"}, ...Constants.defaultPinIcons.map(i => ({if: "value="+i, then: i, icon: i}))]
*/
icon: string | MinimalTagRenderingConfigJson | { builtin: string; override: any }
/**
* question: What colour should the icon be?
* This will only work for the default icons such as `pin`,`circle`,...
* types: <span class="text-lg font-bold">Use a different color depending on the value of some attributes</span> ; color
*
* This will only work for the default icons such as `pin`,`circle`,...
*
* types: <span class="text-lg font-bold">Use a different color depending on the value of some attributes</span> ; color
*/
color?: string | MinimalTagRenderingConfigJson | { builtin: string; override: any }
}
@ -70,16 +71,18 @@ export default interface PointRenderingConfigJson {
* They will be added as a 25% height icon at the bottom right of the icon, with all the badges in a flex layout.
*
* Note: strings are interpreted as icons, so layering and substituting is supported. You can use `circle:white;./my_icon.svg` to add a background circle
* Alternatively, this can reuse a _tagRendering_ from another layer, e.g. one of the 'icons'-tagrenderings.
* See ExpandIconBadges on how this is handled
* group: hidden
*/
iconBadges?: {
iconBadges?: (string | {
if: TagConfigJson
/**
* Badge to show
* Type: icon
*/
then: string | MinimalTagRenderingConfigJson
}[]
})[]
/**
* question: What size should the marker be on the map?

View file

@ -78,10 +78,11 @@ export default class LayerConfig extends WithContextLoader {
*/
private readonly _basedOn: string | undefined
constructor(json: LayerConfigJson,
context?: string,
official: boolean = true,
allLayers?: LayerConfigJson[],
constructor(
json: LayerConfigJson,
context?: string,
official: boolean = true,
allLayers?: LayerConfigJson[]
) {
context = context + "." + json?.id
const translationContext = "layers:" + json.id
@ -113,7 +114,7 @@ export default class LayerConfig extends WithContextLoader {
mercatorCrs: json.source["mercatorCrs"],
idKey: json.source["idKey"],
},
json.id,
json.id
)
}
@ -133,7 +134,7 @@ export default class LayerConfig extends WithContextLoader {
if (json.calculatedTags !== undefined) {
if (!official) {
console.warn(
`Unofficial theme ${this.id} with custom javascript! This is a security risk`,
`Unofficial theme ${this.id} with custom javascript! This is a security risk`
)
}
this.calculatedTags = []
@ -203,7 +204,7 @@ export default class LayerConfig extends WithContextLoader {
tags: pr.tags.map((t) => TagUtils.SimpleTag(t)),
description: Translations.T(
pr.description,
`${translationContext}.presets.${i}.description`,
`${translationContext}.presets.${i}.description`
),
preciseInput: preciseInput,
exampleImages: pr.exampleImages,
@ -217,7 +218,7 @@ export default class LayerConfig extends WithContextLoader {
if (json.lineRendering) {
this.lineRendering = Utils.NoNull(json.lineRendering).map(
(r, i) => new LineRenderingConfig(r, `${context}[${i}]`),
(r, i) => new LineRenderingConfig(r, `${context}[${i}]`)
)
} else {
this.lineRendering = []
@ -225,7 +226,7 @@ export default class LayerConfig extends WithContextLoader {
if (json.pointRendering) {
this.mapRendering = Utils.NoNull(json.pointRendering).map(
(r, i) => new PointRenderingConfig(r, `${context}[${i}](${this.id})`),
(r, i) => new PointRenderingConfig(r, `${context}[${i}](${this.id})`)
)
} else {
this.mapRendering = []
@ -237,7 +238,7 @@ export default class LayerConfig extends WithContextLoader {
r.location.has("centroid") ||
r.location.has("projected_centerpoint") ||
r.location.has("start") ||
r.location.has("end"),
r.location.has("end")
)
if (
@ -259,7 +260,7 @@ export default class LayerConfig extends WithContextLoader {
Constants.priviliged_layers.indexOf(<any>this.id) < 0 &&
this.source !== null /*library layer*/ &&
!this.source?.geojsonSource?.startsWith(
"https://api.openstreetmap.org/api/0.6/notes.json",
"https://api.openstreetmap.org/api/0.6/notes.json"
)
) {
throw (
@ -278,7 +279,7 @@ export default class LayerConfig extends WithContextLoader {
typeof tr !== "string" &&
tr["builtin"] === undefined &&
tr["id"] === undefined &&
tr["rewrite"] === undefined,
tr["rewrite"] === undefined
) ?? []
if (missingIds?.length > 0 && official) {
console.error("Some tagRenderings of", this.id, "are missing an id:", missingIds)
@ -289,8 +290,8 @@ export default class LayerConfig extends WithContextLoader {
(tr, i) =>
new TagRenderingConfig(
<QuestionableTagRenderingConfigJson>tr,
this.id + ".tagRenderings[" + i + "]",
),
this.id + ".tagRenderings[" + i + "]"
)
)
if (json.units !== undefined && !Array.isArray(json.units)) {
throw (
@ -300,19 +301,15 @@ export default class LayerConfig extends WithContextLoader {
)
}
this.units = (json.units ?? []).flatMap((unitJson, i) =>
Unit.fromJson(unitJson, this.tagRenderings, `${context}.unit[${i}]`),
Unit.fromJson(unitJson, this.tagRenderings, `${context}.unit[${i}]`)
)
{
let filter = json.filter
while (
filter !== undefined &&
filter !== null &&
filter["sameAs"] !== undefined
) {
while (filter !== undefined && filter !== null && filter["sameAs"] !== undefined) {
const targetLayerName = filter["sameAs"]
this.filterIsSameAs = targetLayerName
const targetLayer = allLayers?.find(l => l.id === targetLayerName)
const targetLayer = allLayers?.find((l) => l.id === targetLayerName)
if (allLayers && !targetLayer) {
throw "Target layer " + targetLayerName + " not found in this theme"
}
@ -373,7 +370,7 @@ export default class LayerConfig extends WithContextLoader {
}
this.popupInFloatover = json.popupInFloatover ?? false
this.baseTags = TagUtils.changeAsProperties(
this.source?.osmTags?.asChange({ id: "node/-1" }) ?? [{ k: "id", v: "node/-1" }],
this.source?.osmTags?.asChange({ id: "node/-1" }) ?? [{ k: "id", v: "node/-1" }]
)
}
@ -393,7 +390,7 @@ export default class LayerConfig extends WithContextLoader {
neededLayer: string
}[] = [],
addedByDefault = false,
canBeIncluded = true,
canBeIncluded = true
): string {
const extraProps: string[] = []
extraProps.push("This layer is shown at zoomlevel **" + this.minzoom + "** and higher")
@ -401,32 +398,32 @@ export default class LayerConfig extends WithContextLoader {
if (canBeIncluded) {
if (addedByDefault) {
extraProps.push(
"**This layer is included automatically in every theme. This layer might contain no points**",
"**This layer is included automatically in every theme. This layer might contain no points**"
)
}
if (this.shownByDefault === false) {
extraProps.push(
"This layer is not visible by default and must be enabled in the filter by the user. ",
"This layer is not visible by default and must be enabled in the filter by the user. "
)
}
if (this.title === undefined) {
extraProps.push(
"Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable.",
"Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable."
)
}
if (this.name === undefined && this.shownByDefault === false) {
extraProps.push(
"This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-<id>=true",
"This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-<id>=true"
)
}
if (this.name === undefined) {
extraProps.push(
"Not visible in the layer selection by default. If you want to make this layer toggable, override `name`",
"Not visible in the layer selection by default. If you want to make this layer toggable, override `name`"
)
}
if (this.mapRendering.length === 0) {
extraProps.push(
"Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`",
"Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`"
)
}
@ -436,12 +433,12 @@ export default class LayerConfig extends WithContextLoader {
"<img src='../warning.svg' height='1rem'/>",
"This layer is loaded from an external source, namely ",
"`" + this.source.geojsonSource + "`",
].join("\n\n"),
].join("\n\n")
)
}
} else {
extraProps.push(
"This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data.",
"This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data."
)
}
@ -451,7 +448,7 @@ export default class LayerConfig extends WithContextLoader {
usingLayer = [
"## Themes using this layer",
MarkdownUtils.list(
(usedInThemes ?? []).map((id) => `[${id}](https://mapcomplete.org/${id})`),
(usedInThemes ?? []).map((id) => `[${id}](https://mapcomplete.org/${id})`)
),
]
} else if (this.source !== null) {
@ -467,7 +464,7 @@ export default class LayerConfig extends WithContextLoader {
" into the layout as it depends on it: ",
dep.reason,
"(" + dep.context + ")",
].join(" "),
].join(" ")
)
}
@ -494,7 +491,7 @@ export default class LayerConfig extends WithContextLoader {
new And(preset.tags).asHumanString(true) +
snaps
)
}),
})
),
]
}
@ -502,8 +499,8 @@ export default class LayerConfig extends WithContextLoader {
for (const revDep of Utils.Dedup(layerIsNeededBy?.get(this.id) ?? [])) {
extraProps.push(
["This layer is needed as dependency for layer", `[${revDep}](#${revDep})`].join(
" ",
),
" "
)
)
}
@ -514,10 +511,10 @@ export default class LayerConfig extends WithContextLoader {
.filter((values) => values.key !== "id")
.map((values) => {
const embedded: string[] = values.values?.map((v) =>
Link.OsmWiki(values.key, v, true).SetClass("mr-2").AsMarkdown(),
Link.OsmWiki(values.key, v, true).SetClass("mr-2").AsMarkdown()
) ?? ["_no preset options defined, or no values in them_"]
const statistics = `https://taghistory.raifer.tech/?#***/${encodeURIComponent(
values.key,
values.key
)}/`
const tagInfo = `https://taginfo.openstreetmap.org/keys/${values.key}#values`
return [
@ -532,7 +529,7 @@ export default class LayerConfig extends WithContextLoader {
: `[${values.type}](../SpecialInputElements.md#${values.type})`,
embedded.join(" "),
]
}),
})
)
let quickOverview: string[] = []
@ -542,7 +539,7 @@ export default class LayerConfig extends WithContextLoader {
"this quick overview is incomplete",
MarkdownUtils.table(
["attribute", "type", "values which are supported by this layer"],
tableRows,
tableRows
),
]
}
@ -576,19 +573,19 @@ export default class LayerConfig extends WithContextLoader {
const parts = neededTags["and"]
tagsDescription.push(
"Elements must match **all** of the following expressions:",
parts.map((p, i) => i + ". " + p.asHumanString(true, false, {})).join("\n"),
parts.map((p, i) => i + ". " + p.asHumanString(true, false, {})).join("\n")
)
} else if (neededTags["or"]) {
const parts = neededTags["or"]
tagsDescription.push(
"Elements must match **any** of the following expressions:",
parts.map((p) => " - " + p.asHumanString(true, false, {})).join("\n"),
parts.map((p) => " - " + p.asHumanString(true, false, {})).join("\n")
)
} else {
tagsDescription.push(
"Elements must match the expression **" +
neededTags.asHumanString(true, false, {}) +
"**",
neededTags.asHumanString(true, false, {}) +
"**"
)
}

View file

@ -5,10 +5,7 @@ import { TagUtils } from "../../Logic/Tags/TagUtils"
import { And } from "../../Logic/Tags/And"
import { Utils } from "../../Utils"
import { Tag } from "../../Logic/Tags/Tag"
import {
MappingConfigJson,
QuestionableTagRenderingConfigJson,
} from "./Json/QuestionableTagRenderingConfigJson"
import { MappingConfigJson, QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRenderingConfigJson"
import Validators, { ValidatorType } from "../../UI/InputElement/Validators"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
import { RegexTag } from "../../Logic/Tags/RegexTag"
@ -82,6 +79,7 @@ export default class TagRenderingConfig {
public readonly classes: string[] | undefined
public readonly onSoftDelete?: ReadonlyArray<UploadableTag>
public readonly alwaysForceSaveButton: boolean
constructor(
config:
@ -144,6 +142,7 @@ export default class TagRenderingConfig {
this.question = Translations.T(json.question, translationKey + ".question")
this.questionhint = Translations.T(json.questionHint, translationKey + ".questionHint")
this.questionHintIsMd = json["questionHintIsMd"] ?? false
this.alwaysForceSaveButton = json["#force-save-button"] === "yes"
this.description = Translations.T(json.description, translationKey + ".description")
if (json.onSoftDelete && !Array.isArray(json.onSoftDelete)) {
throw context + ".onSoftDelete Not an array: " + typeof json.onSoftDelete

View file

@ -99,7 +99,7 @@ export default class ThemeConfig implements ThemeInformation {
options?: {
definedAtUrl?: string
definitionRaw?: string
},
}
) {
if (json === undefined) {
throw "Cannot construct a layout config, the parameter 'json' is undefined"
@ -130,7 +130,7 @@ export default class ThemeConfig implements ThemeInformation {
throw `The title of a theme should always be a translation, as it sets the corresponding languages (${context}.title). The themenID is ${
this.id
}; the offending object is ${JSON.stringify(
json.title,
json.title
)} which is a ${typeof json.title})`
}
if (this.language.length == 0) {
@ -184,8 +184,8 @@ export default class ThemeConfig implements ThemeInformation {
<LayerConfigJson>lyrJson,
json.id + ".layers." + lyrJson["id"],
official,
<LayerConfigJson[]>json.layers,
),
<LayerConfigJson[]>json.layers
)
)
this.extraLink = new ExtraLinkConfig(
@ -195,7 +195,7 @@ export default class ThemeConfig implements ThemeInformation {
newTab: true,
requirements: ["iframe", "no-welcome-message"],
},
context + ".extraLink",
context + ".extraLink"
)
this.hideFromOverview = json.hideFromOverview ?? false
@ -301,7 +301,7 @@ export default class ThemeConfig implements ThemeInformation {
return false
}
return o instanceof Translation
},
}
)
return { untranslated, total }
@ -309,7 +309,7 @@ export default class ThemeConfig implements ThemeInformation {
public getMatchingLayer(
tags: Record<string, string>,
blacklistLayers?: Set<string>,
blacklistLayers?: Set<string>
): LayerConfig | undefined {
if (tags === undefined) {
return undefined
@ -338,7 +338,7 @@ export default class ThemeConfig implements ThemeInformation {
"Fallthrough: could not find the appropriate layer for an object with tags",
tags,
"within layout",
this,
this
)
return undefined
}

View file

@ -13,9 +13,8 @@
})
</script>
{#if timeoutReached}
<slot />
{:else }
<Loading cls="h-full w-full flex justify-center items-center low-interaction"/>
{:else}
<Loading cls="h-full w-full flex justify-center items-center low-interaction" />
{/if}

View file

@ -0,0 +1,11 @@
<script lang="ts">
import Loading from "./Loading.svelte"
</script>
<div class="relative w-60 h-80">
<div class="animate-pulse w-full h-full bg-gray-400">
</div>
<div class="w-full h-full absolute top-0 flex items-center justify-center">
<Loading />
</div>
</div>

View file

@ -126,12 +126,6 @@
</svelte:fragment>
<!-- All shown components are set by 'usersettings.json', which happily uses some special visualisations created specifically for it -->
<LoginToggle {state} silentFail>
<div class="flex flex-col" slot="not-logged-in">
<LanguagePicker availableLanguages={theme.language} />
<Tr cls="alert" t={Translations.t.userinfo.notLoggedIn} />
<LoginButton clss="primary" osmConnection={state.osmConnection} />
</div>
<SelectedElementView
highlightedRendering={state.guistate.highlightedUserSetting}
layer={usersettingslayer}
@ -143,7 +137,6 @@
{state}
tags={state.userRelatedState.preferencesAsTags}
/>
</LoginToggle>
</Page>
<LoginToggle {state} silentFail>

View file

@ -57,11 +57,17 @@
<Tr t={theme.descriptionTail} />
{#if !theme.official}
<div class="flex w-full justify-end">
<button class="flex small w-fit self-end as-link" on:click={() => Utils.offerContentsAsDownloadableFile(JSON.stringify(theme.source, null, " "), theme.id+".mapcomplete_theme.json")}>
Download the theme definition
</button>
</div>
<button
class="small as-link flex w-fit self-end"
on:click={() =>
Utils.offerContentsAsDownloadableFile(
JSON.stringify(theme.source, null, " "),
theme.id + ".mapcomplete_theme.json"
)}
>
Download the theme definition
</button>
</div>
{/if}
<!-- Buttons: open map, go to location, search -->
<NextButton
@ -110,7 +116,6 @@
{/if}
</div>
<div class="link-underline mt-8 flex justify-end text-sm">
<a href="https://mapcomplete.org" target="_blank">
<Tr t={Translations.t.general.poweredByMapComplete} />

View file

@ -17,6 +17,7 @@
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import DotMenu from "../Base/DotMenu.svelte"
import LoadingPlaceholder from "../Base/LoadingPlaceholder.svelte"
export let image: Partial<ProvidedImage>
let fallbackImage: string = undefined
@ -111,7 +112,11 @@
<slot name="dot-menu-actions" />
</DotMenu>
{/if}
{#if !loaded}
<LoadingPlaceholder />
{/if}
<img
class:hidden={!loaded}
bind:this={imgEl}
on:load={() => (loaded = true)}
class={imgClass ?? ""}

View file

@ -44,7 +44,7 @@
const imageInfo = await panoramax.imageInfo(image.id)
let reporter_email: string = undefined
const userdetails = state.userRelatedState.osmConnection.userDetails
if (!userdetails.data) {
if (userdetails.data?.name) {
reporter_email = userdetails.data.name + "@openstreetmap.org"
}

View file

@ -3,14 +3,24 @@
import type { ProvidedImage } from "../../Logic/ImageProviders/ImageProvider"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import DeletableImage from "./DeletableImage.svelte"
import Loading from "../Base/Loading.svelte"
import LoadingPlaceholder from "../Base/LoadingPlaceholder.svelte"
export let images: Store<ProvidedImage[]>
export let state: SpecialVisualizationState
export let tags: UIEventSource<Record<string, string>>
</script>
<div class="flex w-full space-x-2 overflow-x-auto" style="scroll-snap-type: x proximity">
{#each $images as image (image.url)}
<DeletableImage {image} {state} {tags} />
{/each}
</div>
export let estimated: Store<number>
</script>
{#if $estimated > 0 && $images.length < 1}
<LoadingPlaceholder />
{:else}
<div class="w-full overflow-x-auto" style="scroll-snap-type: x proximity">
<div class="flex space-x-2">
{#each $images as image (image.url)}
<DeletableImage {image} {state} {tags} />
{/each}
</div>
</div>
{/if}

View file

@ -36,7 +36,7 @@
let imagesProvider = state.nearbyImageSearcher
let loadedImages = AllImageProviders.LoadImagesFor(tags).mapD(
let loadedImages = AllImageProviders.loadImagesFor(tags).mapD(
(loaded) => new Set(loaded.map((img) => img.url))
)
let imageState = imagesProvider.getImagesAround(lon, lat)

View file

@ -46,7 +46,7 @@
styleUrl = defaultLayer.style ?? defaultLayer.url
}
console.log("Initiating mapLIbremap with style", styleUrl)
console.log("Initiating mapLibremap with style", styleUrl)
const options: MapOptions = {
container,

View file

@ -50,7 +50,7 @@
}
return features
},
[tagSource],
[tagSource]
)
let mlmap = new UIEventSource(undefined)
@ -76,9 +76,8 @@
mlmap,
new StaticFeatureSource(featuresToShow),
state.theme.layers,
{ zoomToFeatures: true },
{ zoomToFeatures: true }
)
</script>
<div class="h-40 rounded" style="overflow: hidden; pointer-events: none;">

View file

@ -9,6 +9,7 @@ import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisua
import SvelteUIElement from "../Base/SvelteUIElement"
import PlantNet from "../PlantNet/PlantNet.svelte"
import { default as PlantNetCode } from "../../Logic/Web/PlantNet"
export class PlantNetDetectionViz implements SpecialVisualization {
funcName = "plantnet_detection"
needsUrls = [PlantNetCode.baseUrl]
@ -33,7 +34,7 @@ export class PlantNetDetectionViz implements SpecialVisualization {
imagePrefixes = [].concat(...args.map((a) => a.split(",")))
}
const allProvidedImages: Store<ProvidedImage[]> = AllImageProviders.LoadImagesFor(
const allProvidedImages: Store<ProvidedImage[]> = AllImageProviders.loadImagesFor(
tags,
imagePrefixes
)

View file

@ -13,6 +13,7 @@
import { Utils } from "../../../Utils"
import { onDestroy } from "svelte"
import TagRenderingQuestionDynamic from "./TagRenderingQuestionDynamic.svelte"
import LoginToggle from "../../Base/LoginToggle.svelte"
export let layer: LayerConfig
export let tags: UIEventSource<Record<string, string>>
@ -155,48 +156,65 @@
</TagRenderingQuestionDynamic>
{/if}
{#if $allQuestionsToAsk.length === 0}
<div class="thanks">
<Tr t={Translations.t.general.questionBox.done} />
</div>
{/if}
<LoginToggle {state}>
<span slot="not-logged-in" />
{#if $allQuestionsToAsk.length === 0}
<div class="thanks">
<Tr t={Translations.t.general.questionBox.done} />
</div>
{/if}
<div class="mt-4 mb-8">
{#if skipped + answered > 0}
<div class="flex justify-center">
{#if answered === 0}
{#if skipped === 1}
<Tr t={Translations.t.general.questionBox.skippedOne} />
{:else}
<Tr t={Translations.t.general.questionBox.skippedMultiple.Subs({ skipped })} />
{/if}
{:else if answered === 1}
{#if skipped === 0}
<Tr t={Translations.t.general.questionBox.answeredOne} />
<div class="mt-4 mb-8">
{#if skipped + answered > 0}
<div class="flex justify-center">
{#if answered === 0}
{#if skipped === 1}
<Tr t={Translations.t.general.questionBox.skippedOne} />
{:else}
<Tr t={Translations.t.general.questionBox.skippedMultiple.Subs({ skipped })} />
{/if}
{:else if answered === 1}
{#if skipped === 0}
<Tr t={Translations.t.general.questionBox.answeredOne} />
{:else if skipped === 1}
<Tr t={Translations.t.general.questionBox.answeredOneSkippedOne} />
{:else}
<Tr
t={Translations.t.general.questionBox.answeredOneSkippedMultiple.Subs({
skipped,
})}
/>
{/if}
{:else if skipped === 0}
<Tr t={Translations.t.general.questionBox.answeredMultiple.Subs({ answered })} />
{:else if skipped === 1}
<Tr t={Translations.t.general.questionBox.answeredOneSkippedOne} />
<Tr
t={Translations.t.general.questionBox.answeredMultipleSkippedOne.Subs({ answered })}
/>
{:else}
<Tr
t={Translations.t.general.questionBox.answeredOneSkippedMultiple.Subs({ skipped })}
t={Translations.t.general.questionBox.answeredMultipleSkippedMultiple.Subs({
answered,
skipped,
})}
/>
{/if}
{:else if skipped === 0}
<Tr t={Translations.t.general.questionBox.answeredMultiple.Subs({ answered })} />
{:else if skipped === 1}
<Tr
t={Translations.t.general.questionBox.answeredMultipleSkippedOne.Subs({ answered })}
/>
{:else}
<Tr
t={Translations.t.general.questionBox.answeredMultipleSkippedMultiple.Subs({
answered,
skipped,
})}
/>
{/if}
</div>
</div>
{#if skipped + $skippedQuestions.size > 0}
{#if skipped + $skippedQuestions.size > 0}
<button
class="w-full"
on:click={() => {
skippedQuestions.setData(new Set())
skipped = 0
}}
>
<Tr t={Translations.t.general.questionBox.reactivate} />
</button>
{/if}
{/if}
{#if $skippedQuestions.size - skipped > 0}
<button
class="w-full"
on:click={() => {
@ -204,25 +222,13 @@
skipped = 0
}}
>
<Tr t={Translations.t.general.questionBox.reactivate} />
Show the disabled questions for this object
</button>
{/if}
{/if}
{#if $skippedQuestions.size - skipped > 0}
<button
class="w-full"
on:click={() => {
skippedQuestions.setData(new Set())
skipped = 0
}}
>
Show the disabled questions for this object
</button>
{/if}
{#if $debug}
Skipped questions are {Array.from($skippedQuestions).join(", ")}
{/if}
</div>
{#if $debug}
Skipped questions are {Array.from($skippedQuestions).join(", ")}
{/if}
</div>
</LoginToggle>
</div>
{/if}

View file

@ -14,7 +14,6 @@
import TagHint from "../TagHint.svelte"
import LoginToggle from "../../Base/LoginToggle.svelte"
import SubtleButton from "../../Base/SubtleButton.svelte"
import Loading from "../../Base/Loading.svelte"
import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte"
import { Translation } from "../../i18n/Translation"
import Constants from "../../../Models/Constants"
@ -119,7 +118,7 @@
seenFreeforms.push(newProps[confg.freeform.key])
}
return matches
}),
})
]
if (tgs !== undefined && confg.freeform) {
@ -227,7 +226,7 @@
freeform: $freeformInput,
selectedMapping,
checkedMappings,
currentTags: tags.data,
currentTags: tags.data
},
" --> ",
selectedTags
@ -285,7 +284,7 @@
dispatch("saved", { config, applied: selectedTags })
const change = new ChangeTagAction(tags.data.id, selectedTags, tags.data, {
theme: tags.data["_orig_theme"] ?? state.theme.id,
changeType: "answer",
changeType: "answer"
})
freeformInput.set(undefined)
selectedMapping = undefined
@ -329,7 +328,7 @@
const tagsToSet = settableKeys.data.map((k) => new Tag(k, ""))
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
@ -542,12 +541,25 @@
{/if}
<!-- Save and cancel buttons, in a logintoggle -->
<LoginToggle {state}>
<Loading slot="loading" />
<SubtleButton slot="not-logged-in" on:click={() => state?.osmConnection?.AttemptLogin()}>
<Login slot="image" class="h-8 w-8" />
<Tr t={Translations.t.general.loginToStart} slot="message" />
</SubtleButton>
<LoginToggle {state} ignoreLoading>
<div class="flex w-full justify-end" slot="not-logged-in">
{#if config.alwaysForceSaveButton}
<button
on:click={() => onSave()}
class={twJoin(
selectedTags === undefined ? "disabled" : "button-shadow",
"primary"
)}
>
<Tr t={Translations.t.general.save} />
</button>
{:else}
<SubtleButton on:click={() => state?.osmConnection?.AttemptLogin()}>
<Login slot="image" class="h-8 w-8" />
<Tr t={Translations.t.general.loginToStart} slot="message" />
</SubtleButton>
{/if}
</div>
{#if $feedback !== undefined}
<div class="alert" aria-live="assertive" role="alert">
<Tr t={$feedback} />

View file

@ -16,7 +16,7 @@
export let feature: Feature
let theme = state.theme
let title = tags.mapD(tags => layer?.title?.GetRenderValue(tags))
let title = tags.mapD((tags) => layer?.title?.GetRenderValue(tags))
</script>
{#if theme === undefined}

View file

@ -71,6 +71,7 @@
_state = "done"
}
</script>
{#if uploadFailed}
<div class="alert flex">
<ExclamationTriangle class="h-6 w-6" />

View file

@ -126,7 +126,7 @@ class NearbyImageVis implements SpecialVisualization {
tags: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): SvelteUIElement {
const isOpen = args[0] === "open"
const readonly = args[1] === "readonly" || args[1] === "yes"
@ -193,7 +193,7 @@ class StealViz implements SpecialVisualization {
selectedElement: otherFeature,
state,
layer,
}),
})
)
}
if (elements.length === 1) {
@ -201,8 +201,8 @@ class StealViz implements SpecialVisualization {
}
return new Combine(elements).SetClass("flex flex-col")
},
[state.indexedFeatures.featuresById],
),
[state.indexedFeatures.featuresById]
)
)
}
@ -254,11 +254,11 @@ class CloseNoteViz implements SpecialVisualization {
public constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
args: string[],
args: string[]
): SvelteUIElement {
const { text, icon, idkey, comment, minZoom, zoomButton } = Utils.ParseVisArgs(
this.args,
args,
args
)
return new SvelteUIElement(CloseNoteButton, {
@ -299,7 +299,7 @@ export class QuestionViz implements SpecialVisualization {
tags: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): SvelteUIElement {
const labels = args[0]
?.split(";")
@ -331,7 +331,7 @@ export default class SpecialVisualizations {
for (const specialVisualization of SpecialVisualizations.specialVisualizations) {
SpecialVisualizations.specialVisualisationsDict.set(
specialVisualization.funcName,
specialVisualization,
specialVisualization
)
}
}
@ -351,15 +351,15 @@ export default class SpecialVisualizations {
viz.docs,
viz.args.length > 0
? MarkdownUtils.table(
["name", "default", "description"],
viz.args.map((arg) => {
let defaultArg = arg.defaultValue ?? "_undefined_"
if (defaultArg == "") {
defaultArg = "_empty string_"
}
return [arg.name, defaultArg, arg.doc]
}),
)
["name", "default", "description"],
viz.args.map((arg) => {
let defaultArg = arg.defaultValue ?? "_undefined_"
if (defaultArg == "") {
defaultArg = "_empty string_"
}
return [arg.name, defaultArg, arg.doc]
})
)
: undefined,
"#### Example usage of " + viz.funcName,
"<code>" + example + "</code>",
@ -368,18 +368,18 @@ export default class SpecialVisualizations {
public static constructSpecification(
template: string,
extraMappings: SpecialVisualization[] = [],
extraMappings: SpecialVisualization[] = []
): RenderingSpecification[] {
return SpecialVisualisationUtils.constructSpecification(
template,
SpecialVisualizations.specialVisualisationsDict,
extraMappings,
extraMappings
)
}
public static HelpMessage(): string {
const helpTexts: string[] = SpecialVisualizations.specialVisualizations.map((viz) =>
SpecialVisualizations.DocumentationFor(viz),
SpecialVisualizations.DocumentationFor(viz)
)
const firstPart = new Combine([
@ -412,10 +412,10 @@ export default class SpecialVisualizations {
},
},
null,
" ",
),
" "
)
).SetClass("code"),
"In other words: use `{ \"before\": ..., \"after\": ..., \"special\": {\"type\": ..., \"argname\": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)",
'In other words: use `{ "before": ..., "after": ..., "special": {"type": ..., "argname": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)',
])
.SetClass("flex flex-col")
.AsMarkdown()
@ -453,10 +453,10 @@ export default class SpecialVisualizations {
assignTo: state.userRelatedState.language,
availableLanguages: languages,
preferredLanguages: state.osmConnection.userDetails.map(
(ud) => ud.languages,
(ud) => ud.languages
),
})
}),
})
)
},
},
@ -495,7 +495,7 @@ export default class SpecialVisualizations {
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
feature: Feature
): SvelteUIElement {
return new SvelteUIElement(MinimapViz, { state, args, feature, tagSource })
},
@ -507,7 +507,7 @@ export default class SpecialVisualizations {
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
tagSource: UIEventSource<Record<string, string>>
): BaseUIElement {
return new VariableUiElement(
tagSource
@ -517,7 +517,7 @@ export default class SpecialVisualizations {
return new SvelteUIElement(SplitRoadWizard, { id, state })
}
return undefined
}),
})
)
},
},
@ -531,7 +531,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
if (feature.geometry.type !== "Point") {
return undefined
@ -554,7 +554,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
if (!layer.deletion) {
return undefined
@ -580,7 +580,7 @@ export default class SpecialVisualizations {
tags: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
) {
if (feature.geometry.type !== "LineString") {
return undefined
@ -612,7 +612,7 @@ export default class SpecialVisualizations {
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
feature: Feature
): BaseUIElement {
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
return new SvelteUIElement(CreateNewNote, {
@ -675,7 +675,7 @@ export default class SpecialVisualizations {
.map((tags) => tags[args[0]])
.map((wikidata) => {
wikidata = Utils.NoEmpty(
wikidata?.split(";")?.map((wd) => wd.trim()) ?? [],
wikidata?.split(";")?.map((wd) => wd.trim()) ?? []
)[0]
const entry = Wikidata.LoadWikidataEntry(wikidata)
return new VariableUiElement(
@ -685,9 +685,9 @@ export default class SpecialVisualizations {
}
const response = <WikidataResponse>e["success"]
return Translation.fromMap(response.labels)
}),
})
)
}),
})
),
},
new MapillaryLinkVis(),
@ -701,7 +701,7 @@ export default class SpecialVisualizations {
tags: UIEventSource<Record<string, string>>,
_,
__,
layer: LayerConfig,
layer: LayerConfig
) => new SvelteUIElement(AllTagsPanel, { tags, layer }),
},
{
@ -720,8 +720,9 @@ export default class SpecialVisualizations {
if (args.length > 0) {
imagePrefixes = [].concat(...args.map((a) => a.split(",")))
}
const images = AllImageProviders.LoadImagesFor(tags, imagePrefixes)
return new SvelteUIElement(ImageCarousel, { state, tags, images })
const images = AllImageProviders.loadImagesFor(tags, imagePrefixes)
const estimated = tags.mapD(tags => AllImageProviders.estimateNumberOfImages(tags, imagePrefixes))
return new SvelteUIElement(ImageCarousel, { state, tags, images, estimated })
},
},
{
@ -785,7 +786,7 @@ export default class SpecialVisualizations {
nameKey: nameKey,
fallbackName,
},
state.featureSwitchIsTesting,
state.featureSwitchIsTesting
)
return new SvelteUIElement(StarsBarIcon, {
score: reviews.average,
@ -824,9 +825,16 @@ export default class SpecialVisualizations {
nameKey: nameKey,
fallbackName,
},
state.featureSwitchIsTesting,
state.featureSwitchIsTesting
)
return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer, question })
return new SvelteUIElement(ReviewForm, {
reviews,
state,
tags,
feature,
layer,
question,
})
},
},
{
@ -855,7 +863,7 @@ export default class SpecialVisualizations {
nameKey: nameKey,
fallbackName,
},
state.featureSwitchIsTesting,
state.featureSwitchIsTesting
)
return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer })
},
@ -885,23 +893,15 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
return new Combine([
SpecialVisualizations.specialVisualisationsDict.get("create_review").constr(
state,
tagSource,
args,
feature,
layer,
),
SpecialVisualizations.specialVisualisationsDict.get("list_reviews").constr(
state,
tagSource,
args,
feature,
layer,
),
SpecialVisualizations.specialVisualisationsDict
.get("create_review")
.constr(state, tagSource, args, feature, layer),
SpecialVisualizations.specialVisualisationsDict
.get("list_reviews")
.constr(state, tagSource, args, feature, layer),
])
},
},
@ -918,7 +918,7 @@ export default class SpecialVisualizations {
constr(
state: SpecialVisualizationState,
_: UIEventSource<Record<string, string>>,
argument: string[],
argument: string[]
): BaseUIElement {
const [text] = argument
return new SvelteUIElement(ImportReviewIdentity, { state, text })
@ -975,7 +975,7 @@ export default class SpecialVisualizations {
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
args: string[],
args: string[]
): SvelteUIElement {
const keyToUse = args[0]
const prefix = args[1]
@ -1012,17 +1012,17 @@ export default class SpecialVisualizations {
return undefined
}
const allUnits: Unit[] = [].concat(
...(state?.theme?.layers?.map((lyr) => lyr.units) ?? []),
...(state?.theme?.layers?.map((lyr) => lyr.units) ?? [])
)
const unit = allUnits.filter((unit) =>
unit.isApplicableToKey(key),
unit.isApplicableToKey(key)
)[0]
if (unit === undefined) {
return value
}
const getCountry = () => tagSource.data._country
return unit.asHumanLongValue(value, getCountry)
}),
})
)
},
},
@ -1076,7 +1076,7 @@ export default class SpecialVisualizations {
constr: (state) => {
return new SubtleButton(
new SvelteUIElement(Trash),
Translations.t.general.removeLocationHistory,
Translations.t.general.removeLocationHistory
).onClick(() => {
state.historicalUserLocations.features.setData([])
state.selectedElement.setData(undefined)
@ -1117,10 +1117,10 @@ export default class SpecialVisualizations {
new SvelteUIElement(NoteCommentElement, {
comment,
state,
}),
),
})
)
).SetClass("flex flex-col")
}),
})
),
},
{
@ -1153,7 +1153,7 @@ export default class SpecialVisualizations {
tags: UIEventSource<Record<string, string>>,
_: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
) => {
return new SvelteUIElement(FeatureTitle, { state, tags, feature, layer })
},
@ -1171,8 +1171,8 @@ export default class SpecialVisualizations {
const challenge = Stores.FromPromise(
Utils.downloadJsonCached<MaprouletteTask>(
`${Maproulette.defaultEndpoint}/challenge/${parentId}`,
24 * 60 * 60 * 1000,
),
24 * 60 * 60 * 1000
)
)
return new VariableUiElement(
@ -1197,7 +1197,7 @@ export default class SpecialVisualizations {
} else {
return [title, new List(listItems)]
}
}),
})
)
},
docs: "Fetches the metadata of MapRoulette campaign that this task is part of and shows those details (namely `title`, `description` and `instruction`).\n\nThis reads the property `mr_challengeId` to detect the parent campaign.",
@ -1211,15 +1211,15 @@ export default class SpecialVisualizations {
"\n" +
"```json\n" +
"{\n" +
" \"id\": \"mark_duplicate\",\n" +
" \"render\": {\n" +
" \"special\": {\n" +
" \"type\": \"maproulette_set_status\",\n" +
" \"message\": {\n" +
" \"en\": \"Mark as not found or false positive\"\n" +
' "id": "mark_duplicate",\n' +
' "render": {\n' +
' "special": {\n' +
' "type": "maproulette_set_status",\n' +
' "message": {\n' +
' "en": "Mark as not found or false positive"\n' +
" },\n" +
" \"status\": \"2\",\n" +
" \"image\": \"close\"\n" +
' "status": "2",\n' +
' "image": "close"\n' +
" }\n" +
" }\n" +
"}\n" +
@ -1295,7 +1295,7 @@ export default class SpecialVisualizations {
(l) =>
l.name !== null &&
l.title &&
state.perLayer.get(l.id) !== undefined,
state.perLayer.get(l.id) !== undefined
)
.map(
(l) => {
@ -1305,8 +1305,8 @@ export default class SpecialVisualizations {
const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox)
return new StatisticsPanel(fsBboxed)
},
[state.mapProperties.bounds],
),
[state.mapProperties.bounds]
)
)
},
},
@ -1376,7 +1376,7 @@ export default class SpecialVisualizations {
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[],
args: string[]
): SvelteUIElement {
let [text, href, classnames, download, ariaLabel, icon] = args
if (download === "") {
@ -1414,7 +1414,7 @@ export default class SpecialVisualizations {
},
},
null,
" ",
" "
) +
"\n```",
args: [
@ -1438,7 +1438,7 @@ export default class SpecialVisualizations {
featureTags: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
) {
const [key, tr, classesRaw] = args
let classes = classesRaw ?? ""
@ -1456,7 +1456,7 @@ export default class SpecialVisualizations {
"Could not create a special visualization for multi(",
args.join(", ") + ")",
"no properties found for object",
feature.properties.id,
feature.properties.id
)
return undefined
}
@ -1473,7 +1473,7 @@ export default class SpecialVisualizations {
elements.push(subsTr)
}
return elements
}),
})
)
},
},
@ -1493,7 +1493,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
return new VariableUiElement(
tagSource.map((tags) => {
@ -1505,7 +1505,7 @@ export default class SpecialVisualizations {
console.error("Cannot create a translation for", v, "due to", e)
return JSON.stringify(v)
}
}),
})
)
},
},
@ -1525,7 +1525,7 @@ export default class SpecialVisualizations {
tags: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
const key = argument[0]
return new SvelteUIElement(FediverseLink, { key, tags, state })
@ -1547,7 +1547,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
return new FixedUiElement("{" + args[0] + "}")
},
@ -1568,7 +1568,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
const key = argument[0] ?? "value"
return new VariableUiElement(
@ -1586,12 +1586,12 @@ export default class SpecialVisualizations {
} catch (e) {
return new FixedUiElement(
"Could not parse this tag: " +
JSON.stringify(value) +
" due to " +
e,
JSON.stringify(value) +
" due to " +
e
).SetClass("alert")
}
}),
})
)
},
},
@ -1612,7 +1612,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
const giggityUrl = argument[0]
return new SvelteUIElement(Giggity, { tags: tagSource, state, giggityUrl })
@ -1628,12 +1628,12 @@ export default class SpecialVisualizations {
_: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
const tags = (<ThemeViewState>(
state
)).geolocation.currentUserLocation.features.map(
(features) => features[0]?.properties,
(features) => features[0]?.properties
)
return new Combine([
new SvelteUIElement(OrientationDebugPanel, {}),
@ -1655,7 +1655,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
return new SvelteUIElement(MarkAsFavourite, {
tags: tagSource,
@ -1675,7 +1675,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
return new SvelteUIElement(MarkAsFavouriteMini, {
tags: tagSource,
@ -1695,7 +1695,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
return new SvelteUIElement(DirectionIndicator, { state, feature })
},
@ -1708,7 +1708,7 @@ export default class SpecialVisualizations {
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
feature: Feature
): SvelteUIElement {
return new SvelteUIElement(QrCode, { state, tags, feature })
},
@ -1727,7 +1727,7 @@ export default class SpecialVisualizations {
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
args: string[],
args: string[]
): BaseUIElement {
const key = args[0] === "" ? "_direction:centerpoint" : args[0]
return new VariableUiElement(
@ -1738,11 +1738,11 @@ export default class SpecialVisualizations {
})
.mapD((value) => {
const dir = GeoOperations.bearingToHuman(
GeoOperations.parseBearing(value),
GeoOperations.parseBearing(value)
)
console.log("Human dir", dir)
return Translations.t.general.visualFeedback.directionsAbsolute[dir]
}),
})
)
},
},
@ -1772,7 +1772,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
const url = args[0]
const readonly = args[3] === "yes"
@ -1798,12 +1798,12 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
return new Toggle(
undefined,
new SvelteUIElement(LoginButton, { osmConnection: state.osmConnection }),
state.osmConnection.isLoggedIn,
state.osmConnection.isLoggedIn
)
},
},
@ -1841,7 +1841,7 @@ export default class SpecialVisualizations {
tags: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
const key = argument[0] ?? "website"
const useProxy = argument[1] !== "no"
@ -1849,7 +1849,7 @@ export default class SpecialVisualizations {
const isClosed = (argument[4] ?? "yes") === "yes"
const countryStore: Store<string | undefined> = tags.mapD(
(tags) => tags._country,
(tags) => tags._country
)
const sourceUrl: Store<string | undefined> = tags.mapD((tags) => {
if (!tags[key] || tags[key] === "undefined") {
@ -1871,24 +1871,24 @@ export default class SpecialVisualizations {
const features =
await LinkedDataLoader.fetchVeloparkEntry(
url,
loadAll,
loadAll
)
const feature =
features.find(
(f) => f.properties["ref:velopark"] === url,
(f) => f.properties["ref:velopark"] === url
) ?? features[0]
const properties = feature.properties
properties["ref:velopark"] = url
console.log(
"Got properties from velopark:",
properties,
properties
)
return properties
} catch (e) {
console.error(e)
throw e
}
})(),
})()
)
}
if (country === undefined) {
@ -1900,29 +1900,29 @@ export default class SpecialVisualizations {
return await LinkedDataLoader.fetchJsonLd(
url,
{ country },
useProxy ? "proxy" : "fetch-lod",
useProxy ? "proxy" : "fetch-lod"
)
} catch (e) {
console.log(
"Could not get with proxy/download LOD, attempting to download directly. Error for ",
url,
"is",
e,
e
)
return await LinkedDataLoader.fetchJsonLd(
url,
{ country },
"fetch-raw",
"fetch-raw"
)
}
})(),
})()
)
},
[countryStore],
[countryStore]
)
externalData.addCallbackAndRunD((lod) =>
console.log("linked_data_from_website received the following data:", lod),
console.log("linked_data_from_website received the following data:", lod)
)
return new Toggle(
@ -1937,7 +1937,7 @@ export default class SpecialVisualizations {
collapsed: isClosed,
}),
undefined,
sourceUrl.map((url) => !!url),
sourceUrl.map((url) => !!url)
)
},
},
@ -1957,7 +1957,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
const text = argument[0]
const cssClasses = argument[1]
@ -1979,7 +1979,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
const translation = tagSource.map((tags) => {
const layer = state.theme.getMatchingLayer(tags)
@ -2011,7 +2011,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): SvelteUIElement {
return new SvelteUIElement<any, any, any>(ClearCaches, {
msg: argument[0] ?? "Clear local caches",
@ -2036,7 +2036,7 @@ export default class SpecialVisualizations {
tags: UIEventSource<Record<string, string>>,
argument: string[],
selectedElement: Feature,
layer: LayerConfig,
layer: LayerConfig
): SvelteUIElement {
const [header, labelsStr] = argument
const labels = labelsStr.split(";").map((x) => x.trim())
@ -2059,7 +2059,7 @@ export default class SpecialVisualizations {
tags: UIEventSource<Record<string, string>>,
argument: string[],
selectedElement: Feature,
layer: LayerConfig,
layer: LayerConfig
): SvelteUIElement {
const t = Translations.t.preset_type
const question: QuestionableTagRenderingConfigJson = {
@ -2099,7 +2099,7 @@ export default class SpecialVisualizations {
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig,
layer: LayerConfig
): BaseUIElement {
const text = argument[0]
return new SubtleButton(undefined, text).onClick(() => {
@ -2130,7 +2130,7 @@ export default class SpecialVisualizations {
"Invalid special visualisation found: funcName is undefined or doesn't match " +
regex +
invalid.map((sp) => sp.i).join(", ") +
". Did you perhaps type \n funcName: \"funcname\" // type declaration uses COLON\ninstead of:\n funcName = \"funcName\" // value definition uses EQUAL"
'. Did you perhaps type \n funcName: "funcname" // type declaration uses COLON\ninstead of:\n funcName = "funcName" // value definition uses EQUAL'
)
}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import EditLayerState from "./EditLayerState"
import { EditJsonState } from "./EditLayerState"
import type { ConfigMeta } from "./configMeta"
import { UIEventSource } from "../../Logic/UIEventSource"
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
@ -9,13 +9,12 @@
import SchemaBasedInput from "./SchemaBasedInput.svelte"
import type { JsonSchemaType } from "./jsonSchema"
import ShowConversionMessage from "./ShowConversionMessage.svelte"
import type { Translatable } from "../../Models/ThemeConfig/Json/Translatable"
/**
* If 'types' is defined: allow the user to pick one of the types to input.
*/
export let state: EditLayerState
export let state: EditJsonState<any>
export let path: (string | number)[] = []
export let schema: ConfigMeta
let expertMode = state.expertMode
@ -31,8 +30,9 @@
let lastIsString = false
{
const types: string | string[] = Array.isArray(schema.type)
? schema.type[schema.type.length - 1].type
? schema.type[schema.type.length - 1]["type"]
: []
lastIsString = types === "string" || (Array.isArray(types) && types.some((i) => i === "string"))
}
@ -64,6 +64,7 @@
inline: true,
type: schema.hints.typehint,
addExtraTags: ["chosen_type_index="],
helperArgs: []
},
}
let tags = new UIEventSource<Record<string, string>>({})
@ -114,7 +115,7 @@
const type = schema.type[i]
let optionalMatches = 0
for (const key of Object.keys(type.properties ?? {})) {
if (!!existingValue[key]) {
if (existingValue[key]) {
optionalMatches++
}
}

View file

@ -1,11 +1,11 @@
{
"contributors": [
{
"commits": 8826,
"commits": 8870,
"contributor": "Pieter Vander Vennet"
},
{
"commits": 506,
"commits": 514,
"contributor": "Robin van der Linde"
},
{
@ -152,6 +152,10 @@
"commits": 6,
"contributor": "David Haberthür"
},
{
"commits": 5,
"contributor": "tiptoptom"
},
{
"commits": 4,
"contributor": "Languages add-on"
@ -168,6 +172,10 @@
"commits": 4,
"contributor": "Ward Beyens"
},
{
"commits": 3,
"contributor": "Hufkratzer"
},
{
"commits": 3,
"contributor": "Thierry1030"
@ -180,6 +188,10 @@
"commits": 3,
"contributor": "Léo Villeveygoux"
},
{
"commits": 2,
"contributor": "Samu__"
},
{
"commits": 2,
"contributor": "Jens Köcke"
@ -196,10 +208,6 @@
"commits": 2,
"contributor": "Robbert Gurdeep Singh"
},
{
"commits": 2,
"contributor": "tiptoptom"
},
{
"commits": 2,
"contributor": "Niklas Vogel"

View file

@ -1,6 +1,5 @@
{
"ca": "català",
"cs": "čeština",
"da": "dansk",
"de": "Deutsch",
"en": "English",
@ -25,6 +24,7 @@
"sl": "slovenščina",
"sv": "svenska",
"uk": "українська мова",
"zgh": "ⵜⴰⵎⴰⵣⵉⵖⵜ ⵜⴰⵏⴰⵡⴰⵢⵜ ⵜⴰⵎⵖⵔⵉⴱⵉⵜ",
"zh_Hans": "简体中文",
"zh_Hant": "繁體中文"
}

File diff suppressed because it is too large Load diff

View file

@ -1744,6 +1744,18 @@
"types": "<span class=\"text-lg font-bold\">Use a different icon depending on the value of some attributes</span> ; icon",
"question": "What icon should be used?",
"suggestions": [
{
"if": "value=nsi_brand.icon",
"then": "Use icons for brand from the Name Suggestion Index"
},
{
"if": "value=nsi_operator.icon",
"then": "Use icons for operator from the Name Suggestion Index"
},
{
"if": "value=id_presets.shop_rendering",
"then": "Use shop preset icons from iD"
},
{
"if": "value=addSmall",
"then": "addSmall",
@ -2061,7 +2073,7 @@
"type": "string"
}
],
"description": ""
"description": "To reuse icons from a different layer of a library:\n- The library layer has, within tagRenderings one which will output the URL of the image (e.g. mappings: {\"if\": \"shop=xyz\", then: \"./assets/icons/shop_xyz.png\"})\n- Use \"layer_id.tagrendering_id\"\nNote that if you reuse icons from a different icon set, you'll probably want to use `override` to set a default rendering"
},
{
"path": [
@ -2291,8 +2303,65 @@
"hints": {
"group": "hidden"
},
"type": "array",
"description": "A list of extra badges to show next to the icon as small badge\nThey will be added as a 25% height icon at the bottom right of the icon, with all the badges in a flex layout.\nNote: strings are interpreted as icons, so layering and substituting is supported. You can use `circle:white;./my_icon.svg` to add a background circle"
"type": [
{
"type": "object",
"properties": {
"if": {
"$ref": "#/definitions/TagConfigJson",
"description": "The main representation of Tags.\nSee https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md for more documentation\n\ntype: tag"
},
"then": {
"description": "Badge to show\nType: icon",
"anyOf": [
{
"description": "Mostly used for lineRendering and pointRendering",
"type": "object",
"properties": {
"render": {
"description": "question: What value should be shown (if no predefined option matches)?\n\nThis piece of text will be shown in the infobox.\nNote that \"&LBRACEkey&RBRACE\"-parts are substituted by the corresponding values of the element.\n\nThis value will be used if there is no mapping which matches (or there are no matches)\nNote that this is a HTML-interpreted value, so you can add links as e.g. '&lt;a href='{website}'>{website}&lt;/a>' or include images such as `This is of type A &lt;br>&lt;img src='typeA-icon.svg' />`",
"type": "string"
},
"mappings": {
"description": "Allows fixed-tag inputs, shown either as radiobuttons or as checkboxes",
"type": "array",
"items": {
"type": "object",
"properties": {
"if": {
"$ref": "#/definitions/TagConfigJson",
"description": "question: When should this single mapping match?\n\nIf this condition is met, then the text under `then` will be shown.\nIf no value matches, and the user selects this mapping as an option, then these tags will be uploaded to OSM.\n\nFor example: {'if': 'diet:vegetarion=yes', 'then':'A vegetarian option is offered here'}\n\nThis can be an substituting-tag as well, e.g. {'if': 'addr:street:={_calculated_nearby_streetname}', 'then': '{_calculated_nearby_streetname}'}"
},
"then": {
"description": "question: What text should be shown?\n\nIf the condition `if` is met, the text `then` will be rendered.\nIf not known yet, the user will be presented with `then` as an option",
"type": "string"
}
},
"required": [
"if",
"then"
]
}
}
},
"additionalProperties": false
},
{
"type": "string"
}
]
}
},
"required": [
"if",
"then"
]
},
{
"type": "string"
}
],
"description": "A list of extra badges to show next to the icon as small badge\nThey will be added as a 25% height icon at the bottom right of the icon, with all the badges in a flex layout.\nNote: strings are interpreted as icons, so layering and substituting is supported. You can use `circle:white;./my_icon.svg` to add a background circle\nAlternatively, this can reuse a _tagRendering_ from another layer, e.g. one of the 'icons'-tagrenderings.\nSee ExpandIconBadges on how this is handled"
},
{
"path": [
@ -10763,6 +10832,10 @@
"if": "value=caravansites",
"then": "caravansites - camper sites"
},
{
"if": "value=charge_point",
"then": "charge_point - Layer showing individual charge points within a charging station"
},
{
"if": "value=charging_station",
"then": "charging_station - A charging station"
@ -11051,6 +11124,14 @@
"if": "value=note",
"then": "note - This layer shows notes on OpenStreetMap. Having this layer in your theme will trigger the 'add new note' functionality in the 'addNewPoint'-popup (or if your theme has no presets, it'll enable adding notes)"
},
{
"if": "value=nsi_brand",
"then": "nsi_brand - Exposes part of the NSI to reuse in other themes, e.g. for rendering"
},
{
"if": "value=nsi_operator",
"then": "nsi_operator - Exposes part of the NSI to reuse in other themes, e.g. for rendering"
},
{
"if": "value=observation_tower",
"then": "observation_tower - Towers with a panoramic view"
@ -11155,6 +11236,10 @@
"if": "value=school",
"then": "school - Schools giving primary and secondary education and post-secondary, non-tertiary education. Note that this level of education does not imply an age of the pupiles"
},
{
"if": "value=scouting_group",
"then": "scouting_group - A map showing scouting groups."
},
{
"if": "value=search",
"then": "search - Priviliged layer showing the search results"
@ -19976,6 +20061,16 @@
"type": "string",
"description": "An id/name for this filter, used to set the URL parameters"
},
{
"path": [
"filter",
"strict"
],
"required": false,
"hints": {},
"type": "boolean",
"description": "If set, the options will be pruned. Only items for which the filter match the layer source will be kept.\nFor example, we import types of brands from the nsi. This contains a ton of items, e.g.\n[{question: \"Brand X\", osmTags: {\"and\": [\"shop=clothes\", \"brand=Brand X]}, {osmTags: {\"and\": \"shop=convenience\", ...} ...} ]\nOf course, when making a layer about `shop=clothes`, we'll only want to keep the clothes shops.\nIf set to strict and the source is `shop=clothes`, only those options which have shop=clothes will be returned"
},
{
"path": [
"filter",
@ -20817,5 +20912,16 @@
}
],
"description": "In the move wizard, the option `snap object onto {snapName}` is shown"
},
{
"path": [
"#dont-translate"
],
"required": false,
"hints": {
"group": "hidden"
},
"type": "string",
"description": ""
}
]

View file

@ -711,6 +711,10 @@
"if": "value=caravansites",
"then": "<b>caravansites</b> (builtin) - camper sites"
},
{
"if": "value=charge_point",
"then": "<b>charge_point</b> (builtin) - Layer showing individual charge points within a charging station"
},
{
"if": "value=charging_station",
"then": "<b>charging_station</b> (builtin) - A charging station"
@ -999,6 +1003,14 @@
"if": "value=note",
"then": "<b>note</b> (builtin) - This layer shows notes on OpenStreetMap. Having this layer in your theme will trigger the 'add new note' functionality in the 'addNewPoint'-popup (or if your theme has no presets, it'll enable adding notes)"
},
{
"if": "value=nsi_brand",
"then": "<b>nsi_brand</b> (builtin) - Exposes part of the NSI to reuse in other themes, e.g. for rendering"
},
{
"if": "value=nsi_operator",
"then": "<b>nsi_operator</b> (builtin) - Exposes part of the NSI to reuse in other themes, e.g. for rendering"
},
{
"if": "value=observation_tower",
"then": "<b>observation_tower</b> (builtin) - Towers with a panoramic view"
@ -1103,6 +1115,10 @@
"if": "value=school",
"then": "<b>school</b> (builtin) - Schools giving primary and secondary education and post-secondary, non-tertiary education. Note that this level of education does not imply an age of the pupiles"
},
{
"if": "value=scouting_group",
"then": "<b>scouting_group</b> (builtin) - A map showing scouting groups."
},
{
"if": "value=search",
"then": "<b>search</b> (builtin) - Priviliged layer showing the search results"
@ -13427,6 +13443,10 @@
"if": "value=caravansites",
"then": "caravansites - camper sites"
},
{
"if": "value=charge_point",
"then": "charge_point - Layer showing individual charge points within a charging station"
},
{
"if": "value=charging_station",
"then": "charging_station - A charging station"
@ -13715,6 +13735,14 @@
"if": "value=note",
"then": "note - This layer shows notes on OpenStreetMap. Having this layer in your theme will trigger the 'add new note' functionality in the 'addNewPoint'-popup (or if your theme has no presets, it'll enable adding notes)"
},
{
"if": "value=nsi_brand",
"then": "nsi_brand - Exposes part of the NSI to reuse in other themes, e.g. for rendering"
},
{
"if": "value=nsi_operator",
"then": "nsi_operator - Exposes part of the NSI to reuse in other themes, e.g. for rendering"
},
{
"if": "value=observation_tower",
"then": "observation_tower - Towers with a panoramic view"
@ -13819,6 +13847,10 @@
"if": "value=school",
"then": "school - Schools giving primary and secondary education and post-secondary, non-tertiary education. Note that this level of education does not imply an age of the pupiles"
},
{
"if": "value=scouting_group",
"then": "scouting_group - A map showing scouting groups."
},
{
"if": "value=search",
"then": "search - Priviliged layer showing the search results"
@ -35179,6 +35211,10 @@
"if": "value=caravansites",
"then": "caravansites - camper sites"
},
{
"if": "value=charge_point",
"then": "charge_point - Layer showing individual charge points within a charging station"
},
{
"if": "value=charging_station",
"then": "charging_station - A charging station"
@ -35467,6 +35503,14 @@
"if": "value=note",
"then": "note - This layer shows notes on OpenStreetMap. Having this layer in your theme will trigger the 'add new note' functionality in the 'addNewPoint'-popup (or if your theme has no presets, it'll enable adding notes)"
},
{
"if": "value=nsi_brand",
"then": "nsi_brand - Exposes part of the NSI to reuse in other themes, e.g. for rendering"
},
{
"if": "value=nsi_operator",
"then": "nsi_operator - Exposes part of the NSI to reuse in other themes, e.g. for rendering"
},
{
"if": "value=observation_tower",
"then": "observation_tower - Towers with a panoramic view"
@ -35571,6 +35615,10 @@
"if": "value=school",
"then": "school - Schools giving primary and secondary education and post-secondary, non-tertiary education. Note that this level of education does not imply an age of the pupiles"
},
{
"if": "value=scouting_group",
"then": "scouting_group - A map showing scouting groups."
},
{
"if": "value=search",
"then": "search - Priviliged layer showing the search results"

View file

@ -17,7 +17,7 @@
"contributor": "Anonymous"
},
{
"commits": 110,
"commits": 112,
"contributor": "mcliquid"
},
{
@ -29,7 +29,7 @@
"contributor": "Robin van der Linde"
},
{
"commits": 78,
"commits": 80,
"contributor": "mike140"
},
{
@ -50,11 +50,11 @@
},
{
"commits": 51,
"contributor": "gallegonovato"
"contributor": "Supaplex"
},
{
"commits": 50,
"contributor": "Supaplex"
"commits": 51,
"contributor": "gallegonovato"
},
{
"commits": 45,
@ -588,6 +588,10 @@
"commits": 2,
"contributor": "Leo Alcaraz"
},
{
"commits": 1,
"contributor": "Túllio Morais Franca"
},
{
"commits": 1,
"contributor": "Moimoi Ty"