Merge develop

This commit is contained in:
Pieter Vander Vennet 2025-01-12 13:33:38 +01:00
commit baa7379fbf
7880 changed files with 2079327 additions and 39792 deletions

View file

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

View file

@ -1,9 +1,7 @@
import ThemeConfig from "../Models/ThemeConfig/ThemeConfig"
import ThemeConfig, { MinimalThemeInformation } from "../Models/ThemeConfig/ThemeConfig"
import { QueryParameters } from "./Web/QueryParameters"
import { AllKnownLayouts } from "../Customizations/AllKnownLayouts"
import { FixedUiElement } from "../UI/Base/FixedUiElement"
import { Utils } from "../Utils"
import LZString from "lz-string"
import { FixLegacyTheme } from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
import known_layers from "../assets/generated/known_layers.json"
@ -15,10 +13,10 @@ import questions from "../assets/generated/layers/questions.json"
import { DoesImageExist, PrevalidateTheme } from "../Models/ThemeConfig/Conversion/Validation"
import { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion"
import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"
import Hash from "./Web/Hash"
import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import { ThemeConfigJson } from "../Models/ThemeConfig/Json/ThemeConfigJson"
import { ValidateThemeAndLayers } from "../Models/ThemeConfig/Conversion/ValidateThemeAndLayers"
import * as theme_overview from "../assets/generated/theme_overview.json"
export default class DetermineTheme {
private static readonly _knownImages = new Set(Array.from(licenses).map((l) => l.path))
@ -87,14 +85,15 @@ export default class DetermineTheme {
"The layout to load into MapComplete"
).data
const id = layoutId?.toLowerCase()
const layouts = AllKnownLayouts.allKnownLayouts
if (layouts.size() == 0) {
const themes: MinimalThemeInformation[] = theme_overview.themes
if (themes.length == 0) {
throw "Build failed or running, no layouts are known at all"
}
if (layouts.getConfig(id) === undefined) {
const themeInfo = themes.find(th => th.id === id)
if (themeInfo === undefined) {
const alternatives = Utils.sortedByLevenshteinDistance(
id,
Array.from(layouts.keys()),
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(
@ -102,7 +101,10 @@ export default class DetermineTheme {
)}`
throw msg
}
return layouts.get(id)
// Actually fetch the theme
const config = await Utils.downloadJsonCached<ThemeConfigJson>("./assets/generated/themes/"+id+".json", 1000*60*60*60)
return new ThemeConfig(config, true)
}
private static getSharedTagRenderings(): Map<string, QuestionableTagRenderingConfigJson> {
@ -152,9 +154,6 @@ export default class DetermineTheme {
json = {
id: json.id,
description: json.description,
descriptionTail: {
en: "<div class='alert'>Layer only mode.</div> The loaded custom theme actually isn't a custom theme, but only contains a layer.",
},
icon,
title: json.name,
layers: [json],

View file

@ -184,9 +184,7 @@ export default class LayerState {
", but this layer was not loaded"
)
}
console.warn(
"Linking filter and isDisplayed-states of " + layer.id + " and " + layer.filterIsSameAs
)
// We make a shallow copy, reusing the same underlying stores
const copy = new FilteredLayer(layer, toReuse.appliedFilters, toReuse.isDisplayed)
filteredLayers.set(layer.id, copy)
}

View file

@ -1,42 +1,14 @@
import { Utils } from "../../Utils"
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
export class ThemeMetaTagging {
public static readonly themeName = "usersettings"
public static readonly themeName = "usersettings"
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) {
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
feat.properties._description
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
?.at(1)
)
Utils.AddLazyProperty(
feat.properties,
"_d",
() => feat.properties._description?.replace(/&lt;/g, "<")?.replace(/&gt;/g, ">") ?? ""
)
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.href.match(/mastodon|en.osm.town/) !== null
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(
feat.properties,
"_mastodon_candidate",
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
)
feat.properties["__current_backgroun"] = "initial_value"
}
}
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/&lt;/g,'<')?.replace(/&gt;/g,'>') ?? '' )
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
feat.properties['__current_backgroun'] = 'initial_value'
}
}

View file

@ -130,39 +130,38 @@ export class And extends TagsFilter {
* t1.shadows(t2) // => false
* t2.shadows(t0) // => false
* t2.shadows(t1) // => false
*
*
* 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
*/
shadows(other: TagsFilter): boolean {
if (!(other instanceof And)) {
return false
}
const phrases: 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 matchFound = false
for (const otherTag of other.and) {
matchFound = selfTag.shadows(otherTag)
if (matchFound) {
break
let shadowsSome = false;
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;
}
shadowsSome ||= doesShadow;
shadowsAll &&= doesShadow;
}
if (!matchFound) {
return false
// 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;
}
}
for (const otherTag of other.and) {
let matchFound = false
for (const selfTag of this.and) {
matchFound = selfTag.shadows(otherTag)
if (matchFound) {
break
}
}
if (!matchFound) {
return false
}
}
return true
return !shadowedOthers.some(v => !v);
}
usedKeys(): string[] {
@ -182,11 +181,13 @@ export class And extends TagsFilter {
}
/**
* IN some contexts, some expressions can be considered true, e.g.
* In some contexts, some expressions can be considered true, e.g.
* (X=Y | (A=B & X=Y))
* ^---------^
* When the evaluation hits (A=B & X=Y), we know _for sure_ that X=Y does _not_ match, as it would have matched the first clause otherwise.
* This means that the entire 'AND' is considered FALSE
* This means that the entire 'AND' is considered FALSE in this case; but this is already handled by the first half.
* In other words: this long expression is equivalent to (A=B | X=Y).
*
*
* @return only phrases that should be kept.
* @param knownExpression The expression which is known in the subexpression and for which calculations can be done
@ -204,13 +205,14 @@ export class And extends TagsFilter {
* const expr = <And> TagUtils.Tag({and: ["sport=climbing", {or:["club~*", "office~*"]}]} )
* expr.removePhraseConsideredKnown(new Tag("club","climbing"), false) // => expr
*/
removePhraseConsideredKnown(
public removePhraseConsideredKnown(
knownExpression: TagsFilter,
value: boolean
): (TagsFilterClosed & OptimizedTag) | boolean {
const newAnds: TagsFilter[] = []
for (const tag of this.and) {
if (tag instanceof And) {
console.trace("Improper optimization")
throw (
"Optimize expressions before using removePhraseConsideredKnown. Found an AND in an AND: " +
this.asHumanString()

View file

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

View file

@ -4,6 +4,8 @@ import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { ExpressionSpecification } from "maplibre-gl"
import { RegexTag } from "./RegexTag"
import { OptimizedTag } from "./TagTypes"
import { Or } from "./Or"
import { And } from "./And"
export class Tag extends TagsFilter {
public key: string
@ -148,6 +150,12 @@ 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 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,6 +690,17 @@ export class TagUtils {
return result
}
public static removeKnownParts(tag: TagsFilter, known: TagsFilter, valueOfKnown = true): TagsFilter | boolean{
const tagOrBool = And.construct([tag]).optimize()
if(tagOrBool === true || tagOrBool === false){
return tagOrBool
}
if(tagOrBool instanceof And){
return tagOrBool.removePhraseConsideredKnown(known, valueOfKnown)
}
return tagOrBool
}
/**
* Returns `true` if at least one element of the 'guards' shadows one element of the 'listToFilter'.
*
@ -699,7 +710,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)))
}
@ -743,7 +754,7 @@ export class TagUtils {
values.push(i + "")
}
return values
})
}),
)
return Utils.NoNull(spec)
}
@ -751,13 +762,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) {
@ -839,13 +850,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,
)
}
@ -967,10 +978,19 @@ 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()
if (opt === true || opt === false) {
return opt
}
return opt.asJson()
}
}

View file

@ -9,7 +9,8 @@ export abstract class TagsFilter {
/**
* Indicates some form of equivalency:
* if `this.shadows(t)`, then `this.matches(properties)` implies that `t.matches(properties)` for all possible properties
* if `this.shadows(t)`, then `this.matches(properties)` implies that `t.matches(properties)` for all possible properties.
* In other words: 'this' is a _stronger_ condition then 't'
*/
abstract shadows(other: TagsFilter): boolean

View file

@ -1,12 +1,13 @@
import * as nsiFeatures from "../../../node_modules/name-suggestion-index/dist/featureCollection.json"
import { LocationConflation } from "@rapideditor/location-conflation"
import type { Feature, MultiPolygon } from "geojson"
import type { Feature, FeatureCollection, MultiPolygon } from "geojson"
import { Utils } from "../../Utils"
import * as turf from "@turf/turf"
import { Mapping } from "../../Models/ThemeConfig/TagRenderingConfig"
import { Tag } from "../Tags/Tag"
import { TypedTranslation } from "../../UI/i18n/Translation"
import { RegexTag } from "../Tags/RegexTag"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { TagUtils } from "../Tags/TagUtils"
/**
* Main name suggestion index file
@ -42,17 +43,18 @@ interface NSIEntry {
* Represents a single brand/operator/flagpole/...
*/
export interface NSIItem {
displayName: string
id: string
readonly displayName: string
readonly id: string
locationSet: {
include: string[]
exclude: string[]
}
tags: Record<string, string>
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<
@ -64,7 +66,7 @@ export default class NameSuggestionIndex {
>
>
private static loco = new LocationConflation(nsiFeatures) // Some additional boundaries
private loco: LocationConflation // Some additional boundaries
private _supportedTypes: string[]
@ -77,10 +79,12 @@ export default class NameSuggestionIndex {
logos: { wikidata?: string; facebook?: string }
}
>
>
>,
features: Readonly<FeatureCollection>,
) {
this.nsiFile = nsiFile
this.nsiWdFile = nsiWdFile
this.loco = new LocationConflation(features)
}
private static inited: NameSuggestionIndex = undefined
@ -89,12 +93,12 @@ export default class NameSuggestionIndex {
if (NameSuggestionIndex.inited) {
return NameSuggestionIndex.inited
}
const [nsi, nsiWd] = await Promise.all(
["assets/data/nsi/nsi.json", "assets/data/nsi/wikidata.min.json"].map((url) =>
Utils.downloadJsonCached(url, 1000 * 60 * 60 * 24 * 30)
)
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),
),
)
NameSuggestionIndex.inited = new NameSuggestionIndex(<any>nsi, <any>nsiWd["wikidata"])
NameSuggestionIndex.inited = new NameSuggestionIndex(<any>nsi, <any>nsiWd["wikidata"], <any>features)
return NameSuggestionIndex.inited
}
@ -125,13 +129,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) {
@ -172,17 +176,17 @@ export default class NameSuggestionIndex {
public async generateMappings(
type: string,
tags: Record<string, string>,
country: string[],
country?: string[],
location?: [number, number],
options?: {
/**
* If set, sort by frequency instead of alphabetically
*/
sortByFrequency: boolean
}
},
): Promise<Mapping[]> {
const mappings: (Mapping & { frequency: number })[] = []
const frequencies = await NameSuggestionIndex.fetchFrequenciesFor(type, country)
const frequencies = country !== undefined ? await NameSuggestionIndex.fetchFrequenciesFor(type, country) : {}
for (const key in tags) {
if (key.startsWith("_")) {
continue
@ -193,7 +197,7 @@ export default class NameSuggestionIndex {
key,
value,
country.join(";"),
location
location,
)
if (!actualBrands) {
continue
@ -201,8 +205,7 @@ export default class NameSuggestionIndex {
for (const nsiItem of actualBrands) {
const tags = nsiItem.tags
const frequency = frequencies[nsiItem.displayName]
const logos = this.nsiWdFile[nsiItem.tags[type + ":wikidata"]]?.logos
const iconUrl = logos?.facebook ?? logos?.wikidata
const iconUrl = this.getIconExternalUrl(nsiItem, type)
const hasIcon = iconUrl !== undefined
let icon = undefined
if (hasIcon) {
@ -239,7 +242,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)
@ -262,7 +265,7 @@ export default class NameSuggestionIndex {
* Returns a list of all brands/operators
* @param type
*/
public allPossible(type: "brand" | "operator"): NSIItem[] {
public allPossible(type: string): NSIItem[] {
const options: NSIItem[] = []
const tags = this.supportedTags(type)
for (const osmKey in tags) {
@ -284,10 +287,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),
)
}
@ -310,7 +313,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]
@ -351,7 +354,7 @@ export default class NameSuggestionIndex {
const key = i.locationSet.include?.join(";") + "-" + i.locationSet.exclude?.join(";")
const fromCache = NameSuggestionIndex.resolvedSets[key]
const resolvedSet =
fromCache ?? NameSuggestionIndex.loco.resolveLocationSet(i.locationSet)
fromCache ?? this.loco.resolveLocationSet(i.locationSet)
if (!fromCache) {
NameSuggestionIndex.resolvedSets[key] = resolvedSet
}
@ -374,9 +377,52 @@ 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
* @param nsiItem
* @param type
*/
private getIconExternalUrl(nsiItem: NSIItem, type: string): string {
const logos = this.nsiWdFile[nsiItem.tags[type + ":wikidata"]]?.logos
return logos?.facebook ?? logos?.wikidata
}
public getIconUrl(nsiItem: NSIItem, type: string) {
let icon = "./assets/data/nsi/logos/" + nsiItem.id
if (this.isSvg(nsiItem, type)) {
icon = icon + ".svg"
}
return icon
}
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`
* Many of those tags are all added, but having only one of them is a good indication that it should match this item.
*
* This method is a heuristic which attempts to move all the brand-related tags into an `or` but still requiring the `shop` and other tags
*
* (More of an extension method on NSIItem)
*/
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 + ":"))) {
brandDetection.push(k + "=" + tags[k])
} else {
required.push(k + "=" + tags[k])
}
}
return <TagConfigJson>TagUtils.optimzeJson({ and: [...required, { or: brandDetection }] })
}
}

View file

@ -1,5 +1,6 @@
import { Utils } from "../../Utils"
import type { FeatureCollection } from "geojson"
import ScriptUtils from "../../../scripts/ScriptUtils"
export interface TagInfoStats {
/**
@ -39,12 +40,12 @@ 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)}`
}
return await Utils.downloadJsonCached<TagInfoStats>(url, 1000 * 60 * 60)
return await Utils.downloadJsonCached<TagInfoStats>(url, 1000 * 60 * 60 * 24)
}
/**
@ -69,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
}
@ -98,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
@ -110,30 +111,43 @@ export default class TagInfo {
try {
return await ti.getStats(key, value)
} catch (e) {
console.warn("Could not fetch info for", countryCode, key, value, "due to", e)
console.warn("Could not fetch info from taginfo for", countryCode, key, value, "due to", e, "Taginfo country specific instance is ", ti._backend)
return undefined
}
}
private static readonly blacklist = ["VI", "GF", "PR"]
public static async getGlobalDistributionsFor(
/**
* Get a taginfo object for every supportedCountry. This statistic is handled by 'f' and written into the passed in object
* @param writeInto
* @param f
* @param key
* @param value
*/
public static async getGlobalDistributionsFor<T>(
writeInto: Record<string, T>,
f: ((stats: TagInfoStats) => T),
key: string,
value?: string
): Promise<Record<string, TagInfoStats>> {
value?: string,
): Promise<number> {
const countriesAll = await this.geofabrikCountries()
const countries = countriesAll
.map((c) => c["iso3166-1:alpha2"]?.[0])
.filter((c) => !!c && TagInfo.blacklist.indexOf(c) < 0)
const perCountry: Record<string, TagInfoStats> = {}
const results = await Promise.all(
countries.map((country) => TagInfo.getDistributionsFor(country, key, value))
)
for (let i = 0; i < countries.length; i++) {
const countryCode = countries[i]
if (results[i]) {
perCountry[countryCode] = results[i]
let downloaded = 0
for (const country of countries) {
if(writeInto[country] !== undefined){
continue
}
const r = await TagInfo.getDistributionsFor(country, key, value)
if(r === undefined){
continue
}
downloaded ++
writeInto[country] = f(r)
}
return perCountry
return downloaded
}
}