2021-12-12 17:35:08 +01:00
import BaseUIElement from "./BaseUIElement" ;
import Combine from "./Base/Combine" ;
import Svg from "../Svg" ;
import Title from "./Base/Title" ;
import Toggle from "./Input/Toggle" ;
import { SubtleButton } from "./Base/SubtleButton" ;
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" ;
import ValidatedTextField from "./Input/ValidatedTextField" ;
import { Utils } from "../Utils" ;
import { UIEventSource } from "../Logic/UIEventSource" ;
import { VariableUiElement } from "./Base/VariableUIElement" ;
import { FixedUiElement } from "./Base/FixedUiElement" ;
import { Tiles } from "../Models/TileRange" ;
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource" ;
import { DropDown } from "./Input/DropDown" ;
import { AllKnownLayouts } from "../Customizations/AllKnownLayouts" ;
import MinimapImplementation from "./Base/MinimapImplementation" ;
import { OsmConnection } from "../Logic/Osm/OsmConnection" ;
2021-12-13 02:05:34 +01:00
import { BBox } from "../Logic/BBox" ;
import MapState from "../Logic/State/MapState" ;
import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline" ;
import LayerConfig from "../Models/ThemeConfig/LayerConfig" ;
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" ;
import FeatureSource from "../Logic/FeatureSource/FeatureSource" ;
import List from "./Base/List" ;
import { QueryParameters } from "../Logic/Web/QueryParameters" ;
import { SubstitutedTranslation } from "./SubstitutedTranslation" ;
import { AutoAction } from "./Popup/AutoApplyButton" ;
2021-12-13 13:22:23 +01:00
import DynamicGeoJsonTileSource from "../Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource" ;
2021-12-21 18:35:31 +01:00
import * as themeOverview from "../assets/generated/theme_overview.json"
2021-12-12 17:35:08 +01:00
2022-01-26 21:40:38 +01:00
class AutomationPanel extends Combine {
2021-12-17 19:28:05 +01:00
private static readonly openChangeset = new UIEventSource < number > ( undefined ) ;
2021-12-13 13:22:23 +01:00
constructor ( layoutToUse : LayoutConfig , indices : number [ ] , extraCommentText : UIEventSource < string > , tagRenderingToAutomate : { layer : LayerConfig , tagRendering : TagRenderingConfig } ) {
const layerId = tagRenderingToAutomate . layer . id
const trId = tagRenderingToAutomate . tagRendering . id
const tileState = LocalStorageSource . GetParsed ( "automation-tile_state-" + layerId + "-" + trId , { } )
const logMessages = new UIEventSource < string [ ] > ( [ ] )
if ( indices === undefined ) {
2022-01-26 21:40:38 +01:00
throw ( "No tiles loaded - can not automate" )
2021-12-13 13:22:23 +01:00
}
2021-12-17 19:28:05 +01:00
const openChangeset = AutomationPanel . openChangeset ;
2022-01-26 21:40:38 +01:00
2021-12-17 19:28:05 +01:00
openChangeset . addCallbackAndRun ( cs = > console . trace ( "Sync current open changeset to:" , cs ) )
2021-12-12 17:35:08 +01:00
2021-12-13 13:22:23 +01:00
const nextTileToHandle = tileState . map ( handledTiles = > {
for ( const index of indices ) {
if ( handledTiles [ index ] !== undefined ) {
// Already handled
continue
}
return index
}
return undefined
} )
nextTileToHandle . addCallback ( t = > console . warn ( "Next tile to handle is" , t ) )
2021-12-12 17:35:08 +01:00
2021-12-13 13:22:23 +01:00
const neededTimes = new UIEventSource < number [ ] > ( [ ] )
const automaton = new VariableUiElement ( nextTileToHandle . map ( tileIndex = > {
if ( tileIndex === undefined ) {
return new FixedUiElement ( "All done!" ) . SetClass ( "thanks" )
}
2022-01-26 21:40:38 +01:00
console . warn ( "Triggered map on nextTileToHandle" , tileIndex )
2021-12-13 13:22:23 +01:00
const start = new Date ( )
2021-12-13 20:51:44 +01:00
return AutomationPanel . TileHandler ( layoutToUse , tileIndex , layerId , tagRenderingToAutomate . tagRendering , extraCommentText ,
openChangeset ,
( result , logMessage ) = > {
2022-01-26 21:40:38 +01:00
const end = new Date ( )
const timeNeeded = ( end . getTime ( ) - start . getTime ( ) ) / 1000 ;
neededTimes . data . push ( timeNeeded )
neededTimes . ping ( )
tileState . data [ tileIndex ] = result
tileState . ping ( ) ;
if ( logMessage !== undefined ) {
logMessages . data . push ( logMessage )
logMessages . ping ( ) ;
}
} ) ;
2021-12-13 13:22:23 +01:00
} ) )
2021-12-12 17:35:08 +01:00
2021-12-13 13:22:23 +01:00
const statistics = new VariableUiElement ( tileState . map ( states = > {
let total = 0
const perResult = new Map < string , number > ( )
for ( const key in states ) {
total ++
const result = states [ key ]
perResult . set ( result , ( perResult . get ( result ) ? ? 0 ) + 1 )
}
let sum = 0
neededTimes . data . forEach ( v = > {
sum = sum + v
} )
let timePerTile = sum / neededTimes . data . length
return new Combine ( [ "Handled " + total + "/" + indices . length + " tiles: " ,
new List ( Array . from ( perResult . keys ( ) ) . map ( key = > key + ": " + perResult . get ( key ) ) ) ,
2021-12-13 20:51:44 +01:00
"Handling one tile needs " + ( Math . floor ( timePerTile * 100 ) / 100 ) + "s on average. Estimated time left: " + Utils . toHumanTime ( ( indices . length - total ) * timePerTile )
2021-12-13 13:22:23 +01:00
] ) . SetClass ( "flex flex-col" )
} ) )
2022-01-26 21:40:38 +01:00
super ( [ statistics , automaton ,
new SubtleButton ( undefined , "Clear fixed" ) . onClick ( ( ) = > {
2021-12-13 20:51:44 +01:00
const st = tileState . data
2022-01-26 21:40:38 +01:00
for ( const tileIndex in st ) {
if ( st [ tileIndex ] === "fixed" ) {
delete st [ tileIndex ]
}
}
tileState . ping ( ) ;
} ) ,
new VariableUiElement ( logMessages . map ( logMessages = > new List ( logMessages ) ) ) ] )
this . SetClass ( "flex flex-col" )
2021-12-13 13:22:23 +01:00
}
2022-01-26 21:40:38 +01:00
private static TileHandler ( layoutToUse : LayoutConfig , tileIndex : number , targetLayer : string , targetAction : TagRenderingConfig , extraCommentText : UIEventSource < string > ,
2021-12-17 19:28:05 +01:00
openChangeset : UIEventSource < number > ,
2021-12-13 20:51:44 +01:00
whenDone : ( ( result : string , logMessage? : string ) = > void ) ) : BaseUIElement {
2021-12-12 17:35:08 +01:00
2021-12-13 02:05:34 +01:00
const state = new MapState ( layoutToUse , { attemptLogin : false } )
2022-01-26 21:40:38 +01:00
extraCommentText . syncWith ( state . changes . extraComment )
2021-12-13 02:05:34 +01:00
const [ z , x , y ] = Tiles . tile_from_index ( tileIndex )
state . locationControl . setData ( {
zoom : z ,
lon : x ,
lat : y
} )
state . currentBounds . setData (
BBox . fromTileIndex ( tileIndex )
)
let targetTiles : UIEventSource < FeatureSource [ ] > = new UIEventSource < FeatureSource [ ] > ( [ ] )
const pipeline = new FeaturePipeline ( ( tile = > {
const layerId = tile . layer . layerDef . id
if ( layerId === targetLayer ) {
targetTiles . data . push ( tile )
targetTiles . ping ( )
}
} ) , state )
state . locationControl . ping ( ) ;
state . currentBounds . ping ( ) ;
const stateToShow = new UIEventSource ( "" )
pipeline . runningQuery . map (
async isRunning = > {
if ( targetTiles . data . length === 0 ) {
stateToShow . setData ( "No data loaded yet..." )
return ;
}
if ( isRunning ) {
stateToShow . setData ( "Waiting for all layers to be loaded... Has " + targetTiles . data . length + " tiles already" )
return ;
}
if ( targetTiles . data . length === 0 ) {
stateToShow . setData ( "No features found to apply the action" )
whenDone ( "empty" )
return true ;
}
stateToShow . setData ( "Gathering applicable elements" )
let handled = 0
let inspected = 0
2021-12-13 13:22:23 +01:00
let log = [ ]
2021-12-13 02:05:34 +01:00
for ( const targetTile of targetTiles . data ) {
for ( const ffs of targetTile . features . data ) {
inspected ++
if ( inspected % 10 === 0 ) {
stateToShow . setData ( "Inspected " + inspected + " features, updated " + handled + " features" )
}
const feature = ffs . feature
2021-12-13 13:22:23 +01:00
const renderingTr = targetAction . GetRenderValue ( feature . properties )
const rendering = renderingTr . txt
2022-01-26 21:40:38 +01:00
log . push ( "<a href='https://openstreetmap.org/" + feature . properties . id + "' target='_blank'>" + feature . properties . id + "</a>: " + new SubstitutedTranslation ( renderingTr , new UIEventSource < any > ( feature . properties ) , undefined ) . ConstructElement ( ) . innerText )
2021-12-13 02:05:34 +01:00
const actions = Utils . NoNull ( SubstitutedTranslation . ExtractSpecialComponents ( rendering )
. map ( obj = > obj . special ) )
for ( const action of actions ) {
const auto = < AutoAction > action . func
if ( auto . supportsAutoAction !== true ) {
continue
}
await auto . applyActionOn ( {
layoutToUse : state.layoutToUse ,
changes : state.changes
} , state . allElements . getEventSourceById ( feature . properties . id ) , action . args )
handled ++
}
}
}
stateToShow . setData ( "Done! Inspected " + inspected + " features, updated " + handled + " features" )
if ( inspected === 0 ) {
whenDone ( "empty" )
2021-12-13 13:22:23 +01:00
return true ;
2021-12-13 02:05:34 +01:00
}
if ( handled === 0 ) {
2022-01-26 21:40:38 +01:00
whenDone ( "no-action" , "Inspected " + inspected + " elements: " + log . join ( "; " ) )
} else {
2021-12-13 20:51:44 +01:00
state . osmConnection . AttemptLogin ( )
2021-12-17 19:28:05 +01:00
state . changes . flushChanges ( "handled tile automatically, time to flush!" , openChangeset )
2022-01-26 21:40:38 +01:00
whenDone ( "fixed" , "Updated " + handled + " elements, inspected " + inspected + ": " + log . join ( "; " ) )
2021-12-13 02:05:34 +01:00
}
2021-12-13 13:22:23 +01:00
return true ;
2021-12-13 02:05:34 +01:00
} , [ targetTiles ] )
return new Combine ( [
new Title ( "Performing action for tile " + tileIndex , 1 ) ,
new VariableUiElement ( stateToShow ) ] ) . SetClass ( "flex flex-col" )
}
2021-12-13 13:22:23 +01:00
}
2021-12-13 02:05:34 +01:00
2021-12-12 17:35:08 +01:00
2021-12-13 13:22:23 +01:00
class AutomatonGui {
2021-12-12 17:35:08 +01:00
2021-12-13 13:22:23 +01:00
constructor ( ) {
2021-12-13 02:05:34 +01:00
2021-12-13 13:22:23 +01:00
const osmConnection = new OsmConnection ( {
allElements : undefined ,
changes : undefined ,
layoutName : "automaton" ,
singlePage : false ,
oauth_token : QueryParameters.GetQueryParameter ( "oauth_token" , "OAuth token" )
} ) ;
2021-12-13 02:05:34 +01:00
2021-12-13 13:22:23 +01:00
new Combine ( [
new Combine ( [ Svg . robot_svg ( ) . SetClass ( "w-24 h-24 p-4 rounded-full subtle-background" ) ,
new Combine ( [ new Title ( "MapComplete Automaton" , 1 ) ,
"This page helps to automate certain tasks for a theme. Expert use only."
] ) . SetClass ( "flex flex-col m-4" )
] ) . SetClass ( "flex" ) ,
new Toggle (
AutomatonGui . GenerateMainPanel ( ) ,
new SubtleButton ( Svg . osm_logo_svg ( ) , "Login to get started" ) . onClick ( ( ) = > osmConnection . AttemptLogin ( ) ) ,
osmConnection . isLoggedIn
2022-01-26 21:40:38 +01:00
) ] ) . SetClass ( "block p-4" )
2021-12-13 13:22:23 +01:00
. AttachTo ( "main" )
2021-12-12 17:35:08 +01:00
}
2021-12-13 13:22:23 +01:00
2021-12-12 17:35:08 +01:00
private static GenerateMainPanel ( ) : BaseUIElement {
const themeSelect = new DropDown < string > ( "Select a theme" ,
2022-01-26 21:40:38 +01:00
Array . from ( themeOverview ) . map ( l = > ( { value : l.id , shown : l.id } ) )
2021-12-12 17:35:08 +01:00
)
2021-12-13 20:51:44 +01:00
LocalStorageSource . Get ( "automation-theme-id" , "missing_streets" ) . syncWith ( themeSelect . GetValue ( ) )
2021-12-12 17:35:08 +01:00
const tilepath = ValidatedTextField . InputForType ( "url" , {
placeholder : "Specifiy the path of the overview" ,
2022-02-11 01:20:01 +01:00
inputStyle : "width: 100%"
2021-12-12 17:35:08 +01:00
} )
tilepath . SetClass ( "w-full" )
LocalStorageSource . Get ( "automation-tile_path" ) . syncWith ( tilepath . GetValue ( ) , true )
2022-01-26 21:40:38 +01:00
2021-12-13 02:05:34 +01:00
let tilesToRunOver = tilepath . GetValue ( ) . bind ( path = > {
2021-12-12 17:35:08 +01:00
if ( path === undefined ) {
return undefined
}
2022-01-26 21:40:38 +01:00
return UIEventSource . FromPromiseWithErr ( Utils . downloadJsonCached ( path , 1000 * 60 * 60 ) )
2021-12-12 17:35:08 +01:00
} )
2022-01-26 21:40:38 +01:00
2021-12-13 02:05:34 +01:00
const targetZoom = 14
2021-12-12 17:35:08 +01:00
const tilesPerIndex = tilesToRunOver . map ( tiles = > {
2022-01-26 21:40:38 +01:00
2021-12-12 17:35:08 +01:00
if ( tiles === undefined || tiles [ "error" ] !== undefined ) {
return undefined
}
2022-01-26 21:40:38 +01:00
let indexes : number [ ] = [ ] ;
2021-12-12 17:35:08 +01:00
const tilesS = tiles [ "success" ]
2022-01-26 21:40:38 +01:00
DynamicGeoJsonTileSource . RegisterWhitelist ( tilepath . GetValue ( ) . data , tilesS )
2021-12-12 17:35:08 +01:00
const z = Number ( tilesS [ "zoom" ] )
for ( const key in tilesS ) {
if ( key === "zoom" ) {
continue
}
const x = Number ( key )
const ys = tilesS [ key ]
indexes . push ( . . . ys . map ( y = > Tiles . tile_index ( z , x , y ) ) )
}
2021-12-13 02:05:34 +01:00
console . log ( "Got " , indexes . length , "indexes" )
let rezoomed = new Set < number > ( )
for ( const index of indexes ) {
let [ z , x , y ] = Tiles . tile_from_index ( index )
while ( z > targetZoom ) {
z --
x = Math . floor ( x / 2 )
y = Math . floor ( y / 2 )
}
rezoomed . add ( Tiles . tile_index ( z , x , y ) )
}
return Array . from ( rezoomed )
2021-12-12 17:35:08 +01:00
} )
2021-12-13 02:05:34 +01:00
const extraComment = ValidatedTextField . InputForType ( "text" )
LocalStorageSource . Get ( "automaton-extra-comment" ) . syncWith ( extraComment . GetValue ( ) )
2021-12-12 17:35:08 +01:00
return new Combine ( [
themeSelect ,
"Specify the path to a tile overview. This is a hosted .json of the format {x : [y0, y1, y2], x1: [y0, ...]} where x is a string and y are numbers" ,
tilepath ,
2021-12-13 13:22:23 +01:00
"Add an extra comment:" ,
2021-12-13 02:05:34 +01:00
extraComment ,
2022-01-26 21:40:38 +01:00
new VariableUiElement ( extraComment . GetValue ( ) . map ( c = > "Your comment is " + ( c ? . length ? ? 0 ) + "/200 characters long" ) ) . SetClass ( "subtle" ) ,
2021-12-12 17:35:08 +01:00
new VariableUiElement ( tilesToRunOver . map ( t = > {
if ( t === undefined ) {
return "No path given or still loading..."
}
if ( t [ "error" ] !== undefined ) {
return new FixedUiElement ( "Invalid URL or data: " + t [ "error" ] ) . SetClass ( "alert" )
}
return new FixedUiElement ( "Loaded " + tilesPerIndex . data . length + " tiles to automated over" ) . SetClass ( "thanks" )
} ) ) ,
new VariableUiElement ( themeSelect . GetValue ( ) . map ( id = > AllKnownLayouts . allKnownLayouts . get ( id ) ) . map ( layoutToUse = > {
if ( layoutToUse === undefined ) {
return new FixedUiElement ( "Select a valid layout" )
}
2021-12-13 02:05:34 +01:00
if ( tilesPerIndex . data === undefined || tilesPerIndex . data . length === 0 ) {
return "No tiles given"
}
const automatableTagRenderings : { layer : LayerConfig , tagRendering : TagRenderingConfig } [ ] = [ ]
for ( const layer of layoutToUse . layers ) {
for ( const tagRendering of layer . tagRenderings ) {
if ( tagRendering . group === "auto" ) {
automatableTagRenderings . push ( { layer , tagRendering : tagRendering } )
}
}
}
console . log ( "Automatable tag renderings:" , automatableTagRenderings )
if ( automatableTagRenderings . length === 0 ) {
return new FixedUiElement ( 'This theme does not have any tagRendering with "group": "auto" set' ) . SetClass ( "alert" )
}
const pickAuto = new DropDown ( "Pick the action to automate" ,
[
{
value : undefined ,
shown : "Pick an option"
} ,
. . . automatableTagRenderings . map ( config = > (
{
shown : config.layer.id + " - " + config . tagRendering . id ,
value : config
}
) )
]
)
2021-12-12 17:35:08 +01:00
2021-12-13 02:05:34 +01:00
return new Combine ( [
pickAuto ,
2021-12-13 13:22:23 +01:00
new VariableUiElement ( pickAuto . GetValue ( ) . map ( auto = > auto === undefined ? undefined : new AutomationPanel ( layoutToUse , tilesPerIndex . data , extraComment . GetValue ( ) , auto ) ) ) ] )
2021-12-12 17:35:08 +01:00
2021-12-13 02:05:34 +01:00
} , [ tilesPerIndex ] ) ) . SetClass ( "flex flex-col" )
2021-12-12 17:35:08 +01:00
] ) . SetClass ( "flex flex-col" )
}
}
MinimapImplementation . initialize ( )
new AutomatonGui ( )
2021-12-13 13:22:23 +01:00