Add QR-code to all popups, add direction indicator to popup and visual feedback, make reviews accessible to screenreaders (both to read them and to make them)
This commit is contained in:
parent
5567869bb4
commit
bfd818cb38
33 changed files with 415 additions and 98 deletions
|
@ -40,6 +40,30 @@
|
||||||
"centroid"
|
"centroid"
|
||||||
],
|
],
|
||||||
"anchor": "center"
|
"anchor": "center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"marker": [
|
||||||
|
{
|
||||||
|
"color": "--catch-detail-color",
|
||||||
|
"icon": "direction"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"iconSize": {
|
||||||
|
"render": "0,0",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": "alpha~*",
|
||||||
|
"then": "40,40"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pitchAlignment": "map",
|
||||||
|
"rotation": "{alpha}deg",
|
||||||
|
"location": [
|
||||||
|
"point",
|
||||||
|
"centroid"
|
||||||
|
],
|
||||||
|
"anchor": "center"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"lineRendering": []
|
"lineRendering": []
|
||||||
|
|
|
@ -302,6 +302,14 @@
|
||||||
"condition": "_favourite=yes",
|
"condition": "_favourite=yes",
|
||||||
"icon": "circle:white;heart:red",
|
"icon": "circle:white;heart:red",
|
||||||
"metacondition": "__showTimeSensitiveIcons!=no"
|
"metacondition": "__showTimeSensitiveIcons!=no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "direction",
|
||||||
|
"labels": [
|
||||||
|
"defaults",
|
||||||
|
"in_favourite"
|
||||||
|
],
|
||||||
|
"render": "{direction_indicator()}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -366,7 +366,7 @@
|
||||||
},
|
},
|
||||||
"freeform": {
|
"freeform": {
|
||||||
"key": "min_age",
|
"key": "min_age",
|
||||||
"type": "pnat"
|
"type": "nat"
|
||||||
},
|
},
|
||||||
"id": "playground-min_age"
|
"id": "playground-min_age"
|
||||||
},
|
},
|
||||||
|
|
|
@ -399,12 +399,34 @@
|
||||||
"useSearchForMore": "Use the search function to search within {total} more values…",
|
"useSearchForMore": "Use the search function to search within {total} more values…",
|
||||||
"visualFeedback": {
|
"visualFeedback": {
|
||||||
"closestFeaturesAre": "{n} features within viewport.",
|
"closestFeaturesAre": "{n} features within viewport.",
|
||||||
|
"directionsAbsolute": {
|
||||||
|
"E": "east",
|
||||||
|
"N": "north",
|
||||||
|
"NE": "northeast",
|
||||||
|
"NW": "northwest",
|
||||||
|
"S": "south",
|
||||||
|
"SE": "southeast",
|
||||||
|
"SW": "southwest",
|
||||||
|
"W": "west"
|
||||||
|
},
|
||||||
|
"directionsRelative": {
|
||||||
|
"behind": "on your back",
|
||||||
|
"left": "left",
|
||||||
|
"right": "right",
|
||||||
|
"sharp_left": "sharply left",
|
||||||
|
"sharp_right": "sharply right",
|
||||||
|
"slight_left": "slightly left",
|
||||||
|
"slight_right": "slightly right",
|
||||||
|
"straight": "straight ahead"
|
||||||
|
},
|
||||||
"east": "Moving east",
|
"east": "Moving east",
|
||||||
|
"fromGps": "{distance} {direction} of your location",
|
||||||
|
"fromMapCenter": "{distance} {direction} of the map center",
|
||||||
"in": "Zooming in to level {z}",
|
"in": "Zooming in to level {z}",
|
||||||
"islocked": "View locked to your GPS-location, moving disabled. Press the geolocation button to unlock.",
|
"islocked": "View locked to your GPS-location, moving disabled. Press the geolocation button to unlock.",
|
||||||
"locked": "View is now locked to your GPS-location, moving disabled.",
|
"locked": "View is now locked to your GPS-location, moving disabled.",
|
||||||
"navigation": "Use arrow keys to move the map, press space to select the closest feature. Press a number to select locations further away.",
|
"navigation": "Use arrow keys to move the map, press space to select the closest feature. Press a number to select locations further away.",
|
||||||
"noCloseFeatures": "No features in view",
|
"noCloseFeatures": "No features in view.",
|
||||||
"north": "Moving north",
|
"north": "Moving north",
|
||||||
"oneFeatureInView": "One feature within viewport.",
|
"oneFeatureInView": "One feature within viewport.",
|
||||||
"out": "Zooming out to level {z}",
|
"out": "Zooming out to level {z}",
|
||||||
|
@ -636,12 +658,15 @@
|
||||||
"reviews": {
|
"reviews": {
|
||||||
"affiliated_reviewer_warning": "(Affiliated review)",
|
"affiliated_reviewer_warning": "(Affiliated review)",
|
||||||
"attribution": "Reviews are powered by <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> and are available under <a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0</a>.",
|
"attribution": "Reviews are powered by <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> and are available under <a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0</a>.",
|
||||||
|
"averageRating": "Average rating of {n} stars",
|
||||||
"i_am_affiliated": "I am affiliated with this object",
|
"i_am_affiliated": "I am affiliated with this object",
|
||||||
"i_am_affiliated_explanation": "Check if you are an owner, creator, employee, …",
|
"i_am_affiliated_explanation": "Check if you are an owner, creator, employee, …",
|
||||||
"name_required": "A name is required in order to display and create reviews",
|
"name_required": "A name is required in order to display and create reviews",
|
||||||
"no_reviews_yet": "There are no reviews yet. Be the first to write one and help open data and the business!",
|
"no_reviews_yet": "There are no reviews yet. Be the first to write one and help open data and the business!",
|
||||||
"question": "How would you rate {title()}?",
|
"question": "How would you rate {title()}?",
|
||||||
"question_opinion": "How was your experience?",
|
"question_opinion": "How was your experience?",
|
||||||
|
"rate": "Rate {n} stars",
|
||||||
|
"rated": "Rated {n} stars",
|
||||||
"reviewing_as": "Reviewing as {nickname}",
|
"reviewing_as": "Reviewing as {nickname}",
|
||||||
"reviewing_as_anonymous": "Reviewing as anonymous",
|
"reviewing_as_anonymous": "Reviewing as anonymous",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "mapcomplete",
|
"name": "mapcomplete",
|
||||||
"version": "0.36.7",
|
"version": "0.36.8",
|
||||||
"repository": "https://github.com/pietervdvn/MapComplete",
|
"repository": "https://github.com/pietervdvn/MapComplete",
|
||||||
"description": "A small website to edit OSM easily",
|
"description": "A small website to edit OSM easily",
|
||||||
"bugs": "https://github.com/pietervdvn/MapComplete/issues",
|
"bugs": "https://github.com/pietervdvn/MapComplete/issues",
|
||||||
|
|
|
@ -1347,6 +1347,10 @@ video {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid-cols-3 {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.grid-cols-2 {
|
.grid-cols-2 {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
@ -1450,14 +1454,26 @@ video {
|
||||||
row-gap: 0.5rem;
|
row-gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gap-x-1 {
|
||||||
|
-webkit-column-gap: 0.25rem;
|
||||||
|
column-gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.gap-x-2 {
|
.gap-x-2 {
|
||||||
-webkit-column-gap: 0.5rem;
|
-webkit-column-gap: 0.5rem;
|
||||||
column-gap: 0.5rem;
|
column-gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gap-x-1 {
|
.space-x-0\.5 > :not([hidden]) ~ :not([hidden]) {
|
||||||
-webkit-column-gap: 0.25rem;
|
--tw-space-x-reverse: 0;
|
||||||
column-gap: 0.25rem;
|
margin-right: calc(0.125rem * var(--tw-space-x-reverse));
|
||||||
|
margin-left: calc(0.125rem * calc(1 - var(--tw-space-x-reverse)));
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-x-0 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
--tw-space-x-reverse: 0;
|
||||||
|
margin-right: calc(0px * var(--tw-space-x-reverse));
|
||||||
|
margin-left: calc(0px * calc(1 - var(--tw-space-x-reverse)));
|
||||||
}
|
}
|
||||||
|
|
||||||
.space-x-1 > :not([hidden]) ~ :not([hidden]) {
|
.space-x-1 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
@ -1466,6 +1482,18 @@ video {
|
||||||
margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse)));
|
margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.space-y-0\.5 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
|
margin-top: calc(0.125rem * calc(1 - var(--tw-space-y-reverse)));
|
||||||
|
margin-bottom: calc(0.125rem * var(--tw-space-y-reverse));
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-y-0 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
|
margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse)));
|
||||||
|
margin-bottom: calc(0px * var(--tw-space-y-reverse));
|
||||||
|
}
|
||||||
|
|
||||||
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||||
--tw-space-x-reverse: 0;
|
--tw-space-x-reverse: 0;
|
||||||
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
||||||
|
@ -1554,6 +1582,10 @@ video {
|
||||||
text-overflow: clip;
|
text-overflow: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.break-words {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.break-all {
|
.break-all {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
@ -1695,11 +1727,6 @@ video {
|
||||||
border-color: rgb(219 234 254 / var(--tw-border-opacity));
|
border-color: rgb(219 234 254 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-red-500 {
|
|
||||||
--tw-border-opacity: 1;
|
|
||||||
border-color: rgb(239 68 68 / var(--tw-border-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-gray-300 {
|
.border-gray-300 {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(209 213 219 / var(--tw-border-opacity));
|
border-color: rgb(209 213 219 / var(--tw-border-opacity));
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { GeoOperations } from "../GeoOperations"
|
||||||
import { OsmTags } from "../../Models/OsmFeature"
|
import { OsmTags } from "../../Models/OsmFeature"
|
||||||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
|
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
|
||||||
import { MapProperties } from "../../Models/MapProperties"
|
import { MapProperties } from "../../Models/MapProperties"
|
||||||
|
import { Orientation } from "../../Sensors/Orientation"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The geolocation-handler takes a map-location and a geolocation state.
|
* The geolocation-handler takes a map-location and a geolocation state.
|
||||||
|
@ -128,10 +129,10 @@ export default class GeoLocationHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// We check that the GPS location is not out of bounds
|
// We check that the GPS location is not out of bounds
|
||||||
const bounds = this.mapProperties.maxbounds.data
|
const bounds: BBox = this.mapProperties.maxbounds.data
|
||||||
if (bounds !== undefined) {
|
if (bounds !== undefined) {
|
||||||
// B is an array with our lock-location
|
// B is an array with our lock-location
|
||||||
const inRange = new BBox(bounds).contains([newLocation.longitude, newLocation.latitude])
|
const inRange = bounds.contains([newLocation.longitude, newLocation.latitude])
|
||||||
if (!inRange) {
|
if (!inRange) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -167,6 +168,9 @@ export default class GeoLocationHandler {
|
||||||
altitude: location.altitude,
|
altitude: location.altitude,
|
||||||
altitudeAccuracy: location.altitudeAccuracy,
|
altitudeAccuracy: location.altitudeAccuracy,
|
||||||
heading: location.heading,
|
heading: location.heading,
|
||||||
|
alpha: Orientation.singleton.gotMeasurement.data
|
||||||
|
? "" + Orientation.singleton.alpha.data
|
||||||
|
: undefined,
|
||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
|
|
||||||
|
|
|
@ -55,8 +55,9 @@ export default class FeatureReviews {
|
||||||
private static readonly _featureReviewsCache: Record<string, FeatureReviews> = {}
|
private static readonly _featureReviewsCache: Record<string, FeatureReviews> = {}
|
||||||
public readonly subjectUri: Store<string>
|
public readonly subjectUri: Store<string>
|
||||||
public readonly average: Store<number | null>
|
public readonly average: Store<number | null>
|
||||||
private readonly _reviews: UIEventSource<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
|
private readonly _reviews: UIEventSource<
|
||||||
new UIEventSource([])
|
(Review & { kid: string; signature: string; madeByLoggedInUser: Store<boolean> })[]
|
||||||
|
> = new UIEventSource([])
|
||||||
public readonly reviews: Store<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
|
public readonly reviews: Store<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
|
||||||
this._reviews
|
this._reviews
|
||||||
private readonly _lat: number
|
private readonly _lat: number
|
||||||
|
@ -176,11 +177,15 @@ export default class FeatureReviews {
|
||||||
...review,
|
...review,
|
||||||
}
|
}
|
||||||
const keypair: CryptoKeyPair = this._identity.keypair.data
|
const keypair: CryptoKeyPair = this._identity.keypair.data
|
||||||
console.log(r)
|
|
||||||
const jwt = await MangroveReviews.signReview(keypair, r)
|
const jwt = await MangroveReviews.signReview(keypair, r)
|
||||||
console.log("Signed:", jwt)
|
const kid = await MangroveReviews.publicToPem(keypair.publicKey)
|
||||||
await MangroveReviews.submitReview(jwt)
|
await MangroveReviews.submitReview(jwt)
|
||||||
this._reviews.data.push({ ...r, madeByLoggedInUser: new ImmutableStore(true) })
|
this._reviews.data.push({
|
||||||
|
...r,
|
||||||
|
kid,
|
||||||
|
signature: jwt,
|
||||||
|
madeByLoggedInUser: new ImmutableStore(true),
|
||||||
|
})
|
||||||
this._reviews.ping()
|
this._reviews.ping()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +194,7 @@ export default class FeatureReviews {
|
||||||
* @param reviews
|
* @param reviews
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private addReviews(reviews: { payload: Review; kid: string }[]) {
|
private addReviews(reviews: { payload: Review; kid: string; signature: string }[]) {
|
||||||
const self = this
|
const self = this
|
||||||
const alreadyKnown = new Set(self._reviews.data.map((r) => r.rating + " " + r.opinion))
|
const alreadyKnown = new Set(self._reviews.data.map((r) => r.rating + " " + r.opinion))
|
||||||
|
|
||||||
|
@ -199,7 +204,6 @@ export default class FeatureReviews {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL(review.sub)
|
const url = new URL(review.sub)
|
||||||
console.log("URL is", url)
|
|
||||||
if (url.protocol === "geo:") {
|
if (url.protocol === "geo:") {
|
||||||
const coordinate = <[number, number]>(
|
const coordinate = <[number, number]>(
|
||||||
url.pathname.split(",").map((n) => Number(n))
|
url.pathname.split(",").map((n) => Number(n))
|
||||||
|
@ -222,6 +226,8 @@ export default class FeatureReviews {
|
||||||
}
|
}
|
||||||
self._reviews.data.push({
|
self._reviews.data.push({
|
||||||
...review,
|
...review,
|
||||||
|
kid: reviewData.kid,
|
||||||
|
signature: reviewData.signature,
|
||||||
madeByLoggedInUser: this._identity.key_id.map((user_key_id) => {
|
madeByLoggedInUser: this._identity.key_id.map((user_key_id) => {
|
||||||
return reviewData.kid === user_key_id
|
return reviewData.kid === user_key_id
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -99,6 +99,7 @@ export default class Constants {
|
||||||
* In seconds
|
* In seconds
|
||||||
*/
|
*/
|
||||||
static zoomToLocationTimeout = 15
|
static zoomToLocationTimeout = 15
|
||||||
|
public static readonly viewportCenterCloseToGpsCutoff: number = 20
|
||||||
private static readonly config = (() => {
|
private static readonly config = (() => {
|
||||||
const defaultConfig = packagefile.config
|
const defaultConfig = packagefile.config
|
||||||
return { ...defaultConfig, ...extraconfig }
|
return { ...defaultConfig, ...extraconfig }
|
||||||
|
|
|
@ -576,6 +576,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
||||||
"last_edit",
|
"last_edit",
|
||||||
"favourite_state",
|
"favourite_state",
|
||||||
"all_tags",
|
"all_tags",
|
||||||
|
"qr_code",
|
||||||
]
|
]
|
||||||
private readonly _desugaring: DesugaringContext
|
private readonly _desugaring: DesugaringContext
|
||||||
|
|
||||||
|
@ -657,6 +658,13 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!usedSpecialFunctions.has("qr_code")) {
|
||||||
|
json.tagRenderings.push({
|
||||||
|
id: "qr_code",
|
||||||
|
render: { "*": "{qr_code()}" },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (!usedSpecialFunctions.has("all_tags")) {
|
if (!usedSpecialFunctions.has("all_tags")) {
|
||||||
const trc: QuestionableTagRenderingConfigJson = {
|
const trc: QuestionableTagRenderingConfigJson = {
|
||||||
id: "all-tags",
|
id: "all-tags",
|
||||||
|
|
|
@ -631,6 +631,10 @@ export default class TagRenderingConfig {
|
||||||
* , "testcase")
|
* , "testcase")
|
||||||
* config.constructChangeSpecification(undefined, undefined, [false, true, false], {amenity: "public_bookcase"}) // => new And([new Tag("books","adults")])
|
* config.constructChangeSpecification(undefined, undefined, [false, true, false], {amenity: "public_bookcase"}) // => new And([new Tag("books","adults")])
|
||||||
*
|
*
|
||||||
|
* const config = new TagRenderingConfig({"id":"capacity", "render": "Fits {capcity} books",freeform: {"key":"capacity",type:"pnat"} })
|
||||||
|
* config.constructChangeSpecification("", undefined, undefined, {}) // => undefined
|
||||||
|
* config.constructChangeSpecification("5", undefined, undefined, {}).optimize() // => new Tag("capacity", "5")
|
||||||
|
*
|
||||||
* @param freeformValue The freeform value which will be applied as 'freeform.key'. Ignored if 'freeform.key' is not set
|
* @param freeformValue The freeform value which will be applied as 'freeform.key'. Ignored if 'freeform.key' is not set
|
||||||
*
|
*
|
||||||
* @param singleSelectedMapping (Only used if multiAnswer == false): the single mapping to apply. Use (mappings.length) for the freeform
|
* @param singleSelectedMapping (Only used if multiAnswer == false): the single mapping to apply. Use (mappings.length) for the freeform
|
||||||
|
|
|
@ -49,7 +49,7 @@ export class Orientation {
|
||||||
if (rotateAlpha) {
|
if (rotateAlpha) {
|
||||||
this._animateFakeMeasurements = true
|
this._animateFakeMeasurements = true
|
||||||
Stores.Chronic(25).addCallback((date) => {
|
Stores.Chronic(25).addCallback((date) => {
|
||||||
this.alpha.setData((date.getTime() / 100) % 360)
|
this.alpha.setData((date.getTime() / 50) % 360)
|
||||||
if (!this._animateFakeMeasurements) {
|
if (!this._animateFakeMeasurements) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,20 +8,25 @@
|
||||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||||
import { Store } from "../../Logic/UIEventSource"
|
import { Store } from "../../Logic/UIEventSource"
|
||||||
import type { Feature } from "geojson"
|
import type { Feature } from "geojson"
|
||||||
import ThemeViewState from "../../Models/ThemeViewState"
|
|
||||||
import Compass_arrow from "../../assets/svg/Compass_arrow.svelte"
|
import Compass_arrow from "../../assets/svg/Compass_arrow.svelte"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
import { Orientation } from "../../Sensors/Orientation"
|
import { Orientation } from "../../Sensors/Orientation"
|
||||||
|
import Translations from "../i18n/Translations"
|
||||||
|
import Constants from "../../Models/Constants"
|
||||||
|
import Locale from "../i18n/Locale"
|
||||||
|
import { ariaLabelStore } from "../../Utils/ariaLabel"
|
||||||
|
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
|
|
||||||
export let state: ThemeViewState
|
export let state: SpecialVisualizationState
|
||||||
export let feature: Feature
|
export let feature: Feature
|
||||||
|
export let size = "w-8 h-8"
|
||||||
|
|
||||||
let fcenter = GeoOperations.centerpointCoordinates(feature)
|
let fcenter = GeoOperations.centerpointCoordinates(feature)
|
||||||
// Bearing and distance relative to the map center
|
// Bearing and distance relative to the map center
|
||||||
let bearingAndDist: Store<{ bearing: number; dist: number }> = state.mapProperties.location.map(
|
let bearingAndDist: Store<{ bearing: number; dist: number }> = state.mapProperties.location.map(
|
||||||
(l) => {
|
(l) => {
|
||||||
let mapCenter = [l.lon, l.lat]
|
let mapCenter = [l.lon, l.lat]
|
||||||
let bearing = Math.round(GeoOperations.bearing(fcenter, mapCenter))
|
let bearing = Math.round(GeoOperations.bearing(mapCenter, fcenter))
|
||||||
let dist = Math.round(GeoOperations.distanceBetween(fcenter, mapCenter))
|
let dist = Math.round(GeoOperations.distanceBetween(fcenter, mapCenter))
|
||||||
return { bearing, dist }
|
return { bearing, dist }
|
||||||
},
|
},
|
||||||
|
@ -29,19 +34,87 @@
|
||||||
let bearingFromGps = state.geolocation.geolocationState.currentGPSLocation.mapD(coordinate => {
|
let bearingFromGps = state.geolocation.geolocationState.currentGPSLocation.mapD(coordinate => {
|
||||||
return GeoOperations.bearing([coordinate.longitude, coordinate.latitude], fcenter)
|
return GeoOperations.bearing([coordinate.longitude, coordinate.latitude], fcenter)
|
||||||
})
|
})
|
||||||
let compass = Orientation.singleton.alpha.map(compass => compass ?? 0)
|
let compass = Orientation.singleton.alpha
|
||||||
export let size = "w-8 h-8"
|
|
||||||
|
let relativeDirections = Translations.t.general.visualFeedback.directionsRelative
|
||||||
|
let absoluteDirections = Translations.t.general.visualFeedback.directionsAbsolute
|
||||||
|
|
||||||
|
let closeToCurrentLocation = state.geolocation.geolocationState.currentGPSLocation.map(gps => {
|
||||||
|
if (!gps) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let l = state.mapProperties.location.data
|
||||||
|
let mapCenter = [l.lon, l.lat]
|
||||||
|
const dist = GeoOperations.distanceBetween([gps.longitude, gps.latitude], mapCenter)
|
||||||
|
return dist < Constants.viewportCenterCloseToGpsCutoff
|
||||||
|
},
|
||||||
|
[state.mapProperties.location],
|
||||||
|
)
|
||||||
|
let labelFromCenter: Store<string> = bearingAndDist.mapD(({ bearing, dist }) => {
|
||||||
|
const distHuman = GeoOperations.distanceToHuman(dist)
|
||||||
|
const lang = Locale.language.data
|
||||||
|
const t = absoluteDirections[GeoOperations.bearingToHuman(bearing)]
|
||||||
|
const mainTr = Translations.t.general.visualFeedback.fromMapCenter.Subs({
|
||||||
|
distance: distHuman,
|
||||||
|
direction: t.textFor(lang),
|
||||||
|
})
|
||||||
|
return mainTr.textFor(lang)
|
||||||
|
}, [compass, Locale.language])
|
||||||
|
|
||||||
|
|
||||||
|
// Bearing and distance relative to the map center
|
||||||
|
let bearingAndDistGps: Store<{
|
||||||
|
bearing: number;
|
||||||
|
dist: number
|
||||||
|
} | undefined> = state.geolocation.geolocationState.currentGPSLocation.mapD(
|
||||||
|
({ longitude, latitude }) => {
|
||||||
|
let gps = [longitude, latitude]
|
||||||
|
let bearing = Math.round(GeoOperations.bearing(gps, fcenter))
|
||||||
|
let dist = Math.round(GeoOperations.distanceBetween(fcenter, gps))
|
||||||
|
return { bearing, dist }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
let labelFromGps: Store<string | undefined> = bearingAndDistGps.mapD(({ bearing, dist }) => {
|
||||||
|
const distHuman = GeoOperations.distanceToHuman(dist)
|
||||||
|
const lang = Locale.language.data
|
||||||
|
let bearingHuman: string
|
||||||
|
if (compass.data !== undefined) {
|
||||||
|
console.log("compass:", compass.data)
|
||||||
|
const bearingRelative = bearing - compass.data
|
||||||
|
const t = relativeDirections[GeoOperations.bearingToHumanRelative(bearingRelative)]
|
||||||
|
bearingHuman = t.textFor(lang)
|
||||||
|
} else {
|
||||||
|
bearingHuman = absoluteDirections[GeoOperations.bearingToHuman(bearing)].textFor(lang)
|
||||||
|
}
|
||||||
|
const mainTr = Translations.t.general.visualFeedback.fromGps.Subs({
|
||||||
|
distance: distHuman,
|
||||||
|
direction: bearingHuman,
|
||||||
|
})
|
||||||
|
return mainTr.textFor(lang)
|
||||||
|
}, [compass, Locale.language])
|
||||||
|
|
||||||
|
let label = labelFromCenter.map(labelFromCenter => {
|
||||||
|
if (labelFromGps.data !== undefined) {
|
||||||
|
if(closeToCurrentLocation.data){
|
||||||
|
return labelFromGps.data
|
||||||
|
}
|
||||||
|
return labelFromCenter + ", " + labelFromGps.data
|
||||||
|
}
|
||||||
|
return labelFromCenter
|
||||||
|
}, [labelFromGps])
|
||||||
|
function focusMap(){
|
||||||
|
state.mapProperties.location.setData({ lon: fcenter[0], lat: fcenter[1] })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={twMerge("relative", size)}>
|
<button class={twMerge("relative rounded-full soft", size)} use:ariaLabelStore={label} on:click={() => focusMap()}>
|
||||||
<div class={twMerge("absolute top-0 left-0 flex items-center justify-center text-sm",size)}>
|
<div class={twMerge("absolute top-0 left-0 flex items-center justify-center text-sm break-words",size)}>
|
||||||
{GeoOperations.distanceToHuman($bearingAndDist.dist)}
|
{GeoOperations.distanceToHuman($bearingAndDistGps.dist)}
|
||||||
</div>
|
</div>
|
||||||
{#if $bearingFromGps !== undefined}
|
{#if $bearingFromGps !== undefined}
|
||||||
<div class={twMerge("absolute top-0 left-0 rounded-full border border-gray-500", size)}>
|
<div class={twMerge("absolute top-0 left-0 rounded-full", size)}>
|
||||||
<Compass_arrow class={size}
|
<Compass_arrow class={size}
|
||||||
style={`transform: rotate( calc( 45deg + ${$bearingFromGps - $compass}deg) );`} />
|
style={`transform: rotate( calc( 45deg + ${$bearingFromGps - ($compass ?? 0)}deg) );`} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</button>
|
||||||
<span>{$bearingAndDist.bearing}° {GeoOperations.bearingToHuman($bearingAndDist.bearing)} {GeoOperations.bearingToHumanRelative($bearingAndDist.bearing - $compass)}</span>
|
|
||||||
|
|
|
@ -11,11 +11,12 @@
|
||||||
export let cls: string = ""
|
export let cls: string = ""
|
||||||
// Text for the current language
|
// Text for the current language
|
||||||
let txt: Store<string | undefined> = t?.current
|
let txt: Store<string | undefined> = t?.current
|
||||||
|
$: {txt = t?.current}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $txt}
|
{#if $txt}
|
||||||
<span class={cls}>
|
<span class={cls}>
|
||||||
<FromHtml src={$txt} />
|
<FromHtml src={$txt} />
|
||||||
<WeblateLink context={t.context} />
|
<WeblateLink context={t?.context} />
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -4,12 +4,16 @@
|
||||||
import ThemeViewState from "../../Models/ThemeViewState"
|
import ThemeViewState from "../../Models/ThemeViewState"
|
||||||
import Tr from "../Base/Tr.svelte"
|
import Tr from "../Base/Tr.svelte"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
|
import { Orientation } from "../../Sensors/Orientation"
|
||||||
|
import { Translation } from "../i18n/Translation"
|
||||||
|
import Constants from "../../Models/Constants"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates how far away the viewport center is from the current user location
|
* Indicates how far away the viewport center is from the current user location
|
||||||
*/
|
*/
|
||||||
export let state: ThemeViewState
|
export let state: ThemeViewState
|
||||||
const t = Translations.t.general.visualFeedback
|
const t = Translations.t.general.visualFeedback
|
||||||
|
const relativeDir = t.directionsRelative
|
||||||
let map = state.mapProperties
|
let map = state.mapProperties
|
||||||
|
|
||||||
let currentLocation = state.geolocation.geolocationState.currentGPSLocation
|
let currentLocation = state.geolocation.geolocationState.currentGPSLocation
|
||||||
|
@ -23,14 +27,29 @@
|
||||||
const distanceInMeters = Math.round(GeoOperations.distanceBetween(gps, mapCenter))
|
const distanceInMeters = Math.round(GeoOperations.distanceBetween(gps, mapCenter))
|
||||||
const distance = GeoOperations.distanceToHuman(distanceInMeters)
|
const distance = GeoOperations.distanceToHuman(distanceInMeters)
|
||||||
const bearing = Math.round(GeoOperations.bearing(gps, mapCenter))
|
const bearing = Math.round(GeoOperations.bearing(gps, mapCenter))
|
||||||
return { distance, bearing, distanceInMeters }
|
const bearingDirection = GeoOperations.bearingToHuman(bearing)
|
||||||
|
return { distance, bearing, distanceInMeters, bearingDirection }
|
||||||
}, [currentLocation])
|
}, [currentLocation])
|
||||||
|
let hasCompass = Orientation.singleton.gotMeasurement
|
||||||
|
let compass = Orientation.singleton.alpha
|
||||||
|
let relativeBearing: Store<{distance: string, bearing: Translation}> =
|
||||||
|
compass.mapD(compass => {
|
||||||
|
const bearing: Translation = relativeDir[GeoOperations.bearingToHumanRelative(distanceToCurrentLocation.data.bearing - compass)]
|
||||||
|
return {bearing, distance: distanceToCurrentLocation.data.distance}
|
||||||
|
}, [distanceToCurrentLocation])
|
||||||
|
let viewportCenterDetails = Translations.DynamicSubstitute(t.viewportCenterDetails, relativeBearing)
|
||||||
|
let viewportCenterDetailsAbsolute = Translations.DynamicSubstitute(t.viewportCenterDetails, distanceToCurrentLocation.map(({distance, bearing}) => {
|
||||||
|
return {distance, bearing: t.directionsAbsolute[GeoOperations.bearingToHuman(bearing)]}
|
||||||
|
}))
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $currentLocation !== undefined}
|
{#if $currentLocation !== undefined}
|
||||||
{#if $distanceToCurrentLocation.distanceInMeters < 20}
|
{#if $distanceToCurrentLocation.distanceInMeters < Constants.viewportCenterCloseToGpsCutoff}
|
||||||
<Tr t={t.viewportCenterCloseToGps} />
|
<Tr t={t.viewportCenterCloseToGps} />
|
||||||
|
{:else if $hasCompass}
|
||||||
|
{$viewportCenterDetails}
|
||||||
{:else}
|
{:else}
|
||||||
<Tr t={t.viewportCenterDetails.Subs($distanceToCurrentLocation)} />
|
{$viewportCenterDetailsAbsolute}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -55,11 +55,10 @@
|
||||||
|
|
||||||
{#if currentLocation}
|
{#if currentLocation}
|
||||||
<div
|
<div
|
||||||
role="alert"
|
|
||||||
aria-live="assertive"
|
aria-live="assertive"
|
||||||
class="normal-background border-interactive rounded-full px-2 flex flex-col items-center"
|
class="normal-background border-interactive rounded-full px-2 flex flex-col items-center"
|
||||||
>
|
>
|
||||||
{currentLocation}
|
{currentLocation}.
|
||||||
<MapCenterDetails {state}/>
|
<MapCenterDetails {state}/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
||||||
import DirectionIndicator from "../Base/DirectionIndicator.svelte"
|
import DirectionIndicator from "../Base/DirectionIndicator.svelte"
|
||||||
|
import ThemeViewState from "../../Models/ThemeViewState"
|
||||||
|
|
||||||
export let state: SpecialVisualizationState
|
export let state: SpecialVisualizationState
|
||||||
export let feature: Feature
|
export let feature: Feature
|
||||||
|
@ -14,11 +15,14 @@
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a class="small flex space-x-1 cursor-pointer w-fit" href={`#${feature.properties.id}`}>
|
<span class="inline-flex gap-x-1">
|
||||||
|
|
||||||
|
<a class="small flex space-x-0.5 cursor-pointer w-fit items-center" href={`#${feature.properties.id}`}>
|
||||||
{#if i !== undefined}
|
{#if i !== undefined}
|
||||||
<span class="font-bold">{i + 1} </span>
|
<span class="font-bold">{i + 1} </span>
|
||||||
{/if}
|
{/if}
|
||||||
<TagRenderingAnswer config={layer.title} extraClasses="inline-flex w-fit" {layer} selectedElement={feature} {state}
|
<TagRenderingAnswer config={layer.title} extraClasses="inline-flex w-fit" {layer} selectedElement={feature} {state}
|
||||||
{tags} />
|
{tags} />
|
||||||
<DirectionIndicator {feature} {state} />
|
|
||||||
</a>
|
</a>
|
||||||
|
<DirectionIndicator {feature} {state} />
|
||||||
|
</span>
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
lastAction.stabilized(750).addCallbackAndRunD((_) => lastAction.setData(undefined))
|
lastAction.stabilized(750).addCallbackAndRunD((_) => lastAction.setData(undefined))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div aria-live="assertive" class="p-1 interactive" role="alert">
|
<div aria-live="assertive" class="p-1 bg-white m-1 rounded">
|
||||||
{#if $lastAction?.key === "out"}
|
{#if $lastAction?.key === "out"}
|
||||||
<Tr t={t.out.Subs({z: map.zoom.data - 1})} />
|
<Tr t={t.out.Subs({z: map.zoom.data - 1})} />
|
||||||
{:else if $lastAction?.key === "in"}
|
{:else if $lastAction?.key === "in"}
|
||||||
|
@ -46,13 +46,11 @@
|
||||||
<div class="pointer-events-auto">
|
<div class="pointer-events-auto">
|
||||||
<Tr t={$translationWithLength} />
|
<Tr t={$translationWithLength} />
|
||||||
<MapCenterDetails {state} />
|
<MapCenterDetails {state} />
|
||||||
<ol>
|
<div class="grid grid-cols-3 space-x-1 space-y-0.5">
|
||||||
{#each $centerFeatures.slice(0, 9) as feat, i (feat.properties.id)}
|
{#each $centerFeatures.slice(0, 9) as feat, i (feat.properties.id)}
|
||||||
<li>
|
|
||||||
<Summary {state} feature={feat} {i} />
|
<Summary {state} feature={feat} {i} />
|
||||||
</li>
|
|
||||||
{/each}
|
{/each}
|
||||||
</ol>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
||||||
import type { Feature } from "geojson"
|
import type { Feature } from "geojson"
|
||||||
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
|
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||||
import Center from "../../assets/svg/Center.svelte"
|
import DirectionIndicator from "../Base/DirectionIndicator.svelte"
|
||||||
|
|
||||||
export let feature: Feature
|
export let feature: Feature
|
||||||
let properties: Record<string, string> = feature.properties
|
let properties: Record<string, string> = feature.properties
|
||||||
|
@ -30,11 +30,6 @@
|
||||||
center()
|
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]))
|
|
||||||
return GeoOperations.distanceToHuman(meters)
|
|
||||||
})
|
|
||||||
const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"]
|
const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -57,7 +52,7 @@
|
||||||
class="title-icons links-as-button flex flex-wrap items-center gap-x-0.5 self-end justify-self-end p-1 pt-0.5 sm:pt-1"
|
class="title-icons links-as-button flex flex-wrap items-center gap-x-0.5 self-end justify-self-end p-1 pt-0.5 sm:pt-1"
|
||||||
>
|
>
|
||||||
{#each favConfig.titleIcons as titleIconConfig}
|
{#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)}
|
{#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"}>
|
<div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}>
|
||||||
<TagRenderingAnswer
|
<TagRenderingAnswer
|
||||||
config={titleIconConfig}
|
config={titleIconConfig}
|
||||||
|
@ -71,12 +66,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<button class="p-1" on:click={() => center()}>
|
<DirectionIndicator {state} {feature} />
|
||||||
<Center class="h-6 w-6" />
|
|
||||||
</button>
|
|
||||||
<div class="w-14">
|
|
||||||
{$distance}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
|
|
||||||
<div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}>
|
<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.intro.Subs({ length: $favourites?.length ?? 0 })} />
|
||||||
<Tr t={Translations.t.favouritePoi.privacy} />
|
<Tr t={Translations.t.favouritePoi.priintroPrivacyvacy} />
|
||||||
|
|
||||||
{#each $favourites as feature (feature.properties.id)}
|
{#each $favourites as feature (feature.properties.id)}
|
||||||
<FavouriteSummary {feature} {state} />
|
<FavouriteSummary {feature} {state} />
|
||||||
|
|
|
@ -57,7 +57,11 @@
|
||||||
validator = Validators.get(type ?? "string")
|
validator = Validators.get(type ?? "string")
|
||||||
|
|
||||||
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
|
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
|
||||||
|
if(_value.data?.length > 0){
|
||||||
feedback?.setData(validator?.getFeedback(_value.data, getCountry))
|
feedback?.setData(validator?.getFeedback(_value.data, getCountry))
|
||||||
|
}else{
|
||||||
|
feedback?.setData(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
initValueAndDenom()
|
initValueAndDenom()
|
||||||
}
|
}
|
||||||
|
@ -65,9 +69,14 @@
|
||||||
function setValues() {
|
function setValues() {
|
||||||
// Update the value stores
|
// Update the value stores
|
||||||
const v = _value.data
|
const v = _value.data
|
||||||
if (!validator?.isValid(v, getCountry) || v === "") {
|
if(v === ""){
|
||||||
|
value.setData(undefined)
|
||||||
|
feedback.setData(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!validator?.isValid(v, getCountry)) {
|
||||||
feedback?.setData(validator?.getFeedback(v, getCountry))
|
feedback?.setData(validator?.getFeedback(v, getCountry))
|
||||||
value.setData("")
|
value.setData(undefined)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,15 +16,22 @@ export default class NatValidator extends IntValidator {
|
||||||
return str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0
|
return str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* const validator = new NatValidator()
|
||||||
|
* validator.getFeedback(-4).textFor("en") // => "This number should be positive"
|
||||||
|
*/
|
||||||
getFeedback(s: string): Translation {
|
getFeedback(s: string): Translation {
|
||||||
|
console.log("Getting feedback for", s)
|
||||||
|
const n = Number(s)
|
||||||
|
if (!isNaN(n) && n < 0) {
|
||||||
|
return Translations.t.validation.nat.mustBePositive
|
||||||
|
}
|
||||||
const spr = super.getFeedback(s)
|
const spr = super.getFeedback(s)
|
||||||
if (spr !== undefined) {
|
if (spr !== undefined) {
|
||||||
return spr
|
return spr
|
||||||
}
|
}
|
||||||
const n = Number(s)
|
|
||||||
if (n < 0) {
|
|
||||||
return Translations.t.validation.nat.mustBePositive
|
|
||||||
}
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,16 +3,20 @@
|
||||||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||||
import DynamicIcon from "./DynamicIcon.svelte"
|
import DynamicIcon from "./DynamicIcon.svelte"
|
||||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||||
|
import { Orientation } from "../../Sensors/Orientation"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a 'marker', which consists of multiple 'icons'
|
* Renders a 'marker', which consists of multiple 'icons'
|
||||||
*/
|
*/
|
||||||
export let marker: IconConfig[] = config?.marker
|
export let marker: IconConfig[]
|
||||||
export let tags: Store<Record<string, string>>
|
export let tags: Store<Record<string, string>>
|
||||||
export let rotation: TagRenderingConfig = undefined
|
export let rotation: TagRenderingConfig = undefined
|
||||||
let _rotation = rotation
|
let _rotation: Store<string> = rotation
|
||||||
? tags.map((tags) => rotation.GetRenderValue(tags).Subs(tags).txt)
|
? tags.map((tags) => rotation.GetRenderValue(tags).Subs(tags).txt)
|
||||||
: new ImmutableStore(0)
|
: new ImmutableStore("0deg")
|
||||||
|
if(rotation?.render?.txt === "{alpha}deg"){
|
||||||
|
_rotation = Orientation.singleton.alpha.map(alpha => alpha ? (alpha)+"deg" : "0deg ")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if marker && marker}
|
{#if marker && marker}
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
import Confirm from "../../assets/svg/Confirm.svelte"
|
import Confirm from "../../assets/svg/Confirm.svelte"
|
||||||
import Not_found from "../../assets/svg/Not_found.svelte"
|
import Not_found from "../../assets/svg/Not_found.svelte"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
|
import Direction_gradient from "../../assets/svg/Direction_gradient.svelte"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a single icon.
|
* Renders a single icon.
|
||||||
|
@ -100,6 +101,8 @@
|
||||||
<HeartOutlineIcon class={clss} />
|
<HeartOutlineIcon class={clss} />
|
||||||
{:else if icon === "confirm"}
|
{:else if icon === "confirm"}
|
||||||
<Confirm class={clss} {color} />
|
<Confirm class={clss} {color} />
|
||||||
|
{:else if icon === "direction"}
|
||||||
|
<Direction_gradient class={clss} {color} />
|
||||||
{:else if icon === "not_found"}
|
{:else if icon === "not_found"}
|
||||||
<Not_found class={twMerge(clss, "no-image-background")} {color} />
|
<Not_found class={twMerge(clss, "no-image-background")} {color} />
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
@ -171,6 +171,7 @@ class PointRenderingLayer {
|
||||||
store
|
store
|
||||||
.map((tags) => this._config.rotationAlignment.GetRenderValue(tags).Subs(tags).txt)
|
.map((tags) => this._config.rotationAlignment.GetRenderValue(tags).Subs(tags).txt)
|
||||||
.addCallbackAndRun((pitchAligment) => marker.setRotationAlignment(<any>pitchAligment))
|
.addCallbackAndRun((pitchAligment) => marker.setRotationAlignment(<any>pitchAligment))
|
||||||
|
|
||||||
if (feature.geometry.type === "Point") {
|
if (feature.geometry.type === "Point") {
|
||||||
// When the tags get 'pinged', check that the location didn't change
|
// When the tags get 'pinged', check that the location didn't change
|
||||||
store.addCallbackAndRunD(() => {
|
store.addCallbackAndRunD(() => {
|
||||||
|
|
|
@ -4,13 +4,11 @@
|
||||||
import StarsBar from "./StarsBar.svelte"
|
import StarsBar from "./StarsBar.svelte"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
import Tr from "../Base/Tr.svelte"
|
import Tr from "../Base/Tr.svelte"
|
||||||
|
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||||
|
|
||||||
export let review: Review & { madeByLoggedInUser: Store<boolean> }
|
export let review: Review & { kid: string,signature: string, madeByLoggedInUser: Store<boolean> }
|
||||||
let name = review.metadata.nickname
|
let name = review.metadata.nickname
|
||||||
name ??= (review.metadata.given_name ?? "") + " " + (review.metadata.family_name ?? "").trim()
|
name ??= ((review.metadata.given_name ?? "") + " " + (review.metadata.family_name ?? "")).trim()
|
||||||
if (name.length === 0) {
|
|
||||||
name = "Anonymous"
|
|
||||||
}
|
|
||||||
let d = new Date()
|
let d = new Date()
|
||||||
d.setTime(review.iat * 1000)
|
d.setTime(review.iat * 1000)
|
||||||
let date = d.toDateString()
|
let date = d.toDateString()
|
||||||
|
@ -19,18 +17,32 @@
|
||||||
|
|
||||||
<div class={"low-interaction rounded-lg p-1 px-2 " + ($byLoggedInUser ? "border-interactive" : "")}>
|
<div class={"low-interaction rounded-lg p-1 px-2 " + ($byLoggedInUser ? "border-interactive" : "")}>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<StarsBar score={review.rating} />
|
<div tabindex="0" use:ariaLabel={Translations.t.reviews.rated.Subs({n: ""+(Math.round(review.rating / 10)/2)})}>
|
||||||
<div class="flex flex-wrap space-x-2">
|
<StarsBar readonly={true} score={review.rating} />
|
||||||
<div class="font-bold">
|
|
||||||
{name}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-wrap space-x-2">
|
||||||
|
<a href={`https://mangrove.reviews/list?kid=${encodeURIComponent(review.kid)}`} rel="noopener"
|
||||||
|
target="_blank">
|
||||||
|
{#if !name}
|
||||||
|
<i>Anonymous</i>
|
||||||
|
{:else}
|
||||||
|
<span class="font-bold">
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
<span class="subtle">
|
<span class="subtle">
|
||||||
{date}
|
{date}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if review.opinion}
|
{#if review.opinion}
|
||||||
|
<div class="link-no-underline">
|
||||||
|
<a target="_blank" rel="noopener nofollow"
|
||||||
|
href={`https://mangrove.reviews/list?signature=${encodeURIComponent(review.signature)}`}>
|
||||||
{review.opinion}
|
{review.opinion}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if review.metadata.is_affiliated}
|
{#if review.metadata.is_affiliated}
|
||||||
<Tr t={Translations.t.reviews.affiliated_reviewer_warning} />
|
<Tr t={Translations.t.reviews.affiliated_reviewer_warning} />
|
||||||
|
|
|
@ -1,31 +1,32 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
|
||||||
import Svg from "../../Svg"
|
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import Star from "../../assets/svg/Star.svelte"
|
import Star from "../../assets/svg/Star.svelte"
|
||||||
import Star_half from "../../assets/svg/Star_half.svelte"
|
import Star_half from "../../assets/svg/Star_half.svelte"
|
||||||
import Star_outline from "../../assets/svg/Star_outline.svelte"
|
import Star_outline from "../../assets/svg/Star_outline.svelte"
|
||||||
|
import { ariaLabel, ariaLabelStore } from "../../Utils/ariaLabel"
|
||||||
|
import Translations from "../i18n/Translations"
|
||||||
|
|
||||||
export let score: number
|
export let score: number
|
||||||
export let cutoff: number
|
export let cutoff: number
|
||||||
export let starSize = "w-h h-4"
|
export let starSize = "w-h h-4"
|
||||||
export let i: number
|
export let i: number
|
||||||
|
export let readonly = false
|
||||||
|
|
||||||
let dispatch = createEventDispatcher<{ hover: { score: number }, click: { score: number } }>()
|
let dispatch = createEventDispatcher<{ hover: { score: number }, click: { score: number } }>()
|
||||||
let container: HTMLElement
|
let container: HTMLElement
|
||||||
|
|
||||||
function getScore(e: MouseEvent): number {
|
function getScore(e: MouseEvent): number {
|
||||||
|
if (e.clientX === 0 && e.clientY === 0) {
|
||||||
|
// Keyboard triggered 'click' -> return max value
|
||||||
|
return cutoff
|
||||||
|
}
|
||||||
const x = e.clientX - e.target.getBoundingClientRect().x
|
const x = e.clientX - e.target.getBoundingClientRect().x
|
||||||
const w = container.getClientRects()[0]?.width
|
const w = container.getClientRects()[0]?.width
|
||||||
return x / w < 0.5 ? cutoff - 10 : cutoff
|
return x / w < 0.5 ? cutoff - 10 : cutoff
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
{#if readonly}
|
||||||
bind:this={container}
|
|
||||||
on:click={(e) => dispatch("click", { score: getScore(e) })}
|
|
||||||
on:mousemove={(e) => dispatch("hover", { score: getScore(e) })}
|
|
||||||
>
|
|
||||||
{#if score >= cutoff}
|
{#if score >= cutoff}
|
||||||
<Star class={starSize} />
|
<Star class={starSize} />
|
||||||
{:else if score + 10 >= cutoff}
|
{:else if score + 10 >= cutoff}
|
||||||
|
@ -33,4 +34,22 @@
|
||||||
{:else}
|
{:else}
|
||||||
<Star_outline class={starSize} />
|
<Star_outline class={starSize} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
use:ariaLabel={Translations.t.reviews.rate.Subs({n: i+1})}
|
||||||
|
class="small soft rounded-full no-image-background"
|
||||||
|
style="padding: 0; border: none;"
|
||||||
|
bind:this={container}
|
||||||
|
on:click={(e) => dispatch("click", { score: getScore(e) })}
|
||||||
|
on:mousemove={(e) => dispatch("hover", { score: getScore(e) })}
|
||||||
|
>
|
||||||
|
{#if score >= cutoff}
|
||||||
|
<Star class={starSize} />
|
||||||
|
{:else if score + 10 >= cutoff}
|
||||||
|
<Star_half class={starSize} />
|
||||||
|
{:else}
|
||||||
|
<Star_outline class={starSize} />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
import StarElement from "./StarElement.svelte"
|
import StarElement from "./StarElement.svelte"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -9,12 +8,13 @@
|
||||||
|
|
||||||
let cutoffs = [20, 40, 60, 80, 100]
|
let cutoffs = [20, 40, 60, 80, 100]
|
||||||
export let starSize = "w-h h-4"
|
export let starSize = "w-h h-4"
|
||||||
|
export let readonly = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if score !== undefined}
|
{#if score !== undefined}
|
||||||
<div class="flex" on:mouseout>
|
<div class="flex" on:mouseout>
|
||||||
{#each cutoffs as cutoff, i}
|
{#each cutoffs as cutoff, i}
|
||||||
<StarElement {score} {i} {cutoff} {starSize} on:hover on:click />
|
<StarElement {readonly} {score} {i} {cutoff} {starSize} on:hover on:click />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Store } from "../../Logic/UIEventSource"
|
import { Store } from "../../Logic/UIEventSource"
|
||||||
import StarsBar from "./StarsBar.svelte"
|
import StarsBar from "./StarsBar.svelte"
|
||||||
|
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||||
|
import Translations from "../i18n/Translations"
|
||||||
|
|
||||||
export let score: Store<number>
|
export let score: Store<number>
|
||||||
|
let scoreRounded = score.mapD(count => Math.round(count / 10) / 2)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $score !== undefined && $score !== null}
|
{#if $score !== undefined && $score !== null}
|
||||||
<StarsBar score={$score} />
|
<div tabindex="0"
|
||||||
|
use:ariaLabel={Translations.t.reviews.averageRating.Subs({n: $scoreRounded})}>
|
||||||
|
<StarsBar readonly={true} score={$score} />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
|
||||||
import { OsmTags } from "../Models/OsmFeature"
|
import { OsmTags } from "../Models/OsmFeature"
|
||||||
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
|
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
|
||||||
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
|
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
|
||||||
|
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The state needed to render a special Visualisation.
|
* The state needed to render a special Visualisation.
|
||||||
|
@ -87,6 +88,7 @@ export interface SpecialVisualizationState {
|
||||||
readonly imageUploadManager: ImageUploadManager
|
readonly imageUploadManager: ImageUploadManager
|
||||||
|
|
||||||
readonly previewedImage: UIEventSource<ProvidedImage>
|
readonly previewedImage: UIEventSource<ProvidedImage>
|
||||||
|
readonly geolocation: GeoLocationHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpecialVisualization {
|
export interface SpecialVisualization {
|
||||||
|
|
|
@ -85,6 +85,9 @@ import { Unit } from "../Models/Unit"
|
||||||
import Link from "./Base/Link.svelte"
|
import Link from "./Base/Link.svelte"
|
||||||
import OrientationDebugPanel from "./Debug/OrientationDebugPanel.svelte"
|
import OrientationDebugPanel from "./Debug/OrientationDebugPanel.svelte"
|
||||||
import MaprouletteSetStatus from "./MapRoulette/MaprouletteSetStatus.svelte"
|
import MaprouletteSetStatus from "./MapRoulette/MaprouletteSetStatus.svelte"
|
||||||
|
import DirectionIndicator from "./Base/DirectionIndicator.svelte"
|
||||||
|
import Img from "./Base/Img"
|
||||||
|
import Qr from "../Utils/Qr"
|
||||||
|
|
||||||
class NearbyImageVis implements SpecialVisualization {
|
class NearbyImageVis implements SpecialVisualization {
|
||||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||||
|
@ -1539,6 +1542,43 @@ export default class SpecialVisualizations {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
funcName: "direction_indicator",
|
||||||
|
args: [],
|
||||||
|
needsUrls: [],
|
||||||
|
docs: "Gives a distance indicator and a compass pointing towards the location from your GPS-location. If clicked, centers the map on the object",
|
||||||
|
constr(
|
||||||
|
state: SpecialVisualizationState,
|
||||||
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
|
argument: string[],
|
||||||
|
feature: Feature,
|
||||||
|
layer: LayerConfig
|
||||||
|
): BaseUIElement {
|
||||||
|
return new SvelteUIElement(DirectionIndicator, { state, feature })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
funcName: "qr_code",
|
||||||
|
args: [],
|
||||||
|
needsUrls: [],
|
||||||
|
docs: "Generates a QR-code to share the selected object",
|
||||||
|
constr(
|
||||||
|
state: SpecialVisualizationState,
|
||||||
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
|
argument: string[],
|
||||||
|
feature: Feature,
|
||||||
|
layer: LayerConfig
|
||||||
|
): BaseUIElement {
|
||||||
|
const url =
|
||||||
|
window.location.protocol +
|
||||||
|
"//" +
|
||||||
|
window.location.host +
|
||||||
|
window.location.pathname +
|
||||||
|
"#" +
|
||||||
|
feature.properties.id
|
||||||
|
return new Img(new Qr(url).toImageElement(75)).SetStyle("width: 75px")
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
specialVisualizations.push(new AutoApplyButton(specialVisualizations))
|
specialVisualizations.push(new AutoApplyButton(specialVisualizations))
|
||||||
|
|
|
@ -4,6 +4,9 @@ import BaseUIElement from "../BaseUIElement"
|
||||||
import CompiledTranslations from "../../assets/generated/CompiledTranslations"
|
import CompiledTranslations from "../../assets/generated/CompiledTranslations"
|
||||||
import LanguageUtils from "../../Utils/LanguageUtils"
|
import LanguageUtils from "../../Utils/LanguageUtils"
|
||||||
import { ClickableToggle } from "../Input/Toggle"
|
import { ClickableToggle } from "../Input/Toggle"
|
||||||
|
import { Store } from "../../Logic/UIEventSource"
|
||||||
|
import Locale from "./Locale"
|
||||||
|
import { Utils } from "../../Utils"
|
||||||
|
|
||||||
export default class Translations {
|
export default class Translations {
|
||||||
static readonly t: Readonly<typeof CompiledTranslations.t> = CompiledTranslations.t
|
static readonly t: Readonly<typeof CompiledTranslations.t> = CompiledTranslations.t
|
||||||
|
@ -130,6 +133,29 @@ export default class Translations {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static DynamicSubstitute<T extends Record<string, string | Translation>>(
|
||||||
|
translation: TypedTranslation<T>,
|
||||||
|
t: Store<T>
|
||||||
|
): Store<string> {
|
||||||
|
return Locale.language.map(
|
||||||
|
(lang) => {
|
||||||
|
const tags: Record<string, string> = {}
|
||||||
|
for (const k in t.data) {
|
||||||
|
let v = t.data[k]
|
||||||
|
if (!v) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (v["textFor"] !== undefined) {
|
||||||
|
v = v["textFor"](lang)
|
||||||
|
}
|
||||||
|
tags[k] = <string>v
|
||||||
|
}
|
||||||
|
return Utils.SubstituteKeys(translation.textFor(lang), t.data)
|
||||||
|
},
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
static isProbablyATranslation(transl: any) {
|
static isProbablyATranslation(transl: any) {
|
||||||
if (!transl || typeof transl !== "object") {
|
if (!transl || typeof transl !== "object") {
|
||||||
return false
|
return false
|
||||||
|
|
|
@ -4,23 +4,20 @@ import Qrcode from "qrcode-generator"
|
||||||
* Creates a QR-code as Blob
|
* Creates a QR-code as Blob
|
||||||
*/
|
*/
|
||||||
export default class Qr {
|
export default class Qr {
|
||||||
private _textToShow: string
|
private readonly _textToShow: string
|
||||||
|
|
||||||
constructor(textToShow: string) {
|
constructor(textToShow: string) {
|
||||||
this._textToShow = textToShow
|
this._textToShow = textToShow
|
||||||
}
|
}
|
||||||
|
|
||||||
public toImageElement(totalSize: number): string {
|
public toImageElement(totalSize: number): string {
|
||||||
console.log("Creating a QR code for", this._textToShow)
|
|
||||||
const typeNumber = 0
|
const typeNumber = 0
|
||||||
const errorCorrectionLevel = "L"
|
const errorCorrectionLevel = "L"
|
||||||
const qr = Qrcode(typeNumber, errorCorrectionLevel)
|
const qr = Qrcode(typeNumber, errorCorrectionLevel)
|
||||||
qr.addData(this._textToShow)
|
qr.addData(this._textToShow)
|
||||||
qr.make()
|
qr.make()
|
||||||
const moduleCount = qr.getModuleCount()
|
const moduleCount = qr.getModuleCount()
|
||||||
const img = document.createElement("img")
|
|
||||||
const cellSize = Math.round(totalSize / moduleCount)
|
const cellSize = Math.round(totalSize / moduleCount)
|
||||||
console.log("Cellsize", cellSize)
|
|
||||||
return qr.createDataURL(cellSize)
|
return qr.createDataURL(cellSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue