forked from MapComplete/MapComplete
		
	Furhter improvements to velopark: better icons, improvements to loading
This commit is contained in:
		
							parent
							
								
									5a48a2e19c
								
							
						
					
					
						commit
						8bcc8820ac
					
				
					 10 changed files with 560 additions and 381 deletions
				
			
		|  | @ -25,135 +25,186 @@ | ||||||
|   "defaultBackgroundId": "photo", |   "defaultBackgroundId": "photo", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     { |     { | ||||||
|       "builtin": "maproulette_challenge", |       "id": "velopark_maproulette", | ||||||
|       "override": { |       "description": "Maproulette challenge containing velopark data", | ||||||
|         "=name": { |       "source": { | ||||||
|           "en": "Velopark data", |         "osmTags": "mr_taskId~*", | ||||||
|           "nl": "Velopark data" |         "geoJson": "https://maproulette.org/api/v2/challenge/view/43282", | ||||||
|         }, |         "isOsmCache": false | ||||||
|         "=filter": [ |       }, | ||||||
|           { |       "title": { | ||||||
|             "id": "created-only", |         "render": "Velopark parking <b>{mr_velopark_id}</b>" | ||||||
|             "options": [ |       }, | ||||||
|               { |       "name": { | ||||||
|                 "question": { |         "en": "Velopark data", | ||||||
|                   "en": "Only unfinished tasks", |         "nl": "Velopark data" | ||||||
|                   "nl": "Enkel onafgewerkte taken" |       }, | ||||||
|                 }, |       "titleIcons": [ | ||||||
|                 "osmTags": "mr_taskStatus=Created", |         { | ||||||
|                 "default": true |           "id": "maproulette", | ||||||
|               } |           "render": "<a href='https://maproulette.org/challenge/{mr_challengeId}/task/{mr_taskId}' target='_blank'><img src='./assets/layers/maproulette/logomark.svg'/></a>" | ||||||
|             ] |         } | ||||||
|  |       ], | ||||||
|  |       "tagRenderings": [ | ||||||
|  |         { | ||||||
|  |           "id": "velopark-id-display", | ||||||
|  |           "render": { | ||||||
|  |             "*": "<span class='literal-code'>{ref:velopark}</span>" | ||||||
|           } |           } | ||||||
|         ], |  | ||||||
|         "calculatedTags+": [ |  | ||||||
|           "mr_velopark_id=feat.properties['ref:velopark']?.split('/')?.at(-1)", |  | ||||||
|           "_nearby_bicycle_parkings=closestn(feat)(['bike_parking','bike_parking_with_velopark_ref'], 100, undefined, 25)", |  | ||||||
|           "_nearby_bicycle_parkings:count=get(feat)('_nearby_bicycle_parkings').length", |  | ||||||
|           "_nearby_bicycle_parkings:props=get(feat)('_nearby_bicycle_parkings').map(f => ({_distance: Math.round(f.distance), _ref: feat.properties['ref:velopark'], _mr_id: feat.properties.id, '_velopark:id': (f.feat.properties['_velopark:id'] ?? 'unlinked') /*Explicit copy to trigger lazy loading*/, ...f.feat.properties}))" |  | ||||||
|         ], |  | ||||||
|         "=title": { |  | ||||||
|           "render": "Velopark parking <b>{mr_velopark_id}</b>" |  | ||||||
|         }, |         }, | ||||||
|         "source": { |         { | ||||||
|           "geoJson": "https://maproulette.org/api/v2/challenge/view/43282" |           "id": "velopark-link", | ||||||
|  |           "render": { | ||||||
|  |             "special": { | ||||||
|  |               "type": "link", | ||||||
|  |               "href": "https://www.velopark.be/static/data/{mr_velopark_id}", | ||||||
|  |               "text": { | ||||||
|  |                 "en": "See on velopark (webpage)", | ||||||
|  |                 "nl": "Bekijk op Velopark (webpagina)" | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           } | ||||||
|         }, |         }, | ||||||
|         "=tagRenderings": [ |         { | ||||||
|           { |           "id": "velopark-data-link", | ||||||
|             "id": "velopark-link", |           "render": { | ||||||
|             "render": { |             "special": { | ||||||
|               "special": { |               "type": "link", | ||||||
|                 "type": "link", |               "href": "{ref:velopark}", | ||||||
|                 "href": "https://www.velopark.be/static/data/{mr_velopark_id}", |               "text": "Inspect raw data on velopark.be" | ||||||
|                 "text": { |             } | ||||||
|                   "en": "See on velopark (webpage)", |           } | ||||||
|                   "nl": "Bekijk op Velopark (webpagina)" |         }, | ||||||
|                 } |         { | ||||||
|  |           "id": "show-data-velopark", | ||||||
|  |           "render": { | ||||||
|  |             "special": { | ||||||
|  |               "type": "compare_data", | ||||||
|  |               "url": "ref:velopark", | ||||||
|  |               "host": "https://data.velopark.be", | ||||||
|  |               "postprocessing": "velopark", | ||||||
|  |               "readonly": "yes" | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           "id": "closest_parkings", | ||||||
|  |           "render": { | ||||||
|  |             "*": "There are {_nearby_bicycle_parkings:count} bicycle parkings within 25m known in OpenStreetMap. " | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           "id": "list_nearby_bike_parkings", | ||||||
|  |           "render": { | ||||||
|  |             "special": { | ||||||
|  |               "type": "multi", | ||||||
|  |               "key": "_nearby_bicycle_parkings:props", | ||||||
|  |               "tagrendering": "<span class='p-2 m-1 border-2 border-dashed border-gray'><b><a href='#{id}'>{id}</a></b> ({_distance}m, {_velopark:id}) </span>{minimap(20)} {tag_apply(ref:velopark=$_ref,Link,link,id,_mr_id)}" | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           "id": "import_point", | ||||||
|  |           "render": { | ||||||
|  |             "special": { | ||||||
|  |               "type": "import_button", | ||||||
|  |               "targetLayer": "bike_parking_with_velopark_ref bike_parking", | ||||||
|  |               "tags": "amenity=bicycle_parking;ref:velopark=$ref:velopark", | ||||||
|  |               "text": { | ||||||
|  |                 "en": "Create a new bicycle parking in OSM", | ||||||
|  |                 "nl": "Maak een nieuwe parking aan in OSM" | ||||||
|  |               }, | ||||||
|  |               "maproulette_id": "mr_taskId" | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           "id": "close_mr", | ||||||
|  |           "render": { | ||||||
|  |             "special": { | ||||||
|  |               "type": "maproulette_set_status", | ||||||
|  |               "message": { | ||||||
|  |                 "en": "Mark this item as linked", | ||||||
|  |                 "nl": "Markeer als gelinkt" | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|           }, |           } | ||||||
|           { |         }, | ||||||
|             "id": "velopark-data-link", |         { | ||||||
|             "render": { |           "id": "close_mr_incorrect", | ||||||
|               "special": { |           "render": { | ||||||
|                 "type": "link", |             "special": { | ||||||
|                 "href": "{ref:velopark}", |               "type": "maproulette_set_status", | ||||||
|                 "text": "Inspect raw data on velopark.be" |               "message": { | ||||||
|               } |                 "en": "Mark this item as incorrect (duplicate, does not exist anymore, contradictory data)", | ||||||
|  |                 "nl": "Markeer dit object als incorrect (duplicaatin, incorrect of tegenstrijdige data, ...)" | ||||||
|  |               }, | ||||||
|  |               "image": "invalid", | ||||||
|  |               "status": 6 | ||||||
|             } |             } | ||||||
|           }, |           } | ||||||
|           { |         }, | ||||||
|             "id": "show-data-velopark", |         "{nearby_images(open,readonly)}" | ||||||
|             "render": { |       ], | ||||||
|               "special": { |       "lineRendering": [], | ||||||
|                 "type": "compare_data", |       "filter": [ | ||||||
|                 "url": "ref:velopark", |         { | ||||||
|                 "host": "https://data.velopark.be", |           "id": "created-only", | ||||||
|                 "postprocessing": "velopark", |           "options": [ | ||||||
|                 "readonly": "yes" |             { | ||||||
|               } |               "question": { | ||||||
|  |                 "en": "Only unfinished tasks", | ||||||
|  |                 "nl": "Enkel onafgewerkte taken" | ||||||
|  |               }, | ||||||
|  |               "osmTags": "mr_taskStatus=Created", | ||||||
|  |               "default": true | ||||||
|             } |             } | ||||||
|           }, |           ] | ||||||
|           { |         }, | ||||||
|             "id": "closest_parkings", |         { | ||||||
|             "render": { |           "id": "too-hard-only", | ||||||
|               "*": "There are {_nearby_bicycle_parkings:count} bicycle parkings within 25m known in OpenStreetMap. " |           "options": [ | ||||||
|  |             { | ||||||
|  |               "question": { | ||||||
|  |                 "en": "Only too-hard tasks", | ||||||
|  |                 "nl": "Enkel foutieve taken" | ||||||
|  |               }, | ||||||
|  |               "osmTags": "mr_taskStatus=Too_hard"            } | ||||||
|  |           ] | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "calculatedTags": [ | ||||||
|  |         "mr_velopark_id=feat.properties['ref:velopark']?.split('/')?.at(-1)", | ||||||
|  |         "_nearby_bicycle_parkings=closestn(feat)(['bike_parking','bike_parking_with_velopark_ref'], 100, undefined, 25)", | ||||||
|  |         "_nearby_bicycle_parkings:count=get(feat)('_nearby_bicycle_parkings').length", | ||||||
|  |         "_nearby_bicycle_parkings:props=get(feat)('_nearby_bicycle_parkings').map(f => ({_distance: Math.round(f.distance), _ref: feat.properties['ref:velopark'], _mr_id: feat.properties.id, '_velopark:id': (f.feat.properties['_velopark:id'] ?? 'unlinked') /*Explicit copy to trigger lazy loading*/, ...f.feat.properties}))" | ||||||
|  |       ], | ||||||
|  |       "pointRendering": [ | ||||||
|  |         { | ||||||
|  |           "location": [ | ||||||
|  |             "point", | ||||||
|  |             "centroid" | ||||||
|  |           ], | ||||||
|  |           "marker": [ | ||||||
|  |             { | ||||||
|  |               "icon": "square_rounded", | ||||||
|  |               "color": "#ffffff88" | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               "icon": "./assets/themes/velopark/velopark.svg" | ||||||
|             } |             } | ||||||
|           }, |           ], | ||||||
|           { |           "iconSize": "40,40", | ||||||
|             "id": "list_nearby_bike_parkings", |           "anchor": "bottom", | ||||||
|             "render": { |           "iconBadges": [{ | ||||||
|               "special": { |             "if": "mr_taskStatus=Too_Hard", | ||||||
|                 "type": "multi", |             "then": "invalid" | ||||||
|                 "key": "_nearby_bicycle_parkings:props", |           },{ | ||||||
|                 "tagrendering": "<span class='p-2 m-1 border-2 border-dashed border-gray'><b><a href='#{id}'>{id}</a></b> ({_distance}m, {_velopark:id}) </span>{minimap(20)} {tag_apply(ref:velopark=$_ref,Link,link,id,_mr_id)}" |             "if": "mr_taskStatus=Fixed", | ||||||
|               } |             "then": "confirm" | ||||||
|             } |           }] | ||||||
|           }, |         } | ||||||
|           { |       ] | ||||||
|             "id": "import_point", |  | ||||||
|             "render": { |  | ||||||
|               "special": { |  | ||||||
|                 "type": "import_button", |  | ||||||
|                 "targetLayer": "bike_parking_with_velopark_ref bike_parking", |  | ||||||
|                 "tags": "amenity=bicycle_parking;ref:velopark=$ref:velopark", |  | ||||||
|                 "text": { |  | ||||||
|                   "en": "Create a new bicycle parking in OSM", |  | ||||||
|                   "nl": "Maak een nieuwe parking aan in OSM" |  | ||||||
|                 }, |  | ||||||
|                 "maproulette_id": "mr_taskId" |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "id": "close_mr", |  | ||||||
|             "render": { |  | ||||||
|               "special": { |  | ||||||
|                 "type": "maproulette_set_status", |  | ||||||
|                 "message": { |  | ||||||
|                   "en": "Mark this item as linked", |  | ||||||
|                   "nl": "Markeer als gelinkt" |  | ||||||
|                 } |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           }, |  | ||||||
|           { |  | ||||||
|             "id": "close_mr_incorrect", |  | ||||||
|             "render": { |  | ||||||
|               "special": { |  | ||||||
|                 "type": "maproulette_set_status", |  | ||||||
|                 "message": { |  | ||||||
|                   "en": "Mark this item as incorrect (duplicate, does not exist anymore, contradictory data)", |  | ||||||
|                   "nl": "Markeer dit object als incorrect (duplicaatin, incorrect of tegenstrijdige data, ...)" |  | ||||||
|                 }, |  | ||||||
|                 "image": "bug", |  | ||||||
|                 "status": 6 |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           }, |  | ||||||
|           "{nearby_images()}" |  | ||||||
|         ] |  | ||||||
|       } |  | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "builtin": [ |       "builtin": [ | ||||||
|  |  | ||||||
|  | @ -82,6 +82,7 @@ function genImages(dryrun = false) { | ||||||
|         "SocialImageForeground", |         "SocialImageForeground", | ||||||
|         "speech_bubble_black_outline", |         "speech_bubble_black_outline", | ||||||
|         "square", |         "square", | ||||||
|  |         "square_rounded", | ||||||
|         "star", |         "star", | ||||||
|         "star_half", |         "star_half", | ||||||
|         "star_outline", |         "star_outline", | ||||||
|  |  | ||||||
|  | @ -2,15 +2,17 @@ import Script from "../Script" | ||||||
| import { Utils } from "../../src/Utils" | import { Utils } from "../../src/Utils" | ||||||
| import VeloparkLoader, { VeloparkData } from "../../src/Logic/Web/VeloparkLoader" | import VeloparkLoader, { VeloparkData } from "../../src/Logic/Web/VeloparkLoader" | ||||||
| import fs from "fs" | import fs from "fs" | ||||||
| import OverpassFeatureSource from "../../src/Logic/FeatureSource/Sources/OverpassFeatureSource" |  | ||||||
| import { Overpass } from "../../src/Logic/Osm/Overpass" | import { Overpass } from "../../src/Logic/Osm/Overpass" | ||||||
| import { RegexTag } from "../../src/Logic/Tags/RegexTag" | import { RegexTag } from "../../src/Logic/Tags/RegexTag" | ||||||
| import Constants from "../../src/Models/Constants" | import Constants from "../../src/Models/Constants" | ||||||
| import { ImmutableStore } from "../../src/Logic/UIEventSource" | import { ImmutableStore } from "../../src/Logic/UIEventSource" | ||||||
| import { BBox } from "../../src/Logic/BBox" | import { BBox } from "../../src/Logic/BBox" | ||||||
|  | 
 | ||||||
| class VeloParkToGeojson extends Script { | class VeloParkToGeojson extends Script { | ||||||
|     constructor() { |     constructor() { | ||||||
|         super("Downloads the latest Velopark data and converts it to a geojson, which will be saved at the current directory") |         super( | ||||||
|  |             "Downloads the latest Velopark data and converts it to a geojson, which will be saved at the current directory" | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async main(args: string[]): Promise<void> { |     async main(args: string[]): Promise<void> { | ||||||
|  | @ -19,37 +21,48 @@ class VeloParkToGeojson extends Script { | ||||||
|         const url = "https://www.velopark.be/api/parkings/1000" |         const url = "https://www.velopark.be/api/parkings/1000" | ||||||
|         const data = <VeloparkData[]>await Utils.downloadJson(url) |         const data = <VeloparkData[]>await Utils.downloadJson(url) | ||||||
| 
 | 
 | ||||||
|         const bboxBelgium = new BBox([[2.51357303225, 49.5294835476],[ 6.15665815596, 51.4750237087]]) |         const bboxBelgium = new BBox([ | ||||||
|         const alreadyLinkedQuery = new Overpass(new RegexTag("ref:velopark", /.+/), |             [2.51357303225, 49.5294835476], | ||||||
|  |             [6.15665815596, 51.4750237087], | ||||||
|  |         ]) | ||||||
|  |         const alreadyLinkedQuery = new Overpass( | ||||||
|  |             new RegexTag("ref:velopark", /.+/), | ||||||
|             [], |             [], | ||||||
|             Constants.defaultOverpassUrls[0], |             Constants.defaultOverpassUrls[0], | ||||||
|             new ImmutableStore(60*5), |             new ImmutableStore(60 * 5), | ||||||
|             false |             false | ||||||
|             ) |         ) | ||||||
|         const alreadyLinkedFeatures = await alreadyLinkedQuery.queryGeoJson(bboxBelgium) |         const alreadyLinkedFeatures = await alreadyLinkedQuery.queryGeoJson(bboxBelgium) | ||||||
|         const seenIds = new Set<string>(alreadyLinkedFeatures[0].features.map(f => f.properties["ref:velopark"])) |         const seenIds = new Set<string>( | ||||||
|         const features = data.map(f => VeloparkLoader.convert(f)) |             alreadyLinkedFeatures[0].features.map((f) => f.properties["ref:velopark"]) | ||||||
|             .filter(f => !seenIds.has(f.properties["ref:velopark"])) |         ) | ||||||
|  |         console.log("OpenStreetMap contains", seenIds.size, "bicycle parkings with a velopark ref") | ||||||
|  |         const allVelopark = data.map((f) => VeloparkLoader.convert(f)) | ||||||
|  |         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)) | ||||||
|         } |         } | ||||||
|         allProperties.delete("ref:velopark") |         allProperties.delete("ref:velopark") | ||||||
|         for (const feature of features) { |         for (const feature of features) { | ||||||
|             allProperties.forEach(k => { |             allProperties.forEach((k) => { | ||||||
|                 delete feature.properties[k] |                 delete feature.properties[k] | ||||||
|             }) |             }) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         fs.writeFileSync("velopark_id_only_export_" + new Date().toISOString() + ".geojson", JSON.stringify({ |         fs.writeFileSync( | ||||||
|             "type": "FeatureCollection", |             "velopark_id_only_export_" + new Date().toISOString() + ".geojson", | ||||||
|             features, |             JSON.stringify( | ||||||
|         }, null, "    ")) |                 { | ||||||
| 
 |                     type: "FeatureCollection", | ||||||
|  |                     features, | ||||||
|  |                 }, | ||||||
|  |                 null, | ||||||
|  |                 "    " | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| new VeloParkToGeojson().run() | new VeloParkToGeojson().run() | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import { Feature, Geometry, Point } from "geojson" | import { Feature, Geometry } from "geojson" | ||||||
| import { OH } from "../../UI/OpeningHours/OpeningHours" | import { OH } from "../../UI/OpeningHours/OpeningHours" | ||||||
| import EmailValidator from "../../UI/InputElement/Validators/EmailValidator" | import EmailValidator from "../../UI/InputElement/Validators/EmailValidator" | ||||||
| import PhoneValidator from "../../UI/InputElement/Validators/PhoneValidator" | import PhoneValidator from "../../UI/InputElement/Validators/PhoneValidator" | ||||||
|  | @ -12,39 +12,49 @@ import { Utils } from "../../Utils" | ||||||
|  * Reads a velopark-json, converts it to a geojson |  * Reads a velopark-json, converts it to a geojson | ||||||
|  */ |  */ | ||||||
| export default class VeloparkLoader { | export default class VeloparkLoader { | ||||||
| 
 |  | ||||||
|     private static readonly emailReformatting = new EmailValidator() |     private static readonly emailReformatting = new EmailValidator() | ||||||
|     private static readonly phoneValidator = new PhoneValidator() |     private static readonly phoneValidator = new PhoneValidator() | ||||||
| 
 | 
 | ||||||
|     private static readonly coder = new CountryCoder( |     private static readonly coder = new CountryCoder( | ||||||
|         Constants.countryCoderEndpoint, |         Constants.countryCoderEndpoint, | ||||||
|         Utils.downloadJson, |         Utils.downloadJson | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     public static convert(veloparkData: VeloparkData): Feature { |     public static convert(veloparkData: VeloparkData): Feature { | ||||||
| 
 |         console.log("Converting", veloparkData) | ||||||
|         const properties: { |         const properties: { | ||||||
|             "ref:velopark":string, |             "ref:velopark": string | ||||||
|             "operator:email"?: string, |             "operator:email"?: string | ||||||
|             "operator:phone"?: string, |             "operator:phone"?: string | ||||||
|             fee?: string, |             fee?: string | ||||||
|             opening_hours?: string |             opening_hours?: string | ||||||
|             access?: string |             access?: string | ||||||
|             maxstay?: string |             maxstay?: string | ||||||
|             operator?: string |             operator?: string | ||||||
|         } = { |         } = { | ||||||
|             "ref:velopark": veloparkData["id"] ?? veloparkData["@id"] |             "ref:velopark": veloparkData["id"] ?? veloparkData["@id"], | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         for (const k of ["_id", "url", "dateModified", "name", "address"]) { | ||||||
|  |             delete veloparkData[k] | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         VeloparkLoader.cleanup(veloparkData["properties"]) | ||||||
|  |         VeloparkLoader.cleanupEmtpy(veloparkData) | ||||||
|  | 
 | ||||||
|         properties.operator = veloparkData.operatedBy?.companyName |         properties.operator = veloparkData.operatedBy?.companyName | ||||||
| 
 | 
 | ||||||
|         if (veloparkData.contactPoint?.email) { |         if (veloparkData.contactPoint?.email) { | ||||||
|             properties["operator:email"] = VeloparkLoader.emailReformatting.reformat(veloparkData.contactPoint?.email) |             properties["operator:email"] = VeloparkLoader.emailReformatting.reformat( | ||||||
|  |                 veloparkData.contactPoint?.email | ||||||
|  |             ) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|         if (veloparkData.contactPoint?.telephone) { |         if (veloparkData.contactPoint?.telephone) { | ||||||
|             properties["operator:phone"] = VeloparkLoader.phoneValidator.reformat(veloparkData.contactPoint?.telephone, () => "be") |             properties["operator:phone"] = VeloparkLoader.phoneValidator.reformat( | ||||||
|  |                 veloparkData.contactPoint?.telephone, | ||||||
|  |                 () => "be" | ||||||
|  |             ) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         veloparkData.photos?.forEach((p, i) => { |         veloparkData.photos?.forEach((p, i) => { | ||||||
|  | @ -52,130 +62,198 @@ export default class VeloparkLoader { | ||||||
|                 properties["image"] = p.image |                 properties["image"] = p.image | ||||||
|             } else { |             } else { | ||||||
|                 properties["image:" + i] = p.image |                 properties["image:" + i] = p.image | ||||||
| 
 |  | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         let geometry = veloparkData.geometry |         let geometry = veloparkData.geometry | ||||||
|         for (const g of veloparkData["@graph"]) { |         for (const g of veloparkData["@graph"]) { | ||||||
|  |             VeloparkLoader.cleanup(g) | ||||||
|  |             VeloparkLoader.cleanupEmtpy(g) | ||||||
|             if (g.geo[0]) { |             if (g.geo[0]) { | ||||||
|                 geometry = { type: "Point", coordinates: [g.geo[0].longitude, g.geo[0].latitude] } |                 geometry = { type: "Point", coordinates: [g.geo[0].longitude, g.geo[0].latitude] } | ||||||
|             } |             } | ||||||
|             if (g.maximumParkingDuration?.endsWith("D") && g.maximumParkingDuration?.startsWith("P")) { |             if ( | ||||||
|                 const duration = g.maximumParkingDuration.substring(1, g.maximumParkingDuration.length - 1) |                 g.maximumParkingDuration?.endsWith("D") && | ||||||
|  |                 g.maximumParkingDuration?.startsWith("P") | ||||||
|  |             ) { | ||||||
|  |                 const duration = g.maximumParkingDuration.substring( | ||||||
|  |                     1, | ||||||
|  |                     g.maximumParkingDuration.length - 1 | ||||||
|  |                 ) | ||||||
|                 properties.maxstay = duration + " days" |                 properties.maxstay = duration + " days" | ||||||
|             } |             } | ||||||
|             properties.access = g.publicAccess ? "yes" : "no" |             properties.access = g.publicAccess ? "yes" : "no" | ||||||
|             const prefix = "http://schema.org/" |             const prefix = "http://schema.org/" | ||||||
|             if (g.openingHoursSpecification) { |             if (g.openingHoursSpecification) { | ||||||
|                 const oh = OH.simplify(g.openingHoursSpecification.map(spec => { |                 const oh = OH.simplify( | ||||||
|                     const dayOfWeek = spec.dayOfWeek.substring(prefix.length, prefix.length + 2).toLowerCase() |                     g.openingHoursSpecification | ||||||
|                     const startHour = spec.opens |                         .map((spec) => { | ||||||
|                     const endHour = spec.closes === "23:59" ? "24:00" : spec.closes |                             const dayOfWeek = spec.dayOfWeek | ||||||
|                     const merged = OH.MergeTimes(OH.ParseRule(dayOfWeek + " " + startHour + "-" + endHour)) |                                 .substring(prefix.length, prefix.length + 2) | ||||||
|                     return OH.ToString(merged) |                                 .toLowerCase() | ||||||
|                 }).join("; ")) |                             const startHour = spec.opens | ||||||
|  |                             const endHour = spec.closes === "23:59" ? "24:00" : spec.closes | ||||||
|  |                             const merged = OH.MergeTimes( | ||||||
|  |                                 OH.ParseRule(dayOfWeek + " " + startHour + "-" + endHour) | ||||||
|  |                             ) | ||||||
|  |                             return OH.ToString(merged) | ||||||
|  |                         }) | ||||||
|  |                         .join("; ") | ||||||
|  |                 ) | ||||||
|                 properties.opening_hours = oh |                 properties.opening_hours = oh | ||||||
|             } |             } | ||||||
|             if (g.priceSpecification?.[0]) { |             if (g.priceSpecification?.[0]) { | ||||||
|                 properties.fee = g.priceSpecification[0].freeOfCharge ? "no" : "yes" |                 properties.fee = g.priceSpecification[0].freeOfCharge ? "no" : "yes" | ||||||
|             } |             } | ||||||
|  |             const types = { | ||||||
|  |                 "https://data.velopark.be/openvelopark/terms#RegularBicycle": "_", | ||||||
|  |                 "https://data.velopark.be/openvelopark/terms#ElectricBicycle": | ||||||
|  |                     "capacity:electric_bicycle", | ||||||
|  |                 "https://data.velopark.be/openvelopark/terms#CargoBicycle": "capacity:cargo_bike", | ||||||
|  |             } | ||||||
|  |             let totalCapacity = 0 | ||||||
|  |             for (let i = (g.allows ?? []).length - 1; i >= 0; i--) { | ||||||
|  |                 const capacity = g.allows[i] | ||||||
|  |                 const type: string = capacity["@type"] | ||||||
|  |                 if (type === undefined) { | ||||||
|  |                     console.warn("No type found for", capacity.bicycleType) | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 const count = capacity["amount"] | ||||||
|  |                 if (!isNaN(count)) { | ||||||
|  |                     totalCapacity += Number(count) | ||||||
|  |                 } else { | ||||||
|  |                     console.warn("Not a valid number while loading velopark data:", count) | ||||||
|  |                 } | ||||||
|  |                 if (type !== "_") { | ||||||
|  |                     //  properties[type] = count
 | ||||||
|  |                 } | ||||||
|  |                 g.allows.splice(i, 1) | ||||||
|  |             } | ||||||
|  |             if (totalCapacity > 0) { | ||||||
|  |                 properties["capacity"] = totalCapacity | ||||||
|  |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         console.log(JSON.stringify(properties, null, "  ")) | ||||||
|  | 
 | ||||||
|         return { type: "Feature", properties, geometry } |         return { type: "Feature", properties, geometry } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private static cleanup(data: any) { | ||||||
|  |         if (!data?.attributes) { | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         for (const k of ["NIS_CODE", "name_NL", "name_DE", "name_EN", "name_FR"]) { | ||||||
|  |             delete data.attributes[k] | ||||||
|  |         } | ||||||
|  |         VeloparkLoader.cleanupEmtpy(data) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static cleanupEmtpy(data: any) { | ||||||
|  |         for (const key in data) { | ||||||
|  |             if (data[key] === null) { | ||||||
|  |                 delete data[key] | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             if (Object.keys(data[key]).length === 0) { | ||||||
|  |                 delete data[key] | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface VeloparkData { | export interface VeloparkData { | ||||||
|     geometry?: Geometry |     geometry?: Geometry | ||||||
|     "@context": any, |     "@context": any | ||||||
|     "@id": string // "https://data.velopark.be/data/NMBS_541",
 |     "@id": string // "https://data.velopark.be/data/NMBS_541",
 | ||||||
|     "@type": "BicycleParkingStation", |     "@type": "BicycleParkingStation" | ||||||
|     "dateModified": string, |     dateModified: string | ||||||
|     "identifier": number, |     identifier: number | ||||||
|     "name": [ |     name: [ | ||||||
|         { |         { | ||||||
|             "@value": string, |             "@value": string | ||||||
|             "@language": "nl" |             "@language": "nl" | ||||||
|         } |         } | ||||||
|     ], |     ] | ||||||
|     "ownedBy": { |     ownedBy: { | ||||||
|         "@id": string, |         "@id": string | ||||||
|         "@type": "BusinessEntity", |         "@type": "BusinessEntity" | ||||||
|         "companyName": string |         companyName: string | ||||||
|     }, |     } | ||||||
|     "operatedBy": { |     operatedBy: { | ||||||
|         "@type": "BusinessEntity", |         "@type": "BusinessEntity" | ||||||
|         "companyName": string |         companyName: string | ||||||
|     }, |     } | ||||||
|     "address": any, |     address: any | ||||||
|     "hasMap": any, |     hasMap: any | ||||||
|     "contactPoint": { |     contactPoint: { | ||||||
|         "@type": "ContactPoint", |         "@type": "ContactPoint" | ||||||
|         "email": string, |         email: string | ||||||
|         "telephone": string |         telephone: string | ||||||
|     }, |     } | ||||||
|     "photos": { |     photos: { | ||||||
|         "@type": "Photograph", |         "@type": "Photograph" | ||||||
|         "image": string |         image: string | ||||||
|     }[], |     }[] | ||||||
|     "interactionService": { |     interactionService: { | ||||||
|         "@type": "WebSite", |         "@type": "WebSite" | ||||||
|         "url": string |         url: string | ||||||
|     }, |     } | ||||||
|     /** |     /** | ||||||
|      * Contains various extra pieces of data, e.g. services or opening hours |      * Contains various extra pieces of data, e.g. services or opening hours | ||||||
|      */ |      */ | ||||||
|     "@graph": [ |     "@graph": [ | ||||||
|         { |         { | ||||||
|             "@type": "https://data.velopark.be/openvelopark/terms#PublicBicycleParking", |             "@type": "https://data.velopark.be/openvelopark/terms#PublicBicycleParking" | ||||||
|             "openingHoursSpecification": { |             openingHoursSpecification: { | ||||||
|                 "@type": "OpeningHoursSpecification", |                 "@type": "OpeningHoursSpecification" | ||||||
|                 /** |                 /** | ||||||
|                  * Ends with 'Monday', 'Tuesday', ... |                  * Ends with 'Monday', 'Tuesday', ... | ||||||
|                  */ |                  */ | ||||||
|                 "dayOfWeek": "http://schema.org/Monday" |                 dayOfWeek: | ||||||
|  |                     | "http://schema.org/Monday" | ||||||
|                     | "http://schema.org/Tuesday" |                     | "http://schema.org/Tuesday" | ||||||
|                     | "http://schema.org/Wednesday" |                     | "http://schema.org/Wednesday" | ||||||
|                     | "http://schema.org/Thursday" |                     | "http://schema.org/Thursday" | ||||||
|                     | "http://schema.org/Friday" |                     | "http://schema.org/Friday" | ||||||
|                     | "http://schema.org/Saturday" |                     | "http://schema.org/Saturday" | ||||||
|                     | "http://schema.org/Sunday", |                     | "http://schema.org/Sunday" | ||||||
|                 /** |                 /** | ||||||
|                  * opens: 00:00 and closes 23:59 for the entire day |                  * opens: 00:00 and closes 23:59 for the entire day | ||||||
|                  */ |                  */ | ||||||
|                 "opens": string, |                 opens: string | ||||||
|                 "closes": string |                 closes: string | ||||||
|             }[], |             }[] | ||||||
|             /** |             /** | ||||||
|              * P30D = 30 days |              * P30D = 30 days | ||||||
|              */ |              */ | ||||||
|             "maximumParkingDuration": "P30D", |             maximumParkingDuration: "P30D" | ||||||
|             "publicAccess": true, |             publicAccess: true | ||||||
|             "totalCapacity": 110, |             totalCapacity: 110 | ||||||
|             "allows": [ |             allows: [ | ||||||
|                 { |                 { | ||||||
|                     "@type": "AllowedBicycle", |                     "@type": "AllowedBicycle" | ||||||
|                     /* TODO is cargo bikes etc also available?*/ |                     /* TODO is cargo bikes etc also available?*/ | ||||||
|                     "bicycleType": "https://data.velopark.be/openvelopark/terms#RegularBicycle", |                     bicycleType: | ||||||
|                     "bicyclesAmount": number |                         | string | ||||||
|  |                         | "https://data.velopark.be/openvelopark/terms#RegularBicycle" | ||||||
|  |                     bicyclesAmount: number | ||||||
|                 } |                 } | ||||||
|             ], |             ] | ||||||
|             "geo": [ |             geo: [ | ||||||
|                 { |                 { | ||||||
|                     "@type": "GeoCoordinates", |                     "@type": "GeoCoordinates" | ||||||
|                     "latitude": number, |                     latitude: number | ||||||
|                     "longitude": number |                     longitude: number | ||||||
|                 } |                 } | ||||||
|             ], |             ] | ||||||
|             "priceSpecification": [ |             priceSpecification: [ | ||||||
|                 { |                 { | ||||||
|                     "@type": "PriceSpecification", |                     "@type": "PriceSpecification" | ||||||
|                     "freeOfCharge": boolean |                     freeOfCharge: boolean | ||||||
|                 } |                 } | ||||||
|             ] |             ] | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ export default class Constants { | ||||||
|         "import_candidate", |         "import_candidate", | ||||||
|         "usersettings", |         "usersettings", | ||||||
|         "icons", |         "icons", | ||||||
|         "filters" |         "filters", | ||||||
|     ] as const |     ] as const | ||||||
|     /** |     /** | ||||||
|      * Layer IDs of layers which have special properties through built-in hooks |      * Layer IDs of layers which have special properties through built-in hooks | ||||||
|  | @ -117,7 +117,9 @@ export default class Constants { | ||||||
|      */ |      */ | ||||||
|     private static readonly _defaultPinIcons = [ |     private static readonly _defaultPinIcons = [ | ||||||
|         "pin", |         "pin", | ||||||
|  |         "bug", | ||||||
|         "square", |         "square", | ||||||
|  |         "square_rounded", | ||||||
|         "circle", |         "circle", | ||||||
|         "checkmark", |         "checkmark", | ||||||
|         "clock", |         "clock", | ||||||
|  |  | ||||||
|  | @ -55,7 +55,7 @@ | ||||||
|       <div class="flex w-full space-x-1 overflow-x-auto" style="scroll-snap-type: x proximity"> |       <div class="flex w-full space-x-1 overflow-x-auto" style="scroll-snap-type: x proximity"> | ||||||
|         {#each $images as image (image.pictureUrl)} |         {#each $images as image (image.pictureUrl)} | ||||||
|           <span class="w-fit shrink-0" style="scroll-snap-align: start"> |           <span class="w-fit shrink-0" style="scroll-snap-align: start"> | ||||||
|             <LinkableImage {tags} {image} {state} {lon} {lat} {feature} {layer} {linkable} /> |             <LinkableImage {tags} {image} {state} {feature} {layer} {linkable} /> | ||||||
|           </span> |           </span> | ||||||
|         {/each} |         {/each} | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|  | @ -1,47 +1,49 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|     import Pin from "../../assets/svg/Pin.svelte" |   import Pin from "../../assets/svg/Pin.svelte" | ||||||
|     import Square from "../../assets/svg/Square.svelte" |   import Square from "../../assets/svg/Square.svelte" | ||||||
|     import Circle from "../../assets/svg/Circle.svelte" |   import Circle from "../../assets/svg/Circle.svelte" | ||||||
|     import Checkmark from "../../assets/svg/Checkmark.svelte" |   import Checkmark from "../../assets/svg/Checkmark.svelte" | ||||||
|     import Clock from "../../assets/svg/Clock.svelte" |   import Clock from "../../assets/svg/Clock.svelte" | ||||||
|     import Close from "../../assets/svg/Close.svelte" |   import Close from "../../assets/svg/Close.svelte" | ||||||
|     import Crosshair from "../../assets/svg/Crosshair.svelte" |   import Crosshair from "../../assets/svg/Crosshair.svelte" | ||||||
|     import Help from "../../assets/svg/Help.svelte" |   import Help from "../../assets/svg/Help.svelte" | ||||||
|     import Home from "../../assets/svg/Home.svelte" |   import Home from "../../assets/svg/Home.svelte" | ||||||
|     import Invalid from "../../assets/svg/Invalid.svelte" |   import Invalid from "../../assets/svg/Invalid.svelte" | ||||||
|     import Location from "../../assets/svg/Location.svelte" |   import Location from "../../assets/svg/Location.svelte" | ||||||
|     import Location_empty from "../../assets/svg/Location_empty.svelte" |   import Location_empty from "../../assets/svg/Location_empty.svelte" | ||||||
|     import Location_locked from "../../assets/svg/Location_locked.svelte" |   import Location_locked from "../../assets/svg/Location_locked.svelte" | ||||||
|     import Note from "../../assets/svg/Note.svelte" |   import Note from "../../assets/svg/Note.svelte" | ||||||
|     import Resolved from "../../assets/svg/Resolved.svelte" |   import Resolved from "../../assets/svg/Resolved.svelte" | ||||||
|     import Ring from "../../assets/svg/Ring.svelte" |   import Ring from "../../assets/svg/Ring.svelte" | ||||||
|     import Scissors from "../../assets/svg/Scissors.svelte" |   import Scissors from "../../assets/svg/Scissors.svelte" | ||||||
|     import Teardrop from "../../assets/svg/Teardrop.svelte" |   import Teardrop from "../../assets/svg/Teardrop.svelte" | ||||||
|     import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte" |   import Teardrop_with_hole_green from "../../assets/svg/Teardrop_with_hole_green.svelte" | ||||||
|     import Triangle from "../../assets/svg/Triangle.svelte" |   import Triangle from "../../assets/svg/Triangle.svelte" | ||||||
|     import Brick_wall_square from "../../assets/svg/Brick_wall_square.svelte" |   import Brick_wall_square from "../../assets/svg/Brick_wall_square.svelte" | ||||||
|     import Brick_wall_round from "../../assets/svg/Brick_wall_round.svelte" |   import Brick_wall_round from "../../assets/svg/Brick_wall_round.svelte" | ||||||
|     import Gps_arrow from "../../assets/svg/Gps_arrow.svelte" |   import Gps_arrow from "../../assets/svg/Gps_arrow.svelte" | ||||||
|     import { HeartIcon } from "@babeard/svelte-heroicons/solid" |   import { HeartIcon } from "@babeard/svelte-heroicons/solid" | ||||||
|     import { HeartIcon as HeartOutlineIcon } from "@babeard/svelte-heroicons/outline" |   import { HeartIcon as HeartOutlineIcon } from "@babeard/svelte-heroicons/outline" | ||||||
|     import Confirm from "../../assets/svg/Confirm.svelte" |   import Confirm from "../../assets/svg/Confirm.svelte" | ||||||
|     import Not_found from "../../assets/svg/Not_found.svelte" |   import Not_found from "../../assets/svg/Not_found.svelte" | ||||||
|     import { twMerge } from "tailwind-merge" |   import { twMerge } from "tailwind-merge" | ||||||
|     import Direction_gradient from "../../assets/svg/Direction_gradient.svelte" |   import Direction_gradient from "../../assets/svg/Direction_gradient.svelte" | ||||||
|     import Mastodon from "../../assets/svg/Mastodon.svelte" |   import Mastodon from "../../assets/svg/Mastodon.svelte" | ||||||
|     import Party from "../../assets/svg/Party.svelte" |   import Party from "../../assets/svg/Party.svelte" | ||||||
|     import AddSmall from "../../assets/svg/AddSmall.svelte" |   import AddSmall from "../../assets/svg/AddSmall.svelte" | ||||||
|     import { LinkIcon } from "@babeard/svelte-heroicons/mini" |   import { LinkIcon } from "@babeard/svelte-heroicons/mini" | ||||||
|  |   import Square_rounded from "../../assets/svg/Square_rounded.svelte" | ||||||
|  |   import Bug from "../../assets/svg/Bug.svelte" | ||||||
| 
 | 
 | ||||||
|     /** |   /** | ||||||
|      * Renders a single icon. |    * Renders a single icon. | ||||||
|      * |    * | ||||||
|      * Icons -placed on top of each other- form a 'Marker' together |    * Icons -placed on top of each other- form a 'Marker' together | ||||||
|      */ |    */ | ||||||
| 
 | 
 | ||||||
|     export let icon: string | undefined |   export let icon: string | undefined | ||||||
|     export let color: string | undefined = undefined |   export let color: string | undefined = undefined | ||||||
|     export let clss: string | undefined = undefined |   export let clss: string | undefined = undefined | ||||||
| 
 | 
 | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | @ -50,6 +52,11 @@ | ||||||
|     <Pin {color} class={clss} /> |     <Pin {color} class={clss} /> | ||||||
|   {:else if icon === "square"} |   {:else if icon === "square"} | ||||||
|     <Square {color} class={clss} /> |     <Square {color} class={clss} /> | ||||||
|  |   {:else if icon === "square_rounded"} | ||||||
|  |     <Square_rounded {color} class={clss} /> | ||||||
|  |   {:else if icon === "bug"} | ||||||
|  |     <Bug {color} class={clss} /> | ||||||
|  | 
 | ||||||
|   {:else if icon === "circle"} |   {:else if icon === "circle"} | ||||||
|     <Circle {color} class={clss} /> |     <Circle {color} class={clss} /> | ||||||
|   {:else if icon === "checkmark"} |   {:else if icon === "checkmark"} | ||||||
|  | @ -117,7 +124,7 @@ | ||||||
|   {:else if icon === "addSmall"} |   {:else if icon === "addSmall"} | ||||||
|     <AddSmall {color} class={clss} /> |     <AddSmall {color} class={clss} /> | ||||||
|   {:else if icon === "link"} |   {:else if icon === "link"} | ||||||
|     <LinkIcon style="--svg-color: {color}" class={twMerge(clss, "apply-fill")}/> |     <LinkIcon style="--svg-color: {color}" class={twMerge(clss, "apply-fill")} /> | ||||||
|   {:else} |   {:else} | ||||||
|     <img class={clss ?? "h-full w-full"} src={icon} aria-hidden="true" alt="" /> |     <img class={clss ?? "h-full w-full"} src={icon} aria-hidden="true" alt="" /> | ||||||
|   {/if} |   {/if} | ||||||
|  |  | ||||||
|  | @ -54,7 +54,7 @@ | ||||||
|     <Tr t={Translations.t.general.loading} /> |     <Tr t={Translations.t.general.loading} /> | ||||||
|   </Loading> |   </Loading> | ||||||
| {:else if $status === Maproulette.STATUS_OPEN} | {:else if $status === Maproulette.STATUS_OPEN} | ||||||
|   <button class="no-image-background w-full p-4" on:click={() => apply()}> |   <button class="no-image-background w-full p-4 m-0" on:click={() => apply()}> | ||||||
|     <Icon clss="w-8 h-8 mr-2" icon={image} /> |     <Icon clss="w-8 h-8 mr-2" icon={image} /> | ||||||
|     {message} |     {message} | ||||||
|   </button> |   </button> | ||||||
|  |  | ||||||
|  | @ -3,7 +3,11 @@ 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 { RenderingSpecification, SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization" | import { | ||||||
|  |     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" | ||||||
|  | @ -94,6 +98,11 @@ class NearbyImageVis implements SpecialVisualization { | ||||||
|             defaultValue: "closed", |             defaultValue: "closed", | ||||||
|             doc: "Either `open` or `closed`. If `open`, then the image carousel will always be shown", |             doc: "Either `open` or `closed`. If `open`, then the image carousel will always be shown", | ||||||
|         }, |         }, | ||||||
|  |         { | ||||||
|  |             name: "readonly", | ||||||
|  |             required: false, | ||||||
|  |             doc: "If 'readonly', will not show the 'link'-button", | ||||||
|  |         }, | ||||||
|     ] |     ] | ||||||
|     docs = |     docs = | ||||||
|         "A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature" |         "A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature" | ||||||
|  | @ -106,9 +115,10 @@ 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 [lon, lat] = GeoOperations.centerpointCoordinates(feature) |         const [lon, lat] = GeoOperations.centerpointCoordinates(feature) | ||||||
|         return new SvelteUIElement(isOpen ? NearbyImages : NearbyImagesCollapsed, { |         return new SvelteUIElement(isOpen ? NearbyImages : NearbyImagesCollapsed, { | ||||||
|             tags, |             tags, | ||||||
|  | @ -117,6 +127,7 @@ class NearbyImageVis implements SpecialVisualization { | ||||||
|             lat, |             lat, | ||||||
|             feature, |             feature, | ||||||
|             layer, |             layer, | ||||||
|  |             linkable: !readonly, | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -171,7 +182,7 @@ class StealViz implements SpecialVisualization { | ||||||
|                                 selectedElement: otherFeature, |                                 selectedElement: otherFeature, | ||||||
|                                 state, |                                 state, | ||||||
|                                 layer, |                                 layer, | ||||||
|                             }), |                             }) | ||||||
|                         ) |                         ) | ||||||
|                     } |                     } | ||||||
|                     if (elements.length === 1) { |                     if (elements.length === 1) { | ||||||
|  | @ -179,8 +190,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] | ||||||
|             ), |             ) | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -219,7 +230,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(";") | ||||||
|  | @ -273,8 +284,10 @@ export default class SpecialVisualizations { | ||||||
|      * templ.args[0] = "{email}" |      * templ.args[0] = "{email}" | ||||||
|      */ |      */ | ||||||
|     public static constructSpecification( |     public static constructSpecification( | ||||||
|         template: string | { special: Record<string, string | Record<string, string>> & { type: string } }, |         template: | ||||||
|         extraMappings: SpecialVisualization[] = [], |             | string | ||||||
|  |             | { special: Record<string, string | Record<string, string>> & { type: string } }, | ||||||
|  |         extraMappings: SpecialVisualization[] = [] | ||||||
|     ): RenderingSpecification[] { |     ): RenderingSpecification[] { | ||||||
|         if (template === "") { |         if (template === "") { | ||||||
|             return [] |             return [] | ||||||
|  | @ -283,7 +296,7 @@ export default class SpecialVisualizations { | ||||||
|         if (typeof template !== "string") { |         if (typeof template !== "string") { | ||||||
|             console.trace( |             console.trace( | ||||||
|                 "Got a non-expanded template while constructing the specification, it still has a 'special-key':", |                 "Got a non-expanded template while constructing the specification, it still has a 'special-key':", | ||||||
|                 template, |                 template | ||||||
|             ) |             ) | ||||||
|             throw "Got a non-expanded template while constructing the specification" |             throw "Got a non-expanded template while constructing the specification" | ||||||
|         } |         } | ||||||
|  | @ -291,20 +304,20 @@ export default class SpecialVisualizations { | ||||||
|         for (const knownSpecial of allKnownSpecials) { |         for (const knownSpecial of allKnownSpecials) { | ||||||
|             // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
 |             // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
 | ||||||
|             const matched = template.match( |             const matched = template.match( | ||||||
|                 new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s"), |                 new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s") | ||||||
|             ) |             ) | ||||||
|             if (matched != null) { |             if (matched != null) { | ||||||
|                 // We found a special component that should be brought to live
 |                 // We found a special component that should be brought to live
 | ||||||
|                 const partBefore = SpecialVisualizations.constructSpecification( |                 const partBefore = SpecialVisualizations.constructSpecification( | ||||||
|                     matched[1], |                     matched[1], | ||||||
|                     extraMappings, |                     extraMappings | ||||||
|                 ) |                 ) | ||||||
|                 const argument = |                 const argument = | ||||||
|                     matched[2] /* .trim()  // We don't trim, as spaces might be relevant, e.g. "what is ... of {title()}"*/ |                     matched[2] /* .trim()  // We don't trim, as spaces might be relevant, e.g. "what is ... of {title()}"*/ | ||||||
|                 const style = matched[3]?.substring(1) ?? "" |                 const style = matched[3]?.substring(1) ?? "" | ||||||
|                 const partAfter = SpecialVisualizations.constructSpecification( |                 const partAfter = SpecialVisualizations.constructSpecification( | ||||||
|                     matched[4], |                     matched[4], | ||||||
|                     extraMappings, |                     extraMappings | ||||||
|                 ) |                 ) | ||||||
|                 const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "") |                 const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "") | ||||||
|                 if (argument.length > 0) { |                 if (argument.length > 0) { | ||||||
|  | @ -343,31 +356,31 @@ 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 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([ | ||||||
|  | @ -401,10 +414,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") | ||||||
|  | @ -413,20 +426,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]) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -466,7 +479,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 | ||||||
|                         ), |                         ), | ||||||
|                     }) |                     }) | ||||||
|                 }, |                 }, | ||||||
|  | @ -491,7 +504,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 | ||||||
|  | @ -501,7 +514,7 @@ export default class SpecialVisualizations { | ||||||
|                                     return new SplitRoadWizard(<WayId>id, state) |                                     return new SplitRoadWizard(<WayId>id, state) | ||||||
|                                 } |                                 } | ||||||
|                                 return undefined |                                 return undefined | ||||||
|                             }), |                             }) | ||||||
|                     ) |                     ) | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  | @ -515,7 +528,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 | ||||||
|  | @ -538,7 +551,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 | ||||||
|  | @ -566,7 +579,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, { | ||||||
|  | @ -630,7 +643,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( | ||||||
|  | @ -640,9 +653,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(), | ||||||
|  | @ -674,7 +687,7 @@ export default class SpecialVisualizations { | ||||||
|                         AllImageProviders.LoadImagesFor(tags, imagePrefixes), |                         AllImageProviders.LoadImagesFor(tags, imagePrefixes), | ||||||
|                         tags, |                         tags, | ||||||
|                         state, |                         state, | ||||||
|                         feature, |                         feature | ||||||
|                     ) |                     ) | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  | @ -730,7 +743,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, | ||||||
|  | @ -763,7 +776,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 }) | ||||||
|                 }, |                 }, | ||||||
|  | @ -795,7 +808,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 }) | ||||||
|                 }, |                 }, | ||||||
|  | @ -853,7 +866,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] | ||||||
|  | @ -890,10 +903,10 @@ 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 | ||||||
|  | @ -901,7 +914,7 @@ export default class SpecialVisualizations { | ||||||
|                                 const getCountry = () => tagSource.data._country |                                 const getCountry = () => tagSource.data._country | ||||||
|                                 const [v, denom] = unit.findDenomination(value, getCountry) |                                 const [v, denom] = unit.findDenomination(value, getCountry) | ||||||
|                                 return unit.asHumanLongValue(v, getCountry) |                                 return unit.asHumanLongValue(v, getCountry) | ||||||
|                             }), |                             }) | ||||||
|                     ) |                     ) | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  | @ -918,7 +931,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") | ||||||
|  | @ -931,7 +944,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") | ||||||
|  | @ -967,7 +980,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) | ||||||
|  | @ -1005,10 +1018,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") | ||||||
|                             }), |                             }) | ||||||
|                     ), |                     ), | ||||||
|             }, |             }, | ||||||
|             { |             { | ||||||
|  | @ -1049,9 +1062,9 @@ export default class SpecialVisualizations { | ||||||
|                                 return undefined |                                 return undefined | ||||||
|                             } |                             } | ||||||
|                             return new SubstitutedTranslation(title, tagsSource, state).SetClass( |                             return new SubstitutedTranslation(title, tagsSource, state).SetClass( | ||||||
|                                 "px-1", |                                 "px-1" | ||||||
|                             ) |                             ) | ||||||
|                         }), |                         }) | ||||||
|                     ), |                     ), | ||||||
|             }, |             }, | ||||||
|             { |             { | ||||||
|  | @ -1067,8 +1080,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( | ||||||
|  | @ -1093,7 +1106,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.", | ||||||
|  | @ -1107,15 +1120,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" + | ||||||
|  | @ -1181,8 +1194,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] | ||||||
|                             ), |                             ) | ||||||
|                     ) |                     ) | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  | @ -1248,7 +1261,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 === "") { | ||||||
|  | @ -1265,8 +1278,8 @@ export default class SpecialVisualizations { | ||||||
|                                     download: Utils.SubstituteKeys(download, tags), |                                     download: Utils.SubstituteKeys(download, tags), | ||||||
|                                     ariaLabel: Utils.SubstituteKeys(ariaLabel, tags), |                                     ariaLabel: Utils.SubstituteKeys(ariaLabel, tags), | ||||||
|                                     newTab, |                                     newTab, | ||||||
|                                 }), |                                 }) | ||||||
|                         ), |                         ) | ||||||
|                     ) |                     ) | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  | @ -1288,7 +1301,7 @@ export default class SpecialVisualizations { | ||||||
|                             }, |                             }, | ||||||
|                         }, |                         }, | ||||||
|                         null, |                         null, | ||||||
|                         "  ", |                         "  " | ||||||
|                     ) + |                     ) + | ||||||
|                     "\n```", |                     "\n```", | ||||||
|                 args: [ |                 args: [ | ||||||
|  | @ -1310,26 +1323,30 @@ export default class SpecialVisualizations { | ||||||
|                         featureTags.map((tags) => { |                         featureTags.map((tags) => { | ||||||
|                             try { |                             try { | ||||||
|                                 const data = tags[key] |                                 const data = tags[key] | ||||||
|                                 const properties: object[] = typeof data === "string" ? JSON.parse(tags[key]) : data |                                 const properties: object[] = | ||||||
|  |                                     typeof data === "string" ? JSON.parse(tags[key]) : data | ||||||
|                                 const elements = [] |                                 const elements = [] | ||||||
|                                 for (const property of properties) { |                                 for (const property of properties) { | ||||||
|                                     const subsTr = new SubstitutedTranslation( |                                     const subsTr = new SubstitutedTranslation( | ||||||
|                                         translation, |                                         translation, | ||||||
|                                         new UIEventSource<any>(property), |                                         new UIEventSource<any>(property), | ||||||
|                                         state, |                                         state | ||||||
|                                     ) |                                     ) | ||||||
|                                     elements.push(subsTr) |                                     elements.push(subsTr) | ||||||
|                                 } |                                 } | ||||||
|                                 return new List(elements) |                                 return new List(elements) | ||||||
|                             } catch (e) { |                             } catch (e) { | ||||||
|                                 console.log("Something went wrong while generating the elements for a multi", { |                                 console.log( | ||||||
|                                     e, |                                     "Something went wrong while generating the elements for a multi", | ||||||
|                                     tags, |                                     { | ||||||
|                                     key, |                                         e, | ||||||
|                                     loaded: tags[key], |                                         tags, | ||||||
|                                 }) |                                         key, | ||||||
|  |                                         loaded: tags[key], | ||||||
|  |                                     } | ||||||
|  |                                 ) | ||||||
|                             } |                             } | ||||||
|                         }), |                         }) | ||||||
|                     ) |                     ) | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  | @ -1349,7 +1366,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) => { | ||||||
|  | @ -1361,7 +1378,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) | ||||||
|                             } |                             } | ||||||
|                         }), |                         }) | ||||||
|                     ) |                     ) | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  | @ -1381,7 +1398,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() | ||||||
|  | @ -1391,14 +1408,14 @@ 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 | ||||||
|                                 ) |                                 ) | ||||||
|                                 return new SvelteUIElement(Link, { |                                 return new SvelteUIElement(Link, { | ||||||
|                                     text: fediAccount, |                                     text: fediAccount, | ||||||
|                                     url: "https://" + host + "/@" + username, |                                     url: "https://" + host + "/@" + username, | ||||||
|                                     newTab: true, |                                     newTab: true, | ||||||
|                                 }) |                                 }) | ||||||
|                             }), |                             }) | ||||||
|                     ) |                     ) | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  | @ -1418,7 +1435,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] + "}") | ||||||
|                 }, |                 }, | ||||||
|  | @ -1439,7 +1456,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( | ||||||
|  | @ -1457,12 +1474,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") | ||||||
|                             } |                             } | ||||||
|                         }), |                         }) | ||||||
|                     ) |                     ) | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  | @ -1483,7 +1500,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 }) | ||||||
|  | @ -1499,12 +1516,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, {}), | ||||||
|  | @ -1526,7 +1543,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, | ||||||
|  | @ -1546,7 +1563,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, | ||||||
|  | @ -1566,7 +1583,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 }) | ||||||
|                 }, |                 }, | ||||||
|  | @ -1581,7 +1598,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 | ||||||
|  | @ -1603,9 +1620,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" | ||||||
|                                 ) |                                 ) | ||||||
|                             }), |                             }) | ||||||
|                     ) |                     ) | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  | @ -1625,7 +1642,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( | ||||||
|  | @ -1636,11 +1653,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] | ||||||
|                             }), |                             }) | ||||||
|                     ) |                     ) | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  | @ -1664,13 +1681,19 @@ export default class SpecialVisualizations { | ||||||
|                         doc: "Apply some postprocessing. Currently, only 'velopark' is allowed as value", |                         doc: "Apply some postprocessing. Currently, only 'velopark' is allowed as value", | ||||||
|                     }, |                     }, | ||||||
|                     { |                     { | ||||||
|                         name:"readonly", |                         name: "readonly", | ||||||
|                         required: false, |                         required: false, | ||||||
|                         doc: "If 'yes', will not show 'apply'-buttons" |                         doc: "If 'yes', will not show 'apply'-buttons", | ||||||
|                     } |                     }, | ||||||
|                 ], |                 ], | ||||||
|                 docs: "Gives an interactive element which shows a tag comparison between the OSM-object and the upstream object. This allows to copy some or all tags into OSM", |                 docs: "Gives an interactive element which shows a tag comparison between the OSM-object and the upstream object. This allows to copy some or all tags into OSM", | ||||||
|                 constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, args: string[], feature: Feature, layer: LayerConfig): BaseUIElement { |                 constr( | ||||||
|  |                     state: SpecialVisualizationState, | ||||||
|  |                     tagSource: UIEventSource<Record<string, string>>, | ||||||
|  |                     args: string[], | ||||||
|  |                     feature: Feature, | ||||||
|  |                     layer: LayerConfig | ||||||
|  |                 ): BaseUIElement { | ||||||
|                     const url = args[0] |                     const url = args[0] | ||||||
|                     const postprocessVelopark = args[2] === "velopark" |                     const postprocessVelopark = args[2] === "velopark" | ||||||
|                     const readonly = args[3] === "yes" |                     const readonly = args[3] === "yes" | ||||||
|  | @ -1681,7 +1704,7 @@ export default class SpecialVisualizations { | ||||||
|                         tags: tagSource, |                         tags: tagSource, | ||||||
|                         layer, |                         layer, | ||||||
|                         feature, |                         feature, | ||||||
|                         readonly |                         readonly, | ||||||
|                     }) |                     }) | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  | @ -1696,7 +1719,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' | ||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										4
									
								
								src/assets/svg/Square_rounded.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/assets/svg/Square_rounded.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | <script> | ||||||
|  | export let color = "#000000" | ||||||
|  | </script> | ||||||
|  |  <svg {...$$restProps} on:click on:mouseover on:mouseenter on:mouseleave on:keydown    width="500.94501"    height="500.94501"    viewBox="0 0 500.94501 500.94501"    version="1.1"    id="svg5"    sodipodi:docname="square_rounded.svg"    inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)"    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"    xmlns="http://www.w3.org/2000/svg"    xmlns:svg="http://www.w3.org/2000/svg">   <g      id="surface1"      transform="translate(0.4725,0.4725)" />   <path      class="selectable"      id="rect1"      style="fill:{color}"      d="m 75.4725,0.4725 h 350 c 41.55,0 75,33.45 75,75 v 350 c 0,41.55 -33.45,75 -75,75 h -350 c -41.55,0 -75,-33.45 -75,-75 v -350 c 0,-41.55 33.45,-75 75,-75 z" /> </svg>  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue