forked from MapComplete/MapComplete
Logic: better support for tag optimization and simplifying expressions
This commit is contained in:
parent
bd228a6129
commit
a3d26db84a
11 changed files with 430 additions and 260 deletions
|
@ -1,9 +1,14 @@
|
||||||
import Script from "./Script"
|
import Script from "./Script"
|
||||||
import NameSuggestionIndex, { NSIItem } from "../src/Logic/Web/NameSuggestionIndex"
|
import NameSuggestionIndex, { NSIItem } from "../src/Logic/Web/NameSuggestionIndex"
|
||||||
import * as nsiWD from "../node_modules/name-suggestion-index/dist/wikidata.min.json"
|
import * as nsiWD from "../node_modules/name-suggestion-index/dist/wikidata.min.json"
|
||||||
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs"
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs"
|
||||||
import ScriptUtils from "./ScriptUtils"
|
import ScriptUtils from "./ScriptUtils"
|
||||||
import { Utils } from "../src/Utils"
|
import { Utils } from "../src/Utils"
|
||||||
|
import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
|
||||||
|
import FilterConfigJson, { FilterConfigOptionJson } from "../src/Models/ThemeConfig/Json/FilterConfigJson"
|
||||||
|
import { TagConfigJson } from "../src/Models/ThemeConfig/Json/TagConfigJson"
|
||||||
|
import { TagUtils } from "../src/Logic/Tags/TagUtils"
|
||||||
|
import { And } from "../src/Logic/Tags/And"
|
||||||
|
|
||||||
class DownloadNsiLogos extends Script {
|
class DownloadNsiLogos extends Script {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -43,7 +48,7 @@ class DownloadNsiLogos extends Script {
|
||||||
await ScriptUtils.DownloadFileTo(logos.facebook, path)
|
await ScriptUtils.DownloadFileTo(logos.facebook, path)
|
||||||
// Validate
|
// Validate
|
||||||
const content = readFileSync(path, "utf8")
|
const content = readFileSync(path, "utf8")
|
||||||
if (content.startsWith('{"error"')) {
|
if (content.startsWith("{\"error\"")) {
|
||||||
unlinkSync(path)
|
unlinkSync(path)
|
||||||
console.error("Attempted to fetch", logos.facebook, " but this gave an error")
|
console.error("Attempted to fetch", logos.facebook, " but this gave an error")
|
||||||
} else {
|
} else {
|
||||||
|
@ -86,12 +91,8 @@ class DownloadNsiLogos extends Script {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
async main(): Promise<void> {
|
|
||||||
await this.downloadFor("operator")
|
|
||||||
await this.downloadFor("brand")
|
|
||||||
}
|
|
||||||
|
|
||||||
async downloadFor(type: "brand" | "operator"): Promise<void> {
|
async downloadFor(type: string): Promise<void> {
|
||||||
const nsi = await NameSuggestionIndex.getNsiIndex()
|
const nsi = await NameSuggestionIndex.getNsiIndex()
|
||||||
const items = nsi.allPossible(type)
|
const items = nsi.allPossible(type)
|
||||||
const basePath = "./public/assets/data/nsi/logos/"
|
const basePath = "./public/assets/data/nsi/logos/"
|
||||||
|
@ -109,7 +110,7 @@ class DownloadNsiLogos extends Script {
|
||||||
downloadCount++
|
downloadCount++
|
||||||
}
|
}
|
||||||
return downloaded
|
return downloaded
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
for (let j = 0; j < results.length; j++) {
|
for (let j = 0; j < results.length; j++) {
|
||||||
let didDownload = results[j]
|
let didDownload = results[j]
|
||||||
|
@ -124,6 +125,63 @@ class DownloadNsiLogos extends Script {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async generateRendering(type: string) {
|
||||||
|
const nsi = await NameSuggestionIndex.getNsiIndex()
|
||||||
|
const items = nsi.allPossible(type)
|
||||||
|
const brandPrefix = [type, "name", "alt_name", "operator","brand"]
|
||||||
|
const filterOptions: FilterConfigOptionJson[] = items.map(item => {
|
||||||
|
let brandDetection: string[] = []
|
||||||
|
let required: string[] = []
|
||||||
|
const tags: Record<string, string> = item.tags
|
||||||
|
for (const k in tags) {
|
||||||
|
if (brandPrefix.some(br => k === br || k.startsWith(br + ":"))) {
|
||||||
|
brandDetection.push(k + "=" + tags[k])
|
||||||
|
} else {
|
||||||
|
required.push(k + "=" + tags[k])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const osmTags = <TagConfigJson>TagUtils.optimzeJson({ and: [...required, { or: brandDetection }] })
|
||||||
|
return ({
|
||||||
|
question: item.displayName,
|
||||||
|
icon: nsi.getIconUrl(item, type),
|
||||||
|
osmTags,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const config: LayerConfigJson = {
|
||||||
|
"#dont-translate": "*",
|
||||||
|
id: "nsi_" + type,
|
||||||
|
source: "special:library",
|
||||||
|
description: {
|
||||||
|
en: "Exposes part of the NSI to reuse in other themes, e.g. for rendering",
|
||||||
|
},
|
||||||
|
pointRendering: null,
|
||||||
|
filter: [
|
||||||
|
<any> {
|
||||||
|
id: type,
|
||||||
|
strict: true,
|
||||||
|
options: [{question: type}, ...filterOptions],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
allowMove: false,
|
||||||
|
}
|
||||||
|
const path = "./assets/layers/nsi_" + type
|
||||||
|
mkdirSync(path, { recursive: true })
|
||||||
|
writeFileSync(path + "/nsi_" + type + ".json", JSON.stringify(config, null, " "))
|
||||||
|
console.log("Written", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
async main(): Promise<void> {
|
||||||
|
const nsi = await NameSuggestionIndex.getNsiIndex()
|
||||||
|
const types = ["brand", "operator"]
|
||||||
|
for (const type of types) {
|
||||||
|
await this.generateRendering(type)
|
||||||
|
// await this.downloadFor(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
new DownloadNsiLogos().run()
|
new DownloadNsiLogos().run()
|
||||||
|
|
|
@ -130,39 +130,38 @@ export class And extends TagsFilter {
|
||||||
* t1.shadows(t2) // => false
|
* t1.shadows(t2) // => false
|
||||||
* t2.shadows(t0) // => false
|
* t2.shadows(t0) // => false
|
||||||
* t2.shadows(t1) // => 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 {
|
shadows(other: TagsFilter): boolean {
|
||||||
if (!(other instanceof And)) {
|
const phrases: TagsFilter[] = other instanceof And ? other.and : [other];
|
||||||
return false
|
// 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) {
|
for (const selfTag of this.and) {
|
||||||
let matchFound = false
|
let shadowsSome = false;
|
||||||
for (const otherTag of other.and) {
|
let shadowsAll = true;
|
||||||
matchFound = selfTag.shadows(otherTag)
|
for (let i = 0; i < phrases.length; i++){
|
||||||
if (matchFound) {
|
const otherTag = phrases[i]
|
||||||
break
|
const doesShadow = selfTag.shadows(otherTag)
|
||||||
|
if(doesShadow){
|
||||||
|
shadowedOthers[i] = true;
|
||||||
}
|
}
|
||||||
|
shadowsSome ||= doesShadow;
|
||||||
|
shadowsAll &&= doesShadow;
|
||||||
}
|
}
|
||||||
if (!matchFound) {
|
// If A => X and A => Y, then
|
||||||
return false
|
// A&B implies X&Y. We discovered an A that implies all needed values
|
||||||
|
if (shadowsAll) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!shadowsSome) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return !shadowedOthers.some(v => !v);
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
usedKeys(): string[] {
|
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))
|
* (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.
|
* 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.
|
* @return only phrases that should be kept.
|
||||||
* @param knownExpression The expression which is known in the subexpression and for which calculations can be done
|
* @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~*"]}]} )
|
* const expr = <And> TagUtils.Tag({and: ["sport=climbing", {or:["club~*", "office~*"]}]} )
|
||||||
* expr.removePhraseConsideredKnown(new Tag("club","climbing"), false) // => expr
|
* expr.removePhraseConsideredKnown(new Tag("club","climbing"), false) // => expr
|
||||||
*/
|
*/
|
||||||
removePhraseConsideredKnown(
|
public removePhraseConsideredKnown(
|
||||||
knownExpression: TagsFilter,
|
knownExpression: TagsFilter,
|
||||||
value: boolean
|
value: boolean
|
||||||
): (TagsFilterClosed & OptimizedTag) | boolean {
|
): (TagsFilterClosed & OptimizedTag) | boolean {
|
||||||
const newAnds: TagsFilter[] = []
|
const newAnds: TagsFilter[] = []
|
||||||
for (const tag of this.and) {
|
for (const tag of this.and) {
|
||||||
if (tag instanceof And) {
|
if (tag instanceof And) {
|
||||||
|
console.trace("Improper optimization")
|
||||||
throw (
|
throw (
|
||||||
"Optimize expressions before using removePhraseConsideredKnown. Found an AND in an AND: " +
|
"Optimize expressions before using removePhraseConsideredKnown. Found an AND in an AND: " +
|
||||||
this.asHumanString()
|
this.asHumanString()
|
||||||
|
|
|
@ -83,6 +83,7 @@ export class Or extends TagsFilter {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
shadows(other: TagsFilter): boolean {
|
shadows(other: TagsFilter): boolean {
|
||||||
if (other instanceof Or) {
|
if (other instanceof Or) {
|
||||||
for (const selfTag of this.or) {
|
for (const selfTag of this.or) {
|
||||||
|
|
|
@ -4,6 +4,8 @@ import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
|
||||||
import { ExpressionSpecification } from "maplibre-gl"
|
import { ExpressionSpecification } from "maplibre-gl"
|
||||||
import { RegexTag } from "./RegexTag"
|
import { RegexTag } from "./RegexTag"
|
||||||
import { OptimizedTag } from "./TagTypes"
|
import { OptimizedTag } from "./TagTypes"
|
||||||
|
import { Or } from "./Or"
|
||||||
|
import { And } from "./And"
|
||||||
|
|
||||||
export class Tag extends TagsFilter {
|
export class Tag extends TagsFilter {
|
||||||
public key: string
|
public key: string
|
||||||
|
@ -148,6 +150,12 @@ export class Tag extends TagsFilter {
|
||||||
return other.matchesProperties({ [this.key]: this.value })
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -133,11 +133,11 @@ export class TagUtils {
|
||||||
"\n" +
|
"\n" +
|
||||||
"```json\n" +
|
"```json\n" +
|
||||||
"{\n" +
|
"{\n" +
|
||||||
' "mappings": [\n' +
|
" \"mappings\": [\n" +
|
||||||
" {\n" +
|
" {\n" +
|
||||||
' "if":"key:={some_other_key}",\n' +
|
" \"if\":\"key:={some_other_key}\",\n" +
|
||||||
' "then": "...",\n' +
|
" \"then\": \"...\",\n" +
|
||||||
' "hideInAnswer": "some_other_key="\n' +
|
" \"hideInAnswer\": \"some_other_key=\"\n" +
|
||||||
" }\n" +
|
" }\n" +
|
||||||
" ]\n" +
|
" ]\n" +
|
||||||
"}\n" +
|
"}\n" +
|
||||||
|
@ -175,10 +175,10 @@ export class TagUtils {
|
||||||
"\n" +
|
"\n" +
|
||||||
"```json\n" +
|
"```json\n" +
|
||||||
"{\n" +
|
"{\n" +
|
||||||
' "osmTags": {\n' +
|
" \"osmTags\": {\n" +
|
||||||
' "or": [\n' +
|
" \"or\": [\n" +
|
||||||
' "amenity=school",\n' +
|
" \"amenity=school\",\n" +
|
||||||
' "amenity=kindergarten"\n' +
|
" \"amenity=kindergarten\"\n" +
|
||||||
" ]\n" +
|
" ]\n" +
|
||||||
" }\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" +
|
"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" +
|
"\n" +
|
||||||
"In some cases, not every type of tags-filter can be used. For example, _rendering_ an option with a regex is\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" +
|
"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" +
|
"you are building your theme.\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
|
@ -205,18 +205,18 @@ export class TagUtils {
|
||||||
"\n" +
|
"\n" +
|
||||||
"```json\n" +
|
"```json\n" +
|
||||||
"{\n" +
|
"{\n" +
|
||||||
' "and": [\n' +
|
" \"and\": [\n" +
|
||||||
' "key=value",\n' +
|
" \"key=value\",\n" +
|
||||||
" {\n" +
|
" {\n" +
|
||||||
' "or": [\n' +
|
" \"or\": [\n" +
|
||||||
' "other_key=value",\n' +
|
" \"other_key=value\",\n" +
|
||||||
' "other_key=some_other_value"\n' +
|
" \"other_key=some_other_value\"\n" +
|
||||||
" ]\n" +
|
" ]\n" +
|
||||||
" },\n" +
|
" },\n" +
|
||||||
' "key_which_should_be_missing=",\n' +
|
" \"key_which_should_be_missing=\",\n" +
|
||||||
' "key_which_should_have_a_value~*",\n' +
|
" \"key_which_should_have_a_value~*\",\n" +
|
||||||
' "key~.*some_regex_a*_b+_[a-z]?",\n' +
|
" \"key~.*some_regex_a*_b+_[a-z]?\",\n" +
|
||||||
' "height<1"\n' +
|
" \"height<1\"\n" +
|
||||||
" ]\n" +
|
" ]\n" +
|
||||||
"}\n" +
|
"}\n" +
|
||||||
"```\n" +
|
"```\n" +
|
||||||
|
@ -246,7 +246,7 @@ export class TagUtils {
|
||||||
|
|
||||||
static asProperties(
|
static asProperties(
|
||||||
tags: TagsFilter | TagsFilter[],
|
tags: TagsFilter | TagsFilter[],
|
||||||
baseproperties: Record<string, string> = {}
|
baseproperties: Record<string, string> = {},
|
||||||
) {
|
) {
|
||||||
if (Array.isArray(tags)) {
|
if (Array.isArray(tags)) {
|
||||||
tags = new And(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: false): Record<string, string[]>
|
||||||
static SplitKeysRegex(
|
static SplitKeysRegex(
|
||||||
tagsFilters: UploadableTag[],
|
tagsFilters: UploadableTag[],
|
||||||
allowRegex: boolean
|
allowRegex: boolean,
|
||||||
): Record<string, (string | RegexTag)[]>
|
): Record<string, (string | RegexTag)[]>
|
||||||
static SplitKeysRegex(
|
static SplitKeysRegex(
|
||||||
tagsFilters: UploadableTag[],
|
tagsFilters: UploadableTag[],
|
||||||
allowRegex: boolean
|
allowRegex: boolean,
|
||||||
): Record<string, (string | RegexTag)[]> {
|
): Record<string, (string | RegexTag)[]> {
|
||||||
const keyValues: Record<string, (string | RegexTag)[]> = {}
|
const keyValues: Record<string, (string | RegexTag)[]> = {}
|
||||||
tagsFilters = [...tagsFilters] // copy all, use as queue
|
tagsFilters = [...tagsFilters] // copy all, use as queue
|
||||||
|
@ -307,7 +307,7 @@ export class TagUtils {
|
||||||
if (typeof key !== "string") {
|
if (typeof key !== "string") {
|
||||||
console.error(
|
console.error(
|
||||||
"Invalid type to flatten the multiAnswer: key is a regex too",
|
"Invalid type to flatten the multiAnswer: key is a regex too",
|
||||||
tagsFilter
|
tagsFilter,
|
||||||
)
|
)
|
||||||
throw "Invalid type to FlattenMultiAnswer: key is a regex too"
|
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): TagsFilterClosed
|
||||||
public static Tag(
|
public static Tag(
|
||||||
json: TagConfigJson,
|
json: TagConfigJson,
|
||||||
context: string | ConversionContext = ""
|
context: string | ConversionContext = "",
|
||||||
): TagsFilterClosed {
|
): TagsFilterClosed {
|
||||||
try {
|
try {
|
||||||
const ctx = typeof context === "string" ? context : context.path.join(".")
|
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(
|
throw `Error at ${context}: detected a non-uploadable tag at a location where this is not supported: ${t.asHumanString(
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
{}
|
{},
|
||||||
)}`
|
)}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -661,7 +661,7 @@ export class TagUtils {
|
||||||
*/
|
*/
|
||||||
public static removeShadowedElementsFrom(
|
public static removeShadowedElementsFrom(
|
||||||
blacklist: TagsFilter[],
|
blacklist: TagsFilter[],
|
||||||
listToFilter: TagsFilter[]
|
listToFilter: TagsFilter[],
|
||||||
): TagsFilter[] {
|
): TagsFilter[] {
|
||||||
return listToFilter.filter((tf) => !blacklist.some((guard) => guard.shadows(tf)))
|
return listToFilter.filter((tf) => !blacklist.some((guard) => guard.shadows(tf)))
|
||||||
}
|
}
|
||||||
|
@ -699,7 +699,7 @@ export class TagUtils {
|
||||||
*/
|
*/
|
||||||
public static containsEquivalents(
|
public static containsEquivalents(
|
||||||
guards: ReadonlyArray<TagsFilter>,
|
guards: ReadonlyArray<TagsFilter>,
|
||||||
listToFilter: ReadonlyArray<TagsFilter>
|
listToFilter: ReadonlyArray<TagsFilter>,
|
||||||
): boolean {
|
): boolean {
|
||||||
return listToFilter.some((tf) => guards.some((guard) => guard.shadows(tf)))
|
return listToFilter.some((tf) => guards.some((guard) => guard.shadows(tf)))
|
||||||
}
|
}
|
||||||
|
@ -743,7 +743,7 @@ export class TagUtils {
|
||||||
values.push(i + "")
|
values.push(i + "")
|
||||||
}
|
}
|
||||||
return values
|
return values
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
return Utils.NoNull(spec)
|
return Utils.NoNull(spec)
|
||||||
}
|
}
|
||||||
|
@ -751,13 +751,13 @@ export class TagUtils {
|
||||||
private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilterClosed {
|
private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilterClosed {
|
||||||
if (json === undefined) {
|
if (json === undefined) {
|
||||||
throw new Error(
|
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 (typeof json != "string") {
|
||||||
if (json["and"] !== undefined && json["or"] !== undefined) {
|
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(
|
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) {
|
if (json["and"] !== undefined) {
|
||||||
|
@ -839,13 +839,13 @@ export class TagUtils {
|
||||||
return new RegexTag(
|
return new RegexTag(
|
||||||
withRegex.key,
|
withRegex.key,
|
||||||
new RegExp(".+", "si" + withRegex.modifier),
|
new RegExp(".+", "si" + withRegex.modifier),
|
||||||
withRegex.invert
|
withRegex.invert,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return new RegexTag(
|
return new RegexTag(
|
||||||
withRegex.key,
|
withRegex.key,
|
||||||
new RegExp("^(" + value + ")$", "s" + withRegex.modifier),
|
new RegExp("^(" + value + ")$", "s" + withRegex.modifier),
|
||||||
withRegex.invert
|
withRegex.invert,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -967,10 +967,19 @@ export class TagUtils {
|
||||||
return ["", "## `" + mode + "` " + doc.name, "", doc.docs, "", ""].join("\n")
|
return ["", "## `" + mode + "` " + doc.name, "", doc.docs, "", ""].join("\n")
|
||||||
}),
|
}),
|
||||||
"## " +
|
"## " +
|
||||||
TagUtils.comparators.map((comparator) => "`" + comparator[0] + "`").join(" ") +
|
TagUtils.comparators.map((comparator) => "`" + comparator[0] + "`").join(" ") +
|
||||||
" Logical comparators",
|
" Logical comparators",
|
||||||
TagUtils.numberAndDateComparisonDocs,
|
TagUtils.numberAndDateComparisonDocs,
|
||||||
TagUtils.logicalOperator,
|
TagUtils.logicalOperator,
|
||||||
].join("\n")
|
].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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,8 @@ export abstract class TagsFilter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates some form of equivalency:
|
* 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
|
abstract shadows(other: TagsFilter): boolean
|
||||||
|
|
||||||
|
|
|
@ -41,14 +41,14 @@ interface NSIEntry {
|
||||||
* Represents a single brand/operator/flagpole/...
|
* Represents a single brand/operator/flagpole/...
|
||||||
*/
|
*/
|
||||||
export interface NSIItem {
|
export interface NSIItem {
|
||||||
displayName: string
|
readonly displayName: string
|
||||||
id: string
|
readonly id: string
|
||||||
locationSet: {
|
locationSet: {
|
||||||
include: string[]
|
include: string[]
|
||||||
exclude: string[]
|
exclude: string[]
|
||||||
}
|
}
|
||||||
tags: Record<string, string>
|
readonly tags: Readonly<Record<string, string>>
|
||||||
fromTemplate?: boolean
|
readonly fromTemplate?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class NameSuggestionIndex {
|
export default class NameSuggestionIndex {
|
||||||
|
@ -77,7 +77,7 @@ export default class NameSuggestionIndex {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
>,
|
>,
|
||||||
features: Readonly<FeatureCollection>
|
features: Readonly<FeatureCollection>,
|
||||||
) {
|
) {
|
||||||
this.nsiFile = nsiFile
|
this.nsiFile = nsiFile
|
||||||
this.nsiWdFile = nsiWdFile
|
this.nsiWdFile = nsiWdFile
|
||||||
|
@ -92,10 +92,10 @@ export default class NameSuggestionIndex {
|
||||||
}
|
}
|
||||||
const [nsi, nsiWd, features] = await Promise.all(
|
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) =>
|
["./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)
|
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
|
return NameSuggestionIndex.inited
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,13 +126,13 @@ export default class NameSuggestionIndex {
|
||||||
try {
|
try {
|
||||||
return Utils.downloadJsonCached<Record<string, number>>(
|
return Utils.downloadJsonCached<Record<string, number>>(
|
||||||
`./assets/data/nsi/stats/${type}.${c.toUpperCase()}.json`,
|
`./assets/data/nsi/stats/${type}.${c.toUpperCase()}.json`,
|
||||||
24 * 60 * 60 * 1000
|
24 * 60 * 60 * 1000,
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Could not fetch " + type + " statistics due to", e)
|
console.error("Could not fetch " + type + " statistics due to", e)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
stats = Utils.NoNull(stats)
|
stats = Utils.NoNull(stats)
|
||||||
if (stats.length === 1) {
|
if (stats.length === 1) {
|
||||||
|
@ -173,17 +173,17 @@ export default class NameSuggestionIndex {
|
||||||
public async generateMappings(
|
public async generateMappings(
|
||||||
type: string,
|
type: string,
|
||||||
tags: Record<string, string>,
|
tags: Record<string, string>,
|
||||||
country: string[],
|
country?: string[],
|
||||||
location?: [number, number],
|
location?: [number, number],
|
||||||
options?: {
|
options?: {
|
||||||
/**
|
/**
|
||||||
* If set, sort by frequency instead of alphabetically
|
* If set, sort by frequency instead of alphabetically
|
||||||
*/
|
*/
|
||||||
sortByFrequency: boolean
|
sortByFrequency: boolean
|
||||||
}
|
},
|
||||||
): Promise<Mapping[]> {
|
): Promise<Mapping[]> {
|
||||||
const mappings: (Mapping & { frequency: number })[] = []
|
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) {
|
for (const key in tags) {
|
||||||
if (key.startsWith("_")) {
|
if (key.startsWith("_")) {
|
||||||
continue
|
continue
|
||||||
|
@ -194,7 +194,7 @@ export default class NameSuggestionIndex {
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
country.join(";"),
|
country.join(";"),
|
||||||
location
|
location,
|
||||||
)
|
)
|
||||||
if (!actualBrands) {
|
if (!actualBrands) {
|
||||||
continue
|
continue
|
||||||
|
@ -202,8 +202,7 @@ export default class NameSuggestionIndex {
|
||||||
for (const nsiItem of actualBrands) {
|
for (const nsiItem of actualBrands) {
|
||||||
const tags = nsiItem.tags
|
const tags = nsiItem.tags
|
||||||
const frequency = frequencies[nsiItem.displayName]
|
const frequency = frequencies[nsiItem.displayName]
|
||||||
const logos = this.nsiWdFile[nsiItem.tags[type + ":wikidata"]]?.logos
|
const iconUrl = this.getIconExternalUrl(nsiItem, type)
|
||||||
const iconUrl = logos?.facebook ?? logos?.wikidata
|
|
||||||
const hasIcon = iconUrl !== undefined
|
const hasIcon = iconUrl !== undefined
|
||||||
let icon = undefined
|
let icon = undefined
|
||||||
if (hasIcon) {
|
if (hasIcon) {
|
||||||
|
@ -240,7 +239,7 @@ export default class NameSuggestionIndex {
|
||||||
}
|
}
|
||||||
|
|
||||||
public supportedTags(
|
public supportedTags(
|
||||||
type: "operator" | "brand" | "flag" | "transit" | string
|
type: "operator" | "brand" | "flag" | "transit" | string,
|
||||||
): Record<string, string[]> {
|
): Record<string, string[]> {
|
||||||
const tags: Record<string, string[]> = {}
|
const tags: Record<string, string[]> = {}
|
||||||
const keys = Object.keys(this.nsiFile.nsi)
|
const keys = Object.keys(this.nsiFile.nsi)
|
||||||
|
@ -263,7 +262,7 @@ export default class NameSuggestionIndex {
|
||||||
* Returns a list of all brands/operators
|
* Returns a list of all brands/operators
|
||||||
* @param type
|
* @param type
|
||||||
*/
|
*/
|
||||||
public allPossible(type: "brand" | "operator"): NSIItem[] {
|
public allPossible(type: string): NSIItem[] {
|
||||||
const options: NSIItem[] = []
|
const options: NSIItem[] = []
|
||||||
const tags = this.supportedTags(type)
|
const tags = this.supportedTags(type)
|
||||||
for (const osmKey in tags) {
|
for (const osmKey in tags) {
|
||||||
|
@ -285,10 +284,10 @@ export default class NameSuggestionIndex {
|
||||||
type: string,
|
type: string,
|
||||||
tags: { key: string; value: string }[],
|
tags: { key: string; value: string }[],
|
||||||
country: string = undefined,
|
country: string = undefined,
|
||||||
location: [number, number] = undefined
|
location: [number, number] = undefined,
|
||||||
): NSIItem[] {
|
): NSIItem[] {
|
||||||
return tags.flatMap((tag) =>
|
return tags.flatMap((tag) =>
|
||||||
this.getSuggestionsForKV(type, tag.key, tag.value, country, location)
|
this.getSuggestionsForKV(type, tag.key, tag.value, country, location),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,7 +310,7 @@ export default class NameSuggestionIndex {
|
||||||
key: string,
|
key: string,
|
||||||
value: string,
|
value: string,
|
||||||
country: string = undefined,
|
country: string = undefined,
|
||||||
location: [number, number] = undefined
|
location: [number, number] = undefined,
|
||||||
): NSIItem[] {
|
): NSIItem[] {
|
||||||
const path = `${type}s/${key}/${value}`
|
const path = `${type}s/${key}/${value}`
|
||||||
const entry = this.nsiFile.nsi[path]
|
const entry = this.nsiFile.nsi[path]
|
||||||
|
@ -375,9 +374,29 @@ export default class NameSuggestionIndex {
|
||||||
center: [number, number],
|
center: [number, number],
|
||||||
options: {
|
options: {
|
||||||
sortByFrequency: boolean
|
sortByFrequency: boolean
|
||||||
}
|
},
|
||||||
): Promise<Mapping[]> {
|
): Promise<Mapping[]> {
|
||||||
const nsi = await NameSuggestionIndex.getNsiIndex()
|
const nsi = await NameSuggestionIndex.getNsiIndex()
|
||||||
return nsi.generateMappings(key, tags, country, center, options)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
220
src/Models/ThemeConfig/Conversion/ExpandFilter.ts
Normal file
220
src/Models/ThemeConfig/Conversion/ExpandFilter.ts
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
import { DesugaringContext, DesugaringStep } from "./Conversion"
|
||||||
|
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||||
|
import FilterConfigJson, { FilterConfigOptionJson } from "../Json/FilterConfigJson"
|
||||||
|
import predifined_filters from "../../../../assets/layers/filters/filters.json"
|
||||||
|
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||||
|
import { ConversionContext } from "./ConversionContext"
|
||||||
|
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
|
||||||
|
import { Utils } from "../../../Utils"
|
||||||
|
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||||
|
import { Tag } from "../../../Logic/Tags/Tag"
|
||||||
|
import { RegexTag } from "../../../Logic/Tags/RegexTag"
|
||||||
|
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"
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
private prune(sourceTags:FlatTag, filter: FilterConfigJson, context: ConversionContext): FilterConfigJson{
|
||||||
|
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: TagsFilter = <TagsFilter> And.construct([TagUtils.Tag(option.osmTags)]).optimize()
|
||||||
|
if(basetags instanceof And){
|
||||||
|
basetags = <TagsFilter> basetags.removePhraseConsideredKnown(sourceTags, true)
|
||||||
|
}
|
||||||
|
return {...option, osmTags: basetags.asJson()}
|
||||||
|
})
|
||||||
|
const countAfter = newOptions.length
|
||||||
|
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}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
|
||||||
|
if(!Array.isArray(json.filter) || typeof json.source === "string"){
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
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))}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
||||||
|
private static readonly predefinedFilters = ExpandFilter.load_filters()
|
||||||
|
private _state: DesugaringContext
|
||||||
|
|
||||||
|
constructor(state: DesugaringContext) {
|
||||||
|
super(
|
||||||
|
[
|
||||||
|
"Expands filters: replaces a shorthand by the value found in 'filters.json'.",
|
||||||
|
"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",
|
||||||
|
)
|
||||||
|
this._state = state
|
||||||
|
}
|
||||||
|
|
||||||
|
private static load_filters(): Map<string, FilterConfigJson> {
|
||||||
|
const filters = new Map<string, FilterConfigJson>()
|
||||||
|
for (const filter of <FilterConfigJson[]>predifined_filters.filter) {
|
||||||
|
filters.set(filter.id, filter)
|
||||||
|
}
|
||||||
|
return filters
|
||||||
|
}
|
||||||
|
|
||||||
|
public static buildFilterFromTagRendering(
|
||||||
|
tr: TagRenderingConfigJson,
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const qtr = <QuestionableTagRenderingConfigJson>tr
|
||||||
|
const options = qtr.mappings.map((mapping) => {
|
||||||
|
let icon: string = mapping.icon?.["path"] ?? mapping.icon
|
||||||
|
let emoji: string = undefined
|
||||||
|
if (Utils.isEmoji(icon)) {
|
||||||
|
emoji = icon
|
||||||
|
icon = undefined
|
||||||
|
}
|
||||||
|
let osmTags = TagUtils.Tag(mapping.if)
|
||||||
|
if (qtr.multiAnswer && osmTags instanceof Tag) {
|
||||||
|
osmTags = new RegexTag(
|
||||||
|
osmTags.key,
|
||||||
|
new RegExp("^(.+;)?" + osmTags.value + "(;.+)$", "is"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (mapping.alsoShowIf) {
|
||||||
|
osmTags = new Or([osmTags, TagUtils.Tag(mapping.alsoShowIf)])
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FilterConfigOptionJson>{
|
||||||
|
question: mapping.then,
|
||||||
|
osmTags: osmTags.asJson(),
|
||||||
|
searchTerms: mapping.searchTerms,
|
||||||
|
icon,
|
||||||
|
emoji,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Add default option
|
||||||
|
options.unshift({
|
||||||
|
question: tr["question"] ?? Translations.t.general.filterPanel.allTypes,
|
||||||
|
osmTags: undefined,
|
||||||
|
searchTerms: undefined,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
id: tr["id"],
|
||||||
|
options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
|
||||||
|
if (json?.filter === undefined || json?.filter === null) {
|
||||||
|
return json // Nothing to change here
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.filter["sameAs"] !== undefined) {
|
||||||
|
return json // Nothing to change here
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFilters: FilterConfigJson[] = []
|
||||||
|
const filters = <(FilterConfigJson | string)[]>json.filter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create filters based on builtin filters or create them based on the tagRendering
|
||||||
|
*/
|
||||||
|
for (let i = 0; i < filters.length; i++) {
|
||||||
|
const filter = filters[i]
|
||||||
|
if (filter === undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (typeof filter !== "string") {
|
||||||
|
newFilters.push(filter)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingTr = <TagRenderingConfigJson>(
|
||||||
|
json.tagRenderings.find((tr) => !!tr && tr["id"] === filter)
|
||||||
|
)
|
||||||
|
if (matchingTr) {
|
||||||
|
const filter = ExpandFilter.buildFilterFromTagRendering(
|
||||||
|
matchingTr,
|
||||||
|
context.enters("filter", i),
|
||||||
|
)
|
||||||
|
newFilters.push(filter)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.indexOf(".") > 0) {
|
||||||
|
if (!(this._state.sharedLayers?.size > 0)) {
|
||||||
|
// This is a bootstrapping-run, we can safely ignore this
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const split = filter.split(".")
|
||||||
|
if (split.length > 2) {
|
||||||
|
context.err(
|
||||||
|
"invalid filter name: " + filter + ", expected `layername.filterid`",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const layer = this._state.sharedLayers.get(split[0])
|
||||||
|
if (layer === undefined) {
|
||||||
|
context.err("Layer '" + split[0] + "' not found")
|
||||||
|
}
|
||||||
|
const expectedId = split[1]
|
||||||
|
const expandedFilter = (<(FilterConfigJson | string)[]>layer.filter).find(
|
||||||
|
(f) => typeof f !== "string" && f.id === expectedId,
|
||||||
|
)
|
||||||
|
if (expandedFilter === undefined) {
|
||||||
|
context.err("Did not find filter with name " + filter)
|
||||||
|
} else {
|
||||||
|
newFilters.push(<FilterConfigJson>expandedFilter)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Search for the filter:
|
||||||
|
const found = ExpandFilter.predefinedFilters.get(filter)
|
||||||
|
if (found === undefined) {
|
||||||
|
const suggestions = Utils.sortedByLevenshteinDistance(
|
||||||
|
filter,
|
||||||
|
Array.from(ExpandFilter.predefinedFilters.keys()),
|
||||||
|
(t) => t,
|
||||||
|
)
|
||||||
|
context
|
||||||
|
.enter(filter)
|
||||||
|
.err(
|
||||||
|
"While searching for predefined filter " +
|
||||||
|
filter +
|
||||||
|
": this filter is not found. Perhaps you meant one of: " +
|
||||||
|
suggestions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
newFilters.push(found)
|
||||||
|
}
|
||||||
|
return { ...json, filter: newFilters }
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,10 +10,7 @@ import {
|
||||||
SetDefault,
|
SetDefault,
|
||||||
} from "./Conversion"
|
} from "./Conversion"
|
||||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||||
import {
|
import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||||
MinimalTagRenderingConfigJson,
|
|
||||||
TagRenderingConfigJson,
|
|
||||||
} from "../Json/TagRenderingConfigJson"
|
|
||||||
import { Utils } from "../../../Utils"
|
import { Utils } from "../../../Utils"
|
||||||
import RewritableConfigJson from "../Json/RewritableConfigJson"
|
import RewritableConfigJson from "../Json/RewritableConfigJson"
|
||||||
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
|
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
|
||||||
|
@ -21,8 +18,7 @@ import Translations from "../../../UI/i18n/Translations"
|
||||||
import { Translation } from "../../../UI/i18n/Translation"
|
import { Translation } from "../../../UI/i18n/Translation"
|
||||||
import tagrenderingconfigmeta from "../../../../src/assets/schemas/tagrenderingconfigmeta.json"
|
import tagrenderingconfigmeta from "../../../../src/assets/schemas/tagrenderingconfigmeta.json"
|
||||||
import { AddContextToTranslations } from "./AddContextToTranslations"
|
import { AddContextToTranslations } from "./AddContextToTranslations"
|
||||||
import FilterConfigJson, { FilterConfigOptionJson } from "../Json/FilterConfigJson"
|
import FilterConfigJson from "../Json/FilterConfigJson"
|
||||||
import predifined_filters from "../../../../assets/layers/filters/filters.json"
|
|
||||||
import { TagConfigJson } from "../Json/TagConfigJson"
|
import { TagConfigJson } from "../Json/TagConfigJson"
|
||||||
import PointRenderingConfigJson, { IconConfigJson } from "../Json/PointRenderingConfigJson"
|
import PointRenderingConfigJson, { IconConfigJson } from "../Json/PointRenderingConfigJson"
|
||||||
import ValidationUtils from "./ValidationUtils"
|
import ValidationUtils from "./ValidationUtils"
|
||||||
|
@ -33,9 +29,7 @@ import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
|
||||||
import { ConversionContext } from "./ConversionContext"
|
import { ConversionContext } from "./ConversionContext"
|
||||||
import { ExpandRewrite } from "./ExpandRewrite"
|
import { ExpandRewrite } from "./ExpandRewrite"
|
||||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||||
import { Tag } from "../../../Logic/Tags/Tag"
|
import { ExpandFilter, PruneFilters } from "./ExpandFilter"
|
||||||
import { RegexTag } from "../../../Logic/Tags/RegexTag"
|
|
||||||
import { Or } from "../../../Logic/Tags/Or"
|
|
||||||
|
|
||||||
class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
|
class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -108,163 +102,6 @@ class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
|
||||||
return { ...json, filter: filters }
|
return { ...json, filter: filters }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|
||||||
private static readonly predefinedFilters = ExpandFilter.load_filters()
|
|
||||||
private _state: DesugaringContext
|
|
||||||
|
|
||||||
constructor(state: DesugaringContext) {
|
|
||||||
super(
|
|
||||||
[
|
|
||||||
"Expands filters: replaces a shorthand by the value found in 'filters.json'.",
|
|
||||||
"If the string is formatted 'layername.filtername, it will be looked up into that layer instead.",
|
|
||||||
].join(" "),
|
|
||||||
["filter"],
|
|
||||||
"ExpandFilter"
|
|
||||||
)
|
|
||||||
this._state = state
|
|
||||||
}
|
|
||||||
|
|
||||||
private static load_filters(): Map<string, FilterConfigJson> {
|
|
||||||
const filters = new Map<string, FilterConfigJson>()
|
|
||||||
for (const filter of <FilterConfigJson[]>predifined_filters.filter) {
|
|
||||||
filters.set(filter.id, filter)
|
|
||||||
}
|
|
||||||
return filters
|
|
||||||
}
|
|
||||||
|
|
||||||
public static buildFilterFromTagRendering(
|
|
||||||
tr: TagRenderingConfigJson,
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const qtr = <QuestionableTagRenderingConfigJson>tr
|
|
||||||
const options = qtr.mappings.map((mapping) => {
|
|
||||||
let icon: string = mapping.icon?.["path"] ?? mapping.icon
|
|
||||||
let emoji: string = undefined
|
|
||||||
if (Utils.isEmoji(icon)) {
|
|
||||||
emoji = icon
|
|
||||||
icon = undefined
|
|
||||||
}
|
|
||||||
let osmTags = TagUtils.Tag(mapping.if)
|
|
||||||
if (qtr.multiAnswer && osmTags instanceof Tag) {
|
|
||||||
osmTags = new RegexTag(
|
|
||||||
osmTags.key,
|
|
||||||
new RegExp("^(.+;)?" + osmTags.value + "(;.+)$", "is")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (mapping.alsoShowIf) {
|
|
||||||
osmTags = new Or([osmTags, TagUtils.Tag(mapping.alsoShowIf)])
|
|
||||||
}
|
|
||||||
|
|
||||||
return <FilterConfigOptionJson>{
|
|
||||||
question: mapping.then,
|
|
||||||
osmTags: osmTags.asJson(),
|
|
||||||
searchTerms: mapping.searchTerms,
|
|
||||||
icon,
|
|
||||||
emoji,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// Add default option
|
|
||||||
options.unshift({
|
|
||||||
question: tr["question"] ?? Translations.t.general.filterPanel.allTypes,
|
|
||||||
osmTags: undefined,
|
|
||||||
searchTerms: undefined,
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
id: tr["id"],
|
|
||||||
options,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
|
|
||||||
if (json?.filter === undefined || json?.filter === null) {
|
|
||||||
return json // Nothing to change here
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json.filter["sameAs"] !== undefined) {
|
|
||||||
return json // Nothing to change here
|
|
||||||
}
|
|
||||||
|
|
||||||
const newFilters: FilterConfigJson[] = []
|
|
||||||
const filters = <(FilterConfigJson | string)[]>json.filter
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create filters based on builtin filters or create them based on the tagRendering
|
|
||||||
*/
|
|
||||||
for (let i = 0; i < filters.length; i++) {
|
|
||||||
const filter = filters[i]
|
|
||||||
if (filter === undefined) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (typeof filter !== "string") {
|
|
||||||
newFilters.push(filter)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchingTr = <TagRenderingConfigJson>(
|
|
||||||
json.tagRenderings.find((tr) => !!tr && tr["id"] === filter)
|
|
||||||
)
|
|
||||||
if (matchingTr) {
|
|
||||||
const filter = ExpandFilter.buildFilterFromTagRendering(
|
|
||||||
matchingTr,
|
|
||||||
context.enters("filter", i)
|
|
||||||
)
|
|
||||||
newFilters.push(filter)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.indexOf(".") > 0) {
|
|
||||||
if (!(this._state.sharedLayers?.size > 0)) {
|
|
||||||
// This is a bootstrapping-run, we can safely ignore this
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const split = filter.split(".")
|
|
||||||
if (split.length > 2) {
|
|
||||||
context.err(
|
|
||||||
"invalid filter name: " + filter + ", expected `layername.filterid`"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const layer = this._state.sharedLayers.get(split[0])
|
|
||||||
if (layer === undefined) {
|
|
||||||
context.err("Layer '" + split[0] + "' not found")
|
|
||||||
}
|
|
||||||
const expectedId = split[1]
|
|
||||||
const expandedFilter = (<(FilterConfigJson | string)[]>layer.filter).find(
|
|
||||||
(f) => typeof f !== "string" && f.id === expectedId
|
|
||||||
)
|
|
||||||
if (expandedFilter === undefined) {
|
|
||||||
context.err("Did not find filter with name " + filter)
|
|
||||||
} else {
|
|
||||||
newFilters.push(<FilterConfigJson>expandedFilter)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Search for the filter:
|
|
||||||
const found = ExpandFilter.predefinedFilters.get(filter)
|
|
||||||
if (found === undefined) {
|
|
||||||
const suggestions = Utils.sortedByLevenshteinDistance(
|
|
||||||
filter,
|
|
||||||
Array.from(ExpandFilter.predefinedFilters.keys()),
|
|
||||||
(t) => t
|
|
||||||
)
|
|
||||||
context
|
|
||||||
.enter(filter)
|
|
||||||
.err(
|
|
||||||
"While searching for predefined filter " +
|
|
||||||
filter +
|
|
||||||
": this filter is not found. Perhaps you meant one of: " +
|
|
||||||
suggestions
|
|
||||||
)
|
|
||||||
}
|
|
||||||
newFilters.push(found)
|
|
||||||
}
|
|
||||||
return { ...json, filter: newFilters }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExpandTagRendering extends Conversion<
|
class ExpandTagRendering extends Conversion<
|
||||||
| string
|
| string
|
||||||
|
@ -1481,7 +1318,8 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
|
||||||
new Concat(new ExpandTagRendering(state, layer, { noHardcodedStrings: true }))
|
new Concat(new ExpandTagRendering(state, layer, { noHardcodedStrings: true }))
|
||||||
),
|
),
|
||||||
new AddFiltersFromTagRenderings(),
|
new AddFiltersFromTagRenderings(),
|
||||||
new ExpandFilter(state)
|
new ExpandFilter(state),
|
||||||
|
new PruneFilters()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,15 @@ export default interface FilterConfigJson {
|
||||||
* An id/name for this filter, used to set the URL parameters
|
* An id/name for this filter, used to set the URL parameters
|
||||||
*/
|
*/
|
||||||
id: string
|
id: string
|
||||||
|
/**
|
||||||
|
* If set, the options will be pruned. Only items for which the filter match the layer source will be kept.
|
||||||
|
*
|
||||||
|
* For example, we import types of brands from the nsi. This contains a ton of items, e.g.
|
||||||
|
* [{question: "Brand X", osmTags: {"and": ["shop=clothes", "brand=Brand X]}, {osmTags: {"and": "shop=convenience", ...} ...} ]
|
||||||
|
* Of course, when making a layer about `shop=clothes`, we'll only want to keep the clothes shops.
|
||||||
|
* If set to strict and the source is `shop=clothes`, only those options which have shop=clothes will be returned
|
||||||
|
*/
|
||||||
|
strict?: boolean
|
||||||
/**
|
/**
|
||||||
* The options for a filter
|
* The options for a filter
|
||||||
* If there are multiple options these will be a list of radio buttons
|
* If there are multiple options these will be a list of radio buttons
|
||||||
|
|
|
@ -601,4 +601,9 @@ export interface LayerConfigJson {
|
||||||
* group: hidden
|
* group: hidden
|
||||||
*/
|
*/
|
||||||
snapName?: Translatable
|
snapName?: Translatable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* group: hidden
|
||||||
|
*/
|
||||||
|
"#dont-translate": "*"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue