forked from MapComplete/MapComplete
Feature: add favourite
This commit is contained in:
parent
a32ab16a5e
commit
f9827dd6ae
68 changed files with 1641 additions and 885 deletions
157
src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts
Normal file
157
src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts
Normal file
|
@ -0,0 +1,157 @@
|
|||
import StaticFeatureSource from "./StaticFeatureSource"
|
||||
import { Feature } from "geojson"
|
||||
import { Store, UIEventSource } from "../../UIEventSource"
|
||||
import { OsmConnection } from "../../Osm/OsmConnection"
|
||||
import { OsmId } from "../../../Models/OsmFeature"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
import FeaturePropertiesStore from "../Actors/FeaturePropertiesStore"
|
||||
import { IndexedFeatureSource } from "../FeatureSource"
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
|
||||
|
||||
/**
|
||||
* Generates the favourites from the preferences and marks them as favourite
|
||||
*/
|
||||
export default class FavouritesFeatureSource extends StaticFeatureSource {
|
||||
public static readonly prefix = "mapcomplete-favourite-"
|
||||
private readonly _osmConnection: OsmConnection
|
||||
|
||||
constructor(
|
||||
connection: OsmConnection,
|
||||
indexedSource: FeaturePropertiesStore,
|
||||
allFeatures: IndexedFeatureSource,
|
||||
layout: LayoutConfig
|
||||
) {
|
||||
const detectedIds = new UIEventSource<Set<string>>(undefined)
|
||||
const features: Store<Feature[]> = connection.preferencesHandler.preferences.map(
|
||||
(prefs) => {
|
||||
const feats: Feature[] = []
|
||||
const allIds = new Set<string>()
|
||||
for (const key in prefs) {
|
||||
if (!key.startsWith(FavouritesFeatureSource.prefix)) {
|
||||
continue
|
||||
}
|
||||
const id = key.substring(FavouritesFeatureSource.prefix.length)
|
||||
const osmId = id.replace("-", "/")
|
||||
if (id.indexOf("-property-") > 0 || id.indexOf("-layer") > 0) {
|
||||
continue
|
||||
}
|
||||
allIds.add(osmId)
|
||||
const geometry = <[number, number]>JSON.parse(prefs[key])
|
||||
const properties = FavouritesFeatureSource.getPropertiesFor(connection, id)
|
||||
properties._orig_layer = prefs[FavouritesFeatureSource.prefix + id + "-layer"]
|
||||
if (layout.layers.some((l) => l.id === properties._orig_layer)) {
|
||||
continue
|
||||
}
|
||||
properties.id = osmId
|
||||
properties._favourite = "yes"
|
||||
feats.push({
|
||||
type: "Feature",
|
||||
properties,
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: geometry,
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log("Favouritess are", feats)
|
||||
detectedIds.setData(allIds)
|
||||
return feats
|
||||
}
|
||||
)
|
||||
|
||||
super(features)
|
||||
|
||||
this._osmConnection = connection
|
||||
detectedIds.addCallbackAndRunD((detected) =>
|
||||
this.markFeatures(detected, indexedSource, allFeatures)
|
||||
)
|
||||
// We use the indexedFeatureSource as signal to update
|
||||
allFeatures.features.map((_) =>
|
||||
this.markFeatures(detectedIds.data, indexedSource, allFeatures)
|
||||
)
|
||||
}
|
||||
|
||||
private static getPropertiesFor(
|
||||
osmConnection: OsmConnection,
|
||||
id: string
|
||||
): Record<string, string> {
|
||||
const properties: Record<string, string> = {}
|
||||
const prefs = osmConnection.preferencesHandler.preferences.data
|
||||
const minLength = FavouritesFeatureSource.prefix.length + id.length + "-property-".length
|
||||
for (const key in prefs) {
|
||||
if (key.length < minLength) {
|
||||
continue
|
||||
}
|
||||
if (!key.startsWith(FavouritesFeatureSource.prefix + id)) {
|
||||
continue
|
||||
}
|
||||
const propertyName = key.substring(minLength)
|
||||
properties[propertyName] = prefs[key]
|
||||
}
|
||||
return properties
|
||||
}
|
||||
|
||||
public markAsFavourite(
|
||||
feature: Feature,
|
||||
layer: string,
|
||||
theme: string,
|
||||
tags: UIEventSource<Record<string, string> & { id: OsmId }>,
|
||||
isFavourite: boolean = true
|
||||
) {
|
||||
{
|
||||
const id = tags.data.id.replace("/", "-")
|
||||
const pref = this._osmConnection.GetPreference("favourite-" + id)
|
||||
if (isFavourite) {
|
||||
const center = GeoOperations.centerpointCoordinates(feature)
|
||||
pref.setData(JSON.stringify(center))
|
||||
this._osmConnection.GetPreference("favourite-" + id + "-layer").setData(layer)
|
||||
this._osmConnection.GetPreference("favourite-" + id + "-theme").setData(theme)
|
||||
|
||||
for (const key in tags.data) {
|
||||
const pref = this._osmConnection.GetPreference(
|
||||
"favourite-" + id + "-property-" + key.replaceAll(":", "__")
|
||||
)
|
||||
pref.setData(tags.data[key])
|
||||
}
|
||||
} else {
|
||||
this._osmConnection.preferencesHandler.removeAllWithPrefix(
|
||||
"mapcomplete-favourite-" + id
|
||||
)
|
||||
}
|
||||
}
|
||||
if (isFavourite) {
|
||||
tags.data._favourite = "yes"
|
||||
tags.ping()
|
||||
} else {
|
||||
delete tags.data._favourite
|
||||
tags.ping()
|
||||
}
|
||||
}
|
||||
|
||||
private markFeatures(
|
||||
detected: Set<string>,
|
||||
featureProperties: FeaturePropertiesStore,
|
||||
allFeatures: IndexedFeatureSource
|
||||
) {
|
||||
const feature = allFeatures.features.data
|
||||
for (const f of feature) {
|
||||
const id = f.properties.id
|
||||
if (!id) {
|
||||
continue
|
||||
}
|
||||
const store = featureProperties.getStore(id)
|
||||
const origValue = store.data._favourite
|
||||
if (detected.has(id)) {
|
||||
if (origValue !== "yes") {
|
||||
store.data._favourite = "yes"
|
||||
store.ping()
|
||||
}
|
||||
} else {
|
||||
if (origValue) {
|
||||
store.data._favourite = ""
|
||||
store.ping()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,10 +6,14 @@ import FilteringFeatureSource from "./FilteringFeatureSource"
|
|||
import LayerState from "../../State/LayerState"
|
||||
|
||||
export default class NearbyFeatureSource implements FeatureSource {
|
||||
private readonly _result = new UIEventSource<Feature[]>(undefined)
|
||||
|
||||
public readonly features: Store<Feature[]>
|
||||
private readonly _targetPoint: Store<{ lon: number; lat: number }>
|
||||
private readonly _numberOfNeededFeatures: number
|
||||
private readonly _layerState?: LayerState
|
||||
private readonly _currentZoom: Store<number>
|
||||
private readonly _allSources: Store<{ feat: Feature; d: number }[]>[] = []
|
||||
|
||||
constructor(
|
||||
targetPoint: Store<{ lon: number; lat: number }>,
|
||||
|
@ -18,41 +22,44 @@ export default class NearbyFeatureSource implements FeatureSource {
|
|||
layerState?: LayerState,
|
||||
currentZoom?: Store<number>
|
||||
) {
|
||||
this._layerState = layerState
|
||||
this._targetPoint = targetPoint.stabilized(100)
|
||||
this._numberOfNeededFeatures = numberOfNeededFeatures
|
||||
this._currentZoom = currentZoom.stabilized(500)
|
||||
|
||||
const allSources: Store<{ feat: Feature; d: number }[]>[] = []
|
||||
this.features = Stores.ListStabilized(this._result)
|
||||
|
||||
sources.forEach((source, layer) => {})
|
||||
}
|
||||
|
||||
public registerSource(source: FeatureSource, layerId: string) {
|
||||
let minzoom = 999
|
||||
|
||||
const result = new UIEventSource<Feature[]>(undefined)
|
||||
this.features = Stores.ListStabilized(result)
|
||||
|
||||
function update() {
|
||||
let features: { feat: Feature; d: number }[] = []
|
||||
for (const src of allSources) {
|
||||
features.push(...src.data)
|
||||
}
|
||||
features.sort((a, b) => a.d - b.d)
|
||||
if (numberOfNeededFeatures !== undefined) {
|
||||
features = features.slice(0, numberOfNeededFeatures)
|
||||
}
|
||||
result.setData(features.map((f) => f.feat))
|
||||
const flayer = this._layerState?.filteredLayers.get(layerId)
|
||||
if (!flayer) {
|
||||
return
|
||||
}
|
||||
|
||||
sources.forEach((source, layer) => {
|
||||
const flayer = layerState?.filteredLayers.get(layer)
|
||||
minzoom = Math.min(minzoom, flayer.layerDef.minzoom)
|
||||
const calcSource = this.createSource(
|
||||
source.features,
|
||||
flayer.layerDef.minzoom,
|
||||
flayer.isDisplayed
|
||||
)
|
||||
calcSource.addCallbackAndRunD((features) => {
|
||||
update()
|
||||
})
|
||||
allSources.push(calcSource)
|
||||
minzoom = Math.min(minzoom, flayer.layerDef.minzoom)
|
||||
const calcSource = this.createSource(
|
||||
source.features,
|
||||
flayer.layerDef.minzoom,
|
||||
flayer.isDisplayed
|
||||
)
|
||||
calcSource.addCallbackAndRunD((features) => {
|
||||
this.update()
|
||||
})
|
||||
this._allSources.push(calcSource)
|
||||
}
|
||||
|
||||
private update() {
|
||||
let features: { feat: Feature; d: number }[] = []
|
||||
for (const src of this._allSources) {
|
||||
features.push(...src.data)
|
||||
}
|
||||
features.sort((a, b) => a.d - b.d)
|
||||
if (this._numberOfNeededFeatures !== undefined) {
|
||||
features = features.slice(0, this._numberOfNeededFeatures)
|
||||
}
|
||||
this._result.setData(features.map((f) => f.feat))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -285,4 +285,13 @@ export class OsmPreferences {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
removeAllWithPrefix(prefix: string) {
|
||||
for (const key in this.preferences.data) {
|
||||
if (key.startsWith(prefix)) {
|
||||
this.GetPreference(key, undefined, { prefix: "" }).setData(undefined)
|
||||
console.log("Clearing preference", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ export default class Constants {
|
|||
"gps_track",
|
||||
"range",
|
||||
"last_click",
|
||||
"favourite",
|
||||
] as const
|
||||
/**
|
||||
* Special layers which are not included in a theme by default
|
||||
|
@ -131,6 +132,8 @@ export default class Constants {
|
|||
"clock",
|
||||
"invalid",
|
||||
"close",
|
||||
"heart",
|
||||
"heart_outline",
|
||||
] as const
|
||||
public static readonly defaultPinIcons: string[] = <any>Constants._defaultPinIcons
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ import predifined_filters from "../../../../assets/layers/filters/filters.json"
|
|||
import { TagConfigJson } from "../Json/TagConfigJson"
|
||||
import PointRenderingConfigJson, { IconConfigJson } from "../Json/PointRenderingConfigJson"
|
||||
import ValidationUtils from "./ValidationUtils"
|
||||
import { RenderingSpecification, SpecialVisualization } from "../../../UI/SpecialVisualization"
|
||||
import { RenderingSpecification } from "../../../UI/SpecialVisualization"
|
||||
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
|
||||
import { ConfigMeta } from "../../../UI/Studio/configMeta"
|
||||
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
|
||||
|
@ -639,6 +639,13 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
|||
json.tagRenderings.push(this._desugaring.tagRenderings.get("last_edit"))
|
||||
}
|
||||
|
||||
if (!usedSpecialFunctions.has("favourite_status")) {
|
||||
json.tagRenderings.push({
|
||||
id: "favourite_status",
|
||||
render: { "*": "{favourite_status()}" },
|
||||
})
|
||||
}
|
||||
|
||||
if (!usedSpecialFunctions.has("all_tags")) {
|
||||
const trc: QuestionableTagRenderingConfigJson = {
|
||||
id: "all-tags",
|
||||
|
@ -1193,6 +1200,31 @@ class ExpandMarkerRenderings extends DesugaringStep<IconConfigJson> {
|
|||
}
|
||||
}
|
||||
|
||||
class AddFavouriteBadges extends DesugaringStep<LayerConfigJson> {
|
||||
constructor() {
|
||||
super(
|
||||
"Adds the favourite heart to the title and the rendering badges",
|
||||
[],
|
||||
"AddFavouriteBadges"
|
||||
)
|
||||
}
|
||||
|
||||
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
|
||||
if (json.id === "favourite") {
|
||||
return json
|
||||
}
|
||||
const pr = json.pointRendering?.[0]
|
||||
if (pr) {
|
||||
pr.iconBadges ??= []
|
||||
if (!pr.iconBadges.some((ti) => ti.if === "_favourite=yes")) {
|
||||
pr.iconBadges.push({ if: "_favourite=yes", then: "circle:white;heart:red" })
|
||||
}
|
||||
}
|
||||
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
export class AddRatingBadge extends DesugaringStep<LayerConfigJson> {
|
||||
constructor() {
|
||||
super(
|
||||
|
@ -1206,6 +1238,10 @@ export class AddRatingBadge extends DesugaringStep<LayerConfigJson> {
|
|||
if (!json.tagRenderings) {
|
||||
return json
|
||||
}
|
||||
if (json.titleIcons.some((ti) => ti === "icons.rating" || ti["id"] === "rating")) {
|
||||
// already added
|
||||
return json
|
||||
}
|
||||
|
||||
const specialVis: Exclude<RenderingSpecification, string>[] = <
|
||||
Exclude<RenderingSpecification, string>[]
|
||||
|
@ -1247,6 +1283,7 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
|
|||
),
|
||||
new SetDefault("titleIcons", ["icons.defaults"]),
|
||||
new AddRatingBadge(),
|
||||
new AddFavouriteBadges(),
|
||||
new On(
|
||||
"titleIcons",
|
||||
(layer) =>
|
||||
|
|
|
@ -968,7 +968,7 @@ export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
|
|||
"Various validation on tagRenderingConfigs",
|
||||
new DetectShadowedMappings(layerConfig),
|
||||
new DetectConflictingAddExtraTags(),
|
||||
new DetectNonErasedKeysInMappings(),
|
||||
// new DetectNonErasedKeysInMappings(),
|
||||
new DetectMappingsWithImages(doesImageExist),
|
||||
new On("render", new ValidatePossibleLinks()),
|
||||
new On("question", new ValidatePossibleLinks()),
|
||||
|
|
|
@ -305,6 +305,9 @@ export default class LayoutConfig implements LayoutInformation {
|
|||
}
|
||||
for (const layer of this.layers) {
|
||||
if (!layer.source) {
|
||||
if (layer.isShown?.matchesProperties(tags)) {
|
||||
return layer
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (layer.source.osmTags.matchesProperties(tags)) {
|
||||
|
|
|
@ -58,6 +58,7 @@ import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLay
|
|||
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
|
||||
import { Imgur } from "../Logic/ImageProviders/Imgur"
|
||||
import NearbyFeatureSource from "../Logic/FeatureSource/Sources/NearbyFeatureSource"
|
||||
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -96,10 +97,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
readonly indexedFeatures: IndexedFeatureSource & LayoutSource
|
||||
readonly currentView: FeatureSource<Feature<Polygon>>
|
||||
readonly featuresInView: FeatureSource
|
||||
readonly favourites: FavouritesFeatureSource
|
||||
/**
|
||||
* Contains a few (<10) >features that are near the center of the map.
|
||||
*/
|
||||
readonly closestFeatures: FeatureSource
|
||||
readonly closestFeatures: NearbyFeatureSource
|
||||
readonly newFeatures: WritableFeatureSource
|
||||
readonly layerState: LayerState
|
||||
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
||||
|
@ -220,8 +222,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.fullNodeDatabase
|
||||
)
|
||||
|
||||
this.indexedFeatures = layoutSource
|
||||
|
||||
let currentViewIndex = 0
|
||||
const empty = []
|
||||
this.currentView = new StaticFeatureSource(
|
||||
|
@ -242,13 +242,19 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
|
||||
|
||||
this.dataIsLoading = layoutSource.isLoading
|
||||
this.indexedFeatures = layoutSource
|
||||
this.featureProperties = new FeaturePropertiesStore(layoutSource)
|
||||
this.favourites = new FavouritesFeatureSource(
|
||||
this.osmConnection,
|
||||
this.featureProperties,
|
||||
layoutSource,
|
||||
layout
|
||||
)
|
||||
|
||||
const indexedElements = this.indexedFeatures
|
||||
this.featureProperties = new FeaturePropertiesStore(indexedElements)
|
||||
this.changes = new Changes(
|
||||
{
|
||||
dryRun: this.featureSwitches.featureSwitchIsTesting,
|
||||
allElements: indexedElements,
|
||||
allElements: layoutSource,
|
||||
featurePropertiesStore: this.featureProperties,
|
||||
osmConnection: this.osmConnection,
|
||||
historicalUserLocations: this.geolocation.historicalUserLocations,
|
||||
|
@ -258,7 +264,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.historicalUserLocations = this.geolocation.historicalUserLocations
|
||||
this.newFeatures = new NewGeometryFromChangesFeatureSource(
|
||||
this.changes,
|
||||
indexedElements,
|
||||
layoutSource,
|
||||
this.featureProperties
|
||||
)
|
||||
layoutSource.addSource(this.newFeatures)
|
||||
|
@ -627,7 +633,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
)
|
||||
),
|
||||
current_view: this.currentView,
|
||||
favourite: this.favourites,
|
||||
}
|
||||
|
||||
this.closestFeatures.registerSource(specialLayers.favourite, "favourite")
|
||||
if (this.layout?.lockLocation) {
|
||||
const bbox = new BBox(this.layout.lockLocation)
|
||||
this.mapProperties.maxbounds.setData(bbox)
|
||||
|
@ -663,12 +672,16 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
rangeIsDisplayed?.syncWith(this.featureSwitches.featureSwitchIsTesting, true)
|
||||
}
|
||||
|
||||
// enumarate all 'normal' layers and match them with the appropriate 'special' layer - if applicable
|
||||
this.layerState.filteredLayers.forEach((flayer) => {
|
||||
const id = flayer.layerDef.id
|
||||
const features: FeatureSource = specialLayers[id]
|
||||
if (features === undefined) {
|
||||
return
|
||||
}
|
||||
if (id === "favourite") {
|
||||
console.log("Matching special layer", id, flayer)
|
||||
}
|
||||
|
||||
this.featureProperties.trackFeatureSource(features)
|
||||
new ShowDataLayer(this.map, {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
48
src/UI/Popup/MarkAsFavourite.svelte
Normal file
48
src/UI/Popup/MarkAsFavourite.svelte
Normal 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>
|
36
src/UI/Popup/MarkAsFavouriteMini.svelte
Normal file
36
src/UI/Popup/MarkAsFavouriteMini.svelte
Normal 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>
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue