diff --git a/src/Logic/Actors/SelectedElementTagsUpdater.ts b/src/Logic/Actors/SelectedElementTagsUpdater.ts index 67a7c2b44..f34ec863e 100644 --- a/src/Logic/Actors/SelectedElementTagsUpdater.ts +++ b/src/Logic/Actors/SelectedElementTagsUpdater.ts @@ -10,6 +10,7 @@ import { Changes } from "../Osm/Changes" import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig" import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" import { WithChangesState } from "../../Models/ThemeViewState/WithChangesState" +import Objects from "../../Utils/Objects" export default class SelectedElementTagsUpdater { private static readonly metatags = new Set([ @@ -160,7 +161,7 @@ export default class SelectedElementTagsUpdater { const newGeometry = osmObject.asGeoJson()?.geometry const oldFeature = state.indexedFeatures.featuresById.data.get(id) const oldGeometry = oldFeature?.geometry - if (oldGeometry !== undefined && !Utils.SameObject(newGeometry, oldGeometry)) { + if (oldGeometry !== undefined && !Objects.sameObject(newGeometry, oldGeometry)) { console.log("Detected a difference in geometry for ", id) this.invalidateCache(s) oldFeature.geometry = newGeometry diff --git a/src/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts b/src/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts index c9cb4b04c..8f3d009f2 100644 --- a/src/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts +++ b/src/Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage.ts @@ -4,9 +4,9 @@ import TileLocalStorage from "./TileLocalStorage" import { GeoOperations } from "../../GeoOperations" import FeaturePropertiesStore from "./FeaturePropertiesStore" import { UIEventSource } from "../../UIEventSource" -import { Utils } from "../../../Utils" import { Tiles } from "../../../Models/TileRange" import { BBox } from "../../BBox" +import { Lists } from "../../../Utils/Lists" class SingleTileSaver { private readonly _storage: UIEventSource @@ -31,7 +31,7 @@ class SingleTileSaver { } public saveFeatures(features: Feature[]) { - if (Utils.sameList(features, this._storage.data)) { + if (Lists.sameList(features, this._storage.data)) { return } for (const feature of features) { diff --git a/src/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts b/src/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts index dee5f8994..90cc9ef1b 100644 --- a/src/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts +++ b/src/Logic/FeatureSource/Sources/ChangeGeometryApplicator.ts @@ -6,7 +6,7 @@ import { Stores, UIEventSource } from "../../UIEventSource" import { FeatureSource, IndexedFeatureSource } from "../FeatureSource" import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription" import { Feature } from "geojson" -import { Utils } from "../../../Utils" +import Objects from "../../../Utils/Objects" export default class ChangeGeometryApplicator implements FeatureSource { public readonly features: UIEventSource = new UIEventSource([]) @@ -69,7 +69,7 @@ export default class ChangeGeometryApplicator implements FeatureSource { // We only apply the last change as that one'll have the latest geometry const change = changesForFeature[changesForFeature.length - 1] copy.geometry = ChangeDescriptionTools.getGeojsonGeometry(change) - if (Utils.SameObject(copy.geometry, feature.geometry)) { + if (Objects.sameObject(copy.geometry, feature.geometry)) { // No actual changes: pass along the original newFeatures.push(feature) continue diff --git a/src/Logic/FeatureSource/Sources/OverpassFeatureSource.ts b/src/Logic/FeatureSource/Sources/OverpassFeatureSource.ts index 37be66206..7cbad0368 100644 --- a/src/Logic/FeatureSource/Sources/OverpassFeatureSource.ts +++ b/src/Logic/FeatureSource/Sources/OverpassFeatureSource.ts @@ -9,7 +9,7 @@ import { BBox } from "../../BBox" import { OsmFeature } from "../../../Models/OsmFeature" import { Lists } from "../../../Utils/Lists" -;("use strict") +("use strict") /** * A wrapper around the 'Overpass'-object. @@ -229,7 +229,7 @@ export default class OverpassFeatureSource im const requestedBounds = this.state.bounds.data if ( this._lastQueryBBox !== undefined && - Utils.sameList(this._layersToDownload.data, this._lastRequestedLayers) && + Lists.sameList(this._layersToDownload.data, this._lastRequestedLayers) && requestedBounds.isContainedIn(this._lastQueryBBox) ) { return undefined diff --git a/src/Logic/Search/FilterSearch.ts b/src/Logic/Search/FilterSearch.ts index 6dbf346f2..7cc80b653 100644 --- a/src/Logic/Search/FilterSearch.ts +++ b/src/Logic/Search/FilterSearch.ts @@ -32,7 +32,7 @@ export default class FilterSearch { .split(" ") .map((query) => { if (!Strings.isEmoji(query)) { - return Utils.simplifyStringForSearch(query) + return Strings.simplifyStringForSearch(query) } return query }) @@ -64,7 +64,7 @@ export default class FilterSearch { option.searchTerms?.["en"] ?? []), ].flatMap((term) => [term, ...(term?.split(" ") ?? [])]) - terms = terms.map((t) => Utils.simplifyStringForSearch(t)) + terms = terms.map((t) => Strings.simplifyStringForSearch(t)) terms.push(option.emoji) Lists.noNullInplace(terms) const distances = queries.flatMap((query) => diff --git a/src/Logic/Search/LayerSearch.ts b/src/Logic/Search/LayerSearch.ts index 3d23767c4..ebe644122 100644 --- a/src/Logic/Search/LayerSearch.ts +++ b/src/Logic/Search/LayerSearch.ts @@ -2,7 +2,7 @@ import SearchUtils from "./SearchUtils" import ThemeSearch from "./ThemeSearch" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig" -import { Utils } from "../../Utils" +import { Strings } from "../../Utils/Strings" export default class LayerSearch { private readonly _theme: ThemeConfig @@ -24,7 +24,7 @@ export default class LayerSearch { const queryParts = query .trim() .split(" ") - .map((q) => Utils.simplifyStringForSearch(q)) + .map((q) => Strings.simplifyStringForSearch(q)) for (const id in ThemeSearch.officialThemes.layers) { if (options?.whitelist && !options?.whitelist.has(id)) { continue diff --git a/src/Logic/Search/LocalElementSearch.ts b/src/Logic/Search/LocalElementSearch.ts index b34e34bf4..e4d697816 100644 --- a/src/Logic/Search/LocalElementSearch.ts +++ b/src/Logic/Search/LocalElementSearch.ts @@ -6,6 +6,7 @@ import { GeoOperations } from "../GeoOperations" import { ImmutableStore, Store, Stores } from "../UIEventSource" import OpenStreetMapIdSearch from "./OpenStreetMapIdSearch" import { Lists } from "../../Utils/Lists" +import { Strings } from "../../Utils/Strings" type IntermediateResult = { feature: Feature @@ -61,7 +62,7 @@ export default class LocalElementSearch implements GeocodingProvider { ...searchTerms .flatMap((entry) => entry.split(/ /)) .map((entry) => { - let simplified = Utils.simplifyStringForSearch(entry) + let simplified = Strings.simplifyStringForSearch(entry) if (matchStart) { simplified = simplified.slice(0, query.length) } @@ -103,7 +104,7 @@ export default class LocalElementSearch implements GeocodingProvider { const centerPoint: [number, number] = [center.lon, center.lat] const properties = this._state.perLayer const candidateId = OpenStreetMapIdSearch.extractId(query) - query = Utils.simplifyStringForSearch(query) + query = Strings.simplifyStringForSearch(query) const partials: Store[] = [] diff --git a/src/Logic/Search/SearchUtils.ts b/src/Logic/Search/SearchUtils.ts index a50ac5bf7..e5f993de2 100644 --- a/src/Logic/Search/SearchUtils.ts +++ b/src/Logic/Search/SearchUtils.ts @@ -2,6 +2,7 @@ import Locale from "../../UI/i18n/Locale" import { Utils } from "../../Utils" import ThemeSearch from "./ThemeSearch" import { Lists } from "../../Utils/Lists" +import { Strings } from "../../Utils/Strings" export default class SearchUtils { /** Applies special search terms, such as 'studio', 'osmcha', ... @@ -60,7 +61,7 @@ export default class SearchUtils { const queryParts = query .trim() .split(" ") - .map((q) => Utils.simplifyStringForSearch(q)) + .map((q) => Strings.simplifyStringForSearch(q)) let terms: string[] if (Array.isArray(keywords)) { terms = keywords @@ -74,7 +75,7 @@ export default class SearchUtils { const q = queryParts[i] let minDistance: number = 99 for (const term of termsAll) { - const d = Utils.levenshteinDistance(q, Utils.simplifyStringForSearch(term)) + const d = Utils.levenshteinDistance(q, Strings.simplifyStringForSearch(term)) if (d < minDistance) { minDistance = d } diff --git a/src/Logic/UIEventSource.ts b/src/Logic/UIEventSource.ts index c4af67e6e..3790be513 100644 --- a/src/Logic/UIEventSource.ts +++ b/src/Logic/UIEventSource.ts @@ -1,5 +1,6 @@ import { Utils } from "../Utils" import { Readable, Subscriber, Unsubscriber, Updater, Writable } from "svelte/store" +import { Lists } from "../Utils/Lists" /** * Various static utils @@ -66,7 +67,7 @@ export class Stores { stable.setData(undefined) return } - if (Utils.sameList(stable.data, list)) { + if (Lists.sameList(stable.data, list)) { return } stable.setData(list) diff --git a/src/Models/ThemeConfig/Conversion/Validation.ts b/src/Models/ThemeConfig/Conversion/Validation.ts index 4d4919442..c4383e497 100644 --- a/src/Models/ThemeConfig/Conversion/Validation.ts +++ b/src/Models/ThemeConfig/Conversion/Validation.ts @@ -25,6 +25,7 @@ import { eliCategory } from "../../RasterLayerProperties" import licenses from "../../../assets/generated/license_info.json" import { Strings } from "../../../Utils/Strings" import { Lists } from "../../../Utils/Lists" +import Objects from "../../../Utils/Objects" export class ValidateLanguageCompleteness extends DesugaringStep { private readonly _languages: string[] @@ -1085,11 +1086,8 @@ export class DetectDuplicatePresets extends DesugaringStep { const presetBTags = optimizedTags[j] const presetB = presets[j] if ( - Utils.SameObject(presetATags, presetBTags) && - Utils.sameList( - presetA.preciseInput.snapToLayers, - presetB.preciseInput.snapToLayers - ) + Objects.sameObject(presetATags, presetBTags) && + Lists.sameList(presetA.preciseInput.snapToLayers, presetB.preciseInput.snapToLayers) ) { context.err( `This theme has multiple presets with the same tags: ${presetATags.asHumanString( diff --git a/src/UI/Popup/TagRendering/TagRenderingEditable.svelte b/src/UI/Popup/TagRendering/TagRenderingEditable.svelte index c0b16be48..78f7c3006 100644 --- a/src/UI/Popup/TagRendering/TagRenderingEditable.svelte +++ b/src/UI/Popup/TagRendering/TagRenderingEditable.svelte @@ -12,6 +12,7 @@ import { Utils } from "../../../Utils" import { twMerge } from "tailwind-merge" import EditButton from "./EditButton.svelte" + import { Strings } from "../../../Utils/Strings" export let config: TagRenderingConfig export let tags: UIEventSource> @@ -83,7 +84,7 @@ onDestroy(highlightedRendering?.addCallbackAndRun(() => setHighlighting())) onDestroy(_htmlElement.addCallbackAndRun(() => setHighlighting())) } - let answerId = "answer-" + Utils.randomString(5) + let answerId = "answer-" + Strings.randomString(5) let debug = state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false) let apiState: Store = state?.osmConnection?.apiIsOnline ?? new ImmutableStore("online") diff --git a/src/UI/Studio/EditLayerState.ts b/src/UI/Studio/EditLayerState.ts index f73137ab5..594776421 100644 --- a/src/UI/Studio/EditLayerState.ts +++ b/src/UI/Studio/EditLayerState.ts @@ -1,12 +1,7 @@ import { ConfigMeta } from "./configMeta" import { Store, UIEventSource } from "../../Logic/UIEventSource" import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" -import { - Conversion, - ConversionMessage, - DesugaringContext, - Pipe, -} from "../../Models/ThemeConfig/Conversion/Conversion" +import { Conversion, ConversionMessage, DesugaringContext, Pipe } from "../../Models/ThemeConfig/Conversion/Conversion" import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer" import { PrevalidateTheme, ValidateLayer } from "../../Models/ThemeConfig/Conversion/Validation" import { AllSharedLayers } from "../../Customizations/AllSharedLayers" @@ -26,6 +21,7 @@ import { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderi import { ValidateTheme } from "../../Models/ThemeConfig/Conversion/ValidateTheme" import * as questions from "../../../public/assets/generated/layers/questions.json" import { Lists } from "../../Utils/Lists" +import { Strings } from "../../Utils/Strings" export interface HighlightedTagRendering { path: ReadonlyArray @@ -431,7 +427,7 @@ export default class EditLayerState extends EditJsonState { } if (!tr["id"] && !tr["override"]) { const qtr = tr - let id = "" + i + "_" + Utils.randomString(5) + let id = "" + i + "_" + Strings.randomString(5) if (qtr?.freeform?.key) { id = qtr?.freeform?.key } else if (qtr.mappings?.[0]?.if) { diff --git a/src/Utils.ts b/src/Utils.ts index c4f203b6d..123818e2c 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -1378,66 +1378,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } element.scrollIntoView({ behavior: "smooth", block: "nearest" }) } - - /** - * Returns true if the contents of `a` are the same (and in the same order) as `b`. - * Might have false negatives in some cases - * @param a - * @param b - */ - public static sameList(a: ReadonlyArray, b: ReadonlyArray) { - if (a == b) { - return true - } - if (a === undefined || a === null || b === undefined || b === null) { - return false - } - if (a.length !== b.length) { - return false - } - for (let i = 0; i < a.length; i++) { - const ai = a[i] - const bi = b[i] - if (ai == bi) { - continue - } - if (ai === bi) { - continue - } - return false - } - return true - } - - public static SameObject(a: T, b: T, ignoreKeys?: string[]): boolean { - if (a === b) { - return true - } - if (a === undefined || a === null || b === null || b === undefined) { - return false - } - if (typeof a === "object" && typeof b === "object") { - for (const aKey in a) { - if (!(aKey in b)) { - return false - } - } - - for (const bKey in b) { - if (!(bKey in a)) { - return false - } - } - for (const k in a) { - if (!Utils.SameObject(a[k], b[k])) { - return false - } - } - return true - } - return false - } - /** * * Utils.splitIntoSubstitutionParts("abc") // => [{message: "abc"}] @@ -1510,45 +1450,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be filename: path.substring(path.lastIndexOf("/") + 1), } } - - /** - * Removes accents from a string - * @param str - * @constructor - * - * Utils.RemoveDiacritics("bâtiments") // => "batiments" - * Utils.RemoveDiacritics(undefined) // => undefined - */ - public static RemoveDiacritics(str?: string): string { - // See #1729 - if (!str) { - return str - } - return str.normalize("NFD").replace(/\p{Diacritic}/gu, "") - } - - /** - * Simplifies a string to increase the chance of a match - * @param str - * Utils.simplifyStringForSearch("abc def; ghi 564") // => "abcdefghi564" - * Utils.simplifyStringForSearch("âbc déf; ghi 564") // => "abcdefghi564" - * Utils.simplifyStringForSearch(undefined) // => undefined - */ - public static simplifyStringForSearch(str: string): string { - return Utils.RemoveDiacritics(str) - ?.toLowerCase() - ?.replace(/[^a-z0-9]/g, "") - } - - public static randomString(length: number): string { - let result = "" - for (let i = 0; i < length; i++) { - const chr = Math.random().toString(36).substr(2, 3) - result += chr - } - return result - } - /** * Recursively rewrites all keys from `+key`, `key+` and `=key` into `key * diff --git a/src/Utils/Lists.ts b/src/Utils/Lists.ts index 3e0b71b57..7d06a395a 100644 --- a/src/Utils/Lists.ts +++ b/src/Utils/Lists.ts @@ -43,12 +43,15 @@ export class Lists { * 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): NonNullable + public static dedup(arr: NonNullable>): NonNullable 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 + public static dedup(arr: ReadonlyArray | undefined): string[] | undefined + public static dedup(arr: ReadonlyArray): string[] { + if (arr === undefined) { + return undefined + } + if (arr === null) { + return null } const newArr = [] for (const string of arr) { @@ -60,8 +63,8 @@ export class Lists { } public static dedupT(arr: ReadonlyArray): T[] - public static dedupT(arr: null): null - public static dedupT(arr: undefined): undefined + public static dedupT(arr: null): null + public static dedupT(arr: undefined): undefined public static dedupT(arr: ReadonlyArray): T[] { if (arr === undefined) { return undefined @@ -160,7 +163,7 @@ export class Lists { * Lists.duplicates(["a", "b","c","b","b"] // => ["b"] * */ - public static duplicates(arr: string[]): string[] { + public static duplicates(arr: ReadonlyArray): string[] { if (arr === undefined) { return undefined } @@ -175,4 +178,34 @@ export class Lists { return Array.from(duplicates) } + /** + * Returns true if the contents of `a` are the same (and in the same order) as `b`. + * Might have false negatives in some cases + * @param a + * @param b + */ + public static sameList(a: ReadonlyArray, b: ReadonlyArray): boolean { + if (a == b) { + return true + } + if (a === undefined || a === null || b === undefined || b === null) { + return false + } + if (a.length !== b.length) { + return false + } + for (let i = 0; i < a.length; i++) { + const ai = a[i] + const bi = b[i] + if (ai == bi) { + continue + } + if (ai === bi) { + continue + } + return false + } + return true + } + } diff --git a/src/Utils/Objects.ts b/src/Utils/Objects.ts new file mode 100644 index 000000000..1d8b95c15 --- /dev/null +++ b/src/Utils/Objects.ts @@ -0,0 +1,33 @@ +/** + * Various object-related utils + */ +export default class Objects { + public static sameObject(a: T, b: T, ignoreKeys?: string[]): boolean { + if (a === b) { + return true + } + if (a === undefined || a === null || b === null || b === undefined) { + return false + } + if (typeof a === "object" && typeof b === "object") { + for (const aKey in a) { + if (!(aKey in b)) { + return false + } + } + + for (const bKey in b) { + if (!(bKey in a)) { + return false + } + } + for (const k in a) { + if (!Objects.sameObject(a[k], b[k])) { + return false + } + } + return true + } + return false + } +} diff --git a/src/Utils/Strings.ts b/src/Utils/Strings.ts index ae619a3e4..709014115 100644 --- a/src/Utils/Strings.ts +++ b/src/Utils/Strings.ts @@ -21,4 +21,42 @@ export class Strings { 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 } + + /** + * Removes accents from a string + * @param str + * @constructor + * + * Strings.removeDiacritics("bâtiments") // => "batiments" + * Strings.removeDiacritics(undefined) // => undefined + */ + public static removeDiacritics(str?: string): string { + // See #1729 + if (!str) { + return str + } + return str.normalize("NFD").replace(/\p{Diacritic}/gu, "") + } + + /** + * Simplifies a string to increase the chance of a match + * @param str + * Strings.simplifyStringForSearch("abc def; ghi 564") // => "abcdefghi564" + * Strings.simplifyStringForSearch("âbc déf; ghi 564") // => "abcdefghi564" + * Strings.simplifyStringForSearch(undefined) // => undefined + */ + public static simplifyStringForSearch(str: string): string { + return Strings.removeDiacritics(str) + ?.toLowerCase() + ?.replace(/[^a-z0-9]/g, "") + } + + public static randomString(length: number): string { + let result = "" + for (let i = 0; i < length; i++) { + const chr = Math.random().toString(36).substr(2, 3) + result += chr + } + return result + } }