2021-12-12 02:59:24 +01:00
import { AutoAction } from "./AutoApplyButton"
import Translations from "../i18n/Translations"
import { VariableUiElement } from "../Base/VariableUIElement"
import BaseUIElement from "../BaseUIElement"
import { FixedUiElement } from "../Base/FixedUiElement"
2022-06-05 02:24:14 +02:00
import { Store , UIEventSource } from "../../Logic/UIEventSource"
2021-12-12 02:59:24 +01:00
import { SubtleButton } from "../Base/SubtleButton"
import Combine from "../Base/Combine"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { And } from "../../Logic/Tags/And"
import Toggle from "../Input/Toggle"
import { Utils } from "../../Utils"
import { Tag } from "../../Logic/Tags/Tag"
2021-12-13 02:05:34 +01:00
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Changes } from "../../Logic/Osm/Changes"
2023-03-28 05:13:48 +02:00
import { SpecialVisualization , SpecialVisualizationState } from "../SpecialVisualization"
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" ,
doc : "A specification of the tags to apply" ,
} ,
{
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" ,
} ,
]
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
2022-09-28 22:47:12 +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"]]
* /
2022-10-27 01:50:41 +02:00
private static parseTagSpec ( spec : string ) : [ string , string ] [ ] {
const tgsSpec : [ string , string ] [ ] = [ ]
2022-07-18 16:08:41 +02:00
2022-10-27 01:50:41 +02:00
while ( spec . length > 0 ) {
2022-09-28 22:47:12 +02:00
const [ part ] = spec . match ( /((\\;)|[^;])*/ )
spec = spec . substring ( part . length + 1 ) // +1 to remove the pending ';' as well
2022-10-27 01:50:41 +02:00
const kv = part . split ( "=" ) . map ( ( s ) = > s . trim ( ) . replace ( "\\;" , ";" ) )
2022-09-28 22:47:12 +02:00
if ( kv . length == 2 ) {
2022-10-27 01:50:41 +02:00
tgsSpec . push ( < [ string , string ] > kv )
} else if ( kv . length < 2 ) {
2023-05-01 01:14:48 +02:00
console . error ( "Invalid key spec: no '=' found in " + spec )
2022-09-28 22:47:12 +02:00
throw "Invalid key spec: no '=' found in " + spec
2022-10-27 01:50:41 +02:00
} else {
2021-12-12 02:59:24 +01:00
throw "Invalid key spec: multiple '=' found in " + spec
}
2022-09-28 22:47:12 +02:00
}
2021-12-12 02:59:24 +01:00
for ( const spec of tgsSpec ) {
if ( spec [ 0 ] . endsWith ( ":" ) ) {
2022-09-28 22:47:12 +02:00
throw "The key for a tag specification for import or apply ends with ':'. The theme author probably wrote key:=otherkey instead of key=$otherkey"
2021-12-12 02:59:24 +01:00
}
}
2022-09-28 22:47:12 +02:00
return tgsSpec
}
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
if ( ! spec . includes ( ";" ) && ! spec . includes ( "=" ) && spec . includes ( "$" ) ) {
// We seem to be dealing with a single value, fetch it
spec = tagSource . data [ spec . replace ( "$" , "" ) ]
}
2022-10-27 01:50:41 +02:00
const 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
} )
}
2021-12-13 02:05:34 +01:00
async applyActionOn (
state : {
2023-03-28 05:13:48 +02:00
layout : LayoutConfig
2021-12-13 02:05:34 +01:00
changes : Changes
2022-01-26 21:40:38 +01:00
} ,
tags : UIEventSource < any > ,
args : string [ ]
) : 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 )
}
public constr (
2023-03-28 05:13:48 +02:00
state : SpecialVisualizationState ,
tags : UIEventSource < Record < string , string > > ,
2021-12-12 02:59:24 +01:00
args : string [ ]
) : 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
el = t . appliedOnAnotherObject . Subs ( { tags : tagsStr , id : targetId } )
}
return el
} )
) . SetClass ( "subtle" )
const self = this
const applied = new UIEventSource ( false )
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-03-28 05:13:48 +02:00
await self . applyActionOn ( 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
)
}
}