Merge pull request #1731 from pietervdvn/feature/favourites

Feature/favourites
This commit is contained in:
Pieter Vander Vennet 2023-12-05 00:12:29 +01:00 committed by GitHub
commit 4197ec0055
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
80 changed files with 2715 additions and 1059 deletions

View file

@ -11,7 +11,7 @@
<button class={clss} on:click={() => osmConnection.AttemptLogin()}>
<ToSvelte construct={Svg.login_svg().SetClass("w-12 m-1")} />
<slot name="message">
<slot>
<Tr t={Translations.t.general.loginWithOpenStreetMap} />
</slot>
</button>

View file

@ -4,12 +4,12 @@
import Translations from "../i18n/Translations"
import Tr from "./Tr.svelte"
export let osmConnection: OsmConnection
export let osmConnection: OsmConnection;
</script>
<button
on:click={() => {
state.osmConnection.LogOut()
osmConnection.LogOut()
}}
>
<Logout class="h-6 w-6" />

View file

@ -9,7 +9,7 @@
const uiElem = typeof construct === "function" ? construct() : construct
html = uiElem?.ConstructElement()
if (html !== undefined) {
elem.replaceWith(html)
elem?.replaceWith(html)
}
})

View file

@ -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

View file

@ -0,0 +1,83 @@
<script lang="ts">
import type { SpecialVisualizationState } from "../SpecialVisualization";
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte";
import type { Feature } from "geojson";
import { ImmutableStore } from "../../Logic/UIEventSource";
import { GeoOperations } from "../../Logic/GeoOperations";
import Center from "../../assets/svg/Center.svelte";
export let feature: Feature;
let properties: Record<string, string> = feature.properties;
export let state: SpecialVisualizationState;
let tags = state.featureProperties.getStore(properties.id) ?? new ImmutableStore(properties);
const favLayer = state.layerState.filteredLayers.get("favourite");
const favConfig = favLayer.layerDef;
const titleConfig = favConfig.title;
function center() {
const [lon, lat] = GeoOperations.centerpointCoordinates(feature);
state.mapProperties.location.setData(
{ lon, lat }
);
const z = state.mapProperties.zoom.data
state.mapProperties.zoom.setData( Math.min(17, Math.max(12, z )) )
state.guistate.menuIsOpened.setData(false);
}
function select() {
state.selectedLayer.setData(favConfig);
state.selectedElement.setData(feature);
center();
}
const coord = GeoOperations.centerpointCoordinates(feature);
const distance = state.mapProperties.location.stabilized(500).mapD(({ lon, lat }) => {
let meters = Math.round(GeoOperations.distanceBetween(coord, [lon, lat]));
if (meters < 1000) {
return meters + "m";
}
meters = Math.round(meters / 100);
const kmStr = "" + meters;
return kmStr.substring(0, kmStr.length - 1) + "." + kmStr.substring(kmStr.length - 1) + "km";
});
const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"];
</script>
<div class="px-1 my-1 border-2 border-dashed border-gray-300 rounded grid grid-cols-2 items-center no-weblate">
<button class="cursor-pointer ml-1 m-0 link justify-self-start" on:click={() => select()}>
<TagRenderingAnswer config={titleConfig} extraClasses="underline" layer={favConfig} selectedElement={feature}
{tags} />
</button>
<div class="flex items-center justify-self-end title-icons links-as-button gap-x-0.5 p-1 pt-0.5 sm:pt-1">
{#each favConfig.titleIcons as titleIconConfig}
{#if (titleIconBlacklist.indexOf(titleIconConfig.id) < 0) && (titleIconConfig.condition?.matchesProperties(properties) ?? true) && (titleIconConfig.metacondition?.matchesProperties({ ...properties, ...state.userRelatedState.preferencesAsTags.data }) ?? true) && titleIconConfig.IsKnown(properties)}
<div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}>
<TagRenderingAnswer
config={titleIconConfig}
{tags}
selectedElement={feature}
{state}
layer={favLayer}
extraClasses="h-full justify-center"
/>
</div>
{/if}
{/each}
<button class="p-1" on:click={() => center()}>
<Center class="w-6 h-6" />
</button>
<div class="w-14">
{$distance}
</div>
</div>
</div>

View file

@ -0,0 +1,68 @@
<script lang="ts">
import type { SpecialVisualizationState } from "../SpecialVisualization";
import FavouriteSummary from "./FavouriteSummary.svelte";
import Translations from "../i18n/Translations";
import Tr from "../Base/Tr.svelte";
import { DownloadIcon } from "@rgossiaux/svelte-heroicons/solid";
import { Utils } from "../../Utils";
import { GeoOperations } from "../../Logic/GeoOperations";
import type { Feature, LineString, Point } from "geojson";
import LoginToggle from "../Base/LoginToggle.svelte";
import LoginButton from "../Base/LoginButton.svelte";
/**
* A panel showing all your favourites
*/
export let state: SpecialVisualizationState;
let favourites = state.favourites.allFavourites;
function downloadGeojson() {
const contents = { features: favourites.data, type: "FeatureCollection" };
Utils.offerContentsAsDownloadableFile(
JSON.stringify(contents),
"mapcomplete-favourites-" + (new Date().toISOString()) + ".geojson",
{
mimetype: "application/vnd.geo+json"
}
);
}
function downloadGPX() {
const gpx = GeoOperations.toGpxPoints(<Feature<Point>>favourites.data, "MapComplete favourites");
Utils.offerContentsAsDownloadableFile(gpx,
"mapcomplete-favourites-" + (new Date().toISOString()) + ".gpx",
{
mimetype: "{gpx=application/gpx+xml}"
});
}
</script>
<LoginToggle {state}>
<div slot="not-logged-in">
<LoginButton osmConnection={state.osmConnection}>
<Tr t={Translations.t.favouritePoi.loginToSeeList}/>
</LoginButton>
</div>
<div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}>
<Tr t={Translations.t.favouritePoi.intro.Subs({length: $favourites?.length ?? 0})} />
<Tr t={Translations.t.favouritePoi.privacy} />
{#each $favourites as feature (feature.properties.id)}
<FavouriteSummary {feature} {state} />
{/each}
<div class="mt-8">
<button class="flex p-2" on:click={() => downloadGeojson()}>
<DownloadIcon class="h-6 w-6" />
<Tr t={Translations.t.favouritePoi.downloadGeojson} />
</button>
<button class="flex p-2" on:click={() => downloadGPX()}>
<DownloadIcon class="h-6 w-6" />
<Tr t={Translations.t.favouritePoi.downloadGpx} />
</button>
</div>
</div>
</LoginToggle>

View file

@ -1,27 +1,7 @@
<script lang="ts">
import { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig"
import { Store } from "../../Logic/UIEventSource"
import Pin from "../../assets/svg/Pin.svelte"
import Square from "../../assets/svg/Square.svelte"
import Circle from "../../assets/svg/Circle.svelte"
import Checkmark from "../../assets/svg/Checkmark.svelte"
import Clock from "../../assets/svg/Clock.svelte"
import Close from "../../assets/svg/Close.svelte"
import Crosshair from "../../assets/svg/Crosshair.svelte"
import Help from "../../assets/svg/Help.svelte"
import Home from "../../assets/svg/Home.svelte"
import Invalid from "../../assets/svg/Invalid.svelte"
import Location from "../../assets/svg/Location.svelte"
import Location_empty from "../../assets/svg/Location_empty.svelte"
import Location_locked from "../../assets/svg/Location_locked.svelte"
import Note from "../../assets/svg/Note.svelte"
import Resolved from "../../assets/svg/Resolved.svelte"
import Ring from "../../assets/svg/Ring.svelte"
import Scissors from "../../assets/svg/Scissors.svelte"
import Teardrop from "../../assets/svg/Teardrop.svelte"
import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte"
import Triangle from "../../assets/svg/Triangle.svelte"
import Icon from "./Icon.svelte"
import { IconConfig } from "../../Models/ThemeConfig/PointRenderingConfig";
import { Store } from "../../Logic/UIEventSource";
import Icon from "./Icon.svelte";
/**
* Renders a single icon.

View file

@ -7,9 +7,9 @@
/**
* Renders a 'marker', which consists of multiple 'icons'
*/
export let marker: IconConfig[] = config?.marker
export let marker: IconConfig[] = config?.marker;
export let tags: Store<Record<string, string>>
export let rotation: TagRenderingConfig
export let rotation: TagRenderingConfig = undefined;
let _rotation = rotation
? tags.map((tags) => rotation.GetRenderValue(tags).Subs(tags).txt)
: new ImmutableStore(0)
@ -18,7 +18,9 @@
{#if marker && marker}
<div class="relative h-full w-full" style={`transform: rotate(${$_rotation})`}>
{#each marker as icon}
<DynamicIcon {icon} {tags} />
<div class="absolute top-0 left-0 h-full w-full">
<DynamicIcon {icon} {tags} />
</div>
{/each}
</div>
{/if}

View file

@ -1,27 +1,29 @@
<script lang="ts">
import Pin from "../../assets/svg/Pin.svelte"
import Square from "../../assets/svg/Square.svelte"
import Circle from "../../assets/svg/Circle.svelte"
import Checkmark from "../../assets/svg/Checkmark.svelte"
import Clock from "../../assets/svg/Clock.svelte"
import Close from "../../assets/svg/Close.svelte"
import Crosshair from "../../assets/svg/Crosshair.svelte"
import Help from "../../assets/svg/Help.svelte"
import Home from "../../assets/svg/Home.svelte"
import Invalid from "../../assets/svg/Invalid.svelte"
import Location from "../../assets/svg/Location.svelte"
import Location_empty from "../../assets/svg/Location_empty.svelte"
import Location_locked from "../../assets/svg/Location_locked.svelte"
import Note from "../../assets/svg/Note.svelte"
import Resolved from "../../assets/svg/Resolved.svelte"
import Ring from "../../assets/svg/Ring.svelte"
import Scissors from "../../assets/svg/Scissors.svelte"
import Teardrop from "../../assets/svg/Teardrop.svelte"
import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte"
import Triangle from "../../assets/svg/Triangle.svelte"
import Brick_wall_square from "../../assets/svg/Brick_wall_square.svelte"
import Brick_wall_round from "../../assets/svg/Brick_wall_round.svelte"
import Gps_arrow from "../../assets/svg/Gps_arrow.svelte"
import Pin from "../../assets/svg/Pin.svelte";
import Square from "../../assets/svg/Square.svelte";
import Circle from "../../assets/svg/Circle.svelte";
import Checkmark from "../../assets/svg/Checkmark.svelte";
import Clock from "../../assets/svg/Clock.svelte";
import Close from "../../assets/svg/Close.svelte";
import Crosshair from "../../assets/svg/Crosshair.svelte";
import Help from "../../assets/svg/Help.svelte";
import Home from "../../assets/svg/Home.svelte";
import Invalid from "../../assets/svg/Invalid.svelte";
import Location from "../../assets/svg/Location.svelte";
import Location_empty from "../../assets/svg/Location_empty.svelte";
import Location_locked from "../../assets/svg/Location_locked.svelte";
import Note from "../../assets/svg/Note.svelte";
import Resolved from "../../assets/svg/Resolved.svelte";
import Ring from "../../assets/svg/Ring.svelte";
import Scissors from "../../assets/svg/Scissors.svelte";
import Teardrop from "../../assets/svg/Teardrop.svelte";
import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte";
import Triangle from "../../assets/svg/Triangle.svelte";
import Brick_wall_square from "../../assets/svg/Brick_wall_square.svelte";
import Brick_wall_round from "../../assets/svg/Brick_wall_round.svelte";
import Gps_arrow from "../../assets/svg/Gps_arrow.svelte";
import { HeartIcon } from "@babeard/svelte-heroicons/solid";
import { HeartIcon as HeartOutlineIcon } from "@babeard/svelte-heroicons/outline";
/**
* Renders a single icon.
@ -29,68 +31,72 @@
* Icons -placed on top of each other- form a 'Marker' together
*/
export let icon: string | undefined
export let color: string | undefined
export let icon: string | undefined;
export let color: string | undefined = undefined
export let clss: string | undefined = undefined
</script>
{#if icon}
<div class="absolute top-0 left-0 h-full w-full">
{#if icon === "pin"}
<Pin {color} />
<Pin {color} class={clss}/>
{:else if icon === "square"}
<Square {color} />
<Square {color} class={clss}/>
{:else if icon === "circle"}
<Circle {color} />
<Circle {color} class={clss}/>
{:else if icon === "checkmark"}
<Checkmark {color} />
<Checkmark {color} class={clss}/>
{:else if icon === "clock"}
<Clock {color} />
<Clock {color} class={clss}/>
{:else if icon === "close"}
<Close {color} />
<Close {color} class={clss}/>
{:else if icon === "crosshair"}
<Crosshair {color} />
<Crosshair {color} class={clss}/>
{:else if icon === "help"}
<Help {color} />
<Help {color} class={clss}/>
{:else if icon === "home"}
<Home {color} />
<Home {color} class={clss}/>
{:else if icon === "invalid"}
<Invalid {color} />
<Invalid {color} class={clss}/>
{:else if icon === "location"}
<Location {color} />
<Location {color} class={clss}/>
{:else if icon === "location_empty"}
<Location_empty {color} />
<Location_empty {color} class={clss}/>
{:else if icon === "location_locked"}
<Location_locked {color} />
<Location_locked {color} class={clss}/>
{:else if icon === "note"}
<Note {color} />
<Note {color} class={clss}/>
{:else if icon === "resolved"}
<Resolved {color} />
<Resolved {color} class={clss}/>
{:else if icon === "ring"}
<Ring {color} />
<Ring {color} class={clss}/>
{:else if icon === "scissors"}
<Scissors {color} />
<Scissors {color} class={clss}/>
{:else if icon === "teardrop"}
<Teardrop {color} />
<Teardrop {color} class={clss}/>
{:else if icon === "teardrop_with_hole_green"}
<Teardrop_with_hole_green {color} />
<Teardrop_with_hole_green {color} class={clss}/>
{:else if icon === "triangle"}
<Triangle {color} />
<Triangle {color} class={clss}/>
{:else if icon === "brick_wall_square"}
<Brick_wall_square {color} />
<Brick_wall_square {color} class={clss}/>
{:else if icon === "brick_wall_round"}
<Brick_wall_round {color} />
<Brick_wall_round {color} class={clss}/>
{:else if icon === "gps_arrow"}
<Gps_arrow {color} />
<Gps_arrow {color} class={clss}/>
{:else if icon === "checkmark"}
<Checkmark {color} />
<Checkmark {color} class={clss}/>
{:else if icon === "help"}
<Help {color} />
<Help {color} class={clss}/>
{:else if icon === "close"}
<Close {color} />
<Close {color} class={clss}/>
{:else if icon === "invalid"}
<Invalid {color} />
<Invalid {color} class={clss}/>
{:else if icon === "heart"}
<HeartIcon class={clss}/>
{:else if icon === "heart_outline"}
<HeartOutlineIcon class={clss}/>
{:else}
<img class="h-full w-full" src={icon} />
<img class={clss ?? "h-full w-full"} src={icon} aria-hidden="true"
alt="" />
{/if}
</div>
{/if}

View file

@ -1,16 +1,18 @@
<script lang="ts">
import Icon from "./Icon.svelte"
import Icon from "./Icon.svelte";
/**
* Renders a 'marker', which consists of multiple 'icons'
*/
export let icons: { icon: string; color: string }[]
export let icons: { icon: string; color: string }[];
</script>
{#if icons !== undefined && icons.length > 0}
<div class="relative h-full w-full">
{#each icons as icon}
<Icon icon={icon.icon} color={icon.color} />
<div class="absolute top-0 left-0 h-full w-full">
<Icon icon={icon.icon} color={icon.color} />
</div>
{/each}
</div>
{/if}

View file

@ -12,11 +12,9 @@ import { Feature, Point } from "geojson"
import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig"
import { Utils } from "../../Utils"
import * as range_layer from "../../../assets/layers/range/range.json"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
import FilteredLayer from "../../Models/FilteredLayer"
import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource"
import { CLIENT_RENEG_LIMIT } from "tls"
class PointRenderingLayer {
private readonly _config: PointRenderingConfig

View file

@ -0,0 +1,50 @@
<script lang="ts">/**
* Simple visualisation which shows when the POI opens/closes next.
*/
import type { SpecialVisualizationState } from "../SpecialVisualization";
import { Store, Stores } from "../../Logic/UIEventSource";
import { OH } from "./OpeningHours";
import opening_hours from "opening_hours";
import Clock from "../../assets/svg/Clock.svelte";
import { Utils } from "../../Utils";
import Circle from "../../assets/svg/Circle.svelte";
import Ring from "../../assets/svg/Ring.svelte";
import { twMerge } from "tailwind-merge";
export let state: SpecialVisualizationState;
export let tags: Store<Record<string, string>>;
export let keyToUse: string = "opening_hours";
export let prefix: string = undefined;
export let postfix: string = undefined;
let oh: Store<opening_hours | "error" | undefined> = OH.CreateOhObjectStore(tags, keyToUse, prefix, postfix);
let currentState = oh.mapD(oh => typeof oh === "string" ? undefined : oh.getState());
let tomorrow = new Date();
tomorrow.setTime(tomorrow.getTime() + 24 * 60 * 60 * 1000);
let nextChange = oh
.mapD(oh => typeof oh === "string" ? undefined : oh.getNextChange(new Date(), tomorrow), [Stores.Chronic(5 * 60 * 1000)])
.mapD(date => Utils.TwoDigits(date.getHours()) + ":" + Utils.TwoDigits(date.getMinutes()));
let size = nextChange.map(change => change === undefined ? "absolute h-7 w-7" : "absolute h-5 w-5 top-0 left-1/4");
</script>
{#if $currentState !== undefined}
<div class="relative h-8 w-8">
{#if $currentState === true}
<Ring class={$size} color="#0f0" style="z-index: 0" />
<Clock class={$size} color="#0f0" style="z-index: 0" />
{:else if $currentState === false}
<Circle class={$size} color="#f00" style="z-index: 0" />
<Clock class={$size} color="#fff" style="z-index: 0" />
{/if}
{#if $nextChange !== undefined}
<span class="absolute bottom-0 font-bold text-sm" style="z-index: 1; background-color: #ffffff88; margin-top: 3px">
{$nextChange}
</span>
{/if}
</div>
{/if}

View file

@ -1,5 +1,6 @@
import { Utils } from "../../Utils"
import opening_hours from "opening_hours"
import { Store } from "../../Logic/UIEventSource"
export interface OpeningHour {
weekday: number // 0 is monday, 1 is tuesday, ...
@ -494,10 +495,48 @@ This list will be sorted
return [changeHours, changeHourText]
}
public static CreateOhObjectStore(
tags: Store<Record<string, string>>,
key: string = "opening_hours",
prefixToIgnore?: string,
postfixToIgnore?: string
): Store<opening_hours | undefined | "error"> {
prefixToIgnore ??= ""
postfixToIgnore ??= ""
const country = tags.map((tags) => tags._country)
return tags
.mapD((tags) => {
const value: string = tags[key]
if (value === undefined) {
return undefined
}
if (
(prefixToIgnore || postfixToIgnore) &&
value.startsWith(prefixToIgnore) &&
value.endsWith(postfixToIgnore)
) {
return value
.substring(prefixToIgnore.length, value.length - postfixToIgnore.length)
.trim()
}
return value
})
.mapD(
(ohtext) => {
try {
return OH.CreateOhObject(<any>tags.data, ohtext, country.data)
} catch (e) {
return "error"
}
},
[country]
)
}
public static CreateOhObject(
tags: Record<string, string> & { _lat: number; _lon: number; _country?: string },
textToParse: string
textToParse: string,
country?: string
) {
// noinspection JSPotentiallyInvalidConstructorUsage
return new opening_hours(
@ -506,7 +545,7 @@ This list will be sorted
lat: tags._lat,
lon: tags._lon,
address: {
country_code: tags._country?.toLowerCase(),
country_code: country.toLowerCase(),
state: undefined,
},
},

View file

@ -3,7 +3,6 @@ import Combine from "../Base/Combine"
import { FixedUiElement } from "../Base/FixedUiElement"
import { OH } from "./OpeningHours"
import Translations from "../i18n/Translations"
import Constants from "../../Models/Constants"
import BaseUIElement from "../BaseUIElement"
import Toggle from "../Input/Toggle"
import { VariableUiElement } from "../Base/VariableUIElement"
@ -30,48 +29,20 @@ export default class OpeningHoursVisualization extends Toggle {
prefix = "",
postfix = ""
) {
const country = tags.map((tags) => tags._country)
const openingHoursStore = OH.CreateOhObjectStore(tags, key, prefix, postfix)
const ohTable = new VariableUiElement(
tags
.map((tags) => {
const value: string = tags[key]
if (value === undefined) {
return undefined
}
if (value.startsWith(prefix) && value.endsWith(postfix)) {
return value.substring(prefix.length, value.length - postfix.length).trim()
}
return value
}) // This mapping will absorb all other changes to tags in order to prevent regeneration
.map(
(ohtext) => {
if (ohtext === undefined) {
return new FixedUiElement(
"No opening hours defined with key " + key
).SetClass("alert")
}
try {
return OpeningHoursVisualization.CreateFullVisualisation(
OH.CreateOhObject(<any>tags.data, ohtext)
)
} catch (e) {
console.warn(e, e.stack)
return new Combine([
Translations.t.general.opening_hours.error_loading,
new Toggle(
new FixedUiElement(e).SetClass("subtle"),
undefined,
state?.osmConnection?.userDetails.map(
(userdetails) =>
userdetails.csCount >=
Constants.userJourney.tagsVisibleAndWikiLinked
)
),
])
}
},
[country]
)
openingHoursStore.map((opening_hours_obj) => {
if (opening_hours_obj === undefined) {
return new FixedUiElement("No opening hours defined with key " + key).SetClass(
"alert"
)
}
if (opening_hours_obj === "error") {
return Translations.t.general.opening_hours.error_loading
}
return OpeningHoursVisualization.CreateFullVisualisation(opening_hours_obj)
})
)
super(

View file

@ -161,7 +161,7 @@
2. What do we want to add?
3. Are all elements of this category visible? (i.e. there are no filters possibly hiding this, is the data still loading, ...) -->
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in">
<Tr slot="message" t={Translations.t.general.add.pleaseLogin} />
<Tr t={Translations.t.general.add.pleaseLogin} />
</LoginButton>
<div class="h-full w-full">
{#if $zoom < Constants.minZoomLevelToAddNewPoint}

View file

@ -19,7 +19,7 @@
...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ?? [])
)
const allTags = tags.map((tags) => {
const allTags = tags.mapD((tags) => {
const parts: (string | BaseUIElement)[][] = []
for (const key in tags) {
let v = tags[key]

View file

@ -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<LineString>>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<LineString>>feature, title)
Utils.offerContentsAsDownloadableFile(gpx, title + "_mapcomplete_export.gpx", {
mimetype: "{gpx=application/gpx+xml}",
})
})
})
}
}

View file

@ -28,7 +28,6 @@
const t = Translations.t.image.nearby
const c = [lon, lat]
console.log(">>>", image)
let attributedImage = new AttributedImage({
url: image.thumbUrl ?? image.pictureUrl,
provider: AllImageProviders.byName(image.provider),
@ -45,7 +44,7 @@
const url = image.osmTags[key]
if (isLinked) {
const action = new LinkImageAction(currentTags.id, key, url, tags, {
theme: state.layout.id,
theme: tags.data._orig_theme ?? state.layout.id,
changeType: "link-image",
})
state.changes.applyAction(action)
@ -54,7 +53,7 @@
const v = currentTags[k]
if (v === url) {
const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, {
theme: state.layout.id,
theme: tags.data._orig_theme ?? state.layout.id,
changeType: "remove-image",
})
state.changes.applyAction(action)

View file

@ -0,0 +1,48 @@
<script lang="ts">
import type { SpecialVisualizationState } from "../SpecialVisualization";
import { HeartIcon as HeartSolidIcon } from "@babeard/svelte-heroicons/solid";
import { HeartIcon as HeartOutlineIcon } from "@babeard/svelte-heroicons/outline";
import Tr from "../Base/Tr.svelte";
import Translations from "../i18n/Translations";
import LoginToggle from "../Base/LoginToggle.svelte";
import type { Feature } from "geojson";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
/**
* A full-blown 'mark as favourite'-button
*/
export let state: SpecialVisualizationState;
export let feature: Feature
export let tags: Record<string, string>;
export let layer: LayerConfig
let isFavourite = tags?.map(tags => tags._favourite === "yes");
const t = Translations.t.favouritePoi;
function markFavourite(isFavourite: boolean) {
state.favourites.markAsFavourite(feature, layer.id, state.layout.id, tags, isFavourite)
}
</script>
<LoginToggle ignoreLoading={true} {state}>
{#if $isFavourite}
<div class="flex h-fit items-start">
<HeartSolidIcon class="w-16 shrink-0 mr-2" on:click={() => markFavourite(false)} />
<div class="flex flex-col w-full">
<button class="flex flex-col items-start" on:click={() => markFavourite(false)}>
<Tr t={t.button.unmark} />
<Tr cls="normal-font subtle" t={t.button.unmarkNotDeleted}/>
</button>
</div>
</div>
<Tr cls="font-bold thanks m-2 p-2 block" t={t.button.isFavourite} />
{:else}
<div class="flex items-start">
<HeartOutlineIcon class="w-16 shrink-0 mr-2" on:click={() => markFavourite(true)} />
<button class="flex w-full flex-col items-start" on:click={() => markFavourite(true)}>
<Tr t={t.button.markAsFavouriteTitle} />
<Tr cls="normal-font subtle" t={t.button.markDescription}/>
</button>
</div>
{/if}
</LoginToggle>

View file

@ -0,0 +1,36 @@
<script lang="ts">
import type { SpecialVisualizationState } from "../SpecialVisualization";
import { HeartIcon as HeartSolidIcon } from "@babeard/svelte-heroicons/solid";
import { HeartIcon as HeartOutlineIcon } from "@babeard/svelte-heroicons/outline";
import Translations from "../i18n/Translations";
import LoginToggle from "../Base/LoginToggle.svelte";
import type { Feature } from "geojson";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
/**
* A small 'mark as favourite'-button to serve as title-icon
*/
export let state: SpecialVisualizationState;
export let feature: Feature;
export let tags: Record<string, string>;
export let layer: LayerConfig;
let isFavourite = tags?.map(tags => tags._favourite === "yes");
const t = Translations.t.favouritePoi;
function markFavourite(isFavourite: boolean) {
state.favourites.markAsFavourite(feature, layer.id, state.layout.id, tags, isFavourite);
}
</script>
<LoginToggle ignoreLoading={true} {state}>
{#if $isFavourite}
<button class="p-0 m-0 h-8 w-8" on:click={() => markFavourite(false)}>
<HeartSolidIcon/>
</button>
{:else}
<button class="p-0 m-0 h-8 w-8 no-image-background" on:click={() => markFavourite(true)} >
<HeartOutlineIcon/>
</button>
{/if}
</LoginToggle>

View file

@ -3,16 +3,15 @@
* Shows all questions for which the answers are unknown.
* The questions can either be shown all at once or one at a time (in which case they can be skipped)
*/
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
import { UIEventSource } from "../../../Logic/UIEventSource"
import type { Feature } from "geojson"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import If from "../../Base/If.svelte"
import TagRenderingQuestion from "./TagRenderingQuestion.svelte"
import Tr from "../../Base/Tr.svelte"
import Translations from "../../i18n/Translations.js"
import { Utils } from "../../../Utils"
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
import { UIEventSource } from "../../../Logic/UIEventSource";
import type { Feature } from "geojson";
import type { SpecialVisualizationState } from "../../SpecialVisualization";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import TagRenderingQuestion from "./TagRenderingQuestion.svelte";
import Tr from "../../Base/Tr.svelte";
import Translations from "../../i18n/Translations.js";
import { Utils } from "../../../Utils";
export let layer: LayerConfig
export let tags: UIEventSource<Record<string, string>>

View file

@ -26,6 +26,7 @@
onDestroy(
tags.addCallbackAndRun((tags) => {
_tags = tags
console.log("Getting render value for", _tags,config)
trs = Utils.NoNull(config?.GetRenderValues(_tags))
})
)

View file

@ -11,7 +11,7 @@
import Translations from "../../i18n/Translations.js"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { Utils } from "../../../Utils"
import { twMerge } from "tailwind-merge"
export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>>
export let selectedElement: Feature | undefined
@ -71,7 +71,7 @@
}
</script>
<div bind:this={htmlElem} class={clss}>
<div bind:this={htmlElem} class={twMerge(clss, "tr-"+config.id)}>
{#if config.question && (!editingEnabled || $editingEnabled)}
{#if editMode}
<TagRenderingQuestion {config} {tags} {selectedElement} {state} {layer}>

View file

@ -6,6 +6,7 @@
import { UIEventSource } from "../../../Logic/UIEventSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { twJoin } from "tailwind-merge"
import Icon from "../../Map/Icon.svelte";
export let selectedElement: Feature
export let tags: UIEventSource<Record<string, string>>
@ -27,13 +28,8 @@
</script>
{#if mapping.icon !== undefined}
<div class="inline-flex items-center">
<img
class={twJoin(`mapping-icon-${mapping.iconClass}`, "mr-1")}
src={mapping.icon}
aria-hidden="true"
alt=""
/>
<div class="inline-flex">
<Icon icon={mapping.icon} clss={twJoin(`mapping-icon-${mapping.iconClass}`, "mr-1")}/>
<SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement} />
</div>
{:else if mapping.then !== undefined}

View file

@ -1,188 +1,210 @@
<script lang="ts">
import { ImmutableStore, UIEventSource } from "../../../Logic/UIEventSource"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import Tr from "../../Base/Tr.svelte"
import type { Feature } from "geojson"
import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig"
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import FreeformInput from "./FreeformInput.svelte"
import Translations from "../../i18n/Translations.js"
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
import { createEventDispatcher, onDestroy } from "svelte"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import SpecialTranslation from "./SpecialTranslation.svelte"
import TagHint from "../TagHint.svelte"
import LoginToggle from "../../Base/LoginToggle.svelte"
import SubtleButton from "../../Base/SubtleButton.svelte"
import Loading from "../../Base/Loading.svelte"
import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte"
import { Translation } from "../../i18n/Translation"
import Constants from "../../../Models/Constants"
import { Unit } from "../../../Models/Unit"
import UserRelatedState from "../../../Logic/State/UserRelatedState"
import { twJoin } from "tailwind-merge"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import Search from "../../../assets/svg/Search.svelte"
import Login from "../../../assets/svg/Login.svelte"
import { ImmutableStore, UIEventSource } from "../../../Logic/UIEventSource";
import type { SpecialVisualizationState } from "../../SpecialVisualization";
import Tr from "../../Base/Tr.svelte";
import type { Feature } from "geojson";
import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig";
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
import { TagsFilter } from "../../../Logic/Tags/TagsFilter";
import FreeformInput from "./FreeformInput.svelte";
import Translations from "../../i18n/Translations.js";
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction";
import { createEventDispatcher, onDestroy } from "svelte";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import SpecialTranslation from "./SpecialTranslation.svelte";
import TagHint from "../TagHint.svelte";
import LoginToggle from "../../Base/LoginToggle.svelte";
import SubtleButton from "../../Base/SubtleButton.svelte";
import Loading from "../../Base/Loading.svelte";
import TagRenderingMappingInput from "./TagRenderingMappingInput.svelte";
import { Translation } from "../../i18n/Translation";
import Constants from "../../../Models/Constants";
import { Unit } from "../../../Models/Unit";
import UserRelatedState from "../../../Logic/State/UserRelatedState";
import { twJoin } from "tailwind-merge";
import { TagUtils } from "../../../Logic/Tags/TagUtils";
import Search from "../../../assets/svg/Search.svelte";
import Login from "../../../assets/svg/Login.svelte";
export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>>
export let selectedElement: Feature
export let state: SpecialVisualizationState
export let layer: LayerConfig | undefined
export let config: TagRenderingConfig;
export let tags: UIEventSource<Record<string, string>>;
export let selectedElement: Feature;
export let state: SpecialVisualizationState;
export let layer: LayerConfig | undefined;
export let selectedTags: TagsFilter = undefined;
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined);
let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
let unit: Unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key));
// Will be bound if a freeform is available
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key])
let selectedMapping: number = undefined
let checkedMappings: boolean[]
// Will be bound if a freeform is available
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key]);
let selectedMapping: number = undefined;
let checkedMappings: boolean[];
/**
* Prepares and fills the checkedMappings
*/
function initialize(tgs: Record<string, string>, confg: TagRenderingConfig) {
mappings = confg.mappings?.filter((m) => {
if (typeof m.hideInAnswer === "boolean") {
return !m.hideInAnswer
}
return !m.hideInAnswer.matchesProperties(tgs)
let mappings: Mapping[] = config?.mappings;
let searchTerm: UIEventSource<string> = new UIEventSource("");
let dispatch = createEventDispatcher<{
saved: {
config: TagRenderingConfig
applied: TagsFilter
}
}>();
/**
* Prepares and fills the checkedMappings
*/
function initialize(tgs: Record<string, string>, confg: TagRenderingConfig) {
mappings = confg.mappings?.filter((m) => {
if (typeof m.hideInAnswer === "boolean") {
return !m.hideInAnswer;
}
return !m.hideInAnswer.matchesProperties(tgs);
});
// We received a new config -> reinit
unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key));
if (
confg.mappings?.length > 0 &&
confg.multiAnswer &&
(checkedMappings === undefined ||
checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0))
) {
const seenFreeforms = [];
TagUtils.FlattenMultiAnswer();
checkedMappings = [
...confg.mappings.map((mapping) => {
const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs);
if (matches && confg.freeform) {
const newProps = TagUtils.changeAsProperties(mapping.if.asChange());
seenFreeforms.push(newProps[confg.freeform.key]);
}
return matches;
})
// We received a new config -> reinit
unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
];
if (
confg.mappings?.length > 0 &&
confg.multiAnswer &&
(checkedMappings === undefined ||
checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0))
) {
const seenFreeforms = []
TagUtils.FlattenMultiAnswer()
checkedMappings = [
...confg.mappings.map((mapping) => {
const matches = TagUtils.MatchesMultiAnswer(mapping.if, tgs)
if (matches && confg.freeform) {
const newProps = TagUtils.changeAsProperties(mapping.if.asChange())
seenFreeforms.push(newProps[confg.freeform.key])
}
return matches
}),
]
if (tgs !== undefined && confg.freeform) {
const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? []
for (const seenFreeform of seenFreeforms) {
if (!seenFreeform) {
continue
}
const index = unseenFreeformValues.indexOf(seenFreeform)
if (index < 0) {
continue
}
unseenFreeformValues.splice(index, 1)
}
// TODO this has _to much_ values
freeformInput.setData(unseenFreeformValues.join(";"))
checkedMappings.push(unseenFreeformValues.length > 0)
}
if (tgs !== undefined && confg.freeform) {
const unseenFreeformValues = tgs[confg.freeform.key]?.split(";") ?? [];
for (const seenFreeform of seenFreeforms) {
if (!seenFreeform) {
continue;
}
const index = unseenFreeformValues.indexOf(seenFreeform);
if (index < 0) {
continue;
}
unseenFreeformValues.splice(index, 1);
}
if (confg.freeform?.key) {
if (!confg.multiAnswer) {
// Somehow, setting multi-answer freeform values is broken if this is not set
freeformInput.setData(tgs[confg.freeform.key])
}
// TODO this has _to much_ values
freeformInput.setData(unseenFreeformValues.join(";"));
checkedMappings.push(unseenFreeformValues.length > 0);
}
}
if (confg.freeform?.key) {
if (!confg.multiAnswer) {
// Somehow, setting multi-answer freeform values is broken if this is not set
freeformInput.setData(tgs[confg.freeform.key]);
}
} else {
freeformInput.setData(undefined);
}
feedback.setData(undefined);
}
$: {
// Even though 'config' is not declared as a store, Svelte uses it as one to update the component
// We want to (re)-initialize whenever the 'tags' or 'config' change - but not when 'checkedConfig' changes
initialize($tags, config);
}
$: {
try {
selectedTags = config?.constructChangeSpecification(
$freeformInput,
selectedMapping,
checkedMappings,
tags.data
);
} catch (e) {
console.error("Could not calculate changeSpecification:", e);
selectedTags = undefined;
}
}
function onSave() {
if (selectedTags === undefined) {
console.log("SelectedTags is undefined, ignoring 'onSave'-event");
return;
}
if (layer === undefined || (layer?.source === null && layer.id !== "favourite")) {
/**
* This is a special, priviliged layer.
* We simply apply the tags onto the records
*/
const kv = selectedTags.asChange(tags.data);
for (const { k, v } of kv) {
if (v === undefined || v === "") {
delete tags.data[k];
} else {
freeformInput.setData(undefined)
freeformInput.setData(undefined);
}
feedback.setData(undefined)
feedback.setData(undefined);
}
}
$: {
// Even though 'config' is not declared as a store, Svelte uses it as one to update the component
// We want to (re)-initialize whenever the 'tags' or 'config' change - but not when 'checkedConfig' changes
initialize($tags, config)
dispatch("saved", { config, applied: selectedTags });
const change = new ChangeTagAction(tags.data.id, selectedTags, tags.data, {
theme: tags.data["_orig_theme"] ?? state.layout.id,
changeType: "answer"
});
freeformInput.setData(undefined);
selectedMapping = undefined;
selectedTags = undefined;
change
.CreateChangeDescriptions()
.then((changes) => state.changes.applyChanges(changes))
.catch(console.error);
}
function onInputKeypress(e: Event) {
if (e.key === "Enter") {
onSave();
}
export let selectedTags: TagsFilter = undefined
}
let mappings: Mapping[] = config?.mappings
let searchTerm: UIEventSource<string> = new UIEventSource("")
$: {
try {
selectedTags = config?.constructChangeSpecification(
$freeformInput,
selectedMapping,
checkedMappings,
tags.data,
)
} catch (e) {
console.error("Could not calculate changeSpecification:", e)
selectedTags = undefined
}
$: {
try {
selectedTags = config?.constructChangeSpecification(
$freeformInput,
selectedMapping,
checkedMappings,
tags.data
);
} catch (e) {
console.error("Could not calculate changeSpecification:", e);
selectedTags = undefined;
}
}
let dispatch = createEventDispatcher<{
saved: {
config: TagRenderingConfig
applied: TagsFilter
}
}>()
function onSave() {
if (selectedTags === undefined) {
console.log("SelectedTags is undefined, ignoring 'onSave'-event")
return
}
if (layer === undefined || layer?.source === null) {
/**
* This is a special, priviliged layer.
* We simply apply the tags onto the records
*/
const kv = selectedTags.asChange(tags.data)
for (const { k, v } of kv) {
if (v === undefined || v === "") {
delete tags.data[k]
} else {
tags.data[k] = v
}
}
tags.ping()
return
}
dispatch("saved", { config, applied: selectedTags })
const change = new ChangeTagAction(tags.data.id, selectedTags, tags.data, {
theme: state.layout.id,
changeType: "answer",
})
freeformInput.setData(undefined)
selectedMapping = undefined
selectedTags = undefined
change
.CreateChangeDescriptions()
.then((changes) => state.changes.applyChanges(changes))
.catch(console.error)
}
let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false)
let featureSwitchIsDebugging =
state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false)
let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined)
let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0
let question = config.question
$: question = config.question
if (state?.osmConnection) {
onDestroy(
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
numberOfCs = ud.csCount
}),
)
}
let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false);
let featureSwitchIsDebugging =
state?.featureSwitches?.featureSwitchIsDebugging ?? new ImmutableStore(false);
let showTags = state?.userRelatedState?.showTags ?? new ImmutableStore(undefined);
let numberOfCs = state?.osmConnection?.userDetails?.data?.csCount ?? 0;
let question = config.question;
$: question = config.question;
if (state?.osmConnection) {
onDestroy(
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
numberOfCs = ud.csCount;
})
);
}
</script>
{#if question !== undefined}
@ -246,9 +268,8 @@
bind:group={selectedMapping}
name={"mappings-radio-" + config.id}
value={i}
on:keypress={(e) => {
if (e.key === "Enter") onSave()
}}
on:keypress={e => onInputKeypress(e)}
/>
</TagRenderingMappingInput>
{/each}
@ -259,6 +280,7 @@
bind:group={selectedMapping}
name={"mappings-radio-" + config.id}
value={config.mappings?.length}
on:keypress={e => onInputKeypress(e)}
/>
<FreeformInput
{config}
@ -290,6 +312,7 @@
type="checkbox"
name={"mappings-checkbox-" + config.id + "-" + i}
bind:checked={checkedMappings[i]}
on:keypress={e => onInputKeypress(e)}
/>
</TagRenderingMappingInput>
{/each}
@ -299,6 +322,7 @@
type="checkbox"
name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length}
bind:checked={checkedMappings[config.mappings.length]}
on:keypress={e => onInputKeypress(e)}
/>
<FreeformInput
{config}
@ -307,7 +331,6 @@
{unit}
feature={selectedElement}
value={freeformInput}
on:selected={() => (checkedMappings[config.mappings.length] = true)}
on:submit={onSave}
/>
</label>

View file

@ -17,6 +17,7 @@ import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
import { RasterLayerPolygon } from "../Models/RasterLayers"
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
import { OsmTags } from "../Models/OsmFeature"
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
/**
* The state needed to render a special Visualisation.
@ -33,7 +34,6 @@ export interface SpecialVisualizationState {
}
readonly indexedFeatures: IndexedFeatureSource
/**
* Some features will create a new element that should be displayed.
* These can be injected by appending them to this featuresource (and pinging it)
@ -59,6 +59,8 @@ export interface SpecialVisualizationState {
readonly selectedLayer: UIEventSource<LayerConfig>
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>
readonly favourites: FavouritesFeatureSource
/**
* If data is currently being fetched from external sources
*/

View file

@ -79,6 +79,9 @@ import ThemeViewState from "../Models/ThemeViewState"
import LanguagePicker from "./InputElement/LanguagePicker.svelte"
import LogoutButton from "./Base/LogoutButton.svelte"
import OpenJosm from "./Base/OpenJosm.svelte"
import MarkAsFavourite from "./Popup/MarkAsFavourite.svelte"
import MarkAsFavouriteMini from "./Popup/MarkAsFavouriteMini.svelte"
import NextChangeViz from "./OpeningHours/NextChangeViz.svelte"
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -532,6 +535,9 @@ export default class SpecialVisualizations {
feature: Feature,
layer: LayerConfig
): BaseUIElement {
if (!layer.deletion) {
return undefined
}
return new SvelteUIElement(DeleteWizard, {
tags: tagSource,
deleteConfig: layer.deletion,
@ -822,6 +828,46 @@ export default class SpecialVisualizations {
)
},
},
{
funcName: "opening_hours_state",
docs: "A small element, showing if the POI is currently open and when the next change is",
args: [
{
name: "key",
defaultValue: "opening_hours",
doc: "The tagkey from which the opening hours are read.",
},
{
name: "prefix",
defaultValue: "",
doc: "Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__",
},
{
name: "postfix",
defaultValue: "",
doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__",
},
],
needsUrls: [],
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
const keyToUse = args[0]
const prefix = args[1]
const postfix = args[2]
return new SvelteUIElement(NextChangeViz, {
state,
keyToUse,
tags,
prefix,
postfix,
})
},
},
{
funcName: "canonical",
needsUrls: [],
@ -872,20 +918,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")
},
},
{
@ -1482,7 +1530,7 @@ export default class SpecialVisualizations {
const tags = (<ThemeViewState>(
state
)).geolocation.currentUserLocation.features.map(
(features) => features[0].properties
(features) => features[0]?.properties
)
return new SvelteUIElement(AllTagsPanel, {
state,
@ -1490,6 +1538,46 @@ export default class SpecialVisualizations {
})
},
},
{
funcName: "favourite_status",
needsUrls: [],
docs: "A button that allows a (logged in) contributor to mark a location as a favourite location",
args: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
return new SvelteUIElement(MarkAsFavourite, {
tags: tagSource,
state,
layer,
feature,
})
},
},
{
funcName: "favourite_icon",
needsUrls: [],
docs: "A small button that allows a (logged in) contributor to mark a location as a favourite location, sized to fit a title-icon",
args: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
return new SvelteUIElement(MarkAsFavouriteMini, {
tags: tagSource,
state,
layer,
feature,
})
},
},
]
specialVisualizations.push(new AutoApplyButton(specialVisualizations))

View file

@ -1,30 +1,30 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Marker from "../Map/Marker.svelte"
import NextButton from "../Base/NextButton.svelte"
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts"
import { AllSharedLayers } from "../../Customizations/AllSharedLayers"
import { createEventDispatcher } from "svelte"
import { UIEventSource } from "../../Logic/UIEventSource";
import { OsmConnection } from "../../Logic/Osm/OsmConnection";
import Marker from "../Map/Marker.svelte";
import NextButton from "../Base/NextButton.svelte";
import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts";
import { AllSharedLayers } from "../../Customizations/AllSharedLayers";
import { createEventDispatcher } from "svelte";
export let info: { id: string; owner: number }
export let category: "layers" | "themes"
export let osmConnection: OsmConnection
export let info: { id: string; owner: number };
export let category: "layers" | "themes";
export let osmConnection: OsmConnection;
const dispatch = createEventDispatcher<{ layerSelected: string }>();
let displayName = UIEventSource.FromPromise(
osmConnection.getInformationAboutUser(info.owner),
).mapD((response) => response.display_name)
let displayName = UIEventSource.FromPromise(
osmConnection.getInformationAboutUser(info.owner)
).mapD((response) => response.display_name);
let selfId = osmConnection.userDetails.mapD((ud) => ud.uid);
let selfId = osmConnection.userDetails.mapD((ud) => ud.uid)
function fetchIconDescription(layerId): any {
if (category === "themes") {
return AllKnownLayouts.allKnownLayouts.get(layerId).icon
}
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon
function fetchIconDescription(layerId): any {
if (category === "themes") {
return AllKnownLayouts.allKnownLayouts.get(layerId).icon;
}
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
}
const dispatch = createEventDispatcher<{ layerSelected: string }>()
</script>
<NextButton clss="small" on:click={() => dispatch("layerSelected", info)}>

View file

@ -1,90 +1,91 @@
<script lang="ts">
import { Store, UIEventSource } from "../Logic/UIEventSource"
import { Map as MlMap } from "maplibre-gl"
import MaplibreMap from "./Map/MaplibreMap.svelte"
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import MapControlButton from "./Base/MapControlButton.svelte"
import ToSvelte from "./Base/ToSvelte.svelte"
import If from "./Base/If.svelte"
import { GeolocationControl } from "./BigComponents/GeolocationControl"
import type { Feature } from "geojson"
import SelectedElementView from "./BigComponents/SelectedElementView.svelte"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import Filterview from "./BigComponents/Filterview.svelte"
import ThemeViewState from "../Models/ThemeViewState"
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 Tr from "./Base/Tr.svelte"
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
import FloatOver from "./Base/FloatOver.svelte"
import PrivacyPolicy from "./BigComponents/PrivacyPolicy"
import Constants from "../Models/Constants"
import TabbedGroup from "./Base/TabbedGroup.svelte"
import UserRelatedState from "../Logic/State/UserRelatedState"
import LoginToggle from "./Base/LoginToggle.svelte"
import LoginButton from "./Base/LoginButton.svelte"
import CopyrightPanel from "./BigComponents/CopyrightPanel"
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"
import ModalRight from "./Base/ModalRight.svelte"
import { Utils } from "../Utils"
import Hotkeys from "./Base/Hotkeys"
import { VariableUiElement } from "./Base/VariableUIElement"
import SvelteUIElement from "./Base/SvelteUIElement"
import OverlayToggle from "./BigComponents/OverlayToggle.svelte"
import LevelSelector from "./BigComponents/LevelSelector.svelte"
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
import type { RasterLayerPolygon } from "../Models/RasterLayers"
import { AvailableRasterLayers } from "../Models/RasterLayers"
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"
import IfHidden from "./Base/IfHidden.svelte"
import { onDestroy } from "svelte"
import MapillaryLink from "./BigComponents/MapillaryLink.svelte"
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"
import StateIndicator from "./BigComponents/StateIndicator.svelte"
import ShareScreen from "./BigComponents/ShareScreen.svelte"
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte"
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte"
import Cross from "../assets/svg/Cross.svelte"
import Summary from "./BigComponents/Summary.svelte"
import Mastodon from "../assets/svg/Mastodon.svelte"
import Bug from "../assets/svg/Bug.svelte"
import Liberapay from "../assets/svg/Liberapay.svelte"
import Min from "../assets/svg/Min.svelte"
import Plus from "../assets/svg/Plus.svelte"
import Filter from "../assets/svg/Filter.svelte"
import Add from "../assets/svg/Add.svelte"
import Statistics from "../assets/svg/Statistics.svelte"
import Community from "../assets/svg/Community.svelte"
import Download from "../assets/svg/Download.svelte"
import Share from "../assets/svg/Share.svelte"
import LanguagePicker from "./InputElement/LanguagePicker.svelte"
import OpenJosm from "./Base/OpenJosm.svelte"
import { Store, UIEventSource } from "../Logic/UIEventSource";
import { Map as MlMap } from "maplibre-gl";
import MaplibreMap from "./Map/MaplibreMap.svelte";
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
import MapControlButton from "./Base/MapControlButton.svelte";
import ToSvelte from "./Base/ToSvelte.svelte";
import If from "./Base/If.svelte";
import { GeolocationControl } from "./BigComponents/GeolocationControl";
import type { Feature } from "geojson";
import SelectedElementView from "./BigComponents/SelectedElementView.svelte";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import Filterview from "./BigComponents/Filterview.svelte";
import ThemeViewState from "../Models/ThemeViewState";
import type { MapProperties } from "../Models/MapProperties";
import Geosearch from "./BigComponents/Geosearch.svelte";
import Translations from "./i18n/Translations";
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";
import PrivacyPolicy from "./BigComponents/PrivacyPolicy";
import Constants from "../Models/Constants";
import TabbedGroup from "./Base/TabbedGroup.svelte";
import UserRelatedState from "../Logic/State/UserRelatedState";
import LoginToggle from "./Base/LoginToggle.svelte";
import LoginButton from "./Base/LoginButton.svelte";
import CopyrightPanel from "./BigComponents/CopyrightPanel";
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte";
import ModalRight from "./Base/ModalRight.svelte";
import { Utils } from "../Utils";
import Hotkeys from "./Base/Hotkeys";
import { VariableUiElement } from "./Base/VariableUIElement";
import SvelteUIElement from "./Base/SvelteUIElement";
import OverlayToggle from "./BigComponents/OverlayToggle.svelte";
import LevelSelector from "./BigComponents/LevelSelector.svelte";
import ExtraLinkButton from "./BigComponents/ExtraLinkButton";
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte";
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte";
import type { RasterLayerPolygon } from "../Models/RasterLayers";
import { AvailableRasterLayers } from "../Models/RasterLayers";
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte";
import IfHidden from "./Base/IfHidden.svelte";
import { onDestroy } from "svelte";
import MapillaryLink from "./BigComponents/MapillaryLink.svelte";
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte";
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte";
import StateIndicator from "./BigComponents/StateIndicator.svelte";
import ShareScreen from "./BigComponents/ShareScreen.svelte";
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte";
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte";
import Cross from "../assets/svg/Cross.svelte";
import Summary from "./BigComponents/Summary.svelte";
import LanguagePicker from "./InputElement/LanguagePicker.svelte";
import Mastodon from "../assets/svg/Mastodon.svelte";
import Bug from "../assets/svg/Bug.svelte";
import Liberapay from "../assets/svg/Liberapay.svelte";
import OpenJosm from "./Base/OpenJosm.svelte";
import Min from "../assets/svg/Min.svelte";
import Plus from "../assets/svg/Plus.svelte";
import Filter from "../assets/svg/Filter.svelte";
import Add from "../assets/svg/Add.svelte";
import Statistics from "../assets/svg/Statistics.svelte";
import Community from "../assets/svg/Community.svelte";
import Download from "../assets/svg/Download.svelte";
import Share from "../assets/svg/Share.svelte";
import Favourites from "./Favourites/Favourites.svelte";
export let state: ThemeViewState
export let state: ThemeViewState
let layout = state.layout
let maplibremap: UIEventSource<MlMap> = state.map
let selectedElement: UIEventSource<Feature> = state.selectedElement
let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer
let currentZoom = state.mapProperties.zoom
let showCrosshair = state.userRelatedState.showCrosshair
let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation
let centerFeatures = state.closestFeatures.features
const selectedElementView = selectedElement.map(
(selectedElement) => {
// Svelte doesn't properly reload some of the legacy UI-elements
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
const layer = selectedLayer.data
if (selectedElement === undefined || layer === undefined) {
return undefined
}
let currentZoom = state.mapProperties.zoom;
let showCrosshair = state.userRelatedState.showCrosshair;
let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation;
let centerFeatures = state.closestFeatures.features;
const selectedElementView = selectedElement.map(
(selectedElement) => {
// Svelte doesn't properly reload some of the legacy UI-elements
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
const layer = selectedLayer.data;
if (selectedElement === undefined || layer === undefined) {
return undefined;
}
if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) {
return undefined
@ -230,17 +231,15 @@
</a>
</div>
</div>
{#if $arrowKeysWereUsed !== undefined}
{#if $centerFeatures.length > 0}
<div class="interactive pointer-events-auto p-1">
{#each $centerFeatures as feat, i (feat.properties.id)}
<div class="flex">
<b>{i + 1}.</b>
<Summary {state} feature={feat} />
</div>
{/each}
</div>
{/if}
{#if $arrowKeysWereUsed !== undefined && $centerFeatures?.length > 0}
<div class="pointer-events-auto interactive p-1">
{#each $centerFeatures as feat, i (feat.properties.id)}
<div class="flex">
<b>{i+1}.</b><Summary {state} feature={feat}/>
</div>
{/each}
</div>
{/if}
<div class="flex flex-col items-end">
<!-- bottom right elements -->
@ -495,22 +494,31 @@
</div>
<div class="flex" slot="title2">
<HeartIcon class="h-6 w-6" />
<Tr t={Translations.t.favouritePoi.tab}/>
</div>
<div class="flex flex-col m-2" slot="content2">
<h3> <Tr t={Translations.t.favouritePoi.title}/></h3>
<Favourites {state}/>
</div>
<div class="flex" slot="title3">
<Community class="h-6 w-6" />
<Tr t={Translations.t.communityIndex.title} />
</div>
<div class="m-2" slot="content2">
<div class="m-2" slot="content3">
<CommunityIndexView location={state.mapProperties.location} />
</div>
<div class="flex" slot="title3">
<div class="flex" slot="title4">
<EyeIcon class="w-6" />
<Tr t={Translations.t.privacy.title} />
</div>
<div class="m-2" slot="content3">
<div class="m-2" slot="content4">
<ToSvelte construct={() => new PrivacyPolicy()} />
</div>
<Tr slot="title4" t={Translations.t.advanced.title} />
<div class="m-2 flex flex-col" slot="content4">
<Tr slot="title5" t={Translations.t.advanced.title} />
<div class="m-2 flex flex-col" slot="content5">
<If condition={featureSwitches.featureSwitchEnableLogin}>
<OpenIdEditor mapProperties={state.mapProperties} />
<OpenJosm {state} />