diff --git a/assets/layers/bike_parking/bike_parking.json b/assets/layers/bike_parking/bike_parking.json index 6fe4ad904..f0e84bd01 100644 --- a/assets/layers/bike_parking/bike_parking.json +++ b/assets/layers/bike_parking/bike_parking.json @@ -64,7 +64,8 @@ "iconSize": "40,40", "location": [ "point", - "centroid" + "projected_centerpoint", + "polygon_centroid" ], "anchor": "bottom", "marker": [ diff --git a/assets/layers/icons/icons.json b/assets/layers/icons/icons.json index 6219db9e5..fb302d55c 100644 --- a/assets/layers/icons/icons.json +++ b/assets/layers/icons/icons.json @@ -204,6 +204,35 @@ } ] }, + { + "id": "dogicon", + "labels": [ + "defaults", + "in_favourite" + ], + "mappings": [ + { + "if": "dog=no", + "#": "ignore-image-in-then", + "then": "no_dogs" + }, + { + "if": "dog=leashed", + "#": "ignore-image-in-then", + "then": "dogs are allowed but leashed" + }, + { + "if": { + "or": [ + "dog=yes", + "dog=unleashed" + ] + }, + "#": "ignore-image-in-then", + "then": "dogs are allowed" + } + ] + }, { "id": "sharelink", "labels": [ @@ -263,35 +292,6 @@ ], "condition": "id~(node|way|relation)/[0-9]*" }, - { - "id": "dogicon", - "labels": [ - "defaults", - "in_favourite" - ], - "mappings": [ - { - "if": "dog=no", - "#": "ignore-image-in-then", - "then": "no_dogs" - }, - { - "if": "dog=leashed", - "#": "ignore-image-in-then", - "then": "dogs are allowed but leashed" - }, - { - "if": { - "or": [ - "dog=yes", - "dog=unleashed" - ] - }, - "#": "ignore-image-in-then", - "then": "dogs are allowed" - } - ] - }, { "id": "rating", "icon": { diff --git a/assets/layers/toilet/toilet.json b/assets/layers/toilet/toilet.json index 4e528c901..192a60ab7 100644 --- a/assets/layers/toilet/toilet.json +++ b/assets/layers/toilet/toilet.json @@ -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", "labels": [ diff --git a/assets/themes/velopark/velopark.json b/assets/themes/velopark/velopark.json index bd94f04f3..b7130c169 100644 --- a/assets/themes/velopark/velopark.json +++ b/assets/themes/velopark/velopark.json @@ -11,6 +11,9 @@ "en": "

Velopark.be 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.

How to use: That's it! Thanks for helping to import this!", "nl": "

Velopark.be 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.

Hoe te gebruiken? Dat is het! Bedankt om mee te helpen!" }, + "descriptionTail": { + "*": "

Maintainer tools

" + }, "hideFromOverview": true, "icon": "./assets/themes/velopark/velopark.svg", "mustHaveLanguage": [ @@ -30,6 +33,7 @@ "startLon": 3.71025, "startZoom": 18, "defaultBackgroundId": "photo", + "enableNoteImports": false, "layers": [ { "id": "velopark_maproulette", @@ -94,6 +98,14 @@ } } }, + { + "id": "login", + "render": { + "special": { + "type": "login_button" + } + } + }, { "id": "closest_parkings", "render": { @@ -137,7 +149,7 @@ "type": "maproulette_set_status", "message": { "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 } @@ -150,10 +162,15 @@ "type": "maproulette_set_status", "message": { "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", - "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": [ { - "color": "#0088ff" + "color": "#2cf200" } ] } diff --git a/langs/en.json b/langs/en.json index b2a5760c3..213f7fff7 100644 --- a/langs/en.json +++ b/langs/en.json @@ -671,6 +671,7 @@ "reviewPlaceholder": "Describe your experience…", "reviewing_as": "Reviewing as {nickname}", "reviewing_as_anonymous": "Reviewing as anonymous", + "reviews_bug": "Expected more reviews? Some reviews are not displayed due to a bug.", "save": "Save review", "saved": "Review saved. Thanks for sharing!", "saving_review": "Saving…", @@ -678,7 +679,9 @@ "title_singular": "One review", "too_long": "At most {max} characters are allowed. Your review has {amount} characters.", "tos": "If you create a review, you agree to the TOS and privacy policy of Mangrove.reviews", - "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": { "cancel": "Cancel", diff --git a/scripts/velopark/compare.ts b/scripts/velopark/compare.ts new file mode 100644 index 000000000..4bab31dc4 --- /dev/null +++ b/scripts/velopark/compare.ts @@ -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(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 { + 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 = {} + 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() diff --git a/scripts/velopark/veloParkToGeojson.ts b/scripts/velopark/veloParkToGeojson.ts index 0157e9b1f..dcd200a32 100644 --- a/scripts/velopark/veloParkToGeojson.ts +++ b/scripts/velopark/veloParkToGeojson.ts @@ -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 { console.log("Downloading velopark data") // 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") 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 allProperties = new Set() for (const feature of features) { Object.keys(feature.properties).forEach((k) => allProperties.add(k)) } + this.exportTo("velopark_noncynced",features) allProperties.delete("ref:velopark") for (const feature of features) { allProperties.forEach((k) => { @@ -51,17 +68,7 @@ class VeloParkToGeojson extends Script { }) } - fs.writeFileSync( - "velopark_id_only_export_" + new Date().toISOString() + ".geojson", - JSON.stringify( - { - type: "FeatureCollection", - features, - }, - null, - " " - ) - ) + this.exportTo("velopark_nonsynced_id_only", features) } } diff --git a/src/Logic/GeoOperations.ts b/src/Logic/GeoOperations.ts index 571a1c400..44d65b2ea 100644 --- a/src/Logic/GeoOperations.ts +++ b/src/Logic/GeoOperations.ts @@ -156,7 +156,7 @@ export class GeoOperations { const intersection = GeoOperations.calculateIntersection( feature, otherFeature, - featureBBox + featureBBox, ) if (intersection === null) { continue @@ -195,7 +195,7 @@ export class GeoOperations { console.error( "Could not correctly calculate the overlap of ", feature, - ": unsupported type" + ": unsupported type", ) return result } @@ -224,7 +224,7 @@ export class GeoOperations { */ public static inside( pointCoordinate: [number, number] | Feature, - feature: Feature + feature: Feature, ): boolean { // ray-casting algorithm based on // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html @@ -302,7 +302,7 @@ export class GeoOperations { */ public static nearestPoint( way: Feature, - point: [number, number] + point: [number, number], ): Feature< Point, { @@ -324,11 +324,11 @@ export class GeoOperations { public static forceLineString(way: Feature): Feature public static forceLineString( - way: Feature + way: Feature, ): Feature public static forceLineString( - way: Feature + way: Feature, ): Feature { if (way.geometry.type === "Polygon") { way = { ...way } @@ -352,8 +352,8 @@ export class GeoOperations { const headerValuesOrdered: string[] = [] function addH(key: string) { - if(options?.ignoreTags){ - if(key.match(options.ignoreTags)){ + if (options?.ignoreTags) { + if (key.match(options.ignoreTags)) { return } } @@ -455,7 +455,7 @@ export class GeoOperations { */ public static LineIntersections( feature: Feature, - otherFeature: Feature + otherFeature: Feature, ): [number, number][] { return turf .lineIntersect(feature, otherFeature) @@ -492,7 +492,7 @@ export class GeoOperations { locations: | Feature | Feature[], - title?: string + title?: string, ) { title = title?.trim() if (title === undefined || title === "") { @@ -513,7 +513,7 @@ export class GeoOperations { type: "Point", coordinates: p, }, - } + }, ) } for (const l of locationsWithMeta) { @@ -528,7 +528,7 @@ export class GeoOperations { trackPoints.push(trkpt) } const header = - '' + "" return ( header + "\n" + @@ -546,7 +546,7 @@ export class GeoOperations { */ public static toGpxPoints( locations: Feature[], - title?: string + title?: string, ) { title = title?.trim() if (title === undefined || title === "") { @@ -567,7 +567,7 @@ export class GeoOperations { trackPoints.push(trkpt) } const header = - '' + "" return ( header + "\n" + @@ -655,7 +655,7 @@ export class GeoOperations { }, }, distanceMeter, - { units: "meters" } + { units: "meters" }, ).geometry.coordinates } @@ -690,7 +690,7 @@ export class GeoOperations { */ static completelyWithin( feature: Feature, - possiblyEnclosingFeature: Feature + possiblyEnclosingFeature: Feature, ): boolean { return booleanWithin(feature, possiblyEnclosingFeature) } @@ -746,7 +746,7 @@ export class GeoOperations { */ public static featureToCoordinateWithRenderingType( feature: Feature, - location: "point" | "centroid" | "start" | "end" | "projected_centerpoint" | string + location: "point" | "centroid" | "start" | "end" | "projected_centerpoint" | "polygon_centerpoint" | string, ): [number, number] | undefined { switch (location) { case "point": @@ -759,6 +759,11 @@ export class GeoOperations { return undefined } return GeoOperations.centerpointCoordinates(feature) + case "polygon_centerpoint": + if (feature.geometry.type === "Polygon") { + return GeoOperations.centerpointCoordinates(feature) + } + return undefined case "projected_centerpoint": if ( feature.geometry.type === "LineString" || @@ -767,7 +772,7 @@ export class GeoOperations { const centerpoint = GeoOperations.centerpointCoordinates(feature) const projected = GeoOperations.nearestPoint( >feature, - centerpoint + centerpoint, ) return <[number, number]>projected.geometry.coordinates } @@ -944,7 +949,7 @@ export class GeoOperations { * GeoOperations.bearingToHuman(46) // => "NE" */ public static bearingToHuman( - bearing: number + bearing: number, ): "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW" { while (bearing < 0) { bearing += 360 @@ -970,7 +975,7 @@ export class GeoOperations { * */ public static bearingToHumanRelative( - bearing: number + bearing: number, ): | "straight" | "slight_right" @@ -995,12 +1000,12 @@ export class GeoOperations { private static pointInPolygonCoordinates( x: number, y: number, - coordinates: [number, number][][] + coordinates: [number, number][][], ): boolean { const inside = GeoOperations.pointWithinRing( x, y, - /*This is the outer ring of the polygon */ coordinates[0] + /*This is the outer ring of the polygon */ coordinates[0], ) if (!inside) { return false @@ -1009,7 +1014,7 @@ export class GeoOperations { const inHole = GeoOperations.pointWithinRing( x, y, - coordinates[i] /* These are inner rings, aka holes*/ + coordinates[i], /* These are inner rings, aka holes*/ ) if (inHole) { return false @@ -1047,7 +1052,7 @@ export class GeoOperations { feature, otherFeature, featureBBox: BBox, - otherFeatureBBox?: BBox + otherFeatureBBox?: BBox, ): number { if (feature.geometry.type === "LineString") { otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature) @@ -1096,7 +1101,7 @@ export class GeoOperations { let intersection = turf.lineSlice( turf.point(intersectionPointsArray[0]), turf.point(intersectionPointsArray[1]), - feature + feature, ) if (intersection == null) { @@ -1117,7 +1122,7 @@ export class GeoOperations { otherFeature, feature, otherFeatureBBox, - featureBBox + featureBBox, ) } @@ -1137,7 +1142,7 @@ export class GeoOperations { console.log("Applying fallback intersection...") const intersection = turf.intersect( turf.truncate(feature), - turf.truncate(otherFeature) + turf.truncate(otherFeature), ) if (intersection == null) { return null diff --git a/src/Logic/State/UserRelatedState.ts b/src/Logic/State/UserRelatedState.ts index 5cfe3416d..307f5e104 100644 --- a/src/Logic/State/UserRelatedState.ts +++ b/src/Logic/State/UserRelatedState.ts @@ -73,7 +73,6 @@ export default class UserRelatedState { constructor( osmConnection: OsmConnection, - availableLanguages?: string[], layout?: LayoutConfig, featureSwitches?: FeatureSwitchState, mapProperties?: MapProperties @@ -365,6 +364,11 @@ export default class UserRelatedState { [translationMode] ) + this.mangroveIdentity.getKeyId().addCallbackAndRun(kid => { + amendedPrefs.data["mangrove_kid"] = kid + amendedPrefs.ping() + }) + const usersettingMetaTagging = new ThemeMetaTagging() osmConnection.userDetails.addCallback((userDetails) => { for (const k in userDetails) { diff --git a/src/Logic/UIEventSource.ts b/src/Logic/UIEventSource.ts index ed48130ff..70aa5da87 100644 --- a/src/Logic/UIEventSource.ts +++ b/src/Logic/UIEventSource.ts @@ -306,7 +306,8 @@ export abstract class Store implements Readable { export class ImmutableStore extends Store { public readonly data: T - + static FALSE = new ImmutableStore(false) + static TRUE = new ImmutableStore(true) constructor(data: T) { super() this.data = data diff --git a/src/Logic/Web/MangroveReviews.ts b/src/Logic/Web/MangroveReviews.ts index 3aa9582f6..426257bac 100644 --- a/src/Logic/Web/MangroveReviews.ts +++ b/src/Logic/Web/MangroveReviews.ts @@ -5,10 +5,12 @@ import { Feature, Position } from "geojson" import { GeoOperations } from "../GeoOperations" export class MangroveIdentity { - public readonly keypair: Store - public readonly key_id: Store + private readonly keypair: Store + private readonly mangroveIdentity: UIEventSource + private readonly key_id: Store constructor(mangroveIdentity: UIEventSource) { + this.mangroveIdentity = mangroveIdentity const key_id = new UIEventSource(undefined) this.key_id = key_id const keypairEventSource = new UIEventSource(undefined) @@ -23,13 +25,7 @@ export class MangroveIdentity { 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 return } + console.log("Creating a new Mangrove identity!") identity.setData(JSON.stringify(jwk)) } + + /** + * Only called to create a review. + */ + async getKeypair(): Promise { + 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 { + 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 */ public async createReview(review: Omit): Promise { - 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" } const r: Review = { sub: this.subjectUri.data, ...review, } - const keypair: CryptoKeyPair = this._identity.keypair.data + const keypair: CryptoKeyPair = await this._identity.getKeypair() const jwt = await MangroveReviews.signReview(keypair, r) const kid = await MangroveReviews.publicToPem(keypair.publicKey) await MangroveReviews.submitReview(jwt) - this._reviews.data.push({ + const reviewWithKid = { ...r, kid, signature: jwt, madeByLoggedInUser: new ImmutableStore(true), - }) + } + this._reviews.data.push( reviewWithKid) this._reviews.ping() + this._identity.addReview(reviewWithKid) } + + /** * Adds given reviews to the 'reviews'-UI-eventsource * @param reviews @@ -235,7 +288,7 @@ export default class FeatureReviews { ...review, kid: reviewData.kid, 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 }), }) diff --git a/src/Models/ThemeConfig/Json/PointRenderingConfigJson.ts b/src/Models/ThemeConfig/Json/PointRenderingConfigJson.ts index 96e94481b..9d385eb94 100644 --- a/src/Models/ThemeConfig/Json/PointRenderingConfigJson.ts +++ b/src/Models/ThemeConfig/Json/PointRenderingConfigJson.ts @@ -28,9 +28,9 @@ export default interface PointRenderingConfigJson { /** * question: At what location should this icon be shown? * 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. diff --git a/src/Models/ThemeConfig/PointRenderingConfig.ts b/src/Models/ThemeConfig/PointRenderingConfig.ts index 4f83499c5..8f13f6f64 100644 --- a/src/Models/ThemeConfig/PointRenderingConfig.ts +++ b/src/Models/ThemeConfig/PointRenderingConfig.ts @@ -38,9 +38,10 @@ export default class PointRenderingConfig extends WithContextLoader { "start", "end", "projected_centerpoint", + "polygon_centroid" ]) public readonly location: Set< - "point" | "centroid" | "start" | "end" | "projected_centerpoint" | string + "point" | "centroid" | "start" | "end" | "projected_centerpoint" | "polygon_centroid" | string > public readonly marker: IconConfig[] diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 4895ee1aa..f5c576281 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -171,7 +171,6 @@ export default class ThemeViewState implements SpecialVisualizationState { }) this.userRelatedState = new UserRelatedState( this.osmConnection, - layout?.language, layout, this.featureSwitches, this.mapProperties diff --git a/src/Sensors/Orientation.ts b/src/Sensors/Orientation.ts index ca71cf2b5..e9d28ffd3 100644 --- a/src/Sensors/Orientation.ts +++ b/src/Sensors/Orientation.ts @@ -77,6 +77,9 @@ export class Orientation { } private update(event: DeviceOrientationEvent) { + if(event.alpha === null || event.beta === null || event.gamma === null){ + return + } this.gotMeasurement.setData(true) // IF the phone is lying flat, then: // alpha is the compass direction (but not absolute) diff --git a/src/UI/Favourites/FavouriteSummary.svelte b/src/UI/Favourites/FavouriteSummary.svelte index 4a2bd7fb7..a505419f4 100644 --- a/src/UI/Favourites/FavouriteSummary.svelte +++ b/src/UI/Favourites/FavouriteSummary.svelte @@ -29,7 +29,7 @@ center() } - const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"] + let titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"] {#if favLayer !== undefined} diff --git a/src/UI/Favourites/Favourites.svelte b/src/UI/Favourites/Favourites.svelte index 3996e6eb3..406e216d0 100644 --- a/src/UI/Favourites/Favourites.svelte +++ b/src/UI/Favourites/Favourites.svelte @@ -48,7 +48,7 @@
console.log("Got keypress", e)}> - + {#each $favourites as feature (feature.properties.id)} diff --git a/src/UI/MapRoulette/MaprouletteSetStatus.svelte b/src/UI/MapRoulette/MaprouletteSetStatus.svelte index 186298719..c13474fd3 100644 --- a/src/UI/MapRoulette/MaprouletteSetStatus.svelte +++ b/src/UI/MapRoulette/MaprouletteSetStatus.svelte @@ -1,63 +1,82 @@ -{#if failed} -
ERROR - could not close the MapRoulette task
-{:else if applying} - - - -{:else if $status === Maproulette.STATUS_OPEN} - -{:else} - {message_closed} -{/if} + + {#if failed} +
ERROR - could not close the MapRoulette task
+ {:else if applying} + + + + {:else if $status === Maproulette.STATUS_OPEN} + {#if askFeedback !== "" && askFeedback !== undefined} +
+

{askFeedback}

+ + + {feedback} +
+ {:else} + + {/if} + {:else} + {message_closed} + {/if} +
diff --git a/src/UI/Reviews/ReviewForm.svelte b/src/UI/Reviews/ReviewForm.svelte index d6f06e40a..f37722d89 100644 --- a/src/UI/Reviews/ReviewForm.svelte +++ b/src/UI/Reviews/ReviewForm.svelte @@ -35,9 +35,9 @@ let _state: "ask" | "saving" | "done" = "ask" - const connection = state.osmConnection + let connection = state.osmConnection - const hasError: Store = opinion.mapD(op => { + let hasError: Store = opinion.mapD(op => { const tooLong = op.length > FeatureReviews.REVIEW_OPINION_MAX_LENGTH if (tooLong) { return "too_long" @@ -45,6 +45,8 @@ return undefined }) + let uploadFailed: string = undefined + async function save() { if (hasError.data) { return @@ -63,13 +65,24 @@ console.log("Testing - not actually saving review", review) await Utils.waitFor(1000) } 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" } - -{#if _state === "done"} +{#if uploadFailed} +
+ + + {uploadFailed} +
+{:else if _state === "done"} {:else if _state === "saving"} @@ -109,8 +122,9 @@ /> {#if $hasError === "too_long"}
- - + +
{/if} diff --git a/src/UI/Reviews/ReviewsOverview.svelte b/src/UI/Reviews/ReviewsOverview.svelte new file mode 100644 index 000000000..bd1de598b --- /dev/null +++ b/src/UI/Reviews/ReviewsOverview.svelte @@ -0,0 +1,40 @@ + + + +
+ + + +
+ + + {#if $reviews?.length > 0} +
console.log("Got keypress", e)}> + {#each $reviews as review (review.sub)} + + {/each} +
+ {:else} + + {/if} + +
+ + +
+
diff --git a/src/UI/Reviews/SingleReview.svelte b/src/UI/Reviews/SingleReview.svelte index 104a58fef..4e66a2edd 100644 --- a/src/UI/Reviews/SingleReview.svelte +++ b/src/UI/Reviews/SingleReview.svelte @@ -1,22 +1,43 @@ -
-
+
+ {#if showSub} + + {/if} +
>, args: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): BaseUIElement { const isOpen = args[0] === "open" const readonly = args[1] === "readonly" @@ -183,7 +181,7 @@ class StealViz implements SpecialVisualization { selectedElement: otherFeature, state, layer, - }) + }), ) } if (elements.length === 1) { @@ -191,8 +189,8 @@ class StealViz implements SpecialVisualization { } 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>, args: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): BaseUIElement { const labels = args[0] ?.split(";") @@ -267,37 +265,38 @@ export default class SpecialVisualizations { viz.docs, viz.args.length > 0 ? new Table( - ["name", "default", "description"], - viz.args.map((arg) => { - let defaultArg = arg.defaultValue ?? "_undefined_" - if (defaultArg == "") { - defaultArg = "_empty string_" - } - return [arg.name, defaultArg, arg.doc] - }) - ) + ["name", "default", "description"], + viz.args.map((arg) => { + let defaultArg = arg.defaultValue ?? "_undefined_" + if (defaultArg == "") { + defaultArg = "_empty string_" + } + return [arg.name, defaultArg, arg.doc] + }), + ) : undefined, new Title("Example usage of " + viz.funcName, 4), new FixedUiElement( viz.example ?? - "`{" + - viz.funcName + - "(" + - viz.args.map((arg) => arg.defaultValue).join(",") + - ")}`" + "`{" + + viz.funcName + + "(" + + viz.args.map((arg) => arg.defaultValue).join(",") + + ")}`", ).SetClass("literal-code"), ]) } public static constructSpecification( template: string, - extraMappings: SpecialVisualization[] = [] + extraMappings: SpecialVisualization[] = [], ): RenderingSpecification[] { return SpecialVisualisationUtils.constructSpecification(template, extraMappings) } + public static HelpMessage() { const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) => - SpecialVisualizations.DocumentationFor(viz) + SpecialVisualizations.DocumentationFor(viz), ) return new Combine([ @@ -331,10 +330,10 @@ export default class SpecialVisualizations { }, }, null, - " " - ) + " ", + ), ).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"), ...helpTexts, ]).SetClass("flex flex-col") @@ -343,20 +342,20 @@ export default class SpecialVisualizations { // noinspection JSUnusedGlobalSymbols public static renderExampleOfSpecial( state: SpecialVisualizationState, - s: SpecialVisualization + s: SpecialVisualization, ): BaseUIElement { const examples = s.structuredExamples === undefined ? [] : s.structuredExamples().map((e) => { - return s.constr( - state, - new UIEventSource>(e.feature.properties), - e.args, - e.feature, - undefined - ) - }) + return s.constr( + state, + new UIEventSource>(e.feature.properties), + e.args, + e.feature, + undefined, + ) + }) return new Combine([new Title(s.funcName), s.docs, ...examples]) } @@ -396,7 +395,7 @@ export default class SpecialVisualizations { assignTo: state.userRelatedState.language, availableLanguages: state.layout.language, preferredLanguages: state.osmConnection.userDetails.map( - (ud) => ud.languages + (ud) => ud.languages, ), }) }, @@ -421,7 +420,7 @@ export default class SpecialVisualizations { constr( state: SpecialVisualizationState, - tagSource: UIEventSource> + tagSource: UIEventSource>, ): BaseUIElement { return new VariableUiElement( tagSource @@ -431,7 +430,7 @@ export default class SpecialVisualizations { return new SplitRoadWizard(id, state) } return undefined - }) + }), ) }, }, @@ -445,7 +444,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): BaseUIElement { if (feature.geometry.type !== "Point") { return undefined @@ -468,7 +467,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): BaseUIElement { if (!layer.deletion) { return undefined @@ -496,7 +495,7 @@ export default class SpecialVisualizations { state: SpecialVisualizationState, tagSource: UIEventSource>, argument: string[], - feature: Feature + feature: Feature, ): BaseUIElement { const [lon, lat] = GeoOperations.centerpointCoordinates(feature) return new SvelteUIElement(CreateNewNote, { @@ -560,7 +559,7 @@ export default class SpecialVisualizations { .map((tags) => tags[args[0]]) .map((wikidata) => { wikidata = Utils.NoEmpty( - wikidata?.split(";")?.map((wd) => wd.trim()) ?? [] + wikidata?.split(";")?.map((wd) => wd.trim()) ?? [], )[0] const entry = Wikidata.LoadWikidataEntry(wikidata) return new VariableUiElement( @@ -570,9 +569,9 @@ export default class SpecialVisualizations { } const response = e["success"] return Translation.fromMap(response.labels) - }) + }), ) - }) + }), ), }, new MapillaryLinkVis(), @@ -586,7 +585,7 @@ export default class SpecialVisualizations { tags: UIEventSource>, _, __, - layer: LayerConfig + layer: LayerConfig, ) => new SvelteUIElement(AllTagsPanel, { tags, layer }), }, { @@ -608,7 +607,7 @@ export default class SpecialVisualizations { return new ImageCarousel( AllImageProviders.LoadImagesFor(tags, imagePrefixes), tags, - state + state, ) }, }, @@ -664,7 +663,7 @@ export default class SpecialVisualizations { { nameKey: nameKey, fallbackName, - } + }, ) return new SvelteUIElement(StarsBarIcon, { score: reviews.average, @@ -697,7 +696,7 @@ export default class SpecialVisualizations { { nameKey: nameKey, fallbackName, - } + }, ) return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer }) }, @@ -729,7 +728,7 @@ export default class SpecialVisualizations { { nameKey: nameKey, fallbackName, - } + }, ) return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer }) }, @@ -787,7 +786,7 @@ export default class SpecialVisualizations { tags: UIEventSource>, args: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): SvelteUIElement { const keyToUse = args[0] const prefix = args[1] @@ -824,17 +823,17 @@ export default class SpecialVisualizations { return undefined } const allUnits: Unit[] = [].concat( - ...(state?.layout?.layers?.map((lyr) => lyr.units) ?? []) + ...(state?.layout?.layers?.map((lyr) => lyr.units) ?? []), ) const unit = allUnits.filter((unit) => - unit.isApplicableToKey(key) + unit.isApplicableToKey(key), )[0] if (unit === undefined) { return value } const getCountry = () => tagSource.data._country return unit.asHumanLongValue(value, getCountry) - }) + }), ) }, }, @@ -851,7 +850,7 @@ export default class SpecialVisualizations { new Combine([ t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"), t.downloadGeoJsonHelper.SetClass("subtle"), - ]).SetClass("flex flex-col") + ]).SetClass("flex flex-col"), ) .onClick(() => { console.log("Exporting as Geojson") @@ -864,7 +863,7 @@ export default class SpecialVisualizations { title + "_mapcomplete_export.geojson", { mimetype: "application/vnd.geo+json", - } + }, ) }) .SetClass("w-full") @@ -900,7 +899,7 @@ export default class SpecialVisualizations { constr: (state) => { return new SubtleButton( Svg.delete_icon_svg().SetStyle("height: 1.5rem"), - Translations.t.general.removeLocationHistory + Translations.t.general.removeLocationHistory, ).onClick(() => { state.historicalUserLocations.features.setData([]) state.selectedElement.setData(undefined) @@ -938,10 +937,10 @@ export default class SpecialVisualizations { .filter((c) => c.text !== "") .map( (c, i) => - new NoteCommentElement(c, state, i, comments.length) - ) + new NoteCommentElement(c, state, i, comments.length), + ), ).SetClass("flex flex-col") - }) + }), ), }, { @@ -975,7 +974,7 @@ export default class SpecialVisualizations { tagsSource: UIEventSource>, _: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ) => new VariableUiElement( tagsSource.map((tags) => { @@ -993,7 +992,7 @@ export default class SpecialVisualizations { feature, layer, }).SetClass("px-1") - }) + }), ), }, { @@ -1009,8 +1008,8 @@ export default class SpecialVisualizations { let challenge = Stores.FromPromise( Utils.downloadJsonCached( `${Maproulette.defaultEndpoint}/challenge/${parentId}`, - 24 * 60 * 60 * 1000 - ) + 24 * 60 * 60 * 1000, + ), ) return new VariableUiElement( @@ -1035,7 +1034,7 @@ export default class SpecialVisualizations { } else { 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.", @@ -1049,15 +1048,15 @@ export default class SpecialVisualizations { "\n" + "```json\n" + "{\n" + - ' "id": "mark_duplicate",\n' + - ' "render": {\n' + - ' "special": {\n' + - ' "type": "maproulette_set_status",\n' + - ' "message": {\n' + - ' "en": "Mark as not found or false positive"\n' + + " \"id\": \"mark_duplicate\",\n" + + " \"render\": {\n" + + " \"special\": {\n" + + " \"type\": \"maproulette_set_status\",\n" + + " \"message\": {\n" + + " \"en\": \"Mark as not found or false positive\"\n" + " },\n" + - ' "status": "2",\n' + - ' "image": "close"\n' + + " \"status\": \"2\",\n" + + " \"image\": \"close\"\n" + " }\n" + " }\n" + "}\n" + @@ -1086,10 +1085,15 @@ export default class SpecialVisualizations { doc: "The property name containing the maproulette id", 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) => { - let [message, image, message_closed, statusToSet, maproulette_id_key] = args + let [message, image, message_closed, statusToSet, maproulette_id_key, askFeedback] = args if (image === "") { image = "confirm" } @@ -1105,6 +1109,7 @@ export default class SpecialVisualizations { message_closed, statusToSet, maproulette_id_key, + askFeedback }) }, }, @@ -1124,8 +1129,8 @@ export default class SpecialVisualizations { const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox) return new StatisticsPanel(fsBboxed) }, - [state.mapProperties.bounds] - ) + [state.mapProperties.bounds], + ), ) }, }, @@ -1191,7 +1196,7 @@ export default class SpecialVisualizations { constr( state: SpecialVisualizationState, tagSource: UIEventSource>, - args: string[] + args: string[], ): BaseUIElement { let [text, href, classnames, download, ariaLabel] = args if (download === "") { @@ -1203,13 +1208,13 @@ export default class SpecialVisualizations { (tags) => new SvelteUIElement(Link, { 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, download: Utils.SubstituteKeys(download, tags), ariaLabel: Utils.SubstituteKeys(ariaLabel, tags), newTab, - }) - ) + }), + ), ) }, }, @@ -1231,7 +1236,7 @@ export default class SpecialVisualizations { }, }, null, - " " + " ", ) + "\n```", args: [ @@ -1255,7 +1260,7 @@ export default class SpecialVisualizations { featureTags: UIEventSource>, args: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ) { const [key, tr, classesRaw] = args let classes = classesRaw ?? "" @@ -1280,7 +1285,7 @@ export default class SpecialVisualizations { elements.push(subsTr) } return elements - }) + }), ) }, }, @@ -1300,7 +1305,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): BaseUIElement { return new VariableUiElement( tagSource.map((tags) => { @@ -1312,7 +1317,7 @@ export default class SpecialVisualizations { console.error("Cannot create a translation for", v, "due to", e) return JSON.stringify(v) } - }) + }), ) }, }, @@ -1332,7 +1337,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): BaseUIElement { const key = argument[0] const validator = new FediverseValidator() @@ -1342,7 +1347,7 @@ export default class SpecialVisualizations { .map((fediAccount) => { fediAccount = validator.reformat(fediAccount) const [_, username, host] = fediAccount.match( - FediverseValidator.usernameAtServer + FediverseValidator.usernameAtServer, ) const normalLink = new SvelteUIElement(Link, { @@ -1354,10 +1359,10 @@ export default class SpecialVisualizations { const loggedInContributorMastodon = state.userRelatedState?.preferencesAsTags?.data?.[ "_mastodon_link" - ] + ] console.log( "LoggedinContributorMastodon", - loggedInContributorMastodon + loggedInContributorMastodon, ) if (!loggedInContributorMastodon) { return normalLink @@ -1373,7 +1378,7 @@ export default class SpecialVisualizations { newTab: true, }).SetClass("button"), ]) - }) + }), ) }, }, @@ -1393,7 +1398,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, args: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): BaseUIElement { return new FixedUiElement("{" + args[0] + "}") }, @@ -1414,7 +1419,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): BaseUIElement { const key = argument[0] ?? "value" return new VariableUiElement( @@ -1432,12 +1437,12 @@ export default class SpecialVisualizations { } catch (e) { return new FixedUiElement( "Could not parse this tag: " + - JSON.stringify(value) + - " due to " + - e + JSON.stringify(value) + + " due to " + + e, ).SetClass("alert") } - }) + }), ) }, }, @@ -1458,7 +1463,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): BaseUIElement { const giggityUrl = argument[0] return new SvelteUIElement(Giggity, { tags: tagSource, state, giggityUrl }) @@ -1474,12 +1479,12 @@ export default class SpecialVisualizations { _: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): BaseUIElement { const tags = (( state )).geolocation.currentUserLocation.features.map( - (features) => features[0]?.properties + (features) => features[0]?.properties, ) return new Combine([ new SvelteUIElement(OrientationDebugPanel, {}), @@ -1501,7 +1506,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): BaseUIElement { return new SvelteUIElement(MarkAsFavourite, { tags: tagSource, @@ -1521,7 +1526,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): BaseUIElement { return new SvelteUIElement(MarkAsFavouriteMini, { tags: tagSource, @@ -1541,7 +1546,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): BaseUIElement { return new SvelteUIElement(DirectionIndicator, { state, feature }) }, @@ -1556,7 +1561,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, argument: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): BaseUIElement { return new VariableUiElement( 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` + `#${id}` return new Img(new Qr(url).toImageElement(75)).SetStyle( - "width: 75px" + "width: 75px", ) - }) + }), ) }, }, @@ -1600,7 +1605,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, args: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): BaseUIElement { const key = args[0] === "" ? "_direction:centerpoint" : args[0] return new VariableUiElement( @@ -1611,11 +1616,11 @@ export default class SpecialVisualizations { }) .mapD((value) => { const dir = GeoOperations.bearingToHuman( - GeoOperations.parseBearing(value) + GeoOperations.parseBearing(value), ) console.log("Human dir", dir) return Translations.t.general.visualFeedback.directionsAbsolute[dir] - }) + }), ) }, }, @@ -1650,7 +1655,7 @@ export default class SpecialVisualizations { tagSource: UIEventSource>, args: string[], feature: Feature, - layer: LayerConfig + layer: LayerConfig, ): BaseUIElement { const url = args[0] 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>, args: string[], feature: Feature, layer: LayerConfig): BaseUIElement { + return new Toggle(undefined, + new SvelteUIElement(LoginButton), state.osmConnection.isLoggedIn) + }, + }, ] specialVisualizations.push(new AutoApplyButton(specialVisualizations)) @@ -1677,7 +1693,7 @@ export default class SpecialVisualizations { throw ( "Invalid special visualisation found: funcName is undefined for " + 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" ) } diff --git a/src/UI/ThemeViewGUI.svelte b/src/UI/ThemeViewGUI.svelte index 4941975b4..70c229c75 100644 --- a/src/UI/ThemeViewGUI.svelte +++ b/src/UI/ThemeViewGUI.svelte @@ -66,6 +66,7 @@ import FilterPanel from "./BigComponents/FilterPanel.svelte" import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte" import { BBox } from "../Logic/BBox" + import ReviewsOverview from "./Reviews/ReviewsOverview.svelte" export let state: ThemeViewState let layout = state.layout @@ -264,12 +265,15 @@ {#if state.lastClickObject.hasPresets || state.lastClickObject.hasNoteLayer}