Refactoring: split 'Utils' into multiple files; fix some stray uppercase-method names

This commit is contained in:
Pieter Vander Vennet 2025-08-01 04:02:09 +02:00
parent 81be4db044
commit 3ec89826e4
97 changed files with 884 additions and 921 deletions

View file

@ -5,6 +5,7 @@ import { LayerConfigJson } from "../../../src/Models/ThemeConfig/Json/LayerConfi
import FilterConfigJson from "../../../src/Models/ThemeConfig/Json/FilterConfigJson" import FilterConfigJson from "../../../src/Models/ThemeConfig/Json/FilterConfigJson"
import RewritableConfigJson from "../../../src/Models/ThemeConfig/Json/RewritableConfigJson" import RewritableConfigJson from "../../../src/Models/ThemeConfig/Json/RewritableConfigJson"
import { TagRenderingConfigJson } from "../../../src/Models/ThemeConfig/Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "../../../src/Models/ThemeConfig/Json/TagRenderingConfigJson"
import { Lists } from "../../../src/Utils/Lists"
interface ChargingStandard { interface ChargingStandard {
key: string, key: string,
@ -27,36 +28,36 @@ function colonSplit(value: string): string[] {
} }
function loadCsv(file): ChargingStandard[] { function loadCsv(file): ChargingStandard[] {
const entries: string[] = Utils.NoNull(readFileSync(file, "utf8").split("\n").map(str => str.trim())) const entries: string[] = Lists.noNull(readFileSync(file, "utf8").split("\n").map(str => str.trim()))
const header = entries.shift().split(",") const header = entries.shift().split(",")
return Utils.NoNull(entries.map(entry => { return Lists.noNull(entries.map(entry => {
const values = entry.split(",").map(str => str.trim()) const values = entry.split(",").map(str => str.trim())
if (values[0] === undefined || values[0] === "") { if (values[0] === undefined || values[0] === "") {
return undefined return undefined
}
const v = {}
const colonSeperated = ["commonVoltages", "commonOutputs", "commonCurrents", "countryWhiteList", "countryBlackList", "associatedVehicleTypes", "neverAssociatedWith"]
const descriptionTranslations = new Map<string, string>()
for (let j = 0; j < header.length; j++) {
const key = header[j]
if (key.startsWith("description")) {
const language = key.substring("description:".length)
descriptionTranslations.set(language, values[j])
} }
const v = {} if (colonSeperated.indexOf(key) >= 0) {
const colonSeperated = ["commonVoltages", "commonOutputs", "commonCurrents", "countryWhiteList", "countryBlackList", "associatedVehicleTypes", "neverAssociatedWith"] v[key] = colonSplit(values[j])
const descriptionTranslations = new Map<string, string>() } else {
for (let j = 0; j < header.length; j++) { v[key] = values[j]
const key = header[j]
if (key.startsWith("description")) {
const language = key.substring("description:".length)
descriptionTranslations.set(language, values[j])
}
if (colonSeperated.indexOf(key) >= 0) {
v[key] = colonSplit(values[j])
} else {
v[key] = values[j]
}
} }
v["description"] = descriptionTranslations }
if (v["id"] === "") { v["description"] = descriptionTranslations
v["id"] = v["key"] if (v["id"] === "") {
} v["id"] = v["key"]
return <any>v }
return <any>v
})) }))
} }
@ -161,7 +162,7 @@ function run(file, protojson) {
// We add a second time for any amount to trigger a visualisation; but this is not an answer option // We add a second time for any amount to trigger a visualisation; but this is not an answer option
const no_ask_json = { const no_ask_json = {
if: { if: {
and: Utils.NoEmpty([`${e.key}~*`, `${e.key}!=1`, ...e.extraVisualisationCondition.split(";")]), and: Lists.noEmpty([`${e.key}~*`, `${e.key}!=1`, ...e.extraVisualisationCondition.split(";")]),
}, },
then: txt, then: txt,
hideInAnswer: true, hideInAnswer: true,

File diff suppressed because it is too large Load diff

View file

@ -943,9 +943,9 @@
"ru": "Onroerend Erfgoed ID:" "ru": "Onroerend Erfgoed ID:"
}, },
"special": { "special": {
"type": "link",
"href": "https://id.erfgoed.net/erfgoedobjecten/{ref:OnroerendErfgoed}", "href": "https://id.erfgoed.net/erfgoedobjecten/{ref:OnroerendErfgoed}",
"text": "{ref:OnroerendErfgoed}" "text": "{ref:OnroerendErfgoed}",
"type": "link"
} }
}, },
"icon": "./assets/layers/tree/Onroerend_Erfgoed_logo_without_text.svg", "icon": "./assets/layers/tree/Onroerend_Erfgoed_logo_without_text.svg",

View file

@ -103,13 +103,13 @@ class StatsDownloader {
let page = 1 let page = 1
let allFeatures: ChangeSetData[] = [] let allFeatures: ChangeSetData[] = []
const endDay = new Date(year, month - 1 /* Zero-indexed: 0 = january*/, day + 1) const endDay = new Date(year, month - 1 /* Zero-indexed: 0 = january*/, day + 1)
const endDate = `${endDay.getFullYear()}-${Utils.TwoDigits( const endDate = `${endDay.getFullYear()}-${Utils.twoDigits(
endDay.getMonth() + 1 endDay.getMonth() + 1
)}-${Utils.TwoDigits(endDay.getDate())}` )}-${Utils.twoDigits(endDay.getDate())}`
let url = this.urlTemplate let url = this.urlTemplate
.replace( .replace(
"{start_date}", "{start_date}",
year + "-" + Utils.TwoDigits(month) + "-" + Utils.TwoDigits(day) year + "-" + Utils.twoDigits(month) + "-" + Utils.twoDigits(day)
) )
.replace("{end_date}", endDate) .replace("{end_date}", endDate)
.replace("{page}", "" + page) .replace("{page}", "" + page)
@ -142,7 +142,7 @@ class StatsDownloader {
} }
url = result.next url = result.next
} }
allFeatures = Utils.NoNull(allFeatures) allFeatures = Lists.noNull(allFeatures)
allFeatures.forEach((f) => { allFeatures.forEach((f) => {
f.properties = { ...f.properties, ...f.properties.metadata } f.properties = { ...f.properties, ...f.properties.metadata }
if (f.properties.editor.toLowerCase().indexOf("android") >= 0) { if (f.properties.editor.toLowerCase().indexOf("android") >= 0) {

View file

@ -9,6 +9,7 @@ import { existsSync, readFileSync, writeFileSync } from "fs"
import WikidataUtils from "../src/Utils/WikidataUtils" import WikidataUtils from "../src/Utils/WikidataUtils"
import LanguageUtils from "../src/Utils/LanguageUtils" import LanguageUtils from "../src/Utils/LanguageUtils"
import Wikidata from "../src/Logic/Web/Wikidata" import Wikidata from "../src/Logic/Web/Wikidata"
import { Lists } from "../src/Utils/Lists"
interface value<T> { interface value<T> {
value: T value: T
@ -196,7 +197,7 @@ async function main(wipeCache = false) {
}) })
translatedForId["_meta"] = { translatedForId["_meta"] = {
countries: Utils.Dedup(languagesPerCountry[key]), countries: Lists.dedup(languagesPerCountry[key]),
dir: value.directionality, dir: value.directionality,
} }

View file

@ -7,6 +7,7 @@ import Validators from "../src/UI/InputElement/Validators"
import { AllKnownLayouts } from "../src/Customizations/AllKnownLayouts" import { AllKnownLayouts } from "../src/Customizations/AllKnownLayouts"
import { AllSharedLayers } from "../src/Customizations/AllSharedLayers" import { AllSharedLayers } from "../src/Customizations/AllSharedLayers"
import Constants from "../src/Models/Constants" import Constants from "../src/Models/Constants"
import { Lists } from "../src/Utils/Lists"
const metainfo = { const metainfo = {
type: "One of the inputValidator types", type: "One of the inputValidator types",
@ -371,7 +372,7 @@ function extractMeta(
const fullPath = "./src/assets/schemas/" + path + ".json" const fullPath = "./src/assets/schemas/" + path + ".json"
writeFileSync(fullPath, JSON.stringify(paths, null, " ")) writeFileSync(fullPath, JSON.stringify(paths, null, " "))
console.log("Written meta to " + fullPath) console.log("Written meta to " + fullPath)
return Utils.NoNull(paths.map((p) => validateMeta(p))) return Lists.noNull(paths.map((p) => validateMeta(p)))
} }
function main() { function main() {

View file

@ -34,6 +34,7 @@ import { ImmutableStore } from "../src/Logic/UIEventSource"
import * as unitUsage from "../Docs/Schemas/UnitConfigJson.schema.json" import * as unitUsage from "../Docs/Schemas/UnitConfigJson.schema.json"
import { ThemeConfigJson } from "../src/Models/ThemeConfig/Json/ThemeConfigJson" import { ThemeConfigJson } from "../src/Models/ThemeConfig/Json/ThemeConfigJson"
import { ServerSourceInfo, SourceOverview } from "../src/Models/SourceOverview" import { ServerSourceInfo, SourceOverview } from "../src/Models/SourceOverview"
import { Lists } from "../src/Utils/Lists"
/** /**
* Converts a markdown-file into a .json file, which a walkthrough/slideshow element can use * Converts a markdown-file into a .json file, which a walkthrough/slideshow element can use
@ -562,7 +563,7 @@ export class GenerateDocs extends Script {
) )
) )
const serverInfos = Utils.DedupOnId(serverInfosDupl, (item) => item.url) const serverInfos = Utils.DedupOnId(serverInfosDupl, (item) => item.url)
const titles = Utils.Dedup(Utils.NoEmpty(serverInfos.map((s) => s.category))) const titles = Lists.dedup(Lists.noEmpty(serverInfos.map((s) => s.category)))
titles.sort() titles.sort()
function getHost(item: ServerSourceInfo) { function getHost(item: ServerSourceInfo) {
@ -597,7 +598,7 @@ export class GenerateDocs extends Script {
md.push(items.length + " items") md.push(items.length + " items")
md.push(categoryExplanation[title]) md.push(categoryExplanation[title])
const hosts = Utils.Dedup(items.map(getHost)) const hosts = Lists.dedup(items.map(getHost))
hosts.sort() hosts.sort()
if (title === "maplayer") { if (title === "maplayer") {
md.push(MarkdownUtils.list(hosts)) md.push(MarkdownUtils.list(hosts))
@ -636,7 +637,7 @@ export class GenerateDocs extends Script {
return [ return [
item.url, item.url,
identicalDescription ? "" : item.description, identicalDescription ? "" : item.description,
Utils.NoEmpty([ Lists.noEmpty([
item.openData ? "OpenData" : "", item.openData ? "OpenData" : "",
sourceAvailable, sourceAvailable,
selfHostable, selfHostable,
@ -720,9 +721,7 @@ export class GenerateDocs extends Script {
MarkdownUtils.list( MarkdownUtils.list(
Constants.priviliged_layers.map((id) => "[" + id + "](#" + id + ")") Constants.priviliged_layers.map((id) => "[" + id + "](#" + id + ")")
), ),
...Utils.NoNull( ...Lists.noNull(Constants.priviliged_layers.map((id) => AllSharedLayers.sharedLayers.get(id))).map((l) =>
Constants.priviliged_layers.map((id) => AllSharedLayers.sharedLayers.get(id))
).map((l) =>
l.generateDocumentation({ l.generateDocumentation({
usedInThemes: themesPerLayer.get(l.id), usedInThemes: themesPerLayer.get(l.id),
layerIsNeededBy: layerIsNeededBy, layerIsNeededBy: layerIsNeededBy,

View file

@ -13,6 +13,7 @@ import { TagUtils } from "../src/Logic/Tags/TagUtils"
import { TagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson"
import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable" import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable"
import * as questions from "../assets/layers/questions/questions.json" import * as questions from "../assets/layers/questions/questions.json"
import { Lists } from "../src/Utils/Lists"
export class GenerateFavouritesLayer extends Script { export class GenerateFavouritesLayer extends Script {
private readonly layers: LayerConfigJson[] = [] private readonly layers: LayerConfigJson[] = []
@ -155,7 +156,7 @@ export class GenerateFavouritesLayer extends Script {
continue continue
} }
newTr.condition = { newTr.condition = {
and: Utils.NoNull([newTr.condition, layerConfig.source["osmTags"]]), and: Lists.noNull([newTr.condition, layerConfig.source["osmTags"]]),
} }
generatedTagRenderings.push(newTr) generatedTagRenderings.push(newTr)
blacklistedIds.add(newTr.id) blacklistedIds.add(newTr.id)

View file

@ -37,10 +37,8 @@ import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable"
import { ValidateThemeAndLayers } from "../src/Models/ThemeConfig/Conversion/ValidateThemeAndLayers" import { ValidateThemeAndLayers } from "../src/Models/ThemeConfig/Conversion/ValidateThemeAndLayers"
import { ExtractImages } from "../src/Models/ThemeConfig/Conversion/FixImages" import { ExtractImages } from "../src/Models/ThemeConfig/Conversion/FixImages"
import { TagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson"
import { import { LayerConfigDependencyGraph, LevelInfo } from "../src/Models/ThemeConfig/LayerConfigDependencyGraph"
LayerConfigDependencyGraph, import { Lists } from "../src/Utils/Lists"
LevelInfo,
} from "../src/Models/ThemeConfig/LayerConfigDependencyGraph"
// This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files. // This scripts scans 'src/assets/layers/*.json' for layer definition files and 'src/assets/themes/*.json' for theme definition files.
// It spits out an overview of those to be used to load them // It spits out an overview of those to be used to load them
@ -126,20 +124,18 @@ class AddIconSummary extends DesugaringStep<{ raw: LayerConfigJson; parsed: Laye
pr.location.has("point") pr.location.has("point")
) )
const defaultTags = layerConfig.baseTags const defaultTags = layerConfig.baseTags
fixed["_layerIcon"] = Utils.NoNull( fixed["_layerIcon"] = Lists.noNull((pointRendering?.marker ?? []).map((i) => {
(pointRendering?.marker ?? []).map((i) => { const icon = i.icon?.GetRenderValue(defaultTags)?.txt
const icon = i.icon?.GetRenderValue(defaultTags)?.txt if (!icon) {
if (!icon) { return undefined
return undefined }
} const result = { icon }
const result = { icon } const c = i.color?.GetRenderValue(defaultTags)?.txt
const c = i.color?.GetRenderValue(defaultTags)?.txt if (c) {
if (c) { result["color"] = c
result["color"] = c }
} return result
return result }))
})
)
return { raw: fixed, parsed: layerConfig } return { raw: fixed, parsed: layerConfig }
} }
} }
@ -233,7 +229,7 @@ class LayerBuilder extends Conversion<object, Map<string, LayerConfigJson>> {
const origIds: ReadonlyArray<string> = [...ids] const origIds: ReadonlyArray<string> = [...ids]
const deps = this._dependencies const deps = this._dependencies
const allDeps = Utils.Dedup([].concat(...ids.map((id) => deps.get(id)))) const allDeps = Lists.dedup([].concat(...ids.map((id) => deps.get(id))))
const depsRecord = Utils.asRecord(Array.from(deps.keys()), (k) => const depsRecord = Utils.asRecord(Array.from(deps.keys()), (k) =>
deps.get(k).filter((dep) => ids.indexOf(dep) >= 0) deps.get(k).filter((dep) => ids.indexOf(dep) >= 0)
) )
@ -864,7 +860,7 @@ class LayerOverviewUtils extends Script {
) )
} }
if(printAssets){ if(printAssets){
const images = Utils.Dedup(Array.from(sharedThemes.values()).flatMap(th => th._usedImages ?? [] )) const images = Lists.dedup(Array.from(sharedThemes.values()).flatMap(th => th._usedImages ?? []))
writeFileSync("needed_assets.csv", images.join("\n")) writeFileSync("needed_assets.csv", images.join("\n"))
console.log("Written needed_assets.csv") console.log("Written needed_assets.csv")
} }
@ -1278,11 +1274,9 @@ class LayerOverviewUtils extends Script {
} }
} }
const usedImages = Utils.Dedup( const usedImages = Lists.dedup(new ExtractImages(true, knownTagRenderings)
new ExtractImages(true, knownTagRenderings) .convertStrict(themeFile)
.convertStrict(themeFile) .map((x) => x.path))
.map((x) => x.path)
)
usedImages.sort() usedImages.sort()
themeFile["_usedImages"] = usedImages themeFile["_usedImages"] = usedImages

View file

@ -3,6 +3,7 @@ import SmallLicense from "../src/Models/smallLicense"
import ScriptUtils from "./ScriptUtils" import ScriptUtils from "./ScriptUtils"
import Script from "./Script" import Script from "./Script"
import { Utils } from "../src/Utils" import { Utils } from "../src/Utils"
import { Lists } from "../src/Utils/Lists"
const prompt = require("prompt-sync")() const prompt = require("prompt-sync")()
@ -297,9 +298,7 @@ export class GenerateLicenseInfo extends Script {
sources: license.sources, sources: license.sources,
} }
cloned.license = Utils.Dedup( cloned.license = Lists.dedup(cloned.license.split(";").map((l) => this.toSPDXCompliantLicense(l))).join("; ")
cloned.license.split(";").map((l) => this.toSPDXCompliantLicense(l))
).join("; ")
if (cloned.license === "CC0-1.0; TRIVIAL") { if (cloned.license === "CC0-1.0; TRIVIAL") {
cloned.license = "TRIVIAL" cloned.license = "TRIVIAL"
} }

View file

@ -10,6 +10,7 @@ import Script from "./Script"
import NameSuggestionIndex from "../src/Logic/Web/NameSuggestionIndex" import NameSuggestionIndex from "../src/Logic/Web/NameSuggestionIndex"
import TagInfo from "../src/Logic/Web/TagInfo" import TagInfo from "../src/Logic/Web/TagInfo"
import { TagsFilter } from "../src/Logic/Tags/TagsFilter" import { TagsFilter } from "../src/Logic/Tags/TagsFilter"
import { Lists } from "../src/Utils/Lists"
class GenerateNsiStats extends Script { class GenerateNsiStats extends Script {
async createOptimizationFile(includeTags = true) { async createOptimizationFile(includeTags = true) {
@ -139,9 +140,7 @@ class GenerateNsiStats extends Script {
) )
} }
const nsi = await NameSuggestionIndex.singleton() const nsi = await NameSuggestionIndex.singleton()
const allBrandNames: string[] = Utils.Dedup( const allBrandNames: string[] = Lists.dedup(nsi.allPossible(<any>type).map((item) => item.tags[type]))
nsi.allPossible(<any>type).map((item) => item.tags[type])
)
const batchSize = 50 const batchSize = 50
for (let i = 0; i < allBrandNames.length; i += batchSize) { for (let i = 0; i < allBrandNames.length; i += batchSize) {
console.warn( console.warn(
@ -152,7 +151,7 @@ class GenerateNsiStats extends Script {
) )
let downloaded = 0 let downloaded = 0
await Promise.all( await Promise.all(
Utils.TimesT(batchSize, async (j) => { Utils.timesT(batchSize, async (j) => {
const brand = allBrandNames[i + j] const brand = allBrandNames[i + j]
if (!allBrands[brand]) { if (!allBrands[brand]) {
allBrands[brand] = {} allBrands[brand] = {}

View file

@ -5,6 +5,7 @@ import { readFileSync, writeFileSync } from "fs"
import ThemeConfig from "../src/Models/ThemeConfig/ThemeConfig" import ThemeConfig from "../src/Models/ThemeConfig/ThemeConfig"
import LayerConfig from "../src/Models/ThemeConfig/LayerConfig" import LayerConfig from "../src/Models/ThemeConfig/LayerConfig"
import { Utils } from "../src/Utils" import { Utils } from "../src/Utils"
import { Lists } from "../src/Utils/Lists"
/** /**
* Generates all the files in "Docs/TagInfo". These are picked up by the taginfo project, showing a link to the mapcomplete theme if the key is used * Generates all the files in "Docs/TagInfo". These are picked up by the taginfo project, showing a link to the mapcomplete theme if the key is used
@ -269,7 +270,7 @@ function main() {
} }
files.push(generateTagInfoEntry(layout)) files.push(generateTagInfoEntry(layout))
} }
generateProjectsOverview(Utils.NoNull(files)) generateProjectsOverview(Lists.noNull(files))
} }
main() main()

View file

@ -4,6 +4,7 @@ import { Utils } from "../src/Utils"
import ScriptUtils from "./ScriptUtils" import ScriptUtils from "./ScriptUtils"
import Script from "./Script" import Script from "./Script"
import Constants from "../src/Models/Constants" import Constants from "../src/Models/Constants"
import { Lists } from "../src/Utils/Lists"
const knownLanguages = ["en", "nl", "de", "fr", "es", "gl", "ca"] const knownLanguages = ["en", "nl", "de", "fr", "es", "gl", "ca"]
const ignoreTerms = ["searchTerms"] const ignoreTerms = ["searchTerms"]
@ -172,7 +173,7 @@ class TranslationPart {
languages.push(...(value as TranslationPart).knownLanguages()) languages.push(...(value as TranslationPart).knownLanguages())
} }
} }
return Utils.Dedup(languages) return Lists.dedup(languages)
} }
toJson(neededLanguage?: string): string { toJson(neededLanguage?: string): string {
@ -351,7 +352,7 @@ function transformTranslation(
} }
const values: string[] = [] const values: string[] = []
const spaces = Utils.Times((_) => " ", path.length + 1) const spaces = Utils.times((_) => " ", path.length + 1)
for (const key in obj) { for (const key in obj) {
if (key === "#") { if (key === "#") {
@ -381,7 +382,7 @@ function transformTranslation(
let expr = `new Translation(${JSON.stringify(value)}, "core:${path.join(".")}.${key}")` let expr = `new Translation(${JSON.stringify(value)}, "core:${path.join(".")}.${key}")`
if (subParts !== null) { if (subParts !== null) {
// convert '{to_substitute}' into 'to_substitute' // convert '{to_substitute}' into 'to_substitute'
const types = Utils.Dedup(subParts.map((tp) => tp.substring(1, tp.length - 1))) const types = Lists.dedup(subParts.map((tp) => tp.substring(1, tp.length - 1)))
const invalid = types.filter( const invalid = types.filter(
(part) => part.match(/^[a-z0-9A-Z_]+(\(.*\))?$/) == null (part) => part.match(/^[a-z0-9A-Z_]+(\(.*\))?$/) == null
) )
@ -764,7 +765,7 @@ class GenerateTranslations extends Script {
"themes" "themes"
) )
const usedLanguages: string[] = Utils.Dedup(l1.concat(l2)).filter((v) => v !== "*") const usedLanguages: string[] = Lists.dedup(l1.concat(l2)).filter((v) => v !== "*")
usedLanguages.sort() usedLanguages.sort()
fs.writeFileSync( fs.writeFileSync(
"./src/assets/used_languages.json", "./src/assets/used_languages.json",

View file

@ -128,7 +128,7 @@ class NsiLogos extends Script {
} }
const results = await Promise.all( const results = await Promise.all(
Utils.TimesT(stepcount, (j) => j).map(async (j) => { Utils.timesT(stepcount, (j) => j).map(async (j) => {
return await this.downloadLogo(items[i + j], type, basePath, alreadyDownloaded) return await this.downloadLogo(items[i + j], type, basePath, alreadyDownloaded)
}) })
) )

View file

@ -1,7 +1,7 @@
import Script from "../Script" import Script from "../Script"
import { readFileSync, writeFileSync } from "fs" import { readFileSync, writeFileSync } from "fs"
import { OsmId } from "../../src/Models/OsmFeature" import { OsmId } from "../../src/Models/OsmFeature"
import { Utils } from "../../src/Utils" import { Lists } from "../../src/Utils/Lists"
interface DiffItem { interface DiffItem {
/** /**
@ -34,7 +34,7 @@ export class DiffToCsv extends Script {
JSON.parse(readFileSync(file, "utf8")) JSON.parse(readFileSync(file, "utf8"))
) )
const diffs = json.diffs const diffs = json.diffs
const allKeys = Utils.Dedup(diffs.flatMap((item) => item.diffs.map((d) => d.key))) const allKeys = Lists.dedup(diffs.flatMap((item) => item.diffs.map((d) => d.key)))
allKeys.sort() allKeys.sort()
const header = [ const header = [

View file

@ -74,7 +74,7 @@ class VeloParkToGeojson extends Script {
const batchSize = 50 const batchSize = 50
for (let i = 0; i < allVeloparkRaw.length; i += batchSize) { for (let i = 0; i < allVeloparkRaw.length; i += batchSize) {
await Promise.all( await Promise.all(
Utils.TimesT(batchSize, (j) => j).map(async (j) => { Utils.timesT(batchSize, (j) => j).map(async (j) => {
const f = allVeloparkRaw[i + j] const f = allVeloparkRaw[i + j]
if (!f) { if (!f) {
return return

View file

@ -3,6 +3,7 @@ import Locale from "../../UI/i18n/Locale"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { Feature } from "geojson" import { Feature } from "geojson"
import { SpecialVisualizationState } from "../../UI/SpecialVisualization" import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
import { Lists } from "../../Utils/Lists"
export default class TitleHandler { export default class TitleHandler {
constructor(selectedElement: Store<Feature>, state: SpecialVisualizationState) { constructor(selectedElement: Store<Feature>, state: SpecialVisualizationState) {
@ -22,7 +23,7 @@ export default class TitleHandler {
if (layer.title === undefined) { if (layer.title === undefined) {
return defaultTitle return defaultTitle
} }
const toRender = Utils.NoNull(layer?.title?.GetRenderValues(tags)) const toRender = Lists.noNull(layer?.title?.GetRenderValues(tags))
const titleUnsubbed = toRender[0]?.then?.textFor(lng) const titleUnsubbed = toRender[0]?.then?.textFor(lng)
if (titleUnsubbed === undefined) { if (titleUnsubbed === undefined) {
return defaultTitle return defaultTitle

View file

@ -17,6 +17,7 @@ import { ThemeConfigJson } from "../Models/ThemeConfig/Json/ThemeConfigJson"
import { ValidateThemeAndLayers } from "../Models/ThemeConfig/Conversion/ValidateThemeAndLayers" import { ValidateThemeAndLayers } from "../Models/ThemeConfig/Conversion/ValidateThemeAndLayers"
import * as theme_overview from "../assets/generated/theme_overview.json" import * as theme_overview from "../assets/generated/theme_overview.json"
import * as favourite_layer from "../../assets/layers/favourite/favourite.json" import * as favourite_layer from "../../assets/layers/favourite/favourite.json"
import { Lists } from "../Utils/Lists"
export default class DetermineTheme { export default class DetermineTheme {
private static readonly _knownImages = new Set(Array.from(licenses).map((l) => l.path)) private static readonly _knownImages = new Set(Array.from(licenses).map((l) => l.path))
private static readonly loadCustomThemeParam = QueryParameters.GetQueryParameter( private static readonly loadCustomThemeParam = QueryParameters.GetQueryParameter(
@ -143,27 +144,25 @@ export default class DetermineTheme {
if (json.layers === undefined && json.tagRenderings !== undefined) { if (json.layers === undefined && json.tagRenderings !== undefined) {
// We got fed a layer instead of a theme // We got fed a layer instead of a theme
const layerConfig = <LayerConfigJson>json const layerConfig = <LayerConfigJson>json
let icon = Utils.NoNull( let icon = Lists.noNull(layerConfig.pointRendering
layerConfig.pointRendering .flatMap((pr) => pr.marker)
.flatMap((pr) => pr.marker) .map((iconSpec) => {
.map((iconSpec) => { if (!iconSpec) {
if (!iconSpec) { return undefined
return undefined }
} const icon = new TagRenderingConfig(<TagRenderingConfigJson>iconSpec.icon)
const icon = new TagRenderingConfig(<TagRenderingConfigJson>iconSpec.icon) .render.txt
.render.txt if (
if ( iconSpec.color === undefined ||
iconSpec.color === undefined || icon.startsWith("http:") ||
icon.startsWith("http:") || icon.startsWith("https:")
icon.startsWith("https:") ) {
) { return icon
return icon }
} const color = new TagRenderingConfig(<TagRenderingConfigJson>iconSpec.color)
const color = new TagRenderingConfig(<TagRenderingConfigJson>iconSpec.color) .render.txt
.render.txt return icon + ":" + color
return icon + ":" + color })).join(";")
})
).join(";")
if (!icon) { if (!icon) {
icon = "./assets/svg/bug.svg" icon = "./assets/svg/bug.svg"

View file

@ -1,8 +1,8 @@
import { Store, UIEventSource } from "../../UIEventSource" import { Store, UIEventSource } from "../../UIEventSource"
import { FeatureSource, IndexedFeatureSource, UpdatableFeatureSource } from "../FeatureSource" import { FeatureSource, IndexedFeatureSource, UpdatableFeatureSource } from "../FeatureSource"
import { Feature } from "geojson" import { Feature } from "geojson"
import { Utils } from "../../../Utils"
import { OsmFeature } from "../../../Models/OsmFeature" import { OsmFeature } from "../../../Models/OsmFeature"
import { Lists } from "../../../Utils/Lists"
/** /**
* The featureSourceMerger receives complete geometries from various sources. * The featureSourceMerger receives complete geometries from various sources.
@ -23,7 +23,7 @@ export default class FeatureSourceMerger<Src extends FeatureSource = FeatureSour
constructor(...sources: Src[]) { constructor(...sources: Src[]) {
this._featuresById = new UIEventSource<Map<string, Feature>>(new Map<string, Feature>()) this._featuresById = new UIEventSource<Map<string, Feature>>(new Map<string, Feature>())
this.featuresById = this._featuresById this.featuresById = this._featuresById
sources = Utils.NoNull(sources) sources = Lists.noNull(sources)
for (const source of sources) { for (const source of sources) {
source.features.addCallback(() => { source.features.addCallback(() => {
this.addDataFromSources(sources) this.addDataFromSources(sources)
@ -69,7 +69,7 @@ export default class FeatureSourceMerger<Src extends FeatureSource = FeatureSour
} }
protected addData(sources: Feature[][]) { protected addData(sources: Feature[][]) {
sources = Utils.NoNull(sources) sources = Lists.noNull(sources)
let somethingChanged = false let somethingChanged = false
const all: Map<string, Feature> = new Map() const all: Map<string, Feature> = new Map()
const unseen = new Set<string>() const unseen = new Set<string>()

View file

@ -6,6 +6,7 @@ import BaseUIElement from "../../../UI/BaseUIElement"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import { OsmTags } from "../../../Models/OsmFeature" import { OsmTags } from "../../../Models/OsmFeature"
import { FeatureSource } from "../FeatureSource" import { FeatureSource } from "../FeatureSource"
import { Lists } from "../../../Utils/Lists"
/** /**
* Highly specialized feature source. * Highly specialized feature source.
@ -48,11 +49,9 @@ export class LastClickFeatureSource implements FeatureSource {
allPresets.push(html) allPresets.push(html)
} }
this.renderings = Utils.Dedup( this.renderings = Lists.dedup(allPresets.map((uiElem) =>
allPresets.map((uiElem) => Utils.runningFromConsole ? "" : uiElem.ConstructElement().innerHTML
Utils.runningFromConsole ? "" : uiElem.ConstructElement().innerHTML ))
)
)
this._features = new UIEventSource<Feature[]>([]) this._features = new UIEventSource<Feature[]>([])
this.features = this._features this.features = this._features

View file

@ -8,6 +8,7 @@ import { Feature } from "geojson"
import FeatureSourceMerger from "../Sources/FeatureSourceMerger" import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
import OsmObjectDownloader from "../../Osm/OsmObjectDownloader" import OsmObjectDownloader from "../../Osm/OsmObjectDownloader"
import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource" import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource"
import { Lists } from "../../../Utils/Lists"
/** /**
* If a tile is needed (requested via the UIEventSource in the constructor), will download the appropriate tile and pass it via 'handleTile' * If a tile is needed (requested via the UIEventSource in the constructor), will download the appropriate tile and pass it via 'handleTile'
@ -173,7 +174,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
for (let i = 0; i < features.length; i++) { for (let i = 0; i < features.length; i++) {
features[i] = await this.patchIncompleteRelations(features[i], <any>osmJson) features[i] = await this.patchIncompleteRelations(features[i], <any>osmJson)
} }
features = Utils.NoNull(features) features = Lists.noNull(features)
features.forEach((f) => { features.forEach((f) => {
f.properties["_backend"] = this._backend f.properties["_backend"] = this._backend
}) })

View file

@ -8,6 +8,7 @@ import { Utils } from "../../../Utils"
import { TagsFilter } from "../../Tags/TagsFilter" import { TagsFilter } from "../../Tags/TagsFilter"
import { BBox } from "../../BBox" import { BBox } from "../../BBox"
import { OsmTags } from "../../../Models/OsmFeature" import { OsmTags } from "../../../Models/OsmFeature"
import { Lists } from "../../../Utils/Lists"
("use strict") ("use strict")
@ -199,7 +200,7 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource {
*/ */
private GetFilter(interpreterUrl: string, layersToDownload: LayerConfig[]): Overpass { private GetFilter(interpreterUrl: string, layersToDownload: LayerConfig[]): Overpass {
let filters: TagsFilter[] = layersToDownload.map((layer) => layer.source.osmTags) let filters: TagsFilter[] = layersToDownload.map((layer) => layer.source.osmTags)
filters = Utils.NoNull(filters) filters = Lists.noNull(filters)
if (filters.length === 0) { if (filters.length === 0) {
return undefined return undefined
} }

View file

@ -5,6 +5,7 @@ import { Utils } from "../../../Utils"
import { Feature, MultiLineString, Position } from "geojson" import { Feature, MultiLineString, Position } from "geojson"
import { GeoOperations } from "../../GeoOperations" import { GeoOperations } from "../../GeoOperations"
import { UpdatableDynamicTileSource } from "./DynamicTileSource" import { UpdatableDynamicTileSource } from "./DynamicTileSource"
import { Lists } from "../../../Utils/Lists"
/** /**
* The PolygonSourceMerger receives various small pieces of bigger polygons and stitches them together. * The PolygonSourceMerger receives various small pieces of bigger polygons and stitches them together.
@ -32,7 +33,7 @@ export class LineSourceMerger extends UpdatableDynamicTileSource<
} }
protected addDataFromSources(sources: FeatureSourceForTile[]) { protected addDataFromSources(sources: FeatureSourceForTile[]) {
sources = Utils.NoNull(sources) sources = Lists.noNull(sources)
const all: Map<string, Feature<MultiLineString>> = new Map() const all: Map<string, Feature<MultiLineString>> = new Map()
const currentZoom = this._zoomlevel?.data ?? 0 const currentZoom = this._zoomlevel?.data ?? 0
for (const source of sources) { for (const source of sources) {

View file

@ -1,10 +1,10 @@
import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource" import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource"
import { Store } from "../../UIEventSource" import { Store } from "../../UIEventSource"
import { BBox } from "../../BBox" import { BBox } from "../../BBox"
import { Utils } from "../../../Utils"
import { Feature } from "geojson" import { Feature } from "geojson"
import { GeoOperations } from "../../GeoOperations" import { GeoOperations } from "../../GeoOperations"
import { UpdatableDynamicTileSource } from "./DynamicTileSource" import { UpdatableDynamicTileSource } from "./DynamicTileSource"
import { Lists } from "../../../Utils/Lists"
/** /**
* The PolygonSourceMerger receives various small pieces of bigger polygons and stitches them together. * The PolygonSourceMerger receives various small pieces of bigger polygons and stitches them together.
@ -29,7 +29,7 @@ export class PolygonSourceMerger extends UpdatableDynamicTileSource<
} }
protected addDataFromSources(sources: FeatureSourceForTile[]) { protected addDataFromSources(sources: FeatureSourceForTile[]) {
sources = Utils.NoNull(sources) sources = Lists.noNull(sources)
const all: Map<string, Feature> = new Map() const all: Map<string, Feature> = new Map()
const zooms: Map<string, number> = new Map() const zooms: Map<string, number> = new Map()

View file

@ -14,6 +14,7 @@ import {
} from "geojson" } from "geojson"
import { Tiles } from "../Models/TileRange" import { Tiles } from "../Models/TileRange"
import { Utils } from "../Utils" import { Utils } from "../Utils"
import { Lists } from "../Utils/Lists"
("use strict") ("use strict")
@ -597,7 +598,7 @@ export class GeoOperations {
newFeatures.push(intersectionPart) newFeatures.push(intersectionPart)
} }
} }
return Utils.NoNull(newFeatures) return Lists.noNull(newFeatures)
} }
public static toGpx( public static toGpx(
@ -610,7 +611,7 @@ export class GeoOperations {
if (title === undefined || title === "") { if (title === undefined || title === "") {
title = "Uploaded with MapComplete" title = "Uploaded with MapComplete"
} }
title = Utils.EncodeXmlValue(title) title = Utils.encodeXmlValue(title)
const trackPoints: string[] = [] const trackPoints: string[] = []
let locationsWithMeta: Feature<Point>[] let locationsWithMeta: Feature<Point>[]
if (Array.isArray(locations)) { if (Array.isArray(locations)) {
@ -664,7 +665,7 @@ export class GeoOperations {
if (title === undefined || title === "") { if (title === undefined || title === "") {
title = "Created with MapComplete" title = "Created with MapComplete"
} }
title = Utils.EncodeXmlValue(title) title = Utils.encodeXmlValue(title)
const trackPoints: string[] = [] const trackPoints: string[] = []
for (const l of locations) { for (const l of locations) {
let trkpt = ` <wpt lat="${l.geometry.coordinates[1]}" lon="${l.geometry.coordinates[0]}">` let trkpt = ` <wpt lat="${l.geometry.coordinates[1]}" lon="${l.geometry.coordinates[0]}">`

View file

@ -8,6 +8,7 @@ import { WikidataImageProvider } from "./WikidataImageProvider"
import Panoramax from "./Panoramax" import Panoramax from "./Panoramax"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { ServerSourceInfo } from "../../Models/SourceOverview" import { ServerSourceInfo } from "../../Models/SourceOverview"
import { Lists } from "../../Utils/Lists"
/** /**
* A generic 'from the interwebz' image picker, without attribution * A generic 'from the interwebz' image picker, without attribution
@ -101,9 +102,7 @@ export default class AllImageProviders {
Mapillary.singleton, Mapillary.singleton,
AllImageProviders.genericImageProvider, AllImageProviders.genericImageProvider,
] ]
const allPrefixes = Utils.Dedup( const allPrefixes = Lists.dedup(prefixes ?? [].concat(...sources.map((s) => s.defaultKeyPrefixes)))
prefixes ?? [].concat(...sources.map((s) => s.defaultKeyPrefixes))
)
for (const prefix of allPrefixes) { for (const prefix of allPrefixes) {
for (const k in tags) { for (const k in tags) {
const v = tags[k] const v = tags[k]
@ -149,7 +148,7 @@ export default class AllImageProviders {
allSources.push(singleSource) allSources.push(singleSource)
} }
const source = Stores.concat(allSources).map((result) => { const source = Stores.concat(allSources).map((result) => {
const all = Utils.concat(result) const all = result.flatMap(x => x)
return Utils.DedupOnId(all, (i) => [i?.id, i?.url, i?.alt_id]) return Utils.DedupOnId(all, (i) => [i?.id, i?.url, i?.alt_id])
}) })
this._cachedImageStores[cachekey] = source this._cachedImageStores[cachekey] = source

View file

@ -4,6 +4,7 @@ import { Utils } from "../../Utils"
import { Feature, Point } from "geojson" import { Feature, Point } from "geojson"
import { ServerSourceInfo } from "../../Models/SourceOverview" import { ServerSourceInfo } from "../../Models/SourceOverview"
import { ComponentType } from "svelte/types/runtime/internal/dev" import { ComponentType } from "svelte/types/runtime/internal/dev"
import { Lists } from "../../Utils/Lists"
export interface ProvidedImage { export interface ProvidedImage {
url: string url: string
@ -92,7 +93,7 @@ export default abstract class ImageProvider {
) { ) {
continue continue
} }
const values = Utils.NoEmpty(tags[key]?.split(";")?.map((v) => v.trim()) ?? []) const values = Lists.noEmpty(tags[key]?.split(";")?.map((v) => v.trim()) ?? [])
for (const value of values) { for (const value of values) {
if (seenValues.has(value)) { if (seenValues.has(value)) {
continue continue

View file

@ -15,6 +15,7 @@ import NoteCommentElement from "../../UI/Popup/Notes/NoteCommentElement"
import OsmObjectDownloader from "../Osm/OsmObjectDownloader" import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
import ExifReader from "exifreader" import ExifReader from "exifreader"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { Lists } from "../../Utils/Lists"
/** /**
* The ImageUploadManager has a * The ImageUploadManager has a
@ -176,7 +177,7 @@ export class ImageUploadManager {
const failed: Set<ImageUploadArguments> = new Set() const failed: Set<ImageUploadArguments> = new Set()
this.uploadingAll = true this.uploadingAll = true
do { do {
queue = Utils.NoNull(this._queue.imagesInQueue.data ?? []).filter( queue = Lists.noNull(this._queue.imagesInQueue.data ?? []).filter(
(item) => !failed.has(item) (item) => !failed.has(item)
) )

View file

@ -7,6 +7,7 @@ import { Feature, Point } from "geojson"
import { Store, UIEventSource } from "../UIEventSource" import { Store, UIEventSource } from "../UIEventSource"
import { ServerSourceInfo } from "../../Models/SourceOverview" import { ServerSourceInfo } from "../../Models/SourceOverview"
import { ComponentType } from "svelte/types/runtime/internal/dev" import { ComponentType } from "svelte/types/runtime/internal/dev"
import { Lists } from "../../Utils/Lists"
export class Mapillary extends ImageProvider { export class Mapillary extends ImageProvider {
public static readonly singleton = new Mapillary() public static readonly singleton = new Mapillary()
@ -74,11 +75,9 @@ export class Mapillary extends ImageProvider {
pKey, pKey,
} }
const baselink = `https://www.mapillary.com/app/?` const baselink = `https://www.mapillary.com/app/?`
const paramsStr = Utils.NoNull( const paramsStr = Lists.noNull(Object.keys(params).map((k) =>
Object.keys(params).map((k) => params[k] === undefined ? undefined : k + "=" + params[k]
params[k] === undefined ? undefined : k + "=" + params[k] ))
)
)
return baselink + paramsStr.join("&") return baselink + paramsStr.join("&")
} }

View file

@ -6,6 +6,7 @@ import { Utils } from "../../Utils"
import { Feature, Point } from "geojson" import { Feature, Point } from "geojson"
import { ServerSourceInfo } from "../../Models/SourceOverview" import { ServerSourceInfo } from "../../Models/SourceOverview"
import { ComponentType } from "svelte/types/runtime/internal/dev" import { ComponentType } from "svelte/types/runtime/internal/dev"
import { Lists } from "../../Utils/Lists"
export class WikidataImageProvider extends ImageProvider { export class WikidataImageProvider extends ImageProvider {
public static readonly singleton = new WikidataImageProvider() public static readonly singleton = new WikidataImageProvider()
@ -13,7 +14,7 @@ export class WikidataImageProvider extends ImageProvider {
public readonly name = "Wikidata" public readonly name = "Wikidata"
private static readonly keyBlacklist: ReadonlySet<string> = new Set([ private static readonly keyBlacklist: ReadonlySet<string> = new Set([
"mapillary", "mapillary",
...Utils.Times((i) => "mapillary:" + i, 10), ...Utils.times((i) => "mapillary:" + i, 10),
]) ])
private constructor() { private constructor() {
@ -60,7 +61,7 @@ export class WikidataImageProvider extends ImageProvider {
const promises = WikimediaImageProvider.singleton.ExtractUrls(undefined, commons) const promises = WikimediaImageProvider.singleton.ExtractUrls(undefined, commons)
allImages.push(promises) allImages.push(promises)
} }
const resolved = await Promise.all(Utils.NoNull(allImages)) const resolved = await Promise.all(Lists.noNull(allImages))
const flattened = resolved.flatMap((x) => x) const flattened = resolved.flatMap((x) => x)
if (flattened.length === 1) { if (flattened.length === 1) {
flattened[0].originalAttribute = { key, value } flattened[0].originalAttribute = { key, value }

View file

@ -7,8 +7,8 @@ import { TagsFilter } from "../../Tags/TagsFilter"
import { And } from "../../Tags/And" import { And } from "../../Tags/And"
import { Tag } from "../../Tags/Tag" import { Tag } from "../../Tags/Tag"
import { OsmId } from "../../../Models/OsmFeature" import { OsmId } from "../../../Models/OsmFeature"
import { Utils } from "../../../Utils"
import OsmObjectDownloader from "../OsmObjectDownloader" import OsmObjectDownloader from "../OsmObjectDownloader"
import { Lists } from "../../../Utils/Lists"
export default class DeleteAction extends OsmChangeAction { export default class DeleteAction extends OsmChangeAction {
private readonly _softDeletionTags: TagsFilter private readonly _softDeletionTags: TagsFilter
@ -50,12 +50,12 @@ export default class DeleteAction extends OsmChangeAction {
this._softDeletionTags = softDeletionTags this._softDeletionTags = softDeletionTags
} else { } else {
this._softDeletionTags = new And( this._softDeletionTags = new And(
Utils.NoNull([ Lists.noNull([
softDeletionTags, softDeletionTags,
new Tag( new Tag(
"fixme", "fixme",
`A mapcomplete user marked this feature to be deleted (${meta.specialMotivation})` `A mapcomplete user marked this feature to be deleted (${meta.specialMotivation})`
), ),
]) ])
) )
} }

View file

@ -13,6 +13,7 @@ import { Utils } from "../../../Utils"
import { OsmConnection } from "../OsmConnection" import { OsmConnection } from "../OsmConnection"
import { Feature, Geometry, LineString, Point } from "geojson" import { Feature, Geometry, LineString, Point } from "geojson"
import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
import { Lists } from "../../../Utils/Lists"
export default class ReplaceGeometryAction extends OsmChangeAction implements PreviewableAction { export default class ReplaceGeometryAction extends OsmChangeAction implements PreviewableAction {
/** /**
@ -164,7 +165,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr
preview.push(feature) preview.push(feature)
}) })
return StaticFeatureSource.fromGeojson(Utils.NoNull(preview)) return StaticFeatureSource.fromGeojson(Lists.noNull(preview))
} }
/** /**
@ -317,7 +318,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr
candidate = undefined candidate = undefined
moveDistance = Infinity moveDistance = Infinity
distances.forEach((distances, nodeId) => { distances.forEach((distances, nodeId) => {
const minDist = Math.min(...Utils.NoNull(distances)) const minDist = Math.min(...(Lists.noNull(distances)))
if (moveDistance > minDist) { if (moveDistance > minDist) {
// We have found a candidate to move // We have found a candidate to move
candidate = nodeId candidate = nodeId

View file

@ -18,6 +18,7 @@ import DeleteAction from "./Actions/DeleteAction"
import MarkdownUtils from "../../Utils/MarkdownUtils" import MarkdownUtils from "../../Utils/MarkdownUtils"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import { Feature, Point } from "geojson" import { Feature, Point } from "geojson"
import { Lists } from "../../Utils/Lists"
/** /**
* Handles all changes made to OSM. * Handles all changes made to OSM.
@ -260,7 +261,7 @@ export class Changes {
} }
public static GetNeededIds(changes: ChangeDescription[]) { public static GetNeededIds(changes: ChangeDescription[]) {
return Utils.Dedup(changes.filter((c) => c.id >= 0).map((c) => c.type + "/" + c.id)) return Lists.dedup(changes.filter((c) => c.id >= 0).map((c) => c.type + "/" + c.id))
} }
/** /**
@ -467,8 +468,8 @@ export class Changes {
if (change.changes !== undefined) { if (change.changes !== undefined) {
switch (change.type) { switch (change.type) {
case "node": { case "node": {
const nlat = Utils.Round7(change.changes.lat) const nlat = Utils.round7(change.changes.lat)
const nlon = Utils.Round7(change.changes.lon) const nlon = Utils.round7(change.changes.lon)
const n = <OsmNode>obj const n = <OsmNode>obj
if (n.lat !== nlat || n.lon !== nlon) { if (n.lat !== nlat || n.lon !== nlon) {
n.lat = nlat n.lat = nlat
@ -717,11 +718,9 @@ export class Changes {
* We _do not_ pass in the Changes object itself - we want the data from OSM directly in order to apply the changes * We _do not_ pass in the Changes object itself - we want the data from OSM directly in order to apply the changes
*/ */
const downloader = new OsmObjectDownloader(this.backend, undefined) const downloader = new OsmObjectDownloader(this.backend, undefined)
const osmObjects = Utils.NoNull( const osmObjects = Lists.noNull(await Promise.all<{ id: string; osmObj: OsmObject | "deleted" }>(
await Promise.all<{ id: string; osmObj: OsmObject | "deleted" }>( neededIds.map((id) => this.getOsmObject(id, downloader))
neededIds.map((id) => this.getOsmObject(id, downloader)) ))
)
)
// Drop changes to deleted items // Drop changes to deleted items
for (const { osmObj, id } of osmObjects) { for (const { osmObj, id } of osmObjects) {
@ -801,7 +800,7 @@ export class Changes {
value: descr.meta.specialMotivation, value: descr.meta.specialMotivation,
})) }))
const distances = Utils.NoNull(pending.map((descr) => descr.meta.distanceToObject)) const distances = Lists.noNull(pending.map((descr) => descr.meta.distanceToObject))
distances.sort((a, b) => a - b) distances.sort((a, b) => a - b)
const perBinCount = Constants.distanceToChangeObjectBins.map(() => 0) const perBinCount = Constants.distanceToChangeObjectBins.map(() => 0)
@ -816,23 +815,21 @@ export class Changes {
} }
} }
const perBinMessage = Utils.NoNull( const perBinMessage = Lists.noNull(perBinCount.map((count, i) => {
perBinCount.map((count, i) => { if (count === 0) {
if (count === 0) { return undefined
return undefined }
} const maxD = maxDistances[i]
const maxD = maxDistances[i] let key = `change_within_${maxD}m`
let key = `change_within_${maxD}m` if (maxD === Number.MAX_VALUE) {
if (maxD === Number.MAX_VALUE) { key = `change_over_${maxDistances[i - 1]}m`
key = `change_over_${maxDistances[i - 1]}m` }
} return {
return { key,
key, value: count,
value: count, aggregate: true,
aggregate: true, }
} }))
})
)
// This method is only called with changedescriptions for this theme // This method is only called with changedescriptions for this theme
const theme = pending[0].meta.theme const theme = pending[0].meta.theme

View file

@ -8,6 +8,7 @@ import { Utils } from "../../Utils"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import { AndroidPolyfill } from "../Web/AndroidPolyfill" import { AndroidPolyfill } from "../Web/AndroidPolyfill"
import ImageUploadQueue from "../ImageProviders/ImageUploadQueue" import ImageUploadQueue from "../ImageProviders/ImageUploadQueue"
import { Lists } from "../../Utils/Lists"
export interface ChangesetTag { export interface ChangesetTag {
key: string key: string
@ -393,7 +394,7 @@ export class ChangesetHandler {
private async UpdateTags(csId: number, tags: ChangesetTag[]) { private async UpdateTags(csId: number, tags: ChangesetTag[]) {
tags = ChangesetHandler.removeDuplicateMetaTags(tags) tags = ChangesetHandler.removeDuplicateMetaTags(tags)
tags = Utils.NoNull(tags).filter( tags = Lists.noNull(tags).filter(
(tag) => (tag) =>
tag.key !== undefined && tag.key !== undefined &&
tag.value !== undefined && tag.value !== undefined &&

View file

@ -150,7 +150,7 @@ export abstract class OsmObject {
} }
const v = this.tags[key] const v = this.tags[key]
if (v !== "" && v !== undefined) { if (v !== "" && v !== undefined) {
tags += ` <tag k="${Utils.EncodeXmlValue(key)}" v="${Utils.EncodeXmlValue( tags += ` <tag k="${Utils.encodeXmlValue(key)}" v="${Utils.encodeXmlValue(
this.tags[key] this.tags[key]
)}"/> )}"/>
` `

View file

@ -2,6 +2,7 @@ import { Store, UIEventSource } from "../UIEventSource"
import { OsmConnection } from "./OsmConnection" import { OsmConnection } from "./OsmConnection"
import { LocalStorageSource } from "../Web/LocalStorageSource" import { LocalStorageSource } from "../Web/LocalStorageSource"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { Lists } from "../../Utils/Lists"
import OSMAuthInstance = OSMAuth.osmAuth import OSMAuthInstance = OSMAuth.osmAuth
export class OsmPreferences { export class OsmPreferences {
@ -270,7 +271,7 @@ export class OsmPreferences {
return return
} }
// _All_ keys are deleted first, to avoid pending parts // _All_ keys are deleted first, to avoid pending parts
const keysToDelete = Utils.Dedup(OsmPreferences.keysStartingWith(this.seenKeys, k)) const keysToDelete = Lists.dedup(OsmPreferences.keysStartingWith(this.seenKeys, k))
if (v === null || v === undefined || v === "" || v === "undefined" || v === "null") { if (v === null || v === undefined || v === "" || v === "undefined" || v === "null") {
for (const k of keysToDelete) { for (const k of keysToDelete) {
await this.deleteKeyDirectly(k) await this.deleteKeyDirectly(k)

View file

@ -13,7 +13,7 @@ export default class CombinedSearcher implements GeocodingProvider {
* @param providers * @param providers
*/ */
constructor(...providers: ReadonlyArray<GeocodingProvider>) { constructor(...providers: ReadonlyArray<GeocodingProvider>) {
this._providers = Utils.NoNull(providers) this._providers = Utils.noNull(providers)
this._providersWithSuggest = this._providers.filter((pr) => pr.suggest !== undefined) this._providersWithSuggest = this._providers.filter((pr) => pr.suggest !== undefined)
} }

View file

@ -1,7 +1,7 @@
import GeocodingProvider, { GeocodeResult } from "./GeocodingProvider" import GeocodingProvider, { GeocodeResult } from "./GeocodingProvider"
import { Utils } from "../../Utils"
import { ImmutableStore, Store } from "../UIEventSource" import { ImmutableStore, Store } from "../UIEventSource"
import CoordinateParser from "coordinate-parser" import CoordinateParser from "coordinate-parser"
import { Lists } from "../../Utils/Lists"
/** /**
* A simple search-class which interprets possible locations * A simple search-class which interprets possible locations
@ -71,13 +71,11 @@ export default class CoordinateSearch implements GeocodingProvider {
* results[0] // => {lat: 51.047977, lon: 3.51184, "display_name": "lon: 3.51184, lat: 51.047977", "category": "coordinate","osm_id": "3.51184/51.047977", "source": "coordinate:latlon"} * results[0] // => {lat: 51.047977, lon: 3.51184, "display_name": "lon: 3.51184, lat: 51.047977", "category": "coordinate","osm_id": "3.51184/51.047977", "source": "coordinate:latlon"}
*/ */
private directSearch(query: string): GeocodeResult[] { private directSearch(query: string): GeocodeResult[] {
const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map((r) => query.match(r))).map( const matches = Lists.noNull(CoordinateSearch.latLonRegexes.map((r) => query.match(r))).map(
(m) => CoordinateSearch.asResult(m[2], m[1], "latlon") (m) => CoordinateSearch.asResult(m[2], m[1], "latlon")
) )
const matchesLonLat = Utils.NoNull( const matchesLonLat = Lists.noNull(CoordinateSearch.lonLatRegexes.map((r) => query.match(r))).map((m) => CoordinateSearch.asResult(m[1], m[2], "lonlat"))
CoordinateSearch.lonLatRegexes.map((r) => query.match(r))
).map((m) => CoordinateSearch.asResult(m[1], m[2], "lonlat"))
const init = matches.concat(matchesLonLat) const init = matches.concat(matchesLonLat)
if (init.length > 0) { if (init.length > 0) {
return init return init

View file

@ -4,6 +4,7 @@ import FilterConfig, { FilterConfigOption } from "../../Models/ThemeConfig/Filte
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import LayerState from "../State/LayerState" import LayerState from "../State/LayerState"
import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig" import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
import { Lists } from "../../Utils/Lists"
export type FilterSearchResult = { export type FilterSearchResult = {
option: FilterConfigOption option: FilterConfigOption
@ -64,7 +65,7 @@ export default class FilterSearch {
].flatMap((term) => [term, ...(term?.split(" ") ?? [])]) ].flatMap((term) => [term, ...(term?.split(" ") ?? [])])
terms = terms.map((t) => Utils.simplifyStringForSearch(t)) terms = terms.map((t) => Utils.simplifyStringForSearch(t))
terms.push(option.emoji) terms.push(option.emoji)
Utils.NoNullInplace(terms) Lists.noNullInplace(terms)
const distances = queries.flatMap((query) => const distances = queries.flatMap((query) =>
terms.map((entry) => { terms.map((entry) => {
const d = Utils.levenshteinDistance(query, entry.slice(0, query.length)) const d = Utils.levenshteinDistance(query, entry.slice(0, query.length))

View file

@ -5,6 +5,7 @@ import { Feature } from "geojson"
import { GeoOperations } from "../GeoOperations" import { GeoOperations } from "../GeoOperations"
import { ImmutableStore, Store, Stores } from "../UIEventSource" import { ImmutableStore, Store, Stores } from "../UIEventSource"
import OpenStreetMapIdSearch from "./OpenStreetMapIdSearch" import OpenStreetMapIdSearch from "./OpenStreetMapIdSearch"
import { Lists } from "../../Utils/Lists"
type IntermediateResult = { type IntermediateResult = {
feature: Feature feature: Feature
@ -41,13 +42,13 @@ export default class LocalElementSearch implements GeocodingProvider {
for (const feature of features) { for (const feature of features) {
const props = feature.properties const props = feature.properties
const searchTerms: string[] = Utils.NoNull([ const searchTerms: string[] = Lists.noNull([
props.name, props.name,
props.alt_name, props.alt_name,
props.local_name, props.local_name,
props["addr:street"] && props["addr:number"] props["addr:street"] && props["addr:number"]
? props["addr:street"] + props["addr:number"] ? props["addr:street"] + props["addr:number"]
: undefined, : undefined,
]) ])
let levehnsteinD: number let levehnsteinD: number

View file

@ -1,8 +1,8 @@
import { Store, UIEventSource } from "../UIEventSource" import { Store, UIEventSource } from "../UIEventSource"
import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider" import GeocodingProvider, { GeocodeResult, GeocodingOptions } from "./GeocodingProvider"
import { OsmId } from "../../Models/OsmFeature" import { OsmId } from "../../Models/OsmFeature"
import { Utils } from "../../Utils"
import OsmObjectDownloader from "../Osm/OsmObjectDownloader" import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
import { Lists } from "../../Utils/Lists"
export default class OpenStreetMapIdSearch implements GeocodingProvider { export default class OpenStreetMapIdSearch implements GeocodingProvider {
private static readonly regex = private static readonly regex =
@ -76,7 +76,7 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
async search(query: string, _: GeocodingOptions): Promise<GeocodeResult[]> { async search(query: string, _: GeocodingOptions): Promise<GeocodeResult[]> {
if (!isNaN(Number(query))) { if (!isNaN(Number(query))) {
const n = Number(query) const n = Number(query)
return Utils.NoNullInplace( return Lists.noNullInplace(
await Promise.all([ await Promise.all([
this.getInfoAbout(`node/${n}`).catch(() => undefined), this.getInfoAbout(`node/${n}`).catch(() => undefined),
this.getInfoAbout(`way/${n}`).catch(() => undefined), this.getInfoAbout(`way/${n}`).catch(() => undefined),

View file

@ -1,6 +1,7 @@
import Locale from "../../UI/i18n/Locale" import Locale from "../../UI/i18n/Locale"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import ThemeSearch from "./ThemeSearch" import ThemeSearch from "./ThemeSearch"
import { Lists } from "../../Utils/Lists"
export default class SearchUtils { export default class SearchUtils {
/** Applies special search terms, such as 'studio', 'osmcha', ... /** Applies special search terms, such as 'studio', 'osmcha', ...
@ -66,7 +67,7 @@ export default class SearchUtils {
} else { } else {
terms = (keywords[language] ?? []).concat(keywords["*"]) terms = (keywords[language] ?? []).concat(keywords["*"])
} }
const termsAll = Utils.NoNullInplace(terms).flatMap((t) => t.split(" ")) const termsAll = Lists.noNullInplace(terms).flatMap((t) => t.split(" "))
let distanceSummed = 0 let distanceSummed = 0
for (let i = 0; i < queryParts.length; i++) { for (let i = 0; i < queryParts.length; i++) {

View file

@ -8,6 +8,7 @@ import Fuse, { IFuseOptions } from "fuse.js"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import Locale from "../../UI/i18n/Locale" import Locale from "../../UI/i18n/Locale"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { Lists } from "../../Utils/Lists"
export class ThemeSearchIndex { export class ThemeSearchIndex {
private readonly themeIndex: Fuse<MinimalThemeInformation> private readonly themeIndex: Fuse<MinimalThemeInformation>
@ -18,7 +19,7 @@ export class ThemeSearchIndex {
themesToSearch?: MinimalThemeInformation[], themesToSearch?: MinimalThemeInformation[],
layersToIgnore: string[] = [] layersToIgnore: string[] = []
) { ) {
const themes = Utils.NoNull(themesToSearch ?? ThemeSearch.officialThemes?.themes) const themes = Utils.noNull(themesToSearch ?? ThemeSearch.officialThemes?.themes)
if (!themes) { if (!themes) {
throw "No themes loaded. Did generate:layeroverview fail?" throw "No themes loaded. Did generate:layeroverview fail?"
} }
@ -100,7 +101,7 @@ export class ThemeSearchIndex {
const knownHidden: Store<string[]> = Stores.listStabilized( const knownHidden: Store<string[]> = Stores.listStabilized(
UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection) UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection)
.stabilized(1000) .stabilized(1000)
.map((list) => Utils.Dedup(list)) .map((list) => Lists.dedup(list))
) )
const otherThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter( const otherThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(
(th) => th.id !== state.theme.id (th) => th.id !== state.theme.id

View file

@ -18,8 +18,8 @@ import { Feature } from "geojson"
import OpenLocationCodeSearch from "../Search/OpenLocationCodeSearch" import OpenLocationCodeSearch from "../Search/OpenLocationCodeSearch"
import { BBox } from "../BBox" import { BBox } from "../BBox"
import { QueryParameters } from "../Web/QueryParameters" import { QueryParameters } from "../Web/QueryParameters"
import { Utils } from "../../Utils"
import { NominatimGeocoding } from "../Search/NominatimGeocoding" import { NominatimGeocoding } from "../Search/NominatimGeocoding"
import { Lists } from "../../Utils/Lists"
export default class SearchState { export default class SearchState {
public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined) public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
@ -83,7 +83,7 @@ export default class SearchState {
} }
}))) })))
this.runningEngines = isRunningPerEngine.bindD( this.runningEngines = isRunningPerEngine.bindD(
listOfSources => Stores.concat(listOfSources).mapD(list => Utils.NoNull(list))) listOfSources => Stores.concat(listOfSources).mapD(list => Lists.noNull(list)))
this.failedEngines = suggestionsListWithSource this.failedEngines = suggestionsListWithSource
@ -102,7 +102,7 @@ export default class SearchState {
return [] return []
} }
}), }),
))).map(list => Utils.NoNull(list?.flatMap(x => x) ?? [])) ))).map(list => Lists.noNull(list?.flatMap(x => x) ?? []))
this.suggestionsSearchRunning = this.runningEngines.map(running => running?.length > 0) this.suggestionsSearchRunning = this.runningEngines.map(running => running?.length > 0)
this.suggestions = suggestionsList.bindD((suggestions) => this.suggestions = suggestionsList.bindD((suggestions) =>

View file

@ -20,6 +20,7 @@ import Showdown from "showdown"
import { LocalStorageSource } from "../Web/LocalStorageSource" import { LocalStorageSource } from "../Web/LocalStorageSource"
import { GeocodeResult } from "../Search/GeocodingProvider" import { GeocodeResult } from "../Search/GeocodingProvider"
import Translations from "../../UI/i18n/Translations" import Translations from "../../UI/i18n/Translations"
import { Lists } from "../../Utils/Lists"
class RoundRobinStore<T> { class RoundRobinStore<T> {
private readonly _store: UIEventSource<T[]> private readonly _store: UIEventSource<T[]>
@ -41,7 +42,7 @@ class RoundRobinStore<T> {
private set() { private set() {
const v = this._store.data const v = this._store.data
const i = this._index.data const i = this._index.data
const newList = Utils.NoNull(v.slice(i + 1, v.length).concat(v.slice(0, i + 1))) const newList = Lists.noNull(v.slice(i + 1, v.length).concat(v.slice(0, i + 1)))
if (newList.length === 0) { if (newList.length === 0) {
this._index.set(0) this._index.set(0)
} }
@ -89,7 +90,7 @@ export class OptionallySyncedHistory<T extends object | string> {
defaultValue: "sync", defaultValue: "sync",
}) })
this.syncedBackingStore = UIEventSource.concat(Utils.TimesT(maxHistory, (i) => { this.syncedBackingStore = UIEventSource.concat(Utils.timesT(maxHistory, (i) => {
const pref = osmconnection.getPreference(key + "-hist-" + i + "-") const pref = osmconnection.getPreference(key + "-hist-" + i + "-")
return UIEventSource.asObject<T>(pref, undefined) return UIEventSource.asObject<T>(pref, undefined)
})) }))
@ -555,27 +556,25 @@ export default class UserRelatedState {
const untranslated = missing.untranslated.get(language) ?? [] const untranslated = missing.untranslated.get(language) ?? []
const hasMissingTheme = untranslated.some((k) => k.startsWith("themes:")) const hasMissingTheme = untranslated.some((k) => k.startsWith("themes:"))
const missingLayers = Utils.Dedup( const missingLayers = Lists.dedup(untranslated
untranslated .filter((k) => k.startsWith("layers:"))
.filter((k) => k.startsWith("layers:")) .map((k) => k.slice("layers:".length).split(".")[0]))
.map((k) => k.slice("layers:".length).split(".")[0])
)
const zenLinks: { link: string; id: string }[] = Utils.NoNull([ const zenLinks: { link: string; id: string }[] = Lists.noNull([
hasMissingTheme hasMissingTheme
? { ? {
id: "theme:" + layout.id, id: "theme:" + layout.id,
link: Translations.hrefToWeblateZen( link: Translations.hrefToWeblateZen(
language, language,
"themes", "themes",
layout.id layout.id
), ),
} }
: undefined, : undefined,
...missingLayers.map((id) => ({ ...missingLayers.map((id) => ({
id: "layer:" + id, id: "layer:" + id,
link: Translations.hrefToWeblateZen(language, "layers", id), link: Translations.hrefToWeblateZen(language, "layers", id),
})), })),
]) ])
const untranslated_count = untranslated.length const untranslated_count = untranslated.length
amendedPrefs.data["_translation_total"] = "" + total amendedPrefs.data["_translation_total"] = "" + total

View file

@ -11,6 +11,7 @@ import key_counts from "../../assets/key_totals.json"
import { ConversionContext } from "../../Models/ThemeConfig/Conversion/ConversionContext" import { ConversionContext } from "../../Models/ThemeConfig/Conversion/ConversionContext"
import { FlatTag, TagsFilterClosed, UploadableTag } from "./TagTypes" import { FlatTag, TagsFilterClosed, UploadableTag } from "./TagTypes"
import { Lists } from "../../Utils/Lists"
type Tags = Record<string, string> type Tags = Record<string, string>
@ -383,7 +384,7 @@ export class TagUtils {
const keyValues = TagUtils.SplitKeys(tagsFilters) const keyValues = TagUtils.SplitKeys(tagsFilters)
const and: UploadableTag[] = [] const and: UploadableTag[] = []
for (const key in keyValues) { for (const key in keyValues) {
const values = Utils.Dedup(keyValues[key]).filter((v) => v !== "") const values = Lists.dedup(keyValues[key]).filter((v) => v !== "")
values.sort() values.sort()
and.push(new Tag(key, values.join(";"))) and.push(new Tag(key, values.join(";")))
} }
@ -742,7 +743,7 @@ export class TagUtils {
if (level === undefined || level === null) { if (level === undefined || level === null) {
return [] return []
} }
let spec = Utils.NoNull([level]) let spec = Lists.noNull([level])
spec = [].concat(...spec.map((s) => s?.split(";"))) spec = [].concat(...spec.map((s) => s?.split(";")))
spec = [].concat( spec = [].concat(
...spec.map((s) => { ...spec.map((s) => {
@ -764,7 +765,7 @@ export class TagUtils {
return values return values
}) })
) )
return Utils.NoNull(spec) return Lists.noNull(spec)
} }
private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilterClosed { private static ParseTagUnsafe(json: TagConfigJson, context: string = ""): TagsFilterClosed {
@ -919,10 +920,10 @@ export class TagUtils {
public static GetPopularity(tag: TagsFilter): number | undefined { public static GetPopularity(tag: TagsFilter): number | undefined {
if (tag instanceof And) { if (tag instanceof And) {
return Math.min(...Utils.NoNull(tag.and.map((t) => TagUtils.GetPopularity(t)))) - 1 return Math.min(...(Lists.noNull(tag.and.map((t) => TagUtils.GetPopularity(t))))) - 1
} }
if (tag instanceof Or) { if (tag instanceof Or) {
return Math.max(...Utils.NoNull(tag.or.map((t) => TagUtils.GetPopularity(t)))) + 1 return Math.max(...(Lists.noNull(tag.or.map((t) => TagUtils.GetPopularity(t))))) + 1
} }
if (tag instanceof Tag) { if (tag instanceof Tag) {
return TagUtils.GetCount(tag.key, tag.value) return TagUtils.GetCount(tag.key, tag.value)

View file

@ -9,6 +9,7 @@ import { RegexTag } from "../Tags/RegexTag"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import { TagUtils } from "../Tags/TagUtils" import { TagUtils } from "../Tags/TagUtils"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import { Lists } from "../../Utils/Lists"
/** /**
* Main name suggestion index file * Main name suggestion index file
@ -103,7 +104,7 @@ export default class NameSuggestionIndex {
} }
}) })
) )
stats = Utils.NoNull(stats) stats = Lists.noNull(stats)
if (stats.length === 1) { if (stats.length === 1) {
return stats[0] return stats[0]
} }
@ -282,7 +283,7 @@ export default class NameSuggestionIndex {
} }
const keys = Object.keys(this.nsiFile.nsi) const keys = Object.keys(this.nsiFile.nsi)
const all = keys.map((k) => this.nsiFile.nsi[k].properties.path.split("/")[0]) const all = keys.map((k) => this.nsiFile.nsi[k].properties.path.split("/")[0])
this._supportedTypes = Utils.Dedup(all).map((s) => { this._supportedTypes = Lists.dedup(all).map((s) => {
if (s.endsWith("s")) { if (s.endsWith("s")) {
s = s.substring(0, s.length - 1) s = s.substring(0, s.length - 1)
} }

View file

@ -2,6 +2,7 @@ import { Utils } from "../../Utils"
import { Store, UIEventSource } from "../UIEventSource" import { Store, UIEventSource } from "../UIEventSource"
import { SimplifiedClaims, WBK } from "wikibase-sdk" import { SimplifiedClaims, WBK } from "wikibase-sdk"
import { ServerSourceInfo } from "../../Models/SourceOverview" import { ServerSourceInfo } from "../../Models/SourceOverview"
import { Lists } from "../../Utils/Lists"
export class WikidataResponse { export class WikidataResponse {
public readonly id: string public readonly id: string
@ -294,7 +295,7 @@ export default class Wikidata {
} }
}) })
) )
return Utils.NoNull(maybeResponses.map((r) => <WikidataResponse>r["success"])) return Lists.noNull(maybeResponses.map((r) => <WikidataResponse>r["success"]))
} }
/** /**

View file

@ -9,6 +9,7 @@ import { Utils } from "../Utils"
import { TagUtils } from "../Logic/Tags/TagUtils" import { TagUtils } from "../Logic/Tags/TagUtils"
import { And } from "../Logic/Tags/And" import { And } from "../Logic/Tags/And"
import { GlobalFilter } from "./GlobalFilter" import { GlobalFilter } from "./GlobalFilter"
import { Lists } from "../Utils/Lists"
export default class FilteredLayer { export default class FilteredLayer {
/** /**
@ -287,7 +288,7 @@ export default class FilteredLayer {
} }
needed.push(filter.options[state.data].osmTags) needed.push(filter.options[state.data].osmTags)
} }
needed = Utils.NoNull(needed) needed = Lists.noNull(needed)
if (needed.length == 0) { if (needed.length == 0) {
return undefined return undefined
} }

View file

@ -11,9 +11,10 @@ import { ThemeConfigJson } from "../../src/Models/ThemeConfig/Json/ThemeConfigJs
import SpecialVisualizations from "../../src/UI/SpecialVisualizations" import SpecialVisualizations from "../../src/UI/SpecialVisualizations"
import ValidationUtils from "../../src/Models/ThemeConfig/Conversion/ValidationUtils" import ValidationUtils from "../../src/Models/ThemeConfig/Conversion/ValidationUtils"
import { import {
QuestionableTagRenderingConfigJson QuestionableTagRenderingConfigJson,
} from "../../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" } from "../../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import { LayerConfigJson } from "../../src/Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../../src/Models/ThemeConfig/Json/LayerConfigJson"
import { Lists } from "../Utils/Lists"
export interface ServerSourceInfo { export interface ServerSourceInfo {
url: string url: string
@ -81,7 +82,7 @@ export class SourceOverview {
const geojsonSources: string[] = layout?.layers?.map((l) => l.source?.geojsonSource) ?? [] const geojsonSources: string[] = layout?.layers?.map((l) => l.source?.geojsonSource) ?? []
return Utils.NoNull(apiUrls.concat(...geojsonSources)).filter((item) => { return Lists.noNull(apiUrls.concat(...geojsonSources)).filter((item) => {
if (typeof item === "string") { if (typeof item === "string") {
return true return true
} }
@ -117,7 +118,7 @@ export class SourceOverview {
"Background layer source or supporting sources for " + f.properties.id, "Background layer source or supporting sources for " + f.properties.id,
trigger: ["specific_feature"], trigger: ["specific_feature"],
category: "maplayer", category: "maplayer",
moreInfo: Utils.NoEmpty([ moreInfo: Lists.noEmpty([
"https://github.com/osmlab/editor-layer-index", "https://github.com/osmlab/editor-layer-index",
f.properties?.attribution?.url, f.properties?.attribution?.url,
]), ]),

View file

@ -7,6 +7,7 @@ import {
QuestionableTagRenderingConfigJson, QuestionableTagRenderingConfigJson,
} from "../Json/QuestionableTagRenderingConfigJson" } from "../Json/QuestionableTagRenderingConfigJson"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import { ConversionContext } from "./ConversionContext"
export default class AddPrefixToTagRenderingConfig extends DesugaringStep<QuestionableTagRenderingConfigJson> { export default class AddPrefixToTagRenderingConfig extends DesugaringStep<QuestionableTagRenderingConfigJson> {
private readonly _prefix: string private readonly _prefix: string
@ -142,7 +143,8 @@ export default class AddPrefixToTagRenderingConfig extends DesugaringStep<Questi
} }
public convert( public convert(
json: Readonly<QuestionableTagRenderingConfigJson> json: Readonly<QuestionableTagRenderingConfigJson>,
context: ConversionContext
): QuestionableTagRenderingConfigJson { ): QuestionableTagRenderingConfigJson {
let freeform = json.freeform let freeform = json.freeform
if (freeform) { if (freeform) {

View file

@ -1,7 +1,7 @@
import { LayerConfigJson } from "../Json/LayerConfigJson" import { LayerConfigJson } from "../Json/LayerConfigJson"
import { Utils } from "../../../Utils"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
import { ConversionContext } from "./ConversionContext" import { ConversionContext } from "./ConversionContext"
import { Lists } from "../../../Utils/Lists"
export interface DesugaringContext { export interface DesugaringContext {
tagRenderings: Map<string, QuestionableTagRenderingConfigJson> tagRenderings: Map<string, QuestionableTagRenderingConfigJson>
@ -219,7 +219,7 @@ export class Concat<X, T> extends Conversion<X[], T[]> {
return <undefined | null>values return <undefined | null>values
} }
const vals: T[][] = new Each(this._step).convert(values, context.inOperation("concat")) const vals: T[][] = new Each(this._step).convert(values, context.inOperation("concat"))
return [].concat(...vals) return vals.flatMap(l => l)
} }
} }
@ -264,7 +264,6 @@ export class Cached<TIn, TOut> extends Conversion<TIn, TOut> {
} }
export class Fuse<T> extends DesugaringStep<T> { export class Fuse<T> extends DesugaringStep<T> {
protected debug = false
private readonly steps: DesugaringStep<T>[] private readonly steps: DesugaringStep<T>[]
constructor(doc: string, ...steps: DesugaringStep<T>[]) { constructor(doc: string, ...steps: DesugaringStep<T>[]) {
@ -274,18 +273,12 @@ export class Fuse<T> extends DesugaringStep<T> {
"This fused pipeline of the following steps: " + "This fused pipeline of the following steps: " +
steps.map((s) => s.name).join(", ") steps.map((s) => s.name).join(", ")
) )
this.steps = Utils.NoNull(steps) this.steps = Lists.noNull(steps)
} }
public enableDebugging(): Fuse<T> {
this.debug = true
return this
}
convert(json: T, context: ConversionContext): T { convert(json: T, context: ConversionContext): T {
const timings = []
for (let i = 0; i < this.steps.length; i++) { for (let i = 0; i < this.steps.length; i++) {
const start = new Date()
const step = this.steps[i] const step = this.steps[i]
try { try {
const r = step.convert(json, context.inOperation(step.name)) const r = step.convert(json, context.inOperation(step.name))
@ -297,14 +290,6 @@ export class Fuse<T> extends DesugaringStep<T> {
console.error("Step " + step.name + " failed due to ", e, e.stack) console.error("Step " + step.name + " failed due to ", e, e.stack)
throw e throw e
} }
if (this.debug) {
const stop = new Date()
const timeNeededMs = stop.getTime() - start.getTime()
timings.push(timeNeededMs)
}
}
if (this.debug) {
console.log("Time needed,", timings.join(", "))
} }
return json return json
} }

View file

@ -1,9 +1,9 @@
import { DesugaringStep } from "./Conversion" import { DesugaringStep } from "./Conversion"
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { ConversionContext } from "./ConversionContext" import { ConversionContext } from "./ConversionContext"
import { Utils } from "../../../Utils"
import Translations from "../../../UI/i18n/Translations" import Translations from "../../../UI/i18n/Translations"
import { DoesImageExist } from "./Validation" import { DoesImageExist } from "./Validation"
import { Lists } from "../../../Utils/Lists"
export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJson> { export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJson> {
private readonly _doesImageExist: DoesImageExist private readonly _doesImageExist: DoesImageExist
@ -45,7 +45,7 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
for (let i = 0; i < json.mappings.length; i++) { for (let i = 0; i < json.mappings.length; i++) {
const mapping = json.mappings[i] const mapping = json.mappings[i]
const ignore = mapping["#"]?.indexOf(ignoreToken) >= 0 const ignore = mapping["#"]?.indexOf(ignoreToken) >= 0
const images = Utils.Dedup(Translations.T(mapping.then)?.ExtractImages() ?? []) const images = Lists.dedup(Translations.T(mapping.then)?.ExtractImages() ?? [])
const ctx = context.enters("mappings", i) const ctx = context.enters("mappings", i)
if (images.length > 0) { if (images.length > 0) {
if (!ignore) { if (!ignore) {

View file

@ -14,6 +14,7 @@ import Translations from "../../../UI/i18n/Translations"
import { FlatTag, OptimizedTag, TagsFilterClosed } from "../../../Logic/Tags/TagTypes" import { FlatTag, OptimizedTag, TagsFilterClosed } from "../../../Logic/Tags/TagTypes"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter" import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import { Translation } from "../../../UI/i18n/Translation" import { Translation } from "../../../UI/i18n/Translation"
import { Lists } from "../../../Utils/Lists"
export class PruneFilters extends DesugaringStep<LayerConfigJson> { export class PruneFilters extends DesugaringStep<LayerConfigJson> {
constructor() { constructor() {
@ -107,9 +108,7 @@ export class PruneFilters extends DesugaringStep<LayerConfigJson> {
const sourceTags = TagUtils.Tag(json.source["osmTags"]) const sourceTags = TagUtils.Tag(json.source["osmTags"])
return { return {
...json, ...json,
filter: Utils.NoNull( filter: Lists.noNull(json.filter?.map((obj) => this.prune(sourceTags, <FilterConfigJson>obj, context))),
json.filter?.map((obj) => this.prune(sourceTags, <FilterConfigJson>obj, context))
),
} }
} }
} }

View file

@ -8,6 +8,7 @@ import { Utils } from "../../../Utils"
import { AddContextToTranslations } from "./AddContextToTranslations" import { AddContextToTranslations } from "./AddContextToTranslations"
import AddPrefixToTagRenderingConfig from "./AddPrefixToTagRenderingConfig" import AddPrefixToTagRenderingConfig from "./AddPrefixToTagRenderingConfig"
import { Translatable } from "../Json/Translatable" import { Translatable } from "../Json/Translatable"
import { Lists } from "../../../Utils/Lists"
export class ExpandTagRendering extends Conversion< export class ExpandTagRendering extends Conversion<
| string | string
@ -41,8 +42,7 @@ export class ExpandTagRendering extends Conversion<
) { ) {
super( super(
"ExpandTagRendering", "ExpandTagRendering",
"Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question and reusing the builtins", "Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question and reusing the builtins"
[]
) )
this._state = state this._state = state
this._self = self this._self = self
@ -387,7 +387,7 @@ export class ExpandTagRendering extends Conversion<
if (layer === undefined) { if (layer === undefined) {
const candidates = Utils.sortedByLevenshteinDistance( const candidates = Utils.sortedByLevenshteinDistance(
layerName, layerName,
Utils.NoNull(Array.from(state.sharedLayers.keys())) Lists.noNull(Array.from(state.sharedLayers.keys()))
) )
if (candidates.length === 0) { if (candidates.length === 0) {
ctx.err( ctx.err(
@ -413,7 +413,7 @@ export class ExpandTagRendering extends Conversion<
// We are dealing with a looping import, no error is necessary // We are dealing with a looping import, no error is necessary
continue continue
} }
candidates = Utils.NoNull(layer.tagRenderings.map((tr) => tr["id"])).map( candidates = Lists.noNull(layer.tagRenderings.map((tr) => tr["id"])).map(
(id) => layerName + "." + id (id) => layerName + "." + id
) )
} }

View file

@ -7,6 +7,7 @@ import Translations from "../../../UI/i18n/Translations"
import { parse as parse_html } from "node-html-parser" import { parse as parse_html } from "node-html-parser"
import { ConversionContext } from "./ConversionContext" import { ConversionContext } from "./ConversionContext"
import { Lists } from "../../../Utils/Lists"
export class ExtractImages extends Conversion< export class ExtractImages extends Conversion<
ThemeConfigJson, ThemeConfigJson,
@ -243,16 +244,12 @@ export class ExtractImages extends Conversion<
} }
// Split "circle:white;./assets/layers/.../something.svg" into ["circle", "./assets/layers/.../something.svg"] // Split "circle:white;./assets/layers/.../something.svg" into ["circle", "./assets/layers/.../something.svg"]
const allPaths = Utils.NoNull( const allPaths = Lists.noNull(Lists.noEmpty(foundImage.path?.split(";")?.map((part) => {
Utils.NoEmpty( if (part.startsWith("http")) {
foundImage.path?.split(";")?.map((part) => { return part
if (part.startsWith("http")) { }
return part return part.split(":")[0]
} })))
return part.split(":")[0]
})
)
)
for (const path of allPaths) { for (const path of allPaths) {
cleanedImages.push({ cleanedImages.push({
path, path,

View file

@ -1,10 +1,10 @@
import { ThemeConfigJson } from "../Json/ThemeConfigJson" import { ThemeConfigJson } from "../Json/ThemeConfigJson"
import { Utils } from "../../../Utils"
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson" import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
import { LayerConfigJson } from "../Json/LayerConfigJson" import { LayerConfigJson } from "../Json/LayerConfigJson"
import { DesugaringStep, Each, Fuse, On } from "./Conversion" import { DesugaringStep, Each, Fuse, On } from "./Conversion"
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson" import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
import { ConversionContext } from "./ConversionContext" import { ConversionContext } from "./ConversionContext"
import { Lists } from "../../../Utils/Lists"
export class UpdateLegacyLayer extends DesugaringStep< export class UpdateLegacyLayer extends DesugaringStep<
LayerConfigJson | string | { builtin; override } LayerConfigJson | string | { builtin; override }
@ -190,7 +190,7 @@ export class UpdateLegacyLayer extends DesugaringStep<
) { ) {
iconConfig = iconConfig.render iconConfig = iconConfig.render
} }
const icon = Utils.NoEmpty(iconConfig.split(";")) const icon = Lists.noEmpty(iconConfig.split(";"))
pr.marker = icon.map((i) => { pr.marker = icon.map((i) => {
if (i.startsWith("http")) { if (i.startsWith("http")) {
return { icon: i } return { icon: i }
@ -281,7 +281,7 @@ class UpdateLegacyTheme extends DesugaringStep<ThemeConfigJson> {
} }
} }
oldThemeConfig.layers = Utils.NoNull(oldThemeConfig.layers) oldThemeConfig.layers = Lists.noNull(oldThemeConfig.layers)
delete oldThemeConfig["language"] delete oldThemeConfig["language"]
delete oldThemeConfig["version"] delete oldThemeConfig["version"]
delete oldThemeConfig["clustering"] delete oldThemeConfig["clustering"]

View file

@ -1,18 +1,6 @@
import { import { Concat, DesugaringContext, DesugaringStep, Each, FirstOf, Fuse, On, SetDefault } from "./Conversion"
Concat,
DesugaringContext,
DesugaringStep,
Each,
FirstOf,
Fuse,
On,
SetDefault,
} from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson" import { LayerConfigJson } from "../Json/LayerConfigJson"
import { import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
MinimalTagRenderingConfigJson,
TagRenderingConfigJson,
} from "../Json/TagRenderingConfigJson"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import RewritableConfigJson from "../Json/RewritableConfigJson" import RewritableConfigJson from "../Json/RewritableConfigJson"
import SpecialVisualizations from "../../../UI/SpecialVisualizations" import SpecialVisualizations from "../../../UI/SpecialVisualizations"
@ -33,6 +21,7 @@ import { TagUtils } from "../../../Logic/Tags/TagUtils"
import { ExpandFilter, PruneFilters } from "./ExpandFilter" import { ExpandFilter, PruneFilters } from "./ExpandFilter"
import { ExpandTagRendering } from "./ExpandTagRendering" import { ExpandTagRendering } from "./ExpandTagRendering"
import layerconfig from "../../../assets/schemas/layerconfigmeta.json" import layerconfig from "../../../assets/schemas/layerconfigmeta.json"
import { Lists } from "../../../Utils/Lists"
class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> { class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
constructor() { constructor() {
@ -360,7 +349,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
const addByDefault = this.builtinQuestions.filter((tr) => tr.labels?.indexOf(key) >= 0) const addByDefault = this.builtinQuestions.filter((tr) => tr.labels?.indexOf(key) >= 0)
const ids = new Set(addByDefault.map((tr) => tr.id)) const ids = new Set(addByDefault.map((tr) => tr.id))
const idsInOrder = this._desugaring.tagRenderingOrder?.filter((id) => ids.has(id)) ?? [] const idsInOrder = this._desugaring.tagRenderingOrder?.filter((id) => ids.has(id)) ?? []
return Utils.NoNull(idsInOrder.map((id) => this._desugaring.tagRenderings.get(id))) return Lists.noNull(idsInOrder.map((id) => this._desugaring.tagRenderings.get(id)))
} }
convert(json: LayerConfigJson, _: ConversionContext): LayerConfigJson { convert(json: LayerConfigJson, _: ConversionContext): LayerConfigJson {
@ -771,11 +760,11 @@ class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
const condition = tr.condition const condition = tr.condition
for (const trElement of tr.mappings) { for (const trElement of tr.mappings) {
const showIf = TagUtils.optimzeJson({ const showIf = TagUtils.optimzeJson({
and: Utils.NoNull([ and: Lists.noNull([
condition, condition,
{ {
or: Utils.NoNull([trElement.alsoShowIf, trElement.if]), or: Lists.noNull([trElement.alsoShowIf, trElement.if]),
}, },
]), ]),
}) })
if (showIf === true) { if (showIf === true) {
@ -984,14 +973,12 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
const allAutoIndex = json.titleIcons.indexOf(<any>"auto:*") const allAutoIndex = json.titleIcons.indexOf(<any>"auto:*")
if (allAutoIndex >= 0) { if (allAutoIndex >= 0) {
const generated = Utils.NoNull( const generated = Lists.noNull(json.tagRenderings.map((tr) => {
json.tagRenderings.map((tr) => { if (typeof tr === "string") {
if (typeof tr === "string") { return undefined
return undefined }
} return this.createTitleIconsBasedOn(<any>tr)
return this.createTitleIconsBasedOn(<any>tr) }))
})
)
json.titleIcons.splice(allAutoIndex, 1, ...generated) json.titleIcons.splice(allAutoIndex, 1, ...generated)
return json return json
} }
@ -1113,9 +1100,7 @@ export class OrderTagRendering extends DesugaringStep<TagRenderingConfigJson | s
} }
export class OrderLayer extends DesugaringStep<string | LayerConfigJson> { export class OrderLayer extends DesugaringStep<string | LayerConfigJson> {
private static readonly layerAttributesOrder: ReadonlyArray<string> = Utils.Dedup( private static readonly layerAttributesOrder: ReadonlyArray<string> = Lists.dedup((<ConfigMeta[]>layerconfig).filter((c) => c.path.length === 1).map((c) => c.path[0]))
(<ConfigMeta[]>layerconfig).filter((c) => c.path.length === 1).map((c) => c.path[0])
)
constructor() { constructor() {
super("OrderLayer", "Reorders a tagRendering to the default order") super("OrderLayer", "Reorders a tagRendering to the default order")
@ -1151,7 +1136,8 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
"Fully prepares and expands a layer for the LayerConfig.", "Fully prepares and expands a layer for the LayerConfig.",
new DeriveSource(), new DeriveSource(),
new On("tagRenderings", new Each(new RewriteSpecial())), new On("tagRenderings", new Each(new RewriteSpecial())),
new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)), new On("tagRenderings", new Concat(new ExpandRewrite())
.andThenF(Utils.Flatten)),
new On( new On(
"tagRenderings", "tagRenderings",
(layer) => (layer) =>

View file

@ -1,14 +1,4 @@
import { import { Concat, Conversion, DesugaringContext, DesugaringStep, Each, Fuse, On, Pass, SetDefault } from "./Conversion"
Concat,
Conversion,
DesugaringContext,
DesugaringStep,
Each,
Fuse,
On,
Pass,
SetDefault,
} from "./Conversion"
import { ThemeConfigJson } from "../Json/ThemeConfigJson" import { ThemeConfigJson } from "../Json/ThemeConfigJson"
import { OrderLayer, PrepareLayer, RewriteSpecial } from "./PrepareLayer" import { OrderLayer, PrepareLayer, RewriteSpecial } from "./PrepareLayer"
import { LayerConfigJson } from "../Json/LayerConfigJson" import { LayerConfigJson } from "../Json/LayerConfigJson"
@ -22,6 +12,7 @@ import ValidationUtils from "./ValidationUtils"
import { ConversionContext } from "./ConversionContext" import { ConversionContext } from "./ConversionContext"
import { ConfigMeta } from "../../../UI/Studio/configMeta" import { ConfigMeta } from "../../../UI/Studio/configMeta"
import themeconfig from "../../../assets/schemas/layoutconfigmeta.json" import themeconfig from "../../../assets/schemas/layoutconfigmeta.json"
import { Lists } from "../../../Utils/Lists"
class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJson[]> { class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJson[]> {
private readonly _state: DesugaringContext private readonly _state: DesugaringContext
@ -198,7 +189,7 @@ export class AddDefaultLayers extends DesugaringStep<ThemeConfigJson> {
convert(json: ThemeConfigJson, context: ConversionContext): ThemeConfigJson { convert(json: ThemeConfigJson, context: ConversionContext): ThemeConfigJson {
const state = this._state const state = this._state
json.layers = Utils.NoNull([...(json.layers ?? [])]) json.layers = Lists.noNull([...(json.layers ?? [])])
const alreadyLoaded = new Set(json.layers.map((l) => l["id"])) const alreadyLoaded = new Set(json.layers.map((l) => l["id"]))
for (const layerName of Constants.added_by_default) { for (const layerName of Constants.added_by_default) {
@ -612,9 +603,7 @@ class PostvalidateTheme extends DesugaringStep<ThemeConfigJson> {
} }
} }
export class OrderTheme extends Fuse<ThemeConfigJson> { export class OrderTheme extends Fuse<ThemeConfigJson> {
private static readonly themeAttributesOrder: ReadonlyArray<string> = Utils.Dedup( private static readonly themeAttributesOrder: ReadonlyArray<string> = Lists.dedup((<ConfigMeta[]>themeconfig).filter((c) => c.path.length === 1).map((c) => c.path[0]))
(<ConfigMeta[]>themeconfig).filter((c) => c.path.length === 1).map((c) => c.path[0])
)
constructor() { constructor() {
super("Reorders the layer to the default order", super("Reorders the layer to the default order",

View file

@ -10,6 +10,7 @@ import { And } from "../../../Logic/Tags/And"
import { DoesImageExist, ValidateFilter, ValidatePointRendering } from "./Validation" import { DoesImageExist, ValidateFilter, ValidatePointRendering } from "./Validation"
import { ValidateTagRenderings } from "./ValidateTagRenderings" import { ValidateTagRenderings } from "./ValidateTagRenderings"
import { TagsFilterClosed } from "../../../Logic/Tags/TagTypes" import { TagsFilterClosed } from "../../../Logic/Tags/TagTypes"
import { Lists } from "../../../Utils/Lists"
export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> { export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
private readonly _isBuiltin: boolean private readonly _isBuiltin: boolean
@ -207,10 +208,8 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
} }
{ {
// duplicate ids in tagrenderings check // duplicate ids in tagrenderings check
const duplicates = Utils.NoNull( const duplicates = Lists.noNull(Utils.Duplicates(json.tagRenderings?.map((tr) => tr?.["id"])))
Utils.Duplicates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"]))) if (duplicates?.length > 0) {
)
if (duplicates.length > 0) {
// It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list // It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list
context context
.enter("tagRenderings") .enter("tagRenderings")

View file

@ -1,8 +1,8 @@
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { Utils } from "../../../Utils"
import SpecialVisualizations from "../../../UI/SpecialVisualizations" import SpecialVisualizations from "../../../UI/SpecialVisualizations"
import { RenderingSpecification, SpecialVisualization } from "../../../UI/SpecialVisualization" import { RenderingSpecification, SpecialVisualization } from "../../../UI/SpecialVisualization"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
import { Lists } from "../../../Utils/Lists"
export default class ValidationUtils { export default class ValidationUtils {
public static getAllSpecialVisualisations( public static getAllSpecialVisualisations(
@ -51,9 +51,9 @@ export default class ValidationUtils {
JSON.stringify(renderingConfig.mappings) JSON.stringify(renderingConfig.mappings)
) )
} }
const translations: any[] = Utils.NoNull([ const translations: any[] = Lists.noNull([
renderingConfig.render, renderingConfig.render,
...(renderingConfig.mappings ?? []).map((m) => m.then), ...(renderingConfig.mappings ?? []).map((m) => m.then),
]) ])
const all: RenderingSpecification[] = [] const all: RenderingSpecification[] = []
for (let translation of translations) { for (let translation of translations) {

View file

@ -10,6 +10,7 @@ import { Utils } from "../../Utils"
import { RegexTag } from "../../Logic/Tags/RegexTag" import { RegexTag } from "../../Logic/Tags/RegexTag"
import MarkdownUtils from "../../Utils/MarkdownUtils" import MarkdownUtils from "../../Utils/MarkdownUtils"
import Validators, { ValidatorType } from "../../UI/InputElement/Validators" import Validators, { ValidatorType } from "../../UI/InputElement/Validators"
import { Lists } from "../../Utils/Lists"
export type FilterConfigOption = { export type FilterConfigOption = {
question: Translation question: Translation
@ -225,17 +226,17 @@ export default class FilterConfig {
public GenerateDocs(): string { public GenerateDocs(): string {
const hasField = this.options.some((opt) => opt.fields?.length > 0) const hasField = this.options.some((opt) => opt.fields?.length > 0)
return MarkdownUtils.table( return MarkdownUtils.table(
Utils.NoNull(["id", "question", "osmTags", hasField ? "fields" : undefined]), Lists.noNull(["id", "question", "osmTags", hasField ? "fields" : undefined]),
this.options.map((opt, i) => { this.options.map((opt, i) => {
const isDefault = this.options.length > 1 && (this.defaultSelection ?? 0) == i const isDefault = this.options.length > 1 && (this.defaultSelection ?? 0) == i
return <string[]>( return <string[]>(
Utils.NoNull([ Lists.noNull([
this.id + "." + i, this.id + "." + i,
isDefault ? `*${opt.question.txt}* (default)` : opt.question, isDefault ? `*${opt.question.txt}* (default)` : opt.question,
opt.osmTags?.asHumanString() ?? "", opt.osmTags?.asHumanString() ?? "",
opt.fields?.length > 0 opt.fields?.length > 0
? opt.fields.map((f) => f.name + " (" + f.type + ")").join(" ") ? opt.fields.map((f) => f.name + " (" + f.type + ")").join(" ")
: undefined, : undefined,
]) ])
) )
}) })

View file

@ -23,6 +23,7 @@ import MarkdownUtils from "../../Utils/MarkdownUtils"
import { And } from "../../Logic/Tags/And" import { And } from "../../Logic/Tags/And"
import OsmWiki from "../../Logic/Osm/OsmWiki" import OsmWiki from "../../Logic/Osm/OsmWiki"
import { UnitUtils } from "../UnitUtils" import { UnitUtils } from "../UnitUtils"
import { Lists } from "../../Utils/Lists"
export default class LayerConfig extends WithContextLoader { export default class LayerConfig extends WithContextLoader {
public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const
@ -226,7 +227,7 @@ export default class LayerConfig extends WithContextLoader {
} }
if (json.lineRendering) { if (json.lineRendering) {
this.lineRendering = Utils.NoNull(json.lineRendering).map( this.lineRendering = Lists.noNull(json.lineRendering).map(
(r, i) => new LineRenderingConfig(r, `${context}[${i}]`) (r, i) => new LineRenderingConfig(r, `${context}[${i}]`)
) )
} else { } else {
@ -234,7 +235,7 @@ export default class LayerConfig extends WithContextLoader {
} }
if (json.pointRendering) { if (json.pointRendering) {
this.mapRendering = Utils.NoNull(json.pointRendering).map( this.mapRendering = Lists.noNull(json.pointRendering).map(
(r, i) => new PointRenderingConfig(r, `${context}[${i}](${this.id})`) (r, i) => new PointRenderingConfig(r, `${context}[${i}](${this.id})`)
) )
} else { } else {
@ -283,7 +284,7 @@ export default class LayerConfig extends WithContextLoader {
} }
const missingIds = const missingIds =
Utils.NoNull(json.tagRenderings)?.filter( Lists.noNull(json.tagRenderings)?.filter(
(tr) => (tr) =>
typeof tr !== "string" && typeof tr !== "string" &&
tr["builtin"] === undefined && tr["builtin"] === undefined &&
@ -298,7 +299,7 @@ export default class LayerConfig extends WithContextLoader {
throw msg throw msg
} }
this.tagRenderings = (Utils.NoNull(json.tagRenderings) ?? []).map( this.tagRenderings = (Lists.noNull(json.tagRenderings) ?? []).map(
(tr, i) => (tr, i) =>
new TagRenderingConfig( new TagRenderingConfig(
<QuestionableTagRenderingConfigJson>tr, <QuestionableTagRenderingConfigJson>tr,
@ -431,7 +432,7 @@ export default class LayerConfig extends WithContextLoader {
return [ return [
`[${tr.id}](#${tr.id}) ${origDef}`, `[${tr.id}](#${tr.id}) ${origDef}`,
Utils.NoNull([q, r, options]).join("<br/>"), Lists.noNull([q, r, options]).join("<br/>"),
tr.labels.join(", "), tr.labels.join(", "),
key, key,
] ]
@ -560,7 +561,7 @@ export default class LayerConfig extends WithContextLoader {
] ]
} }
for (const revDep of Utils.Dedup(layerIsNeededBy?.get(this.id) ?? [])) { for (const revDep of Lists.dedup(layerIsNeededBy?.get(this.id) ?? [])) {
extraProps.push( extraProps.push(
["This layer is needed as dependency for layer", `[${revDep}](#${revDep})`].join( ["This layer is needed as dependency for layer", `[${revDep}](#${revDep})`].join(
" " " "
@ -568,32 +569,30 @@ export default class LayerConfig extends WithContextLoader {
) )
} }
const tableRows: string[][] = Utils.NoNull( const tableRows: string[][] = Lists.noNull(this.tagRenderings
this.tagRenderings .map((tr) => tr.FreeformValues())
.map((tr) => tr.FreeformValues()) .filter((values) => values !== undefined)
.filter((values) => values !== undefined) .filter((values) => values.key !== "id")
.filter((values) => values.key !== "id") .map((values) => {
.map((values) => { const embedded: string[] = values.values?.map((v) =>
const embedded: string[] = values.values?.map((v) => OsmWiki.constructLinkMd(values.key, v)
OsmWiki.constructLinkMd(values.key, v) ) ?? ["_no preset options defined, or no values in them_"]
) ?? ["_no preset options defined, or no values in them_"] const statistics = `https://taghistory.raifer.tech/?#***/${encodeURIComponent(
const statistics = `https://taghistory.raifer.tech/?#***/${encodeURIComponent( values.key
values.key )}/`
)}/` const tagInfo = `https://taginfo.openstreetmap.org/keys/${values.key}#values`
const tagInfo = `https://taginfo.openstreetmap.org/keys/${values.key}#values` return [
return [ [
[ `<a target="_blank" href='${tagInfo}'><img src='https://mapcomplete.org/assets/svg/search.svg' height='18px'></a>`,
`<a target="_blank" href='${tagInfo}'><img src='https://mapcomplete.org/assets/svg/search.svg' height='18px'></a>`, `<a target="_blank" href='${statistics}'><img src='https://mapcomplete.org/assets/svg/statistics.svg' height='18px'></a>`,
`<a target="_blank" href='${statistics}'><img src='https://mapcomplete.org/assets/svg/statistics.svg' height='18px'></a>`, OsmWiki.constructLinkMd(values.key),
OsmWiki.constructLinkMd(values.key), ].join(" "),
].join(" "), values.type === undefined
values.type === undefined ? "Multiple choice"
? "Multiple choice" : `[${values.type}](../SpecialInputElements.md#${values.type})`,
: `[${values.type}](../SpecialInputElements.md#${values.type})`, embedded.join(" "),
embedded.join(" "), ]
] }))
})
)
let quickOverview: string[] = [] let quickOverview: string[] = []
if (tableRows.length > 0) { if (tableRows.length > 0) {
@ -693,7 +692,7 @@ export default class LayerConfig extends WithContextLoader {
} }
AllTagRenderings(): TagRenderingConfig[] { AllTagRenderings(): TagRenderingConfig[] {
return Utils.NoNull([...this.tagRenderings, ...this.titleIcons, this.title]) return Lists.noNull([...this.tagRenderings, ...this.titleIcons, this.title])
} }
public isLeftRightSensitive(): boolean { public isLeftRightSensitive(): boolean {

View file

@ -5,10 +5,7 @@ import { TagUtils } from "../../Logic/Tags/TagUtils"
import { And } from "../../Logic/Tags/And" import { And } from "../../Logic/Tags/And"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { Tag } from "../../Logic/Tags/Tag" import { Tag } from "../../Logic/Tags/Tag"
import { import { MappingConfigJson, QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRenderingConfigJson"
MappingConfigJson,
QuestionableTagRenderingConfigJson,
} from "./Json/QuestionableTagRenderingConfigJson"
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"
@ -21,6 +18,7 @@ import { UploadableTag } from "../../Logic/Tags/TagTypes"
import LayerConfig from "./LayerConfig" import LayerConfig from "./LayerConfig"
import ComparingTag from "../../Logic/Tags/ComparingTag" import ComparingTag from "../../Logic/Tags/ComparingTag"
import { Unit } from "../Unit" import { Unit } from "../Unit"
import { Lists } from "../../Utils/Lists"
export interface Mapping { export interface Mapping {
readonly if: UploadableTag readonly if: UploadableTag
@ -317,7 +315,7 @@ export default class TagRenderingConfig {
} }
keys.push(...mapping.if.usedKeys()) keys.push(...mapping.if.usedKeys())
} }
keys = Utils.Dedup(keys) keys = Lists.dedup(keys)
for (let i = 0; i < this.mappings.length; i++) { for (let i = 0; i < this.mappings.length; i++) {
const mapping = this.mappings[i] const mapping = this.mappings[i]
if (mapping.hideInAnswer) { if (mapping.hideInAnswer) {
@ -352,7 +350,7 @@ export default class TagRenderingConfig {
} }
allKeys = allKeys.concat(mapping.if.usedKeys()) allKeys = allKeys.concat(mapping.if.usedKeys())
} }
allKeys = Utils.Dedup(allKeys) allKeys = Lists.dedup(allKeys)
if (allKeys.length > 1 && !allHaveIfNot) { if (allKeys.length > 1 && !allHaveIfNot) {
throw `${context}: A multi-answer is defined, which generates values over multiple keys. Please define ifnot-tags too on every mapping` throw `${context}: A multi-answer is defined, which generates values over multiple keys. Please define ifnot-tags too on every mapping`
} }
@ -541,20 +539,18 @@ export default class TagRenderingConfig {
if?: TagsFilter if?: TagsFilter
then: TypedTranslation<Record<string, string>> then: TypedTranslation<Record<string, string>>
img?: string img?: string
}[] = Utils.NoNull( }[] = Lists.noNull((this.mappings ?? [])?.filter((mapping) => {
(this.mappings ?? [])?.filter((mapping) => { if (mapping.if === undefined) {
if (mapping.if === undefined) { return true
return true }
} if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) {
if (TagUtils.MatchesMultiAnswer(mapping.if, tags)) { return true
return true }
} if (mapping.alsoShowIf?.matchesProperties(tags)) {
if (mapping.alsoShowIf?.matchesProperties(tags)) { return true
return true }
} return false
return false }))
})
)
if (freeformKeyDefined && tags[this.freeform.key] !== undefined) { if (freeformKeyDefined && tags[this.freeform.key] !== undefined) {
const usedFreeformValues = new Set<string>( const usedFreeformValues = new Set<string>(
@ -659,9 +655,7 @@ export default class TagRenderingConfig {
const key = this.freeform?.key const key = this.freeform?.key
const answerMappings = this.mappings?.filter((m) => m.hideInAnswer !== true) const answerMappings = this.mappings?.filter((m) => m.hideInAnswer !== true)
if (key === undefined) { if (key === undefined) {
const values: { k: string; v: string }[][] = Utils.NoNull( const values: { k: string; v: string }[][] = Lists.noNull(answerMappings?.map((m) => m.if.asChange({})) ?? [])
answerMappings?.map((m) => m.if.asChange({})) ?? []
)
if (values.length === 0) { if (values.length === 0) {
return return
} }
@ -677,17 +671,13 @@ export default class TagRenderingConfig {
} }
return { return {
key: commonKey, key: commonKey,
values: Utils.NoNull( values: Lists.noNull(values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v)),
values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v)
),
} }
} }
let values = Utils.NoNull( let values = Lists.noNull(answerMappings?.map(
answerMappings?.map( (m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v
(m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v ) ?? [])
) ?? []
)
if (values.length === undefined) { if (values.length === undefined) {
values = undefined values = undefined
} }
@ -1027,18 +1017,18 @@ export default class TagRenderingConfig {
].join(" ") ].join(" ")
} }
return Utils.NoNull([ return Lists.noNull([
"### " + this.id, "### " + this.id,
this.description, this.description,
this.question !== undefined this.question !== undefined
? "The question is `" + this.question.txt + "`" ? "The question is `" + this.question.txt + "`"
: "_This tagrendering has no question and is thus read-only_", : "_This tagrendering has no question and is thus read-only_",
freeform, freeform,
mappings, mappings,
condition, condition,
labels, labels,
"", "",
reuse, reuse,
]).join("\n") ]).join("\n")
} }
@ -1061,7 +1051,7 @@ export default class TagRenderingConfig {
tags.push(m.ifnot) tags.push(m.ifnot)
} }
return Utils.NoNull(tags) return Lists.noNull(tags)
} }
/** /**

View file

@ -13,6 +13,7 @@ import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import TagRenderingConfig from "./TagRenderingConfig" import TagRenderingConfig from "./TagRenderingConfig"
import { TagUtils } from "../../Logic/Tags/TagUtils" import { TagUtils } from "../../Logic/Tags/TagUtils"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
import { Lists } from "../../Utils/Lists"
/** /**
* Minimal information about a theme * Minimal information about a theme
@ -381,7 +382,7 @@ export default class ThemeConfig implements ThemeInformation {
const usedImages = jsonNoFavourites._usedImages const usedImages = jsonNoFavourites._usedImages
usedImages.sort() usedImages.sort()
this.usedImages = Utils.Dedup(usedImages) this.usedImages = Lists.dedup(usedImages)
return this.usedImages return this.usedImages
} }
} }

View file

@ -13,7 +13,6 @@
import ThemesList from "./BigComponents/ThemesList.svelte" import ThemesList from "./BigComponents/ThemesList.svelte"
import { MinimalThemeInformation } from "../Models/ThemeConfig/ThemeConfig" import { MinimalThemeInformation } from "../Models/ThemeConfig/ThemeConfig"
import LoginButton from "./Base/LoginButton.svelte" import LoginButton from "./Base/LoginButton.svelte"
import { Utils } from "../Utils"
import Searchbar from "./Base/Searchbar.svelte" import Searchbar from "./Base/Searchbar.svelte"
import ThemeSearch, { ThemeSearchIndex } from "../Logic/Search/ThemeSearch" import ThemeSearch, { ThemeSearchIndex } from "../Logic/Search/ThemeSearch"
import SearchUtils from "../Logic/Search/SearchUtils" import SearchUtils from "../Logic/Search/SearchUtils"
@ -26,6 +25,7 @@
import AccordionSingle from "./Flowbite/AccordionSingle.svelte" import AccordionSingle from "./Flowbite/AccordionSingle.svelte"
import MenuDrawerIndex from "./BigComponents/MenuDrawerIndex.svelte" import MenuDrawerIndex from "./BigComponents/MenuDrawerIndex.svelte"
import InsetSpacer from "./Base/InsetSpacer.svelte" import InsetSpacer from "./Base/InsetSpacer.svelte"
import { Lists } from "../Utils/Lists"
AndroidPolyfill.init().then(() => console.log("Android polyfill setup completed")) AndroidPolyfill.init().then(() => console.log("Android polyfill setup completed"))
const featureSwitches = new OsmConnectionFeatureSwitches() const featureSwitches = new OsmConnectionFeatureSwitches()
@ -80,7 +80,7 @@
const customThemes: Store<MinimalThemeInformation[]> = Stores.listStabilized<string>( const customThemes: Store<MinimalThemeInformation[]> = Stores.listStabilized<string>(
state.installedUserThemes.stabilized(1000) state.installedUserThemes.stabilized(1000)
).mapD((stableIds) => Utils.NoNullInplace(stableIds.map((id) => state.getUnofficialTheme(id)))) ).mapD((stableIds) => Lists.noNullInplace(stableIds.map((id) => state.getUnofficialTheme(id))))
function filtered(themes: Store<MinimalThemeInformation[]>): Store<MinimalThemeInformation[]> { function filtered(themes: Store<MinimalThemeInformation[]>): Store<MinimalThemeInformation[]> {
const searchIndex = Locale.language.map( const searchIndex = Locale.language.map(

View file

@ -3,8 +3,7 @@ import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import { TagUtils } from "../../Logic/Tags/TagUtils" import { TagUtils } from "../../Logic/Tags/TagUtils"
import { OsmFeature } from "../../Models/OsmFeature" import { OsmFeature } from "../../Models/OsmFeature"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { labels } from "wikibase-sdk/dist/src/helpers/simplify" import { Lists } from "../../Utils/Lists"
import { isInteger } from "tailwind-merge/dist/lib/validators"
class ChartJsColours { class ChartJsColours {
public static readonly unknownColor = "rgba(128, 128, 128, 0.2)" public static readonly unknownColor = "rgba(128, 128, 128, 0.2)"
@ -243,7 +242,7 @@ export class ChartJsUtils {
if (options?.period === "month") { if (options?.period === "month") {
trimmedDays = trimmedDays.map((d) => d.substring(0, 7)) trimmedDays = trimmedDays.map((d) => d.substring(0, 7))
} }
trimmedDays = Utils.Dedup(trimmedDays) trimmedDays = Lists.dedup(trimmedDays)
for (let i = 0; i < labels.length; i++) { for (let i = 0; i < labels.length; i++) {
const label = labels[i] const label = labels[i]

View file

@ -10,7 +10,7 @@ export default class Combine extends BaseUIElement {
constructor(uiElements: (string | BaseUIElement)[]) { constructor(uiElements: (string | BaseUIElement)[]) {
super() super()
this.uiElements = Utils.NoNull(uiElements).map((el) => { this.uiElements = Utils.noNull(uiElements).map((el) => {
if (typeof el === "string") { if (typeof el === "string") {
return new FixedUiElement(el) return new FixedUiElement(el)
} }

View file

@ -4,6 +4,7 @@ import { Translation } from "../i18n/Translation"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import MarkdownUtils from "../../Utils/MarkdownUtils" import MarkdownUtils from "../../Utils/MarkdownUtils"
import Locale from "../i18n/Locale" import Locale from "../i18n/Locale"
import { Lists } from "../../Utils/Lists"
export default class Hotkeys { export default class Hotkeys {
public static readonly _docs: UIEventSource< public static readonly _docs: UIEventSource<
@ -130,7 +131,7 @@ export default class Hotkeys {
] ]
}) })
.sort() .sort()
byKey = Utils.NoNull(byKey) byKey = Lists.noNull(byKey)
for (let i = byKey.length - 1; i > 0; i--) { for (let i = byKey.length - 1; i > 0; i--) {
if (byKey[i - 1][0] === byKey[i][0]) { if (byKey[i - 1][0] === byKey[i][0]) {
byKey.splice(i, 1) byKey.splice(i, 1)

View file

@ -64,7 +64,7 @@ export default class TableOfContents {
} }
} }
const heading = Utils.Times(() => "#", firstTitle.depth) const heading = Utils.times(() => "#", firstTitle.depth)
toc = heading + " Table of contents\n\n" + toc toc = heading + " Table of contents\n\n" + toc
const firstTitleIndex = md.indexOf(firstTitle.title) const firstTitleIndex = md.indexOf(firstTitle.title)

View file

@ -2,6 +2,7 @@
import type SmallLicense from "../../Models/smallLicense" import type SmallLicense from "../../Models/smallLicense"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { twJoin } from "tailwind-merge" import { twJoin } from "tailwind-merge"
import { Lists } from "../../Utils/Lists"
export let iconPath: string export let iconPath: string
export let license: SmallLicense export let license: SmallLicense
@ -10,7 +11,7 @@
} catch (e) { } catch (e) {
console.warn(e) console.warn(e)
} }
let sources = Utils.NoNull(Utils.NoEmpty(license?.sources)) let sources = Utils.noNull(Lists.noEmpty(license?.sources))
function sourceName(lnk: string) { function sourceName(lnk: string) {
try { try {

View file

@ -3,6 +3,7 @@
import type { ChartConfiguration } from "chart.js" import type { ChartConfiguration } from "chart.js"
import ChartJs from "../Base/ChartJs.svelte" import ChartJs from "../Base/ChartJs.svelte"
import { ChartJsUtils } from "../Base/ChartJsUtils" import { ChartJsUtils } from "../Base/ChartJsUtils"
import { Lists } from "../../Utils/Lists"
export let values: Store<string[]> export let values: Store<string[]>
let counts: Store<Map<string, number>> = values.map( let counts: Store<Map<string, number>> = values.map(
@ -11,7 +12,7 @@
return undefined return undefined
} }
values = Utils.NoNull(values) values = Utils.noNull(values)
const counts = new Map<string, number>() const counts = new Map<string, number>()
for (const value of values) { for (const value of values) {
const c = counts.get(value) ?? 0 const c = counts.get(value) ?? 0
@ -23,7 +24,7 @@
let max: Store<number> = counts.mapD(counts => Math.max(...Array.from(counts.values()))) let max: Store<number> = counts.mapD(counts => Math.max(...Array.from(counts.values())))
let keys: Store<string> = counts.mapD(counts => { let keys: Store<string> = counts.mapD(counts => {
const keys = Utils.Dedup(counts.keys()) const keys = Lists.dedup(counts.keys())
keys.sort(/*inplace sort*/) keys.sort(/*inplace sort*/)
return keys return keys
}) })

View file

@ -13,12 +13,12 @@
import SnappingFeatureSource from "../../Logic/FeatureSource/Sources/SnappingFeatureSource" import SnappingFeatureSource from "../../Logic/FeatureSource/Sources/SnappingFeatureSource"
import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger" import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { Utils } from "../../Utils"
import Move_arrows from "../../assets/svg/Move_arrows.svelte" import Move_arrows from "../../assets/svg/Move_arrows.svelte"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import { Tag } from "../../Logic/Tags/Tag" import { Tag } from "../../Logic/Tags/Tag"
import { TagUtils } from "../../Logic/Tags/TagUtils" import { TagUtils } from "../../Logic/Tags/TagUtils"
import type { WayId } from "../../Models/OsmFeature" import type { WayId } from "../../Models/OsmFeature"
import { Lists } from "../../Utils/Lists"
/** /**
* An advanced location input, which has support to: * An advanced location input, which has support to:
@ -114,7 +114,7 @@
}) })
} }
const snappedLocation = new SnappingFeatureSource( const snappedLocation = new SnappingFeatureSource(
new FeatureSourceMerger(...Utils.NoNull(snapSources)), new FeatureSourceMerger(...(Lists.noNull(snapSources))),
// We snap to the (constantly updating) map location // We snap to the (constantly updating) map location
mapProperties.location, mapProperties.location,
{ {

View file

@ -10,11 +10,11 @@
import { QueryParameters } from "../../Logic/Web/QueryParameters" import { QueryParameters } from "../../Logic/Web/QueryParameters"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import { Utils } from "../../Utils"
import Qr from "../../Utils/Qr" import Qr from "../../Utils/Qr"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte" import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import Copyable from "../Base/Copyable.svelte" import Copyable from "../Base/Copyable.svelte"
import { Lists } from "../../Utils/Lists"
export let state: ThemeViewState export let state: ThemeViewState
const tr = Translations.t.general.sharescreen const tr = Translations.t.general.sharescreen
@ -45,7 +45,7 @@
enableGeolocation: boolean enableGeolocation: boolean
) { ) {
const layout = state.theme const layout = state.theme
let excluded = Utils.NoNull([ let excluded = Lists.noNull([
showWelcomeMessage ? undefined : "fs-welcome-message", showWelcomeMessage ? undefined : "fs-welcome-message",
enableLogin ? undefined : "fs-enable-login", enableLogin ? undefined : "fs-enable-login",
enableFilters ? undefined : "fs-filter", enableFilters ? undefined : "fs-filter",
@ -79,7 +79,7 @@
.filter((part) => !part.startsWith("layer-")) .filter((part) => !part.startsWith("layer-"))
.concat(...layers) .concat(...layers)
.concat(excluded.map((k) => k + "=" + false)) .concat(excluded.map((k) => k + "=" + false))
linkToShare = baseLink + Utils.Dedup(params).join("&") linkToShare = baseLink + Lists.dedup(params).join("&")
if (layout.definitionRaw !== undefined) { if (layout.definitionRaw !== undefined) {
linkToShare += "&userlayout=" + (layout.definedAtUrl ?? layout.id) linkToShare += "&userlayout=" + (layout.definedAtUrl ?? layout.id)

View file

@ -23,7 +23,7 @@
? "flex flex-wrap items-center justify-center gap-x-2" ? "flex flex-wrap items-center justify-center gap-x-2"
: "theme-list my-2 gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3"} : "theme-list my-2 gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3"}
> >
{#each Utils.DedupOnId(Utils.NoNull(themes)) as theme (theme.id)} {#each Utils.DedupOnId(Utils.noNull(themes)) as theme (theme.id)}
<ThemeButton {theme} {state} iconOnly={onlyIcons}> <ThemeButton {theme} {state} iconOnly={onlyIcons}>
{#if $search && hasSelection && themes?.[0] === theme} {#if $search && hasSelection && themes?.[0] === theme}
<span class="thanks hidden-on-mobile" aria-hidden="true"> <span class="thanks hidden-on-mobile" aria-hidden="true">

View file

@ -46,7 +46,7 @@
} }
return trs.find((tr) => return trs.find((tr) =>
tr.mappings.some((mapping) => { tr.mappings.some((mapping) => {
const ifTags = Or.construct(Utils.NoNull([mapping.if, mapping.alsoShowIf])) const ifTags = Or.construct(Lists.noNull([mapping.if, mapping.alsoShowIf]))
const keys = ifTags.usedKeys() const keys = ifTags.usedKeys()
return keys.some((k) => k == key) return keys.some((k) => k == key)
}) })

View file

@ -3,12 +3,12 @@
*/ */
import { Stores, UIEventSource } from "../../../../Logic/UIEventSource" import { Stores, UIEventSource } from "../../../../Logic/UIEventSource"
import SingleCollectionTime from "./SingleCollectionTime.svelte" import SingleCollectionTime from "./SingleCollectionTime.svelte"
import { Utils } from "../../../../Utils"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid" import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
import { Lists } from "../../../../Utils/Lists"
export let value: UIEventSource<string> export let value: UIEventSource<string>
let initialRules: string[] = Utils.NoEmpty(value.data?.split(";")?.map(v => v.trim())) let initialRules: string[] = Lists.noEmpty(value.data?.split(";")?.map(v => v.trim()))
let singleRules: UIEventSource<UIEventSource<string>[]> = new UIEventSource( let singleRules: UIEventSource<UIEventSource<string>[]> = new UIEventSource(
initialRules?.map(v => new UIEventSource(v)) ?? [] initialRules?.map(v => new UIEventSource(v)) ?? []
) )
@ -17,7 +17,7 @@ if(singleRules.data.length === 0){
} }
singleRules.bindD(stores => Stores.concat(stores)).addCallbackAndRunD(subrules => { singleRules.bindD(stores => Stores.concat(stores)).addCallbackAndRunD(subrules => {
console.log("Setting subrules", subrules) console.log("Setting subrules", subrules)
value.set(Utils.NoEmpty(subrules).join("; ")) value.set(Lists.noEmpty(subrules).join("; "))
}) })
function rm(rule: UIEventSource){ function rm(rule: UIEventSource){

View file

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import PlusCircle from "@rgossiaux/svelte-heroicons/solid/PlusCircle"
import TimeInput from "../TimeInput.svelte" import TimeInput from "../TimeInput.svelte"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid" import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
import Checkbox from "../../../Base/Checkbox.svelte" import Checkbox from "../../../Base/Checkbox.svelte"
@ -8,7 +7,8 @@
import { Stores, UIEventSource } from "../../../../Logic/UIEventSource" import { Stores, UIEventSource } from "../../../../Logic/UIEventSource"
import Translations from "../../../i18n/Translations" import Translations from "../../../i18n/Translations"
import { OH } from "../../../OpeningHours/OpeningHours" import { OH } from "../../../OpeningHours/OpeningHours"
import { Utils } from "../../../../Utils" import { Lists } from "../../../../Utils/Lists"
import { Translation } from "../../../i18n/Translation"
export let value: UIEventSource<string> export let value: UIEventSource<string>
@ -18,7 +18,7 @@
*/ */
let weekdays: Translation[] = [wt.monday, wt.tuesday, wt.wednesday, wt.thursday, wt.friday, wt.saturday, wt.sunday, Translations.t.general.opening_hours.ph] let weekdays: Translation[] = [wt.monday, wt.tuesday, wt.wednesday, wt.thursday, wt.friday, wt.saturday, wt.sunday, Translations.t.general.opening_hours.ph]
let initialTimes= Utils.NoEmpty(value.data?.split(" ")?.[1]?.split(",")?.map(s => s.trim())??[]) let initialTimes= Lists.noEmpty(value.data?.split(" ")?.[1]?.split(",")?.map(s => s.trim()) ?? [])
let values = new UIEventSource(initialTimes.map(t => new UIEventSource(t))) let values = new UIEventSource(initialTimes.map(t => new UIEventSource(t)))
if(values.data.length === 0){ if(values.data.length === 0){
values.data.push(new UIEventSource("")) values.data.push(new UIEventSource(""))
@ -29,7 +29,7 @@
let daysOfTheWeek = [...OH.days, "PH"] let daysOfTheWeek = [...OH.days, "PH"]
let selectedDays = daysOfTheWeek.map(() => new UIEventSource(false)) let selectedDays = daysOfTheWeek.map(() => new UIEventSource(false))
let initialDays = Utils.NoEmpty(value.data?.split(" ")?.[0]?.split(",") ?? []) let initialDays = Lists.noEmpty(value.data?.split(" ")?.[0]?.split(",") ?? [])
for (const initialDay of initialDays) { for (const initialDay of initialDays) {
if (initialDay.indexOf("-") > 0) { if (initialDay.indexOf("-") > 0) {
let [start, end] = initialDay.split("-") let [start, end] = initialDay.split("-")
@ -48,17 +48,17 @@
} }
let selectedDaysBound = Stores.concat(selectedDays) let selectedDaysBound = Stores.concat(selectedDays)
.mapD(days => Utils.NoNull(days.map((selected, i) => selected ? daysOfTheWeek[i] : undefined))) .mapD(days => Lists.noNull(days.map((selected, i) => selected ? daysOfTheWeek[i] : undefined)))
let valuesConcat: Store<string[]> = values.bindD(values => Stores.concat(values)) let valuesConcat: Store<string[]> = values.bindD(values => Stores.concat(values))
.mapD(values => Utils.NoEmpty(values)) .mapD(values => Lists.noEmpty(values))
valuesConcat.mapD(times => { valuesConcat.mapD(times => {
console.log(times) console.log(times)
times = Utils.NoNull(times) times = Lists.noNull(times)
if (!times || times?.length === 0) { if (!times || times?.length === 0) {
return undefined return undefined
} }
times?.sort(/*concatted, new array*/) times?.sort(/*concatted, new array*/)
return (Utils.NoEmpty(selectedDaysBound.data).join(",") + " " + times).trim() return (Lists.noEmpty(selectedDaysBound.data).join(",") + " " + times).trim()
}, [selectedDaysBound]).addCallbackAndRunD(v => value.set(v)) }, [selectedDaysBound]).addCallbackAndRunD(v => value.set(v))
function selectWeekdays() { function selectWeekdays() {

View file

@ -29,7 +29,7 @@
let element: HTMLTableElement let element: HTMLTableElement
function range(n: number) { function range(n: number) {
return Utils.TimesT(n, (n) => n) return Utils.timesT(n, (n) => n)
} }
function clearSelection() { function clearSelection() {

View file

@ -13,6 +13,7 @@
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import WikidataValidator from "../Validators/WikidataValidator" import WikidataValidator from "../Validators/WikidataValidator"
import Searchbar from "../../Base/Searchbar.svelte" import Searchbar from "../../Base/Searchbar.svelte"
import { Lists } from "../../../Utils/Lists"
const t = Translations.t.general.wikipedia const t = Translations.t.general.wikipedia
@ -80,7 +81,7 @@
seen.push(item) seen.push(item)
} }
} }
return Utils.NoNull(seen).filter((i) => !knownIds.has(i.id)) return Lists.noNull(seen).filter((i) => !knownIds.has(i.id))
}) })
</script> </script>

View file

@ -12,6 +12,7 @@
import type { Feature } from "geojson" import type { Feature } from "geojson"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
import WikidataValidator from "../Validators/WikidataValidator" import WikidataValidator from "../Validators/WikidataValidator"
import { Lists } from "../../../Utils/Lists"
export let args: (string | number | boolean)[] = [] export let args: (string | number | boolean)[] = []
export let feature: Feature export let feature: Feature
@ -43,12 +44,8 @@
}) })
) )
let instanceOf: number[] = Utils.NoNull( let instanceOf: number[] = Lists.noNull((options?.instanceOf ?? []).map((i) => Wikidata.QIdToNumber(i)))
(options?.instanceOf ?? []).map((i) => Wikidata.QIdToNumber(i)) let notInstanceOf: number[] = Lists.noNull((options?.notInstanceOf ?? []).map((i) => Wikidata.QIdToNumber(i)))
)
let notInstanceOf: number[] = Utils.NoNull(
(options?.notInstanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
)
let allowMultipleArg = options?.["multiple"] ?? "yes" let allowMultipleArg = options?.["multiple"] ?? "yes"
let allowMultiple = allowMultipleArg === "yes" || "" + allowMultipleArg === "true" let allowMultiple = allowMultipleArg === "yes" || "" + allowMultipleArg === "true"

View file

@ -45,7 +45,7 @@
}, },
[Stores.chronic(5 * 60 * 1000)] [Stores.chronic(5 * 60 * 1000)]
) )
.mapD((date) => Utils.TwoDigits(date.getHours()) + ":" + Utils.TwoDigits(date.getMinutes())) .mapD((date) => Utils.twoDigits(date.getHours()) + ":" + Utils.twoDigits(date.getMinutes()))
let size = nextChange.map((change) => let size = nextChange.map((change) =>
change === undefined ? "absolute h-7 w-7" : "absolute h-5 w-5 top-0 left-1/4" change === undefined ? "absolute h-7 w-7" : "absolute h-5 w-5 top-0 left-1/4"

View file

@ -40,7 +40,7 @@ export class OH {
if (h == 24) { if (h == 24) {
return "00:00" return "00:00"
} }
return Utils.TwoDigits(h) + ":" + Utils.TwoDigits(m) return Utils.twoDigits(h) + ":" + Utils.twoDigits(m)
} }
/** /**

View file

@ -66,9 +66,7 @@
const feature = state.indexedFeatures.featuresById.data.get(targetFeatureId) const feature = state.indexedFeatures.featuresById.data.get(targetFeatureId)
const featureTags = state.featureProperties.getStore(targetFeatureId) const featureTags = state.featureProperties.getStore(targetFeatureId)
const rendering = tagRenderingConfig.GetRenderValue(featureTags.data).txt const rendering = tagRenderingConfig.GetRenderValue(featureTags.data).txt
const specialRenderings = Utils.NoNull( const specialRenderings = Lists.noNull(SpecialVisualizations.constructSpecification(rendering)).filter((v) => typeof v !== "string" && v.func["supportsAutoAction"] === true)
SpecialVisualizations.constructSpecification(rendering)
).filter((v) => typeof v !== "string" && v.func["supportsAutoAction"] === true)
if (specialRenderings.length == 0) { if (specialRenderings.length == 0) {
console.warn( console.warn(

View file

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig" import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
import { Utils } from "../../../Utils"
import { Translation } from "../../i18n/Translation" import { Translation } from "../../i18n/Translation"
import TagRenderingMapping from "./TagRenderingMapping.svelte" import TagRenderingMapping from "./TagRenderingMapping.svelte"
import type { SpecialVisualizationState } from "../../SpecialVisualization" import type { SpecialVisualizationState } from "../../SpecialVisualization"
@ -9,6 +8,8 @@
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
import { Lists } from "../../../Utils/Lists"
export let tags: UIEventSource<Record<string, string> | undefined> export let tags: UIEventSource<Record<string, string> | undefined>
@ -27,7 +28,7 @@
throw "Config is undefined in tagRenderingAnswer" throw "Config is undefined in tagRenderingAnswer"
} }
let trs: Store<{ then: Translation; icon?: string; iconClass?: string }[]> = tags.mapD((tags) => let trs: Store<{ then: Translation; icon?: string; iconClass?: string }[]> = tags.mapD((tags) =>
Utils.NoNull(config?.GetRenderValues(tags)),onDestroy Lists.noNull(config?.GetRenderValues(tags)),onDestroy
) )
</script> </script>

View file

@ -28,7 +28,6 @@
import { And } from "../../../Logic/Tags/And" import { And } from "../../../Logic/Tags/And"
import { get } from "svelte/store" import { get } from "svelte/store"
import Markdown from "../../Base/Markdown.svelte" import Markdown from "../../Base/Markdown.svelte"
import { Utils } from "../../../Utils"
import type { UploadableTag } from "../../../Logic/Tags/TagTypes" import type { UploadableTag } from "../../../Logic/Tags/TagTypes"
import { TagTypes } from "../../../Logic/Tags/TagTypes" import { TagTypes } from "../../../Logic/Tags/TagTypes"
@ -36,6 +35,7 @@
import If from "../../Base/If.svelte" import If from "../../Base/If.svelte"
import DotMenu from "../../Base/DotMenu.svelte" import DotMenu from "../../Base/DotMenu.svelte"
import SidebarUnit from "../../Base/SidebarUnit.svelte" import SidebarUnit from "../../Base/SidebarUnit.svelte"
import { Lists } from "../../../Utils/Lists"
export let config: TagRenderingConfig export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>> export let tags: UIEventSource<Record<string, string>>
@ -152,7 +152,7 @@
feedback.setData(undefined) feedback.setData(undefined)
} }
let usedKeys: string[] = Utils.Dedup(config.usedTags().flatMap((t) => t.usedKeys())) let usedKeys: string[] = Lists.dedup(config.usedTags().flatMap((t) => t.usedKeys()))
/** /**
* The 'minimalTags' is a subset of the tags of the feature, only containing the values relevant for this object. * The 'minimalTags' is a subset of the tags of the feature, only containing the values relevant for this object.
@ -342,7 +342,7 @@
let menuIsOpened = new UIEventSource(false) let menuIsOpened = new UIEventSource(false)
function disableQuestion() { function disableQuestion() {
const newList = Utils.Dedup([config.id, ...disabledInTheme.data]) const newList = Lists.dedup([config.id, ...disabledInTheme.data])
disabledInTheme.set(newList) disabledInTheme.set(newList)
menuIsOpened.set(false) menuIsOpened.set(false)
} }

View file

@ -27,7 +27,7 @@
signature: string, signature: string,
madeByLoggedInUser: Store<boolean> madeByLoggedInUser: Store<boolean>
})[] })[]
> = reviews.reviews.map((r) => Utils.NoNull(r)) > = reviews.reviews.map((r) => Lists.noNull(r))
let test = state.featureSwitches.featureSwitchIsTesting let test = state.featureSwitches.featureSwitchIsTesting
let debug = state.featureSwitches.featureSwitchIsDebugging let debug = state.featureSwitches.featureSwitchIsDebugging
let subject: Store<string> = reviews.subjectUri let subject: Store<string> = reviews.subjectUri

View file

@ -6,22 +6,19 @@
import ThemeSearch from "../../Logic/Search/ThemeSearch" import ThemeSearch from "../../Logic/Search/ThemeSearch"
import SidebarUnit from "../Base/SidebarUnit.svelte" import SidebarUnit from "../Base/SidebarUnit.svelte"
import ThemeResult from "./ThemeResult.svelte" import ThemeResult from "./ThemeResult.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import DotMenu from "../Base/DotMenu.svelte" import DotMenu from "../Base/DotMenu.svelte"
import { TrashIcon } from "@babeard/svelte-heroicons/mini" import { TrashIcon } from "@babeard/svelte-heroicons/mini"
import { CogIcon } from "@rgossiaux/svelte-heroicons/solid" import { CogIcon } from "@rgossiaux/svelte-heroicons/solid"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import { MinimalThemeInformation } from "../../Models/ThemeConfig/ThemeConfig"
import ThemeViewState from "../../Models/ThemeViewState"
import { WithSearchState } from "../../Models/ThemeViewState/WithSearchState" import { WithSearchState } from "../../Models/ThemeViewState/WithSearchState"
import { Store } from "../../Logic/UIEventSource" import { Store } from "../../Logic/UIEventSource"
import { Utils } from "../../Utils" import { Lists } from "../../Utils/Lists"
export let state: WithSearchState export let state: WithSearchState
let searchTerm = state.searchState.searchTerm let searchTerm = state.searchState.searchTerm
let visitedThemes = state.userRelatedState.recentlyVisitedThemes.value let visitedThemes = state.userRelatedState.recentlyVisitedThemes.value
let recentThemes: Store<string[]> = visitedThemes.map((themes) => let recentThemes: Store<string[]> = visitedThemes.map((themes) =>
Utils.Dedup(themes.filter((th) => th !== state.theme.id)).slice(0, 6)) Lists.dedup(themes.filter((th) => th !== state.theme.id)).slice(0, 6))
let themeResults = state.searchState.themeSuggestions let themeResults = state.searchState.themeSuggestions
const t = Translations.t.general.search const t = Translations.t.general.search

View file

@ -1,8 +1,4 @@
import { import { SpecialVisualization, SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization"
SpecialVisualization,
SpecialVisualizationState,
SpecialVisualizationSvelte,
} from "../SpecialVisualization"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import SvelteUIElement from "../Base/SvelteUIElement" import SvelteUIElement from "../Base/SvelteUIElement"
@ -16,6 +12,7 @@ import { Translation } from "../i18n/Translation"
import { MapillaryLinkVis } from "../Popup/MapillaryLinkVis" import { MapillaryLinkVis } from "../Popup/MapillaryLinkVis"
import SendEmail from "../Popup/SendEmail.svelte" import SendEmail from "../Popup/SendEmail.svelte"
import DynLink from "../Base/DynLink.svelte" import DynLink from "../Base/DynLink.svelte"
import { Lists } from "../../Utils/Lists"
class FediverseLinkVis extends SpecialVisualization { class FediverseLinkVis extends SpecialVisualization {
funcName = "fediverse_link" funcName = "fediverse_link"
@ -88,9 +85,7 @@ class WikidatalabelVis extends SpecialVisualization {
const id = tagsSource const id = tagsSource
.map((tags) => tags[args[0]]) .map((tags) => tags[args[0]])
.map((wikidata) => { .map((wikidata) => {
const wikidataIds = Utils.NoEmpty( const wikidataIds = Lists.noEmpty(wikidata?.split(";")?.map((wd) => wd.trim()) ?? [])
wikidata?.split(";")?.map((wd) => wd.trim()) ?? []
)
return wikidataIds?.[0] return wikidataIds?.[0]
}) })
const entry = id.bind((id) => Wikidata.LoadWikidataEntry(id)) const entry = id.bind((id) => Wikidata.LoadWikidataEntry(id))

View file

@ -57,7 +57,7 @@
.filter((tr) => tr.question !== undefined) .filter((tr) => tr.question !== undefined)
let diffInDays = overview.mapD((overview) => { let diffInDays = overview.mapD((overview) => {
const dateStrings = Utils.NoNull(overview._meta.map((cs) => cs.properties.date)) const dateStrings = Lists.noNull(overview._meta.map((cs) => cs.properties.date))
const dates: number[] = dateStrings.map((d) => new Date(d).getTime()) const dates: number[] = dateStrings.map((d) => new Date(d).getTime())
const mindate = Math.min(...dates) const mindate = Math.min(...dates)
const maxdate = Math.max(...dates) const maxdate = Math.max(...dates)

View file

@ -82,7 +82,7 @@ export class ChangesetsOverview {
public readonly _meta: (ChangeSetData & OsmFeature)[] public readonly _meta: (ChangeSetData & OsmFeature)[]
private constructor(meta: (ChangeSetData & OsmFeature)[]) { private constructor(meta: (ChangeSetData & OsmFeature)[]) {
this._meta = Utils.NoNull(meta) this._meta = Lists.noNull(meta)
} }
public static fromDirtyData(meta: (ChangeSetData & OsmFeature)[]): ChangesetsOverview { public static fromDirtyData(meta: (ChangeSetData & OsmFeature)[]): ChangesetsOverview {

View file

@ -9,7 +9,7 @@
import Loading from "../Base/Loading.svelte" import Loading from "../Base/Loading.svelte"
import Checkbox from "../Base/Checkbox.svelte" import Checkbox from "../Base/Checkbox.svelte"
import PlantNet from "../../Logic/Web/PlantNet" import PlantNet from "../../Logic/Web/PlantNet"
import { Lists } from "../../Utils/Lists"
let services: MCService[] = [] let services: MCService[] = []
let recheckSignal: UIEventSource<any> = new UIEventSource<any>(undefined) let recheckSignal: UIEventSource<any> = new UIEventSource<any>(undefined)
@ -345,7 +345,7 @@
let someLoading = new UIEventSource(true) let someLoading = new UIEventSource(true)
function setAll() { function setAll() {
const data = Utils.NoNull(services.map((s) => s.status.data)) const data = Lists.noNull(services.map((s) => s.status.data))
someLoading.setData(data.length !== services.length) someLoading.setData(data.length !== services.length)
if (data.some((d) => d === "offline")) { if (data.some((d) => d === "offline")) {
all.setData("offline") all.setData("offline")

View file

@ -20,7 +20,7 @@
import DeleteButton from "./DeleteButton.svelte" import DeleteButton from "./DeleteButton.svelte"
import StudioHashSetter from "./StudioHashSetter" import StudioHashSetter from "./StudioHashSetter"
import TitledPanel from "../Base/TitledPanel.svelte" import TitledPanel from "../Base/TitledPanel.svelte"
import Popup from "../Base/Popup.svelte" import { Lists } from "../../Utils/Lists"
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw const layerSchema: ConfigMeta[] = <any>layerSchemaRaw
@ -36,7 +36,7 @@
) )
const configuration = state.configuration const configuration = state.configuration
const allNames = Utils.Dedup(layerSchema.map((meta) => meta.hints.group)) const allNames = Lists.dedup(layerSchema.map((meta) => meta.hints.group))
const perRegion: Record<string, ConfigMeta[]> = {} const perRegion: Record<string, ConfigMeta[]> = {}
for (const region of allNames) { for (const region of allNames) {

View file

@ -1,12 +1,7 @@
import { ConfigMeta } from "./configMeta" import { ConfigMeta } from "./configMeta"
import { Store, UIEventSource } from "../../Logic/UIEventSource" import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import { import { Conversion, ConversionMessage, DesugaringContext, Pipe } from "../../Models/ThemeConfig/Conversion/Conversion"
Conversion,
ConversionMessage,
DesugaringContext,
Pipe,
} from "../../Models/ThemeConfig/Conversion/Conversion"
import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer" import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer"
import { PrevalidateTheme, ValidateLayer } from "../../Models/ThemeConfig/Conversion/Validation" import { PrevalidateTheme, ValidateLayer } from "../../Models/ThemeConfig/Conversion/Validation"
import { AllSharedLayers } from "../../Customizations/AllSharedLayers" import { AllSharedLayers } from "../../Customizations/AllSharedLayers"
@ -25,6 +20,7 @@ import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
import { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"
import { ValidateTheme } from "../../Models/ThemeConfig/Conversion/ValidateTheme" import { ValidateTheme } from "../../Models/ThemeConfig/Conversion/ValidateTheme"
import * as questions from "../../../public/assets/generated/layers/questions.json" import * as questions from "../../../public/assets/generated/layers/questions.json"
import { Lists } from "../../Utils/Lists"
export interface HighlightedTagRendering { export interface HighlightedTagRendering {
path: ReadonlyArray<string | number> path: ReadonlyArray<string | number>
@ -380,7 +376,7 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
if (data[key]) { if (data[key]) {
// A bit of cleanup // A bit of cleanup
const lBefore = data[key].length const lBefore = data[key].length
const cleaned = Utils.NoNull(data[key]) const cleaned = Lists.noNull(data[key])
if (cleaned.length != lBefore) { if (cleaned.length != lBefore) {
data[key] = cleaned data[key] = cleaned
return true return true
@ -422,7 +418,7 @@ export default class EditLayerState extends EditJsonState<LayerConfigJson> {
private addMissingTagRenderingIds() { private addMissingTagRenderingIds() {
this.configuration.addCallbackD((config) => { this.configuration.addCallbackD((config) => {
const trs = Utils.NoNull(config.tagRenderings ?? []) const trs = Lists.noNull(config.tagRenderings ?? [])
for (let i = 0; i < trs.length; i++) { for (let i = 0; i < trs.length; i++) {
const tr = trs[i] const tr = trs[i]
if (typeof tr === "string") { if (typeof tr === "string") {
@ -544,7 +540,7 @@ export class EditThemeState extends EditJsonState<ThemeConfigJson> {
const prepare = this.buildValidation(state) const prepare = this.buildValidation(state)
const context = ConversionContext.construct([], ["prepare"]) const context = ConversionContext.construct([], ["prepare"])
if (configuration.layers) { if (configuration.layers) {
Utils.NoNullInplace(configuration.layers) Lists.noNullInplace(configuration.layers)
} }
try { try {
prepare.convert(<ThemeConfigJson>configuration, context) prepare.convert(<ThemeConfigJson>configuration, context)

View file

@ -1,4 +1,6 @@
import DOMPurify from "dompurify" import DOMPurify from "dompurify"
import { Lists } from "./Utils/Lists"
import { Strings } from "./Utils/Strings"
export class Utils { export class Utils {
/** /**
@ -109,7 +111,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return <T>parsed return <T>parsed
} }
static EncodeXmlValue(str) { static encodeXmlValue(str) {
if (typeof str !== "string") { if (typeof str !== "string") {
str = "" + str str = "" + str
} }
@ -122,26 +124,11 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
.replace(/'/g, "&apos;") .replace(/'/g, "&apos;")
} }
/** public static upper(str: string) {
* Gives a clean float, or undefined if parsing fails return str.substring(0, 1).toUpperCase() + str.substring(1)
* @param str
*/
static asFloat(str): number {
if (str) {
const i = parseFloat(str)
if (isNaN(i)) {
return undefined
}
return i
}
return undefined
} }
public static Upper(str: string) { public static twoDigits(i: number) {
return str.substr(0, 1).toUpperCase() + str.substr(1)
}
public static TwoDigits(i: number) {
if (i < 10) { if (i < 10) {
return "0" + i return "0" + i
} }
@ -151,16 +138,16 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
/** /**
* Converts a number to a number with precisely 7 decimals * Converts a number to a number with precisely 7 decimals
* *
* Utils.Round7(12.123456789) // => 12.1234568 * Utils.round7(12.123456789) // => 12.1234568
*/ */
public static Round7(i: number): number { public static round7(i: number): number {
if (i == undefined) { if (i == undefined) {
return undefined return undefined
} }
return Math.round(i * 10000000) / 10000000 return Math.round(i * 10000000) / 10000000
} }
public static Times(f: (i: number) => string, count: number): string { public static times(f: (i: number) => string, count: number): string {
let res = "" let res = ""
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
res += f(i) res += f(i)
@ -168,7 +155,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return res return res
} }
public static TimesT<T>(count: number, f: (i: number) => T): T[] { public static timesT<T>(count: number, f: (i: number) => T): T[] {
const res: T[] = [] const res: T[] = []
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
res.push(f(i)) res.push(f(i))
@ -176,16 +163,8 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return res return res
} }
/** public static noNull<T>(array: ReadonlyArray<T>): NonNullable<T>[] {
* Creates a shallow copy of the array. All elements that are not undefined/null will be in the new list return Lists.noNull(array)
* @param array
* @constructor
*/
public static NoNull<T>(array: ReadonlyArray<T> | undefined): T[] | undefined
public static NoNull(array: undefined): undefined
public static NoNull<T>(array: ReadonlyArray<T>): T[]
public static NoNull<T>(array: ReadonlyArray<T>): NonNullable<T>[] {
return <NonNullable<T>[]>(<unknown>array?.filter((o) => o !== undefined && o !== null))
} }
public static Hist(array: ReadonlyArray<string>): Map<string, number> { public static Hist(array: ReadonlyArray<string>): Map<string, number> {
@ -195,29 +174,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} }
return hist return hist
} }
/**
* Removes all empty strings from this list
* If undefined or null is given, an empty list is returned
*
* Utils.NoEmpty(undefined) // => []
* Utils.NoEmpty(["abc","","def", null]) // => ["abc","def", null]
*
*/
public static NoEmpty(array: string[]): string[] {
const ls: string[] = []
if (!array) {
return ls
}
for (const t of array) {
if (t === "") {
continue
}
ls.push(t)
}
return ls
}
public static EllipsesAfter(str: string, l: number = 100) { public static EllipsesAfter(str: string, l: number = 100) {
if (str === undefined || str === null) { if (str === undefined || str === null) {
return undefined return undefined
@ -297,28 +253,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} }
return str return str
} }
/**
* Creates a new array with all elements from 'arr' in such a way that every element will be kept only once
* Elements are returned in the same order as they appear in the lists.
* Null/Undefined is returned as is. If an emtpy array is given, a new empty array will be returned
*/
public static Dedup(arr: NonNullable<string[]>): NonNullable<string[]>
public static Dedup(arr: undefined): undefined
public static Dedup(arr: string[] | undefined): string[] | undefined
public static Dedup(arr: string[]): string[] {
if (arr === undefined || arr === null) {
return arr
}
const newArr = []
for (const string of arr) {
if (newArr.indexOf(string) < 0) {
newArr.push(string)
}
}
return newArr
}
public static DedupT<T>(arr: T[]): T[] { public static DedupT<T>(arr: T[]): T[] {
if (!arr) { if (!arr) {
return arr return arr
@ -359,7 +293,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
uniq.push(img) uniq.push(img)
} }
} else { } else {
const ksNoNull = Utils.NoNull(ks) const ksNoNull = Lists.noNull(ks)
const hasBeenSeen = ksNoNull.some((k) => seen.has(k)) const hasBeenSeen = ksNoNull.some((k) => seen.has(k))
if (!hasBeenSeen) { if (!hasBeenSeen) {
uniq.push(img) uniq.push(img)
@ -1213,7 +1147,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
if (days > 0) { if (days > 0) {
return days + "days" + " " + hours + "h" return days + "days" + " " + hours + "h"
} }
return hours + ":" + Utils.TwoDigits(minutes) + ":" + Utils.TwoDigits(seconds) return hours + ":" + Utils.twoDigits(minutes) + ":" + Utils.twoDigits(seconds)
} }
public static toHumanByteSize(bytes: number): string { public static toHumanByteSize(bytes: number): string {
@ -1247,9 +1181,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
const date = const date =
lastWeek.getFullYear() + lastWeek.getFullYear() +
"-" + "-" +
Utils.TwoDigits(lastWeek.getMonth() + 1) + Utils.twoDigits(lastWeek.getMonth() + 1) +
"-" + "-" +
Utils.TwoDigits(lastWeek.getDate()) Utils.twoDigits(lastWeek.getDate())
let osmcha_link = `"date__gte":[{"label":"${date}","value":"${date}"}],"editor":[{"label":"mapcomplete","value":"mapcomplete"}]` let osmcha_link = `"date__gte":[{"label":"${date}","value":"${date}"}],"editor":[{"label":"mapcomplete","value":"mapcomplete"}]`
if (theme !== undefined) { if (theme !== undefined) {
osmcha_link = osmcha_link =
@ -1833,16 +1767,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} }
return 50 * Math.round(number / 50) return 50 * Math.round(number / 50)
} }
public static NoNullInplace<T>(items: T[]): T[] {
for (let i = items.length - 1; i >= 0; i--) {
if (items[i] === null || items[i] === undefined || items[i] === "") {
items.splice(i, 1)
}
}
return items
}
/** /**
* Removes or rewrites some characters in links, as some blink/chromium based browsers are picky about them * Removes or rewrites some characters in links, as some blink/chromium based browsers are picky about them
* *
@ -1860,8 +1784,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return href return href
} }
private static emojiRegex = /[\p{Extended_Pictographic}🛰]/u
/** /**
* Returns 'true' if the given string contains at least one and only emoji characters * Returns 'true' if the given string contains at least one and only emoji characters
* *
@ -1870,19 +1792,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
* Utils.isEmoji("🍕") // => true * Utils.isEmoji("🍕") // => true
*/ */
public static isEmoji(string: string) { public static isEmoji(string: string) {
return Utils.emojiRegex.test(string) || Utils.isEmojiFlag(string) return Strings.isEmoji(string)
} }
/**
* Utils.isEmojiFlag("🇧🇪") // => true
*/
public static isEmojiFlag(string: string) {
return /[🇦-🇿]{2}/u.test(string) // flags, see https://stackoverflow.com/questions/53360006/detect-with-regex-if-emoji-is-country-flag
}
public static concat<T>(param: T[][]): T[] {
return [].concat(...param)
}
/** /**
* *
@ -1905,9 +1817,4 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return <T>copy return <T>copy
} }
public static pass(){
// Does nothing
}
} }

89
src/Utils/Lists.ts Normal file
View file

@ -0,0 +1,89 @@
/**
* Various utils to manipulate lists
*/
export class Lists {
/**
* Creates a shallow copy of the array. All elements that are not undefined/null will be in the new list
*/
public static noNull<T>(array: ReadonlyArray<T> | undefined): T[] | undefined
public static noNull(array: undefined): undefined
public static noNull<T>(array: ReadonlyArray<T>): T[]
public static noNull<T>(array: ReadonlyArray<T>): NonNullable<T>[] {
return <NonNullable<T>[]>(<unknown>array?.filter((o) => o !== undefined && o !== null))
}
/**
* Removes all empty strings from this list
* If undefined or null is given, an empty list is returned
*
* Lists.noEmpty(undefined) // => []
* Lists.noEmpty(["abc","","def", null]) // => ["abc","def", null]
*
*/
public static noEmpty(array: undefined | null): string[]
public static noEmpty(array: undefined[]): undefined[]
public static noEmpty(array: (string | "")[]): Exclude<string, "">[]
public static noEmpty(array: (string | undefined | null | "")[]): (string | undefined | null)[]
public static noEmpty(array: string[]): string[] {
const ls: string[] = []
if (!array) {
return ls
}
for (const t of array) {
if (t === "") {
continue
}
ls.push(t)
}
return ls
}
/**
* Creates a new array with all elements from 'arr' in such a way that every element will be kept only once
* Elements are returned in the same order as they appear in the lists.
* Null/Undefined is returned as is. If an empty array is given, a new empty array will be returned
*/
public static dedup(arr: NonNullable<string[]>): NonNullable<string[]>
public static dedup(arr: undefined): undefined
public static dedup(arr: string[] | undefined): string[] | undefined
public static dedup(arr: string[]): string[] {
if (arr === undefined || arr === null) {
return arr
}
const newArr = []
for (const string of arr) {
if (newArr.indexOf(string) < 0) {
newArr.push(string)
}
}
return newArr
}
public static dedupT<T>(arr: ReadonlyArray<T>): T[] ;
public static dedupT<T>(arr: null): null ;
public static dedupT<T>(arr: undefined): undefined ;
public static dedupT<T>(arr: ReadonlyArray<T>): T[] {
if (arr === undefined) {
return undefined
}
if (arr === null) {
return null
}
const items = []
for (const item of arr) {
if (items.indexOf(item) < 0) {
items.push(item)
}
}
return items
}
public static noNullInplace<T>(items: T[]): T[] {
for (let i = items.length - 1; i >= 0; i--) {
if (items[i] === null || items[i] === undefined || items[i] === "") {
items.splice(i, 1)
}
}
return items
}
}

25
src/Utils/Strings.ts Normal file
View file

@ -0,0 +1,25 @@
/**
* Various string-related utils
*/
export class Strings{
private static emojiRegex = /[\p{Extended_Pictographic}🛰]/u
/**
* Returns 'true' if the given string contains at least one and only emoji characters
*
* Strings.isEmoji("⛰\uFE0F") // => true
* Strings.isEmoji("🇧🇪") // => true
* Strings.isEmoji("🍕") // => true
*/
public static isEmoji(string: string): boolean {
return Strings.emojiRegex.test(string) || Strings.isEmojiFlag(string)
}
/**
* Strings.isEmojiFlag("🇧🇪") // => true
*/
public static isEmojiFlag(string: string): boolean {
return /[🇦-🇿]{2}/u.test(string) // flags, see https://stackoverflow.com/questions/53360006/detect-with-regex-if-emoji-is-country-flag
}
}