forked from MapComplete/MapComplete
		
	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
 | 
				
			||||||
    feedback?.setData(validator?.getFeedback(_value.data, getCountry))
 | 
					    if(_value.data?.length > 0){
 | 
				
			||||||
 | 
					      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)})}>
 | 
				
			||||||
 | 
					      <StarsBar readonly={true} score={review.rating} />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
    <div class="flex flex-wrap space-x-2">
 | 
					    <div class="flex flex-wrap space-x-2">
 | 
				
			||||||
      <div class="font-bold">
 | 
					      <a href={`https://mangrove.reviews/list?kid=${encodeURIComponent(review.kid)}`} rel="noopener"
 | 
				
			||||||
        {name}
 | 
					         target="_blank">
 | 
				
			||||||
      </div>
 | 
					        {#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}
 | 
				
			||||||
    {review.opinion}
 | 
					    <div class="link-no-underline">
 | 
				
			||||||
 | 
					    <a target="_blank" rel="noopener nofollow"
 | 
				
			||||||
 | 
					       href={`https://mangrove.reviews/list?signature=${encodeURIComponent(review.signature)}`}>
 | 
				
			||||||
 | 
					      {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…
	
	Add table
		Add a link
		
	
		Reference in a new issue