2023-06-09 16:13:35 +02:00
import { AutoAction } from "./AutoApplyButton"
2021-12-12 02:59:24 +01:00
import Translations from "../i18n/Translations"
2023-06-09 16:13:35 +02:00
import { VariableUiElement } from "../Base/VariableUIElement"
2021-12-12 02:59:24 +01:00
import BaseUIElement from "../BaseUIElement"
2023-06-09 16:13:35 +02:00
import { FixedUiElement } from "../Base/FixedUiElement"
import { Store , UIEventSource } from "../../Logic/UIEventSource"
import { SubtleButton } from "../Base/SubtleButton"
2021-12-12 02:59:24 +01:00
import Combine from "../Base/Combine"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
2023-06-09 16:13:35 +02:00
import { And } from "../../Logic/Tags/And"
2021-12-12 02:59:24 +01:00
import Toggle from "../Input/Toggle"
2023-06-09 16:13:35 +02:00
import { Utils } from "../../Utils"
import { Tag } from "../../Logic/Tags/Tag"
2021-12-13 02:05:34 +01:00
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
2023-06-09 16:13:35 +02:00
import { Changes } from "../../Logic/Osm/Changes"
import { SpecialVisualization , SpecialVisualizationState } from "../SpecialVisualization"
2023-06-01 02:52:21 +02:00
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource" ;
import { Feature } from "geojson" ;
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" ;
2023-06-09 16:13:35 +02:00
import Maproulette from "../../Logic/Maproulette" ;
2021-12-12 02:59:24 +01:00
2022-11-02 14:44:06 +01:00
export default class TagApplyButton implements AutoAction , SpecialVisualization {
2021-12-12 02:59:24 +01:00
public readonly funcName = "tag_apply"
public readonly docs =
"Shows a big button; clicking this button will apply certain tags onto the feature.\n\nThe first argument takes a specification of which tags to add.\n" +
Utils . Special_visualizations_tagsToApplyHelpText
public readonly supportsAutoAction = true
public readonly args = [
{
name : "tags_to_apply" ,
2023-06-09 16:13:35 +02:00
doc : "A specification of the tags to apply. This is either hardcoded in the layer or the `$name` of a property containing the tags to apply. If redirected and the value of the linked property starts with `{`, the other property will be interpreted as a json object" ,
2021-12-12 02:59:24 +01:00
} ,
{
name : "message" ,
doc : "The text to show to the contributor" ,
} ,
{
name : "image" ,
doc : "An image to show to the contributor on the button" ,
} ,
{
name : "id_of_object_to_apply_this_one" ,
defaultValue : undefined ,
doc : "If specified, applies the the tags onto _another_ object. The id will be read from properties[id_of_object_to_apply_this_one] of the selected object. The tags are still calculated based on the tags of the _selected_ element" ,
} ,
2023-06-09 16:13:35 +02:00
{
name : "maproulette_task_id" ,
defaultValue : undefined ,
doc : "If specified, this maproulette-challenge will be closed when the tags are applied"
}
2021-12-12 02:59:24 +01:00
]
2022-01-26 21:40:38 +01:00
public readonly example =
"`{tag_apply(survey_date=$_now:date, Surveyed today!)}`, `{tag_apply(addr:street=$addr:street, Apply the address, apply_icon.svg, _closest_osm_id)"
2021-12-12 02:59:24 +01:00
2023-03-28 05:13:48 +02:00
public static generateTagsToApply (
spec : string ,
tagSource : Store < Record < string , string > >
) : Store < Tag [ ] > {
2022-09-28 22:47:12 +02:00
// Check whether we need to look up a single value
2023-06-09 16:13:35 +02:00
if ( ! spec . includes ( ";" ) && ! spec . includes ( "=" ) && spec . startsWith ( "$" ) ) {
2022-09-28 22:47:12 +02:00
// We seem to be dealing with a single value, fetch it
spec = tagSource . data [ spec . replace ( "$" , "" ) ]
2023-06-09 16:13:35 +02:00
2022-09-28 22:47:12 +02:00
}
2023-06-09 16:13:35 +02:00
let tgsSpec : [ string , string ] [ ]
if ( spec . startsWith ( "{" ) ) {
const properties = JSON . parse ( spec )
tgsSpec = [ ]
for ( const key of Object . keys ( properties ) ) {
tgsSpec . push ( [ key , properties [ key ] ] )
}
} else {
tgsSpec = TagApplyButton . parseTagSpec ( spec )
}
2021-12-12 02:59:24 +01:00
return tagSource . map ( ( tags ) = > {
const newTags : Tag [ ] = [ ]
for ( const [ key , value ] of tgsSpec ) {
if ( value . indexOf ( "$" ) >= 0 ) {
let parts = value . split ( "$" )
// THe first of the split won't start with a '$', so no substitution needed
let actualValue = parts [ 0 ]
parts . shift ( )
for ( const part of parts ) {
const [ _ , varName , leftOver ] = part . match ( /([a-zA-Z0-9_:]*)(.*)/ )
actualValue += ( tags [ varName ] ? ? "" ) + leftOver
}
newTags . push ( new Tag ( key , actualValue ) )
} else {
newTags . push ( new Tag ( key , value ) )
}
}
return newTags
} )
}
2023-06-09 16:13:35 +02:00
/ * *
* Parses a tag specification
*
* TagApplyButton . parseTagSpec ( "key=value;key0=value0" ) // => [["key","value"],["key0","value0"]]
*
* // Should handle escaped ";"
* TagApplyButton . parseTagSpec ( "key=value;key0=value0\\;value1" ) // => [["key","value"],["key0","value0;value1"]]
* /
private static parseTagSpec ( spec : string ) : [ string , string ] [ ] {
const tgsSpec : [ string , string ] [ ] = [ ]
while ( spec . length > 0 ) {
const [ part ] = spec . match ( /((\\;)|[^;])*/ )
spec = spec . substring ( part . length + 1 ) // +1 to remove the pending ';' as well
const kv = part . split ( "=" ) . map ( ( s ) = > s . trim ( ) . replace ( "\\;" , ";" ) )
if ( kv . length == 2 ) {
tgsSpec . push ( < [ string , string ] > kv )
} else if ( kv . length < 2 ) {
console . error ( "Invalid key spec: no '=' found in " + spec )
throw "Invalid key spec: no '=' found in " + spec
} else {
throw "Invalid key spec: multiple '=' found in " + spec
}
}
for ( const spec of tgsSpec ) {
if ( spec [ 0 ] . endsWith ( ":" ) ) {
throw "The key for a tag specification for import or apply ends with ':'. The theme author probably wrote key:=otherkey instead of key=$otherkey"
}
}
return tgsSpec
}
2023-06-01 02:52:21 +02:00
public async applyActionOn (
feature : Feature ,
2021-12-13 02:05:34 +01:00
state : {
2023-03-28 05:13:48 +02:00
layout : LayoutConfig
2021-12-13 02:05:34 +01:00
changes : Changes
2023-06-01 02:52:21 +02:00
indexedFeatures : IndexedFeatureSource
2022-01-26 21:40:38 +01:00
} ,
tags : UIEventSource < any > ,
2023-06-01 02:52:21 +02:00
args : string [ ] ,
2022-01-26 21:40:38 +01:00
) : Promise < void > {
2021-12-12 02:59:24 +01:00
const tagsToApply = TagApplyButton . generateTagsToApply ( args [ 0 ] , tags )
const targetIdKey = args [ 3 ]
const targetId = tags . data [ targetIdKey ] ? ? tags . data . id
const changeAction = new ChangeTagAction (
targetId ,
new And ( tagsToApply . data ) ,
tags . data , // We pass in the tags of the selected element, not the tags of the target element!
{
2023-03-28 05:13:48 +02:00
theme : state.layout.id ,
2021-12-12 02:59:24 +01:00
changeType : "answer" ,
}
)
await state . changes . applyAction ( changeAction )
2023-06-09 16:13:35 +02:00
const maproulette_id_key = args [ 4 ]
if ( maproulette_id_key ) {
const maproulette_id = Number ( tags . data [ maproulette_id_key ] )
await Maproulette . singleton . closeTask ( maproulette_id , Maproulette . STATUS_FIXED , {
comment : "Tags are copied onto " + targetId + " with MapComplete"
} )
2023-06-09 18:07:43 +02:00
tags . data [ "mr_taskStatus" ] = "Fixed"
tags . ping ( )
2023-06-09 16:13:35 +02:00
}
2021-12-12 02:59:24 +01:00
}
public constr (
2023-03-28 05:13:48 +02:00
state : SpecialVisualizationState ,
tags : UIEventSource < Record < string , string > > ,
2023-06-01 02:52:21 +02:00
args : string [ ] ,
feature : Feature ,
2023-06-02 08:42:08 +02:00
_ : LayerConfig
2021-12-12 02:59:24 +01:00
) : BaseUIElement {
const tagsToApply = TagApplyButton . generateTagsToApply ( args [ 0 ] , tags )
const msg = args [ 1 ]
let image = args [ 2 ] ? . trim ( )
if ( image === "" || image === "undefined" ) {
image = undefined
}
const targetIdKey = args [ 3 ]
const t = Translations . t . general . apply_button
const tagsExplanation = new VariableUiElement (
tagsToApply . map ( ( tagsToApply ) = > {
const tagsStr = tagsToApply . map ( ( t ) = > t . asHumanString ( false , true ) ) . join ( "&" )
let el : BaseUIElement = new FixedUiElement ( tagsStr )
if ( targetIdKey !== undefined ) {
const targetId = tags . data [ targetIdKey ] ? ? tags . data . id
2023-06-09 16:13:35 +02:00
el = t . appliedOnAnotherObject . Subs ( { tags : tagsStr , id : targetId } )
2021-12-12 02:59:24 +01:00
}
return el
} )
) . SetClass ( "subtle" )
const self = this
2023-06-09 18:07:43 +02:00
const applied = new UIEventSource ( tags ? . data ? . [ "mr_taskStatus" ] !== "Created" ) // This will default to 'false' for non-maproulette challenges
2021-12-12 02:59:24 +01:00
const applyButton = new SubtleButton (
image ,
new Combine ( [ msg , tagsExplanation ] ) . SetClass ( "flex flex-col" )
2023-03-28 05:13:48 +02:00
) . onClick ( async ( ) = > {
2021-12-12 02:59:24 +01:00
applied . setData ( true )
2023-06-01 02:52:21 +02:00
await self . applyActionOn ( feature , state , tags , args )
2021-12-12 02:59:24 +01:00
} )
return new Toggle (
new Toggle ( t . isApplied . SetClass ( "thanks" ) , applyButton , applied ) ,
undefined ,
state . osmConnection . isLoggedIn
)
}
}