Feature: add favourite

This commit is contained in:
Pieter Vander Vennet 2023-11-22 19:39:19 +01:00
parent a32ab16a5e
commit f9827dd6ae
68 changed files with 1641 additions and 885 deletions

View file

@ -4,12 +4,10 @@
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()
}}>
<Logout class="w-6 h-6"/>
<Tr t={Translations.t.general.logout}/>
<button on:click={() => {osmConnection.LogOut()}}>
<Logout class="w-6 h-6" />
<Tr t={Translations.t.general.logout} />
</button>

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

@ -8,7 +8,7 @@
* Renders a 'marker', which consists of multiple 'icons'
*/
export let marker: IconConfig[] = config?.marker;
export let rotation: TagRenderingConfig
export let rotation: TagRenderingConfig;
export let tags: Store<Record<string, string>>;
let _rotation = rotation ? tags.map(tags => rotation.GetRenderValue(tags).Subs(tags).txt) : new ImmutableStore(0);
</script>
@ -16,7 +16,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 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;
export let clss: string | 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

@ -18,7 +18,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

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

@ -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>>
@ -28,12 +29,7 @@
{#if mapping.icon !== undefined}
<div class="inline-flex">
<img
class={twJoin(`mapping-icon-${mapping.iconClass}`, "mr-1")}
src={mapping.icon}
aria-hidden="true"
alt=""
/>
<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

@ -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,8 @@ 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"
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -1481,7 +1483,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,
@ -1489,6 +1491,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,11 +1,16 @@
<script lang="ts">
// Testing grounds
import LanguagePicker from "./InputElement/LanguagePicker.svelte";
import Translations from "./i18n/Translations";
import Tr from "./Base/Tr.svelte";
import Locale from "./i18n/Locale";
import MarkAsFavourite from "./Popup/MarkAsFavourite.svelte";
let language = Locale.language
import { UIEventSource } from "../Logic/UIEventSource";
import { OsmConnection } from "../Logic/Osm/OsmConnection";
const osmConnection = new OsmConnection({attemptLogin: true})
function resetFavs(){
osmConnection.preferencesHandler.removeAllWithPrefix("mapcomplete-favourite-")
console.log("CLEARED!")
}
</script>
<MarkAsFavourite/>
<button on:click={() => resetFavs()} >Clear</button>

View file

@ -76,7 +76,6 @@
let showCrosshair = state.userRelatedState.showCrosshair;
let arrowKeysWereUsed = state.mapProperties.lastKeyNavigation;
let centerFeatures = state.closestFeatures.features;
$: console.log("Centerfeatures are", $centerFeatures)
const selectedElementView = selectedElement.map(
(selectedElement) => {
// Svelte doesn't properly reload some of the legacy UI-elements
@ -232,7 +231,7 @@
</div>
</div>
{#if $arrowKeysWereUsed !== undefined}
{#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">