forked from MapComplete/MapComplete
		
	NSI: add script to download logos and statistics, dynamically inject extra mappings, hide low-priority mappings if applicable
This commit is contained in:
		
							parent
							
								
									30d1f175c6
								
							
						
					
					
						commit
						c5b4cdf450
					
				
					 18 changed files with 459 additions and 114 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -19,6 +19,9 @@ missing_translations.txt
 | 
				
			||||||
.DS_Store
 | 
					.DS_Store
 | 
				
			||||||
Svg.ts
 | 
					Svg.ts
 | 
				
			||||||
data/
 | 
					data/
 | 
				
			||||||
 | 
					src/assets/generated/nsi_stats/brand.json
 | 
				
			||||||
 | 
					src/assets/generated/nsi_stats/brand.summarized.json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Folder.DotSettings.user
 | 
					Folder.DotSettings.user
 | 
				
			||||||
index_*.ts
 | 
					index_*.ts
 | 
				
			||||||
.~lock.*
 | 
					.~lock.*
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -290,6 +290,7 @@
 | 
				
			||||||
        "loginToStart": "Log in to answer this question",
 | 
					        "loginToStart": "Log in to answer this question",
 | 
				
			||||||
        "loginWithOpenStreetMap": "Login with OpenStreetMap",
 | 
					        "loginWithOpenStreetMap": "Login with OpenStreetMap",
 | 
				
			||||||
        "logout": "Log out",
 | 
					        "logout": "Log out",
 | 
				
			||||||
 | 
					        "mappingsAreHidden": "Some options are hidden. Use search to show more options.",
 | 
				
			||||||
        "menu": {
 | 
					        "menu": {
 | 
				
			||||||
            "aboutMapComplete": "About MapComplete",
 | 
					            "aboutMapComplete": "About MapComplete",
 | 
				
			||||||
            "filter": "Filter data"
 | 
					            "filter": "Filter data"
 | 
				
			||||||
| 
						 | 
					@ -686,11 +687,11 @@
 | 
				
			||||||
        "intro": "Privacy is important - for both the individual and for society. MapComplete tries to respect your privacy as much as possible - up to the point no annoying cookie banner is needed. However, we still would like to inform you which information is gathered and shared, under which circumstances and why these trade-offs are made.",
 | 
					        "intro": "Privacy is important - for both the individual and for society. MapComplete tries to respect your privacy as much as possible - up to the point no annoying cookie banner is needed. However, we still would like to inform you which information is gathered and shared, under which circumstances and why these trade-offs are made.",
 | 
				
			||||||
        "items": {
 | 
					        "items": {
 | 
				
			||||||
            "changesYouMake": "The changes you made",
 | 
					            "changesYouMake": "The changes you made",
 | 
				
			||||||
            "username": "Your username",
 | 
					 | 
				
			||||||
            "date": "When this change is made",
 | 
					            "date": "When this change is made",
 | 
				
			||||||
            "theme": "The theme you used while making the change",
 | 
					            "distanceIndicator": "An indication of how close you were to changed objects. Other mappers can use this information to determine if a change was made based on survey or on remote research",
 | 
				
			||||||
            "language": "The language of the user interface",
 | 
					            "language": "The language of the user interface",
 | 
				
			||||||
            "distanceIndicator": "An indication of how close you were to changed objects. Other mappers can use this information to determine if a change was made based on survey or on remote research"
 | 
					            "theme": "The theme you used while making the change",
 | 
				
			||||||
 | 
					            "username": "Your username"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "miscCookies": "MapComplete integrates with various other services, especially to load images of features. Images are hosted on various third-party servers, which might set cookies on their own.",
 | 
					        "miscCookies": "MapComplete integrates with various other services, especially to load images of features. Images are hosted on various third-party servers, which might set cookies on their own.",
 | 
				
			||||||
        "miscCookiesTitle": "Other cookies",
 | 
					        "miscCookiesTitle": "Other cookies",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										22
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										22
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							| 
						 | 
					@ -1,12 +1,12 @@
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
  "name": "mapcomplete",
 | 
					  "name": "mapcomplete",
 | 
				
			||||||
  "version": "0.42.5",
 | 
					  "version": "0.42.6",
 | 
				
			||||||
  "lockfileVersion": 2,
 | 
					  "lockfileVersion": 2,
 | 
				
			||||||
  "requires": true,
 | 
					  "requires": true,
 | 
				
			||||||
  "packages": {
 | 
					  "packages": {
 | 
				
			||||||
    "": {
 | 
					    "": {
 | 
				
			||||||
      "name": "mapcomplete",
 | 
					      "name": "mapcomplete",
 | 
				
			||||||
      "version": "0.42.5",
 | 
					      "version": "0.42.6",
 | 
				
			||||||
      "license": "GPL-3.0-or-later",
 | 
					      "license": "GPL-3.0-or-later",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@comunica/core": "^3.0.1",
 | 
					        "@comunica/core": "^3.0.1",
 | 
				
			||||||
| 
						 | 
					@ -24,6 +24,7 @@
 | 
				
			||||||
        "@turf/length": "^6.5.0",
 | 
					        "@turf/length": "^6.5.0",
 | 
				
			||||||
        "@turf/turf": "^6.5.0",
 | 
					        "@turf/turf": "^6.5.0",
 | 
				
			||||||
        "@types/dompurify": "^3.0.2",
 | 
					        "@types/dompurify": "^3.0.2",
 | 
				
			||||||
 | 
					        "@types/follow-redirects": "^1.14.4",
 | 
				
			||||||
        "@types/pg": "^8.10.9",
 | 
					        "@types/pg": "^8.10.9",
 | 
				
			||||||
        "@types/qrcode-generator": "^1.0.6",
 | 
					        "@types/qrcode-generator": "^1.0.6",
 | 
				
			||||||
        "@types/showdown": "^2.0.0",
 | 
					        "@types/showdown": "^2.0.0",
 | 
				
			||||||
| 
						 | 
					@ -39,6 +40,7 @@
 | 
				
			||||||
        "email-validator": "^2.0.4",
 | 
					        "email-validator": "^2.0.4",
 | 
				
			||||||
        "escape-html": "^1.0.3",
 | 
					        "escape-html": "^1.0.3",
 | 
				
			||||||
        "fake-dom": "^1.0.4",
 | 
					        "fake-dom": "^1.0.4",
 | 
				
			||||||
 | 
					        "follow-redirects": "^1.15.6",
 | 
				
			||||||
        "geojson2svg": "^1.3.3",
 | 
					        "geojson2svg": "^1.3.3",
 | 
				
			||||||
        "html-to-image": "^1.11.11",
 | 
					        "html-to-image": "^1.11.11",
 | 
				
			||||||
        "i18next-client": "^1.11.4",
 | 
					        "i18next-client": "^1.11.4",
 | 
				
			||||||
| 
						 | 
					@ -6568,6 +6570,14 @@
 | 
				
			||||||
      "version": "1.0.0",
 | 
					      "version": "1.0.0",
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/@types/follow-redirects": {
 | 
				
			||||||
 | 
					      "version": "1.14.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/follow-redirects/-/follow-redirects-1.14.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-GWXfsD0Jc1RWiFmMuMFCpXMzi9L7oPDVwxUnZdg89kDNnqsRfUKXEtUYtA98A6lig1WXH/CYY/fvPW9HuN5fTA==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@types/node": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/geojson": {
 | 
					    "node_modules/@types/geojson": {
 | 
				
			||||||
      "version": "7946.0.14",
 | 
					      "version": "7946.0.14",
 | 
				
			||||||
      "license": "MIT"
 | 
					      "license": "MIT"
 | 
				
			||||||
| 
						 | 
					@ -24194,6 +24204,14 @@
 | 
				
			||||||
    "@types/estree": {
 | 
					    "@types/estree": {
 | 
				
			||||||
      "version": "1.0.0"
 | 
					      "version": "1.0.0"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "@types/follow-redirects": {
 | 
				
			||||||
 | 
					      "version": "1.14.4",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/@types/follow-redirects/-/follow-redirects-1.14.4.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-GWXfsD0Jc1RWiFmMuMFCpXMzi9L7oPDVwxUnZdg89kDNnqsRfUKXEtUYtA98A6lig1WXH/CYY/fvPW9HuN5fTA==",
 | 
				
			||||||
 | 
					      "requires": {
 | 
				
			||||||
 | 
					        "@types/node": "*"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "@types/geojson": {
 | 
					    "@types/geojson": {
 | 
				
			||||||
      "version": "7946.0.14"
 | 
					      "version": "7946.0.14"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -107,6 +107,7 @@
 | 
				
			||||||
    "housekeeping": "git pull && npx update-browserslist-db@latest && npm run weblate-fix-heavy && npm run generate && npm run generate:docs && npm run generate:contributor-list && vite-node scripts/fetchLanguages.ts && vite-node scripts/generateSunnyUnlabeled.ts && npm run format && git add assets/ langs/ Docs/ **/*.ts Docs/* src/* && git commit -m 'chore: automated housekeeping...'",
 | 
					    "housekeeping": "git pull && npx update-browserslist-db@latest && npm run weblate-fix-heavy && npm run generate && npm run generate:docs && npm run generate:contributor-list && vite-node scripts/fetchLanguages.ts && vite-node scripts/generateSunnyUnlabeled.ts && npm run format && git add assets/ langs/ Docs/ **/*.ts Docs/* src/* && git commit -m 'chore: automated housekeeping...'",
 | 
				
			||||||
    "reuse-compliance": "reuse lint",
 | 
					    "reuse-compliance": "reuse lint",
 | 
				
			||||||
    "backup:images": "vite-node scripts/generateImageAnalysis.ts -- ~/data/imgur-image-backup/",
 | 
					    "backup:images": "vite-node scripts/generateImageAnalysis.ts -- ~/data/imgur-image-backup/",
 | 
				
			||||||
 | 
					    "downloadNsiLogos": "vite-node scripts/downloadNsiLogos.ts",
 | 
				
			||||||
    "dloadVelopark": "vite-node scripts/velopark/veloParkToGeojson.ts ",
 | 
					    "dloadVelopark": "vite-node scripts/velopark/veloParkToGeojson.ts ",
 | 
				
			||||||
    "compareVelopark": "vite-node scripts/velopark/compare.ts -- velopark_nonsynced_.geojson ~/Projecten/OSM/Fietsberaad/2024-02-02\\ Fietsenstallingen_OSM_met_velopark_ref.geojson\n",
 | 
					    "compareVelopark": "vite-node scripts/velopark/compare.ts -- velopark_nonsynced_.geojson ~/Projecten/OSM/Fietsberaad/2024-02-02\\ Fietsenstallingen_OSM_met_velopark_ref.geojson\n",
 | 
				
			||||||
    "scrapeWebsites": "vite-node scripts/importscripts/compareWebsiteData.ts -- ~/Downloads/ShopsWithWebsiteNodes.csv ~/data/scraped_websites/",
 | 
					    "scrapeWebsites": "vite-node scripts/importscripts/compareWebsiteData.ts -- ~/Downloads/ShopsWithWebsiteNodes.csv ~/data/scraped_websites/",
 | 
				
			||||||
| 
						 | 
					@ -142,6 +143,7 @@
 | 
				
			||||||
    "@turf/length": "^6.5.0",
 | 
					    "@turf/length": "^6.5.0",
 | 
				
			||||||
    "@turf/turf": "^6.5.0",
 | 
					    "@turf/turf": "^6.5.0",
 | 
				
			||||||
    "@types/dompurify": "^3.0.2",
 | 
					    "@types/dompurify": "^3.0.2",
 | 
				
			||||||
 | 
					    "@types/follow-redirects": "^1.14.4",
 | 
				
			||||||
    "@types/pg": "^8.10.9",
 | 
					    "@types/pg": "^8.10.9",
 | 
				
			||||||
    "@types/qrcode-generator": "^1.0.6",
 | 
					    "@types/qrcode-generator": "^1.0.6",
 | 
				
			||||||
    "@types/showdown": "^2.0.0",
 | 
					    "@types/showdown": "^2.0.0",
 | 
				
			||||||
| 
						 | 
					@ -157,6 +159,7 @@
 | 
				
			||||||
    "email-validator": "^2.0.4",
 | 
					    "email-validator": "^2.0.4",
 | 
				
			||||||
    "escape-html": "^1.0.3",
 | 
					    "escape-html": "^1.0.3",
 | 
				
			||||||
    "fake-dom": "^1.0.4",
 | 
					    "fake-dom": "^1.0.4",
 | 
				
			||||||
 | 
					    "follow-redirects": "^1.15.6",
 | 
				
			||||||
    "geojson2svg": "^1.3.3",
 | 
					    "geojson2svg": "^1.3.3",
 | 
				
			||||||
    "html-to-image": "^1.11.11",
 | 
					    "html-to-image": "^1.11.11",
 | 
				
			||||||
    "i18next-client": "^1.11.4",
 | 
					    "i18next-client": "^1.11.4",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,10 @@
 | 
				
			||||||
import * as fs from "fs"
 | 
					import * as fs from "fs"
 | 
				
			||||||
import { existsSync, lstatSync, readdirSync, readFileSync } from "fs"
 | 
					import { existsSync, lstatSync, readdirSync, readFileSync } from "fs"
 | 
				
			||||||
import { Utils } from "../src/Utils"
 | 
					import { Utils } from "../src/Utils"
 | 
				
			||||||
import * as https from "https"
 | 
					import {https} from "follow-redirects"
 | 
				
			||||||
import { LayoutConfigJson } from "../src/Models/ThemeConfig/Json/LayoutConfigJson"
 | 
					import { LayoutConfigJson } from "../src/Models/ThemeConfig/Json/LayoutConfigJson"
 | 
				
			||||||
import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
 | 
					import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
 | 
				
			||||||
import xml2js from "xml2js"
 | 
					import xml2js from "xml2js"
 | 
				
			||||||
import { resolve } from "node:dns"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class ScriptUtils {
 | 
					export default class ScriptUtils {
 | 
				
			||||||
    public static fixUtils() {
 | 
					    public static fixUtils() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										113
									
								
								scripts/downloadNsiLogos.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								scripts/downloadNsiLogos.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,113 @@
 | 
				
			||||||
 | 
					import Script from "./Script"
 | 
				
			||||||
 | 
					import NameSuggestionIndex, { NSIItem } from "../src/Logic/Web/NameSuggestionIndex"
 | 
				
			||||||
 | 
					import * as nsiWD from "../node_modules/name-suggestion-index/dist/wikidata.min.json"
 | 
				
			||||||
 | 
					import { existsSync, writeFileSync } from "fs"
 | 
				
			||||||
 | 
					import ScriptUtils from "./ScriptUtils"
 | 
				
			||||||
 | 
					import { Utils } from "../src/Utils"
 | 
				
			||||||
 | 
					import { WikimediaImageProvider } from "../src/Logic/ImageProviders/WikimediaImageProvider"
 | 
				
			||||||
 | 
					import { renameSync } from "node:fs"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class DownloadNsiLogos extends Script {
 | 
				
			||||||
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        super("Downloads all images of the NSI")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private async getWikimediaUrl(startUrl: string) {
 | 
				
			||||||
 | 
					        if (!startUrl) {
 | 
				
			||||||
 | 
					            return startUrl
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private async downloadLogo(nsiItem: NSIItem, type: string, basePath: string) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            return await this.downloadLogoUnsafe(nsiItem, type, basePath)
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					            console.error("Could not download", nsiItem.displayName, "due to", e)
 | 
				
			||||||
 | 
					            return false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private async downloadLogoUnsafe(nsiItem: NSIItem, type: string, basePath: string) {
 | 
				
			||||||
 | 
					        if (nsiItem === undefined) {
 | 
				
			||||||
 | 
					            return false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let path = basePath + nsiItem.id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const logos = nsiWD["wikidata"][nsiItem?.tags?.[type + ":wikidata"]]?.logos
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (NameSuggestionIndex.isSvg(nsiItem, type)) {
 | 
				
			||||||
 | 
					            path = path + ".svg"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (existsSync(path)) {
 | 
				
			||||||
 | 
					            return false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!logos) {
 | 
				
			||||||
 | 
					            return false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (logos.facebook) {
 | 
				
			||||||
 | 
					            // Facebook logo's are generally better and square
 | 
				
			||||||
 | 
					            await ScriptUtils.DownloadFileTo(logos.facebook, path)
 | 
				
			||||||
 | 
					            return true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (logos.wikidata) {
 | 
				
			||||||
 | 
					            let url: string = logos.wikidata
 | 
				
			||||||
 | 
					            console.log("Downloading", url)
 | 
				
			||||||
 | 
					            let ttl = 10
 | 
				
			||||||
 | 
					            do {
 | 
				
			||||||
 | 
					                ttl--
 | 
				
			||||||
 | 
					                const dloaded = await Utils.downloadAdvanced(url, {
 | 
				
			||||||
 | 
					                    "User-Agent": "MapComplete NSI scraper/0.1 (https://github.com/pietervdvn/MapComplete; pietervdvn@posteo.net)"
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					                const redirect: string | undefined = dloaded["redirect"]
 | 
				
			||||||
 | 
					                if (redirect) {
 | 
				
			||||||
 | 
					                    console.log("Got a redirect from", url, "to", redirect)
 | 
				
			||||||
 | 
					                    url = redirect
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if ((<string>logos.wikidata).toLowerCase().endsWith(".svg")) {
 | 
				
			||||||
 | 
					                    console.log("Written SVG", path)
 | 
				
			||||||
 | 
					                    if(!path.endsWith(".svg")){
 | 
				
			||||||
 | 
					                        throw "Undetected svg path:"+logos.wikidata
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    writeFileSync(path, dloaded["content"], "utf8")
 | 
				
			||||||
 | 
					                    return true
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                console.log("Got data from", url, "-->", path)
 | 
				
			||||||
 | 
					                await ScriptUtils.DownloadFileTo(url, path)
 | 
				
			||||||
 | 
					                return true
 | 
				
			||||||
 | 
					            } while (ttl > 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async main(args: string[]): Promise<void> {
 | 
				
			||||||
 | 
					        const type = "brand"
 | 
				
			||||||
 | 
					        const items = NameSuggestionIndex.allPossible(type)
 | 
				
			||||||
 | 
					        const basePath = "./public/assets/data/nsi/logos/"
 | 
				
			||||||
 | 
					        let downloadCount = 0
 | 
				
			||||||
 | 
					        const stepcount = 100
 | 
				
			||||||
 | 
					        for (let i = 0; i < items.length; i += stepcount) {
 | 
				
			||||||
 | 
					            if (i % 100 === 0) {
 | 
				
			||||||
 | 
					                console.log(i + "/" + items.length, "downloaded " + downloadCount)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            await Promise.all(Utils.TimesT(stepcount, j => j).map(async j => {
 | 
				
			||||||
 | 
					                const downloaded = await this.downloadLogo(items[i + j], type, basePath)
 | 
				
			||||||
 | 
					                if (downloaded) {
 | 
				
			||||||
 | 
					                    downloadCount++
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					new DownloadNsiLogos().run()
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ import ScriptUtils from "./ScriptUtils"
 | 
				
			||||||
import TagRenderingConfig from "../src/Models/ThemeConfig/TagRenderingConfig"
 | 
					import TagRenderingConfig from "../src/Models/ThemeConfig/TagRenderingConfig"
 | 
				
			||||||
import { And } from "../src/Logic/Tags/And"
 | 
					import { And } from "../src/Logic/Tags/And"
 | 
				
			||||||
import Script from "./Script"
 | 
					import Script from "./Script"
 | 
				
			||||||
import NameSuggestionIndex, { NSIItem } from "../src/Logic/Web/NameSuggestionIndex"
 | 
					import NameSuggestionIndex from "../src/Logic/Web/NameSuggestionIndex"
 | 
				
			||||||
import TagInfo, { TagInfoStats } from "../src/Logic/Web/TagInfo"
 | 
					import TagInfo, { TagInfoStats } from "../src/Logic/Web/TagInfo"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Utilities {
 | 
					class Utilities {
 | 
				
			||||||
| 
						 | 
					@ -18,6 +18,7 @@ class Utilities {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return newR
 | 
					        return newR
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
class GenerateStats extends Script {
 | 
					class GenerateStats extends Script {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,9 +62,7 @@ class GenerateStats extends Script {
 | 
				
			||||||
        await Promise.all(
 | 
					        await Promise.all(
 | 
				
			||||||
            Array.from(keysAndTags.keys()).map(async (key) => {
 | 
					            Array.from(keysAndTags.keys()).map(async (key) => {
 | 
				
			||||||
                const values = keysAndTags.get(key)
 | 
					                const values = keysAndTags.get(key)
 | 
				
			||||||
                const data = await Utils.downloadJson(
 | 
					                const data = await TagInfo.global.getStats(key)
 | 
				
			||||||
                    `https://taginfo.openstreetmap.org/api/4/key/stats?key=${key}`
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                const count = data.data.find((item) => item.type === "all").count
 | 
					                const count = data.data.find((item) => item.type === "all").count
 | 
				
			||||||
                keyTotal.set(key, count)
 | 
					                keyTotal.set(key, count)
 | 
				
			||||||
                console.log(key, "-->", count)
 | 
					                console.log(key, "-->", count)
 | 
				
			||||||
| 
						 | 
					@ -72,9 +71,7 @@ class GenerateStats extends Script {
 | 
				
			||||||
                    tagTotal.set(key, new Map<string, number>())
 | 
					                    tagTotal.set(key, new Map<string, number>())
 | 
				
			||||||
                    await Promise.all(
 | 
					                    await Promise.all(
 | 
				
			||||||
                        Array.from(values).map(async (value) => {
 | 
					                        Array.from(values).map(async (value) => {
 | 
				
			||||||
                            const tagData = await Utils.downloadJson(
 | 
					                           const tagData: TagInfoStats= await TagInfo.global.getStats(key, value)
 | 
				
			||||||
                                `https://taginfo.openstreetmap.org/api/4/tag/stats?key=${key}&value=${value}`
 | 
					 | 
				
			||||||
                            )
 | 
					 | 
				
			||||||
                            const count = tagData.data .find((item) => item.type === "all").count
 | 
					                            const count = tagData.data .find((item) => item.type === "all").count
 | 
				
			||||||
                            tagTotal.get(key).set(value, count)
 | 
					                            tagTotal.get(key).set(value, count)
 | 
				
			||||||
                            console.log(key + "=" + value, "-->", count)
 | 
					                            console.log(key + "=" + value, "-->", count)
 | 
				
			||||||
| 
						 | 
					@ -98,21 +95,75 @@ class GenerateStats extends Script {
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async createNameSuggestionIndexFile() {
 | 
					    private summarizeNSI(sourcefile: string, pathNoExtension: string): void {
 | 
				
			||||||
        const type = "brand"
 | 
					        const data = <Record<string, Record<string, number>>>JSON.parse(readFileSync(sourcefile, "utf8"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const allCountries: Set<string> = new Set()
 | 
				
			||||||
 | 
					        for (const brand in data) {
 | 
				
			||||||
 | 
					            const perCountry = data[brand]
 | 
				
			||||||
 | 
					            for (const country in perCountry) {
 | 
				
			||||||
 | 
					                allCountries.add(country)
 | 
				
			||||||
 | 
					                const count = perCountry[country]
 | 
				
			||||||
 | 
					                if (count === 0) {
 | 
				
			||||||
 | 
					                    delete perCountry[country]
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const pathOut = pathNoExtension + ".summarized.json"
 | 
				
			||||||
 | 
					        writeFileSync(pathOut, JSON.stringify(
 | 
				
			||||||
 | 
					            data, null, "  "), "utf8")
 | 
				
			||||||
 | 
					        console.log("Written", pathOut)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const allBrands = Object.keys(data)
 | 
				
			||||||
 | 
					        allBrands.sort()
 | 
				
			||||||
 | 
					        for (const country of allCountries) {
 | 
				
			||||||
 | 
					            const summary = <Record<string, number>>{}
 | 
				
			||||||
 | 
					            for (const brand of allBrands) {
 | 
				
			||||||
 | 
					                const count = data[brand][country]
 | 
				
			||||||
 | 
					                if (count > 2) { // Eéntje is geentje
 | 
				
			||||||
 | 
					                    // We ignore count == 1 as they are rather exceptional
 | 
				
			||||||
 | 
					                    summary[brand] = data[brand][country]
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const countryPath = pathNoExtension + "." + country + ".json"
 | 
				
			||||||
 | 
					            writeFileSync(countryPath, JSON.stringify(summary), "utf8")
 | 
				
			||||||
 | 
					            console.log("Written", countryPath)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async createNameSuggestionIndexFile(basepath: string,type: "brand" | "operator") {
 | 
				
			||||||
 | 
					        const path = basepath+type+'.json'
 | 
				
			||||||
        let allBrands = <Record<string, Record<string, number>>>{}
 | 
					        let allBrands = <Record<string, Record<string, number>>>{}
 | 
				
			||||||
        const path = "./src/assets/generated/nsi_stats/" + type + ".json"
 | 
					 | 
				
			||||||
        if (existsSync(path)) {
 | 
					        if (existsSync(path)) {
 | 
				
			||||||
            allBrands = JSON.parse(readFileSync(path, "utf8"))
 | 
					            allBrands = JSON.parse(readFileSync(path, "utf8"))
 | 
				
			||||||
            console.log("Loaded",Object.keys(allBrands).length," previously loaded brands")
 | 
					            console.log("Loaded",Object.keys(allBrands).length," previously loaded brands")
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        let lastWrite = new Date()
 | 
					        let lastWrite = new Date()
 | 
				
			||||||
        const allBrandNames: string[] = NameSuggestionIndex.allPossible(type)
 | 
					        let skipped = 0
 | 
				
			||||||
        for (const brand of allBrandNames) {
 | 
					        const allBrandNames: string[] = Utils.Dedup(NameSuggestionIndex.allPossible(type).map(item => item.tags[type]))
 | 
				
			||||||
 | 
					        for (let i = 0; i < allBrandNames.length; i++){
 | 
				
			||||||
 | 
					            if(i % 100 == 0){
 | 
				
			||||||
 | 
					                console.log("Downloading ",i+"/"+allBrandNames.length,"; skipped",skipped)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const brand = allBrandNames[i]
 | 
				
			||||||
 | 
					            if(!!allBrands[brand] && Object.keys(allBrands[brand]).length == 0){
 | 
				
			||||||
 | 
					                delete allBrands[brand]
 | 
				
			||||||
 | 
					                console.log("Deleted", brand, "as no entries at all")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            if(allBrands[brand] !== undefined){
 | 
					            if(allBrands[brand] !== undefined){
 | 
				
			||||||
                console.log("Skipping", brand,", already loaded")
 | 
					                const max = Math.max(...Object.values(allBrands[brand]))
 | 
				
			||||||
 | 
					                skipped++
 | 
				
			||||||
 | 
					                if(max < 0){
 | 
				
			||||||
 | 
					                    console.log("HMMMM:", allBrands[brand])
 | 
				
			||||||
 | 
					                    delete allBrands[brand]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                }else{
 | 
				
			||||||
                    continue
 | 
					                    continue
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            const distribution: Record<string, number> = Utilities.mapValues(await TagInfo.getGlobalDistributionsFor(type, brand), s => s.data.find(t => t.type === "all").count)
 | 
					            const distribution: Record<string, number> = Utilities.mapValues(await TagInfo.getGlobalDistributionsFor(type, brand), s => s.data.find(t => t.type === "all").count)
 | 
				
			||||||
            allBrands[brand] = distribution
 | 
					            allBrands[brand] = distribution
 | 
				
			||||||
            if ((new Date().getTime() - lastWrite.getTime()) / 1000 >= 5) {
 | 
					            if ((new Date().getTime() - lastWrite.getTime()) / 1000 >= 5) {
 | 
				
			||||||
| 
						 | 
					@ -128,8 +179,11 @@ class GenerateStats extends Script {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async main(_: string[]) {
 | 
					    async main(_: string[]) {
 | 
				
			||||||
        //  this.createOptimizationFile()
 | 
					        await this.createOptimizationFile()
 | 
				
			||||||
        await this.createNameSuggestionIndexFile()
 | 
					        const type = "brand"
 | 
				
			||||||
 | 
					        const basepath = "./src/assets/generated/stats/"
 | 
				
			||||||
 | 
					        await this.createNameSuggestionIndexFile(basepath, type)
 | 
				
			||||||
 | 
					        this.summarizeNSI(basepath+type+".json", "./public/assets/data/stats/"+type)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,15 @@
 | 
				
			||||||
import * as nsi from "../../../node_modules/name-suggestion-index/dist/nsi.json"
 | 
					import * as nsi from "../../../node_modules/name-suggestion-index/dist/nsi.json"
 | 
				
			||||||
 | 
					import * as nsiWD from "../../../node_modules/name-suggestion-index/dist/wikidata.min.json"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import * as nsiFeatures from "../../../node_modules/name-suggestion-index/dist/featureCollection.json"
 | 
					import * as nsiFeatures from "../../../node_modules/name-suggestion-index/dist/featureCollection.json"
 | 
				
			||||||
import { LocationConflation } from "@rapideditor/location-conflation"
 | 
					import { LocationConflation } from "@rapideditor/location-conflation"
 | 
				
			||||||
import type { Feature, FeatureCollection, MultiPolygon } from "geojson"
 | 
					 | 
				
			||||||
import * as turf from "@turf/turf"
 | 
					 | 
				
			||||||
import { Utils } from "../../Utils"
 | 
					 | 
				
			||||||
import TagInfo from "./TagInfo"
 | 
					 | 
				
			||||||
import type { Feature, MultiPolygon } from "geojson"
 | 
					import type { Feature, MultiPolygon } from "geojson"
 | 
				
			||||||
 | 
					import { Utils } from "../../Utils"
 | 
				
			||||||
import * as turf from "@turf/turf"
 | 
					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"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Main name suggestion index file
 | 
					 * Main name suggestion index file
 | 
				
			||||||
| 
						 | 
					@ -48,9 +51,7 @@ export interface NSIItem {
 | 
				
			||||||
        include: string[],
 | 
					        include: string[],
 | 
				
			||||||
        exclude: string[]
 | 
					        exclude: string[]
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    tags: {
 | 
					    tags: Record<string, string>
 | 
				
			||||||
        [key: string]: string
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    fromTemplate?: boolean
 | 
					    fromTemplate?: boolean
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -71,15 +72,87 @@ export default class NameSuggestionIndex {
 | 
				
			||||||
        return this._supportedTypes
 | 
					        return this._supportedTypes
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static async buildTaginfoCountsPerCountry(type = "brand", key: string, value: string) {
 | 
					
 | 
				
			||||||
        const allData: { nsi: NSIItem, stats }[] = []
 | 
					    /**
 | 
				
			||||||
        const brands = NameSuggestionIndex.getSuggestionsFor(type, key, value)
 | 
					     * Fetches the data files for a single country. Note that it contains _all_ entries having this brand, not for a single type of object
 | 
				
			||||||
        for (const brand of brands) {
 | 
					     * @param type
 | 
				
			||||||
            const brandValue = brand.tags[type]
 | 
					     * @param countries
 | 
				
			||||||
            const allStats = await TagInfo.getGlobalDistributionsFor(type, brandValue)
 | 
					     * @private
 | 
				
			||||||
            allData.push({ nsi: brand, stats: allStats })
 | 
					     */
 | 
				
			||||||
 | 
					    private static async fetchFrequenciesFor(type: string, countries: string[]) {
 | 
				
			||||||
 | 
					        let stats = await Promise.all(countries.map(c => {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return Utils.downloadJsonCached<Record<string, number>>(`./assets/data/nsi/stats/${type}.${c.toUpperCase()}.json`, 24 * 60 * 60 * 1000)
 | 
				
			||||||
 | 
					            } catch (e) {
 | 
				
			||||||
 | 
					                console.error("Could not fetch " + type + " statistics due to", e)
 | 
				
			||||||
 | 
					                return undefined
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        return allData
 | 
					        }))
 | 
				
			||||||
 | 
					        stats = Utils.NoNull(stats)
 | 
				
			||||||
 | 
					        if (stats.length === 1) {
 | 
				
			||||||
 | 
					            return stats[0]
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const merged = stats[0]
 | 
				
			||||||
 | 
					        for (let i = 1; i < stats.length; i++) {
 | 
				
			||||||
 | 
					            for (const countryCode in stats[i]) {
 | 
				
			||||||
 | 
					                merged[countryCode] = (merged[countryCode] ?? 0) + stats[i][countryCode]
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return merged
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static isSvg(nsiItem: NSIItem, type: string): boolean | undefined {
 | 
				
			||||||
 | 
					        const logos = nsiWD["wikidata"][nsiItem?.tags?.[type + ":wikidata"]]?.logos
 | 
				
			||||||
 | 
					        if(nsiItem.id === "axa-2f6feb"){
 | 
				
			||||||
 | 
					            console.trace(">>> HI")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!logos) {
 | 
				
			||||||
 | 
					            return undefined
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (logos.facebook) {
 | 
				
			||||||
 | 
					            return false
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const url: string = logos.wikidata
 | 
				
			||||||
 | 
					        if (url.toLowerCase().endsWith(".svg")) {
 | 
				
			||||||
 | 
					            return true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static async generateMappings(type: string, key: string, value: string, country: string[], location?: [number, number]) {
 | 
				
			||||||
 | 
					        const mappings: Mapping[] = []
 | 
				
			||||||
 | 
					        const frequencies = await NameSuggestionIndex.fetchFrequenciesFor(type, country)
 | 
				
			||||||
 | 
					        const actualBrands = NameSuggestionIndex.getSuggestionsFor(type, key, value, country.join(";"), location)
 | 
				
			||||||
 | 
					        for (const nsiItem of actualBrands) {
 | 
				
			||||||
 | 
					            const tags = nsiItem.tags
 | 
				
			||||||
 | 
					            const frequency = frequencies[nsiItem.displayName]
 | 
				
			||||||
 | 
					            const logos = nsiWD["wikidata"][nsiItem.tags[type + ":wikidata"]]?.logos
 | 
				
			||||||
 | 
					            let iconUrl = logos?.facebook ?? logos?.wikidata
 | 
				
			||||||
 | 
					            const hasIcon = iconUrl !== undefined
 | 
				
			||||||
 | 
					            let icon = undefined
 | 
				
			||||||
 | 
					            if (hasIcon) {
 | 
				
			||||||
 | 
					                // Using <img src=...> works fine without an extension for JPG and PNG, but _not_ svg :(
 | 
				
			||||||
 | 
					                icon = "./assets/data/nsi/logos/" + nsiItem.id
 | 
				
			||||||
 | 
					                if (NameSuggestionIndex.isSvg(nsiItem, type)) {
 | 
				
			||||||
 | 
					                    console.log("Is svg:", nsiItem.displayName)
 | 
				
			||||||
 | 
					                    icon = icon + ".svg"
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            mappings.push({
 | 
				
			||||||
 | 
					                if: new Tag(type, tags[type]),
 | 
				
			||||||
 | 
					                addExtraTags: Object.keys(tags).filter(k => k !== type).map(k => new Tag(k, tags[k])),
 | 
				
			||||||
 | 
					                then: new TypedTranslation<{}>({ "*": nsiItem.displayName }),
 | 
				
			||||||
 | 
					                hideInAnswer: false,
 | 
				
			||||||
 | 
					                ifnot: undefined,
 | 
				
			||||||
 | 
					                alsoShowIf: undefined,
 | 
				
			||||||
 | 
					                icon,
 | 
				
			||||||
 | 
					                iconClass: "medium",
 | 
				
			||||||
 | 
					                priorityIf: frequency > 0 ? new RegexTag("id", /.*/) : undefined,
 | 
				
			||||||
 | 
					                searchTerms: { "*": [nsiItem.displayName, nsiItem.id] }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return mappings
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static supportedTags(type: "operator" | "brand" | "flag" | "transit" | string): Record<string, string[]> {
 | 
					    public static supportedTags(type: "operator" | "brand" | "flag" | "transit" | string): Record<string, string[]> {
 | 
				
			||||||
| 
						 | 
					@ -101,26 +174,27 @@ export default class NameSuggestionIndex {
 | 
				
			||||||
        return tags
 | 
					        return tags
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static allPossible(type: "brand" | "operator"): string[] {
 | 
					    /**
 | 
				
			||||||
        const options: string[] = []
 | 
					     * Returns a list of all brands/operators
 | 
				
			||||||
 | 
					     * @param type
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public static allPossible(type: "brand" | "operator"): NSIItem[] {
 | 
				
			||||||
 | 
					        const options: NSIItem[] = []
 | 
				
			||||||
        const tags = NameSuggestionIndex.supportedTags(type)
 | 
					        const tags = NameSuggestionIndex.supportedTags(type)
 | 
				
			||||||
        for (const osmKey in tags) {
 | 
					        for (const osmKey in tags) {
 | 
				
			||||||
            const values = tags[osmKey]
 | 
					            const values = tags[osmKey]
 | 
				
			||||||
            for (const osmValue of values) {
 | 
					            for (const osmValue of values) {
 | 
				
			||||||
                const suggestions = this.getSuggestionsFor(type, osmKey, osmValue)
 | 
					                const suggestions = this.getSuggestionsFor(type, osmKey, osmValue)
 | 
				
			||||||
                for (const suggestion of suggestions) {
 | 
					                options.push(...suggestions)
 | 
				
			||||||
                    const value = suggestion.tags[type]
 | 
					 | 
				
			||||||
                    options.push(value)
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        }
 | 
					        return (options)
 | 
				
			||||||
        return Utils.Dedup(options)
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     *
 | 
					     *
 | 
				
			||||||
     * @param path
 | 
					     * @param path
 | 
				
			||||||
     * @param country
 | 
					     * @param country: a string containing one or more country codes, separated by ";"
 | 
				
			||||||
     * @param location: center point of the feature, should be [lon, lat]
 | 
					     * @param location: center point of the feature, should be [lon, lat]
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    public static getSuggestionsFor(type: string, key: string, value: string, country: string = undefined, location: [number, number] = undefined): NSIItem[] {
 | 
					    public static getSuggestionsFor(type: string, key: string, value: string, country: string = undefined, location: [number, number] = undefined): NSIItem[] {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,9 +38,9 @@ export default class TagInfo {
 | 
				
			||||||
    public async getStats(key: string, value?: string): Promise<TagInfoStats> {
 | 
					    public async getStats(key: string, value?: string): Promise<TagInfoStats> {
 | 
				
			||||||
        let url: string
 | 
					        let url: string
 | 
				
			||||||
        if (value) {
 | 
					        if (value) {
 | 
				
			||||||
            url = `${this._backend}api/4/tag/stats?key=${key}&value=${value}`
 | 
					            url = `${this._backend}api/4/tag/stats?key=${encodeURIComponent(key)}&value=${encodeURIComponent(value)}`
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            url = `${this._backend}api/4/key/stats?key=${key}`
 | 
					            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)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -104,13 +104,14 @@ export default class TagInfo {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static readonly blacklist =["VI","GF","PR"]
 | 
				
			||||||
    public static async getGlobalDistributionsFor(key: string, value?: string): Promise<Record<string, TagInfoStats>> {
 | 
					    public static async getGlobalDistributionsFor(key: string, value?: string): Promise<Record<string, TagInfoStats>> {
 | 
				
			||||||
        const countries = await this.geofabrikCountries()
 | 
					        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 perCountry: Record<string, TagInfoStats> = {}
 | 
				
			||||||
        const results = await Promise.all(countries.map(country => TagInfo.getDistributionsFor(country?.["iso3166-1:alpha2"]?.[0], key, value)))
 | 
					        const results = await Promise.all(countries.map(country => TagInfo.getDistributionsFor(country, key, value)))
 | 
				
			||||||
        for (let i = 0; i < countries.length; i++){
 | 
					        for (let i = 0; i < countries.length; i++){
 | 
				
			||||||
            const country = countries[i]
 | 
					            const countryCode = countries[i]
 | 
				
			||||||
            const countryCode = country["iso3166-1:alpha2"]?.[0]
 | 
					 | 
				
			||||||
            if(results[i]){
 | 
					            if(results[i]){
 | 
				
			||||||
             perCountry[countryCode] = results[i]
 | 
					             perCountry[countryCode] = results[i]
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,20 +10,22 @@ import Combine from "../../UI/Base/Combine"
 | 
				
			||||||
import Title from "../../UI/Base/Title"
 | 
					import Title from "../../UI/Base/Title"
 | 
				
			||||||
import Link from "../../UI/Base/Link"
 | 
					import Link from "../../UI/Base/Link"
 | 
				
			||||||
import List from "../../UI/Base/List"
 | 
					import List from "../../UI/Base/List"
 | 
				
			||||||
import {
 | 
					import { MappingConfigJson, QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRenderingConfigJson"
 | 
				
			||||||
    MappingConfigJson,
 | 
					 | 
				
			||||||
    QuestionableTagRenderingConfigJson,
 | 
					 | 
				
			||||||
} from "./Json/QuestionableTagRenderingConfigJson"
 | 
					 | 
				
			||||||
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
 | 
					import { FixedUiElement } from "../../UI/Base/FixedUiElement"
 | 
				
			||||||
import Validators, { ValidatorType } from "../../UI/InputElement/Validators"
 | 
					import Validators, { ValidatorType } from "../../UI/InputElement/Validators"
 | 
				
			||||||
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
 | 
					import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
 | 
				
			||||||
import { RegexTag } from "../../Logic/Tags/RegexTag"
 | 
					import { RegexTag } from "../../Logic/Tags/RegexTag"
 | 
				
			||||||
 | 
					import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
 | 
				
			||||||
 | 
					import NameSuggestionIndex from "../../Logic/Web/NameSuggestionIndex"
 | 
				
			||||||
 | 
					import { GeoOperations } from "../../Logic/GeoOperations"
 | 
				
			||||||
 | 
					import { Feature } from "geojson"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Icon {}
 | 
					export interface Icon {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Mapping {
 | 
					export interface Mapping {
 | 
				
			||||||
    readonly if: UploadableTag
 | 
					    readonly if: UploadableTag
 | 
				
			||||||
    readonly alsoShowIf: Tag | undefined
 | 
					    readonly alsoShowIf?: Tag
 | 
				
			||||||
    readonly ifnot?: UploadableTag
 | 
					    readonly ifnot?: UploadableTag
 | 
				
			||||||
    readonly then: TypedTranslation<object>
 | 
					    readonly then: TypedTranslation<object>
 | 
				
			||||||
    readonly icon: string
 | 
					    readonly icon: string
 | 
				
			||||||
| 
						 | 
					@ -75,7 +77,7 @@ export default class TagRenderingConfig {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public readonly multiAnswer: boolean
 | 
					    public readonly multiAnswer: boolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public readonly mappings?: Mapping[]
 | 
					    public readonly mappings: Mapping[]
 | 
				
			||||||
    public readonly editButtonAriaLabel?: Translation
 | 
					    public readonly editButtonAriaLabel?: Translation
 | 
				
			||||||
    public readonly labels: string[]
 | 
					    public readonly labels: string[]
 | 
				
			||||||
    public readonly classes: string[] | undefined
 | 
					    public readonly classes: string[] | undefined
 | 
				
			||||||
| 
						 | 
					@ -201,7 +203,7 @@ export default class TagRenderingConfig {
 | 
				
			||||||
                    ) ?? [],
 | 
					                    ) ?? [],
 | 
				
			||||||
                inline: json.freeform.inline ?? false,
 | 
					                inline: json.freeform.inline ?? false,
 | 
				
			||||||
                default: json.freeform.default,
 | 
					                default: json.freeform.default,
 | 
				
			||||||
                helperArgs: json.freeform.helperArgs,
 | 
					                helperArgs: json.freeform.helperArgs
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            if (json.freeform["extraTags"] !== undefined) {
 | 
					            if (json.freeform["extraTags"] !== undefined) {
 | 
				
			||||||
                throw `Freeform.extraTags is defined. This should probably be 'freeform.addExtraTag' (at ${context})`
 | 
					                throw `Freeform.extraTags is defined. This should probably be 'freeform.addExtraTag' (at ${context})`
 | 
				
			||||||
| 
						 | 
					@ -249,6 +251,8 @@ export default class TagRenderingConfig {
 | 
				
			||||||
                    commonIconSize
 | 
					                    commonIconSize
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					        }else{
 | 
				
			||||||
 | 
					            this.mappings = []
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!json.multiAnswer && this.mappings !== undefined && this.question !== undefined) {
 | 
					        if (!json.multiAnswer && this.mappings !== undefined && this.question !== undefined) {
 | 
				
			||||||
| 
						 | 
					@ -319,7 +323,7 @@ export default class TagRenderingConfig {
 | 
				
			||||||
        multiAnswer?: boolean,
 | 
					        multiAnswer?: boolean,
 | 
				
			||||||
        isQuestionable?: boolean,
 | 
					        isQuestionable?: boolean,
 | 
				
			||||||
        commonSize: string = "small"
 | 
					        commonSize: string = "small"
 | 
				
			||||||
    ) {
 | 
					    ): Mapping {
 | 
				
			||||||
        const ctx = `${translationKey}.mappings.${i}`
 | 
					        const ctx = `${translationKey}.mappings.${i}`
 | 
				
			||||||
        if (mapping.if === undefined) {
 | 
					        if (mapping.if === undefined) {
 | 
				
			||||||
            throw `Invalid mapping: "if" is not defined`
 | 
					            throw `Invalid mapping: "if" is not defined`
 | 
				
			||||||
| 
						 | 
					@ -395,7 +399,7 @@ export default class TagRenderingConfig {
 | 
				
			||||||
            iconClass,
 | 
					            iconClass,
 | 
				
			||||||
            addExtraTags,
 | 
					            addExtraTags,
 | 
				
			||||||
            searchTerms: mapping.searchTerms,
 | 
					            searchTerms: mapping.searchTerms,
 | 
				
			||||||
            priorityIf: prioritySearch,
 | 
					            priorityIf: prioritySearch
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (isQuestionable) {
 | 
					        if (isQuestionable) {
 | 
				
			||||||
            if (hideInAnswer !== true && mp.if !== undefined && !mp.if.isUsableAsAnswer()) {
 | 
					            if (hideInAnswer !== true && mp.if !== undefined && !mp.if.isUsableAsAnswer()) {
 | 
				
			||||||
| 
						 | 
					@ -497,7 +501,7 @@ export default class TagRenderingConfig {
 | 
				
			||||||
                    then: new TypedTranslation<object>(
 | 
					                    then: new TypedTranslation<object>(
 | 
				
			||||||
                        this.render.replace("{" + this.freeform.key + "}", leftover).translations,
 | 
					                        this.render.replace("{" + this.freeform.key + "}", leftover).translations,
 | 
				
			||||||
                        this.render.context
 | 
					                        this.render.context
 | 
				
			||||||
                    ),
 | 
					                    )
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -588,7 +592,7 @@ export default class TagRenderingConfig {
 | 
				
			||||||
                    key: commonKey,
 | 
					                    key: commonKey,
 | 
				
			||||||
                    values: Utils.NoNull(
 | 
					                    values: Utils.NoNull(
 | 
				
			||||||
                        values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v)
 | 
					                        values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v)
 | 
				
			||||||
                    ),
 | 
					                    )
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -603,7 +607,7 @@ export default class TagRenderingConfig {
 | 
				
			||||||
            return {
 | 
					            return {
 | 
				
			||||||
                key,
 | 
					                key,
 | 
				
			||||||
                type: this.freeform.type,
 | 
					                type: this.freeform.type,
 | 
				
			||||||
                values,
 | 
					                values
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (e) {
 | 
					        } catch (e) {
 | 
				
			||||||
            console.error("Could not create FreeformValues for tagrendering", this.id)
 | 
					            console.error("Could not create FreeformValues for tagrendering", this.id)
 | 
				
			||||||
| 
						 | 
					@ -691,7 +695,7 @@ export default class TagRenderingConfig {
 | 
				
			||||||
            // Either no mappings, or this is a radio-button selected freeform value
 | 
					            // Either no mappings, or this is a radio-button selected freeform value
 | 
				
			||||||
            const tag = new And([
 | 
					            const tag = new And([
 | 
				
			||||||
                new Tag(this.freeform.key, freeformValue),
 | 
					                new Tag(this.freeform.key, freeformValue),
 | 
				
			||||||
                ...(this.freeform.addExtraTags ?? []),
 | 
					                ...(this.freeform.addExtraTags ?? [])
 | 
				
			||||||
            ])
 | 
					            ])
 | 
				
			||||||
            const newProperties = tag.applyOn(currentProperties)
 | 
					            const newProperties = tag.applyOn(currentProperties)
 | 
				
			||||||
            if (this.invalidValues?.matchesProperties(newProperties)) {
 | 
					            if (this.invalidValues?.matchesProperties(newProperties)) {
 | 
				
			||||||
| 
						 | 
					@ -715,7 +719,7 @@ export default class TagRenderingConfig {
 | 
				
			||||||
                selectedMappings.push(
 | 
					                selectedMappings.push(
 | 
				
			||||||
                    new And([
 | 
					                    new And([
 | 
				
			||||||
                        new Tag(this.freeform.key, freeformValue),
 | 
					                        new Tag(this.freeform.key, freeformValue),
 | 
				
			||||||
                        ...(this.freeform.addExtraTags ?? []),
 | 
					                        ...(this.freeform.addExtraTags ?? [])
 | 
				
			||||||
                    ])
 | 
					                    ])
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
| 
						 | 
					@ -743,12 +747,12 @@ export default class TagRenderingConfig {
 | 
				
			||||||
        if (useFreeform) {
 | 
					        if (useFreeform) {
 | 
				
			||||||
            return new And([
 | 
					            return new And([
 | 
				
			||||||
                new Tag(this.freeform.key, freeformValue),
 | 
					                new Tag(this.freeform.key, freeformValue),
 | 
				
			||||||
                ...(this.freeform.addExtraTags ?? []),
 | 
					                ...(this.freeform.addExtraTags ?? [])
 | 
				
			||||||
            ])
 | 
					            ])
 | 
				
			||||||
        } else if (singleSelectedMapping !== undefined) {
 | 
					        } else if (singleSelectedMapping !== undefined) {
 | 
				
			||||||
            return new And([
 | 
					            return new And([
 | 
				
			||||||
                this.mappings[singleSelectedMapping].if,
 | 
					                this.mappings[singleSelectedMapping].if,
 | 
				
			||||||
                ...(this.mappings[singleSelectedMapping].addExtraTags ?? []),
 | 
					                ...(this.mappings[singleSelectedMapping].addExtraTags ?? [])
 | 
				
			||||||
            ])
 | 
					            ])
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            console.error("TagRenderingConfig.ConstructSpecification has a weird fallback for", {
 | 
					            console.error("TagRenderingConfig.ConstructSpecification has a weird fallback for", {
 | 
				
			||||||
| 
						 | 
					@ -756,7 +760,7 @@ export default class TagRenderingConfig {
 | 
				
			||||||
                singleSelectedMapping,
 | 
					                singleSelectedMapping,
 | 
				
			||||||
                multiSelectedMapping,
 | 
					                multiSelectedMapping,
 | 
				
			||||||
                currentProperties,
 | 
					                currentProperties,
 | 
				
			||||||
                useFreeform,
 | 
					                useFreeform
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return undefined
 | 
					            return undefined
 | 
				
			||||||
| 
						 | 
					@ -771,8 +775,8 @@ export default class TagRenderingConfig {
 | 
				
			||||||
                Link.OsmWiki(this.freeform.key),
 | 
					                Link.OsmWiki(this.freeform.key),
 | 
				
			||||||
                new Combine([
 | 
					                new Combine([
 | 
				
			||||||
                    "This is rendered with ",
 | 
					                    "This is rendered with ",
 | 
				
			||||||
                    new FixedUiElement(this.render.txt).SetClass("code font-bold"),
 | 
					                    new FixedUiElement(this.render.txt).SetClass("code font-bold")
 | 
				
			||||||
                ]),
 | 
					                ])
 | 
				
			||||||
            ]
 | 
					            ]
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -785,8 +789,8 @@ export default class TagRenderingConfig {
 | 
				
			||||||
                            new Combine([
 | 
					                            new Combine([
 | 
				
			||||||
                                new FixedUiElement(m.then.txt).SetClass("font-bold"),
 | 
					                                new FixedUiElement(m.then.txt).SetClass("font-bold"),
 | 
				
			||||||
                                " corresponds with ",
 | 
					                                " corresponds with ",
 | 
				
			||||||
                                m.if.asHumanString(true, false, {}),
 | 
					                                m.if.asHumanString(true, false, {})
 | 
				
			||||||
                            ]),
 | 
					                            ])
 | 
				
			||||||
                        ]
 | 
					                        ]
 | 
				
			||||||
                        if (m.hideInAnswer === true) {
 | 
					                        if (m.hideInAnswer === true) {
 | 
				
			||||||
                            msgs.push("_This option cannot be chosen as answer_")
 | 
					                            msgs.push("_This option cannot be chosen as answer_")
 | 
				
			||||||
| 
						 | 
					@ -809,7 +813,7 @@ export default class TagRenderingConfig {
 | 
				
			||||||
                "This tagrendering is only visible in the popup if the following condition is met:",
 | 
					                "This tagrendering is only visible in the popup if the following condition is met:",
 | 
				
			||||||
                new FixedUiElement(
 | 
					                new FixedUiElement(
 | 
				
			||||||
                    (<TagsFilter>this.condition.optimize()).asHumanString(true, false, {})
 | 
					                    (<TagsFilter>this.condition.optimize()).asHumanString(true, false, {})
 | 
				
			||||||
                ).SetClass("code"),
 | 
					                ).SetClass("code")
 | 
				
			||||||
            ])
 | 
					            ])
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -817,7 +821,7 @@ export default class TagRenderingConfig {
 | 
				
			||||||
        if (this.labels?.length > 0) {
 | 
					        if (this.labels?.length > 0) {
 | 
				
			||||||
            labels = new Combine([
 | 
					            labels = new Combine([
 | 
				
			||||||
                "This tagrendering has labels ",
 | 
					                "This tagrendering has labels ",
 | 
				
			||||||
                ...this.labels.map((label) => new FixedUiElement(label).SetClass("code")),
 | 
					                ...this.labels.map((label) => new FixedUiElement(label).SetClass("code"))
 | 
				
			||||||
            ]).SetClass("flex")
 | 
					            ]).SetClass("flex")
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -827,7 +831,7 @@ export default class TagRenderingConfig {
 | 
				
			||||||
            this.question !== undefined
 | 
					            this.question !== undefined
 | 
				
			||||||
                ? new Combine([
 | 
					                ? new Combine([
 | 
				
			||||||
                    "The question is ",
 | 
					                    "The question is ",
 | 
				
			||||||
                      new FixedUiElement(this.question.txt).SetClass("font-bold bold"),
 | 
					                    new FixedUiElement(this.question.txt).SetClass("font-bold bold")
 | 
				
			||||||
                ])
 | 
					                ])
 | 
				
			||||||
                : new FixedUiElement(
 | 
					                : new FixedUiElement(
 | 
				
			||||||
                    "This tagrendering has no question and is thus read-only"
 | 
					                    "This tagrendering has no question and is thus read-only"
 | 
				
			||||||
| 
						 | 
					@ -835,7 +839,7 @@ export default class TagRenderingConfig {
 | 
				
			||||||
            new Combine(withRender),
 | 
					            new Combine(withRender),
 | 
				
			||||||
            mappings,
 | 
					            mappings,
 | 
				
			||||||
            condition,
 | 
					            condition,
 | 
				
			||||||
            labels,
 | 
					            labels
 | 
				
			||||||
        ]).SetClass("flex flex-col")
 | 
					        ]).SetClass("flex flex-col")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -860,4 +864,29 @@ export default class TagRenderingConfig {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Utils.NoNull(tags)
 | 
					        return Utils.NoNull(tags)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class TagRenderingConfigUtils {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static withNameSuggestionIndex(config: TagRenderingConfig, tags: UIEventSource<Record<string, string>>, feature?: Feature): Store<TagRenderingConfig> {
 | 
				
			||||||
 | 
					        if(config.freeform?.type !== "nsi"){
 | 
				
			||||||
 | 
					            return new ImmutableStore(config)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const extraMappings = tags.mapD(tags => tags._country).bindD(country => {
 | 
				
			||||||
 | 
					            const [k, v] = ("" + config.freeform.helperArgs[0]).split("=")
 | 
				
			||||||
 | 
					            const center = GeoOperations.centerpointCoordinates(feature)
 | 
				
			||||||
 | 
					            return UIEventSource.FromPromise(NameSuggestionIndex.generateMappings(config.freeform.key, k, v, country.split(";"), center))
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					       return extraMappings.map(extraMappings => {
 | 
				
			||||||
 | 
					            if(!extraMappings || extraMappings.length == 0){
 | 
				
			||||||
 | 
					                return config
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            const clone: TagRenderingConfig = Object.create(config)
 | 
				
			||||||
 | 
					           /// SHHHTTT, this is not cheating at all!
 | 
				
			||||||
 | 
					            clone.mappings.splice(clone.mappings.length, 0, ...extraMappings)
 | 
				
			||||||
 | 
					            return clone
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,9 @@
 | 
				
			||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { UIEventSource } from "../../Logic/UIEventSource"
 | 
					  import { Store } from "../../Logic/UIEventSource"
 | 
				
			||||||
  import { marked } from "marked"
 | 
					  import { marked } from "marked"
 | 
				
			||||||
  export let src: string
 | 
					
 | 
				
			||||||
  export let srcWritable: UIEventSource<string> = undefined
 | 
					  export let src: string = undefined
 | 
				
			||||||
 | 
					  export let srcWritable: Store<string> = undefined
 | 
				
			||||||
  srcWritable?.addCallbackAndRunD(t => {
 | 
					  srcWritable?.addCallbackAndRunD(t => {
 | 
				
			||||||
    src = t
 | 
					    src = t
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,7 @@
 | 
				
			||||||
  import UserRelatedState from "../../Logic/State/UserRelatedState"
 | 
					  import UserRelatedState from "../../Logic/State/UserRelatedState"
 | 
				
			||||||
  import Delete_icon from "../../assets/svg/Delete_icon.svelte"
 | 
					  import Delete_icon from "../../assets/svg/Delete_icon.svelte"
 | 
				
			||||||
  import BackButton from "../Base/BackButton.svelte"
 | 
					  import BackButton from "../Base/BackButton.svelte"
 | 
				
			||||||
 | 
					  import TagRenderingEditableDynamic from "../Popup/TagRendering/TagRenderingEditableDynamic.svelte"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export let state: SpecialVisualizationState
 | 
					  export let state: SpecialVisualizationState
 | 
				
			||||||
  export let selectedElement: Feature
 | 
					  export let selectedElement: Feature
 | 
				
			||||||
| 
						 | 
					@ -68,7 +69,7 @@
 | 
				
			||||||
    tabindex="-1"
 | 
					    tabindex="-1"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    {#each $knownTagRenderings as config (config.id)}
 | 
					    {#each $knownTagRenderings as config (config.id)}
 | 
				
			||||||
      <TagRenderingEditable
 | 
					      <TagRenderingEditableDynamic
 | 
				
			||||||
        {tags}
 | 
					        {tags}
 | 
				
			||||||
        {config}
 | 
					        {config}
 | 
				
			||||||
        {state}
 | 
					        {state}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,7 +19,6 @@
 | 
				
			||||||
  import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte"
 | 
					  import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte"
 | 
				
			||||||
  import SlopeInput from "./Helpers/SlopeInput.svelte"
 | 
					  import SlopeInput from "./Helpers/SlopeInput.svelte"
 | 
				
			||||||
  import type { SpecialVisualizationState } from "../SpecialVisualization"
 | 
					  import type { SpecialVisualizationState } from "../SpecialVisualization"
 | 
				
			||||||
  import NameSuggestionIndexInput from "./Helpers/NameSuggestionIndexInput.svelte"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export let type: ValidatorType
 | 
					  export let type: ValidatorType
 | 
				
			||||||
  export let value: UIEventSource<string | object>
 | 
					  export let value: UIEventSource<string | object>
 | 
				
			||||||
| 
						 | 
					@ -54,6 +53,4 @@
 | 
				
			||||||
  <SlopeInput {value} {feature} {state} />
 | 
					  <SlopeInput {value} {feature} {state} />
 | 
				
			||||||
{:else if type === "wikidata"}
 | 
					{:else if type === "wikidata"}
 | 
				
			||||||
  <ToSvelte construct={() => InputHelpers.constructWikidataHelper(value, properties)} />
 | 
					  <ToSvelte construct={() => InputHelpers.constructWikidataHelper(value, properties)} />
 | 
				
			||||||
{:else if type === "nsi"}
 | 
					 | 
				
			||||||
  <NameSuggestionIndexInput {value} {feature} {helperArgs} {key} {extraTags} />
 | 
					 | 
				
			||||||
{/if}
 | 
					{/if}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@
 | 
				
			||||||
   * Shows all questions for which the answers are unknown.
 | 
					   * Shows all questions for which the answers are unknown.
 | 
				
			||||||
   * The questions can either be shown all at once or one at a time (in which case they can be skipped)
 | 
					   * The questions can either be shown all at once or one at a time (in which case they can be skipped)
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
 | 
					  import TagRenderingConfig, { TagRenderingConfigUtils } from "../../../Models/ThemeConfig/TagRenderingConfig"
 | 
				
			||||||
  import { Store, UIEventSource } from "../../../Logic/UIEventSource"
 | 
					  import { Store, UIEventSource } from "../../../Logic/UIEventSource"
 | 
				
			||||||
  import type { Feature } from "geojson"
 | 
					  import type { Feature } from "geojson"
 | 
				
			||||||
  import type { SpecialVisualizationState } from "../../SpecialVisualization"
 | 
					  import type { SpecialVisualizationState } from "../../SpecialVisualization"
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@
 | 
				
			||||||
  import Translations from "../../i18n/Translations.js"
 | 
					  import Translations from "../../i18n/Translations.js"
 | 
				
			||||||
  import { Utils } from "../../../Utils"
 | 
					  import { Utils } from "../../../Utils"
 | 
				
			||||||
  import { onDestroy } from "svelte"
 | 
					  import { onDestroy } from "svelte"
 | 
				
			||||||
 | 
					  import TagRenderingQuestionDynamic from "./TagRenderingQuestionDynamic.svelte"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export let layer: LayerConfig
 | 
					  export let layer: LayerConfig
 | 
				
			||||||
  export let tags: UIEventSource<Record<string, string>>
 | 
					  export let tags: UIEventSource<Record<string, string>>
 | 
				
			||||||
| 
						 | 
					@ -93,7 +94,7 @@
 | 
				
			||||||
  let skipped: number = 0
 | 
					  let skipped: number = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function skip(question: { id: string }, didAnswer: boolean = false) {
 | 
					  function skip(question: { id: string }, didAnswer: boolean = false) {
 | 
				
			||||||
    skippedQuestions.data.add(question.id)
 | 
					    skippedQuestions.data.add(question.id) // Must use ID, the config object might be a copy of the original
 | 
				
			||||||
    skippedQuestions.ping()
 | 
					    skippedQuestions.ping()
 | 
				
			||||||
    if (didAnswer) {
 | 
					    if (didAnswer) {
 | 
				
			||||||
      answered++
 | 
					      answered++
 | 
				
			||||||
| 
						 | 
					@ -161,11 +162,11 @@
 | 
				
			||||||
      {#if $showAllQuestionsAtOnce}
 | 
					      {#if $showAllQuestionsAtOnce}
 | 
				
			||||||
        <div class="flex flex-col gap-y-1">
 | 
					        <div class="flex flex-col gap-y-1">
 | 
				
			||||||
          {#each $allQuestionsToAsk as question (question.id)}
 | 
					          {#each $allQuestionsToAsk as question (question.id)}
 | 
				
			||||||
            <TagRenderingQuestion config={question} {tags} {selectedElement} {state} {layer} />
 | 
					            <TagRenderingQuestionDynamic config={question} {tags} {selectedElement} {state} {layer} />
 | 
				
			||||||
          {/each}
 | 
					          {/each}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      {:else if $firstQuestion !== undefined}
 | 
					      {:else if $firstQuestion !== undefined}
 | 
				
			||||||
        <TagRenderingQuestion
 | 
					        <TagRenderingQuestionDynamic
 | 
				
			||||||
          config={$firstQuestion}
 | 
					          config={$firstQuestion}
 | 
				
			||||||
          {layer}
 | 
					          {layer}
 | 
				
			||||||
          {selectedElement}
 | 
					          {selectedElement}
 | 
				
			||||||
| 
						 | 
					@ -184,7 +185,7 @@
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <Tr t={Translations.t.general.skip} />
 | 
					            <Tr t={Translations.t.general.skip} />
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
        </TagRenderingQuestion>
 | 
					        </TagRenderingQuestionDynamic>
 | 
				
			||||||
      {/if}
 | 
					      {/if}
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  {/if}
 | 
					  {/if}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					<script lang="ts">/**
 | 
				
			||||||
 | 
					 * Wrapper around 'tagRenderingEditable' but might add mappings dynamically
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import TagRenderingConfig, { TagRenderingConfigUtils } from "../../../Models/ThemeConfig/TagRenderingConfig"
 | 
				
			||||||
 | 
					import { UIEventSource } from "../../../Logic/UIEventSource"
 | 
				
			||||||
 | 
					import type { Feature } from "geojson"
 | 
				
			||||||
 | 
					import type { SpecialVisualizationState } from "../../SpecialVisualization"
 | 
				
			||||||
 | 
					import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
 | 
				
			||||||
 | 
					import TagRenderingEditable from "./TagRenderingEditable.svelte"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export let config: TagRenderingConfig
 | 
				
			||||||
 | 
					export let tags: UIEventSource<Record<string, string>>
 | 
				
			||||||
 | 
					export let selectedElement: Feature | undefined
 | 
				
			||||||
 | 
					export let state: SpecialVisualizationState
 | 
				
			||||||
 | 
					export let layer: LayerConfig = undefined
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export let highlightedRendering: UIEventSource<string> = undefined
 | 
				
			||||||
 | 
					export let clss = undefined
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let dynamicConfig = TagRenderingConfigUtils.withNameSuggestionIndex(config, tags, selectedElement)
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<TagRenderingEditable
 | 
				
			||||||
 | 
					  {tags}
 | 
				
			||||||
 | 
					  config={$dynamicConfig}
 | 
				
			||||||
 | 
					  {state}
 | 
				
			||||||
 | 
					  {selectedElement}
 | 
				
			||||||
 | 
					  {layer}
 | 
				
			||||||
 | 
					  {highlightedRendering}
 | 
				
			||||||
 | 
					  {clss}
 | 
				
			||||||
 | 
					/>
 | 
				
			||||||
| 
						 | 
					@ -28,11 +28,11 @@
 | 
				
			||||||
  export let mappingIsSelected: boolean
 | 
					  export let mappingIsSelected: boolean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * If there are many mappings, we might hide it.
 | 
					   * If there are many mappings, we might hide it, e.g. because of search.
 | 
				
			||||||
   * This is the searchterm where it might hide
 | 
					   * This is the searchterm where it might hide
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  export let searchTerm: undefined | UIEventSource<string>
 | 
					  export let searchTerm: undefined | UIEventSource<string>
 | 
				
			||||||
 | 
					  export let hideUnlessSearched = false
 | 
				
			||||||
  $: {
 | 
					  $: {
 | 
				
			||||||
    if (selectedElement !== undefined || mapping !== undefined) {
 | 
					    if (selectedElement !== undefined || mapping !== undefined) {
 | 
				
			||||||
      searchTerm.setData(undefined)
 | 
					      searchTerm.setData(undefined)
 | 
				
			||||||
| 
						 | 
					@ -42,17 +42,21 @@
 | 
				
			||||||
  let matchesTerm: Store<boolean> | undefined =
 | 
					  let matchesTerm: Store<boolean> | undefined =
 | 
				
			||||||
    searchTerm?.map(
 | 
					    searchTerm?.map(
 | 
				
			||||||
      (search) => {
 | 
					      (search) => {
 | 
				
			||||||
 | 
					        search = search?.trim()
 | 
				
			||||||
        if (!search) {
 | 
					        if (!search) {
 | 
				
			||||||
 | 
					          if(hideUnlessSearched){
 | 
				
			||||||
 | 
					            if (mapping.priorityIf?.matchesProperties(tags.data)) {
 | 
				
			||||||
 | 
					              return true
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return false
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
          return true
 | 
					          return true
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (mappingIsSelected) {
 | 
					        if (mappingIsSelected) {
 | 
				
			||||||
          return true
 | 
					          return true
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        search = search.toLowerCase()
 | 
					 | 
				
			||||||
        // There is a searchterm - this might hide the mapping
 | 
					        // There is a searchterm - this might hide the mapping
 | 
				
			||||||
        if (mapping.priorityIf?.matchesProperties(tags.data)) {
 | 
					        search = search.toLowerCase()
 | 
				
			||||||
          return true
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (mapping.then.txt.toLowerCase().indexOf(search) >= 0) {
 | 
					        if (mapping.then.txt.toLowerCase().indexOf(search) >= 0) {
 | 
				
			||||||
          return true
 | 
					          return true
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -293,6 +293,7 @@
 | 
				
			||||||
  let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined)
 | 
					  let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined)
 | 
				
			||||||
  let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0
 | 
					  let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0
 | 
				
			||||||
  let question = config.question
 | 
					  let question = config.question
 | 
				
			||||||
 | 
					  let hideMappingsUnlessSearchedFor = config.mappings.length > 8 && config.mappings.some(m => m.priorityIf)
 | 
				
			||||||
  $: question = config.question
 | 
					  $: question = config.question
 | 
				
			||||||
  if (state?.osmConnection) {
 | 
					  if (state?.osmConnection) {
 | 
				
			||||||
    onDestroy(
 | 
					    onDestroy(
 | 
				
			||||||
| 
						 | 
					@ -335,7 +336,7 @@
 | 
				
			||||||
          {/if}
 | 
					          {/if}
 | 
				
			||||||
        </legend>
 | 
					        </legend>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {#if config.mappings?.length >= 8}
 | 
					        {#if config.mappings?.length >= 8 || hideMappingsUnlessSearchedFor}
 | 
				
			||||||
          <div class="sticky flex w-full" aria-hidden="true">
 | 
					          <div class="sticky flex w-full" aria-hidden="true">
 | 
				
			||||||
            <Search class="h-6 w-6" />
 | 
					            <Search class="h-6 w-6" />
 | 
				
			||||||
            <input
 | 
					            <input
 | 
				
			||||||
| 
						 | 
					@ -345,7 +346,14 @@
 | 
				
			||||||
              use:placeholder={Translations.t.general.searchAnswer}
 | 
					              use:placeholder={Translations.t.general.searchAnswer}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					          {#if hideMappingsUnlessSearchedFor}
 | 
				
			||||||
 | 
					            <div class="rounded border border-black border-dashed p-1 px-2 m-1">
 | 
				
			||||||
 | 
					              <Tr t={Translations.t.general.mappingsAreHidden}/>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
          {/if}
 | 
					          {/if}
 | 
				
			||||||
 | 
					        {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {#if config.freeform?.key && !(mappings?.length > 0)}
 | 
					        {#if config.freeform?.key && !(mappings?.length > 0)}
 | 
				
			||||||
          <!-- There are no options to choose from, simply show the input element: fill out the text field -->
 | 
					          <!-- There are no options to choose from, simply show the input element: fill out the text field -->
 | 
				
			||||||
| 
						 | 
					@ -373,6 +381,7 @@
 | 
				
			||||||
                {selectedElement}
 | 
					                {selectedElement}
 | 
				
			||||||
                {layer}
 | 
					                {layer}
 | 
				
			||||||
                {searchTerm}
 | 
					                {searchTerm}
 | 
				
			||||||
 | 
					                hideUnlessSearched={hideMappingsUnlessSearchedFor}
 | 
				
			||||||
                mappingIsSelected={selectedMapping === i}
 | 
					                mappingIsSelected={selectedMapping === i}
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                <input
 | 
					                <input
 | 
				
			||||||
| 
						 | 
					@ -420,6 +429,7 @@
 | 
				
			||||||
                {selectedElement}
 | 
					                {selectedElement}
 | 
				
			||||||
                {layer}
 | 
					                {layer}
 | 
				
			||||||
                {searchTerm}
 | 
					                {searchTerm}
 | 
				
			||||||
 | 
					                hideUnlessSearched={hideMappingsUnlessSearchedFor}
 | 
				
			||||||
                mappingIsSelected={checkedMappings[i]}
 | 
					                mappingIsSelected={checkedMappings[i]}
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                <input
 | 
					                <input
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,27 +6,31 @@ import { UIEventSource } from "../../../Logic/UIEventSource"
 | 
				
			||||||
import type { Feature } from "geojson"
 | 
					import type { Feature } from "geojson"
 | 
				
			||||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
 | 
					import type { SpecialVisualizationState } from "../../SpecialVisualization"
 | 
				
			||||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
 | 
					import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
 | 
				
			||||||
import TagRenderingEditable from "./TagRenderingEditable.svelte"
 | 
					import TagRenderingQuestion from "./TagRenderingQuestion.svelte"
 | 
				
			||||||
 | 
					import type { UploadableTag } from "../../../Logic/Tags/TagUtils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export let config: TagRenderingConfig
 | 
					export let config: TagRenderingConfig
 | 
				
			||||||
export let tags: UIEventSource<Record<string, string>>
 | 
					export let tags: UIEventSource<Record<string, string>>
 | 
				
			||||||
export let selectedElement: Feature | undefined
 | 
					
 | 
				
			||||||
 | 
					export let selectedElement: Feature
 | 
				
			||||||
export let state: SpecialVisualizationState
 | 
					export let state: SpecialVisualizationState
 | 
				
			||||||
export let layer: LayerConfig = undefined
 | 
					export let layer: LayerConfig | undefined
 | 
				
			||||||
 | 
					export let selectedTags: UploadableTag = undefined
 | 
				
			||||||
 | 
					export let extraTags: UIEventSource<Record<string, string>> = new UIEventSource({})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export let allowDeleteOfFreeform: boolean = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export let highlightedRendering: UIEventSource<string> = undefined
 | 
					 | 
				
			||||||
export let clss = undefined
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
let dynamicConfig = TagRenderingConfigUtils.withNameSuggestionIndex(config, tags, selectedElement)
 | 
					let dynamicConfig = TagRenderingConfigUtils.withNameSuggestionIndex(config, tags, selectedElement)
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<TagRenderingEditable
 | 
					<TagRenderingQuestion
 | 
				
			||||||
  {tags}
 | 
					  {tags}
 | 
				
			||||||
  config={$dynamicConfig}
 | 
					  config={$dynamicConfig}
 | 
				
			||||||
  {state}
 | 
					  {state}
 | 
				
			||||||
  {selectedElement}
 | 
					  {selectedElement}
 | 
				
			||||||
  {layer}
 | 
					  {layer}
 | 
				
			||||||
  {highlightedRendering}
 | 
					  {selectedTags}
 | 
				
			||||||
  {clss}
 | 
					  {allowDeleteOfFreeform}
 | 
				
			||||||
 | 
					  {extraTags}
 | 
				
			||||||
/>
 | 
					/>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue