forked from MapComplete/MapComplete
Feature(NSI): support using NSI-images as icon, test on shops
This commit is contained in:
parent
80192f003a
commit
b913ea867f
14 changed files with 1173790 additions and 71 deletions
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"#":"no-translations",
|
||||
"#no-index": "yes",
|
||||
"#dont-translate": "*",
|
||||
"#filter": "no-auto",
|
||||
"pointRendering": [
|
||||
|
|
569064
assets/layers/nsi_brand/nsi_brand.json
Normal file
569064
assets/layers/nsi_brand/nsi_brand.json
Normal file
File diff suppressed because it is too large
Load diff
604545
assets/layers/nsi_operator/nsi_operator.json
Normal file
604545
assets/layers/nsi_operator/nsi_operator.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -109,7 +109,7 @@
|
|||
},
|
||||
{
|
||||
"icon": {
|
||||
"builtin": "id_presets.shop_rendering",
|
||||
"builtin": "nsi_brand.icon",
|
||||
"override": {
|
||||
"render": "./assets/layers/id_presets/maki-shop.svg",
|
||||
"+mappings": [
|
||||
|
|
|
@ -95,7 +95,7 @@
|
|||
"reset:translations": "vite-node scripts/generateTranslations.ts -- --ignore-weblate",
|
||||
"generate:layouts": "vite-node scripts/generateLayouts.ts",
|
||||
"generate:docs": "rm -rf Docs/Themes/* && rm -rf Docs/Layers/* && rm -rf Docs/TagInfo && mkdir Docs/TagInfo && export NODE_OPTIONS=\"--max-old-space-size=16000\" && vite-node scripts/generateDocs.ts && vite-node scripts/generateTaginfoProjectFiles.ts",
|
||||
"generate:layeroverview": "export NODE_OPTIONS=\"--max-old-space-size=8192\" && vite-node scripts/generateLayerOverview.ts",
|
||||
"generate:layeroverview": "export NODE_OPTIONS=\"--max-old-space-size=16000\" && vite-node scripts/generateLayerOverview.ts",
|
||||
"generate:mapcomplete-changes-theme": "export NODE_OPTIONS=\"--max-old-space-size=8192\" && vite-node scripts/generateLayerOverview.ts -- --generate-change-map",
|
||||
"refresh:layeroverview": "export NODE_OPTIONS=\"--max-old-space-size=8192\" && vite-node scripts/generateLayerOverview.ts -- --force",
|
||||
"generate:licenses": "vite-node scripts/generateLicenseInfo.ts -- --no-fail",
|
||||
|
|
|
@ -5,10 +5,9 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "
|
|||
import ScriptUtils from "./ScriptUtils"
|
||||
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 { FilterConfigOptionJson } from "../src/Models/ThemeConfig/Json/FilterConfigJson"
|
||||
import { TagUtils } from "../src/Logic/Tags/TagUtils"
|
||||
import { And } from "../src/Logic/Tags/And"
|
||||
import { TagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson"
|
||||
|
||||
class DownloadNsiLogos extends Script {
|
||||
constructor() {
|
||||
|
@ -129,34 +128,56 @@ 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,
|
||||
osmTags: NameSuggestionIndex.asFilterTags(item),
|
||||
})
|
||||
})
|
||||
const mappings = items.map(item => ({
|
||||
if: NameSuggestionIndex.asFilterTags(item),
|
||||
then: nsi.getIconUrl(item, type),
|
||||
}))
|
||||
|
||||
console.log("Checking for shadow-mappings...")
|
||||
for (let i = mappings.length - 1; i >= 0 ; i--) {
|
||||
const condition = TagUtils.Tag(mappings[i].if)
|
||||
if(i % 100 === 0){
|
||||
console.log("Checking for shadow-mappings...",i,"/",mappings.length )
|
||||
|
||||
}
|
||||
const shadowsSomething = mappings.some((m,j) => {
|
||||
if(i===j ){
|
||||
return false
|
||||
}
|
||||
return condition.shadows(TagUtils.Tag(m.if))
|
||||
})
|
||||
// If this one matches, the other one will match as well
|
||||
// We can thus remove this one in favour of the other one
|
||||
if(shadowsSomething){
|
||||
mappings.splice(i, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const iconsTr: TagRenderingConfigJson = <any>{
|
||||
strict: true,
|
||||
id: "icon",
|
||||
mappings,
|
||||
}
|
||||
|
||||
const config: LayerConfigJson = {
|
||||
"#dont-translate": "*",
|
||||
"#no-index": "yes",
|
||||
id: "nsi_" + type,
|
||||
source: "special:library",
|
||||
description: {
|
||||
en: "Exposes part of the NSI to reuse in other themes, e.g. for rendering",
|
||||
},
|
||||
pointRendering: null,
|
||||
tagRenderings: [
|
||||
iconsTr,
|
||||
],
|
||||
filter: [
|
||||
<any>{
|
||||
id: type,
|
||||
|
|
|
@ -497,6 +497,8 @@ class LayerOverviewUtils extends Script {
|
|||
priviliged.delete(key)
|
||||
})
|
||||
|
||||
|
||||
|
||||
// These two get a free pass
|
||||
priviliged.delete("summary")
|
||||
priviliged.delete("last_click")
|
||||
|
@ -527,7 +529,7 @@ class LayerOverviewUtils extends Script {
|
|||
writeFileSync(
|
||||
"./src/assets/generated/known_layers.json",
|
||||
JSON.stringify({
|
||||
layers: Array.from(sharedLayers.values()).filter((l) => l.id !== "favourite"),
|
||||
layers: Array.from(sharedLayers.values()).filter((l) => !(l["#no-index"] === "yes")),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import { Utils } from "../Utils"
|
||||
import known_layers from "../assets/generated/known_layers.json"
|
||||
import * as known_layers from "../assets/generated/known_layers.json"
|
||||
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
|
||||
export class AllSharedLayers {
|
||||
public static sharedLayers: Map<string, LayerConfig> = AllSharedLayers.getSharedLayers()
|
||||
public static getSharedLayersConfigs(): Map<string, LayerConfigJson> {
|
||||
const sharedLayers = new Map<string, LayerConfigJson>()
|
||||
for (const layer of known_layers["layers"]) {
|
||||
// @ts-ignore
|
||||
sharedLayers.set(layer.id, layer)
|
||||
for (const layer of (known_layers).layers) {
|
||||
if(layer.id === undefined){
|
||||
console.error("Layer without id! "+JSON.stringify(layer).slice(0,80), known_layers.layers.length)
|
||||
continue
|
||||
}else{
|
||||
console.log("Loaded",layer.id)
|
||||
}
|
||||
sharedLayers.set(layer.id, <any> layer)
|
||||
}
|
||||
|
||||
return sharedLayers
|
||||
|
|
|
@ -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'.
|
||||
*
|
||||
|
|
|
@ -6,6 +6,8 @@ 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
|
||||
|
@ -52,6 +54,7 @@ export interface NSIItem {
|
|||
}
|
||||
|
||||
export default class NameSuggestionIndex {
|
||||
|
||||
public static readonly supportedTypes = ["brand", "flag", "operator", "transit"] as const
|
||||
private readonly nsiFile: Readonly<NSIFile>
|
||||
private readonly nsiWdFile: Readonly<
|
||||
|
@ -399,4 +402,27 @@ export default class NameSuggestionIndex {
|
|||
}
|
||||
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 }] })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,11 +36,8 @@ export class PruneFilters extends DesugaringStep<LayerConfigJson>{
|
|||
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()}
|
||||
let basetags = TagUtils.Tag(option.osmTags)
|
||||
return {...option, osmTags: (<TagsFilter>TagUtils.removeKnownParts(basetags ,sourceTags)).asJson()}
|
||||
})
|
||||
const countAfter = newOptions.length
|
||||
if(countAfter !== countBefore){
|
||||
|
|
|
@ -159,11 +159,15 @@ class ExpandTagRendering extends Conversion<
|
|||
ctx: ConversionContext
|
||||
): QuestionableTagRenderingConfigJson[] {
|
||||
const trs = this.convertOnce(spec, ctx)
|
||||
|
||||
const result = []
|
||||
if(!Array.isArray(trs)){
|
||||
ctx.err("Result of lookup for "+spec+" is not iterable; got "+trs)
|
||||
return undefined
|
||||
}
|
||||
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))
|
||||
result.push(...stable)
|
||||
if (this._options?.addToContext) {
|
||||
for (const tr of stable) {
|
||||
|
@ -181,6 +185,40 @@ class ExpandTagRendering extends Conversion<
|
|||
return result
|
||||
}
|
||||
|
||||
private pruneMappings(tagRendering: QuestionableTagRenderingConfigJson, ctx: ConversionContext): QuestionableTagRenderingConfigJson{
|
||||
if(!tagRendering["strict"]){
|
||||
return tagRendering
|
||||
}
|
||||
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 after = newMappings?.length ?? 0
|
||||
if(before - after > 0){
|
||||
ctx.info(`Pruned mappings for ${tagRendering.id}, from ${before} to ${after} (removed ${before - after})`)
|
||||
}
|
||||
const tr = {
|
||||
...tagRendering,
|
||||
mappings: newMappings
|
||||
}
|
||||
delete tr["strict"]
|
||||
return tr
|
||||
}
|
||||
|
||||
private lookup(name: string, ctx: ConversionContext): TagRenderingConfigJson[] | undefined {
|
||||
const direct = this.directLookup(name)
|
||||
|
||||
|
@ -285,11 +323,12 @@ class ExpandTagRendering extends Conversion<
|
|||
const state = this._state
|
||||
|
||||
if (typeof tr === "string") {
|
||||
let lookup
|
||||
if (this._state.tagRenderings !== null) {
|
||||
lookup = this.lookup(tr, ctx)
|
||||
const lookup = this.lookup(tr, ctx)
|
||||
if(lookup){
|
||||
return lookup
|
||||
}
|
||||
}
|
||||
if (lookup === undefined) {
|
||||
if (
|
||||
this._state.sharedLayers?.size > 0 &&
|
||||
ctx.path.at(-1) !== "icon" &&
|
||||
|
@ -298,7 +337,7 @@ 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(", "),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -308,7 +347,7 @@ class ExpandTagRendering extends Conversion<
|
|||
tr +
|
||||
" \n Did you perhaps forget to add the layer as prefix, such as `icons." +
|
||||
tr +
|
||||
"`? "
|
||||
"`? ",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -319,8 +358,6 @@ class ExpandTagRendering extends Conversion<
|
|||
},
|
||||
]
|
||||
}
|
||||
return lookup
|
||||
}
|
||||
|
||||
if (tr["builtin"] !== undefined) {
|
||||
let names: string | string[] = tr["builtin"]
|
||||
|
@ -356,6 +393,9 @@ class ExpandTagRendering extends Conversion<
|
|||
let candidates = Array.from(state.tagRenderings.keys())
|
||||
if (name.indexOf(".") > 0) {
|
||||
const [layerName] = name.split(".")
|
||||
if(layerName === undefined){
|
||||
ctx.err("Layername is undefined", name)
|
||||
}
|
||||
let layer = state.sharedLayers.get(layerName)
|
||||
if (layerName === this._self?.id) {
|
||||
layer = this._self
|
||||
|
@ -363,7 +403,7 @@ class ExpandTagRendering extends Conversion<
|
|||
if (layer === undefined) {
|
||||
const candidates = Utils.sortedByLevenshteinDistance(
|
||||
layerName,
|
||||
Array.from(state.sharedLayers.keys()),
|
||||
Utils.NoNull(Array.from(state.sharedLayers.keys())),
|
||||
(s) => s
|
||||
)
|
||||
if (state.sharedLayers.size === 0) {
|
||||
|
@ -1017,7 +1057,8 @@ class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
|
|||
class PreparePointRendering extends Fuse<PointRenderingConfigJson> {
|
||||
constructor(state: DesugaringContext, layer: LayerConfigJson) {
|
||||
super(
|
||||
"Prepares point renderings by expanding 'icon' and 'iconBadges'",
|
||||
"Prepares point renderings by expanding 'icon' and 'iconBadges'." +
|
||||
" A tagRendering from the host tagRenderings will be substituted in",
|
||||
new On(
|
||||
"marker",
|
||||
new Each(
|
||||
|
|
|
@ -110,10 +110,8 @@ export class DoesImageExist extends DesugaringStep<string> {
|
|||
}
|
||||
|
||||
if (!this._knownImagePaths.has(image)) {
|
||||
if (this.doesPathExist === undefined) {
|
||||
context.err(
|
||||
`Image with path ${image} not found or not attributed; it is used in ${context}`
|
||||
)
|
||||
if (this.doesPathExist === undefined || image.indexOf("nsi/logos/") >= 0) {
|
||||
// pass
|
||||
} 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. `
|
||||
|
|
|
@ -4,8 +4,16 @@ import { TagConfigJson } from "./TagConfigJson"
|
|||
export interface IconConfigJson {
|
||||
/**
|
||||
* question: What icon should be used?
|
||||
*
|
||||
* To reuse icons from a different layer of a library:
|
||||
* - 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"})
|
||||
* - Use "layer_id.tagrendering_id"
|
||||
*
|
||||
* Note that if you reuse icons from a different icon set, you'll probably want to use `override` to set a default rendering
|
||||
*
|
||||
*
|
||||
* types: <span class="text-lg font-bold">Use a different icon depending on the value of some attributes</span> ; icon
|
||||
* suggestions: return Constants.defaultPinIcons.map(i => ({if: "value="+i, then: i, icon: i}))
|
||||
* suggestions: return [ "nsi_brand.icon", "nsi_operator.icon", "id_presets.shop_rendering", ...Constants.defaultPinIcons.map(i => ({if: "value="+i, then: i, icon: i}))]
|
||||
*/
|
||||
icon: string | MinimalTagRenderingConfigJson | { builtin: string; override: any }
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue