2020-10-14 12:15:09 +02:00
import Combine from "./Base/Combine"
2023-05-06 01:23:55 +02:00
import { FixedUiElement } from "./Base/FixedUiElement"
2021-06-11 22:51:45 +02:00
import BaseUIElement from "./BaseUIElement"
2021-06-21 00:19:19 +02:00
import Title from "./Base/Title"
import Table from "./Base/Table"
2023-05-06 01:23:55 +02:00
import { RenderingSpecification , SpecialVisualization , SpecialVisualizationState , } from "./SpecialVisualization"
import { HistogramViz } from "./Popup/HistogramViz"
import { MinimapViz } from "./Popup/MinimapViz"
import { ShareLinkViz } from "./Popup/ShareLinkViz"
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
import { MultiApplyViz } from "./Popup/MultiApplyViz"
import { AddNoteCommentViz } from "./Popup/AddNoteCommentViz"
import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz"
2022-11-02 13:47:34 +01:00
import TagApplyButton from "./Popup/TagApplyButton"
2023-05-06 01:23:55 +02:00
import { CloseNoteButton } from "./Popup/CloseNoteButton"
import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"
import { Store , Stores , UIEventSource } from "../Logic/UIEventSource"
2023-03-24 19:21:15 +01:00
import AllTagsPanel from "./Popup/AllTagsPanel.svelte"
2022-11-02 13:47:34 +01:00
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"
2023-05-06 01:23:55 +02:00
import { ImageCarousel } from "./Image/ImageCarousel"
import { ImageUploadFlow } from "./Image/ImageUploadFlow"
import { VariableUiElement } from "./Base/VariableUIElement"
import { Utils } from "../Utils"
import Wikidata , { WikidataResponse } from "../Logic/Web/Wikidata"
import { Translation } from "./i18n/Translation"
2022-11-02 13:47:34 +01:00
import Translations from "./i18n/Translations"
import ReviewForm from "./Reviews/ReviewForm"
import ReviewElement from "./Reviews/ReviewElement"
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"
import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"
2023-05-06 01:23:55 +02:00
import { SubtleButton } from "./Base/SubtleButton"
2022-11-02 13:47:34 +01:00
import Svg from "../Svg"
import Hash from "../Logic/Web/Hash"
import NoteCommentElement from "./Popup/NoteCommentElement"
import ImgurUploader from "../Logic/ImageProviders/ImgurUploader"
import FileSelectorButton from "./Input/FileSelectorButton"
2023-05-06 01:23:55 +02:00
import { LoginToggle } from "./Popup/LoginButton"
2022-11-02 13:47:34 +01:00
import Toggle from "./Input/Toggle"
2023-05-06 01:23:55 +02:00
import { SubstitutedTranslation } from "./SubstitutedTranslation"
2022-11-02 13:47:34 +01:00
import List from "./Base/List"
import StatisticsPanel from "./BigComponents/StatisticsPanel"
import AutoApplyButton from "./Popup/AutoApplyButton"
2023-05-06 01:23:55 +02:00
import { LanguageElement } from "./Popup/LanguageElement"
2023-01-21 23:58:14 +01:00
import FeatureReviews from "../Logic/Web/MangroveReviews"
2023-02-14 00:09:04 +01:00
import Maproulette from "../Logic/Maproulette"
2023-02-15 18:24:08 +01:00
import SvelteUIElement from "./Base/SvelteUIElement"
2023-05-06 01:23:55 +02:00
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
2023-03-31 03:28:11 +02:00
import QuestionViz from "./Popup/QuestionViz"
2023-05-06 01:23:55 +02:00
import { Feature , Point } from "geojson"
import { GeoOperations } from "../Logic/GeoOperations"
2023-04-06 01:33:08 +02:00
import CreateNewNote from "./Popup/CreateNewNote.svelte"
import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte"
2023-04-07 02:13:57 +02:00
import UserProfile from "./BigComponents/UserProfile.svelte"
import LanguagePicker from "./LanguagePicker"
import Link from "./Base/Link"
2023-04-13 22:44:35 +02:00
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
2023-05-06 01:23:55 +02:00
import NearbyImages , { NearbyImageOptions , P4CPicture , SelectOneNearbyImage , } from "./Popup/NearbyImages"
import { Tag } from "../Logic/Tags/Tag"
2023-04-13 22:44:35 +02:00
import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction"
2023-05-06 01:23:55 +02:00
import { And } from "../Logic/Tags/And"
import { SaveButton } from "./Popup/SaveButton"
2023-04-13 22:44:35 +02:00
import Lazy from "./Base/Lazy"
2023-05-06 01:23:55 +02:00
import { CheckBox } from "./Input/Checkboxes"
2023-04-13 22:44:35 +02:00
import Slider from "./Input/Slider"
2023-06-02 08:42:08 +02:00
import { OsmTags , WayId } from "../Models/OsmFeature"
2023-04-14 02:42:57 +02:00
import MoveWizard from "./Popup/MoveWizard"
2023-04-20 01:52:23 +02:00
import SplitRoadWizard from "./Popup/SplitRoadWizard"
2023-05-06 01:23:55 +02:00
import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz"
2023-04-21 16:02:36 +02:00
import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"
2023-05-06 01:23:55 +02:00
import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte" ;
2023-06-01 02:52:21 +02:00
import { PointImportButtonViz } from "./Popup/ImportButtons/PointImportButtonViz" ;
2023-05-30 02:52:22 +02:00
import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz" ;
2023-06-01 02:52:21 +02:00
import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz" ;
2023-06-01 23:40:05 +02:00
import DeleteWizard from "./Popup/DeleteFlow/DeleteWizard.svelte" ;
2023-06-06 00:01:01 +02:00
import { OpenJosm } from "./BigComponents/OpenJosm" ;
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte" ;
2022-05-06 12:41:24 +02:00
2023-04-13 22:44:35 +02:00
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
args : { name : string ; defaultValue? : string ; doc : string ; required? : boolean } [ ] = [
{
name : "mode" ,
defaultValue : "expandable" ,
doc : "Indicates how this component is initialized. Options are: \n\n- `open`: always show and load the pictures\n- `collapsable`: show the pictures, but a user can collapse them\n- `expandable`: shown by default; but a user can collapse them." ,
} ,
{
name : "mapillary" ,
defaultValue : "true" ,
doc : "If 'true', includes a link to mapillary on this location." ,
} ,
]
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"
funcName = "nearby_images"
constr (
state : SpecialVisualizationState ,
tagSource : UIEventSource < Record < string , string > > ,
args : string [ ] ,
feature : Feature ,
layer : LayerConfig
) : BaseUIElement {
const t = Translations . t . image . nearbyPictures
const mode : "open" | "expandable" | "collapsable" = < any > args [ 0 ]
const [ lon , lat ] = GeoOperations . centerpointCoordinates ( feature )
const id : string = tagSource . data [ "id" ]
const canBeEdited : boolean = ! ! id ? . match ( "(node|way|relation)/-?[0-9]+" )
const selectedImage = new UIEventSource < P4CPicture > ( undefined )
let saveButton : BaseUIElement = undefined
if ( canBeEdited ) {
const confirmText : BaseUIElement = new SubstitutedTranslation (
t . confirm ,
tagSource ,
state
)
const onSave = async ( ) = > {
console . log ( "Selected a picture..." , selectedImage . data )
const osmTags = selectedImage . data . osmTags
const tags : Tag [ ] = [ ]
for ( const key in osmTags ) {
tags . push ( new Tag ( key , osmTags [ key ] ) )
}
await state ? . changes ? . applyAction (
new ChangeTagAction ( id , new And ( tags ) , tagSource . data , {
theme : state?.layout.id ,
changeType : "link-image" ,
} )
)
}
saveButton = new SaveButton (
selectedImage ,
2023-05-09 00:06:51 +02:00
state ,
2023-04-13 22:44:35 +02:00
confirmText ,
t . noImageSelected
)
. onClick ( onSave )
. SetClass ( "flex justify-end" )
}
const nearby = new Lazy ( ( ) = > {
const towardsCenter = new CheckBox ( t . onlyTowards , false )
const maxSearchRadius = 100
const stepSize = 10
const defaultValue = Math . floor ( maxSearchRadius / ( 2 * stepSize ) ) * stepSize
const fromOsmPreferences = state ? . osmConnection
? . GetPreference ( "nearby-images-radius" , "" + defaultValue )
. sync (
( s ) = > Number ( s ) ,
[ ] ,
( i ) = > "" + i
)
const radiusValue = new UIEventSource ( fromOsmPreferences . data )
radiusValue . addCallbackAndRunD ( ( v ) = > fromOsmPreferences . setData ( v ) )
const radius = new Slider ( stepSize , maxSearchRadius , {
value : radiusValue ,
step : 10 ,
} )
const alreadyInTheImage = AllImageProviders . LoadImagesFor ( tagSource )
const options : NearbyImageOptions & { value } = {
lon ,
lat ,
searchRadius : maxSearchRadius ,
shownRadius : radius.GetValue ( ) ,
value : selectedImage ,
blacklist : alreadyInTheImage ,
towardscenter : towardsCenter.GetValue ( ) ,
maxDaysOld : 365 * 3 ,
}
const slideshow = canBeEdited
? new SelectOneNearbyImage ( options , state . indexedFeatures )
: new NearbyImages ( options , state . indexedFeatures )
const controls = new Combine ( [
towardsCenter ,
new Combine ( [
new VariableUiElement (
2023-05-29 02:18:24 +02:00
radius . GetValue ( ) . map ( ( radius ) = > t . withinRadius . Subs ( { radius } ) )
2023-04-13 22:44:35 +02:00
) ,
radius ,
] ) . SetClass ( "flex justify-between" ) ,
] ) . SetClass ( "flex flex-col" )
return new Combine ( [
slideshow ,
controls ,
saveButton ,
new MapillaryLinkVis ( ) . constr ( state , tagSource , [ ] , feature ) . SetClass ( "mt-6" ) ,
] )
} )
let withEdit : BaseUIElement = nearby
if ( canBeEdited ) {
withEdit = new Combine ( [ t . hasMatchingPicture , nearby ] ) . SetClass ( "flex flex-col" )
}
if ( mode === "open" ) {
return withEdit
}
const toggleState = new UIEventSource < boolean > ( mode === "collapsable" )
return new Toggle (
new Combine ( [ new Title ( t . title ) , withEdit ] ) ,
new Title ( t . browseNearby ) . onClick ( ( ) = > toggleState . setData ( true ) ) ,
toggleState
)
}
}
class StealViz implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
funcName = "steal"
docs = "Shows a tagRendering from a different object as if this was the object itself"
args = [
{
name : "featureId" ,
doc : "The key of the attribute which contains the id of the feature from which to use the tags" ,
required : true ,
} ,
{
name : "tagRenderingId" ,
doc : "The layer-id and tagRenderingId to render. Can be multiple value if ';'-separated (in which case every value must also contain the layerId, e.g. `layerId.tagRendering0; layerId.tagRendering1`). Note: this can cause layer injection" ,
required : true ,
} ,
]
2023-04-14 02:42:57 +02:00
2023-04-13 22:44:35 +02:00
constr ( state : SpecialVisualizationState , featureTags , args ) {
const [ featureIdKey , layerAndtagRenderingIds ] = args
const tagRenderings : [ LayerConfig , TagRenderingConfig ] [ ] = [ ]
for ( const layerAndTagRenderingId of layerAndtagRenderingIds . split ( ";" ) ) {
const [ layerId , tagRenderingId ] = layerAndTagRenderingId . trim ( ) . split ( "." )
const layer = state . layout . layers . find ( ( l ) = > l . id === layerId )
const tagRendering = layer . tagRenderings . find ( ( tr ) = > tr . id === tagRenderingId )
tagRenderings . push ( [ layer , tagRendering ] )
}
if ( tagRenderings . length === 0 ) {
throw "Could not create stolen tagrenddering: tagRenderings not found"
}
return new VariableUiElement (
featureTags . map ( ( tags ) = > {
const featureId = tags [ featureIdKey ]
if ( featureId === undefined ) {
return undefined
}
const otherTags = state . featureProperties . getStore ( featureId )
2023-05-06 01:23:55 +02:00
const otherFeature = state . indexedFeatures . featuresById . data . get ( featureId ) ;
2023-04-13 22:44:35 +02:00
const elements : BaseUIElement [ ] = [ ]
for ( const [ layer , tagRendering ] of tagRenderings ) {
2023-05-06 01:23:55 +02:00
elements . push ( new SvelteUIElement ( TagRenderingEditable , {
config : tagRendering ,
tags : otherTags ,
selectedElement : otherFeature ,
2023-04-13 22:44:35 +02:00
state ,
2023-05-06 01:23:55 +02:00
layer
} ) )
2023-04-13 22:44:35 +02:00
}
if ( elements . length === 1 ) {
return elements [ 0 ]
}
return new Combine ( elements ) . SetClass ( "flex flex-col" )
2023-05-06 01:23:55 +02:00
} , [ state . indexedFeatures . featuresById ] )
2023-04-13 22:44:35 +02:00
)
}
getLayerDependencies ( args ) : string [ ] {
const [ _ , tagRenderingId ] = args
if ( tagRenderingId . indexOf ( "." ) < 0 ) {
throw "Error: argument 'layerId.tagRenderingId' of special visualisation 'steal' should contain a dot"
}
const [ layerId , __ ] = tagRenderingId . split ( "." )
return [ layerId ]
}
}
2023-04-14 02:42:57 +02:00
2020-10-09 20:10:21 +02:00
export default class SpecialVisualizations {
2022-11-02 13:47:34 +01:00
public static specialVisualizations : SpecialVisualization [ ] = SpecialVisualizations . initList ( )
2021-12-12 02:59:24 +01:00
2023-04-07 02:13:57 +02:00
static undoEncoding ( str : string ) {
return str
. trim ( )
. replace ( /&LPARENS/g , "(" )
. replace ( /&RPARENS/g , ")" )
. replace ( /&LBRACE/g , "{" )
. replace ( /&RBRACE/g , "}" )
. replace ( /&COMMA/g , "," )
}
2023-03-29 17:21:20 +02:00
/ * *
*
* For a given string , returns a specification what parts are fixed and what parts are special renderings .
* Note that _normal_ substitutions are ignored .
*
* // Return empty list on empty input
2023-04-13 20:58:49 +02:00
* SpecialVisualizations . constructSpecification ( "" ) // => []
2023-03-29 17:21:20 +02:00
*
* // Advanced cases with commas, braces and newlines should be handled without problem
2023-04-13 20:58:49 +02:00
* const templates = SpecialVisualizations . constructSpecification ( "{send_email(&LBRACEemail&RBRACE,Broken bicycle pump,Hello&COMMA\n\nWith this email&COMMA I'd like to inform you that the bicycle pump located at https://mapcomplete.osm.be/cyclofix?lat=&LBRACE_lat&RBRACE&lon=&LBRACE_lon&RBRACE&z=18#&LBRACEid&RBRACE is broken.\n\n Kind regards,Report this bicycle pump as broken)}" )
* const templ = < Exclude < RenderingSpecification , string > > templates [ 0 ]
* templ . func . funcName // => "send_email"
* templ . args [ 0 ] = "{email}"
2023-03-29 17:21:20 +02:00
* /
public static constructSpecification (
template : string ,
extraMappings : SpecialVisualization [ ] = [ ]
) : RenderingSpecification [ ] {
if ( template === "" ) {
return [ ]
}
2023-03-31 03:28:11 +02:00
if ( template [ "type" ] !== undefined ) {
2023-04-06 01:33:08 +02:00
console . trace (
"Got a non-expanded template while constructing the specification:" ,
template
)
2023-03-31 03:28:11 +02:00
throw "Got a non-expanded template while constructing the specification"
}
2023-03-29 17:21:20 +02:00
const allKnownSpecials = extraMappings . concat ( SpecialVisualizations . specialVisualizations )
for ( const knownSpecial of allKnownSpecials ) {
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
const matched = template . match (
new RegExp ( ` (.*){ ${ knownSpecial . funcName } \\ ((.*?) \\ )(:.*)?}(.*) ` , "s" )
)
if ( matched != null ) {
// We found a special component that should be brought to live
const partBefore = SpecialVisualizations . constructSpecification (
matched [ 1 ] ,
extraMappings
)
2023-05-29 02:18:24 +02:00
const argument = matched [ 2 ] /* .trim() // We don't trim, as spaces might be relevant, e.g. "what is ... of {title()}"*/
2023-03-29 17:21:20 +02:00
const style = matched [ 3 ] ? . substring ( 1 ) ? ? ""
const partAfter = SpecialVisualizations . constructSpecification (
matched [ 4 ] ,
extraMappings
)
const args = knownSpecial . args . map ( ( arg ) = > arg . defaultValue ? ? "" )
if ( argument . length > 0 ) {
2023-04-07 02:13:57 +02:00
const realArgs = argument . split ( "," ) . map ( ( str ) = > this . undoEncoding ( str ) )
2023-03-29 17:21:20 +02:00
for ( let i = 0 ; i < realArgs . length ; i ++ ) {
if ( args . length <= i ) {
args . push ( realArgs [ i ] )
} else {
args [ i ] = realArgs [ i ]
}
}
}
const element : RenderingSpecification = {
args : args ,
style : style ,
func : knownSpecial ,
}
return [ . . . partBefore , element , . . . partAfter ]
}
}
// Let's to a small sanity check to help the theme designers:
if ( template . search ( /{[^}]+\([^}]*\)}/ ) >= 0 ) {
// Hmm, we might have found an invalid rendering name
2023-05-29 02:18:24 +02:00
let suggestion = ""
if ( SpecialVisualizations . specialVisualizations ? . length > 0 ) {
suggestion =
"did you mean one of: " +
SpecialVisualizations . specialVisualizations
. map ( ( sp ) = > sp . funcName + "()" )
. join ( ", " )
}
2023-03-29 17:21:20 +02:00
console . warn (
"Found a suspicious special rendering value in: " ,
2023-05-29 02:18:24 +02:00
template , suggestion
2023-03-29 17:21:20 +02:00
)
}
// IF we end up here, no changes have to be made - except to remove any resting {}
return [ template ]
}
2023-02-14 00:09:04 +01:00
public static DocumentationFor ( viz : string | SpecialVisualization ) : BaseUIElement | undefined {
if ( typeof viz === "string" ) {
viz = SpecialVisualizations . specialVisualizations . find ( ( sv ) = > sv . funcName === viz )
}
if ( viz === undefined ) {
return undefined
}
return new Combine ( [
new Title ( viz . funcName , 3 ) ,
viz . docs ,
viz . args . length > 0
? new Table (
2023-05-29 02:18:24 +02:00
[ "name" , "default" , "description" ] ,
viz . args . map ( ( arg ) = > {
let defaultArg = arg . defaultValue ? ? "_undefined_"
if ( defaultArg == "" ) {
defaultArg = "_empty string_"
}
return [ arg . name , defaultArg , arg . doc ]
} )
)
2023-02-14 00:09:04 +01:00
: undefined ,
new Title ( "Example usage of " + viz . funcName , 4 ) ,
new FixedUiElement (
viz . example ? ?
2023-05-29 02:18:24 +02:00
"`{" +
viz . funcName +
"(" +
viz . args . map ( ( arg ) = > arg . defaultValue ) . join ( "," ) +
")}`"
2023-02-14 00:09:04 +01:00
) . SetClass ( "literal-code" ) ,
] )
}
public static HelpMessage() {
const helpTexts = SpecialVisualizations . specialVisualizations . map ( ( viz ) = >
SpecialVisualizations . DocumentationFor ( viz )
)
return new Combine ( [
new Combine ( [
new Title ( "Special tag renderings" , 1 ) ,
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's." ,
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args" ,
new Title ( "Using expanded syntax" , 4 ) ,
` Instead of using \` {"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}} \` , one can also write ` ,
new FixedUiElement (
JSON . stringify (
{
render : {
special : {
type : "some_special_visualisation" ,
argname : "some_arg" ,
message : {
en : "some other really long message" ,
nl : "een boodschap in een andere taal" ,
} ,
other_arg_name : "more args" ,
} ,
before : {
en : "Some text to prefix before the special element (e.g. a title)" ,
nl : "Een tekst om voor het element te zetten (bv. een titel)" ,
} ,
after : {
en : "Some text to put after the element, e.g. a footer" ,
} ,
} ,
} ,
null ,
" "
)
) . SetClass ( "code" ) ,
'In other words: use `{ "before": ..., "after": ..., "special": {"type": ..., "argname": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)' ,
] ) . SetClass ( "flex flex-col" ) ,
. . . helpTexts ,
] ) . SetClass ( "flex flex-col" )
}
2023-04-06 01:33:08 +02:00
// noinspection JSUnusedGlobalSymbols
public static renderExampleOfSpecial (
state : SpecialVisualizationState ,
s : SpecialVisualization
) : BaseUIElement {
const examples =
s . structuredExamples === undefined
? [ ]
: s . structuredExamples ( ) . map ( ( e ) = > {
2023-05-29 02:18:24 +02:00
return s . constr (
state ,
new UIEventSource < Record < string , string > > ( e . feature . properties ) ,
e . args ,
e . feature ,
undefined
)
} )
2023-04-06 01:33:08 +02:00
return new Combine ( [ new Title ( s . funcName ) , s . docs , . . . examples ] )
}
2022-11-02 13:47:34 +01:00
private static initList ( ) : SpecialVisualization [ ] {
2022-01-08 04:22:50 +01:00
const specialVisualizations : SpecialVisualization [ ] = [
2023-03-31 03:28:11 +02:00
new QuestionViz ( ) ,
2023-04-02 02:59:20 +02:00
{
funcName : "add_new_point" ,
docs : "An element which allows to add a new point on the 'last_click'-location. Only makes sense in the layer `last_click`" ,
args : [ ] ,
2023-04-06 01:33:08 +02:00
constr ( state : SpecialVisualizationState , _ , __ , feature ) : BaseUIElement {
let [ lon , lat ] = GeoOperations . centerpointCoordinates ( feature )
return new SvelteUIElement ( AddNewPoint , {
state ,
2023-05-29 02:18:24 +02:00
coordinate : { lon , lat } ,
2023-04-06 01:33:08 +02:00
} )
2023-04-02 02:59:20 +02:00
} ,
} ,
2023-04-07 02:13:57 +02:00
{
funcName : "user_profile" ,
args : [ ] ,
docs : "A component showing information about the currently logged in user (username, profile description, profile picture + link to edit them). Mostly meant to be used in the 'user-settings'" ,
constr ( state : SpecialVisualizationState ) : BaseUIElement {
return new SvelteUIElement ( UserProfile , {
osmConnection : state.osmConnection ,
} )
} ,
} ,
{
funcName : "language_picker" ,
args : [ ] ,
docs : "A component to set the language of the user interface" ,
constr ( state : SpecialVisualizationState ) : BaseUIElement {
return new LanguagePicker (
state . layout . language ,
2023-06-06 00:01:01 +02:00
state . userRelatedState . language
2023-04-07 02:13:57 +02:00
)
} ,
} ,
{
funcName : "logout" ,
args : [ ] ,
docs : "Shows a button where the user can log out" ,
constr ( state : SpecialVisualizationState ) : BaseUIElement {
2023-05-08 01:55:21 +02:00
return new SubtleButton ( Svg . logout_svg ( ) , Translations . t . general . logout , {
2023-04-07 02:13:57 +02:00
imgSize : "w-6 h-6" ,
} ) . onClick ( ( ) = > {
state . osmConnection . LogOut ( )
} )
} ,
} ,
2022-10-28 04:33:05 +02:00
new HistogramViz ( ) ,
new StealViz ( ) ,
new MinimapViz ( ) ,
2023-04-14 02:42:57 +02:00
{
funcName : "split_button" ,
docs : "Adds a button which allows to split a way" ,
args : [ ] ,
constr (
state : SpecialVisualizationState ,
2023-04-20 01:52:23 +02:00
tagSource : UIEventSource < Record < string , string > >
2023-04-14 02:42:57 +02:00
) : BaseUIElement {
return new VariableUiElement (
tagSource
. map ( ( tags ) = > tags . id )
2023-04-20 01:52:23 +02:00
. map ( ( id ) = > {
if ( id . startsWith ( "way/" ) ) {
return new SplitRoadWizard ( < WayId > id , state )
}
return undefined
} )
2023-04-14 02:42:57 +02:00
)
} ,
} ,
{
funcName : "move_button" ,
docs : "Adds a button which allows to move the object to another location. The config will be read from the layer config" ,
args : [ ] ,
constr (
state : SpecialVisualizationState ,
tagSource : UIEventSource < Record < string , string > > ,
argument : string [ ] ,
feature : Feature ,
layer : LayerConfig
) : BaseUIElement {
if ( feature . geometry . type !== "Point" ) {
return undefined
}
return new MoveWizard (
< Feature < Point > > feature ,
< UIEventSource < OsmTags > > tagSource ,
state ,
layer . allowMove
)
} ,
} ,
{
funcName : "delete_button" ,
docs : "Adds a button which allows to delete the object at this location. The config will be read from the layer config" ,
args : [ ] ,
constr (
state : SpecialVisualizationState ,
tagSource : UIEventSource < Record < string , string > > ,
argument : string [ ] ,
feature : Feature ,
layer : LayerConfig
) : BaseUIElement {
2023-06-01 23:40:05 +02:00
return new SvelteUIElement ( DeleteWizard , {
tags : tagSource ,
deleteConfig : layer.deletion ,
state ,
feature ,
layer
} )
2023-04-14 02:42:57 +02:00
} ,
} ,
2022-11-02 13:47:34 +01:00
new ShareLinkViz ( ) ,
2023-04-20 18:58:31 +02:00
new ExportAsGpxViz ( ) ,
2022-10-28 04:33:05 +02:00
new UploadToOsmViz ( ) ,
new MultiApplyViz ( ) ,
new AddNoteCommentViz ( ) ,
2023-04-06 01:33:08 +02:00
{
funcName : "open_note" ,
args : [ ] ,
docs : "Creates a new map note on the given location. This options is placed in the 'last_click'-popup automatically if the 'notes'-layer is enabled" ,
constr (
state : SpecialVisualizationState ,
tagSource : UIEventSource < Record < string , string > > ,
argument : string [ ] ,
feature : Feature
) : BaseUIElement {
const [ lon , lat ] = GeoOperations . centerpointCoordinates ( feature )
2023-05-29 02:18:24 +02:00
return new SvelteUIElement ( CreateNewNote , { state , coordinate : { lon , lat } } )
2023-04-06 01:33:08 +02:00
} ,
} ,
2023-03-28 05:13:48 +02:00
new CloseNoteButton ( ) ,
2022-10-28 04:33:05 +02:00
new PlantNetDetectionViz ( ) ,
2023-03-28 05:13:48 +02:00
new TagApplyButton ( ) ,
2023-06-01 02:52:21 +02:00
new PointImportButtonViz ( ) ,
2023-05-30 02:52:22 +02:00
new WayImportButtonViz ( ) ,
2023-06-01 02:52:21 +02:00
new ConflateImportButtonViz ( ) ,
2023-03-28 05:13:48 +02:00
2022-10-28 04:33:05 +02:00
new NearbyImageVis ( ) ,
2023-03-28 05:13:48 +02:00
2021-12-12 02:59:24 +01:00
{
funcName : "wikipedia" ,
2023-04-21 16:02:36 +02:00
docs : "A box showing the corresponding wikipedia article(s) - based on the **wikidata** tag." ,
2021-12-12 02:59:24 +01:00
args : [
2021-06-23 02:15:28 +02:00
{
2021-12-12 02:59:24 +01:00
name : "keyToShowWikipediaFor" ,
2022-05-01 20:56:16 +02:00
doc : "Use the wikidata entry from this key to show the wikipedia article for. Multiple keys can be given (separated by ';'), in which case the first matching value is used" ,
defaultValue : "wikidata;wikipedia" ,
2021-06-23 02:15:28 +02:00
} ,
2021-12-12 02:59:24 +01:00
] ,
example :
"`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height" ,
2023-05-06 01:23:55 +02:00
constr : ( _ , tagsSource , args ) = > {
2022-05-01 20:56:16 +02:00
const keys = args [ 0 ] . split ( ";" ) . map ( ( k ) = > k . trim ( ) )
2023-04-21 16:02:36 +02:00
const wikiIds : Store < string [ ] > = tagsSource . map ( ( tags ) = > {
const key = keys . find ( ( k ) = > tags [ k ] !== undefined && tags [ k ] !== "" )
return tags [ key ] ? . split ( ";" ) ? . map ( ( id ) = > id . trim ( ) )
} )
return new SvelteUIElement ( WikipediaPanel , {
wikiIds ,
} )
2021-12-12 02:59:24 +01:00
} ,
2022-09-08 21:40:48 +02:00
} ,
2022-04-22 01:45:54 +02:00
{
funcName : "wikidata_label" ,
docs : "Shows the label of the corresponding wikidata-item" ,
args : [
{
name : "keyToShowWikidataFor" ,
doc : "Use the wikidata entry from this key to show the label" ,
defaultValue : "wikidata" ,
} ,
] ,
example :
"`{wikidata_label()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the label itself" ,
constr : ( _ , tagsSource , args ) = >
new VariableUiElement (
tagsSource
. map ( ( tags ) = > tags [ args [ 0 ] ] )
. map ( ( wikidata ) = > {
wikidata = Utils . NoEmpty (
wikidata ? . split ( ";" ) ? . map ( ( wd ) = > wd . trim ( ) ) ? ? [ ]
) [ 0 ]
const entry = Wikidata . LoadWikidataEntry ( wikidata )
return new VariableUiElement (
entry . map ( ( e ) = > {
if ( e === undefined || e [ "success" ] === undefined ) {
return wikidata
}
const response = < WikidataResponse > e [ "success" ]
return Translation . fromMap ( response . labels )
} )
2022-09-08 21:40:48 +02:00
)
2022-04-22 01:45:54 +02:00
} )
2022-09-08 21:40:48 +02:00
) ,
2022-04-22 01:45:54 +02:00
} ,
2023-03-28 05:13:48 +02:00
new MapillaryLinkVis ( ) ,
new LanguageElement ( ) ,
{
funcName : "all_tags" ,
docs : "Prints all key-value pairs of the object - used for debugging" ,
args : [ ] ,
constr : ( state , tags : UIEventSource < any > ) = >
2023-05-29 02:18:24 +02:00
new SvelteUIElement ( AllTagsPanel , { tags , state } ) ,
2023-03-28 05:13:48 +02:00
} ,
{
funcName : "image_carousel" ,
docs : "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)" ,
args : [
{
name : "image_key" ,
defaultValue : AllImageProviders.defaultKeys.join ( "," ) ,
doc : "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated " ,
} ,
] ,
constr : ( state , tags , args ) = > {
let imagePrefixes : string [ ] = undefined
if ( args . length > 0 ) {
imagePrefixes = [ ] . concat ( . . . args . map ( ( a ) = > a . split ( "," ) ) )
}
return new ImageCarousel (
AllImageProviders . LoadImagesFor ( tags , imagePrefixes ) ,
tags ,
state
)
} ,
} ,
{
funcName : "image_upload" ,
docs : "Creates a button where a user can upload an image to IMGUR" ,
args : [
{
name : "image-key" ,
doc : "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)" ,
defaultValue : "image" ,
} ,
{
name : "label" ,
doc : "The text to show on the button" ,
defaultValue : "Add image" ,
} ,
] ,
constr : ( state , tags , args ) = > {
return new ImageUploadFlow ( tags , state , args [ 0 ] , args [ 1 ] )
} ,
} ,
2021-12-12 02:59:24 +01:00
{
funcName : "reviews" ,
docs : "Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten" ,
example :
"`{reviews()}` for a vanilla review, `{reviews(name, play_forest)}` to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used" ,
args : [
{
name : "subjectKey" ,
defaultValue : "name" ,
doc : "The key to use to determine the subject. If specified, the subject will be <b>tags[subjectKey]</b>" ,
} ,
{
name : "fallback" ,
doc : "The identifier to use, if <i>tags[subjectKey]</i> as specified above is not available. This is effectively a fallback value" ,
} ,
] ,
2023-03-28 05:13:48 +02:00
constr : ( state , tags , args , feature ) = > {
2023-01-21 23:58:14 +01:00
const nameKey = args [ 0 ] ? ? "name"
let fallbackName = args [ 1 ]
2023-03-28 05:13:48 +02:00
const mangrove = FeatureReviews . construct (
feature ,
tags ,
state . userRelatedState . mangroveIdentity ,
{
nameKey : nameKey ,
fallbackName ,
}
)
2023-01-21 23:58:14 +01:00
const form = new ReviewForm ( ( r ) = > mangrove . createReview ( r ) , state )
return new ReviewElement ( mangrove , form )
2021-12-12 02:59:24 +01:00
} ,
2022-09-08 21:40:48 +02:00
} ,
2021-12-12 02:59:24 +01:00
{
funcName : "opening_hours_table" ,
docs : "Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'." ,
args : [
{
2021-06-20 03:09:55 +02:00
name : "key" ,
2021-12-12 02:59:24 +01:00
defaultValue : "opening_hours" ,
doc : "The tagkey from which the table is constructed." ,
} ,
{
name : "prefix" ,
defaultValue : "" ,
doc : "Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__" ,
} ,
{
name : "postfix" ,
defaultValue : "" ,
doc : "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__" ,
} ,
] ,
example :
"A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`" ,
2022-01-19 20:34:04 +01:00
constr : ( state , tagSource : UIEventSource < any > , args ) = > {
2021-12-21 18:35:31 +01:00
return new OpeningHoursVisualization (
tagSource ,
state ,
args [ 0 ] ,
args [ 1 ] ,
args [ 2 ]
)
2021-12-12 02:59:24 +01:00
} ,
2022-09-08 21:40:48 +02:00
} ,
2021-12-12 02:59:24 +01:00
{
funcName : "live" ,
docs : "Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed_value)}" ,
example :
"{live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}" ,
args : [
{
2022-04-22 01:45:54 +02:00
name : "Url" ,
2022-03-29 00:20:10 +02:00
doc : "The URL to load" ,
required : true ,
2021-12-12 02:59:24 +01:00
} ,
{
name : "Shorthands" ,
doc : "A list of shorthands, of the format 'shorthandname:path.path.path'. separated by ;" ,
} ,
{
2022-03-29 00:20:10 +02:00
name : "path" ,
doc : "The path (or shorthand) that should be returned" ,
2021-12-12 02:59:24 +01:00
} ,
] ,
2023-03-28 05:13:48 +02:00
constr : ( _ , tagSource : UIEventSource < any > , args ) = > {
2021-12-12 02:59:24 +01:00
const url = args [ 0 ]
const shorthands = args [ 1 ]
const neededValue = args [ 2 ]
const source = LiveQueryHandler . FetchLiveData ( url , shorthands . split ( ";" ) )
return new VariableUiElement (
source . map ( ( data ) = > data [ neededValue ] ? ? "Loading..." )
2022-09-08 21:40:48 +02:00
)
2021-12-12 02:59:24 +01:00
} ,
2022-09-08 21:40:48 +02:00
} ,
2021-12-12 02:59:24 +01:00
{
funcName : "canonical" ,
2022-07-26 12:05:34 +02:00
docs : "Converts a short, canonical value into the long, translated text including the unit. This only works if a `unit` is defined for the corresponding value. The unit specification will be included in the text. " ,
example :
"If the object has `length=42`, then `{canonical(length)}` will be shown as **42 meter** (in english), **42 metre** (in french), ..." ,
2021-12-12 02:59:24 +01:00
args : [
{
name : "key" ,
2022-03-29 00:20:10 +02:00
doc : "The key of the tag to give the canonical text for" ,
required : true ,
2021-12-12 02:59:24 +01:00
} ,
] ,
constr : ( state , tagSource , args ) = > {
const key = args [ 0 ]
return new VariableUiElement (
tagSource
. map ( ( tags ) = > tags [ key ] )
. map ( ( value ) = > {
if ( value === undefined ) {
return undefined
}
2022-04-28 00:32:15 +02:00
const allUnits = [ ] . concat (
2023-03-28 05:13:48 +02:00
. . . ( state ? . layout ? . layers ? . map ( ( lyr ) = > lyr . units ) ? ? [ ] )
2022-09-08 21:40:48 +02:00
)
2021-12-12 02:59:24 +01:00
const unit = allUnits . filter ( ( unit ) = >
unit . isApplicableToKey ( key )
) [ 0 ]
if ( unit === undefined ) {
return value
}
return unit . asHumanLongValue ( value )
} )
)
} ,
2022-09-08 21:40:48 +02:00
} ,
2021-12-23 21:28:41 +01:00
{
funcName : "export_as_geojson" ,
docs : "Exports the selected feature as GeoJson-file" ,
args : [ ] ,
2023-04-20 18:58:31 +02:00
constr : ( state , tagSource , tagsSource , feature , layer ) = > {
2021-12-23 21:28:41 +01:00
const t = Translations . t . general . download
2022-09-08 21:40:48 +02:00
2021-12-23 21:28:41 +01:00
return new SubtleButton (
2023-05-08 01:55:21 +02:00
Svg . download_svg ( ) ,
2021-12-23 21:28:41 +01:00
new Combine ( [
t . downloadFeatureAsGeojson . SetClass ( "font-bold text-lg" ) ,
t . downloadGeoJsonHelper . SetClass ( "subtle" ) ,
] ) . SetClass ( "flex flex-col" )
) . onClick ( ( ) = > {
console . log ( "Exporting as Geojson" )
const tags = tagSource . data
const title =
2023-04-20 18:58:31 +02:00
layer ? . title ? . GetRenderValue ( tags ) ? . Subs ( tags ) ? . txt ? ? "geojson"
2021-12-23 21:28:41 +01:00
const data = JSON . stringify ( feature , null , " " )
Utils . offerContentsAsDownloadableFile (
data ,
title + "_mapcomplete_export.geojson" ,
{
mimetype : "application/vnd.geo+json" ,
}
)
} )
} ,
2022-09-08 21:40:48 +02:00
} ,
2021-12-23 21:28:41 +01:00
{
funcName : "open_in_iD" ,
docs : "Opens the current view in the iD-editor" ,
args : [ ] ,
2022-01-08 04:22:50 +01:00
constr : ( state , feature ) = > {
2023-06-06 00:01:01 +02:00
return new SvelteUIElement ( OpenIdEditor ,
{ mapProperties : state.mapProperties , objectId : feature.data.id } )
2021-12-23 21:28:41 +01:00
} ,
2022-09-08 21:40:48 +02:00
} ,
2022-06-08 12:53:04 +02:00
{
funcName : "open_in_josm" ,
docs : "Opens the current view in the JOSM-editor" ,
args : [ ] ,
2023-03-28 05:13:48 +02:00
constr : ( state ) = > {
return new OpenJosm ( state . osmConnection , state . mapProperties . bounds )
2022-06-08 12:53:04 +02:00
} ,
2022-09-08 21:40:48 +02:00
} ,
2021-12-12 02:59:24 +01:00
{
funcName : "clear_location_history" ,
docs : "A button to remove the travelled track information from the device" ,
args : [ ] ,
constr : ( state ) = > {
return new SubtleButton (
Svg . delete_icon_svg ( ) . SetStyle ( "height: 1.5rem" ) ,
Translations . t . general . removeLocationHistory
) . onClick ( ( ) = > {
state . historicalUserLocations . features . setData ( [ ] )
Hash . hash . setData ( undefined )
} )
2022-01-07 04:14:53 +01:00
} ,
2022-09-08 21:40:48 +02:00
} ,
2022-01-08 14:08:04 +01:00
{
2022-01-08 04:22:50 +01:00
funcName : "visualize_note_comments" ,
2022-01-12 02:31:51 +01:00
docs : "Visualises the comments for notes" ,
2022-09-08 21:40:48 +02:00
args : [
{
2022-01-08 04:22:50 +01:00
name : "commentsKey" ,
doc : "The property name of the comments, which should be stringified json" ,
defaultValue : "comments" ,
2022-09-08 21:40:48 +02:00
} ,
{
2022-01-12 02:31:51 +01:00
name : "start" ,
2022-01-26 21:40:38 +01:00
doc : "Drop the first 'start' comments" ,
2022-01-12 02:31:51 +01:00
defaultValue : "0" ,
2022-09-08 21:40:48 +02:00
} ,
] ,
2022-01-08 14:08:04 +01:00
constr : ( state , tags , args ) = >
new VariableUiElement (
2022-09-08 21:40:48 +02:00
tags
2022-07-27 23:59:04 +02:00
. map ( ( tags ) = > tags [ args [ 0 ] ] )
2022-01-08 04:22:50 +01:00
. map ( ( commentsStr ) = > {
2022-01-08 14:08:04 +01:00
const comments : any [ ] = JSON . parse ( commentsStr )
2022-01-12 02:31:51 +01:00
const startLoc = Number ( args [ 1 ] ? ? 0 )
2022-01-26 21:40:38 +01:00
if ( ! isNaN ( startLoc ) && startLoc > 0 ) {
2022-01-12 02:31:51 +01:00
comments . splice ( 0 , startLoc )
2022-09-08 21:40:48 +02:00
}
2022-07-29 20:04:36 +02:00
return new Combine (
2022-09-08 21:40:48 +02:00
comments
2022-01-08 04:22:50 +01:00
. filter ( ( c ) = > c . text !== "" )
2022-01-08 14:08:04 +01:00
. map ( ( c ) = > new NoteCommentElement ( c ) )
2022-01-08 17:44:23 +01:00
) . SetClass ( "flex flex-col" )
2022-09-08 21:40:48 +02:00
} )
) ,
} ,
{
2022-01-08 14:08:04 +01:00
funcName : "add_image_to_note" ,
docs : "Adds an image to a node" ,
args : [
{
name : "Id-key" ,
doc : "The property name where the ID of the note to close can be found" ,
defaultValue : "id" ,
} ,
] ,
2023-02-14 00:09:04 +01:00
example :
" The following example sets the status to '2' (false positive)\n" +
"\n" +
"```json\n" +
"{\n" +
' "id": "mark_duplicate",\n' +
' "render": {\n' +
' "special": {\n' +
' "type": "maproulette_set_status",\n' +
' "message": {\n' +
' "en": "Mark as not found or false positive"\n' +
" },\n" +
' "status": "2",\n' +
' "image": "close"\n' +
" }\n" +
" }\n" +
"}\n" +
"```" ,
2022-01-08 14:08:04 +01:00
constr : ( state , tags , args ) = > {
2022-01-26 21:40:38 +01:00
const isUploading = new UIEventSource ( false )
const t = Translations . t . notes
const id = tags . data [ args [ 0 ] ? ? "id" ]
2022-09-08 21:40:48 +02:00
2022-09-02 12:39:40 +02:00
const uploader = new ImgurUploader ( async ( url ) = > {
2022-01-08 14:08:04 +01:00
isUploading . setData ( false )
2022-09-02 12:39:40 +02:00
await state . osmConnection . addCommentToNote ( id , url )
2022-01-08 14:08:04 +01:00
NoteCommentElement . addCommentTo ( url , tags , state )
} )
const label = new Combine ( [
2023-05-08 01:55:21 +02:00
Svg . camera_plus_svg ( ) . SetClass ( "block w-12 h-12 p-1 text-4xl " ) ,
2022-01-08 14:08:04 +01:00
Translations . t . image . addPicture ,
2022-01-08 17:44:23 +01:00
] ) . SetClass (
2022-01-08 14:08:04 +01:00
"p-2 border-4 border-black rounded-full font-bold h-full align-center w-full flex justify-center"
)
2022-01-26 21:40:38 +01:00
2022-01-08 14:08:04 +01:00
const fileSelector = new FileSelectorButton ( label )
fileSelector . GetValue ( ) . addCallback ( ( filelist ) = > {
isUploading . setData ( true )
uploader . uploadMany ( "Image for osm.org/note/" + id , "CC0" , filelist )
} )
2022-01-08 17:44:23 +01:00
const ti = Translations . t . image
const uploadPanel = new Combine ( [
fileSelector ,
ti . respectPrivacy . SetClass ( "text-sm" ) ,
] ) . SetClass ( "flex flex-col" )
2022-01-26 21:40:38 +01:00
return new LoginToggle (
new Toggle (
2022-01-08 14:08:04 +01:00
Translations . t . image . uploadingPicture . SetClass ( "alert" ) ,
2022-01-08 17:44:23 +01:00
uploadPanel ,
isUploading
) ,
t . loginToAddPicture ,
state
)
2022-02-16 02:24:15 +01:00
} ,
2022-09-08 21:40:48 +02:00
} ,
2022-02-16 02:24:15 +01:00
{
2022-03-10 23:20:50 +01:00
funcName : "title" ,
2022-02-16 02:24:15 +01:00
args : [ ] ,
2022-03-10 23:20:50 +01:00
docs : "Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?'" ,
example :
"`What is the phone number of {title()}`, which might automatically become `What is the phone number of XYZ`." ,
2023-04-07 02:13:57 +02:00
constr : ( state , tagsSource ) = >
2022-02-20 00:30:28 +01:00
new VariableUiElement (
tagsSource . map ( ( tags ) = > {
2023-03-28 05:13:48 +02:00
const layer = state . layout . getMatchingLayer ( tags )
2022-02-16 02:24:15 +01:00
const title = layer ? . title ? . GetRenderValue ( tags )
2022-03-10 23:20:50 +01:00
if ( title === undefined ) {
2022-02-20 00:30:28 +01:00
return undefined
}
2023-05-29 02:18:24 +02:00
return new SubstitutedTranslation ( title , tagsSource , state ) . SetClass ( "px-1" )
2022-02-16 02:24:15 +01:00
} )
2022-09-08 21:40:48 +02:00
) ,
2022-05-06 12:41:24 +02:00
} ,
2022-07-13 16:12:25 +02:00
{
funcName : "maproulette_task" ,
args : [ ] ,
2023-03-28 05:13:48 +02:00
constr ( state , tagSource ) {
2022-07-13 16:12:25 +02:00
let parentId = tagSource . data . mr_challengeId
2023-02-12 22:58:21 +01:00
if ( parentId === undefined ) {
console . warn ( "Element " , tagSource . data . id , " has no mr_challengeId" )
return undefined
}
2022-07-27 23:59:04 +02:00
let challenge = Stores . FromPromise (
Utils . downloadJsonCached (
` https://maproulette.org/api/v2/challenge/ ${ parentId } ` ,
24 * 60 * 60 * 1000
2022-09-08 21:40:48 +02:00
)
)
2022-07-13 16:12:25 +02:00
2022-10-28 04:33:05 +02:00
return new VariableUiElement (
2022-07-27 23:59:04 +02:00
challenge . map ( ( challenge ) = > {
2022-07-13 16:12:25 +02:00
let listItems : BaseUIElement [ ] = [ ]
let title : BaseUIElement
2022-07-27 23:59:04 +02:00
2022-07-13 16:12:25 +02:00
if ( challenge ? . name ) {
title = new Title ( challenge . name )
}
if ( challenge ? . description ) {
listItems . push ( new FixedUiElement ( challenge . description ) )
}
if ( challenge ? . instruction ) {
listItems . push ( new FixedUiElement ( challenge . instruction ) )
}
2022-07-27 23:59:04 +02:00
if ( listItems . length === 0 ) {
2022-07-13 16:12:25 +02:00
return undefined
} else {
return [ title , new List ( listItems ) ]
}
} )
2022-09-08 21:40:48 +02:00
)
2022-07-27 09:28:42 +02:00
} ,
2023-02-12 22:58:21 +01:00
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." ,
2022-09-08 21:40:48 +02:00
} ,
2023-02-14 00:09:04 +01:00
{
funcName : "maproulette_set_status" ,
docs : "Change the status of the given MapRoulette task" ,
args : [
{
name : "message" ,
doc : "A message to show to the user" ,
} ,
{
name : "image" ,
doc : "Image to show" ,
defaultValue : "confirm" ,
} ,
{
name : "message_confirm" ,
doc : "What to show when the task is closed, either by the user or was already closed." ,
} ,
{
name : "status" ,
doc : "A statuscode to apply when the button is clicked. 1 = `close`, 2 = `false_positive`, 3 = `skip`, 4 = `deleted`, 5 = `already fixed` (on the map, e.g. for duplicates), 6 = `too hard`" ,
defaultValue : "1" ,
} ,
{
name : "maproulette_id" ,
doc : "The property name containing the maproulette id" ,
defaultValue : "mr_taskId" ,
} ,
] ,
2023-03-29 17:21:20 +02:00
constr : ( state , tagsSource , args ) = > {
2023-02-14 00:09:04 +01:00
let [ message , image , message_closed , status , maproulette_id_key ] = args
if ( image === "" ) {
image = "confirm"
}
if ( Svg . All [ image ] !== undefined || Svg . All [ image + ".svg" ] !== undefined ) {
if ( image . endsWith ( ".svg" ) ) {
image = image . substring ( 0 , image . length - 4 )
}
image = Svg [ image + "_ui" ] ( )
}
const failed = new UIEventSource ( false )
const closeButton = new SubtleButton ( image , message ) . OnClickWithLoading (
Translations . t . general . loading ,
async ( ) = > {
const maproulette_id =
tagsSource . data [ maproulette_id_key ] ? ? tagsSource . data . id
try {
2023-03-24 19:21:15 +01:00
await Maproulette . singleton . closeTask (
2023-02-14 00:09:04 +01:00
Number ( maproulette_id ) ,
Number ( status ) ,
{
2023-03-28 05:13:48 +02:00
tags : ` MapComplete MapComplete: ${ state . layout . id } ` ,
2023-02-14 00:09:04 +01:00
}
)
tagsSource . data [ "mr_taskStatus" ] =
Maproulette . STATUS_MEANING [ Number ( status ) ]
tagsSource . data . status = status
tagsSource . ping ( )
} catch ( e ) {
console . error ( e )
failed . setData ( true )
}
}
)
let message_closed_element = undefined
if ( message_closed !== undefined && message_closed !== "" ) {
message_closed_element = new FixedUiElement ( message_closed )
}
return new VariableUiElement (
tagsSource
. map (
( tgs ) = >
tgs [ "status" ] ? ?
Maproulette . STATUS_MEANING [ tgs [ "mr_taskStatus" ] ]
)
. map ( Number )
. map (
( status ) = > {
if ( failed . data ) {
return new FixedUiElement (
"ERROR - could not close the MapRoulette task"
) . SetClass ( "block alert" )
}
if ( status === Maproulette . STATUS_OPEN ) {
return closeButton
}
return message_closed_element ? ? "Closed!"
} ,
[ failed ]
)
)
} ,
} ,
2022-07-25 18:55:15 +02:00
{
funcName : "statistics" ,
docs : "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer" ,
args : [ ] ,
2023-03-29 17:21:20 +02:00
constr : ( state ) = > {
2023-03-28 05:13:48 +02:00
return new Combine (
state . layout . layers
. filter ( ( l ) = > l . name !== null )
. map (
( l ) = > {
const fs = state . perLayer . get ( l . id )
2023-04-27 00:58:21 +02:00
const bbox = state . mapProperties . bounds
2023-03-28 05:13:48 +02:00
const fsBboxed = new BBoxFeatureSourceForLayer ( fs , bbox )
return new StatisticsPanel ( fsBboxed )
} ,
[ state . mapProperties . bounds ]
)
)
2022-09-08 21:40:48 +02:00
} ,
} ,
2022-07-27 23:59:04 +02:00
{
funcName : "send_email" ,
docs : "Creates a `mailto`-link where some fields are already set and correctly escaped. The user will be promted to send the email" ,
args : [
2022-09-08 21:40:48 +02:00
{
2022-07-27 23:59:04 +02:00
name : "to" ,
doc : "Who to send the email to?" ,
2022-07-29 20:04:36 +02:00
required : true ,
2022-09-08 21:40:48 +02:00
} ,
{
2022-07-27 23:59:04 +02:00
name : "subject" ,
doc : "The subject of the email" ,
required : true ,
2022-09-08 21:40:48 +02:00
} ,
{
2022-07-27 23:59:04 +02:00
name : "body" ,
doc : "The text in the email" ,
2022-07-29 20:04:36 +02:00
required : true ,
2022-09-08 21:40:48 +02:00
} ,
2022-07-27 23:59:04 +02:00
2022-09-08 21:40:48 +02:00
{
2022-07-27 23:59:04 +02:00
name : "button_text" ,
doc : "The text shown on the button in the UI" ,
2022-07-29 20:04:36 +02:00
required : true ,
2022-09-08 21:40:48 +02:00
} ,
] ,
2023-03-28 05:13:48 +02:00
constr ( __ , tags , args ) {
2022-07-27 23:59:04 +02:00
return new VariableUiElement (
tags . map ( ( tags ) = > {
const [ to , subject , body , button_text ] = args . map ( ( str ) = >
Utils . SubstituteKeys ( str , tags )
)
const url =
"mailto:" +
to +
"?subject=" +
encodeURIComponent ( subject ) +
"&body=" +
encodeURIComponent ( body )
return new SubtleButton ( Svg . envelope_svg ( ) , button_text , {
url ,
} )
} )
2022-09-08 21:40:48 +02:00
)
2022-07-28 09:16:19 +02:00
} ,
2022-09-08 21:40:48 +02:00
} ,
2022-07-28 09:16:19 +02:00
{
2023-04-07 02:13:57 +02:00
funcName : "link" ,
docs : "Construct a link. By using the 'special' visualisation notation, translation should be easier" ,
args : [
{
name : "text" ,
doc : "Text to be shown" ,
required : true ,
} ,
{
name : "href" ,
doc : "The URL to link to" ,
required : true ,
} ,
2023-04-15 02:28:24 +02:00
{
name : "class" ,
doc : "CSS-classes to add to the element" ,
} ,
2023-04-07 02:13:57 +02:00
] ,
constr (
state : SpecialVisualizationState ,
tagSource : UIEventSource < Record < string , string > > ,
2023-04-13 22:44:35 +02:00
args : string [ ]
2023-04-07 02:13:57 +02:00
) : BaseUIElement {
2023-04-15 02:28:24 +02:00
const [ text , href , classnames ] = args
2023-04-07 02:13:57 +02:00
return new VariableUiElement (
2023-04-15 02:28:24 +02:00
tagSource . map ( ( tags ) = >
new Link (
Utils . SubstituteKeys ( text , tags ) ,
Utils . SubstituteKeys ( href , tags ) ,
true
) . SetClass ( classnames )
2023-04-07 02:13:57 +02:00
)
)
} ,
} ,
{
2022-07-28 09:16:19 +02:00
funcName : "multi" ,
docs : "Given an embedded tagRendering (read only) and a key, will read the keyname as a JSON-list. Every element of this list will be considered as tags and rendered with the tagRendering" ,
2022-07-29 20:04:36 +02:00
example :
"```json\n" +
JSON . stringify (
2022-07-28 09:16:19 +02:00
{
render : {
special : {
2022-07-29 20:04:36 +02:00
type : "multi" ,
key : "_doors_from_building_properties" ,
2023-02-09 02:45:19 +01:00
tagrendering : {
en : "The building containing this feature has a <a href='#{id}'>door</a> of width {entrance:width}" ,
2022-09-08 21:40:48 +02:00
} ,
} ,
} ,
2022-07-29 20:04:36 +02:00
} ,
2022-09-08 21:40:48 +02:00
null ,
" "
) +
2022-10-11 01:39:09 +02:00
"\n```" ,
2022-09-08 21:40:48 +02:00
args : [
2022-07-29 20:04:36 +02:00
{
2021-12-12 02:59:24 +01:00
name : "key" ,
2022-08-22 19:16:37 +02:00
doc : "The property to read and to interpret as a list of properties" ,
2022-07-29 20:04:36 +02:00
required : true ,
2022-09-08 21:40:48 +02:00
} ,
{
2022-07-29 20:04:36 +02:00
name : "tagrendering" ,
doc : "An entire tagRenderingConfig" ,
required : true ,
} ,
2022-09-08 21:40:48 +02:00
] ,
2022-07-28 09:16:19 +02:00
constr ( state , featureTags , args ) {
const [ key , tr ] = args
2023-05-29 02:18:24 +02:00
const translation = new Translation ( { "*" : tr } )
2022-07-29 20:04:36 +02:00
return new VariableUiElement (
featureTags . map ( ( tags ) = > {
const properties : object [ ] = JSON . parse ( tags [ key ] )
const elements = [ ]
for ( const property of properties ) {
const subsTr = new SubstitutedTranslation (
translation ,
new UIEventSource < any > ( property ) ,
state
)
elements . push ( subsTr )
}
return new List ( elements )
} )
2022-09-08 21:40:48 +02:00
)
2022-07-29 20:04:36 +02:00
} ,
2022-09-08 21:40:48 +02:00
} ,
2020-10-17 02:37:53 +02:00
]
2022-01-08 04:22:50 +01:00
specialVisualizations . push ( new AutoApplyButton ( specialVisualizations ) )
2022-04-22 01:45:54 +02:00
2022-11-02 13:47:34 +01:00
const invalid = specialVisualizations
2023-05-29 02:18:24 +02:00
. map ( ( sp , i ) = > ( { sp , i } ) )
2022-11-02 13:47:34 +01:00
. filter ( ( sp ) = > sp . sp . funcName === undefined )
if ( invalid . length > 0 ) {
throw (
"Invalid special visualisation found: funcName is undefined for " +
invalid . map ( ( sp ) = > sp . i ) . join ( ", " ) +
'. Did you perhaps type \n funcName: "funcname" // type declaration uses COLON\ninstead of:\n funcName = "funcName" // value definition uses EQUAL'
)
2022-10-28 04:33:05 +02:00
}
2022-01-08 04:22:50 +01:00
return specialVisualizations
2020-10-17 02:37:53 +02:00
}
2020-10-09 20:10:21 +02:00
}