diff --git a/.gitignore b/.gitignore index c5a6e0be2..7124fdc77 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ scratch assets/editor-layer-index.json assets/generated/* src/assets/generated/ +assets/layers/favourite/favourite.json public/*.webmanifest /*.html !/index.html diff --git a/assets/layers/favourite/favourite.proto.json b/assets/layers/favourite/favourite.proto.json index 73ab80572..57ff79f1f 100644 --- a/assets/layers/favourite/favourite.proto.json +++ b/assets/layers/favourite/favourite.proto.json @@ -1,4 +1,5 @@ { + "#":"no-translations", "pointRendering": [ { "location": [ @@ -37,35 +38,9 @@ "render": { "en": "Favourite location", "nl": "Favoriete locatie" - }, - "mappings": [ - { - "if": "name~*", - "then": { - "*": "{name}" - } - } - ] + } }, "tagRenderings": [ - { - "id": "Explanation", - "classes": "thanks", - "icon": { - "class": "large", - "path": "heart" - }, - "render": { - "en": "You marked this location as a personal favourite. As such, it is shown on every map you load.", - "nl": "Je hebt deze locatie als persoonlijke favoriet aangeduid en worden op alle MapComplete-kaarten getoond." - } - }, - { - "id": "show_images", - "render": { - "*": "{image_carousel()}" - } - }, - "all_tags" + ] } diff --git a/package.json b/package.json index f1ecefd07..816714f4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapcomplete", - "version": "0.35.0", + "version": "0.36.0", "repository": "https://github.com/pietervdvn/MapComplete", "description": "A small website to edit OSM easily", "bugs": "https://github.com/pietervdvn/MapComplete/issues", diff --git a/scripts/generateFavouritesLayer.ts b/scripts/generateFavouritesLayer.ts index 334742b45..2cc0abf9d 100644 --- a/scripts/generateFavouritesLayer.ts +++ b/scripts/generateFavouritesLayer.ts @@ -2,24 +2,242 @@ import Script from "./Script" import { LayerConfigJson } from "../src/Models/ThemeConfig/Json/LayerConfigJson" import { readFileSync, writeFileSync } from "fs" import { AllSharedLayers } from "../src/Customizations/AllSharedLayers" +import { AllKnownLayoutsLazy } from "../src/Customizations/AllKnownLayouts" +import { Utils } from "../src/Utils" +import { AddEditingElements } from "../src/Models/ThemeConfig/Conversion/PrepareLayer" +import { + MappingConfigJson, + QuestionableTagRenderingConfigJson, +} from "../src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" +import { TagConfigJson } from "../src/Models/ThemeConfig/Json/TagConfigJson" +import { TagUtils } from "../src/Logic/Tags/TagUtils" +import { TagRenderingConfigJson } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson" +import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable" + +export class GenerateFavouritesLayer extends Script { + private readonly layers: LayerConfigJson[] = [] -class PrepareFavouritesLayerJson extends Script { constructor() { super("Prepares the 'favourites'-layer") + const allThemes = new AllKnownLayoutsLazy(false).values() + for (const theme of allThemes) { + if (theme.hideFromOverview) { + continue + } + for (const layer of theme.layers) { + if (!layer.source) { + continue + } + if (layer.source.geojsonSource) { + continue + } + const layerConfig = AllSharedLayers.getSharedLayersConfigs().get(layer.id) + if (!layerConfig) { + continue + } + this.layers.push(layerConfig) + } + } + } + + private addTagRenderings(proto: LayerConfigJson) { + const blacklistedIds = new Set([ + "images", + "questions", + "mapillary", + "leftover-questions", + "last_edit", + "minimap", + "move-button", + "delete-button", + "all-tags", + ...AddEditingElements.addedElements, + ]) + + const generateTagRenderings: (string | QuestionableTagRenderingConfigJson)[] = [] + const trPerId = new Map< + string, + { conditions: TagConfigJson[]; tr: QuestionableTagRenderingConfigJson } + >() + for (const layerConfig of this.layers) { + if (!layerConfig.tagRenderings) { + continue + } + for (const tagRendering of layerConfig.tagRenderings) { + if (typeof tagRendering === "string") { + if (blacklistedIds.has(tagRendering)) { + continue + } + generateTagRenderings.push(tagRendering) + blacklistedIds.add(tagRendering) + continue + } + if (tagRendering["builtin"]) { + continue + } + const id = tagRendering.id + if (blacklistedIds.has(id)) { + continue + } + if (trPerId.has(id)) { + const old = trPerId.get(id).tr + + // We need to figure out if this was a 'recycled' tag rendering or just happens to have the same id + function isSame(fieldName: string) { + return old[fieldName]?.["en"] === tagRendering[fieldName]?.["en"] + } + + const sameQuestion = isSame("question") && isSame("render") + if (!sameQuestion) { + const newTr = Utils.Clone(tagRendering) + newTr.id = layerConfig.id + "_" + newTr.id + if (blacklistedIds.has(newTr.id)) { + continue + } + newTr.condition = { + and: Utils.NoNull([(newTr.condition, layerConfig.source["osmTags"])]), + } + generateTagRenderings.push(newTr) + blacklistedIds.add(newTr.id) + continue + } + } + if (!trPerId.has(id)) { + const newTr = Utils.Clone(tagRendering) + generateTagRenderings.push(newTr) + trPerId.set(newTr.id, { tr: newTr, conditions: [] }) + } + const conditions = trPerId.get(id).conditions + if (tagRendering["condition"]) { + conditions.push({ + and: [tagRendering["condition"], layerConfig.source["osmTags"]], + }) + } else { + conditions.push(layerConfig.source["osmTags"]) + } + } + } + + for (const { tr, conditions } of Array.from(trPerId.values())) { + const optimized = TagUtils.optimzeJson({ or: conditions }) + if (optimized === true) { + continue + } + if (optimized === false) { + throw "Optimized into 'false', this is weird..." + } + tr.condition = optimized + } + + const allTags: QuestionableTagRenderingConfigJson = { + id: "all-tags", + render: { "*": "{all_tags()}" }, + + metacondition: { + or: [ + "__featureSwitchIsDebugging=true", + "mapcomplete-show_tags=full", + "mapcomplete-show_debug=yes", + ], + }, + } + proto.tagRenderings = [ + ...generateTagRenderings, + ...proto.tagRenderings, + "questions", + allTags, + ] + } + + private addTitle(proto: LayerConfigJson) { + const mappings: MappingConfigJson[] = [] + for (const layer of this.layers) { + const t = layer.title + const tags: TagConfigJson = layer.source["osmTags"] + if (!t) { + continue + } + if (typeof t === "string") { + mappings.push({ if: tags, then: t }) + } else if (t["render"] !== undefined || t["mappings"] !== undefined) { + const tr = t + for (let i = 0; i < (tr.mappings ?? []).length; i++) { + const mapping = tr.mappings[i] + const optimized = TagUtils.optimzeJson({ + and: [mapping.if, tags], + }) + if (optimized === false) { + console.warn( + "The following tags yielded 'false':", + JSON.stringify(mapping.if), + JSON.stringify(tags) + ) + continue + } + if (optimized === true) { + console.error( + "The following tags yielded 'false':", + JSON.stringify(mapping.if), + JSON.stringify(tags) + ) + throw "Tags for title optimized to true" + } + + if (!mapping.then) { + throw ( + "The title has a missing 'then' for mapping " + + i + + " in layer " + + layer.id + ) + } + mappings.push({ + if: optimized, + then: mapping.then, + }) + } + if (tr.render) { + mappings.push({ + if: tags, + then: tr.render, + }) + } + } else { + mappings.push({ if: tags, then: >t }) + } + } + + if (proto.title["mappings"]) { + mappings.unshift(...proto.title["mappings"]) + } + if (proto.title["render"]) { + mappings.push({ + if: "id~*", + then: proto.title["render"], + }) + } + + proto.title = { + mappings, + } } async main(args: string[]): Promise { - const allConfigs = AllSharedLayers.getSharedLayersConfigs() + console.log("Generating the favourite layer: stealing _all_ tagRenderings") const proto = this.readLayer("favourite/favourite.proto.json") - const questions = allConfigs.get("questions") - proto.tagRenderings.push(...questions.tagRenderings) - + this.addTagRenderings(proto) + this.addTitle(proto) writeFileSync("./assets/layers/favourite/favourite.json", JSON.stringify(proto, null, " ")) } private readLayer(path: string): LayerConfigJson { - return JSON.parse(readFileSync("./assets/layers/" + path, "utf8")) + try { + return JSON.parse(readFileSync("./assets/layers/" + path, "utf8")) + } catch (e) { + console.error("Could not read ./assets/layers/" + path) + throw e + } } } -new PrepareFavouritesLayerJson().run() +new GenerateFavouritesLayer().run() diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index 32f06ee64..dcfbb71ad 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -28,6 +28,7 @@ import { QuestionableTagRenderingConfigJson } from "../src/Models/ThemeConfig/Js import LayerConfig from "../src/Models/ThemeConfig/LayerConfig" import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig" import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext" +import { GenerateFavouritesLayer } from "./generateFavouritesLayer" // 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 @@ -381,16 +382,11 @@ class LayerOverviewUtils extends Script { forceReload ) - writeFileSync( - "./src/assets/generated/known_themes.json", - JSON.stringify({ - themes: Array.from(sharedThemes.values()), - }) - ) - writeFileSync( "./src/assets/generated/known_layers.json", - JSON.stringify({ layers: Array.from(sharedLayers.values()) }) + JSON.stringify({ + layers: Array.from(sharedLayers.values()).filter((l) => l.id !== "favourite"), + }) ) const mcChangesPath = "./assets/themes/mapcomplete-changes/mapcomplete-changes.json" @@ -428,6 +424,19 @@ class LayerOverviewUtils extends Script { ConversionContext.construct([], []) ) + for (const [_, theme] of sharedThemes) { + theme.layers = theme.layers.filter( + (l) => Constants.added_by_default.indexOf(l["id"]) < 0 + ) + } + + writeFileSync( + "./src/assets/generated/known_themes.json", + JSON.stringify({ + themes: Array.from(sharedThemes.values()), + }) + ) + const end = new Date() const millisNeeded = end.getTime() - start.getTime() if (AllSharedLayers.getSharedLayersConfigs().size == 0) { @@ -791,4 +800,5 @@ class LayerOverviewUtils extends Script { } } +new GenerateFavouritesLayer().run() new LayerOverviewUtils().run() diff --git a/src/Customizations/AllKnownLayouts.ts b/src/Customizations/AllKnownLayouts.ts index 12c27223c..cbd28cf5b 100644 --- a/src/Customizations/AllKnownLayouts.ts +++ b/src/Customizations/AllKnownLayouts.ts @@ -1,45 +1,54 @@ import known_themes from "../assets/generated/known_themes.json" import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" +import favourite from "../assets/generated/layers/favourite.json" import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson" +import { AllSharedLayers } from "./AllSharedLayers" +import Constants from "../Models/Constants" /** * Somewhat of a dictionary, which lazily parses needed themes */ export class AllKnownLayoutsLazy { - private readonly dict: Map LayoutConfig }> = - new Map() - constructor() { + private readonly raw: Map = new Map() + private readonly dict: Map = new Map() + + constructor(includeFavouriteLayer = true) { for (const layoutConfigJson of known_themes["themes"]) { - this.dict.set(layoutConfigJson.id, { - func: () => { - const layout = new LayoutConfig(layoutConfigJson, true) - for (let i = 0; i < layout.layers.length; i++) { - let layer = layout.layers[i] - if (typeof layer === "string") { - throw "Layer " + layer + " was not expanded in " + layout.id - } + for (const layerId of Constants.added_by_default) { + if (layerId === "favourite") { + if (includeFavouriteLayer) { + layoutConfigJson.layers.push(favourite) } - return layout - }, - }) + continue + } + const defaultLayer = AllSharedLayers.getSharedLayersConfigs().get(layerId) + if (defaultLayer === undefined) { + console.error("Could not find builtin layer", layerId) + continue + } + layoutConfigJson.layers.push(defaultLayer) + } + this.raw.set(layoutConfigJson.id, layoutConfigJson) } } + public getConfig(key: string): LayoutConfigJson { + return this.raw.get(key) + } + public get(key: string): LayoutConfig { - const thunk = this.dict.get(key) - if (thunk === undefined) { - return undefined + const cached = this.dict.get(key) + if (cached !== undefined) { + return cached } - if (thunk["data"]) { - return thunk["data"] - } - const layout = thunk["func"]() - this.dict.set(key, { data: layout }) + + const layout = new LayoutConfig(this.getConfig(key)) + this.dict.set(key, layout) return layout } public keys() { - return this.dict.keys() + return this.raw.keys() } public values() { diff --git a/src/Logic/Actors/GeoLocationHandler.ts b/src/Logic/Actors/GeoLocationHandler.ts index 9333a8413..691be8dbb 100644 --- a/src/Logic/Actors/GeoLocationHandler.ts +++ b/src/Logic/Actors/GeoLocationHandler.ts @@ -173,7 +173,6 @@ export default class GeoLocationHandler { properties[k] = location[k] } } - console.debug("Current location object:", location) properties["_all"] = JSON.stringify(location) const feature = { diff --git a/src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts b/src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts index bd6d52b4d..01703987f 100644 --- a/src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts +++ b/src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts @@ -16,6 +16,11 @@ export default class FavouritesFeatureSource extends StaticFeatureSource { private readonly _osmConnection: OsmConnection private readonly _detectedIds: Store + /** + * All favourites, including the ones which are filtered away because they are already displayed + */ + public readonly allFavourites: Store + constructor( connection: OsmConnection, indexedSource: FeaturePropertiesStore, @@ -53,6 +58,7 @@ export default class FavouritesFeatureSource extends StaticFeatureSource { ) super(featuresWithoutAlreadyPresent) + this.allFavourites = features this._osmConnection = connection this._detectedIds = Stores.ListStabilized( @@ -76,6 +82,7 @@ export default class FavouritesFeatureSource extends StaticFeatureSource { const geometry = <[number, number]>JSON.parse(prefs[key]) const properties = FavouritesFeatureSource.getPropertiesFor(prefs, id) properties._orig_layer = prefs[FavouritesFeatureSource.prefix + id + "-layer"] + properties._orig_theme = prefs[FavouritesFeatureSource.prefix + id + "-theme"] properties.id = osmId properties._favourite = "yes" diff --git a/src/Models/ThemeConfig/Conversion/Conversion.ts b/src/Models/ThemeConfig/Conversion/Conversion.ts index b6422bd55..d74028731 100644 --- a/src/Models/ThemeConfig/Conversion/Conversion.ts +++ b/src/Models/ThemeConfig/Conversion/Conversion.ts @@ -2,6 +2,7 @@ import { LayerConfigJson } from "../Json/LayerConfigJson" import { Utils } from "../../../Utils" import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" import { ConversionContext } from "./ConversionContext" +import { T } from "vitest/dist/types-aac763a5" export interface DesugaringContext { tagRenderings: Map @@ -81,18 +82,36 @@ export class Pure extends Conversion { } } +export class Bypass extends DesugaringStep { + private readonly _applyIf: (t: T) => boolean + private readonly _step: DesugaringStep + constructor(applyIf: (t: T) => boolean, step: DesugaringStep) { + super("Applies the step on the object, if the object satisfies the predicate", [], "Bypass") + this._applyIf = applyIf + this._step = step + } + + convert(json: T, context: ConversionContext): T { + if (!this._applyIf(json)) { + return json + } + return this._step.convert(json, context) + } +} + export class Each extends Conversion { private readonly _step: Conversion private readonly _msg: string + private readonly _filter: (x: X) => boolean - constructor(step: Conversion, msg?: string) { + constructor(step: Conversion, options?: { msg?: string }) { super( "Applies the given step on every element of the list", [], "OnEach(" + step.name + ")" ) this._step = step - this._msg = msg + this._msg = options?.msg } convert(values: X[], context: ConversionContext): Y[] { diff --git a/src/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts b/src/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts index c88eb6541..aa587e65e 100644 --- a/src/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts +++ b/src/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts @@ -85,7 +85,7 @@ export default class CreateNoteImportLayer extends Conversion { } export class AddEditingElements extends DesugaringStep { + static addedElements: string[] = [ + "minimap", + "just_created", + "split_button", + "move_button", + "delete_button", + "last_edit", + "favourite_state", + "all_tags", + ] private readonly _desugaring: DesugaringContext constructor(desugaring: DesugaringContext) { @@ -1210,7 +1220,7 @@ class AddFavouriteBadges extends DesugaringStep { } convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson { - if (json.id === "favourite") { + if (json.source === "special" || json.source === "special:library") { return json } const pr = json.pointRendering?.[0] diff --git a/src/Models/ThemeConfig/Conversion/Validation.ts b/src/Models/ThemeConfig/Conversion/Validation.ts index 62a495bf7..8db411c2f 100644 --- a/src/Models/ThemeConfig/Conversion/Validation.ts +++ b/src/Models/ThemeConfig/Conversion/Validation.ts @@ -1,4 +1,4 @@ -import { Conversion, DesugaringStep, Each, Fuse, On, Pipe, Pure } from "./Conversion" +import { Bypass, Conversion, DesugaringStep, Each, Fuse, On } from "./Conversion" import { LayerConfigJson } from "../Json/LayerConfigJson" import LayerConfig from "../LayerConfig" import { Utils } from "../../../Utils" @@ -11,7 +11,6 @@ import { TagUtils } from "../../../Logic/Tags/TagUtils" import { ExtractImages } from "./FixImages" import { And } from "../../../Logic/Tags/And" import Translations from "../../../UI/i18n/Translations" -import Svg from "../../../Svg" import FilterConfigJson from "../Json/FilterConfigJson" import DeleteConfig from "../DeleteConfig" import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson" @@ -276,9 +275,9 @@ export class ValidateThemeAndLayers extends Fuse { new On( "layers", new Each( - new Pipe( - new ValidateLayer(undefined, isBuiltin, doesImageExist, false, true), - new Pure((x) => x?.raw) + new Bypass( + (layer) => Constants.added_by_default.indexOf(layer.id) < 0, + new ValidateLayerConfig(undefined, isBuiltin, doesImageExist, false, true) ) ) ) @@ -968,7 +967,7 @@ export class ValidateTagRenderings extends Fuse { "Various validation on tagRenderingConfigs", new DetectShadowedMappings(layerConfig), new DetectConflictingAddExtraTags(), - // new DetectNonErasedKeysInMappings(), + // TODO enable new DetectNonErasedKeysInMappings(), new DetectMappingsWithImages(doesImageExist), new On("render", new ValidatePossibleLinks()), new On("question", new ValidatePossibleLinks()), @@ -1350,6 +1349,29 @@ export class PrevalidateLayer extends DesugaringStep { } } +export class ValidateLayerConfig extends DesugaringStep { + private readonly validator: ValidateLayer + constructor( + path: string, + isBuiltin: boolean, + doesImageExist: DoesImageExist, + studioValidations: boolean = false, + skipDefaultLayers: boolean = false + ) { + super("Thin wrapper around 'ValidateLayer", [], "ValidateLayerConfig") + this.validator = new ValidateLayer( + path, + isBuiltin, + doesImageExist, + studioValidations, + skipDefaultLayers + ) + } + + convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson { + return this.validator.convert(json, context).raw + } +} export class ValidateLayer extends Conversion< LayerConfigJson, { parsed: LayerConfig; raw: LayerConfigJson } diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index a94ad07b4..fbf17de77 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -462,6 +462,7 @@ export default class ThemeViewState implements SpecialVisualizationState { * @private */ private selectClosestAtCenter(i: number = 0) { + this.mapProperties.lastKeyNavigation.setData(Date.now() / 1000) const toSelect = this.closestFeatures.features.data[i] if (!toSelect) { return @@ -567,46 +568,6 @@ export default class ThemeViewState implements SpecialVisualizationState { }) } - private addLastClick(last_click: LastClickFeatureSource) { - // The last_click gets a _very_ special treatment as it interacts with various parts - - this.featureProperties.trackFeatureSource(last_click) - this.indexedFeatures.addSource(last_click) - - last_click.features.addCallbackAndRunD((features) => { - if (this.selectedLayer.data?.id === "last_click") { - // The last-click location moved, but we have selected the last click of the previous location - // So, we update _after_ clearing the selection to make sure no stray data is sticking around - this.selectedElement.setData(undefined) - this.selectedElement.setData(features[0]) - } - }) - - new ShowDataLayer(this.map, { - features: new FilteringFeatureSource(this.newPointDialog, last_click), - doShowLayer: this.featureSwitches.featureSwitchEnableLogin, - layer: this.newPointDialog.layerDef, - selectedElement: this.selectedElement, - selectedLayer: this.selectedLayer, - metaTags: this.userRelatedState.preferencesAsTags, - onClick: (feature: Feature) => { - if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) { - this.map.data.flyTo({ - zoom: Constants.minZoomLevelToAddNewPoint, - center: this.mapProperties.lastClickLocation.data, - }) - return - } - // We first clear the selection to make sure no weird state is around - this.selectedLayer.setData(undefined) - this.selectedElement.setData(undefined) - - this.selectedElement.setData(feature) - this.selectedLayer.setData(this.newPointDialog.layerDef) - }, - }) - } - /** * Add the special layers to the map */ @@ -663,9 +624,7 @@ export default class ThemeViewState implements SpecialVisualizationState { } const rangeFLayer: FilteredLayer = this.layerState.filteredLayers.get("range") - const rangeIsDisplayed = rangeFLayer?.isDisplayed - if ( !QueryParameters.wasInitialized(FilteredLayer.queryParameterKey(rangeFLayer.layerDef)) ) { diff --git a/src/UI/BigComponents/UploadTraceToOsmUI.ts b/src/UI/BigComponents/UploadTraceToOsmUI.ts index 5344da228..2b281ee06 100644 --- a/src/UI/BigComponents/UploadTraceToOsmUI.ts +++ b/src/UI/BigComponents/UploadTraceToOsmUI.ts @@ -121,9 +121,9 @@ export default class UploadTraceToOsmUI extends LoginToggle { ]).SetClass("flex p-2 rounded-xl border-2 subtle-border items-center"), new Toggle( confirmPanel, - new SubtleButton(new SvelteUIElement(Upload), t.title).onClick(() => - clicked.setData(true) - ), + new SubtleButton(new SvelteUIElement(Upload), t.title) + .onClick(() => clicked.setData(true)) + .SetClass("w-full"), clicked ), uploadFinished diff --git a/src/UI/Favourites/FavouriteSummary.svelte b/src/UI/Favourites/FavouriteSummary.svelte index e69de29bb..92f34be74 100644 --- a/src/UI/Favourites/FavouriteSummary.svelte +++ b/src/UI/Favourites/FavouriteSummary.svelte @@ -0,0 +1,10 @@ + + +
+ {JSON.stringify(properties)} + {properties?.id ?? "undefined"} + OSM +
diff --git a/src/UI/Favourites/Favourites.svelte b/src/UI/Favourites/Favourites.svelte index ff6e0331f..cb0382e46 100644 --- a/src/UI/Favourites/Favourites.svelte +++ b/src/UI/Favourites/Favourites.svelte @@ -1,8 +1,19 @@ + +
+ You marked {$favourites.length} locations as a favourite location. + + This list is only visible to you +{#each $favourites as f} + +{/each} +
diff --git a/src/UI/Popup/ExportAsGpxViz.ts b/src/UI/Popup/ExportAsGpxViz.ts index c7821da10..a5acf8582 100644 --- a/src/UI/Popup/ExportAsGpxViz.ts +++ b/src/UI/Popup/ExportAsGpxViz.ts @@ -31,14 +31,16 @@ export class ExportAsGpxViz implements SpecialVisualization { t.downloadFeatureAsGpx.SetClass("font-bold text-lg"), t.downloadGpxHelper.SetClass("subtle"), ]).SetClass("flex flex-col") - ).onClick(() => { - console.log("Exporting as GPX!") - const tags = tagSource.data - const title = layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track" - const gpx = GeoOperations.toGpx(>feature, title) - Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", { - mimetype: "{gpx=application/gpx+xml}", + ) + .SetClass("w-full") + .onClick(() => { + console.log("Exporting as GPX!") + const tags = tagSource.data + const title = layer.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "gpx_track" + const gpx = GeoOperations.toGpx(>feature, title) + Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", { + mimetype: "{gpx=application/gpx+xml}", + }) }) - }) } } diff --git a/src/UI/SpecialVisualizations.ts b/src/UI/SpecialVisualizations.ts index d2da94d4b..139a9e60e 100644 --- a/src/UI/SpecialVisualizations.ts +++ b/src/UI/SpecialVisualizations.ts @@ -534,6 +534,9 @@ export default class SpecialVisualizations { feature: Feature, layer: LayerConfig ): BaseUIElement { + if (!layer.deletion) { + return undefined + } return new SvelteUIElement(DeleteWizard, { tags: tagSource, deleteConfig: layer.deletion, @@ -873,20 +876,22 @@ export default class SpecialVisualizations { t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"), t.downloadGeoJsonHelper.SetClass("subtle"), ]).SetClass("flex flex-col") - ).onClick(() => { - console.log("Exporting as Geojson") - const tags = tagSource.data - const title = - layer?.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson" - const data = JSON.stringify(feature, null, " ") - Utils.offerContentsAsDownloadableFile( - data, - title + "_mapcomplete_export.geojson", - { - mimetype: "application/vnd.geo+json", - } - ) - }) + ) + .onClick(() => { + console.log("Exporting as Geojson") + const tags = tagSource.data + const title = + layer?.title?.GetRenderValue(tags)?.Subs(tags)?.txt ?? "geojson" + const data = JSON.stringify(feature, null, " ") + Utils.offerContentsAsDownloadableFile( + data, + title + "_mapcomplete_export.geojson", + { + mimetype: "application/vnd.geo+json", + } + ) + }) + .SetClass("w-full") }, }, { diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index 0db04b1f8..221dfeef3 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -15,7 +15,7 @@ import type { MapProperties } from "../Models/MapProperties"; import Geosearch from "./BigComponents/Geosearch.svelte"; import Translations from "./i18n/Translations"; - import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; + import { CogIcon, EyeIcon, HeartIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"; import Tr from "./Base/Tr.svelte"; import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"; import FloatOver from "./Base/FloatOver.svelte"; @@ -64,6 +64,8 @@ import Community from "../assets/svg/Community.svelte"; import Download from "../assets/svg/Download.svelte"; import Share from "../assets/svg/Share.svelte"; + import FavouriteSummary from "./Favourites/FavouriteSummary.svelte"; + import Favourites from "./Favourites/Favourites.svelte"; export let state: ThemeViewState; let layout = state.layout; @@ -493,22 +495,31 @@
+ + Your favourites +
+ +
+

Your favourite locations

+ +
+
-
+
-
+
-
+
new PrivacyPolicy()} />
- -
+ +
diff --git a/test/Models/ThemeConfig/Conversion/PrepareTheme.spec.ts b/test/Models/ThemeConfig/Conversion/PrepareTheme.spec.ts index 009c9f08a..ec4118934 100644 --- a/test/Models/ThemeConfig/Conversion/PrepareTheme.spec.ts +++ b/test/Models/ThemeConfig/Conversion/PrepareTheme.spec.ts @@ -179,7 +179,21 @@ describe("PrepareTheme", () => { id: "layer-example", name: null, minzoom: 18, - pointRendering: [{ location: ["point"], label: "xyz" }], + pointRendering: [ + { + location: ["point"], + label: "xyz", + iconBadges: [ + { + if: "_favourite=yes", + then: { + id: "circlewhiteheartred", + render: "circle:white;heart:red", + }, + }, + ], + }, + ], lineRendering: [{ width: 1 }], titleIcons: [], })