diff --git a/.github/workflows/deploy_pietervdvn.yml b/.github/workflows/deploy_pietervdvn.yml index 00d2dedbc..f787b077d 100644 --- a/.github/workflows/deploy_pietervdvn.yml +++ b/.github/workflows/deploy_pietervdvn.yml @@ -3,7 +3,7 @@ on: push: branches: - develop - - feature/vite + - feature/svelte jobs: build: runs-on: ubuntu-latest diff --git a/.prettierrc.json b/.prettierrc.json index 7acf4f02d..65a69c06b 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,4 +1,6 @@ { "semi": false, - "printWidth": 100 + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] } diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 68d524f21..8ca85028f 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,6 +2,8 @@ "recommendations": [ "esbenp.prettier-vscode", "eamodio.gitlens", - "GitHub.vscode-pull-request-github" + "GitHub.vscode-pull-request-github", + "svelte.svelte-vscode", + "bradlc.vscode-tailwindcss" ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 25b1f4dbe..26b59e881 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,21 +1,21 @@ { - "json.schemas": [ - { - "fileMatch": [ - "/assets/layers/*/*.json", - "!/assets/layers/*/license_info.json" - ], - "url": "./Docs/Schemas/LayerConfigJson.schema.json" - }, - { - "fileMatch": [ - "/assets/themes/*/*.json", - "!/assets/themes/*/license_info.json" - ], - "url": "./Docs/Schemas/LayoutConfigJson.schema.json" - } - ], - "editor.tabSize": 2, - "files.autoSave": "onFocusChange", - "search.useIgnoreFiles": true - } \ No newline at end of file + "json.schemas": [ + { + "fileMatch": ["/assets/layers/*/*.json", "!/assets/layers/*/license_info.json"], + "url": "./Docs/Schemas/LayerConfigJson.schema.json" + }, + { + "fileMatch": ["/assets/themes/*/*.json", "!/assets/themes/*/license_info.json"], + "url": "./Docs/Schemas/LayoutConfigJson.schema.json" + } + ], + "editor.tabSize": 2, + "files.autoSave": "onFocusChange", + "search.useIgnoreFiles": true, + "css.lint.unknownAtRules": "ignore", + "scss.lint.unknownAtRules": "ignore", + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[svelte]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/Customizations/SharedTagRenderings.ts b/Customizations/SharedTagRenderings.ts index 11b8954f4..85877b7e5 100644 --- a/Customizations/SharedTagRenderings.ts +++ b/Customizations/SharedTagRenderings.ts @@ -1,5 +1,4 @@ import questions from "../assets/tagRenderings/questions.json" -import icons from "../assets/tagRenderings/icons.json" import { Utils } from "../Utils" import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson" @@ -14,11 +13,9 @@ export default class SharedTagRenderings { SharedTagRenderings.generatedSharedFields() public static SharedTagRenderingJson: Map = SharedTagRenderings.generatedSharedFieldsJsons() - public static SharedIcons: Map = - SharedTagRenderings.generatedSharedFields(true) - private static generatedSharedFields(iconsOnly = false): Map { - const configJsons = SharedTagRenderings.generatedSharedFieldsJsons(iconsOnly) + private static generatedSharedFields(): Map { + const configJsons = SharedTagRenderings.generatedSharedFieldsJsons() const d = new Map() for (const key of Array.from(configJsons.keys())) { try { @@ -31,7 +28,7 @@ export default class SharedTagRenderings { console.error( "BUG: could not parse", key, - " from questions.json or icons.json - this error happened during the build step of the SharedTagRenderings", + " from questions.json - this error happened during the build step of the SharedTagRenderings", e ) } @@ -40,24 +37,14 @@ export default class SharedTagRenderings { return d } - private static generatedSharedFieldsJsons( - iconsOnly = false - ): Map { + private static generatedSharedFieldsJsons(): Map { const dict = new Map() - if (!iconsOnly) { - for (const key in questions) { - if (key === "id") { - continue - } - dict.set(key, questions[key]) - } - } - for (const key in icons) { + for (const key in questions) { if (key === "id") { continue } - dict.set(key, icons[key]) + dict.set(key, questions[key]) } dict.forEach((value, key) => { diff --git a/Logic/Actors/GeoLocationHandler.ts b/Logic/Actors/GeoLocationHandler.ts index 9c6fbce3b..a5f51959e 100644 --- a/Logic/Actors/GeoLocationHandler.ts +++ b/Logic/Actors/GeoLocationHandler.ts @@ -2,8 +2,10 @@ import { QueryParameters } from "../Web/QueryParameters" import { BBox } from "../BBox" import Constants from "../../Models/Constants" import { GeoLocationPointProperties, GeoLocationState } from "../State/GeoLocationState" -import State from "../../State" import { UIEventSource } from "../UIEventSource" +import Loc from "../../Models/Loc" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" +import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource" /** * The geolocation-handler takes a map-location and a geolocation state. @@ -12,12 +14,24 @@ import { UIEventSource } from "../UIEventSource" */ export default class GeoLocationHandler { public readonly geolocationState: GeoLocationState - private readonly _state: State + private readonly _state: { + currentUserLocation: SimpleFeatureSource + layoutToUse: LayoutConfig + locationControl: UIEventSource + selectedElement: UIEventSource + leafletMap?: UIEventSource + } public readonly mapHasMoved: UIEventSource = new UIEventSource(false) constructor( geolocationState: GeoLocationState, - state: State // { locationControl: UIEventSource, selectedElement: UIEventSource, leafletMap?: UIEventSource }) + state: { + locationControl: UIEventSource + currentUserLocation: SimpleFeatureSource + layoutToUse: LayoutConfig + selectedElement: UIEventSource + leafletMap?: UIEventSource + } ) { this.geolocationState = geolocationState this._state = state diff --git a/Logic/DetermineLayout.ts b/Logic/DetermineLayout.ts index d0d166ab8..020467758 100644 --- a/Logic/DetermineLayout.ts +++ b/Logic/DetermineLayout.ts @@ -194,8 +194,7 @@ export default class DetermineLayout { let { errors } = new ValidateThemeAndLayers( new DoesImageExist(new Set(), (_) => true), "", - false, - SharedTagRenderings.SharedTagRendering + false ).convert(json, "validation") if (errors.length > 0) { throw "Detected errors: " + errors.join("\n") diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index 457656538..409911f8e 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -185,16 +185,20 @@ export class GeoOperations { * GeoOperations.inside([1.42822265625, 48.61838518688487], multiPolygon) // => false * GeoOperations.inside([4.02099609375, 47.81315451752768], multiPolygon) // => false */ - public static inside(pointCoordinate, feature): boolean { + public static inside( + pointCoordinate: [number, number] | Feature, + feature: Feature + ): boolean { // ray-casting algorithm based on // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html if (feature.geometry.type === "Point") { + // The feature that should 'contain' pointCoordinate is a point itself, so it cannot contain anything return false } - if (pointCoordinate.geometry !== undefined) { - pointCoordinate = pointCoordinate.geometry.coordinates + if (pointCoordinate["geometry"] !== undefined) { + pointCoordinate = pointCoordinate["geometry"].coordinates } const x: number = pointCoordinate[0] @@ -203,6 +207,7 @@ export class GeoOperations { if (feature.geometry.type === "MultiPolygon") { const coordinatess = feature.geometry.coordinates for (const coordinates of coordinatess) { + // @ts-ignore const inThisPolygon = GeoOperations.pointInPolygonCoordinates(x, y, coordinates) if (inThisPolygon) { return true @@ -212,6 +217,7 @@ export class GeoOperations { } if (feature.geometry.type === "Polygon") { + // @ts-ignore return GeoOperations.pointInPolygonCoordinates(x, y, feature.geometry.coordinates) } diff --git a/Logic/Osm/OsmPreferences.ts b/Logic/Osm/OsmPreferences.ts index 8a37690ab..cd10b414f 100644 --- a/Logic/Osm/OsmPreferences.ts +++ b/Logic/Osm/OsmPreferences.ts @@ -1,8 +1,6 @@ import { UIEventSource } from "../UIEventSource" import UserDetails, { OsmConnection } from "./OsmConnection" import { Utils } from "../../Utils" -import { DomEvent } from "leaflet" -import preventDefault = DomEvent.preventDefault export class OsmPreferences { public preferences = new UIEventSource>({}, "all-osm-preferences") diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index f39963ead..eb8479e7f 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -58,6 +58,49 @@ export class SimpleMetaTagger { } } +export class ReferencingWaysMetaTagger extends SimpleMetaTagger { + /** + * Disable this metatagger, e.g. for caching or tests + * This is a bit a work-around + */ + public static enabled = true + constructor() { + super( + { + keys: ["_referencing_ways"], + isLazy: true, + doc: "_referencing_ways contains - for a node - which ways use this this node as point in their geometry. ", + }, + (feature, _, __, state) => { + if (!ReferencingWaysMetaTagger.enabled) { + return false + } + //this function has some extra code to make it work in SimpleAddUI.ts to also work for newly added points + const id = feature.properties.id + if (!id.startsWith("node/")) { + return false + } + console.trace("Downloading referencing ways for", feature.properties.id) + OsmObject.DownloadReferencingWays(id).then((referencingWays) => { + const currentTagsSource = state.allElements?.getEventSourceById(id) ?? [] + const wayIds = referencingWays.map((w) => "way/" + w.id) + wayIds.sort() + const wayIdsStr = wayIds.join(";") + if ( + wayIdsStr !== "" && + currentTagsSource.data["_referencing_ways"] !== wayIdsStr + ) { + currentTagsSource.data["_referencing_ways"] = wayIdsStr + currentTagsSource.ping() + } + }) + + return true + } + ) + } +} + export class CountryTagger extends SimpleMetaTagger { private static readonly coder = new CountryCoder( Constants.countryCoderEndpoint, @@ -492,33 +535,7 @@ export default class SimpleMetaTaggers { } ) - public static referencingWays = new SimpleMetaTagger( - { - keys: ["_referencing_ways"], - isLazy: true, - includesDates: true, - doc: "_referencing_ways contains - for a node - which ways use this this node as point in their geometry. ", - }, - (feature, _, __, state) => { - //this function has some extra code to make it work in SimpleAddUI.ts to also work for newly added points - const id = feature.properties.id - if (!id.startsWith("node/")) { - return false - } - OsmObject.DownloadReferencingWays(id).then((referencingWays) => { - const currentTagsSource = state.allElements?.getEventSourceById(id) ?? [] - const wayIds = referencingWays.map((w) => "way/" + w.id) - wayIds.sort() - const wayIdsStr = wayIds.join(";") - if (wayIdsStr !== "" && currentTagsSource.data["_referencing_ways"] !== wayIdsStr) { - currentTagsSource.data["_referencing_ways"] = wayIdsStr - currentTagsSource.ping() - } - }) - - return true - } - ) + public static referencingWays = new ReferencingWaysMetaTagger() public static metatags: SimpleMetaTagger[] = [ SimpleMetaTaggers.latlon, diff --git a/Logic/State/FeatureSwitchState.ts b/Logic/State/FeatureSwitchState.ts index e690307d0..d012ef3de 100644 --- a/Logic/State/FeatureSwitchState.ts +++ b/Logic/State/FeatureSwitchState.ts @@ -18,6 +18,7 @@ export default class FeatureSwitchState { public readonly featureSwitchBackgroundSelection: UIEventSource public readonly featureSwitchAddNew: UIEventSource public readonly featureSwitchWelcomeMessage: UIEventSource + public readonly featureSwitchCommunityIndex: UIEventSource public readonly featureSwitchExtraLinkEnabled: UIEventSource public readonly featureSwitchMoreQuests: UIEventSource public readonly featureSwitchShareScreen: UIEventSource @@ -91,6 +92,11 @@ export default class FeatureSwitchState { () => true, "Disables/enables the help menu or welcome message" ) + this.featureSwitchCommunityIndex = featSw( + "fs-community-index", + () => true, + "Disables/enables the button to get in touch with the community" + ) this.featureSwitchExtraLinkEnabled = featSw( "fs-iframe-popout", (_) => true, diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index ac286171c..f7ffbb916 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -1,4 +1,5 @@ import { Utils } from "../Utils" +import { Readable, Subscriber, Unsubscriber } from "svelte/store" /** * Various static utils @@ -88,7 +89,7 @@ export class Stores { } } -export abstract class Store { +export abstract class Store implements Readable { abstract readonly data: T /** @@ -113,6 +114,18 @@ export abstract class Store { abstract map(f: (t: T) => J): Store abstract map(f: (t: T) => J, extraStoresToWatch: Store[]): Store + public mapD(f: (t: T) => J, extraStoresToWatch?: Store[]): Store { + return this.map((t) => { + if (t === undefined) { + return undefined + } + if (t === null) { + return null + } + return f(t) + }, extraStoresToWatch) + } + /** * Add a callback function which will run on future data changes */ @@ -258,6 +271,17 @@ export abstract class Store { } }) } + + /** + * Same as 'addCallbackAndRun', added to be compatible with Svelte + * @param run + * @param invalidate + */ + public subscribe(run: Subscriber & ((value: T) => void), invalidate?): Unsubscriber { + // We don't need to do anything with 'invalidate', see + // https://github.com/sveltejs/svelte/issues/3859 + return this.addCallbackAndRun(run) + } } export class ImmutableStore extends Store { diff --git a/Models/BaseLayer.ts b/Models/BaseLayer.ts index dd249998b..1e4c2a933 100644 --- a/Models/BaseLayer.ts +++ b/Models/BaseLayer.ts @@ -1,9 +1,7 @@ -import { TileLayer } from "leaflet" - export default interface BaseLayer { id: string name: string - layer: () => TileLayer + layer: () => any /*leaflet.TileLayer - not importing as it breaks scripts*/ max_zoom: number min_zoom: number feature: any diff --git a/Models/ThemeConfig/Conversion/FixImages.ts b/Models/ThemeConfig/Conversion/FixImages.ts index a873abb6d..e6116c90c 100644 --- a/Models/ThemeConfig/Conversion/FixImages.ts +++ b/Models/ThemeConfig/Conversion/FixImages.ts @@ -5,18 +5,25 @@ import metapaths from "../../../assets/layoutconfigmeta.json" import tagrenderingmetapaths from "../../../assets/questionabletagrenderingconfigmeta.json" import Translations from "../../../UI/i18n/Translations" -export class ExtractImages extends Conversion { +export class ExtractImages extends Conversion< + LayoutConfigJson, + { path: string; context: string }[] +> { private _isOfficial: boolean - private _sharedTagRenderings: Map + private _sharedTagRenderings: Set private static readonly layoutMetaPaths = metapaths.filter( (mp) => ExtractImages.mightBeTagRendering(mp) || - (mp.typeHint !== undefined && (mp.typeHint === "image" || mp.typeHint === "icon")) + (mp.typeHint !== undefined && + (mp.typeHint === "image" || + mp.typeHint === "icon" || + mp.typeHint === "image[]" || + mp.typeHint === "icon[]")) ) private static readonly tagRenderingMetaPaths = tagrenderingmetapaths - constructor(isOfficial: boolean, sharedTagRenderings: Map) { + constructor(isOfficial: boolean, sharedTagRenderings: Set) { super("Extract all images from a layoutConfig using the meta paths.", [], "ExctractImages") this._isOfficial = isOfficial this._sharedTagRenderings = sharedTagRenderings @@ -64,23 +71,23 @@ export class ExtractImages extends Conversion { * ] * } * ] - * }, "test").result; + * }, "test").result.map(i => i.path); * images.length // => 2 - * images.findIndex(img => img == "./assets/layers/bike_parking/staple.svg") // => 0 - * images.findIndex(img => img == "./assets/layers/bike_parking/bollard.svg") // => 1 + * images.findIndex(img => img == "./assets/layers/bike_parking/staple.svg") >= 0 // => true + * images.findIndex(img => img == "./assets/layers/bike_parking/bollard.svg") >= 0 // => true * * // should not pickup rotation, should drop color - * const images = new ExtractImages(true, new Map()).convert({"layers": [{mapRendering: [{"location": ["point", "centroid"],"icon": "pin:black",rotation: 180,iconSize: "40,40,center"}]}] + * const images = new ExtractImages(true, new Set()).convert({"layers": [{mapRendering: [{"location": ["point", "centroid"],"icon": "pin:black",rotation: 180,iconSize: "40,40,center"}]}] * }, "test").result * images.length // => 1 - * images[0] // => "pin" + * images[0].path // => "pin" * */ convert( json: LayoutConfigJson, context: string - ): { result: string[]; errors: string[]; warnings: string[] } { - const allFoundImages: string[] = [] + ): { result: { path: string; context: string }[]; errors: string[]; warnings: string[] } { + const allFoundImages: { path: string; context: string }[] = [] const errors = [] const warnings = [] for (const metapath of ExtractImages.layoutMetaPaths) { @@ -108,7 +115,7 @@ export class ExtractImages extends Conversion { continue } - allFoundImages.push(foundImage) + allFoundImages.push({ path: foundImage, context: context + "." + path }) } else { // This is a tagRendering. // Either every rendered value might be an icon @@ -137,7 +144,10 @@ export class ExtractImages extends Conversion { JSON.stringify(img.leaf) ) } else { - allFoundImages.push(img.leaf) + allFoundImages.push({ + path: img.leaf, + context: context + "." + path, + }) } } if (!allRenderedValuesAreImages && isImage) { @@ -146,7 +156,12 @@ export class ExtractImages extends Conversion { ...Translations.T( img.leaf, "extract_images from " + img.path.join(".") - ).ExtractImages(false) + ) + .ExtractImages(false) + .map((path) => ({ + path, + context: context + "." + path, + })) ) } } @@ -161,20 +176,30 @@ export class ExtractImages extends Conversion { ) continue } - allFoundImages.push(foundElement.leaf) + if (typeof foundElement.leaf !== "string") { + continue + } + allFoundImages.push({ + context: context + "." + foundElement.path.join("."), + path: foundElement.leaf, + }) } } } - const splitParts = [] - .concat( - ...Utils.NoNull(allFoundImages) - .map((img) => img["path"] ?? img) - .map((img) => img.split(";")) + const cleanedImages: { path: string; context: string }[] = [] + + for (const foundImage of allFoundImages) { + // Split "circle:white;./assets/layers/.../something.svg" into ["circle", "./assets/layers/.../something.svg"] + const allPaths = Utils.NoNull( + Utils.NoEmpty(foundImage.path?.split(";")?.map((part) => part.split(":")[0])) ) - .map((img) => img.split(":")[0]) - .filter((img) => img !== "") - return { result: Utils.Dedup(splitParts), errors, warnings } + for (const path of allPaths) { + cleanedImages.push({ path, context: foundImage.context }) + } + } + + return { result: cleanedImages, errors, warnings } } } diff --git a/Models/ThemeConfig/Conversion/PrepareLayer.ts b/Models/ThemeConfig/Conversion/PrepareLayer.ts index fb00edb80..10b021c75 100644 --- a/Models/ThemeConfig/Conversion/PrepareLayer.ts +++ b/Models/ThemeConfig/Conversion/PrepareLayer.ts @@ -7,29 +7,24 @@ import { FirstOf, Fuse, On, - SetDefault, -} from "./Conversion" -import { LayerConfigJson } from "../Json/LayerConfigJson" -import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" -import { Utils } from "../../../Utils" -import RewritableConfigJson from "../Json/RewritableConfigJson" -import SpecialVisualizations from "../../../UI/SpecialVisualizations" -import Translations from "../../../UI/i18n/Translations" -import { Translation } from "../../../UI/i18n/Translation" -import tagrenderingconfigmeta from "../../../assets/tagrenderingconfigmeta.json" -import { AddContextToTranslations } from "./AddContextToTranslations" -import FilterConfigJson from "../Json/FilterConfigJson" -import predifined_filters from "../../../assets/layers/filters/filters.json" + SetDefault +} from "./Conversion"; +import { LayerConfigJson } from "../Json/LayerConfigJson"; +import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"; +import { Utils } from "../../../Utils"; +import RewritableConfigJson from "../Json/RewritableConfigJson"; +import SpecialVisualizations from "../../../UI/SpecialVisualizations"; +import Translations from "../../../UI/i18n/Translations"; +import { Translation } from "../../../UI/i18n/Translation"; +import tagrenderingconfigmeta from "../../../assets/tagrenderingconfigmeta.json"; +import { AddContextToTranslations } from "./AddContextToTranslations"; +import FilterConfigJson from "../Json/FilterConfigJson"; +import predifined_filters from "../../../assets/layers/filters/filters.json"; +import { TagConfigJson } from "../Json/TagConfigJson"; +import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"; +import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"; class ExpandFilter extends DesugaringStep { - private static load_filters(): Map { - let filters = new Map() - for (const filter of predifined_filters.filter) { - filters.set(filter.id, filter) - } - return filters - } - private static readonly predefinedFilters = ExpandFilter.load_filters() constructor() { @@ -40,6 +35,14 @@ class ExpandFilter extends DesugaringStep { ) } + private static load_filters(): Map { + let filters = new Map() + for (const filter of predifined_filters.filter) { + filters.set(filter.id, filter) + } + return filters + } + convert( json: LayerConfigJson, context: string @@ -128,6 +131,37 @@ class ExpandTagRendering extends Conversion< } private lookup(name: string): TagRenderingConfigJson[] { + const direct = this.directLookup(name) + if (direct === undefined) { + return undefined + } + const result: TagRenderingConfigJson[] = [] + for (const tagRenderingConfigJson of direct) { + if (tagRenderingConfigJson["builtin"] !== undefined) { + let nm: string | string[] = tagRenderingConfigJson["builtin"] + let indirect: TagRenderingConfigJson[] + if (typeof nm === "string") { + indirect = this.lookup(nm) + } else { + indirect = [].concat(...nm.map((n) => this.lookup(n))) + } + for (let foundTr of indirect) { + foundTr = Utils.Clone(foundTr) + Utils.Merge(tagRenderingConfigJson["override"] ?? {}, foundTr) + foundTr.id = tagRenderingConfigJson.id ?? foundTr.id + result.push(foundTr) + } + } else { + result.push(tagRenderingConfigJson) + } + } + return result + } + + /** + * Looks up a tagRendering based on the name. + */ + private directLookup(name: string): TagRenderingConfigJson[] { const state = this._state if (state.tagRenderings.has(name)) { return [state.tagRenderings.get(name)] @@ -747,6 +781,79 @@ export class RewriteSpecial extends DesugaringStep { } } +class ExpandIconBadges extends DesugaringStep { + private _state: DesugaringContext + private _layer: LayerConfigJson + private _expand: ExpandTagRendering + + constructor(state: DesugaringContext, layer: LayerConfigJson) { + super("Expands shorthand properties on iconBadges", ["iconBadges"], "ExpandIconBadges") + this._state = state + this._layer = layer + this._expand = new ExpandTagRendering(state, layer) + } + + convert( + json: PointRenderingConfigJson | LineRenderingConfigJson, + context: string + ): { + result: PointRenderingConfigJson | LineRenderingConfigJson + errors?: string[] + warnings?: string[] + information?: string[] + } { + if (!json["iconBadges"]) { + return { result: json } + } + const badgesJson = (json).iconBadges + + const iconBadges: { if: TagConfigJson; then: string | TagRenderingConfigJson }[] = [] + + const errs: string[] = [] + const warns: string[] = [] + for (let i = 0; i < badgesJson.length; i++) { + const iconBadge: { if: TagConfigJson; then: string | TagRenderingConfigJson } = + badgesJson[i] + const { errors, result, warnings } = this._expand.convert( + iconBadge.then, + context + ".iconBadges[" + i + "]" + ) + errs.push(...errors) + warns.push(...warnings) + if (result === undefined) { + iconBadges.push(iconBadge) + continue + } + + iconBadges.push( + ...result.map((resolved) => ({ + if: iconBadge.if, + then: resolved, + })) + ) + } + + return { + result: { ...json, iconBadges }, + errors: errs, + warnings: warns, + } + } +} + +class PreparePointRendering extends Fuse { + constructor(state: DesugaringContext, layer: LayerConfigJson) { + super( + "Prepares point renderings by expanding 'icon' and 'iconBadges'", + new On( + "icon", + new FirstOf(new ExpandTagRendering(state, layer, { applyCondition: false })) + ), + new ExpandIconBadges(state, layer) + ) + } +} + export class PrepareLayer extends Fuse { constructor(state: DesugaringContext) { super( @@ -755,19 +862,11 @@ export class PrepareLayer extends Fuse { new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)), new On("tagRenderings", (layer) => new Concat(new ExpandTagRendering(state, layer))), new On("mapRendering", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)), - new On( + new On<(PointRenderingConfigJson | LineRenderingConfigJson)[], LayerConfigJson>( "mapRendering", - (layer) => - new Each( - new On( - "icon", - new FirstOf( - new ExpandTagRendering(state, layer, { applyCondition: false }) - ) - ) - ) + (layer) => new Each(new PreparePointRendering(state, layer)) ), - new SetDefault("titleIcons", ["defaults"]), + new SetDefault("titleIcons", ["icons.defaults"]), new On("titleIcons", (layer) => new Concat(new ExpandTagRendering(state, layer))), new ExpandFilter() ) diff --git a/Models/ThemeConfig/Conversion/Validation.ts b/Models/ThemeConfig/Conversion/Validation.ts index bab2435d4..a155c41cf 100644 --- a/Models/ThemeConfig/Conversion/Validation.ts +++ b/Models/ThemeConfig/Conversion/Validation.ts @@ -60,13 +60,16 @@ class ValidateLanguageCompleteness extends DesugaringStep { export class DoesImageExist extends DesugaringStep { private readonly _knownImagePaths: Set + private readonly _ignore?: Set private readonly doesPathExist: (path: string) => boolean = undefined constructor( knownImagePaths: Set, - checkExistsSync: (path: string) => boolean = undefined + checkExistsSync: (path: string) => boolean = undefined, + ignore?: Set ) { super("Checks if an image exists", [], "DoesImageExist") + this._ignore = ignore this._knownImagePaths = knownImagePaths this.doesPathExist = checkExistsSync } @@ -75,6 +78,10 @@ export class DoesImageExist extends DesugaringStep { image: string, context: string ): { result: string; errors?: string[]; warnings?: string[]; information?: string[] } { + if (this._ignore?.has(image)) { + return { result: image } + } + const errors = [] const warnings = [] const information = [] @@ -124,20 +131,23 @@ class ValidateTheme extends DesugaringStep { */ private readonly _path?: string private readonly _isBuiltin: boolean - private _sharedTagRenderings: Map + //private readonly _sharedTagRenderings: Map private readonly _validateImage: DesugaringStep + private readonly _extractImages: ExtractImages = undefined constructor( doesImageExist: DoesImageExist, path: string, isBuiltin: boolean, - sharedTagRenderings: Map + sharedTagRenderings?: Set ) { super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme") this._validateImage = doesImageExist this._path = path this._isBuiltin = isBuiltin - this._sharedTagRenderings = sharedTagRenderings + if (sharedTagRenderings) { + this._extractImages = new ExtractImages(this._isBuiltin, sharedTagRenderings) + } } convert( @@ -169,13 +179,10 @@ class ValidateTheme extends DesugaringStep { } } } - if (this._isBuiltin) { + if (this._isBuiltin && this._extractImages !== undefined) { // Check images: are they local, are the licenses there, is the theme icon square, ... - const images = new ExtractImages( - this._isBuiltin, - this._sharedTagRenderings - ).convertStrict(json, "validation") - const remoteImages = images.filter((img) => img.indexOf("http") == 0) + const images = this._extractImages.convertStrict(json, "validation") + const remoteImages = images.filter((img) => img.path.indexOf("http") == 0) for (const remoteImage of remoteImages) { errors.push( "Found a remote image: " + @@ -187,8 +194,8 @@ class ValidateTheme extends DesugaringStep { } for (const image of images) { this._validateImage.convertJoin( - image, - context === undefined ? "" : ` in a layer defined in the theme ${context}`, + image.path, + context === undefined ? "" : ` in the theme ${context} at ${image.context}`, errors, warnings, information @@ -268,7 +275,7 @@ export class ValidateThemeAndLayers extends Fuse { doesImageExist: DoesImageExist, path: string, isBuiltin: boolean, - sharedTagRenderings: Map + sharedTagRenderings?: Set ) { super( "Validates a theme and the contained layers", @@ -901,53 +908,6 @@ export class DetectDuplicateFilters extends DesugaringStep<{ ) } - /** - * Add all filter options into 'perOsmTag' - */ - private addLayerFilters( - layer: LayerConfigJson, - perOsmTag: Map< - string, - { - layer: LayerConfigJson - layout: LayoutConfigJson | undefined - filter: FilterConfigJson - }[] - >, - layout?: LayoutConfigJson | undefined - ): void { - if (layer.filter === undefined || layer.filter === null) { - return - } - if (layer.filter["sameAs"] !== undefined) { - return - } - for (const filter of <(string | FilterConfigJson)[]>layer.filter) { - if (typeof filter === "string") { - continue - } - - if (filter["#"]?.indexOf("ignore-possible-duplicate") >= 0) { - continue - } - - for (const option of filter.options) { - if (option.osmTags === undefined) { - continue - } - const key = JSON.stringify(option.osmTags) - if (!perOsmTag.has(key)) { - perOsmTag.set(key, []) - } - perOsmTag.get(key).push({ - layer, - filter, - layout, - }) - } - } - } - convert( json: { layers: LayerConfigJson[]; themes: LayoutConfigJson[] }, context: string @@ -1014,4 +974,51 @@ export class DetectDuplicateFilters extends DesugaringStep<{ information, } } + + /** + * Add all filter options into 'perOsmTag' + */ + private addLayerFilters( + layer: LayerConfigJson, + perOsmTag: Map< + string, + { + layer: LayerConfigJson + layout: LayoutConfigJson | undefined + filter: FilterConfigJson + }[] + >, + layout?: LayoutConfigJson | undefined + ): void { + if (layer.filter === undefined || layer.filter === null) { + return + } + if (layer.filter["sameAs"] !== undefined) { + return + } + for (const filter of <(string | FilterConfigJson)[]>layer.filter) { + if (typeof filter === "string") { + continue + } + + if (filter["#"]?.indexOf("ignore-possible-duplicate") >= 0) { + continue + } + + for (const option of filter.options) { + if (option.osmTags === undefined) { + continue + } + const key = JSON.stringify(option.osmTags) + if (!perOsmTag.has(key)) { + perOsmTag.set(key, []) + } + perOsmTag.get(key).push({ + layer, + filter, + layout, + }) + } + } + } } diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index fdca8d638..9ce60b479 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -8,7 +8,22 @@ import { ExtractImages } from "./Conversion/FixImages" import ExtraLinkConfig from "./ExtraLinkConfig" import { Utils } from "../../Utils" import used_languages from "../../assets/generated/used_languages.json" -export default class LayoutConfig { + +/** + * Minimal information about a theme + **/ +export class LayoutInformation { + id: string + icon: string + title: any + shortDescription: any + definition?: any + mustHaveLanguage?: boolean + hideFromOverview?: boolean + keywords?: any[] +} + +export default class LayoutConfig implements LayoutInformation { public static readonly defaultSocialImage = "assets/SocialImage.png" public readonly id: string public readonly credits?: string @@ -82,10 +97,12 @@ export default class LayoutConfig { this.credits = json.credits this.language = json.mustHaveLanguage ?? Object.keys(json.title) this.usedImages = Array.from( - new ExtractImages(official, undefined).convertStrict( - json, - "while extracting the images of " + json.id + " " + context ?? "" - ) + new ExtractImages(official, undefined) + .convertStrict( + json, + "while extracting the images of " + json.id + " " + context ?? "" + ) + .map((i) => i.path) ).sort() { if (typeof json.title === "string") { diff --git a/Models/ThemeConfig/PointRenderingConfig.ts b/Models/ThemeConfig/PointRenderingConfig.ts index 03242f33c..8c3712f1f 100644 --- a/Models/ThemeConfig/PointRenderingConfig.ts +++ b/Models/ThemeConfig/PointRenderingConfig.ts @@ -1,7 +1,6 @@ import PointRenderingConfigJson from "./Json/PointRenderingConfigJson" import TagRenderingConfig from "./TagRenderingConfig" import { TagsFilter } from "../../Logic/Tags/TagsFilter" -import SharedTagRenderings from "../../Customizations/SharedTagRenderings" import { TagUtils } from "../../Logic/Tags/TagUtils" import { Utils } from "../../Utils" import Svg from "../../Svg" @@ -12,7 +11,6 @@ import { FixedUiElement } from "../../UI/Base/FixedUiElement" import Img from "../../UI/Base/Img" import Combine from "../../UI/Base/Combine" import { VariableUiElement } from "../../UI/Base/VariableUIElement" -import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" export default class PointRenderingConfig extends WithContextLoader { private static readonly allowed_location_codes = new Set([ @@ -37,6 +35,10 @@ export default class PointRenderingConfig extends WithContextLoader { constructor(json: PointRenderingConfigJson, context: string) { super(json, context) + if (json === undefined || json === null) { + throw "Invalid PointRenderingConfig: undefined or null" + } + if (typeof json.location === "string") { json.location = [json.location] } @@ -69,18 +71,9 @@ export default class PointRenderingConfig extends WithContextLoader { } this.cssClasses = this.tr("cssClasses", undefined) this.iconBadges = (json.iconBadges ?? []).map((overlay, i) => { - let tr: TagRenderingConfig - if ( - typeof overlay.then === "string" && - SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined - ) { - tr = SharedTagRenderings.SharedIcons.get(overlay.then) - } else { - tr = new TagRenderingConfig(overlay.then, `iconBadges.${i}`) - } return { if: TagUtils.Tag(overlay.if), - then: tr, + then: new TagRenderingConfig(overlay.then, `iconBadges.${i}`), } }) diff --git a/UI/AllTagsPanel.svelte b/UI/AllTagsPanel.svelte new file mode 100644 index 000000000..4eab95c50 --- /dev/null +++ b/UI/AllTagsPanel.svelte @@ -0,0 +1,54 @@ + + +
+ +
+ + diff --git a/UI/AllTagsPanel.ts b/UI/AllTagsPanel.ts deleted file mode 100644 index 32b8a2c84..000000000 --- a/UI/AllTagsPanel.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { VariableUiElement } from "./Base/VariableUIElement" -import { UIEventSource } from "../Logic/UIEventSource" -import Table from "./Base/Table" - -export class AllTagsPanel extends VariableUiElement { - constructor(tags: UIEventSource, state?) { - const calculatedTags = [].concat( - // SimpleMetaTagger.lazyTags, - ...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ?? - []) - ) - - super( - tags.map((tags) => { - const parts = [] - for (const key in tags) { - if (!tags.hasOwnProperty(key)) { - continue - } - let v = tags[key] - if (v === "") { - v = "empty string" - } - parts.push([key, v ?? "undefined"]) - } - - for (const key of calculatedTags) { - const value = tags[key] - if (value === undefined) { - continue - } - let type = "" - if (typeof value !== "string") { - type = " " + typeof value + "" - } - parts.push(["" + key + "", value]) - } - - return new Table(["key", "value"], parts) - .SetStyle( - "border: 1px solid black; border-radius: 1em;padding:1em;display:block;" - ) - .SetClass("zebra-table") - }) - ) - } -} diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts index b7dccbe4e..13ac4b5f7 100644 --- a/UI/Base/Minimap.ts +++ b/UI/Base/Minimap.ts @@ -3,7 +3,6 @@ import Loc from "../../Models/Loc" import BaseLayer from "../../Models/BaseLayer" import { UIEventSource } from "../../Logic/UIEventSource" import { BBox } from "../../Logic/BBox" -import { deprecate } from "util" export interface MinimapOptions { background?: UIEventSource diff --git a/UI/Base/MinimapImplementation.ts b/UI/Base/MinimapImplementation.ts index 22972f35f..271bd7253 100644 --- a/UI/Base/MinimapImplementation.ts +++ b/UI/Base/MinimapImplementation.ts @@ -23,7 +23,7 @@ import StrayClickHandler from "../../Logic/Actors/StrayClickHandler" * The stray-click-hanlders adds a marker to the map if no feature was clicked. * Shows the given uiToShow-element in the messagebox */ -export class StrayClickHandlerImplementation { +class StrayClickHandlerImplementation { private _lastMarker constructor( @@ -91,6 +91,7 @@ export class StrayClickHandlerImplementation { }) } } + export default class MinimapImplementation extends BaseUIElement implements MinimapObj { private static _nextId = 0 public readonly leafletMap: UIEventSource diff --git a/UI/Base/SubtleButton.svelte b/UI/Base/SubtleButton.svelte new file mode 100644 index 000000000..adce8c3ce --- /dev/null +++ b/UI/Base/SubtleButton.svelte @@ -0,0 +1,82 @@ + + + + + {#if imageUrl !== undefined} + {#if typeof imageUrl === "string"} + + {:else } +