2020-10-09 20:10:21 +02:00
import { UIEventSource } from "../Logic/UIEventSource" ;
2020-10-12 01:25:27 +02:00
import { VariableUiElement } from "./Base/VariableUIElement" ;
import LiveQueryHandler from "../Logic/Web/LiveQueryHandler" ;
2020-10-14 12:15:09 +02:00
import { ImageCarousel } from "./Image/ImageCarousel" ;
import Combine from "./Base/Combine" ;
import { FixedUiElement } from "./Base/FixedUiElement" ;
import { ImageUploadFlow } from "./Image/ImageUploadFlow" ;
2021-01-03 13:50:18 +01:00
2021-01-04 04:06:21 +01:00
import ShareButton from "./BigComponents/ShareButton" ;
2020-11-22 03:50:09 +01:00
import Svg from "../Svg" ;
2020-12-08 23:44:34 +01:00
import ReviewElement from "./Reviews/ReviewElement" ;
import MangroveReviews from "../Logic/Web/MangroveReviews" ;
import Translations from "./i18n/Translations" ;
import ReviewForm from "./Reviews/ReviewForm" ;
2021-06-16 14:23:53 +02:00
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization" ;
2020-10-14 12:15:09 +02:00
2021-01-03 13:50:18 +01:00
import State from "../State" ;
2021-01-29 03:23:53 +01:00
import { ImageSearcher } from "../Logic/Actors/ImageSearcher" ;
2021-06-11 22:51:45 +02:00
import BaseUIElement from "./BaseUIElement" ;
2021-06-15 01:24:04 +02:00
import LayerConfig from "../Customizations/JSON/LayerConfig" ;
2021-06-21 00:19:19 +02:00
import Title from "./Base/Title" ;
import Table from "./Base/Table" ;
2021-06-21 03:13:05 +02:00
import Histogram from "./BigComponents/Histogram" ;
2021-06-23 02:15:28 +02:00
import Loc from "../Models/Loc" ;
2021-06-24 01:17:29 +02:00
import { Utils } from "../Utils" ;
2021-06-24 13:51:58 +02:00
import BaseLayer from "../Models/BaseLayer" ;
2021-01-03 13:50:18 +01:00
2021-07-01 02:43:49 +02:00
export interface SpecialVisualization {
2021-06-27 19:21:31 +02:00
funcName : string ,
constr : ( ( state : State , tagSource : UIEventSource < any > , argument : string [ ] ) = > BaseUIElement ) ,
docs : string ,
example? : string ,
args : { name : string , defaultValue? : string , doc : string } [ ]
}
2020-10-09 20:10:21 +02:00
export default class SpecialVisualizations {
2021-06-15 01:24:04 +02:00
2020-10-09 20:10:21 +02:00
2021-07-01 02:43:49 +02:00
static constructMiniMap : ( options ? : {
background? : UIEventSource < BaseLayer > ,
location? : UIEventSource < Loc > ,
2021-07-20 01:33:58 +02:00
allowMoving? : boolean ,
leafletOptions? : any
2021-07-01 02:43:49 +02:00
} ) = > BaseUIElement ;
static constructShowDataLayer : ( features : UIEventSource < { feature : any ; freshness : Date } [ ] > , leafletMap : UIEventSource < any > , layoutToUse : UIEventSource < any > , enablePopups? : boolean , zoomToFeatures? : boolean ) = > any ;
2021-06-27 19:21:31 +02:00
public static specialVisualizations : SpecialVisualization [ ] =
2021-06-22 03:16:45 +02:00
[
{
funcName : "all_tags" ,
docs : "Prints all key-value pairs of the object - used for debugging" ,
args : [ ] ,
constr : ( ( state : State , tags : UIEventSource < any > ) = > {
return new VariableUiElement ( tags . map ( tags = > {
const parts = [ ] ;
for ( const key in tags ) {
if ( ! tags . hasOwnProperty ( key ) ) {
continue ;
}
parts . push ( key + "=" + tags [ key ] ) ;
2021-06-11 22:51:45 +02:00
}
2021-06-22 03:16:45 +02:00
return parts . join ( "<br/>" )
} ) ) . SetStyle ( "border: 1px solid black; border-radius: 1em;padding:1em;display:block;" )
} )
} ,
2020-12-08 23:44:34 +01:00
2020-10-14 12:15:09 +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 : [ {
2020-10-17 02:37:53 +02:00
name : "image key/prefix" ,
defaultValue : "image" ,
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... "
} ,
{
name : "smart search" ,
defaultValue : "true" ,
doc : "Also include images given via 'Wikidata', 'wikimedia_commons' and 'mapillary"
} ] ,
2021-03-13 19:07:38 +01:00
constr : ( state : State , tags , args ) = > {
2021-01-29 03:23:53 +01:00
const imagePrefix = args [ 0 ] ;
const loadSpecial = args [ 1 ] . toLowerCase ( ) === "true" ;
2021-03-26 03:24:58 +01:00
const searcher : UIEventSource < { key : string , url : string } [ ] > = ImageSearcher . construct ( tags , imagePrefix , loadSpecial ) ;
2021-01-29 03:23:53 +01:00
return new ImageCarousel ( searcher , tags ) ;
2020-10-11 22:37:55 +02:00
}
2020-10-14 12:15:09 +02:00
} ,
{
funcName : "image_upload" ,
docs : "Creates a button where a user can upload an image to IMGUR" ,
args : [ {
2020-10-17 02:37:53 +02:00
name : "image-key" ,
2020-10-14 12:15:09 +02:00
doc : "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)" ,
2020-10-17 02:37:53 +02:00
defaultValue : "image"
2020-10-14 12:15:09 +02:00
} ] ,
2021-03-13 19:07:38 +01:00
constr : ( state : State , tags , args ) = > {
2020-10-17 02:37:53 +02:00
return new ImageUploadFlow ( tags , args [ 0 ] )
2020-10-14 12:15:09 +02:00
}
} ,
2021-06-23 02:15:28 +02:00
{
funcName : "minimap" ,
docs : "A small map showing the selected feature. Note that no styling is applied, wrap this in a div" ,
args : [
{
2021-06-27 19:21:31 +02:00
doc : "The (maximum) zoomlevel: the target zoomlevel after fitting the entire feature. The minimap will fit the entire feature, then zoom out to this zoom level. The higher, the more zoomed in with 1 being the entire world and 19 being really close" ,
2021-06-23 02:15:28 +02:00
name : "zoomlevel" ,
defaultValue : "18"
2021-06-24 01:17:29 +02:00
} ,
{
doc : "(Matches all resting arguments) This argument should be the key of a property of the feature. The corresponding value is interpreted as either the id or the a list of ID's. The features with these ID's will be shown on this minimap." ,
name : "idKey" ,
defaultValue : "id"
2021-06-23 02:15:28 +02:00
}
] ,
2021-06-24 01:17:29 +02:00
example : "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`" ,
2021-06-23 02:15:28 +02:00
constr : ( state , tagSource , args ) = > {
2021-06-24 01:17:29 +02:00
const keys = [ . . . args ]
keys . splice ( 0 , 1 )
const featureStore = state . allElements . ContainingFeatures
2021-06-24 01:55:45 +02:00
const featuresToShow : UIEventSource < { freshness : Date , feature : any } [ ] > = tagSource . map ( properties = > {
2021-06-24 01:17:29 +02:00
const values : string [ ] = Utils . NoNull ( keys . map ( key = > properties [ key ] ) )
const features : { freshness : Date , feature : any } [ ] = [ ]
for ( const value of values ) {
let idList = [ value ]
if ( value . startsWith ( "[" ) ) {
// This is a list of values
idList = JSON . parse ( value )
}
for ( const id of idList ) {
features . push ( {
freshness : new Date ( ) ,
feature : featureStore.get ( id )
} )
}
}
return features
} )
2021-06-23 02:15:28 +02:00
const properties = tagSource . data ;
let zoom = 18
2021-06-24 01:17:29 +02:00
if ( args [ 0 ] ) {
2021-06-23 02:15:28 +02:00
const parsed = Number ( args [ 0 ] )
2021-06-24 01:17:29 +02:00
if ( ! isNaN ( parsed ) && parsed > 0 && parsed < 25 ) {
2021-06-23 02:15:28 +02:00
zoom = parsed ;
}
}
2021-07-01 02:43:49 +02:00
const locationSource = new UIEventSource < Loc > ( {
2021-06-27 19:21:31 +02:00
lat : Number ( properties . _lat ) ,
lon : Number ( properties . _lon ) ,
zoom : zoom
} )
2021-06-24 13:51:58 +02:00
const minimap = SpecialVisualizations . constructMiniMap (
2021-06-23 02:15:28 +02:00
{
background : state.backgroundLayer ,
2021-06-27 19:21:31 +02:00
location : locationSource ,
2021-06-23 02:15:28 +02:00
allowMoving : false
}
)
2021-07-01 02:43:49 +02:00
2021-06-27 19:21:31 +02:00
locationSource . addCallback ( loc = > {
2021-07-01 02:43:49 +02:00
if ( loc . zoom > zoom ) {
2021-06-27 19:21:31 +02:00
// We zoom back
locationSource . data . zoom = zoom ;
locationSource . ping ( ) ;
}
} )
2021-06-23 02:15:28 +02:00
2021-06-24 13:51:58 +02:00
SpecialVisualizations . constructShowDataLayer (
2021-06-24 01:17:29 +02:00
featuresToShow ,
2021-06-24 13:51:58 +02:00
minimap [ "leafletMap" ] ,
2021-06-23 02:15:28 +02:00
State . state . layoutToUse ,
2021-06-24 01:17:29 +02:00
false ,
true
2021-06-23 02:15:28 +02:00
)
2020-12-08 23:44:34 +01:00
2021-06-24 01:55:45 +02:00
2021-06-23 02:15:28 +02:00
minimap . SetStyle ( "overflow: hidden; pointer-events: none;" )
return minimap ;
}
} ,
2020-12-08 23:44:34 +01:00
{
funcName : "reviews" ,
2021-03-13 19:07:38 +01:00
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 : "<b>{reviews()}<b> for a vanilla review, <b>{reviews(name, play_forest)}</b> to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used" ,
2020-12-31 22:41:52 +01:00
args : [ {
2021-03-13 19:07:38 +01:00
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"
2020-12-31 22:41:52 +01:00
} ] ,
2021-03-13 19:07:38 +01:00
constr : ( state : State , tags , args ) = > {
2020-12-08 23:44:34 +01:00
const tgs = tags . data ;
2021-03-13 19:07:38 +01:00
const key = args [ 0 ] ? ? "name"
let subject = tgs [ key ] ? ? args [ 1 ] ;
if ( subject === undefined || subject === "" ) {
2020-12-08 23:44:34 +01:00
return Translations . t . reviews . name_required ;
}
2020-12-31 21:13:16 +01:00
const mangrove = MangroveReviews . Get ( Number ( tgs . _lon ) , Number ( tgs . _lat ) ,
encodeURIComponent ( subject ) ,
2021-01-03 13:50:18 +01:00
state . mangroveIdentity ,
state . osmConnection . _dryRun
2020-12-08 23:44:34 +01:00
) ;
2021-06-14 19:21:33 +02:00
const form = new ReviewForm ( ( r , whenDone ) = > mangrove . AddReview ( r , whenDone ) , state . osmConnection ) ;
2020-12-08 23:44:34 +01:00
return new ReviewElement ( mangrove . GetSubjectUri ( ) , mangrove . GetReviews ( ) , form ) ;
}
} ,
2020-10-14 12:15:09 +02: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 : [ {
name : "key" ,
defaultValue : "opening_hours" ,
2020-10-17 02:37:53 +02:00
doc : "The tagkey from which the table is constructed."
2020-10-14 12:15:09 +02:00
} ] ,
2021-03-13 19:07:38 +01:00
constr : ( state : State , tagSource : UIEventSource < any > , args ) = > {
2021-06-16 14:23:53 +02:00
return new OpeningHoursVisualization ( tagSource , args [ 0 ] )
2020-10-14 12:15:09 +02:00
}
} ,
2020-10-11 22:37:55 +02:00
2020-10-12 01:25:27 +02: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)}" ,
2020-10-17 02:37:53 +02:00
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)}" ,
2020-10-12 01:25:27 +02:00
args : [ {
name : "Url" , doc : "The URL to load"
} , {
name : "Shorthands" ,
doc : "A list of shorthands, of the format 'shorthandname:path.path.path'. Seperated by ;"
} , {
name : "path" , doc : "The path (or shorthand) that should be returned"
} ] ,
2021-03-13 19:07:38 +01:00
constr : ( state : State , tagSource : UIEventSource < any > , args ) = > {
2020-10-12 01:25:27 +02: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..." ) ) ;
}
2020-10-19 12:08:42 +02:00
} ,
2021-06-21 03:13:05 +02:00
2021-06-20 03:09:55 +02:00
{
funcName : "histogram" ,
2021-06-21 03:13:05 +02:00
docs : "Create a histogram for a list of given values, read from the properties." ,
2021-06-20 03:09:55 +02:00
example : "`{histogram('some_key')}` with properties being `{some_key: ['a','b','a','c']} to create a histogram" ,
2021-06-21 03:13:05 +02:00
args : [
2021-06-20 03:09:55 +02:00
{
name : "key" ,
doc : "The key to be read and to generate a histogram from"
2021-06-21 03:13:05 +02:00
} ,
{
name : "title" ,
doc : "The text to put above the given values column" ,
defaultValue : ""
} ,
{
name : "countHeader" ,
doc : "The text to put above the counts" ,
defaultValue : ""
} ,
{
2021-06-24 01:17:29 +02:00
name : "colors*" ,
2021-06-21 03:13:05 +02:00
doc : "(Matches all resting arguments - optional) Matches a regex onto a color value, e.g. `3[a-zA-Z+-]*:#33cc33`"
2021-06-20 03:09:55 +02:00
}
] ,
2021-06-21 03:13:05 +02:00
constr : ( state : State , tagSource : UIEventSource < any > , args : string [ ] ) = > {
2021-06-20 03:09:55 +02:00
2021-06-21 03:13:05 +02:00
let assignColors = undefined ;
if ( args . length >= 3 ) {
const colors = [ . . . args ]
colors . splice ( 0 , 3 )
const mapping = colors . map ( c = > {
const splitted = c . split ( ":" ) ;
const value = splitted . pop ( )
const regex = splitted . join ( ":" )
return { regex : "^" + regex + "$" , color : value }
} )
assignColors = ( key ) = > {
for ( const kv of mapping ) {
if ( key . match ( kv . regex ) !== null ) {
return kv . color
2021-06-20 03:09:55 +02:00
}
2021-06-21 03:13:05 +02:00
}
return undefined
}
}
const listSource : UIEventSource < string [ ] > = tagSource
. map ( tags = > {
try {
const value = tags [ args [ 0 ] ]
if ( value === "" || value === undefined ) {
return undefined
2021-06-20 03:09:55 +02:00
}
2021-06-21 03:13:05 +02:00
return JSON . parse ( value )
} catch ( e ) {
console . error ( "Could not load histogram: parsing of the list failed: " , e )
return undefined ;
2021-06-20 03:09:55 +02:00
}
} )
2021-06-21 03:13:05 +02:00
return new Histogram ( listSource , args [ 1 ] , args [ 2 ] , assignColors )
}
2021-06-20 03:09:55 +02:00
} ,
2020-11-21 16:44:48 +01:00
{
funcName : "share_link" ,
docs : "Creates a link that (attempts to) open the native 'share'-screen" ,
example : "{share_link()} to share the current page, {share_link(<some_url>)} to share the given url" ,
args : [
{
name : "url" ,
2021-03-13 19:07:38 +01:00
doc : "The url to share (default: current URL)" ,
2020-11-21 16:44:48 +01:00
}
] ,
2021-03-13 19:07:38 +01:00
constr : ( state : State , tagSource : UIEventSource < any > , args ) = > {
2020-11-24 12:52:01 +01:00
if ( window . navigator . share ) {
2021-06-15 01:24:04 +02:00
const generateShareData = ( ) = > {
const title = state ? . layoutToUse ? . data ? . title ? . txt ? ? "MapComplete" ;
let matchingLayer : LayerConfig = undefined ;
for ( const layer of ( state ? . layoutToUse ? . data ? . layers ? ? [ ] ) ) {
if ( layer . source . osmTags . matchesProperties ( tagSource ? . data ) ) {
matchingLayer = layer
}
}
let name = matchingLayer ? . title ? . GetRenderValue ( tagSource . data ) ? . txt ? ? tagSource . data ? . name ? ? "POI" ;
if ( name ) {
name = ` ${ name } ( ${ title } ) `
} else {
name = title ;
}
let url = args [ 0 ] ? ? ""
if ( url === "" ) {
url = window . location . href
}
return {
title : name ,
url : url ,
text : state?.layoutToUse?.data?.shortDescription?.txt ? ? "MapComplete"
}
2020-11-23 12:54:10 +01:00
}
2021-06-15 01:24:04 +02:00
2021-06-22 03:16:45 +02:00
return new ShareButton ( Svg . share_svg ( ) . SetClass ( "w-8 h-8" ) , generateShareData )
2020-11-21 16:44:48 +01:00
} else {
2020-11-23 02:55:18 +01:00
return new FixedUiElement ( "" )
2020-11-21 16:44:48 +01:00
}
2020-11-17 16:29:51 +01:00
2020-11-21 16:44:48 +01:00
}
2021-06-22 03:16:45 +02:00
} ,
2021-06-24 01:55:45 +02:00
{
funcName : "canonical" ,
docs : "Converts a short, canonical value into the long, translated text" ,
example : "{canonical(length)} will give 42 metre (in french)" ,
args : [ {
name : "key" ,
doc : "The key of the tag to give the canonical text for"
} ] ,
constr : ( state , tagSource , args ) = > {
const key = args [ 0 ]
return new VariableUiElement (
tagSource . map ( tags = > tags [ key ] ) . map ( value = > {
if ( value === undefined ) {
return undefined
}
const unit = state . layoutToUse . data . units . filter ( unit = > unit . isApplicableToKey ( key ) ) [ 0 ]
if ( unit === undefined ) {
return value ;
}
return unit . asHumanLongValue ( value ) ;
} ,
[ state . layoutToUse ] )
)
}
2020-11-21 16:44:48 +01:00
}
2020-10-12 01:25:27 +02:00
2020-10-11 22:37:55 +02:00
]
2021-07-11 15:44:17 +02:00
2021-07-03 14:35:44 +02:00
static HelpMessage : BaseUIElement = SpecialVisualizations . GenHelpMessage ( ) ;
2020-10-17 02:37:53 +02:00
private static GenHelpMessage() {
const helpTexts =
SpecialVisualizations . specialVisualizations . map ( viz = > new Combine (
[
2021-06-21 00:19:19 +02:00
new Title ( viz . funcName , 3 ) ,
2020-10-17 02:37:53 +02:00
viz . docs ,
2021-06-21 03:13:05 +02:00
new Table ( [ "name" , "default" , "description" ] ,
2021-06-21 00:19:19 +02:00
viz . args . map ( arg = > [ arg . name , arg . defaultValue ? ? "undefined" , arg . doc ] )
2021-06-21 03:13:05 +02:00
) ,
2021-06-21 00:19:19 +02:00
new Title ( "Example usage" , 4 ) ,
2020-10-17 02:37:53 +02:00
new FixedUiElement (
viz . example ? ? "{" + viz . funcName + "(" + viz . args . map ( arg = > arg . defaultValue ) . join ( "," ) + ")}"
) . SetClass ( "literal-code" ) ,
]
) ) ;
return new Combine ( [
2021-06-21 03:13:05 +02:00
new Title ( "Special tag renderings" , 3 ) ,
2020-10-17 02:37:53 +02:00
"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." ,
2021-06-24 14:03:02 +02:00
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_fcs need to use quotes around your arguments, the comma is enough to seperate them. This also implies you cannot use a comma in your args" ,
2020-10-17 02:37:53 +02:00
. . . helpTexts
]
) ;
}
2020-10-09 20:10:21 +02:00
}