diff --git a/Logic/Actors/SelectedElementTagsUpdater.ts b/Logic/Actors/SelectedElementTagsUpdater.ts index bb90fe28b..5bc925cae 100644 --- a/Logic/Actors/SelectedElementTagsUpdater.ts +++ b/Logic/Actors/SelectedElementTagsUpdater.ts @@ -50,6 +50,9 @@ export default class SelectedElementTagsUpdater { const state = this.state state.selectedElement.addCallbackAndRunD(async (s) => { let id = s.properties?.id + if (!id) { + return + } const backendUrl = state.osmConnection._oauth_config.url if (id.startsWith(backendUrl)) { diff --git a/Logic/Actors/TitleHandler.ts b/Logic/Actors/TitleHandler.ts index 921afa804..2cad3c993 100644 --- a/Logic/Actors/TitleHandler.ts +++ b/Logic/Actors/TitleHandler.ts @@ -20,32 +20,24 @@ export default class TitleHandler { (selected) => { const defaultTitle = state.layout?.title?.txt ?? "MapComplete" - if (selected === undefined) { + if (selected === undefined || selectedLayer.data === undefined) { return defaultTitle } const tags = selected.properties - for (const layer of state.layout?.layers ?? []) { - if (layer.title === undefined) { - continue - } - if (layer.source.osmTags.matchesProperties(tags)) { - const tagsSource = - allElements.getStore(tags.id) ?? - new UIEventSource>(tags) - const title = new SvelteUIElement(TagRenderingAnswer, { - tags: tagsSource, - state, - selectedElement: selectedElement.data, - layer: selectedLayer.data, - }) - return ( - new Combine([defaultTitle, " | ", title]).ConstructElement() - ?.textContent ?? defaultTitle - ) - } - } - return defaultTitle + const layer = selectedLayer.data + const tagsSource = + allElements.getStore(tags.id) ?? new UIEventSource>(tags) + const title = new SvelteUIElement(TagRenderingAnswer, { + tags: tagsSource, + state, + selectedElement: selectedElement.data, + layer, + }) + return ( + new Combine([defaultTitle, " | ", title]).ConstructElement()?.textContent ?? + defaultTitle + ) }, [Locale.language, selectedLayer] ) diff --git a/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts b/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts index b4700e72b..1336ab374 100644 --- a/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts +++ b/Logic/FeatureSource/Actors/FeaturePropertiesStore.ts @@ -43,6 +43,10 @@ export default class FeaturePropertiesStore { return this._elements.get(id) } + public addSpecial(id: string, store: UIEventSource>) { + this._elements.set(id, store) + } + /** * Overwrites the tags of the old properties object, returns true if a change was made. * Metatags are overriden if they are in the new properties, but not removed diff --git a/Logic/FeatureSource/Sources/LastClickFeatureSource.ts b/Logic/FeatureSource/Sources/LastClickFeatureSource.ts new file mode 100644 index 000000000..f9d109e38 --- /dev/null +++ b/Logic/FeatureSource/Sources/LastClickFeatureSource.ts @@ -0,0 +1,57 @@ +import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig" +import FeatureSource from "../FeatureSource" +import { ImmutableStore, Store } from "../../UIEventSource" +import { Feature, Point } from "geojson" +import { TagUtils } from "../../Tags/TagUtils" +import BaseUIElement from "../../../UI/BaseUIElement" +import { Utils } from "../../../Utils" +import { regex_not_newline_characters } from "svelte/types/compiler/utils/patterns" +import { render } from "sass" + +/** + * Highly specialized feature source. + * Based on a lon/lat UIEVentSource, will generate the corresponding feature with the correct properties + */ +export class LastClickFeatureSource implements FeatureSource { + features: Store + + public properties: Record + constructor(location: Store<{ lon: number; lat: number }>, layout: LayoutConfig) { + const allPresets: BaseUIElement[] = [] + for (const layer of layout.layers) + for (let i = 0; i < (layer.presets ?? []).length; i++) { + const preset = layer.presets[i] + const tags = new ImmutableStore(TagUtils.KVtoProperties(preset.tags)) + const { html } = layer.mapRendering[0].RenderIcon(tags, false, { + noSize: true, + includeBadges: false, + }) + allPresets.push(html) + } + + const renderings = Utils.Dedup( + allPresets.map((uiElem) => uiElem.ConstructElement().innerHTML) + ) + + const properties = { + lastclick: "yes", + id: "last_click", + has_note_layer: layout.layers.some((l) => l.id === "note") ? "yes" : "no", + has_presets: layout.layers.some((l) => l.presets?.length > 0) ? "yes" : "no", + renderings: renderings.join(""), + number_of_presets: "" + renderings.length, + first_preset: renderings[0], + } + this.properties = properties + this.features = location.mapD(({ lon, lat }) => [ + >{ + type: "Feature", + properties, + geometry: { + type: "Point", + coordinates: [lon, lat], + }, + }, + ]) + } +} diff --git a/Logic/Tags/TagUtils.ts b/Logic/Tags/TagUtils.ts index b0cc2989b..e3f093310 100644 --- a/Logic/Tags/TagUtils.ts +++ b/Logic/Tags/TagUtils.ts @@ -21,16 +21,16 @@ export class TagUtils { [">", (a, b) => a > b], ] - static KVtoProperties(tags: Tag[]): any { - const properties = {} + static KVtoProperties(tags: Tag[]): Record { + const properties : Record = {} for (const tag of tags) { properties[tag.key] = tag.value } return properties } - static changeAsProperties(kvs: { k: string; v: string }[]): any { - const tags = {} + static changeAsProperties(kvs: { k: string; v: string }[]): Record { + const tags: Record = {} for (const kv of kvs) { tags[kv.k] = kv.v } diff --git a/Models/Constants.ts b/Models/Constants.ts index 6b882285d..d0b8e1a66 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -33,6 +33,7 @@ export default class Constants { "home_location", "gps_track", "range", + "last_click", ] as const /** * Special layers which are not included in a theme by default @@ -66,10 +67,10 @@ export default class Constants { themeGeneratorReadOnlyUnlock: 50, themeGeneratorFullUnlock: 500, addNewPointWithUnreadMessagesUnlock: 500, - minZoomLevelToAddNewPoints: Constants.isRetina() ? 18 : 19, importHelperUnlock: 5000, } + static readonly minZoomLevelToAddNewPoint = Constants.isRetina() ? 18 : 19 /** * Used by 'PendingChangesUploader', which waits this amount of seconds to upload changes. * (Note that pendingChanges might upload sooner if the popup is closed or similar) diff --git a/Models/MapProperties.ts b/Models/MapProperties.ts index 39a49adcc..41cf8d044 100644 --- a/Models/MapProperties.ts +++ b/Models/MapProperties.ts @@ -1,4 +1,4 @@ -import { UIEventSource } from "../Logic/UIEventSource" +import { Store, UIEventSource } from "../Logic/UIEventSource" import { BBox } from "../Logic/BBox" import { RasterLayerPolygon } from "./RasterLayers" @@ -10,5 +10,7 @@ export interface MapProperties { readonly maxbounds: UIEventSource readonly allowMoving: UIEventSource + readonly lastClickLocation: Store<{ lon: number; lat: number }> + readonly allowZooming: UIEventSource } diff --git a/Models/ThemeConfig/Conversion/FixImages.ts b/Models/ThemeConfig/Conversion/FixImages.ts index e6116c90c..83be16804 100644 --- a/Models/ThemeConfig/Conversion/FixImages.ts +++ b/Models/ThemeConfig/Conversion/FixImages.ts @@ -5,6 +5,7 @@ import metapaths from "../../../assets/layoutconfigmeta.json" import tagrenderingmetapaths from "../../../assets/questionabletagrenderingconfigmeta.json" import Translations from "../../../UI/i18n/Translations" +import { parse as parse_html } from "node-html-parser" export class ExtractImages extends Conversion< LayoutConfigJson, { path: string; context: string }[] @@ -190,6 +191,17 @@ export class ExtractImages extends Conversion< const cleanedImages: { path: string; context: string }[] = [] for (const foundImage of allFoundImages) { + if (foundImage.path.startsWith("<") && foundImage.path.endsWith(">")) { + // These is probably html - we ignore + const doc = parse_html(foundImage.path) + const images = Array.from(doc.getElementsByTagName("img")) + const paths = images.map((i) => i.getAttribute("src")) + cleanedImages.push( + ...paths.map((path) => ({ path, context: foundImage.context + " (in html)" })) + ) + continue + } + // 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])) diff --git a/Models/ThemeConfig/Conversion/Validation.ts b/Models/ThemeConfig/Conversion/Validation.ts index 8fa4c2dde..da89260a8 100644 --- a/Models/ThemeConfig/Conversion/Validation.ts +++ b/Models/ThemeConfig/Conversion/Validation.ts @@ -101,6 +101,11 @@ export class DoesImageExist extends DesugaringStep { } } + if (image.startsWith("<") && image.endsWith(">")) { + // This is probably HTML, you're on your own here + return { result: image } + } + if (!this._knownImagePaths.has(image)) { if (this.doesPathExist === undefined) { errors.push( @@ -730,9 +735,9 @@ export class ValidateLayer extends DesugaringStep { } } - if (json.minzoom > Constants.userJourney.minZoomLevelToAddNewPoints) { + if (json.minzoom > Constants.minZoomLevelToAddNewPoint) { ;(json.presets?.length > 0 ? errors : warnings).push( - `At ${context}: minzoom is ${json.minzoom}, this should be at most ${Constants.userJourney.minZoomLevelToAddNewPoints} as a preset is set. Why? Selecting the pin for a new item will zoom in to level before adding the point. Having a greater minzoom will hide the points, resulting in possible duplicates` + `At ${context}: minzoom is ${json.minzoom}, this should be at most ${Constants.minZoomLevelToAddNewPoint} as a preset is set. Why? Selecting the pin for a new item will zoom in to level before adding the point. Having a greater minzoom will hide the points, resulting in possible duplicates` ) } { diff --git a/Models/ThemeConfig/Json/PointRenderingConfigJson.ts b/Models/ThemeConfig/Json/PointRenderingConfigJson.ts index 6741b822c..54ad00db9 100644 --- a/Models/ThemeConfig/Json/PointRenderingConfigJson.ts +++ b/Models/ThemeConfig/Json/PointRenderingConfigJson.ts @@ -69,15 +69,25 @@ export default interface PointRenderingConfigJson { label?: string | TagRenderingConfigJson /** - * A snippet of css code + * A snippet of css code which is applied onto the container of the entire marker */ css?: string | TagRenderingConfigJson /** - * A snippet of css-classes. They can be space-separated + * A snippet of css-classes which are applied onto the container of the entire marker. They can be space-separated */ cssClasses?: string | TagRenderingConfigJson + /** + * Css that is applied onto the label + */ + labelCss?: string | TagRenderingConfigJson + + /** + * Css classes that are applied onto the label; can be space-separated + */ + labelCssClasses?: string | TagRenderingConfigJson + /** * If the map is pitched, the marker will stay parallel to the screen. * Set to 'map' if you want to put it flattened on the map diff --git a/Models/ThemeConfig/LayerConfig.ts b/Models/ThemeConfig/LayerConfig.ts index 05fc634bf..459d60f9b 100644 --- a/Models/ThemeConfig/LayerConfig.ts +++ b/Models/ThemeConfig/LayerConfig.ts @@ -30,6 +30,7 @@ import { FixedUiElement } from "../../UI/Base/FixedUiElement" import Svg from "../../Svg" import { ImmutableStore } from "../../Logic/UIEventSource" import { OsmTags } from "../OsmFeature" +import Constants from "../Constants" export default class LayerConfig extends WithContextLoader { public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const @@ -322,7 +323,8 @@ export default class LayerConfig extends WithContextLoader { } else if ( !hasCenterRendering && this.lineRendering.length === 0 && - !this.source.geojsonSource?.startsWith( + Constants.priviliged_layers.indexOf(this.id) < 0 && + !this.source?.geojsonSource?.startsWith( "https://api.openstreetmap.org/api/0.6/notes.json" ) ) { @@ -425,8 +427,10 @@ export default class LayerConfig extends WithContextLoader { return mapRendering.GetBaseIcon(this.GetBaseTags()) } - public GetBaseTags(): any { - return TagUtils.changeAsProperties(this.source.osmTags.asChange({ id: "node/-1" })) + public GetBaseTags(): Record { + return TagUtils.changeAsProperties( + this.source?.osmTags?.asChange({ id: "node/-1" }) ?? [{ k: "id", v: "node/-1" }] + ) } public GenerateDocumentation( diff --git a/Models/ThemeConfig/PointRenderingConfig.ts b/Models/ThemeConfig/PointRenderingConfig.ts index f13daa4ea..11d0e0953 100644 --- a/Models/ThemeConfig/PointRenderingConfig.ts +++ b/Models/ThemeConfig/PointRenderingConfig.ts @@ -11,6 +11,7 @@ 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 { html } from "svelte/types/compiler/utils/namespaces" export default class PointRenderingConfig extends WithContextLoader { private static readonly allowed_location_codes = new Set([ @@ -28,6 +29,8 @@ export default class PointRenderingConfig extends WithContextLoader { public readonly iconBadges: { if: TagsFilter; then: TagRenderingConfig }[] public readonly iconSize: TagRenderingConfig public readonly label: TagRenderingConfig + public readonly labelCss: TagRenderingConfig + public readonly labelCssClasses: TagRenderingConfig public readonly rotation: TagRenderingConfig public readonly cssDef: TagRenderingConfig public readonly cssClasses?: TagRenderingConfig @@ -72,6 +75,8 @@ export default class PointRenderingConfig extends WithContextLoader { this.cssDef = this.tr("css", undefined) } this.cssClasses = this.tr("cssClasses", undefined) + this.labelCss = this.tr("labelCss", undefined) + this.labelCssClasses = this.tr("labelCssClasses", undefined) this.iconBadges = (json.iconBadges ?? []).map((overlay, i) => { return { if: TagUtils.Tag(overlay.if), @@ -150,7 +155,7 @@ export default class PointRenderingConfig extends WithContextLoader { } } - public GetBaseIcon(tags?: any): BaseUIElement { + public GetBaseIcon(tags?: Record): BaseUIElement { tags = tags ?? { id: "node/-1" } let defaultPin: BaseUIElement = undefined if (this.label === undefined) { @@ -168,6 +173,10 @@ export default class PointRenderingConfig extends WithContextLoader { // This layer doesn't want to show an icon right now return undefined } + if (htmlDefs.startsWith("<") && htmlDefs.endsWith(">")) { + // This is probably already prepared HTML + return new FixedUiElement(Utils.SubstituteKeys(htmlDefs, tags)) + } return PointRenderingConfig.FromHtmlMulti(htmlDefs, rotation, false, defaultPin) } @@ -225,10 +234,10 @@ export default class PointRenderingConfig extends WithContextLoader { } if (mode === "top") { - anchorH = -iconH / 2 + anchorH = iconH / 2 } if (mode === "bottom") { - anchorH = iconH / 2 + anchorH = -iconH / 2 } const icon = this.GetSimpleIcon(tags) @@ -244,10 +253,11 @@ export default class PointRenderingConfig extends WithContextLoader { iconAndBadges.SetClass("w-full h-full") } - const css = this.cssDef?.GetRenderValue(tags)?.txt - const cssClasses = this.cssClasses?.GetRenderValue(tags)?.txt + const css = this.cssDef?.GetRenderValue(tags.data)?.txt + const cssClasses = this.cssClasses?.GetRenderValue(tags.data)?.txt let label = this.GetLabel(tags) + let htmlEl: BaseUIElement if (icon === undefined && label === undefined) { htmlEl = undefined @@ -288,6 +298,12 @@ export default class PointRenderingConfig extends WithContextLoader { badge.then.GetRenderValue(tags)?.txt, tags ) + if (htmlDefs.startsWith("<") && htmlDefs.endsWith(">")) { + // This is probably an HTML-element + return new FixedUiElement(Utils.SubstituteKeys(htmlDefs, tags)) + .SetStyle("width: 1.5rem") + .SetClass("block") + } const badgeElement = PointRenderingConfig.FromHtmlMulti( htmlDefs, "0", @@ -308,14 +324,20 @@ export default class PointRenderingConfig extends WithContextLoader { if (this.label === undefined) { return undefined } + const cssLabel = this.labelCss?.GetRenderValue(tags.data)?.txt + const cssClassesLabel = this.labelCssClasses?.GetRenderValue(tags.data)?.txt const self = this return new VariableUiElement( tags.map((tags) => { const label = self.label ?.GetRenderValue(tags) ?.Subs(tags) - ?.SetClass("block text-center") - return new Combine([label]).SetClass("flex flex-col items-center mt-1") + ?.SetClass("block center absolute text-center ") + ?.SetClass(cssClassesLabel) + if (cssLabel) { + label.SetStyle(cssLabel) + } + return new Combine([label]).SetClass("flex flex-col items-center") }) ) } diff --git a/Models/ThemeViewState.ts b/Models/ThemeViewState.ts index 1a770b73e..f3b43e0b9 100644 --- a/Models/ThemeViewState.ts +++ b/Models/ThemeViewState.ts @@ -1,7 +1,7 @@ import LayoutConfig from "./ThemeConfig/LayoutConfig" import { SpecialVisualizationState } from "../UI/SpecialVisualization" import { Changes } from "../Logic/Osm/Changes" -import { Store, UIEventSource } from "../Logic/UIEventSource" +import { ImmutableStore, Store, UIEventSource } from "../Logic/UIEventSource" import FeatureSource, { IndexedFeatureSource, WritableFeatureSource, @@ -38,6 +38,7 @@ import Constants from "./Constants" import Hotkeys from "../UI/Base/Hotkeys" import Translations from "../UI/i18n/Translations" import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" +import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource" /** * @@ -105,6 +106,7 @@ export default class ThemeViewState implements SpecialVisualizationState { this.mapProperties, this.userRelatedState.gpsLocationHistoryRetentionTime ) + this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location) this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id) @@ -203,11 +205,45 @@ export default class ThemeViewState implements SpecialVisualizationState { */ private drawSpecialLayers() { type AddedByDefaultTypes = typeof Constants.added_by_default[number] + const empty = [] + { + // The last_click gets a _very_ special treatment + const last_click = new LastClickFeatureSource( + this.mapProperties.lastClickLocation, + this.layout + ) + const last_click_layer = this.layerState.filteredLayers.get("last_click") + this.featureProperties.addSpecial( + "last_click", + new UIEventSource>(last_click.properties) + ) + new ShowDataLayer(this.map, { + features: last_click, + doShowLayer: new ImmutableStore(true), + layer: last_click_layer.layerDef, + selectedElement: this.selectedElement, + selectedLayer: this.selectedLayer, + onClick: (feature: Feature) => { + if (this.mapProperties.zoom.data < Constants.minZoomLevelToAddNewPoint) { + this.map.data.flyTo({ + zoom: Constants.minZoomLevelToAddNewPoint, + center: this.mapProperties.lastClickLocation.data, + }) + return + } + this.selectedElement.setData(feature) + this.selectedLayer.setData(last_click_layer.layerDef) + }, + }) + } + /** * A listing which maps the layerId onto the featureSource */ - const empty = [] - const specialLayers: Record = { + const specialLayers: Record< + Exclude | "current_view", + FeatureSource + > = { home_location: this.userRelatedState.homeLocation, gps_location: this.geolocation.currentUserLocation, gps_location_history: this.geolocation.historicalUserLocations, @@ -261,12 +297,7 @@ export default class ThemeViewState implements SpecialVisualizationState { */ private initActors() { // Various actors that we don't need to reference - new TitleHandler( - this.selectedElement, - this.selectedLayer, - this.featureProperties, - this.layout - ) + new TitleHandler(this.selectedElement, this.selectedLayer, this.featureProperties, this) new ChangeToElementsActor(this.changes, this.featureProperties) new PendingChangesUploader(this.changes, this.selectedElement) new SelectedElementTagsUpdater({ diff --git a/UI/Base/FloatOver.svelte b/UI/Base/FloatOver.svelte new file mode 100644 index 000000000..2242c4578 --- /dev/null +++ b/UI/Base/FloatOver.svelte @@ -0,0 +1,11 @@ + + +
+
+ +
+
diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index 3582848ae..e3471ea5f 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -1,7 +1,7 @@ /** * Asks to add a feature at the last clicked location, at least if zoom is sufficient */ -import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" +import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource" import Svg from "../../Svg" import { SubtleButton } from "../Base/SubtleButton" import Combine from "../Base/Combine" @@ -16,18 +16,14 @@ import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction" import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject" import PresetConfig from "../../Models/ThemeConfig/PresetConfig" import FilteredLayer from "../../Models/FilteredLayer" -import Loc from "../../Models/Loc" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import { Changes } from "../../Logic/Osm/Changes" -import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" -import { ElementStorage } from "../../Logic/ElementStorage" import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint" import Loading from "../Base/Loading" import Hash from "../../Logic/Web/Hash" import { WayId } from "../../Models/OsmFeature" import { Tag } from "../../Logic/Tags/Tag" import { LoginToggle } from "../Popup/LoginButton" -import { GlobalFilter } from "../../Models/GlobalFilter" +import { SpecialVisualizationState } from "../SpecialVisualization" +import { Feature } from "geojson" /* * The SimpleAddUI is a single panel, which can have multiple states: @@ -47,34 +43,8 @@ export interface PresetInfo extends PresetConfig { export default class SimpleAddUI extends LoginToggle { /** * - * @param isShown - * @param resetScrollSignal - * @param filterViewIsOpened - * @param state - * @param takeLocationFrom: defaults to state.lastClickLocation. Take this location to add the new point around */ - constructor( - isShown: UIEventSource, - resetScrollSignal: UIEventSource, - filterViewIsOpened: UIEventSource, - state: { - featureSwitchIsTesting: UIEventSource - featureSwitchUserbadge: Store - layoutToUse: LayoutConfig - osmConnection: OsmConnection - changes: Changes - allElements: ElementStorage - LastClickLocation: UIEventSource<{ lat: number; lon: number }> - featurePipeline: FeaturePipeline - selectedElement: UIEventSource - locationControl: UIEventSource - filteredLayers: UIEventSource - featureSwitchFilter: UIEventSource - backgroundLayer: UIEventSource - globalFilters: UIEventSource - }, - takeLocationFrom?: UIEventSource<{ lat: number; lon: number }> - ) { + constructor(state: SpecialVisualizationState) { const readYourMessages = new Combine([ Translations.t.general.readYourMessages.Clone().SetClass("alert"), new SubtleButton(Svg.envelope_ui(), Translations.t.general.goToInbox, { @@ -83,13 +53,10 @@ export default class SimpleAddUI extends LoginToggle { }), ]) - takeLocationFrom = takeLocationFrom ?? state.LastClickLocation + const filterViewIsOpened = state.guistate.filterViewIsOpened + const takeLocationFrom = state.mapProperties.lastClickLocation const selectedPreset = new UIEventSource(undefined) - selectedPreset.addCallback((_) => { - resetScrollSignal.ping() - }) - isShown.addCallback((_) => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened takeLocationFrom.addCallback((_) => selectedPreset.setData(undefined)) const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset, state) @@ -104,14 +71,13 @@ export default class SimpleAddUI extends LoginToggle { tags.push(new Tag("_referencing_ways", "way/" + snapOntoWay.id)) } const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, { - theme: state.layoutToUse?.id ?? "unkown", + theme: state.layout?.id ?? "unkown", changeType: "create", snapOnto: snapOntoWay, }) await state.changes.applyAction(newElementAction) selectedPreset.setData(undefined) - isShown.setData(false) - const selectedFeature = state.allElements.ContainingFeatures.get( + const selectedFeature: Feature = state.indexedFeatures.featuresById.data.get( newElementAction.newElementId ) state.selectedElement.setData(selectedFeature) @@ -156,7 +122,7 @@ export default class SimpleAddUI extends LoginToggle { confirm, cancel, () => { - isShown.setData(false) + selectedPreset.setData(undefined) }, { cancelIcon: Svg.back_svg(), @@ -172,11 +138,11 @@ export default class SimpleAddUI extends LoginToggle { new Toggle( new Loading(Translations.t.general.add.stillLoading).SetClass("alert"), addUi, - state.featurePipeline.runningQuery + state.dataIsLoading ), Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"), - state.locationControl.map( - (loc) => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints + state.mapProperties.zoom.map( + (zoom) => zoom >= Constants.minZoomLevelToAddNewPoint ) ), readYourMessages, @@ -222,12 +188,7 @@ export default class SimpleAddUI extends LoginToggle { private static CreateAllPresetsPanel( selectedPreset: UIEventSource, - state: { - featureSwitchIsTesting: UIEventSource - filteredLayers: UIEventSource - featureSwitchFilter: UIEventSource - osmConnection: OsmConnection - } + state: SpecialVisualizationState ): BaseUIElement { const presetButtons = SimpleAddUI.CreatePresetButtons(state, selectedPreset) let intro: BaseUIElement = Translations.t.general.add.intro @@ -260,18 +221,14 @@ export default class SimpleAddUI extends LoginToggle { /* * Generates the list with all the buttons.*/ private static CreatePresetButtons( - state: { - filteredLayers: UIEventSource - featureSwitchFilter: UIEventSource - osmConnection: OsmConnection - }, + state: SpecialVisualizationState, selectedPreset: UIEventSource ): BaseUIElement { const allButtons = [] - for (const layer of state.filteredLayers.data) { + for (const layer of Array.from(state.layerState.filteredLayers.values())) { if (layer.isDisplayed.data === false) { // The layer is not displayed... - if (!state.featureSwitchFilter.data) { + if (!state.featureSwitches.featureSwitchFilter.data) { // ...and we cannot enable the layer control -> we skip, as these presets can never be shown anyway continue } diff --git a/UI/DefaultGUI.ts b/UI/DefaultGUI.ts index 5de8eb26b..8f3149882 100644 --- a/UI/DefaultGUI.ts +++ b/UI/DefaultGUI.ts @@ -48,7 +48,6 @@ export default class DefaultGUI { public setup() { this.SetupUIElements() this.SetupMap() - ScrollableFullScreen.ActivateCurrent() if ( this.state.layoutToUse.customCss !== undefined && @@ -168,13 +167,6 @@ export default class DefaultGUI { features: state.selectedElementsLayer, state, }) - - state.leafletMap.addCallbackAndRunD((_) => { - // Lets assume that all showDataLayers are initialized at this point - state.selectedElement.ping() - State.state.locationControl.ping() - return true - }) } private SetupUIElements() { diff --git a/UI/Map/ShowDataLayer.ts b/UI/Map/ShowDataLayer.ts index 94194aadb..c232d012c 100644 --- a/UI/Map/ShowDataLayer.ts +++ b/UI/Map/ShowDataLayer.ts @@ -20,11 +20,12 @@ import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeature class PointRenderingLayer { private readonly _config: PointRenderingConfig + private readonly _visibility?: Store private readonly _fetchStore?: (id: string) => Store> private readonly _map: MlMap private readonly _onClick: (feature: Feature) => void private readonly _allMarkers: Map = new Map() - + private _dirty = false constructor( map: MlMap, features: FeatureSource, @@ -33,6 +34,7 @@ class PointRenderingLayer { fetchStore?: (id: string) => Store>, onClick?: (feature: Feature) => void ) { + this._visibility = visibility this._config = config this._map = map this._fetchStore = fetchStore @@ -40,10 +42,20 @@ class PointRenderingLayer { const self = this features.features.addCallbackAndRunD((features) => self.updateFeatures(features)) - visibility?.addCallbackAndRunD((visible) => self.setVisibility(visible)) + visibility?.addCallbackAndRunD((visible) => { + if (visible === true && self._dirty) { + self.updateFeatures(features.features.data) + } + self.setVisibility(visible) + }) } private updateFeatures(features: Feature[]) { + if (this._visibility?.data === false) { + this._dirty = true + return + } + this._dirty = false const cache = this._allMarkers const unseenKeys = new Set(cache.keys()) for (const location of this._config.location) { @@ -58,6 +70,9 @@ class PointRenderingLayer { this._config ) } + const id = feature.properties.id + "-" + location + unseenKeys.delete(id) + const loc = GeoOperations.featureToCoordinateWithRenderingType( feature, location @@ -65,8 +80,6 @@ class PointRenderingLayer { if (loc === undefined) { continue } - const id = feature.properties.id + "-" + location - unseenKeys.delete(id) if (cache.has(id)) { const cached = cache.get(id) @@ -357,10 +370,12 @@ export default class ShowDataLayer { private initDrawFeatures(map: MlMap) { let { features, doShowLayer, fetchStore, selectedElement, selectedLayer } = this._options - const onClick = (feature: Feature) => { - selectedElement?.setData(feature) - selectedLayer?.setData(this._options.layer) - } + const onClick = + this._options.onClick ?? + ((feature: Feature) => { + selectedElement?.setData(feature) + selectedLayer?.setData(this._options.layer) + }) for (let i = 0; i < this._options.layer.lineRendering.length; i++) { const lineRenderingConfig = this._options.layer.lineRendering[i] new LineRenderingLayer( diff --git a/UI/Map/ShowDataLayerOptions.ts b/UI/Map/ShowDataLayerOptions.ts index b81175d90..0f46b27c2 100644 --- a/UI/Map/ShowDataLayerOptions.ts +++ b/UI/Map/ShowDataLayerOptions.ts @@ -33,4 +33,6 @@ export interface ShowDataLayerOptions { * If given, the map will update when a property is changed */ fetchStore?: (id: string) => Store> + + onClick?: (feature: Feature) => void } diff --git a/UI/Popup/NearbyImageVis.ts b/UI/Popup/NearbyImageVis.ts index 52e359c3b..194a058ac 100644 --- a/UI/Popup/NearbyImageVis.ts +++ b/UI/Popup/NearbyImageVis.ts @@ -18,6 +18,8 @@ import Toggle from "../Input/Toggle" import Title from "../Base/Title" import { MapillaryLinkVis } from "./MapillaryLinkVis" import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" +import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import { Feature } from "geojson" export class NearbyImageVis implements SpecialVisualization { args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [ @@ -39,11 +41,12 @@ export class NearbyImageVis implements SpecialVisualization { constr( state: SpecialVisualizationState, tagSource: UIEventSource>, - args: string[] + args: string[], + feature: Feature, + layer: LayerConfig ): BaseUIElement { const t = Translations.t.image.nearbyPictures const mode: "open" | "expandable" | "collapsable" = args[0] - const feature = state.indexedFeatures.featuresById.data.get(tagSource.data.id) const [lon, lat] = GeoOperations.centerpointCoordinates(feature) const id: string = tagSource.data["id"] const canBeEdited: boolean = !!id?.match("(node|way|relation)/-?[0-9]+") @@ -128,7 +131,7 @@ export class NearbyImageVis implements SpecialVisualization { slideshow, controls, saveButton, - new MapillaryLinkVis().constr(state, tagSource, []).SetClass("mt-6"), + new MapillaryLinkVis().constr(state, tagSource, [], feature).SetClass("mt-6"), ]) }) diff --git a/UI/Popup/TagRendering/Inline.svelte b/UI/Popup/TagRendering/Inline.svelte index 2d1655049..798d39ef9 100644 --- a/UI/Popup/TagRendering/Inline.svelte +++ b/UI/Popup/TagRendering/Inline.svelte @@ -4,6 +4,7 @@ import { onDestroy } from "svelte"; import { Translation } from "../../i18n/Translation"; import Locale from "../../i18n/Locale"; + import FromHtml from "../../Base/FromHtml.svelte"; export let template: Translation; let _template: string @@ -20,7 +21,7 @@ - {Utils.SubstituteKeys(before, _tags)} + - {Utils.SubstituteKeys(after, _tags)} + diff --git a/UI/Popup/TagRendering/Questionbox.svelte b/UI/Popup/TagRendering/Questionbox.svelte index 2fa28653e..e8f4dddec 100644 --- a/UI/Popup/TagRendering/Questionbox.svelte +++ b/UI/Popup/TagRendering/Questionbox.svelte @@ -41,10 +41,7 @@ return true; } - console.log("Got layer", layer, onlyForLabels, notForLabels); - const baseQuestions = (layer.tagRenderings ?? [])?.filter(tr => allowed(tr.labels) && tr.question !== undefined); - console.log("BaseQuestions are", baseQuestions); let skippedQuestions = new UIEventSource>(new Set()); let questionsToAsk = tags.map(tags => { diff --git a/UI/Popup/TagRendering/TagRenderingQuestion.svelte b/UI/Popup/TagRendering/TagRenderingQuestion.svelte index 695b49f7a..397c8218b 100644 --- a/UI/Popup/TagRendering/TagRenderingQuestion.svelte +++ b/UI/Popup/TagRendering/TagRenderingQuestion.svelte @@ -15,6 +15,7 @@ import { createEventDispatcher } from "svelte"; import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid"; + import SpecialTranslation from "./SpecialTranslation.svelte"; export let config: TagRenderingConfig; export let tags: UIEventSource>; @@ -86,15 +87,15 @@
- + {config.id}
- +
{#if config.questionhint}
- +
{/if} diff --git a/UI/SpecialVisualization.ts b/UI/SpecialVisualization.ts index 60e8fd209..5c8bab79c 100644 --- a/UI/SpecialVisualization.ts +++ b/UI/SpecialVisualization.ts @@ -12,6 +12,7 @@ import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/Fu import { MangroveIdentity } from "../Logic/Web/MangroveReviews" import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" import LayerConfig from "../Models/ThemeConfig/LayerConfig" +import FeatureSwitchState from "../Logic/State/FeatureSwitchState" /** * The state needed to render a special Visualisation. @@ -19,6 +20,7 @@ import LayerConfig from "../Models/ThemeConfig/LayerConfig" export interface SpecialVisualizationState { readonly guistate: DefaultGuiState readonly layout: LayoutConfig + readonly featureSwitches: FeatureSwitchState readonly layerState: LayerState readonly featureProperties: { getStore(id: string): UIEventSource> } diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 6c06a7ecc..2902af09e 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -56,6 +56,7 @@ import Maproulette from "../Logic/Maproulette" import SvelteUIElement from "./Base/SvelteUIElement" import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" import QuestionViz from "./Popup/QuestionViz" +import SimpleAddUI from "./BigComponents/SimpleAddUI" export default class SpecialVisualizations { public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList() @@ -232,6 +233,15 @@ export default class SpecialVisualizations { private static initList(): SpecialVisualization[] { const specialVisualizations: SpecialVisualization[] = [ new QuestionViz(), + { + funcName: "add_new_point", + docs: "An element which allows to add a new point on the 'last_click'-location. Only makes sense in the layer `last_click`", + args: [], + constr(state: SpecialVisualizationState): BaseUIElement { + return new SimpleAddUI(state) + }, + }, + new HistogramViz(), new StealViz(), new MinimapViz(), @@ -670,7 +680,9 @@ export default class SpecialVisualizations { if (title === undefined) { return undefined } - return new SubstitutedTranslation(title, tagsSource, state) + return new SubstitutedTranslation(title, tagsSource, state).RemoveClass( + "w-full" + ) }) ), }, diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index b14348ccc..c1654fe26 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -11,6 +11,7 @@ import LinkToWeblate from "./Base/LinkToWeblate" import { SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization" import SpecialVisualizations from "./SpecialVisualizations" import { Feature } from "geojson" +import LayerConfig from "../Models/ThemeConfig/LayerConfig" export class SubstitutedTranslation extends VariableUiElement { public constructor( @@ -24,7 +25,8 @@ export class SubstitutedTranslation extends VariableUiElement { state: SpecialVisualizationState, tagSource: UIEventSource>, argument: string[], - feature: Feature + feature: Feature, + layer: LayerConfig ) => BaseUIElement) > = undefined ) { @@ -85,7 +87,7 @@ export class SubstitutedTranslation extends VariableUiElement { tagsSource.data.id ) return viz.func - .constr(state, tagsSource, proto.args, feature) + .constr(state, tagsSource, proto.args, feature, undefined) ?.SetStyle(proto.style) } catch (e) { console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, e) diff --git a/UI/ThemeViewGUI.svelte b/UI/ThemeViewGUI.svelte index bbd65b805..3c644daab 100644 --- a/UI/ThemeViewGUI.svelte +++ b/UI/ThemeViewGUI.svelte @@ -1,5 +1,5 @@