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,10 +71,8 @@ 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,13 +77,13 @@ 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
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
config: string | TagRenderingConfigJson | (QuestionableTagRenderingConfigJson & {questionHintIsMd?: boolean}),
|
config: string | TagRenderingConfigJson | (QuestionableTagRenderingConfigJson & { questionHintIsMd?: boolean }),
|
||||||
context?: string
|
context?: string
|
||||||
) {
|
) {
|
||||||
let json = <string | QuestionableTagRenderingConfigJson>config
|
let json = <string | QuestionableTagRenderingConfigJson>config
|
||||||
|
@ -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…
Reference in a new issue