Refactoring: move functions out of Utils

This commit is contained in:
Pieter Vander Vennet 2025-08-26 03:18:25 +02:00
parent 442f5a2923
commit 5ea2414204
16 changed files with 126 additions and 145 deletions

View file

@ -22,7 +22,6 @@ import ThemeViewState from "../src/Models/ThemeViewState"
import Validators from "../src/UI/InputElement/Validators"
import questions from "../public/assets/generated/layers/questions.json"
import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson"
import { Utils } from "../src/Utils"
import { TagUtils } from "../src/Logic/Tags/TagUtils"
import Script from "./Script"
import { Changes } from "../src/Logic/Osm/Changes"
@ -562,7 +561,7 @@ export class GenerateDocs extends Script {
item.url.startsWith("pmtiles://")
)
)
const serverInfos = Utils.DedupOnId(serverInfosDupl, (item) => item.url)
const serverInfos = Lists.dedupOnId(serverInfosDupl, (item) => item.url)
const titles = Lists.dedup(Lists.noEmpty(serverInfos.map((s) => s.category)))
titles.sort()

View file

@ -327,7 +327,7 @@ export class GenerateLicenseInfo extends Script {
}
licenses.sort((a, b) => (a.path < b.path ? -1 : 1))
licenses = Utils.DedupOnId(licenses, (l) => l.path)
licenses = Lists.dedupOnId(licenses, (l) => l.path)
const path = dir + "/license_info.json"
if (licenses.length === 0) {
console.log("Removing", path, "as it is empty")

View file

@ -6,7 +6,6 @@ import { ImmutableStore, Store, Stores } from "../UIEventSource"
import ImageProvider, { ProvidedImage } from "./ImageProvider"
import { WikidataImageProvider } from "./WikidataImageProvider"
import Panoramax from "./Panoramax"
import { Utils } from "../../Utils"
import { ServerSourceInfo } from "../../Models/SourceOverview"
import { Lists } from "../../Utils/Lists"
@ -151,7 +150,7 @@ export default class AllImageProviders {
}
const source = Stores.concat(allSources).map((result) => {
const all = result.flatMap((x) => x)
return Utils.DedupOnId(all, (i) => [i?.id, i?.url, i?.alt_id])
return Lists.dedupOnId(all, (i) => [i?.id, i?.url, i?.alt_id])
})
this._cachedImageStores[cachekey] = source
return source

View file

@ -10,6 +10,7 @@ import { Point } from "geojson"
import { ImageData, Panoramax, PanoramaxXYZ } from "panoramax-js/dist"
import { Mapillary } from "../ImageProviders/Mapillary"
import { ServerSourceInfo } from "../../Models/SourceOverview"
import { Lists } from "../../Utils/Lists"
interface ImageFetcher {
/**
@ -465,6 +466,6 @@ export class CombinedFetcher {
this.fetchImage(source, lat, lon, state, sink)
}
return { images: sink.mapD((imgs) => Utils.DedupOnId(imgs, (i) => i["id"])), state }
return { images: sink.mapD((imgs) => Lists.dedupOnId(imgs, (i) => i["id"])), state }
}
}

View file

@ -10,7 +10,9 @@ import ThemeConfig from "../../src/Models/ThemeConfig/ThemeConfig"
import { ThemeConfigJson } from "../../src/Models/ThemeConfig/Json/ThemeConfigJson"
import SpecialVisualizations from "../../src/UI/SpecialVisualizations"
import ValidationUtils from "../../src/Models/ThemeConfig/Conversion/ValidationUtils"
import { QuestionableTagRenderingConfigJson } from "../../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import {
QuestionableTagRenderingConfigJson
} from "../../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import { LayerConfigJson } from "../../src/Models/ThemeConfig/Json/LayerConfigJson"
import { Lists } from "../Utils/Lists"
@ -206,7 +208,7 @@ export class SourceOverview {
}
urls = urls.filter((item) => !!item?.url)
urls.sort((a, b) => (a < b ? -1 : 1))
urls = Utils.DedupOnId(urls, (item) => item.url)
urls = Lists.dedupOnId(urls, (item) => item.url)
this.eliUrlsCached = urls
return urls
}

View file

@ -1,18 +1,6 @@
import {
Concat,
DesugaringContext,
DesugaringStep,
Each,
FirstOf,
Fuse,
On,
SetDefault,
} from "./Conversion"
import { Concat, DesugaringContext, DesugaringStep, Each, FirstOf, Fuse, On, SetDefault } from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import {
MinimalTagRenderingConfigJson,
TagRenderingConfigJson,
} from "../Json/TagRenderingConfigJson"
import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { Utils } from "../../../Utils"
import RewritableConfigJson from "../Json/RewritableConfigJson"
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
@ -1163,7 +1151,9 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
"Fully prepares and expands a layer for the LayerConfig.",
new DeriveSource(),
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(function <T>(list: (T | T[])[]): T[] {
return Lists.flatten(list)
})),
new On(
"tagRenderings",
(layer) =>
@ -1180,7 +1170,9 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
new On<
(LineRenderingConfigJson | RewritableConfigJson<LineRenderingConfigJson>)[],
LayerConfigJson
>("lineRendering", new Each(new ExpandRewrite()).andThenF(Utils.Flatten)),
>("lineRendering", new Each(new ExpandRewrite()).andThenF(function <T>(list: (T | T[])[]): T[] {
return Lists.flatten(list)
})),
new On<PointRenderingConfigJson[], LayerConfigJson>(
"pointRendering",
(layer) =>

View file

@ -178,9 +178,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
{
// Check for multiple, identical builtin questions - usability for studio users
const duplicates = Utils.Duplicates(
<string[]>json.tagRenderings.filter((tr) => typeof tr === "string")
)
const duplicates = Lists.duplicates(<string[]>json.tagRenderings.filter((tr) => typeof tr === "string"))
for (let i = 0; i < json.tagRenderings.length; i++) {
const tagRendering = json.tagRenderings[i]
if (typeof tagRendering === "string" && duplicates.indexOf(tagRendering) > 0) {
@ -209,7 +207,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
{
// duplicate ids in tagrenderings check
const duplicates = Lists.noNull(
Utils.Duplicates(json.tagRenderings?.map((tr) => tr?.["id"]))
Lists.duplicates(json.tagRenderings?.map((tr) => tr?.["id"]))
)
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
@ -312,9 +310,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
)
}
const duplicateIds = Utils.Duplicates(
(json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions")
)
const duplicateIds = Lists.duplicates((json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions"))
if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
context
.enter("tagRenderings")

View file

@ -7,6 +7,7 @@ import ThemeConfig from "../ThemeConfig"
import { Utils } from "../../../Utils"
import { DetectDuplicatePresets, DoesImageExist, ValidateLanguageCompleteness } from "./Validation"
import Constants from "../../Constants"
import { Lists } from "../../../Utils/Lists"
export class ValidateTheme extends DesugaringStep<ThemeConfigJson> {
/**
@ -105,7 +106,7 @@ export class ValidateTheme extends DesugaringStep<ThemeConfigJson> {
}
this._validateImage.convert(theme.icon, context.enter("icon"))
}
const dups = Utils.Duplicates(json.layers.map((layer) => layer["id"]))
const dups = Lists.duplicates(json.layers.map((layer) => layer["id"]))
if (dups.length > 0) {
context.err(
`The theme ${json.id} defines multiple layers with id ${dups.join(", ")}`

View file

@ -24,6 +24,7 @@ import { AvailableRasterLayers } from "../../RasterLayers"
import { eliCategory } from "../../RasterLayerProperties"
import licenses from "../../../assets/generated/license_info.json"
import { Strings } from "../../../Utils/Strings"
import { Lists } from "../../../Utils/Lists"
export class ValidateLanguageCompleteness extends DesugaringStep<ThemeConfig> {
private readonly _languages: string[]
@ -1064,7 +1065,7 @@ export class DetectDuplicatePresets extends DesugaringStep<ThemeConfig> {
const enNames = presets.map((p) => p.title.textFor("en"))
if (new Set(enNames).size != enNames.length) {
const dups = Utils.Duplicates(enNames)
const dups = Lists.duplicates(enNames)
const layersWithDup = json.layers.filter((l) =>
l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0)
)

View file

@ -13,7 +13,6 @@ import PointRenderingConfig from "./PointRenderingConfig"
import WithContextLoader from "./WithContextLoader"
import LineRenderingConfig from "./LineRenderingConfig"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
import { Utils } from "../../Utils"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import FilterConfigJson from "./Json/FilterConfigJson"
import { Overpass } from "../../Logic/Osm/Overpass"
@ -340,7 +339,7 @@ export default class LayerConfig extends WithContextLoader {
}
{
const duplicateIds = Utils.Duplicates(this.filters.map((f) => f.id))
const duplicateIds = Lists.duplicates(this.filters.map((f) => f.id))
if (duplicateIds.length > 0) {
throw `Some filters have a duplicate id: ${duplicateIds} (at ${context}.filters)`
}

View file

@ -23,6 +23,7 @@
import Translations from "../i18n/Translations"
import { default as Trans } from "../Base/Tr.svelte"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import { Lists } from "../../Utils/Lists"
export let state: ThemeViewState & SpecialVisualizationState = undefined
export let autoDownload = state.autoDownloadOfflineBasemap
@ -202,7 +203,7 @@
<AccordionItem paddingDefault="p-2">
<Trans t={t.overview} slot="header" />
<div class="leave-room">
{Utils.toHumanByteSize(Utils.sum($installed.map((area) => area.size)))}
{Utils.toHumanByteSize(Lists.sum($installed.map((area) => area.size)))}
<button
on:click={() => {
installed?.data?.forEach((area) => del(area))

View file

@ -8,6 +8,7 @@
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import { Utils } from "../../Utils"
import { Lists } from "../../Utils/Lists"
export let search: UIEventSource<string> = new UIEventSource<string>(undefined)
export let themes: MinimalThemeInformation[]
@ -23,7 +24,7 @@
? "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"}
>
{#each Utils.DedupOnId(Utils.noNull(themes)) as theme (theme.id)}
{#each Lists.dedupOnId(Utils.noNull(themes)) as theme (theme.id)}
<ThemeButton {theme} {state} iconOnly={onlyIcons}>
{#if $search && hasSelection && themes?.[0] === theme}
<span class="thanks hidden-on-mobile" aria-hidden="true">

View file

@ -9,10 +9,9 @@
import OpeningHoursRangeElement from "./OpeningHoursRangeElement.svelte"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import { OH } from "../OpeningHours"
import type { OpeningRange } from "../OpeningHours"
import { Utils } from "../../../Utils"
import { OH } from "../OpeningHours"
import { Lists } from "../../../Utils/Lists"
export let oh: opening_hours
export let ranges: OpeningRange[][] // Per weekday
@ -47,7 +46,7 @@
changeTexts: string[]
}[] = OH.partitionOHForDistance(changeHoursWeekend, changeHourTextWeekend)
let allChangeMoments: number[] = Utils.DedupT([...changeHours, ...changeHoursWeekend])
let allChangeMoments: number[] = Lists.dedupT([...changeHours, ...changeHoursWeekend])
let todayChangeMoments: Set<number> = new Set(OH.allChangeMoments(todayRanges)[0])
// By default, we always show the range between 8 - 19h, in order to give a stable impression
// Ofc, a bigger range is used if needed

View file

@ -5,10 +5,12 @@
import { TrashIcon } from "@babeard/svelte-heroicons/mini"
import ShowConversionMessage from "./ShowConversionMessage.svelte"
import Markdown from "../Base/Markdown.svelte"
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import type {
QuestionableTagRenderingConfigJson
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import CollapsedTagRenderingPreview from "./CollapsedTagRenderingPreview.svelte"
import { Accordion } from "flowbite-svelte"
import { Utils } from "../../Utils"
import { Lists } from "../../Utils/Lists"
export let state: EditJsonState<any>
export let path: (string | number)[] = []
@ -39,7 +41,7 @@
let datapath = path
const currentValue = state.getStoreFor<(string | QuestionableTagRenderingConfigJson)[]>(datapath)
currentValue.set(Utils.DedupT(currentValue.data))
currentValue.set(Lists.dedupT(currentValue.data))
console.log("Current value is", currentValue.data)
if (currentValue.data === undefined) {
currentValue.setData([])

View file

@ -252,99 +252,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
}
return str
}
public static DedupT<T>(arr: T[]): T[] {
if (!arr) {
return arr
}
const items = []
for (const item of arr) {
if (items.indexOf(item) < 0) {
items.push(item)
}
}
return items
}
/**
* Deduplicates the given array based on some ID-properties.
* Removes all falsey values
* @param arr
* @param toKey
* @constructor
*/
public static DedupOnId<T = { id: string }>(
arr: T[],
toKey?: (t: T) => string | string[]
): T[] {
const uniq: T[] = []
const seen = new Set<string>()
if (toKey === undefined) {
toKey = (item) => item["id"]
}
for (const img of arr) {
if (!img) {
continue
}
const ks = toKey(img)
if (typeof ks === "string") {
if (!seen.has(ks)) {
seen.add(ks)
uniq.push(img)
}
} else if (ks) {
const ksNoNull = Lists.noNull(ks)
const hasBeenSeen = ksNoNull.some((k) => seen.has(k))
if (!hasBeenSeen) {
uniq.push(img)
}
for (const k of ksNoNull) {
seen.add(k)
}
}
}
return uniq
}
/**
* Finds all duplicates in a list of strings
*
* Utils.Duplicates(["a", "b", "c"]) // => []
* Utils.Duplicates(["a", "b","c","b"] // => ["b"]
* Utils.Duplicates(["a", "b","c","b","b"] // => ["b"]
*
*/
public static Duplicates(arr: string[]): string[] {
if (arr === undefined) {
return undefined
}
const seen = new Set<string>()
const duplicates = new Set<string>()
for (const string of arr) {
if (seen.has(string)) {
duplicates.add(string)
}
seen.add(string)
}
return Array.from(duplicates)
}
/**
* In the given list, all values which are lists will be merged with the values, e.g.
*
* Utils.Flatten([ [1,2], 3, [4, [5 ,6]] ]) // => [1, 2, 3, 4, [5, 6]]
*/
public static Flatten<T>(list: (T | T[])[]): T[] {
const result = []
for (const value of list) {
if (Array.isArray(value)) {
result.push(...value)
} else {
result.push(value)
}
}
return result
}
/**
* Utils.Identical([1,2], [1,2]) // => true
* Utils.Identical([1,2,3], [1,2,4}]) // => false
@ -1787,14 +1694,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return href
}
public static sum(list: number[]): number {
let total = 0
for (const number of list) {
total += number
}
return total
}
/**
*
* JSON.stringify(Utils.reorder({b: "0", a: "1"}, ["a", "b"])) // => '{"a":"1","b":"0"}'

View file

@ -86,4 +86,93 @@ export class Lists {
}
return items
}
public static sum(list: number[]): number {
let total = 0
for (const number of list) {
total += number
}
return total
}
/**
* In the given list, all values which are lists will be merged with the values, e.g.
*
* Utils.Flatten([ [1,2], 3, [4, [5 ,6]] ]) // => [1, 2, 3, 4, [5, 6]]
*/
public static flatten<T>(list: (T | T[])[]): T[] {
const result = []
for (const value of list) {
if (Array.isArray(value)) {
result.push(...value)
} else {
result.push(value)
}
}
return result
}
/**
* Deduplicates the given array based on some ID-properties.
* Removes all falsey values
* @param arr
* @param toKey
* @constructor
*/
public static dedupOnId<T = { id: string }>(
arr: T[],
toKey?: (t: T) => string | string[]
): T[] {
const uniq: T[] = []
const seen = new Set<string>()
if (toKey === undefined) {
toKey = (item) => item["id"]
}
for (const img of arr) {
if (!img) {
continue
}
const ks = toKey(img)
if (typeof ks === "string") {
if (!seen.has(ks)) {
seen.add(ks)
uniq.push(img)
}
} else if (ks) {
const ksNoNull = Lists.noNull(ks)
const hasBeenSeen = ksNoNull.some((k) => seen.has(k))
if (!hasBeenSeen) {
uniq.push(img)
}
for (const k of ksNoNull) {
seen.add(k)
}
}
}
return uniq
}
/**
* Finds all duplicates in a list of strings
*
* Utils.Duplicates(["a", "b", "c"]) // => []
* Utils.Duplicates(["a", "b","c","b"] // => ["b"]
* Utils.Duplicates(["a", "b","c","b","b"] // => ["b"]
*
*/
public static duplicates(arr: string[]): string[] {
if (arr === undefined) {
return undefined
}
const seen = new Set<string>()
const duplicates = new Set<string>()
for (const string of arr) {
if (seen.has(string)) {
duplicates.add(string)
}
seen.add(string)
}
return Array.from(duplicates)
}
}