Merge branch 'develop' into RobinLinde-patch-1
This commit is contained in:
commit
7215702896
23 changed files with 672 additions and 277 deletions
|
@ -64,7 +64,8 @@
|
||||||
"iconSize": "40,40",
|
"iconSize": "40,40",
|
||||||
"location": [
|
"location": [
|
||||||
"point",
|
"point",
|
||||||
"centroid"
|
"projected_centerpoint",
|
||||||
|
"polygon_centroid"
|
||||||
],
|
],
|
||||||
"anchor": "bottom",
|
"anchor": "bottom",
|
||||||
"marker": [
|
"marker": [
|
||||||
|
|
|
@ -204,6 +204,35 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "dogicon",
|
||||||
|
"labels": [
|
||||||
|
"defaults",
|
||||||
|
"in_favourite"
|
||||||
|
],
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": "dog=no",
|
||||||
|
"#": "ignore-image-in-then",
|
||||||
|
"then": "<img textmode='🐕 ⃠' alt='no_dogs' src='./assets/layers/questions/no_dogs.svg'>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": "dog=leashed",
|
||||||
|
"#": "ignore-image-in-then",
|
||||||
|
"then": "<img textmode='🐕' alt='dogs are allowed but leashed' src='./assets/layers/questions/dogs_leashed.svg'>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"or": [
|
||||||
|
"dog=yes",
|
||||||
|
"dog=unleashed"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"#": "ignore-image-in-then",
|
||||||
|
"then": "<img textmode='🐕' alt='dogs are allowed' src='./assets/layers/questions/dogs_allowed.svg'>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "sharelink",
|
"id": "sharelink",
|
||||||
"labels": [
|
"labels": [
|
||||||
|
@ -263,35 +292,6 @@
|
||||||
],
|
],
|
||||||
"condition": "id~(node|way|relation)/[0-9]*"
|
"condition": "id~(node|way|relation)/[0-9]*"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "dogicon",
|
|
||||||
"labels": [
|
|
||||||
"defaults",
|
|
||||||
"in_favourite"
|
|
||||||
],
|
|
||||||
"mappings": [
|
|
||||||
{
|
|
||||||
"if": "dog=no",
|
|
||||||
"#": "ignore-image-in-then",
|
|
||||||
"then": "<img textmode='🐕 ⃠' alt='no_dogs' src='./assets/layers/questions/no_dogs.svg'>"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"if": "dog=leashed",
|
|
||||||
"#": "ignore-image-in-then",
|
|
||||||
"then": "<img textmode='🐕' alt='dogs are allowed but leashed' src='./assets/layers/questions/dogs_leashed.svg'>"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"if": {
|
|
||||||
"or": [
|
|
||||||
"dog=yes",
|
|
||||||
"dog=unleashed"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"#": "ignore-image-in-then",
|
|
||||||
"then": "<img textmode='🐕' alt='dogs are allowed' src='./assets/layers/questions/dogs_allowed.svg'>"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "rating",
|
"id": "rating",
|
||||||
"icon": {
|
"icon": {
|
||||||
|
|
|
@ -548,6 +548,115 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"condition": "toilets:position!=urinal",
|
||||||
|
"id": "gender_segregated",
|
||||||
|
"question": {
|
||||||
|
"en": "Are these toilets gender-segregated?",
|
||||||
|
"nl": "Zijn deze toiletten gescheiden op basis van geslacht?"
|
||||||
|
},
|
||||||
|
"questionHint": {
|
||||||
|
"en": "Are there separate stalls or separate areas for men and women and are they signposted as such?",
|
||||||
|
"nl": "Is er een aparte ruimte voor mannen en vrouwen en zijn deze ruimtes ook expliciet aangegeven?"
|
||||||
|
},
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": "gender_segregated=yes",
|
||||||
|
"then": {
|
||||||
|
"en": "There is a separate, signposted area for men and women",
|
||||||
|
"nl": "Er zijn aparte ruimtes of toiletten voor mannen en vrouwen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": "gender_segregated=no",
|
||||||
|
"then": {
|
||||||
|
"en": "There is no separate, signposted area for men and women",
|
||||||
|
"nl": "Mannen en vrouwen gebruiken dezelfde ruimtes en toiletten"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "menstrual_products",
|
||||||
|
"question": {
|
||||||
|
"en": "Are free, menstrual products distributed here?",
|
||||||
|
"nl": "Zijn er gratis menstruatieproducten beschikbaar?"
|
||||||
|
},
|
||||||
|
"questionHint": {
|
||||||
|
"en": "This is only about menstrual products that are free of charge. If e.g. a vending machine is available which charges for menstrual products, ignore it for this question.",
|
||||||
|
"nl": "Dit gaat enkel over menstruatieproducten die gratis geschikbaar zijn. Indien er bv. een verkoopautomaat met menstruatieproducten is, negeer deze dan"
|
||||||
|
},
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": "toilets:menstrual_products=yes",
|
||||||
|
"then": {
|
||||||
|
"en": "Free menstrual products are available to all visitors of these toilets",
|
||||||
|
"nl": "Er zijn gratis menstruatieprocten beschikbaar voor alle bezoekers van deze toiletten"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": "toilets:menstrual_products=limited",
|
||||||
|
"then": {
|
||||||
|
"en": "Free menstrual products are available to some visitors of these toilets",
|
||||||
|
"nl": "De gratis menstruatieproducten zijn enkel beschikbaar in een deel van de toiletten"
|
||||||
|
},
|
||||||
|
"hideInAnswer": "gender_segregated=yes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": "toilets:menstrual_products=no",
|
||||||
|
"alsoShowIf": "toilets:menstrual_products=",
|
||||||
|
"then": {
|
||||||
|
"en": "No free menstrual products are available here",
|
||||||
|
"nl": "Er zijn geen gratis menstruatieproducten beschikbaar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "menstrual_products_location",
|
||||||
|
"question": {
|
||||||
|
"en": "Where are the free menstrual products located?",
|
||||||
|
"nl": "Waar bevinden de gratis menstruatieproducten zich?"
|
||||||
|
},
|
||||||
|
"condition": {
|
||||||
|
"or": [
|
||||||
|
"toilets:menstrual_products=limited",
|
||||||
|
"toilets:menstrual_products:location~*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"render": {
|
||||||
|
"en": "The menstrual products are located in {toilets:menstrual_products:location}",
|
||||||
|
"nl": "De menstruatieproducten bevinden zich in {toilets:menstrual_products:location}"
|
||||||
|
},
|
||||||
|
"freeform": {
|
||||||
|
"key": "toilets:menstrual_products:location",
|
||||||
|
"inline": true
|
||||||
|
},
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"then": {
|
||||||
|
"en": "The free, menstrual products are located in the toilet for women",
|
||||||
|
"nl": "De gratis menstruatieproducten bevinden zich in het vrouwentoilet"
|
||||||
|
},
|
||||||
|
"if": "toilets:menstrual_products:location=female_toilet",
|
||||||
|
"alsoShowIf": "toilets:menstrual_products:location="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"then": {
|
||||||
|
"en": "The free, menstrual products are located in the toilet for men",
|
||||||
|
"nl": "De gratis menstruatieproducten bevinden zich in het mannentoilet"
|
||||||
|
},
|
||||||
|
"if": "toilets:menstrual_products:location=male_toilet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": "toilets:menstrual_products:location=wheelchair_toilet",
|
||||||
|
"then": {
|
||||||
|
"en": "The free, menstrual products are located in the toilet for wheelchair users",
|
||||||
|
"nl": "De gratis menstruatieproducten bevinden zich in het rolstoeltoegankelijke toilet"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "toilets-changing-table",
|
"id": "toilets-changing-table",
|
||||||
"labels": [
|
"labels": [
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
"en": "<p><a href='https://velopark.be' target='_blank'>Velopark.be</a> is a website collecting data about bicycle parkings in a semi-crowdsourced way. However, only 'authorized' instances are allowed to make changes there, in practice the operator of the bicycle parking such as SNCB, de Lijn or the municipality. They have now decided to synchronize their dataset with OpenStreetMap, and this MapComplete-instance is set up to help link and import their data into OpenStreetMap.</p> How to use: <ul><li>A velopark-icon on the map (yellow with bicycle silhouette) represents a bicycle known by Velopark but not yet known by OpenStreetMap</li><li>Blue pins are bicycle parkings known by OpenStreetMap</li><li>Light blue pins are bicycle parkings known by OpenStreetMap with a reference to Velopark.be (<span class='literal-code'>ref-velopark=*</span>)</li><li>Click a velopark item, you can either link it with a nearby OSM-bicycle parking or create a new bicycle parking. Note that the geometry of Velopark is often incorrect and can be a few up till 100 meters away from the actual bicycle parking. Use aerial imagery, linked images and streetview to determine the correct location</li><li>Once linked, you can compare the Velopark- and OSM-attributes and apply correct attributes</li><li>If Velopark has an image, you can also link the image</li></ul> That's it! Thanks for helping to import this!",
|
"en": "<p><a href='https://velopark.be' target='_blank'>Velopark.be</a> is a website collecting data about bicycle parkings in a semi-crowdsourced way. However, only 'authorized' instances are allowed to make changes there, in practice the operator of the bicycle parking such as SNCB, de Lijn or the municipality. They have now decided to synchronize their dataset with OpenStreetMap, and this MapComplete-instance is set up to help link and import their data into OpenStreetMap.</p> How to use: <ul><li>A velopark-icon on the map (yellow with bicycle silhouette) represents a bicycle known by Velopark but not yet known by OpenStreetMap</li><li>Blue pins are bicycle parkings known by OpenStreetMap</li><li>Light blue pins are bicycle parkings known by OpenStreetMap with a reference to Velopark.be (<span class='literal-code'>ref-velopark=*</span>)</li><li>Click a velopark item, you can either link it with a nearby OSM-bicycle parking or create a new bicycle parking. Note that the geometry of Velopark is often incorrect and can be a few up till 100 meters away from the actual bicycle parking. Use aerial imagery, linked images and streetview to determine the correct location</li><li>Once linked, you can compare the Velopark- and OSM-attributes and apply correct attributes</li><li>If Velopark has an image, you can also link the image</li></ul> That's it! Thanks for helping to import this!",
|
||||||
"nl": "<p><a href='https://velopark.be' target='_blank'>Velopark.be</a> is een website die data verzamelt over fietsenstallingen in een semi-crowdsource manier. Hierbij kunnen enkel geautorizeerde gebruikers data bijdragen, in de praktijk de uitbaters van de fietsenstallingen zoals de bevoegde gemeentebesturen, de NMBS of de Lijn. Velopark.be heeft nu beslist om hun data met OpenStreetMap te synchronizeren. Deze website is de tool om van Velopark.be naar OpenStreetMap te gaan en hun data te importeren.</p> Hoe te gebruiken? <ul><li>Een velopark-logo op de kaart (geel met een fietssilhouette) duidt een fietsenstalling aan die gekend is in Velopark maar nog niet gekend (of gelinkt) is aan een fietsenstalling in OpenStreetMap</li><li>Een blauwe pin duidt een fietsenstalling aan die gekend is in OpenStreetMap</li><li>Een licht-blauwe pin duidt een fietsenstalling aan uit OpenStreetMap die een link heeft naar Velopark.be (<span class='literal-code'>ref-velopark=*</span>)</li><li>Als je op een velopark-item klikt op, kan je deze linken met een fietsenstalling in de buurt (<25m) of een nieuwe fietstalling aan OpenStreetMap toevoegen. Let op: de geometrie van Velopark is zelden correct en wijkt makkelijk 10 meter of meer af van de echte locatie - in uitzonderlijke gevallen zelfs tot meer dan 100 meter. Gebruik de meest recente luchtfoto's, de gelinkte foto's en mapillary om de correcte locatie te bepalen</li><li>Eens gelinkt, kan je de Velopark- en OSM-attributen vergelijken en de correcte attributen toepassen in OpenStreetMap</li><li>Indien velopark een foto heeft, kan je die ook nog linken</li></ul> Dat is het! Bedankt om mee te helpen!"
|
"nl": "<p><a href='https://velopark.be' target='_blank'>Velopark.be</a> is een website die data verzamelt over fietsenstallingen in een semi-crowdsource manier. Hierbij kunnen enkel geautorizeerde gebruikers data bijdragen, in de praktijk de uitbaters van de fietsenstallingen zoals de bevoegde gemeentebesturen, de NMBS of de Lijn. Velopark.be heeft nu beslist om hun data met OpenStreetMap te synchronizeren. Deze website is de tool om van Velopark.be naar OpenStreetMap te gaan en hun data te importeren.</p> Hoe te gebruiken? <ul><li>Een velopark-logo op de kaart (geel met een fietssilhouette) duidt een fietsenstalling aan die gekend is in Velopark maar nog niet gekend (of gelinkt) is aan een fietsenstalling in OpenStreetMap</li><li>Een blauwe pin duidt een fietsenstalling aan die gekend is in OpenStreetMap</li><li>Een licht-blauwe pin duidt een fietsenstalling aan uit OpenStreetMap die een link heeft naar Velopark.be (<span class='literal-code'>ref-velopark=*</span>)</li><li>Als je op een velopark-item klikt op, kan je deze linken met een fietsenstalling in de buurt (<25m) of een nieuwe fietstalling aan OpenStreetMap toevoegen. Let op: de geometrie van Velopark is zelden correct en wijkt makkelijk 10 meter of meer af van de echte locatie - in uitzonderlijke gevallen zelfs tot meer dan 100 meter. Gebruik de meest recente luchtfoto's, de gelinkte foto's en mapillary om de correcte locatie te bepalen</li><li>Eens gelinkt, kan je de Velopark- en OSM-attributen vergelijken en de correcte attributen toepassen in OpenStreetMap</li><li>Indien velopark een foto heeft, kan je die ook nog linken</li></ul> Dat is het! Bedankt om mee te helpen!"
|
||||||
},
|
},
|
||||||
|
"descriptionTail": {
|
||||||
|
"*": "<h3>Maintainer tools</h3><ul><li><a class='link-underline' href='https://maproulette.org/api/v2/challenge/view/43282' download='Velopark_sync_2024-01-15.geojson'>Download the first sync results</a></li></ul>"
|
||||||
|
},
|
||||||
"hideFromOverview": true,
|
"hideFromOverview": true,
|
||||||
"icon": "./assets/themes/velopark/velopark.svg",
|
"icon": "./assets/themes/velopark/velopark.svg",
|
||||||
"mustHaveLanguage": [
|
"mustHaveLanguage": [
|
||||||
|
@ -30,6 +33,7 @@
|
||||||
"startLon": 3.71025,
|
"startLon": 3.71025,
|
||||||
"startZoom": 18,
|
"startZoom": 18,
|
||||||
"defaultBackgroundId": "photo",
|
"defaultBackgroundId": "photo",
|
||||||
|
"enableNoteImports": false,
|
||||||
"layers": [
|
"layers": [
|
||||||
{
|
{
|
||||||
"id": "velopark_maproulette",
|
"id": "velopark_maproulette",
|
||||||
|
@ -94,6 +98,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "login",
|
||||||
|
"render": {
|
||||||
|
"special": {
|
||||||
|
"type": "login_button"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "closest_parkings",
|
"id": "closest_parkings",
|
||||||
"render": {
|
"render": {
|
||||||
|
@ -137,7 +149,7 @@
|
||||||
"type": "maproulette_set_status",
|
"type": "maproulette_set_status",
|
||||||
"message": {
|
"message": {
|
||||||
"en": "Mark this item as linked manually. Use this if you did apply the reference via copy-paste or via another editor",
|
"en": "Mark this item as linked manually. Use this if you did apply the reference via copy-paste or via another editor",
|
||||||
"nl": "Markeer als gelinkt. Gebruik deze optie indien je de ID plakte in een fietsenstalling of via een andere editor toevoegdemap"
|
"nl": "Markeer als gelinkt. Gebruik deze optie indien je de ID plakte in een fietsenstalling of via een andere editor toevoegd"
|
||||||
},
|
},
|
||||||
"status": 1
|
"status": 1
|
||||||
}
|
}
|
||||||
|
@ -150,10 +162,15 @@
|
||||||
"type": "maproulette_set_status",
|
"type": "maproulette_set_status",
|
||||||
"message": {
|
"message": {
|
||||||
"en": "Mark this item as incorrect or too hard to solve (duplicate, does not exist anymore, contradictory data, not placeable from aerial imagery)",
|
"en": "Mark this item as incorrect or too hard to solve (duplicate, does not exist anymore, contradictory data, not placeable from aerial imagery)",
|
||||||
"nl": "Markeer dit object als incorrect of te moeillijk (duplicaat, incorrect of tegenstrijdige data, niet eenduidig te plaatsen adhv luchtfoto's, ...)"
|
"nl": "Markeer dit object als incorrecte velopark data of te moeillijk (duplicaat, incorrect of tegenstrijdige data, niet eenduidig te plaatsen adhv luchtfoto's, ...)"
|
||||||
},
|
},
|
||||||
"image": "invalid",
|
"image": "invalid",
|
||||||
"status": 6
|
"status": 6,
|
||||||
|
"ask_feedback": {
|
||||||
|
"en": "Is this point incorrect or is it difficult to solve? Please provide some feedback below",
|
||||||
|
"nl": "Is dit punt foutief of te moeilijk? Gelieve wat feedback te geven"
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -248,7 +265,7 @@
|
||||||
{
|
{
|
||||||
"marker": [
|
"marker": [
|
||||||
{
|
{
|
||||||
"color": "#0088ff"
|
"color": "#2cf200"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -671,6 +671,7 @@
|
||||||
"reviewPlaceholder": "Describe your experience…",
|
"reviewPlaceholder": "Describe your experience…",
|
||||||
"reviewing_as": "Reviewing as {nickname}",
|
"reviewing_as": "Reviewing as {nickname}",
|
||||||
"reviewing_as_anonymous": "Reviewing as anonymous",
|
"reviewing_as_anonymous": "Reviewing as anonymous",
|
||||||
|
"reviews_bug": "Expected more reviews? Some reviews are not displayed due to a bug.",
|
||||||
"save": "Save review",
|
"save": "Save review",
|
||||||
"saved": "Review saved. Thanks for sharing!",
|
"saved": "Review saved. Thanks for sharing!",
|
||||||
"saving_review": "Saving…",
|
"saving_review": "Saving…",
|
||||||
|
@ -678,7 +679,9 @@
|
||||||
"title_singular": "One review",
|
"title_singular": "One review",
|
||||||
"too_long": "At most {max} characters are allowed. Your review has {amount} characters.",
|
"too_long": "At most {max} characters are allowed. Your review has {amount} characters.",
|
||||||
"tos": "If you create a review, you agree to <a href='https://mangrove.reviews/terms' target='_blank'>the TOS and privacy policy of Mangrove.reviews</a>",
|
"tos": "If you create a review, you agree to <a href='https://mangrove.reviews/terms' target='_blank'>the TOS and privacy policy of Mangrove.reviews</a>",
|
||||||
"write_a_comment": "Leave a review…"
|
"write_a_comment": "Leave a review…",
|
||||||
|
"your_reviews": "Your previous reviews",
|
||||||
|
"your_reviews_empty": "We couldn't find any of your previous reviews"
|
||||||
},
|
},
|
||||||
"split": {
|
"split": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
|
|
72
scripts/velopark/compare.ts
Normal file
72
scripts/velopark/compare.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import Script from "../Script"
|
||||||
|
import fs from "fs"
|
||||||
|
import { Feature, FeatureCollection } from "geojson"
|
||||||
|
import { GeoOperations } from "../../src/Logic/GeoOperations"
|
||||||
|
import * as os from "os"
|
||||||
|
// vite-node scripts/velopark/compare.ts -- scripts/velopark/velopark_all_2024-02-14T12\:18\:41.772Z.geojson ~/Projecten/OSM/Fietsberaad/2024-02-02\ Fietsenstallingen_OSM_met_velopark_ref.geojson
|
||||||
|
class Compare extends Script {
|
||||||
|
|
||||||
|
compare(veloId: string, osmParking: Feature, veloParking: Feature): {distance: number, ref: string, osmid: string, diffs: {
|
||||||
|
osm: string, velopark: string, key: string
|
||||||
|
}[] }{
|
||||||
|
const osmCenterpoint = GeoOperations.centerpointCoordinates(osmParking)
|
||||||
|
const veloparkCenterpoint = GeoOperations.centerpointCoordinates(veloParking)
|
||||||
|
const distance = Math.round(GeoOperations.distanceBetween(osmCenterpoint, veloparkCenterpoint))
|
||||||
|
const diffs: { osm: string, velopark: string, key: string}[] = []
|
||||||
|
|
||||||
|
const allKeys = new Set<string>(Object.keys(osmParking.properties).concat(Object.keys(veloParking.properties)))
|
||||||
|
for (const key of allKeys) {
|
||||||
|
if(osmParking.properties[key] === veloParking.properties[key]){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if(Number(osmParking.properties[key]) === veloParking.properties[key]){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if(veloParking.properties[key] === undefined){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
diffs.push({
|
||||||
|
key,
|
||||||
|
osm: osmParking.properties[key],
|
||||||
|
velopark: veloParking.properties[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ref: veloId,
|
||||||
|
osmid: osmParking.properties["@id"],
|
||||||
|
distance, diffs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async main(args: string[]): Promise<void> {
|
||||||
|
let [velopark, osm, key] = args
|
||||||
|
key ??= "ref:velopark"
|
||||||
|
const veloparkData: FeatureCollection = JSON.parse(fs.readFileSync(velopark, "utf-8"))
|
||||||
|
const osmData : FeatureCollection = JSON.parse(fs.readFileSync(osm, "utf-8"))
|
||||||
|
|
||||||
|
const veloparkById : Record<string, Feature> = {}
|
||||||
|
for (const parking of veloparkData.features) {
|
||||||
|
veloparkById[parking.properties[key]] = parking
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffs = []
|
||||||
|
for (const parking of osmData.features) {
|
||||||
|
const veloId = parking.properties[key]
|
||||||
|
const veloparking = veloparkById[veloId]
|
||||||
|
if(veloparking === undefined){
|
||||||
|
console.error("No velopark entry found for", veloId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
diffs.push(this.compare(veloId, parking, veloparking))
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync("report_diff.json",JSON.stringify(diffs))
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
constructor() {
|
||||||
|
super("Compares a velopark geojson with OSM geojson. Usage: `compare velopark.geojson osm.geojson [key-to-compare-on]`. If key-to-compare-on is not given, `ref:velopark` will be used")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
new Compare().run()
|
|
@ -15,6 +15,20 @@ class VeloParkToGeojson extends Script {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportTo(filename: string, features){
|
||||||
|
fs.writeFileSync(
|
||||||
|
filename+"_" + new Date().toISOString() + ".geojson",
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
" "
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async main(args: string[]): Promise<void> {
|
async main(args: string[]): Promise<void> {
|
||||||
console.log("Downloading velopark data")
|
console.log("Downloading velopark data")
|
||||||
// Download data for NIS-code 1000. 1000 means: all of belgium
|
// Download data for NIS-code 1000. 1000 means: all of belgium
|
||||||
|
@ -38,12 +52,15 @@ class VeloParkToGeojson extends Script {
|
||||||
)
|
)
|
||||||
console.log("OpenStreetMap contains", seenIds.size, "bicycle parkings with a velopark ref")
|
console.log("OpenStreetMap contains", seenIds.size, "bicycle parkings with a velopark ref")
|
||||||
const allVelopark = data.map((f) => VeloparkLoader.convert(f))
|
const allVelopark = data.map((f) => VeloparkLoader.convert(f))
|
||||||
|
this.exportTo("velopark_all", allVelopark)
|
||||||
|
|
||||||
const features = allVelopark.filter((f) => !seenIds.has(f.properties["ref:velopark"]))
|
const features = allVelopark.filter((f) => !seenIds.has(f.properties["ref:velopark"]))
|
||||||
|
|
||||||
const allProperties = new Set<string>()
|
const allProperties = new Set<string>()
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
Object.keys(feature.properties).forEach((k) => allProperties.add(k))
|
Object.keys(feature.properties).forEach((k) => allProperties.add(k))
|
||||||
}
|
}
|
||||||
|
this.exportTo("velopark_noncynced",features)
|
||||||
allProperties.delete("ref:velopark")
|
allProperties.delete("ref:velopark")
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
allProperties.forEach((k) => {
|
allProperties.forEach((k) => {
|
||||||
|
@ -51,17 +68,7 @@ class VeloParkToGeojson extends Script {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(
|
this.exportTo("velopark_nonsynced_id_only", features)
|
||||||
"velopark_id_only_export_" + new Date().toISOString() + ".geojson",
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
type: "FeatureCollection",
|
|
||||||
features,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
" "
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -156,7 +156,7 @@ export class GeoOperations {
|
||||||
const intersection = GeoOperations.calculateIntersection(
|
const intersection = GeoOperations.calculateIntersection(
|
||||||
feature,
|
feature,
|
||||||
otherFeature,
|
otherFeature,
|
||||||
featureBBox
|
featureBBox,
|
||||||
)
|
)
|
||||||
if (intersection === null) {
|
if (intersection === null) {
|
||||||
continue
|
continue
|
||||||
|
@ -195,7 +195,7 @@ export class GeoOperations {
|
||||||
console.error(
|
console.error(
|
||||||
"Could not correctly calculate the overlap of ",
|
"Could not correctly calculate the overlap of ",
|
||||||
feature,
|
feature,
|
||||||
": unsupported type"
|
": unsupported type",
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -224,7 +224,7 @@ export class GeoOperations {
|
||||||
*/
|
*/
|
||||||
public static inside(
|
public static inside(
|
||||||
pointCoordinate: [number, number] | Feature<Point>,
|
pointCoordinate: [number, number] | Feature<Point>,
|
||||||
feature: Feature
|
feature: Feature,
|
||||||
): boolean {
|
): boolean {
|
||||||
// ray-casting algorithm based on
|
// ray-casting algorithm based on
|
||||||
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
|
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
|
||||||
|
@ -302,7 +302,7 @@ export class GeoOperations {
|
||||||
*/
|
*/
|
||||||
public static nearestPoint(
|
public static nearestPoint(
|
||||||
way: Feature<LineString>,
|
way: Feature<LineString>,
|
||||||
point: [number, number]
|
point: [number, number],
|
||||||
): Feature<
|
): Feature<
|
||||||
Point,
|
Point,
|
||||||
{
|
{
|
||||||
|
@ -324,11 +324,11 @@ export class GeoOperations {
|
||||||
public static forceLineString(way: Feature<LineString | Polygon>): Feature<LineString>
|
public static forceLineString(way: Feature<LineString | Polygon>): Feature<LineString>
|
||||||
|
|
||||||
public static forceLineString(
|
public static forceLineString(
|
||||||
way: Feature<MultiLineString | MultiPolygon>
|
way: Feature<MultiLineString | MultiPolygon>,
|
||||||
): Feature<MultiLineString>
|
): Feature<MultiLineString>
|
||||||
|
|
||||||
public static forceLineString(
|
public static forceLineString(
|
||||||
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>
|
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>,
|
||||||
): Feature<LineString | MultiLineString> {
|
): Feature<LineString | MultiLineString> {
|
||||||
if (way.geometry.type === "Polygon") {
|
if (way.geometry.type === "Polygon") {
|
||||||
way = { ...way }
|
way = { ...way }
|
||||||
|
@ -352,8 +352,8 @@ export class GeoOperations {
|
||||||
const headerValuesOrdered: string[] = []
|
const headerValuesOrdered: string[] = []
|
||||||
|
|
||||||
function addH(key: string) {
|
function addH(key: string) {
|
||||||
if(options?.ignoreTags){
|
if (options?.ignoreTags) {
|
||||||
if(key.match(options.ignoreTags)){
|
if (key.match(options.ignoreTags)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -455,7 +455,7 @@ export class GeoOperations {
|
||||||
*/
|
*/
|
||||||
public static LineIntersections(
|
public static LineIntersections(
|
||||||
feature: Feature<LineString | MultiLineString | Polygon | MultiPolygon>,
|
feature: Feature<LineString | MultiLineString | Polygon | MultiPolygon>,
|
||||||
otherFeature: Feature<LineString | MultiLineString | Polygon | MultiPolygon>
|
otherFeature: Feature<LineString | MultiLineString | Polygon | MultiPolygon>,
|
||||||
): [number, number][] {
|
): [number, number][] {
|
||||||
return turf
|
return turf
|
||||||
.lineIntersect(feature, otherFeature)
|
.lineIntersect(feature, otherFeature)
|
||||||
|
@ -492,7 +492,7 @@ export class GeoOperations {
|
||||||
locations:
|
locations:
|
||||||
| Feature<LineString>
|
| Feature<LineString>
|
||||||
| Feature<Point, { date?: string; altitude?: number | string }>[],
|
| Feature<Point, { date?: string; altitude?: number | string }>[],
|
||||||
title?: string
|
title?: string,
|
||||||
) {
|
) {
|
||||||
title = title?.trim()
|
title = title?.trim()
|
||||||
if (title === undefined || title === "") {
|
if (title === undefined || title === "") {
|
||||||
|
@ -513,7 +513,7 @@ export class GeoOperations {
|
||||||
type: "Point",
|
type: "Point",
|
||||||
coordinates: p,
|
coordinates: p,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
for (const l of locationsWithMeta) {
|
for (const l of locationsWithMeta) {
|
||||||
|
@ -528,7 +528,7 @@ export class GeoOperations {
|
||||||
trackPoints.push(trkpt)
|
trackPoints.push(trkpt)
|
||||||
}
|
}
|
||||||
const header =
|
const header =
|
||||||
'<gpx version="1.1" creator="mapcomplete.org" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
|
"<gpx version=\"1.1\" creator=\"mapcomplete.org\" xmlns=\"http://www.topografix.com/GPX/1/1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">"
|
||||||
return (
|
return (
|
||||||
header +
|
header +
|
||||||
"\n<name>" +
|
"\n<name>" +
|
||||||
|
@ -546,7 +546,7 @@ export class GeoOperations {
|
||||||
*/
|
*/
|
||||||
public static toGpxPoints(
|
public static toGpxPoints(
|
||||||
locations: Feature<Point, { date?: string; altitude?: number | string }>[],
|
locations: Feature<Point, { date?: string; altitude?: number | string }>[],
|
||||||
title?: string
|
title?: string,
|
||||||
) {
|
) {
|
||||||
title = title?.trim()
|
title = title?.trim()
|
||||||
if (title === undefined || title === "") {
|
if (title === undefined || title === "") {
|
||||||
|
@ -567,7 +567,7 @@ export class GeoOperations {
|
||||||
trackPoints.push(trkpt)
|
trackPoints.push(trkpt)
|
||||||
}
|
}
|
||||||
const header =
|
const header =
|
||||||
'<gpx version="1.1" creator="mapcomplete.org" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
|
"<gpx version=\"1.1\" creator=\"mapcomplete.org\" xmlns=\"http://www.topografix.com/GPX/1/1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">"
|
||||||
return (
|
return (
|
||||||
header +
|
header +
|
||||||
"\n<name>" +
|
"\n<name>" +
|
||||||
|
@ -655,7 +655,7 @@ export class GeoOperations {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
distanceMeter,
|
distanceMeter,
|
||||||
{ units: "meters" }
|
{ units: "meters" },
|
||||||
).geometry.coordinates
|
).geometry.coordinates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -690,7 +690,7 @@ export class GeoOperations {
|
||||||
*/
|
*/
|
||||||
static completelyWithin(
|
static completelyWithin(
|
||||||
feature: Feature<Geometry, any>,
|
feature: Feature<Geometry, any>,
|
||||||
possiblyEnclosingFeature: Feature<Polygon | MultiPolygon, any>
|
possiblyEnclosingFeature: Feature<Polygon | MultiPolygon, any>,
|
||||||
): boolean {
|
): boolean {
|
||||||
return booleanWithin(feature, possiblyEnclosingFeature)
|
return booleanWithin(feature, possiblyEnclosingFeature)
|
||||||
}
|
}
|
||||||
|
@ -746,7 +746,7 @@ export class GeoOperations {
|
||||||
*/
|
*/
|
||||||
public static featureToCoordinateWithRenderingType(
|
public static featureToCoordinateWithRenderingType(
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
location: "point" | "centroid" | "start" | "end" | "projected_centerpoint" | string
|
location: "point" | "centroid" | "start" | "end" | "projected_centerpoint" | "polygon_centerpoint" | string,
|
||||||
): [number, number] | undefined {
|
): [number, number] | undefined {
|
||||||
switch (location) {
|
switch (location) {
|
||||||
case "point":
|
case "point":
|
||||||
|
@ -759,6 +759,11 @@ export class GeoOperations {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return GeoOperations.centerpointCoordinates(feature)
|
return GeoOperations.centerpointCoordinates(feature)
|
||||||
|
case "polygon_centerpoint":
|
||||||
|
if (feature.geometry.type === "Polygon") {
|
||||||
|
return GeoOperations.centerpointCoordinates(feature)
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
case "projected_centerpoint":
|
case "projected_centerpoint":
|
||||||
if (
|
if (
|
||||||
feature.geometry.type === "LineString" ||
|
feature.geometry.type === "LineString" ||
|
||||||
|
@ -767,7 +772,7 @@ export class GeoOperations {
|
||||||
const centerpoint = GeoOperations.centerpointCoordinates(feature)
|
const centerpoint = GeoOperations.centerpointCoordinates(feature)
|
||||||
const projected = GeoOperations.nearestPoint(
|
const projected = GeoOperations.nearestPoint(
|
||||||
<Feature<LineString>>feature,
|
<Feature<LineString>>feature,
|
||||||
centerpoint
|
centerpoint,
|
||||||
)
|
)
|
||||||
return <[number, number]>projected.geometry.coordinates
|
return <[number, number]>projected.geometry.coordinates
|
||||||
}
|
}
|
||||||
|
@ -944,7 +949,7 @@ export class GeoOperations {
|
||||||
* GeoOperations.bearingToHuman(46) // => "NE"
|
* GeoOperations.bearingToHuman(46) // => "NE"
|
||||||
*/
|
*/
|
||||||
public static bearingToHuman(
|
public static bearingToHuman(
|
||||||
bearing: number
|
bearing: number,
|
||||||
): "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW" {
|
): "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW" {
|
||||||
while (bearing < 0) {
|
while (bearing < 0) {
|
||||||
bearing += 360
|
bearing += 360
|
||||||
|
@ -970,7 +975,7 @@ export class GeoOperations {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public static bearingToHumanRelative(
|
public static bearingToHumanRelative(
|
||||||
bearing: number
|
bearing: number,
|
||||||
):
|
):
|
||||||
| "straight"
|
| "straight"
|
||||||
| "slight_right"
|
| "slight_right"
|
||||||
|
@ -995,12 +1000,12 @@ export class GeoOperations {
|
||||||
private static pointInPolygonCoordinates(
|
private static pointInPolygonCoordinates(
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
coordinates: [number, number][][]
|
coordinates: [number, number][][],
|
||||||
): boolean {
|
): boolean {
|
||||||
const inside = GeoOperations.pointWithinRing(
|
const inside = GeoOperations.pointWithinRing(
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
/*This is the outer ring of the polygon */ coordinates[0]
|
/*This is the outer ring of the polygon */ coordinates[0],
|
||||||
)
|
)
|
||||||
if (!inside) {
|
if (!inside) {
|
||||||
return false
|
return false
|
||||||
|
@ -1009,7 +1014,7 @@ export class GeoOperations {
|
||||||
const inHole = GeoOperations.pointWithinRing(
|
const inHole = GeoOperations.pointWithinRing(
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
coordinates[i] /* These are inner rings, aka holes*/
|
coordinates[i], /* These are inner rings, aka holes*/
|
||||||
)
|
)
|
||||||
if (inHole) {
|
if (inHole) {
|
||||||
return false
|
return false
|
||||||
|
@ -1047,7 +1052,7 @@ export class GeoOperations {
|
||||||
feature,
|
feature,
|
||||||
otherFeature,
|
otherFeature,
|
||||||
featureBBox: BBox,
|
featureBBox: BBox,
|
||||||
otherFeatureBBox?: BBox
|
otherFeatureBBox?: BBox,
|
||||||
): number {
|
): number {
|
||||||
if (feature.geometry.type === "LineString") {
|
if (feature.geometry.type === "LineString") {
|
||||||
otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature)
|
otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature)
|
||||||
|
@ -1096,7 +1101,7 @@ export class GeoOperations {
|
||||||
let intersection = turf.lineSlice(
|
let intersection = turf.lineSlice(
|
||||||
turf.point(intersectionPointsArray[0]),
|
turf.point(intersectionPointsArray[0]),
|
||||||
turf.point(intersectionPointsArray[1]),
|
turf.point(intersectionPointsArray[1]),
|
||||||
feature
|
feature,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (intersection == null) {
|
if (intersection == null) {
|
||||||
|
@ -1117,7 +1122,7 @@ export class GeoOperations {
|
||||||
otherFeature,
|
otherFeature,
|
||||||
feature,
|
feature,
|
||||||
otherFeatureBBox,
|
otherFeatureBBox,
|
||||||
featureBBox
|
featureBBox,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1137,7 +1142,7 @@ export class GeoOperations {
|
||||||
console.log("Applying fallback intersection...")
|
console.log("Applying fallback intersection...")
|
||||||
const intersection = turf.intersect(
|
const intersection = turf.intersect(
|
||||||
turf.truncate(feature),
|
turf.truncate(feature),
|
||||||
turf.truncate(otherFeature)
|
turf.truncate(otherFeature),
|
||||||
)
|
)
|
||||||
if (intersection == null) {
|
if (intersection == null) {
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -73,7 +73,6 @@ export default class UserRelatedState {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
osmConnection: OsmConnection,
|
osmConnection: OsmConnection,
|
||||||
availableLanguages?: string[],
|
|
||||||
layout?: LayoutConfig,
|
layout?: LayoutConfig,
|
||||||
featureSwitches?: FeatureSwitchState,
|
featureSwitches?: FeatureSwitchState,
|
||||||
mapProperties?: MapProperties
|
mapProperties?: MapProperties
|
||||||
|
@ -365,6 +364,11 @@ export default class UserRelatedState {
|
||||||
[translationMode]
|
[translationMode]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.mangroveIdentity.getKeyId().addCallbackAndRun(kid => {
|
||||||
|
amendedPrefs.data["mangrove_kid"] = kid
|
||||||
|
amendedPrefs.ping()
|
||||||
|
})
|
||||||
|
|
||||||
const usersettingMetaTagging = new ThemeMetaTagging()
|
const usersettingMetaTagging = new ThemeMetaTagging()
|
||||||
osmConnection.userDetails.addCallback((userDetails) => {
|
osmConnection.userDetails.addCallback((userDetails) => {
|
||||||
for (const k in userDetails) {
|
for (const k in userDetails) {
|
||||||
|
|
|
@ -306,7 +306,8 @@ export abstract class Store<T> implements Readable<T> {
|
||||||
|
|
||||||
export class ImmutableStore<T> extends Store<T> {
|
export class ImmutableStore<T> extends Store<T> {
|
||||||
public readonly data: T
|
public readonly data: T
|
||||||
|
static FALSE = new ImmutableStore<boolean>(false)
|
||||||
|
static TRUE = new ImmutableStore<boolean>(true)
|
||||||
constructor(data: T) {
|
constructor(data: T) {
|
||||||
super()
|
super()
|
||||||
this.data = data
|
this.data = data
|
||||||
|
|
|
@ -5,10 +5,12 @@ import { Feature, Position } from "geojson"
|
||||||
import { GeoOperations } from "../GeoOperations"
|
import { GeoOperations } from "../GeoOperations"
|
||||||
|
|
||||||
export class MangroveIdentity {
|
export class MangroveIdentity {
|
||||||
public readonly keypair: Store<CryptoKeyPair>
|
private readonly keypair: Store<CryptoKeyPair>
|
||||||
public readonly key_id: Store<string>
|
private readonly mangroveIdentity: UIEventSource<string>
|
||||||
|
private readonly key_id: Store<string>
|
||||||
|
|
||||||
constructor(mangroveIdentity: UIEventSource<string>) {
|
constructor(mangroveIdentity: UIEventSource<string>) {
|
||||||
|
this.mangroveIdentity = mangroveIdentity
|
||||||
const key_id = new UIEventSource<string>(undefined)
|
const key_id = new UIEventSource<string>(undefined)
|
||||||
this.key_id = key_id
|
this.key_id = key_id
|
||||||
const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined)
|
const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined)
|
||||||
|
@ -23,13 +25,7 @@ export class MangroveIdentity {
|
||||||
key_id.setData(pem)
|
key_id.setData(pem)
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
|
||||||
if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") {
|
|
||||||
MangroveIdentity.CreateIdentity(mangroveIdentity).then((_) => {})
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Could not create identity: ", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,8 +40,61 @@ export class MangroveIdentity {
|
||||||
// Identity has been loaded via osmPreferences by now - we don't overwrite
|
// Identity has been loaded via osmPreferences by now - we don't overwrite
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
console.log("Creating a new Mangrove identity!")
|
||||||
identity.setData(JSON.stringify(jwk))
|
identity.setData(JSON.stringify(jwk))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only called to create a review.
|
||||||
|
*/
|
||||||
|
async getKeypair(): Promise<CryptoKeyPair> {
|
||||||
|
if(this.keypair.data ?? "" === ""){
|
||||||
|
// We want to create a review, but it seems like no key has been setup at this moment
|
||||||
|
// We create the key
|
||||||
|
try {
|
||||||
|
if (!Utils.runningFromConsole && (this.mangroveIdentity.data ?? "") === "") {
|
||||||
|
await MangroveIdentity.CreateIdentity(this.mangroveIdentity)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not create identity: ", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.keypair.data
|
||||||
|
}
|
||||||
|
|
||||||
|
getKeyId(): Store<string> {
|
||||||
|
return this.key_id
|
||||||
|
}
|
||||||
|
|
||||||
|
private allReviewsById : UIEventSource<(Review & {kid: string, signature: string})[]>= undefined
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all reviews that are made for the current identity.
|
||||||
|
*/
|
||||||
|
public getAllReviews(): Store<(Review & {kid: string, signature: string})[]>{
|
||||||
|
if(this.allReviewsById !== undefined){
|
||||||
|
return this.allReviewsById
|
||||||
|
}
|
||||||
|
this.allReviewsById = new UIEventSource( [])
|
||||||
|
this.key_id.map(pem => {
|
||||||
|
if(pem === undefined){
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
MangroveReviews.getReviews({
|
||||||
|
kid: pem
|
||||||
|
}).then(allReviews => {
|
||||||
|
this.allReviewsById.setData(allReviews.reviews.map(r => ({
|
||||||
|
...r, ...r.payload
|
||||||
|
})))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return this.allReviewsById
|
||||||
|
}
|
||||||
|
|
||||||
|
addReview(review: Review & {kid, signature}) {
|
||||||
|
this.allReviewsById?.setData(this.allReviewsById?.data?.concat([review]))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -176,26 +225,30 @@ export default class FeatureReviews {
|
||||||
* The given review is uploaded to mangrove.reviews and added to the list of known reviews
|
* The given review is uploaded to mangrove.reviews and added to the list of known reviews
|
||||||
*/
|
*/
|
||||||
public async createReview(review: Omit<Review, "sub">): Promise<void> {
|
public async createReview(review: Omit<Review, "sub">): Promise<void> {
|
||||||
if(review.opinion.length > FeatureReviews .REVIEW_OPINION_MAX_LENGTH){
|
if(review.opinion !== undefined && review.opinion.length > FeatureReviews .REVIEW_OPINION_MAX_LENGTH){
|
||||||
throw "Opinion too long, should be at most "+FeatureReviews.REVIEW_OPINION_MAX_LENGTH+" characters long"
|
throw "Opinion too long, should be at most "+FeatureReviews.REVIEW_OPINION_MAX_LENGTH+" characters long"
|
||||||
}
|
}
|
||||||
const r: Review = {
|
const r: Review = {
|
||||||
sub: this.subjectUri.data,
|
sub: this.subjectUri.data,
|
||||||
...review,
|
...review,
|
||||||
}
|
}
|
||||||
const keypair: CryptoKeyPair = this._identity.keypair.data
|
const keypair: CryptoKeyPair = await this._identity.getKeypair()
|
||||||
const jwt = await MangroveReviews.signReview(keypair, r)
|
const jwt = await MangroveReviews.signReview(keypair, r)
|
||||||
const kid = await MangroveReviews.publicToPem(keypair.publicKey)
|
const kid = await MangroveReviews.publicToPem(keypair.publicKey)
|
||||||
await MangroveReviews.submitReview(jwt)
|
await MangroveReviews.submitReview(jwt)
|
||||||
this._reviews.data.push({
|
const reviewWithKid = {
|
||||||
...r,
|
...r,
|
||||||
kid,
|
kid,
|
||||||
signature: jwt,
|
signature: jwt,
|
||||||
madeByLoggedInUser: new ImmutableStore(true),
|
madeByLoggedInUser: new ImmutableStore(true),
|
||||||
})
|
}
|
||||||
|
this._reviews.data.push( reviewWithKid)
|
||||||
this._reviews.ping()
|
this._reviews.ping()
|
||||||
|
this._identity.addReview(reviewWithKid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds given reviews to the 'reviews'-UI-eventsource
|
* Adds given reviews to the 'reviews'-UI-eventsource
|
||||||
* @param reviews
|
* @param reviews
|
||||||
|
@ -235,7 +288,7 @@ export default class FeatureReviews {
|
||||||
...review,
|
...review,
|
||||||
kid: reviewData.kid,
|
kid: reviewData.kid,
|
||||||
signature: reviewData.signature,
|
signature: reviewData.signature,
|
||||||
madeByLoggedInUser: this._identity.key_id.map((user_key_id) => {
|
madeByLoggedInUser: this._identity.getKeyId().map((user_key_id) => {
|
||||||
return reviewData.kid === user_key_id
|
return reviewData.kid === user_key_id
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
|
@ -28,9 +28,9 @@ export default interface PointRenderingConfigJson {
|
||||||
/**
|
/**
|
||||||
* question: At what location should this icon be shown?
|
* question: At what location should this icon be shown?
|
||||||
* multianswer: true
|
* multianswer: true
|
||||||
* suggestions: return [{if: "value=point",then: "Show an icon for point (node) objects"},{if: "value=centroid",then: "Show an icon for line or polygon (way) objects at their centroid location"}, {if: "value=start",then: "Show an icon for line (way) objects at the start"},{if: "value=end",then: "Show an icon for line (way) object at the end"},{if: "value=projected_centerpoint",then: "Show an icon for line (way) object near the centroid location, but moved onto the line"}]
|
* suggestions: return [{if: "value=point",then: "Show an icon for point (node) objects"},{if: "value=centroid",then: "Show an icon for line or polygon (way) objects at their centroid location"}, {if: "value=start",then: "Show an icon for line (way) objects at the start"},{if: "value=end",then: "Show an icon for line (way) object at the end"},{if: "value=projected_centerpoint",then: "Show an icon for line (way) object near the centroid location, but moved onto the line. Does not show an item on polygons"}, ,{if: "value=polygon_centroid",then: "Show an icon at a polygon centroid (but not if it is a way)"}]
|
||||||
*/
|
*/
|
||||||
location: ("point" | "centroid" | "start" | "end" | "projected_centerpoint" | string)[]
|
location: ("point" | "centroid" | "start" | "end" | "projected_centerpoint" | "polygon_centroid" | string)[]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The marker for an element.
|
* The marker for an element.
|
||||||
|
|
|
@ -38,9 +38,10 @@ export default class PointRenderingConfig extends WithContextLoader {
|
||||||
"start",
|
"start",
|
||||||
"end",
|
"end",
|
||||||
"projected_centerpoint",
|
"projected_centerpoint",
|
||||||
|
"polygon_centroid"
|
||||||
])
|
])
|
||||||
public readonly location: Set<
|
public readonly location: Set<
|
||||||
"point" | "centroid" | "start" | "end" | "projected_centerpoint" | string
|
"point" | "centroid" | "start" | "end" | "projected_centerpoint" | "polygon_centroid" | string
|
||||||
>
|
>
|
||||||
|
|
||||||
public readonly marker: IconConfig[]
|
public readonly marker: IconConfig[]
|
||||||
|
|
|
@ -171,7 +171,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
})
|
})
|
||||||
this.userRelatedState = new UserRelatedState(
|
this.userRelatedState = new UserRelatedState(
|
||||||
this.osmConnection,
|
this.osmConnection,
|
||||||
layout?.language,
|
|
||||||
layout,
|
layout,
|
||||||
this.featureSwitches,
|
this.featureSwitches,
|
||||||
this.mapProperties
|
this.mapProperties
|
||||||
|
|
|
@ -77,6 +77,9 @@ export class Orientation {
|
||||||
}
|
}
|
||||||
|
|
||||||
private update(event: DeviceOrientationEvent) {
|
private update(event: DeviceOrientationEvent) {
|
||||||
|
if(event.alpha === null || event.beta === null || event.gamma === null){
|
||||||
|
return
|
||||||
|
}
|
||||||
this.gotMeasurement.setData(true)
|
this.gotMeasurement.setData(true)
|
||||||
// IF the phone is lying flat, then:
|
// IF the phone is lying flat, then:
|
||||||
// alpha is the compass direction (but not absolute)
|
// alpha is the compass direction (but not absolute)
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
center()
|
center()
|
||||||
}
|
}
|
||||||
|
|
||||||
const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"]
|
let titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if favLayer !== undefined}
|
{#if favLayer !== undefined}
|
||||||
|
|
|
@ -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.priintroPrivacyvacy} />
|
<Tr t={Translations.t.favouritePoi.introPrivacy} />
|
||||||
|
|
||||||
{#each $favourites as feature (feature.properties.id)}
|
{#each $favourites as feature (feature.properties.id)}
|
||||||
<FavouriteSummary {feature} {state} />
|
<FavouriteSummary {feature} {state} />
|
||||||
|
|
|
@ -1,63 +1,82 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import Loading from "../../assets/svg/Loading.svelte"
|
import Loading from "../../assets/svg/Loading.svelte"
|
||||||
import Tr from "../Base/Tr.svelte"
|
import Tr from "../Base/Tr.svelte"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
import Icon from "../Map/Icon.svelte"
|
import Icon from "../Map/Icon.svelte"
|
||||||
import Maproulette from "../../Logic/Maproulette"
|
import Maproulette from "../../Logic/Maproulette"
|
||||||
|
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A UI-element to change the status of a maproulette-task
|
* A UI-element to change the status of a maproulette-task
|
||||||
*/
|
*/
|
||||||
export let state: SpecialVisualizationState
|
export let state: SpecialVisualizationState
|
||||||
export let tags: UIEventSource<Record<string, string>>
|
export let tags: UIEventSource<Record<string, string>>
|
||||||
export let message: string
|
export let message: string
|
||||||
export let image: string
|
export let image: string
|
||||||
export let message_closed: string
|
export let message_closed: string
|
||||||
export let statusToSet: string
|
export let statusToSet: string
|
||||||
export let maproulette_id_key: string
|
export let maproulette_id_key: string
|
||||||
|
|
||||||
let applying = false
|
export let askFeedback: string = ""
|
||||||
let failed = false
|
|
||||||
|
|
||||||
/** Current status of the task*/
|
let applying = false
|
||||||
let status: Store<number> = tags
|
let failed = false
|
||||||
.map((tgs) => {
|
let feedback: string = ""
|
||||||
if (tgs["status"]) {
|
|
||||||
return tgs["status"]
|
|
||||||
}
|
|
||||||
return Maproulette.codeToIndex(tgs["mr_taskStatus"])
|
|
||||||
})
|
|
||||||
.map(Number)
|
|
||||||
|
|
||||||
async function apply() {
|
/** Current status of the task*/
|
||||||
const maproulette_id = tags.data[maproulette_id_key] ?? tags.data.mr_taskId ?? tags.data.id
|
let status: Store<number> = tags
|
||||||
try {
|
.map((tgs) => {
|
||||||
await Maproulette.singleton.closeTask(Number(maproulette_id), Number(statusToSet), {
|
if (tgs["status"]) {
|
||||||
tags: `MapComplete MapComplete:${state.layout.id}`,
|
return tgs["status"]
|
||||||
})
|
}
|
||||||
tags.data["mr_taskStatus"] = Maproulette.STATUS_MEANING[Number(statusToSet)]
|
return Maproulette.codeToIndex(tgs["mr_taskStatus"])
|
||||||
tags.data.status = statusToSet
|
})
|
||||||
tags.ping()
|
.map(Number)
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
async function apply() {
|
||||||
failed = true
|
const maproulette_id = tags.data[maproulette_id_key] ?? tags.data.mr_taskId ?? tags.data.id
|
||||||
|
try {
|
||||||
|
await Maproulette.singleton.closeTask(Number(maproulette_id), Number(statusToSet), {
|
||||||
|
tags: `MapComplete MapComplete:${state.layout.id}`,
|
||||||
|
comment: feedback
|
||||||
|
})
|
||||||
|
tags.data["mr_taskStatus"] = Maproulette.STATUS_MEANING[Number(statusToSet)]
|
||||||
|
tags.data.status = statusToSet
|
||||||
|
tags.ping()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
failed = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if failed}
|
<LoginToggle ignoreLoading={true} {state}>
|
||||||
<div class="alert">ERROR - could not close the MapRoulette task</div>
|
{#if failed}
|
||||||
{:else if applying}
|
<div class="alert">ERROR - could not close the MapRoulette task</div>
|
||||||
<Loading>
|
{:else if applying}
|
||||||
<Tr t={Translations.t.general.loading} />
|
<Loading>
|
||||||
</Loading>
|
<Tr t={Translations.t.general.loading} />
|
||||||
{:else if $status === Maproulette.STATUS_OPEN}
|
</Loading>
|
||||||
<button class="no-image-background w-full p-4 m-0" on:click={() => apply()}>
|
{:else if $status === Maproulette.STATUS_OPEN}
|
||||||
<Icon clss="w-8 h-8 mr-2 shrink-0" icon={image} />
|
{#if askFeedback !== "" && askFeedback !== undefined}
|
||||||
{message}
|
<div class="flex flex-col p-1 gap-y-1 interactive border border-gray-500 border-dashed">
|
||||||
</button>
|
<h3>{askFeedback}</h3>
|
||||||
{:else}
|
<textarea bind:value={feedback}></textarea>
|
||||||
{message_closed}
|
<button class="no-image-background w-full p-4 m-0" class:disabled={feedback===""} on:click={() => apply()}>
|
||||||
{/if}
|
<Icon clss="w-8 h-8 mr-2 shrink-0" icon={image} />
|
||||||
|
{message}
|
||||||
|
</button>
|
||||||
|
{feedback}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<button class="no-image-background w-full p-4 m-0" on:click={() => apply()}>
|
||||||
|
<Icon clss="w-8 h-8 mr-2 shrink-0" icon={image} />
|
||||||
|
{message}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
{message_closed}
|
||||||
|
{/if}
|
||||||
|
</LoginToggle>
|
||||||
|
|
|
@ -35,9 +35,9 @@
|
||||||
|
|
||||||
let _state: "ask" | "saving" | "done" = "ask"
|
let _state: "ask" | "saving" | "done" = "ask"
|
||||||
|
|
||||||
const connection = state.osmConnection
|
let connection = state.osmConnection
|
||||||
|
|
||||||
const hasError: Store<undefined | "too_long"> = opinion.mapD(op => {
|
let hasError: Store<undefined | "too_long"> = opinion.mapD(op => {
|
||||||
const tooLong = op.length > FeatureReviews.REVIEW_OPINION_MAX_LENGTH
|
const tooLong = op.length > FeatureReviews.REVIEW_OPINION_MAX_LENGTH
|
||||||
if (tooLong) {
|
if (tooLong) {
|
||||||
return "too_long"
|
return "too_long"
|
||||||
|
@ -45,6 +45,8 @@
|
||||||
return undefined
|
return undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let uploadFailed: string = undefined
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
if (hasError.data) {
|
if (hasError.data) {
|
||||||
return
|
return
|
||||||
|
@ -63,13 +65,24 @@
|
||||||
console.log("Testing - not actually saving review", review)
|
console.log("Testing - not actually saving review", review)
|
||||||
await Utils.waitFor(1000)
|
await Utils.waitFor(1000)
|
||||||
} else {
|
} else {
|
||||||
await reviews.createReview(review)
|
try {
|
||||||
|
|
||||||
|
await reviews.createReview(review)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not create review due to", e)
|
||||||
|
uploadFailed = "" + e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_state = "done"
|
_state = "done"
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
{#if uploadFailed}
|
||||||
{#if _state === "done"}
|
<div class="alert flex">
|
||||||
|
<ExclamationTriangle class="w-6 h-6" />
|
||||||
|
<Tr t={Translations.t.general.error}/>
|
||||||
|
{uploadFailed}
|
||||||
|
</div>
|
||||||
|
{:else if _state === "done"}
|
||||||
<Tr cls="thanks w-full" t={t.saved} />
|
<Tr cls="thanks w-full" t={t.saved} />
|
||||||
{:else if _state === "saving"}
|
{:else if _state === "saving"}
|
||||||
<Loading>
|
<Loading>
|
||||||
|
@ -109,8 +122,9 @@
|
||||||
/>
|
/>
|
||||||
{#if $hasError === "too_long"}
|
{#if $hasError === "too_long"}
|
||||||
<div class="alert flex items-center px-2">
|
<div class="alert flex items-center px-2">
|
||||||
<ExclamationTriangle class="w-12 h-12"/>
|
<ExclamationTriangle class="w-12 h-12" />
|
||||||
<Tr t={t.too_long.Subs({max: FeatureReviews.REVIEW_OPINION_MAX_LENGTH, amount: $opinion?.length ?? 0})}> </Tr>
|
<Tr
|
||||||
|
t={t.too_long.Subs({max: FeatureReviews.REVIEW_OPINION_MAX_LENGTH, amount: $opinion?.length ?? 0})}></Tr>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
|
|
40
src/UI/Reviews/ReviewsOverview.svelte
Normal file
40
src/UI/Reviews/ReviewsOverview.svelte
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
|
import Translations from "../i18n/Translations"
|
||||||
|
import Tr from "../Base/Tr.svelte"
|
||||||
|
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||||
|
import LoginButton from "../Base/LoginButton.svelte"
|
||||||
|
import SingleReview from "./SingleReview.svelte"
|
||||||
|
import Mangrove_logo from "../../assets/svg/Mangrove_logo.svelte"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A panel showing all the reviews by the logged-in user
|
||||||
|
*/
|
||||||
|
export let state: SpecialVisualizationState
|
||||||
|
let reviews = state.userRelatedState.mangroveIdentity.getAllReviews()
|
||||||
|
const t = Translations.t.reviews
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<LoginToggle {state}>
|
||||||
|
<div slot="not-logged-in">
|
||||||
|
<LoginButton osmConnection={state.osmConnection}>
|
||||||
|
<Tr t={Translations.t.favouritePoi.loginToSeeList} />
|
||||||
|
</LoginButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{#if $reviews?.length > 0}
|
||||||
|
<div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}>
|
||||||
|
{#each $reviews as review (review.sub)}
|
||||||
|
<SingleReview {review} showSub={true} {state} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Tr t={t.your_reviews_empty} />
|
||||||
|
{/if}
|
||||||
|
<a class="link-underline" href="https://github.com/pietervdvn/MapComplete/issues/1782" target="_blank" rel="noopener noreferrer"><Tr t={t.reviews_bug}/></a>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<Mangrove_logo class="h-12 w-12 shrink-0 p-1" />
|
||||||
|
<Tr cls="text-sm subtle" t={t.attribution} />
|
||||||
|
</div>
|
||||||
|
</LoginToggle>
|
|
@ -1,22 +1,43 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Review } from "mangrove-reviews-typescript"
|
import { Review } from "mangrove-reviews-typescript"
|
||||||
import { Store } from "../../Logic/UIEventSource"
|
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||||
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"
|
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||||
|
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
|
|
||||||
export let review: Review & { kid: string; signature: string; madeByLoggedInUser: Store<boolean> }
|
export let state: SpecialVisualizationState = undefined
|
||||||
|
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()
|
||||||
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()
|
||||||
let byLoggedInUser = review.madeByLoggedInUser
|
let byLoggedInUser = review.madeByLoggedInUser ?? ImmutableStore.FALSE
|
||||||
|
|
||||||
|
export let showSub = false
|
||||||
|
let subUrl = new URL(review.sub)
|
||||||
|
let [lat, lon] = subUrl.pathname.split(",").map(l => Number(l))
|
||||||
|
let sub = subUrl.searchParams.get("q")
|
||||||
|
|
||||||
|
function selectFeature(){
|
||||||
|
console.log("Selecting and zooming to", {lon, lat})
|
||||||
|
state?.mapProperties?.location?.setData({lon, lat})
|
||||||
|
state?.mapProperties?.zoom?.setData(Math.max(16, state?.mapProperties?.zoom?.data))
|
||||||
|
|
||||||
|
state?.guistate?.closeAll()
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={"low-interaction rounded-lg p-1 px-2 " + ($byLoggedInUser ? "border-interactive" : "")}>
|
<div class={"low-interaction rounded-lg p-1 px-2 flex flex-col" + ($byLoggedInUser ? "border-interactive" : "")}>
|
||||||
<div class="flex items-center justify-between">
|
{#if showSub}
|
||||||
|
<button class="link" on:click={() => selectFeature()}>
|
||||||
|
<h3>{sub}</h3>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
<div
|
<div
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
use:ariaLabel={Translations.t.reviews.rated.Subs({
|
use:ariaLabel={Translations.t.reviews.rated.Subs({
|
||||||
|
|
|
@ -3,11 +3,7 @@ import { FixedUiElement } from "./Base/FixedUiElement"
|
||||||
import BaseUIElement from "./BaseUIElement"
|
import BaseUIElement from "./BaseUIElement"
|
||||||
import Title from "./Base/Title"
|
import Title from "./Base/Title"
|
||||||
import Table from "./Base/Table"
|
import Table from "./Base/Table"
|
||||||
import {
|
import { RenderingSpecification, SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization"
|
||||||
RenderingSpecification,
|
|
||||||
SpecialVisualization,
|
|
||||||
SpecialVisualizationState,
|
|
||||||
} from "./SpecialVisualization"
|
|
||||||
import { HistogramViz } from "./Popup/HistogramViz"
|
import { HistogramViz } from "./Popup/HistogramViz"
|
||||||
import { MinimapViz } from "./Popup/MinimapViz"
|
import { MinimapViz } from "./Popup/MinimapViz"
|
||||||
import { ShareLinkViz } from "./Popup/ShareLinkViz"
|
import { ShareLinkViz } from "./Popup/ShareLinkViz"
|
||||||
|
@ -90,6 +86,8 @@ import Qr from "../Utils/Qr"
|
||||||
import ComparisonTool from "./Comparison/ComparisonTool.svelte"
|
import ComparisonTool from "./Comparison/ComparisonTool.svelte"
|
||||||
import SpecialTranslation from "./Popup/TagRendering/SpecialTranslation.svelte"
|
import SpecialTranslation from "./Popup/TagRendering/SpecialTranslation.svelte"
|
||||||
import SpecialVisualisationUtils from "./SpecialVisualisationUtils"
|
import SpecialVisualisationUtils from "./SpecialVisualisationUtils"
|
||||||
|
import LoginButton from "./Base/LoginButton.svelte"
|
||||||
|
import Toggle from "./Input/Toggle"
|
||||||
|
|
||||||
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
|
||||||
|
@ -116,7 +114,7 @@ class NearbyImageVis implements SpecialVisualization {
|
||||||
tags: UIEventSource<Record<string, string>>,
|
tags: UIEventSource<Record<string, string>>,
|
||||||
args: string[],
|
args: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
const isOpen = args[0] === "open"
|
const isOpen = args[0] === "open"
|
||||||
const readonly = args[1] === "readonly"
|
const readonly = args[1] === "readonly"
|
||||||
|
@ -183,7 +181,7 @@ class StealViz implements SpecialVisualization {
|
||||||
selectedElement: otherFeature,
|
selectedElement: otherFeature,
|
||||||
state,
|
state,
|
||||||
layer,
|
layer,
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (elements.length === 1) {
|
if (elements.length === 1) {
|
||||||
|
@ -191,8 +189,8 @@ class StealViz implements SpecialVisualization {
|
||||||
}
|
}
|
||||||
return new Combine(elements).SetClass("flex flex-col")
|
return new Combine(elements).SetClass("flex flex-col")
|
||||||
},
|
},
|
||||||
[state.indexedFeatures.featuresById]
|
[state.indexedFeatures.featuresById],
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,7 +229,7 @@ export class QuestionViz implements SpecialVisualization {
|
||||||
tags: UIEventSource<Record<string, string>>,
|
tags: UIEventSource<Record<string, string>>,
|
||||||
args: string[],
|
args: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
const labels = args[0]
|
const labels = args[0]
|
||||||
?.split(";")
|
?.split(";")
|
||||||
|
@ -267,37 +265,38 @@ export default class SpecialVisualizations {
|
||||||
viz.docs,
|
viz.docs,
|
||||||
viz.args.length > 0
|
viz.args.length > 0
|
||||||
? new Table(
|
? new Table(
|
||||||
["name", "default", "description"],
|
["name", "default", "description"],
|
||||||
viz.args.map((arg) => {
|
viz.args.map((arg) => {
|
||||||
let defaultArg = arg.defaultValue ?? "_undefined_"
|
let defaultArg = arg.defaultValue ?? "_undefined_"
|
||||||
if (defaultArg == "") {
|
if (defaultArg == "") {
|
||||||
defaultArg = "_empty string_"
|
defaultArg = "_empty string_"
|
||||||
}
|
}
|
||||||
return [arg.name, defaultArg, arg.doc]
|
return [arg.name, defaultArg, arg.doc]
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
: undefined,
|
: undefined,
|
||||||
new Title("Example usage of " + viz.funcName, 4),
|
new Title("Example usage of " + viz.funcName, 4),
|
||||||
new FixedUiElement(
|
new FixedUiElement(
|
||||||
viz.example ??
|
viz.example ??
|
||||||
"`{" +
|
"`{" +
|
||||||
viz.funcName +
|
viz.funcName +
|
||||||
"(" +
|
"(" +
|
||||||
viz.args.map((arg) => arg.defaultValue).join(",") +
|
viz.args.map((arg) => arg.defaultValue).join(",") +
|
||||||
")}`"
|
")}`",
|
||||||
).SetClass("literal-code"),
|
).SetClass("literal-code"),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
public static constructSpecification(
|
public static constructSpecification(
|
||||||
template: string,
|
template: string,
|
||||||
extraMappings: SpecialVisualization[] = []
|
extraMappings: SpecialVisualization[] = [],
|
||||||
): RenderingSpecification[] {
|
): RenderingSpecification[] {
|
||||||
return SpecialVisualisationUtils.constructSpecification(template, extraMappings)
|
return SpecialVisualisationUtils.constructSpecification(template, extraMappings)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HelpMessage() {
|
public static HelpMessage() {
|
||||||
const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) =>
|
const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) =>
|
||||||
SpecialVisualizations.DocumentationFor(viz)
|
SpecialVisualizations.DocumentationFor(viz),
|
||||||
)
|
)
|
||||||
|
|
||||||
return new Combine([
|
return new Combine([
|
||||||
|
@ -331,10 +330,10 @@ export default class SpecialVisualizations {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
" "
|
" ",
|
||||||
)
|
),
|
||||||
).SetClass("code"),
|
).SetClass("code"),
|
||||||
'In other words: use `{ "before": ..., "after": ..., "special": {"type": ..., "argname": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)',
|
"In other words: use `{ \"before\": ..., \"after\": ..., \"special\": {\"type\": ..., \"argname\": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)",
|
||||||
]).SetClass("flex flex-col"),
|
]).SetClass("flex flex-col"),
|
||||||
...helpTexts,
|
...helpTexts,
|
||||||
]).SetClass("flex flex-col")
|
]).SetClass("flex flex-col")
|
||||||
|
@ -343,20 +342,20 @@ export default class SpecialVisualizations {
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
public static renderExampleOfSpecial(
|
public static renderExampleOfSpecial(
|
||||||
state: SpecialVisualizationState,
|
state: SpecialVisualizationState,
|
||||||
s: SpecialVisualization
|
s: SpecialVisualization,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
const examples =
|
const examples =
|
||||||
s.structuredExamples === undefined
|
s.structuredExamples === undefined
|
||||||
? []
|
? []
|
||||||
: s.structuredExamples().map((e) => {
|
: s.structuredExamples().map((e) => {
|
||||||
return s.constr(
|
return s.constr(
|
||||||
state,
|
state,
|
||||||
new UIEventSource<Record<string, string>>(e.feature.properties),
|
new UIEventSource<Record<string, string>>(e.feature.properties),
|
||||||
e.args,
|
e.args,
|
||||||
e.feature,
|
e.feature,
|
||||||
undefined
|
undefined,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
return new Combine([new Title(s.funcName), s.docs, ...examples])
|
return new Combine([new Title(s.funcName), s.docs, ...examples])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -396,7 +395,7 @@ export default class SpecialVisualizations {
|
||||||
assignTo: state.userRelatedState.language,
|
assignTo: state.userRelatedState.language,
|
||||||
availableLanguages: state.layout.language,
|
availableLanguages: state.layout.language,
|
||||||
preferredLanguages: state.osmConnection.userDetails.map(
|
preferredLanguages: state.osmConnection.userDetails.map(
|
||||||
(ud) => ud.languages
|
(ud) => ud.languages,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -421,7 +420,7 @@ export default class SpecialVisualizations {
|
||||||
|
|
||||||
constr(
|
constr(
|
||||||
state: SpecialVisualizationState,
|
state: SpecialVisualizationState,
|
||||||
tagSource: UIEventSource<Record<string, string>>
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
return new VariableUiElement(
|
return new VariableUiElement(
|
||||||
tagSource
|
tagSource
|
||||||
|
@ -431,7 +430,7 @@ export default class SpecialVisualizations {
|
||||||
return new SplitRoadWizard(<WayId>id, state)
|
return new SplitRoadWizard(<WayId>id, state)
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -445,7 +444,7 @@ export default class SpecialVisualizations {
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
argument: string[],
|
argument: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
if (feature.geometry.type !== "Point") {
|
if (feature.geometry.type !== "Point") {
|
||||||
return undefined
|
return undefined
|
||||||
|
@ -468,7 +467,7 @@ export default class SpecialVisualizations {
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
argument: string[],
|
argument: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
if (!layer.deletion) {
|
if (!layer.deletion) {
|
||||||
return undefined
|
return undefined
|
||||||
|
@ -496,7 +495,7 @@ export default class SpecialVisualizations {
|
||||||
state: SpecialVisualizationState,
|
state: SpecialVisualizationState,
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
argument: string[],
|
argument: string[],
|
||||||
feature: Feature
|
feature: Feature,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
|
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
|
||||||
return new SvelteUIElement(CreateNewNote, {
|
return new SvelteUIElement(CreateNewNote, {
|
||||||
|
@ -560,7 +559,7 @@ export default class SpecialVisualizations {
|
||||||
.map((tags) => tags[args[0]])
|
.map((tags) => tags[args[0]])
|
||||||
.map((wikidata) => {
|
.map((wikidata) => {
|
||||||
wikidata = Utils.NoEmpty(
|
wikidata = Utils.NoEmpty(
|
||||||
wikidata?.split(";")?.map((wd) => wd.trim()) ?? []
|
wikidata?.split(";")?.map((wd) => wd.trim()) ?? [],
|
||||||
)[0]
|
)[0]
|
||||||
const entry = Wikidata.LoadWikidataEntry(wikidata)
|
const entry = Wikidata.LoadWikidataEntry(wikidata)
|
||||||
return new VariableUiElement(
|
return new VariableUiElement(
|
||||||
|
@ -570,9 +569,9 @@ export default class SpecialVisualizations {
|
||||||
}
|
}
|
||||||
const response = <WikidataResponse>e["success"]
|
const response = <WikidataResponse>e["success"]
|
||||||
return Translation.fromMap(response.labels)
|
return Translation.fromMap(response.labels)
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
})
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
new MapillaryLinkVis(),
|
new MapillaryLinkVis(),
|
||||||
|
@ -586,7 +585,7 @@ export default class SpecialVisualizations {
|
||||||
tags: UIEventSource<Record<string, string>>,
|
tags: UIEventSource<Record<string, string>>,
|
||||||
_,
|
_,
|
||||||
__,
|
__,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
) => new SvelteUIElement(AllTagsPanel, { tags, layer }),
|
) => new SvelteUIElement(AllTagsPanel, { tags, layer }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -608,7 +607,7 @@ export default class SpecialVisualizations {
|
||||||
return new ImageCarousel(
|
return new ImageCarousel(
|
||||||
AllImageProviders.LoadImagesFor(tags, imagePrefixes),
|
AllImageProviders.LoadImagesFor(tags, imagePrefixes),
|
||||||
tags,
|
tags,
|
||||||
state
|
state,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -664,7 +663,7 @@ export default class SpecialVisualizations {
|
||||||
{
|
{
|
||||||
nameKey: nameKey,
|
nameKey: nameKey,
|
||||||
fallbackName,
|
fallbackName,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
return new SvelteUIElement(StarsBarIcon, {
|
return new SvelteUIElement(StarsBarIcon, {
|
||||||
score: reviews.average,
|
score: reviews.average,
|
||||||
|
@ -697,7 +696,7 @@ export default class SpecialVisualizations {
|
||||||
{
|
{
|
||||||
nameKey: nameKey,
|
nameKey: nameKey,
|
||||||
fallbackName,
|
fallbackName,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer })
|
return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer })
|
||||||
},
|
},
|
||||||
|
@ -729,7 +728,7 @@ export default class SpecialVisualizations {
|
||||||
{
|
{
|
||||||
nameKey: nameKey,
|
nameKey: nameKey,
|
||||||
fallbackName,
|
fallbackName,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer })
|
return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer })
|
||||||
},
|
},
|
||||||
|
@ -787,7 +786,7 @@ export default class SpecialVisualizations {
|
||||||
tags: UIEventSource<Record<string, string>>,
|
tags: UIEventSource<Record<string, string>>,
|
||||||
args: string[],
|
args: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): SvelteUIElement {
|
): SvelteUIElement {
|
||||||
const keyToUse = args[0]
|
const keyToUse = args[0]
|
||||||
const prefix = args[1]
|
const prefix = args[1]
|
||||||
|
@ -824,17 +823,17 @@ export default class SpecialVisualizations {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
const allUnits: Unit[] = [].concat(
|
const allUnits: Unit[] = [].concat(
|
||||||
...(state?.layout?.layers?.map((lyr) => lyr.units) ?? [])
|
...(state?.layout?.layers?.map((lyr) => lyr.units) ?? []),
|
||||||
)
|
)
|
||||||
const unit = allUnits.filter((unit) =>
|
const unit = allUnits.filter((unit) =>
|
||||||
unit.isApplicableToKey(key)
|
unit.isApplicableToKey(key),
|
||||||
)[0]
|
)[0]
|
||||||
if (unit === undefined) {
|
if (unit === undefined) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
const getCountry = () => tagSource.data._country
|
const getCountry = () => tagSource.data._country
|
||||||
return unit.asHumanLongValue(value, getCountry)
|
return unit.asHumanLongValue(value, getCountry)
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -851,7 +850,7 @@ export default class SpecialVisualizations {
|
||||||
new Combine([
|
new Combine([
|
||||||
t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"),
|
t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"),
|
||||||
t.downloadGeoJsonHelper.SetClass("subtle"),
|
t.downloadGeoJsonHelper.SetClass("subtle"),
|
||||||
]).SetClass("flex flex-col")
|
]).SetClass("flex flex-col"),
|
||||||
)
|
)
|
||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
console.log("Exporting as Geojson")
|
console.log("Exporting as Geojson")
|
||||||
|
@ -864,7 +863,7 @@ export default class SpecialVisualizations {
|
||||||
title + "_mapcomplete_export.geojson",
|
title + "_mapcomplete_export.geojson",
|
||||||
{
|
{
|
||||||
mimetype: "application/vnd.geo+json",
|
mimetype: "application/vnd.geo+json",
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.SetClass("w-full")
|
.SetClass("w-full")
|
||||||
|
@ -900,7 +899,7 @@ export default class SpecialVisualizations {
|
||||||
constr: (state) => {
|
constr: (state) => {
|
||||||
return new SubtleButton(
|
return new SubtleButton(
|
||||||
Svg.delete_icon_svg().SetStyle("height: 1.5rem"),
|
Svg.delete_icon_svg().SetStyle("height: 1.5rem"),
|
||||||
Translations.t.general.removeLocationHistory
|
Translations.t.general.removeLocationHistory,
|
||||||
).onClick(() => {
|
).onClick(() => {
|
||||||
state.historicalUserLocations.features.setData([])
|
state.historicalUserLocations.features.setData([])
|
||||||
state.selectedElement.setData(undefined)
|
state.selectedElement.setData(undefined)
|
||||||
|
@ -938,10 +937,10 @@ export default class SpecialVisualizations {
|
||||||
.filter((c) => c.text !== "")
|
.filter((c) => c.text !== "")
|
||||||
.map(
|
.map(
|
||||||
(c, i) =>
|
(c, i) =>
|
||||||
new NoteCommentElement(c, state, i, comments.length)
|
new NoteCommentElement(c, state, i, comments.length),
|
||||||
)
|
),
|
||||||
).SetClass("flex flex-col")
|
).SetClass("flex flex-col")
|
||||||
})
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -975,7 +974,7 @@ export default class SpecialVisualizations {
|
||||||
tagsSource: UIEventSource<Record<string, string>>,
|
tagsSource: UIEventSource<Record<string, string>>,
|
||||||
_: string[],
|
_: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
) =>
|
) =>
|
||||||
new VariableUiElement(
|
new VariableUiElement(
|
||||||
tagsSource.map((tags) => {
|
tagsSource.map((tags) => {
|
||||||
|
@ -993,7 +992,7 @@ export default class SpecialVisualizations {
|
||||||
feature,
|
feature,
|
||||||
layer,
|
layer,
|
||||||
}).SetClass("px-1")
|
}).SetClass("px-1")
|
||||||
})
|
}),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1009,8 +1008,8 @@ export default class SpecialVisualizations {
|
||||||
let challenge = Stores.FromPromise(
|
let challenge = Stores.FromPromise(
|
||||||
Utils.downloadJsonCached(
|
Utils.downloadJsonCached(
|
||||||
`${Maproulette.defaultEndpoint}/challenge/${parentId}`,
|
`${Maproulette.defaultEndpoint}/challenge/${parentId}`,
|
||||||
24 * 60 * 60 * 1000
|
24 * 60 * 60 * 1000,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return new VariableUiElement(
|
return new VariableUiElement(
|
||||||
|
@ -1035,7 +1034,7 @@ export default class SpecialVisualizations {
|
||||||
} else {
|
} else {
|
||||||
return [title, new List(listItems)]
|
return [title, new List(listItems)]
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
docs: "Fetches the metadata of MapRoulette campaign that this task is part of and shows those details (namely `title`, `description` and `instruction`).\n\nThis reads the property `mr_challengeId` to detect the parent campaign.",
|
docs: "Fetches the metadata of MapRoulette campaign that this task is part of and shows those details (namely `title`, `description` and `instruction`).\n\nThis reads the property `mr_challengeId` to detect the parent campaign.",
|
||||||
|
@ -1049,15 +1048,15 @@ export default class SpecialVisualizations {
|
||||||
"\n" +
|
"\n" +
|
||||||
"```json\n" +
|
"```json\n" +
|
||||||
"{\n" +
|
"{\n" +
|
||||||
' "id": "mark_duplicate",\n' +
|
" \"id\": \"mark_duplicate\",\n" +
|
||||||
' "render": {\n' +
|
" \"render\": {\n" +
|
||||||
' "special": {\n' +
|
" \"special\": {\n" +
|
||||||
' "type": "maproulette_set_status",\n' +
|
" \"type\": \"maproulette_set_status\",\n" +
|
||||||
' "message": {\n' +
|
" \"message\": {\n" +
|
||||||
' "en": "Mark as not found or false positive"\n' +
|
" \"en\": \"Mark as not found or false positive\"\n" +
|
||||||
" },\n" +
|
" },\n" +
|
||||||
' "status": "2",\n' +
|
" \"status\": \"2\",\n" +
|
||||||
' "image": "close"\n' +
|
" \"image\": \"close\"\n" +
|
||||||
" }\n" +
|
" }\n" +
|
||||||
" }\n" +
|
" }\n" +
|
||||||
"}\n" +
|
"}\n" +
|
||||||
|
@ -1086,10 +1085,15 @@ export default class SpecialVisualizations {
|
||||||
doc: "The property name containing the maproulette id",
|
doc: "The property name containing the maproulette id",
|
||||||
defaultValue: "mr_taskId",
|
defaultValue: "mr_taskId",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "ask_feedback",
|
||||||
|
doc: "If not an empty string, this will be used as question to ask some additional feedback. A text field will be added",
|
||||||
|
defaultValue: ""
|
||||||
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
constr: (state, tagsSource, args) => {
|
constr: (state, tagsSource, args) => {
|
||||||
let [message, image, message_closed, statusToSet, maproulette_id_key] = args
|
let [message, image, message_closed, statusToSet, maproulette_id_key, askFeedback] = args
|
||||||
if (image === "") {
|
if (image === "") {
|
||||||
image = "confirm"
|
image = "confirm"
|
||||||
}
|
}
|
||||||
|
@ -1105,6 +1109,7 @@ export default class SpecialVisualizations {
|
||||||
message_closed,
|
message_closed,
|
||||||
statusToSet,
|
statusToSet,
|
||||||
maproulette_id_key,
|
maproulette_id_key,
|
||||||
|
askFeedback
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1124,8 +1129,8 @@ export default class SpecialVisualizations {
|
||||||
const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox)
|
const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox)
|
||||||
return new StatisticsPanel(fsBboxed)
|
return new StatisticsPanel(fsBboxed)
|
||||||
},
|
},
|
||||||
[state.mapProperties.bounds]
|
[state.mapProperties.bounds],
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1191,7 +1196,7 @@ export default class SpecialVisualizations {
|
||||||
constr(
|
constr(
|
||||||
state: SpecialVisualizationState,
|
state: SpecialVisualizationState,
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
args: string[]
|
args: string[],
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
let [text, href, classnames, download, ariaLabel] = args
|
let [text, href, classnames, download, ariaLabel] = args
|
||||||
if (download === "") {
|
if (download === "") {
|
||||||
|
@ -1203,13 +1208,13 @@ export default class SpecialVisualizations {
|
||||||
(tags) =>
|
(tags) =>
|
||||||
new SvelteUIElement(Link, {
|
new SvelteUIElement(Link, {
|
||||||
text: Utils.SubstituteKeys(text, tags),
|
text: Utils.SubstituteKeys(text, tags),
|
||||||
href: Utils.SubstituteKeys(href, tags).replaceAll(/ /g, '%20') /* Chromium based browsers eat the spaces */,
|
href: Utils.SubstituteKeys(href, tags).replaceAll(/ /g, "%20") /* Chromium based browsers eat the spaces */,
|
||||||
classnames,
|
classnames,
|
||||||
download: Utils.SubstituteKeys(download, tags),
|
download: Utils.SubstituteKeys(download, tags),
|
||||||
ariaLabel: Utils.SubstituteKeys(ariaLabel, tags),
|
ariaLabel: Utils.SubstituteKeys(ariaLabel, tags),
|
||||||
newTab,
|
newTab,
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1231,7 +1236,7 @@ export default class SpecialVisualizations {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
" "
|
" ",
|
||||||
) +
|
) +
|
||||||
"\n```",
|
"\n```",
|
||||||
args: [
|
args: [
|
||||||
|
@ -1255,7 +1260,7 @@ export default class SpecialVisualizations {
|
||||||
featureTags: UIEventSource<Record<string, string>>,
|
featureTags: UIEventSource<Record<string, string>>,
|
||||||
args: string[],
|
args: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
) {
|
) {
|
||||||
const [key, tr, classesRaw] = args
|
const [key, tr, classesRaw] = args
|
||||||
let classes = classesRaw ?? ""
|
let classes = classesRaw ?? ""
|
||||||
|
@ -1280,7 +1285,7 @@ export default class SpecialVisualizations {
|
||||||
elements.push(subsTr)
|
elements.push(subsTr)
|
||||||
}
|
}
|
||||||
return elements
|
return elements
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1300,7 +1305,7 @@ export default class SpecialVisualizations {
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
argument: string[],
|
argument: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
return new VariableUiElement(
|
return new VariableUiElement(
|
||||||
tagSource.map((tags) => {
|
tagSource.map((tags) => {
|
||||||
|
@ -1312,7 +1317,7 @@ export default class SpecialVisualizations {
|
||||||
console.error("Cannot create a translation for", v, "due to", e)
|
console.error("Cannot create a translation for", v, "due to", e)
|
||||||
return JSON.stringify(v)
|
return JSON.stringify(v)
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1332,7 +1337,7 @@ export default class SpecialVisualizations {
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
argument: string[],
|
argument: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
const key = argument[0]
|
const key = argument[0]
|
||||||
const validator = new FediverseValidator()
|
const validator = new FediverseValidator()
|
||||||
|
@ -1342,7 +1347,7 @@ export default class SpecialVisualizations {
|
||||||
.map((fediAccount) => {
|
.map((fediAccount) => {
|
||||||
fediAccount = validator.reformat(fediAccount)
|
fediAccount = validator.reformat(fediAccount)
|
||||||
const [_, username, host] = fediAccount.match(
|
const [_, username, host] = fediAccount.match(
|
||||||
FediverseValidator.usernameAtServer
|
FediverseValidator.usernameAtServer,
|
||||||
)
|
)
|
||||||
|
|
||||||
const normalLink = new SvelteUIElement(Link, {
|
const normalLink = new SvelteUIElement(Link, {
|
||||||
|
@ -1354,10 +1359,10 @@ export default class SpecialVisualizations {
|
||||||
const loggedInContributorMastodon =
|
const loggedInContributorMastodon =
|
||||||
state.userRelatedState?.preferencesAsTags?.data?.[
|
state.userRelatedState?.preferencesAsTags?.data?.[
|
||||||
"_mastodon_link"
|
"_mastodon_link"
|
||||||
]
|
]
|
||||||
console.log(
|
console.log(
|
||||||
"LoggedinContributorMastodon",
|
"LoggedinContributorMastodon",
|
||||||
loggedInContributorMastodon
|
loggedInContributorMastodon,
|
||||||
)
|
)
|
||||||
if (!loggedInContributorMastodon) {
|
if (!loggedInContributorMastodon) {
|
||||||
return normalLink
|
return normalLink
|
||||||
|
@ -1373,7 +1378,7 @@ export default class SpecialVisualizations {
|
||||||
newTab: true,
|
newTab: true,
|
||||||
}).SetClass("button"),
|
}).SetClass("button"),
|
||||||
])
|
])
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1393,7 +1398,7 @@ export default class SpecialVisualizations {
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
args: string[],
|
args: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
return new FixedUiElement("{" + args[0] + "}")
|
return new FixedUiElement("{" + args[0] + "}")
|
||||||
},
|
},
|
||||||
|
@ -1414,7 +1419,7 @@ export default class SpecialVisualizations {
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
argument: string[],
|
argument: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
const key = argument[0] ?? "value"
|
const key = argument[0] ?? "value"
|
||||||
return new VariableUiElement(
|
return new VariableUiElement(
|
||||||
|
@ -1432,12 +1437,12 @@ export default class SpecialVisualizations {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return new FixedUiElement(
|
return new FixedUiElement(
|
||||||
"Could not parse this tag: " +
|
"Could not parse this tag: " +
|
||||||
JSON.stringify(value) +
|
JSON.stringify(value) +
|
||||||
" due to " +
|
" due to " +
|
||||||
e
|
e,
|
||||||
).SetClass("alert")
|
).SetClass("alert")
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1458,7 +1463,7 @@ export default class SpecialVisualizations {
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
argument: string[],
|
argument: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
const giggityUrl = argument[0]
|
const giggityUrl = argument[0]
|
||||||
return new SvelteUIElement(Giggity, { tags: tagSource, state, giggityUrl })
|
return new SvelteUIElement(Giggity, { tags: tagSource, state, giggityUrl })
|
||||||
|
@ -1474,12 +1479,12 @@ export default class SpecialVisualizations {
|
||||||
_: UIEventSource<Record<string, string>>,
|
_: UIEventSource<Record<string, string>>,
|
||||||
argument: string[],
|
argument: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
const tags = (<ThemeViewState>(
|
const tags = (<ThemeViewState>(
|
||||||
state
|
state
|
||||||
)).geolocation.currentUserLocation.features.map(
|
)).geolocation.currentUserLocation.features.map(
|
||||||
(features) => features[0]?.properties
|
(features) => features[0]?.properties,
|
||||||
)
|
)
|
||||||
return new Combine([
|
return new Combine([
|
||||||
new SvelteUIElement(OrientationDebugPanel, {}),
|
new SvelteUIElement(OrientationDebugPanel, {}),
|
||||||
|
@ -1501,7 +1506,7 @@ export default class SpecialVisualizations {
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
argument: string[],
|
argument: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
return new SvelteUIElement(MarkAsFavourite, {
|
return new SvelteUIElement(MarkAsFavourite, {
|
||||||
tags: tagSource,
|
tags: tagSource,
|
||||||
|
@ -1521,7 +1526,7 @@ export default class SpecialVisualizations {
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
argument: string[],
|
argument: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
return new SvelteUIElement(MarkAsFavouriteMini, {
|
return new SvelteUIElement(MarkAsFavouriteMini, {
|
||||||
tags: tagSource,
|
tags: tagSource,
|
||||||
|
@ -1541,7 +1546,7 @@ export default class SpecialVisualizations {
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
argument: string[],
|
argument: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
return new SvelteUIElement(DirectionIndicator, { state, feature })
|
return new SvelteUIElement(DirectionIndicator, { state, feature })
|
||||||
},
|
},
|
||||||
|
@ -1556,7 +1561,7 @@ export default class SpecialVisualizations {
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
argument: string[],
|
argument: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
return new VariableUiElement(
|
return new VariableUiElement(
|
||||||
tagSource
|
tagSource
|
||||||
|
@ -1578,9 +1583,9 @@ export default class SpecialVisualizations {
|
||||||
`${window.location.protocol}//${window.location.host}${window.location.pathname}?${layout}lat=${lat}&lon=${lon}&z=15` +
|
`${window.location.protocol}//${window.location.host}${window.location.pathname}?${layout}lat=${lat}&lon=${lon}&z=15` +
|
||||||
`#${id}`
|
`#${id}`
|
||||||
return new Img(new Qr(url).toImageElement(75)).SetStyle(
|
return new Img(new Qr(url).toImageElement(75)).SetStyle(
|
||||||
"width: 75px"
|
"width: 75px",
|
||||||
)
|
)
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1600,7 +1605,7 @@ export default class SpecialVisualizations {
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
args: string[],
|
args: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
const key = args[0] === "" ? "_direction:centerpoint" : args[0]
|
const key = args[0] === "" ? "_direction:centerpoint" : args[0]
|
||||||
return new VariableUiElement(
|
return new VariableUiElement(
|
||||||
|
@ -1611,11 +1616,11 @@ export default class SpecialVisualizations {
|
||||||
})
|
})
|
||||||
.mapD((value) => {
|
.mapD((value) => {
|
||||||
const dir = GeoOperations.bearingToHuman(
|
const dir = GeoOperations.bearingToHuman(
|
||||||
GeoOperations.parseBearing(value)
|
GeoOperations.parseBearing(value),
|
||||||
)
|
)
|
||||||
console.log("Human dir", dir)
|
console.log("Human dir", dir)
|
||||||
return Translations.t.general.visualFeedback.directionsAbsolute[dir]
|
return Translations.t.general.visualFeedback.directionsAbsolute[dir]
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1650,7 +1655,7 @@ export default class SpecialVisualizations {
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
args: string[],
|
args: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig,
|
||||||
): BaseUIElement {
|
): BaseUIElement {
|
||||||
const url = args[0]
|
const url = args[0]
|
||||||
const postprocessVelopark = args[2] === "velopark"
|
const postprocessVelopark = args[2] === "velopark"
|
||||||
|
@ -1666,6 +1671,17 @@ export default class SpecialVisualizations {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
funcName: "login_button",
|
||||||
|
args: [
|
||||||
|
],
|
||||||
|
docs: "Show a login button",
|
||||||
|
needsUrls: [],
|
||||||
|
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, args: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
|
||||||
|
return new Toggle(undefined,
|
||||||
|
new SvelteUIElement(LoginButton), state.osmConnection.isLoggedIn)
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
specialVisualizations.push(new AutoApplyButton(specialVisualizations))
|
specialVisualizations.push(new AutoApplyButton(specialVisualizations))
|
||||||
|
@ -1677,7 +1693,7 @@ export default class SpecialVisualizations {
|
||||||
throw (
|
throw (
|
||||||
"Invalid special visualisation found: funcName is undefined for " +
|
"Invalid special visualisation found: funcName is undefined for " +
|
||||||
invalid.map((sp) => sp.i).join(", ") +
|
invalid.map((sp) => sp.i).join(", ") +
|
||||||
'. Did you perhaps type \n funcName: "funcname" // type declaration uses COLON\ninstead of:\n funcName = "funcName" // value definition uses EQUAL'
|
". Did you perhaps type \n funcName: \"funcname\" // type declaration uses COLON\ninstead of:\n funcName = \"funcName\" // value definition uses EQUAL"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,6 +66,7 @@
|
||||||
import FilterPanel from "./BigComponents/FilterPanel.svelte"
|
import FilterPanel from "./BigComponents/FilterPanel.svelte"
|
||||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte"
|
import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte"
|
||||||
import { BBox } from "../Logic/BBox"
|
import { BBox } from "../Logic/BBox"
|
||||||
|
import ReviewsOverview from "./Reviews/ReviewsOverview.svelte"
|
||||||
|
|
||||||
export let state: ThemeViewState
|
export let state: ThemeViewState
|
||||||
let layout = state.layout
|
let layout = state.layout
|
||||||
|
@ -264,12 +265,15 @@
|
||||||
{#if state.lastClickObject.hasPresets || state.lastClickObject.hasNoteLayer}
|
{#if state.lastClickObject.hasPresets || state.lastClickObject.hasNoteLayer}
|
||||||
<button
|
<button
|
||||||
class="pointer-events-auto w-fit"
|
class="pointer-events-auto w-fit"
|
||||||
|
class:disabled={$currentZoom < Constants.minZoomLevelToAddNewPoint}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
state.openNewDialog()
|
state.openNewDialog()
|
||||||
}}
|
}}
|
||||||
on:keydown={forwardEventToMap}
|
on:keydown={forwardEventToMap}
|
||||||
>
|
>
|
||||||
{#if state.lastClickObject.hasPresets}
|
{#if $currentZoom < Constants.minZoomLevelToAddNewPoint}
|
||||||
|
<Tr t={Translations.t.general.add.zoomInFurther}/>
|
||||||
|
{:else if state.lastClickObject.hasPresets}
|
||||||
<Tr t={Translations.t.general.add.title} />
|
<Tr t={Translations.t.general.add.title} />
|
||||||
{:else}
|
{:else}
|
||||||
<Tr t={Translations.t.notes.addAComment} />
|
<Tr t={Translations.t.notes.addAComment} />
|
||||||
|
@ -355,14 +359,16 @@
|
||||||
|
|
||||||
<LoginToggle ignoreLoading={true} {state}>
|
<LoginToggle ignoreLoading={true} {state}>
|
||||||
{#if ($showCrosshair === "yes" && $currentZoom >= 17) || $showCrosshair === "always" || $visualFeedback}
|
{#if ($showCrosshair === "yes" && $currentZoom >= 17) || $showCrosshair === "always" || $visualFeedback}
|
||||||
|
<!-- Don't use h-full: h-full does _not_ include the area under the URL-bar, which offsets the crosshair a bit -->
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center justify-center"
|
class="pointer-events-none absolute top-0 left-0 flex w-full items-center justify-center"
|
||||||
|
style="height: 100vh"
|
||||||
>
|
>
|
||||||
<Cross class="h-4 w-4" />
|
<Cross class="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<!-- Add in an empty container to remove error messages if login fails -->
|
||||||
<svelte:fragment slot="error" />
|
<svelte:fragment slot="error" />
|
||||||
<!-- Add in an empty container to remove errors -->
|
|
||||||
</LoginToggle>
|
</LoginToggle>
|
||||||
|
|
||||||
<If condition={state.previewedImage.map((i) => i !== undefined)}>
|
<If condition={state.previewedImage.map((i) => i !== undefined)}>
|
||||||
|
@ -588,6 +594,10 @@
|
||||||
<Tr t={Translations.t.favouritePoi.title} />
|
<Tr t={Translations.t.favouritePoi.title} />
|
||||||
</h3>
|
</h3>
|
||||||
<Favourites {state} />
|
<Favourites {state} />
|
||||||
|
<h3>
|
||||||
|
<Tr t={Translations.t.reviews.your_reviews} />
|
||||||
|
</h3>
|
||||||
|
<ReviewsOverview {state}/>
|
||||||
</div>
|
</div>
|
||||||
</TabbedGroup>
|
</TabbedGroup>
|
||||||
</FloatOver>
|
</FloatOver>
|
||||||
|
|
Loading…
Reference in a new issue