2022-10-27 01:50:41 +02:00
import { Translation } from "../../UI/i18n/Translation"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
2021-08-07 23:11:34 +02:00
import FilterConfigJson from "./Json/FilterConfigJson"
import Translations from "../../UI/i18n/Translations"
2022-10-27 01:50:41 +02:00
import { TagUtils } from "../../Logic/Tags/TagUtils"
import { TagConfigJson } from "./Json/TagConfigJson"
import { UIEventSource } from "../../Logic/UIEventSource"
import { QueryParameters } from "../../Logic/Web/QueryParameters"
import { Utils } from "../../Utils"
import { RegexTag } from "../../Logic/Tags/RegexTag"
2022-12-16 13:45:07 +01:00
import BaseUIElement from "../../UI/BaseUIElement"
import Table from "../../UI/Base/Table"
import Combine from "../../UI/Base/Combine"
2023-04-06 01:33:08 +02:00
export type FilterConfigOption = {
question : Translation
osmTags : TagsFilter | undefined
/* Only set if fields are present. Used to create `osmTags` (which are used to _actually_ filter) when the field is written*/
readonly originalTagsSpec : TagConfigJson
fields : { name : string ; type : string } [ ]
}
2021-08-07 23:11:34 +02:00
export default class FilterConfig {
2021-09-27 18:35:32 +02:00
public readonly id : string
2023-04-06 01:33:08 +02:00
public readonly options : FilterConfigOption [ ]
2022-02-14 15:59:42 +01:00
public readonly defaultSelection? : number
2022-01-08 04:22:50 +01:00
2021-08-07 23:11:34 +02:00
constructor ( json : FilterConfigJson , context : string ) {
2021-09-09 00:05:51 +02:00
if ( json . options === undefined ) {
2021-09-06 22:19:57 +02:00
throw ` A filter without options was given at ${ context } `
}
2021-09-27 18:35:32 +02:00
if ( json . id === undefined ) {
throw ` A filter without id was found at ${ context } `
}
2021-11-07 16:34:51 +01:00
if ( json . id . match ( /^[a-zA-Z0-9_-]*$/ ) === null ) {
2021-09-27 18:35:32 +02:00
throw ` A filter with invalid id was found at ${ context } . Ids should only contain letters, numbers or - _ `
}
2021-09-06 22:19:57 +02:00
2021-09-09 00:05:51 +02:00
if ( json . options . map === undefined ) {
2021-09-06 22:19:57 +02:00
throw ` A filter was given where the options aren't a list at ${ context } `
}
2021-09-27 18:35:32 +02:00
this . id = json . id
2022-02-11 03:57:39 +01:00
let defaultSelection : number = undefined
2021-08-07 23:11:34 +02:00
this . options = json . options . map ( ( option , i ) = > {
2022-04-01 12:51:55 +02:00
const ctx = ` ${ context } .options. ${ i } `
2021-08-07 23:11:34 +02:00
const question = Translations . T ( option . question , ` ${ ctx } .question ` )
2022-06-07 03:38:13 +02:00
let osmTags : undefined | TagsFilter = undefined
2022-01-08 22:11:24 +01:00
if ( ( option . fields ? . length ? ? 0 ) == 0 && option . osmTags !== undefined ) {
2022-01-08 04:22:50 +01:00
osmTags = TagUtils . Tag ( option . osmTags , ` ${ ctx } .osmTags ` )
2022-06-07 03:38:13 +02:00
FilterConfig . validateSearch ( osmTags , ctx )
2022-01-08 04:22:50 +01:00
}
2021-09-09 00:05:51 +02:00
if ( question === undefined ) {
2022-01-07 17:31:39 +01:00
throw ` Invalid filter: no question given at ${ ctx } `
2021-09-07 00:23:00 +02:00
}
2021-08-07 23:11:34 +02:00
2022-01-07 17:31:39 +01:00
const fields : { name : string ; type : string } [ ] = ( option . fields ? ? [ ] ) . map ( ( f , i ) = > {
const type = f . type ? ? "string"
2023-03-24 19:21:15 +01:00
// Type is validated against 'ValidatedTextField' in Validation.ts, in ValidateFilterConfig
2022-01-07 17:31:39 +01:00
if ( f . name === undefined || f . name === "" || f . name . match ( /[a-z0-9_-]+/ ) == null ) {
throw ` Invalid filter: a variable name should match [a-z0-9_-]+ at ${ ctx } .fields[ ${ i } ] `
}
return {
name : f.name ,
type ,
}
} )
2022-01-08 04:22:50 +01:00
2022-03-29 21:31:59 +02:00
for ( const field of fields ) {
2022-10-29 03:02:42 +02:00
for ( let ln in question . translations ) {
const txt = question . translations [ ln ]
2022-11-02 14:44:06 +01:00
if ( ln . startsWith ( "_" ) ) {
2022-10-29 03:02:42 +02:00
continue
}
2022-03-29 21:31:59 +02:00
if ( txt . indexOf ( "{" + field . name + "}" ) < 0 ) {
throw (
"Error in filter with fields at " +
context +
".question." +
2022-10-29 03:02:42 +02:00
ln +
2022-03-29 21:31:59 +02:00
": The question text should contain every field, but it doesn't contain `{" +
field +
"}`: " +
txt
2022-09-08 21:40:48 +02:00
)
2022-03-29 21:31:59 +02:00
}
2022-10-29 03:02:42 +02:00
}
2022-03-29 21:31:59 +02:00
}
2022-02-11 03:57:39 +01:00
if ( option . default ) {
if ( defaultSelection === undefined ) {
defaultSelection = i
} else {
throw ` Invalid filter: multiple filters are set as default, namely ${ i } and ${ defaultSelection } at ${ context } `
}
}
2022-09-08 21:40:48 +02:00
2022-06-07 03:38:13 +02:00
if ( option . osmTags !== undefined ) {
FilterConfig . validateSearch ( TagUtils . Tag ( option . osmTags ) , ctx )
}
2022-01-08 04:22:50 +01:00
2022-01-07 17:31:39 +01:00
return {
question : question ,
osmTags : osmTags ,
fields ,
originalTagsSpec : option.osmTags ,
2022-09-08 21:40:48 +02:00
}
2021-08-07 23:11:34 +02:00
} )
2022-09-08 21:40:48 +02:00
2022-02-14 15:59:42 +01:00
this . defaultSelection = defaultSelection
2021-11-07 16:34:51 +01:00
2022-01-07 17:31:39 +01:00
if ( this . options . some ( ( o ) = > o . fields . length > 0 ) && this . options . length > 1 ) {
throw ` Invalid filter at ${ context } : a filter with textfields should only offer a single option. `
}
2022-01-08 04:22:50 +01:00
if ( this . options . length > 1 && this . options [ 0 ] . osmTags !== undefined ) {
2021-11-07 16:34:51 +01:00
throw (
"Error in " +
context +
"." +
this . id +
": the first option of a multi-filter should always be the 'reset' option and not have any filters"
2022-09-08 21:40:48 +02:00
)
2021-09-30 00:26:21 +02:00
}
2021-08-07 23:11:34 +02:00
}
2022-01-08 04:22:50 +01:00
2022-06-07 03:38:13 +02:00
private static validateSearch ( osmTags : TagsFilter , ctx : string ) {
osmTags . visit ( ( t ) = > {
if ( ! ( t instanceof RegexTag ) ) {
return
}
if ( typeof t . value == "string" ) {
return
}
if (
t . value . source == "^..*$" ||
2022-10-11 01:01:24 +02:00
t . value . source == ".+" ||
2022-06-07 03:38:13 +02:00
t . value . source == "^[\\s\\S][\\s\\S]*$" /*Compiled regex with 'm'*/
) {
return
}
if ( ! t . value . ignoreCase ) {
throw ` At ${ ctx } : The filter for key ' ${ t . key } ' uses a regex ' ${ t . value } ', but you should use a case invariant regex with ~i~ instead, as search should be case insensitive `
}
} )
}
2022-01-08 04:22:50 +01:00
2023-04-06 02:20:25 +02:00
public initState ( layerId : string ) : UIEventSource < undefined | number | string > {
2022-02-11 03:57:39 +01:00
let defaultValue = ""
if ( this . options . length > 1 ) {
2022-02-14 15:59:42 +01:00
defaultValue = "" + ( this . defaultSelection ? ? 0 )
2023-04-06 02:20:25 +02:00
} else if ( this . options [ 0 ] . fields ? . length > 0 ) {
defaultValue = "{}"
2022-02-11 03:57:39 +01:00
} else {
2022-02-14 15:41:14 +01:00
// Only a single option
2022-02-14 15:59:42 +01:00
if ( this . defaultSelection === 0 ) {
2022-02-14 15:41:14 +01:00
defaultValue = "true"
2023-03-28 05:13:48 +02:00
} else {
defaultValue = "false"
2022-02-11 03:57:39 +01:00
}
}
2022-01-08 04:22:50 +01:00
const qp = QueryParameters . GetQueryParameter (
2023-04-06 02:20:25 +02:00
` filter- ${ layerId } - ${ this . id } ` ,
2022-01-08 04:22:50 +01:00
defaultValue ,
"State of filter " + this . id
)
if ( this . options . length > 1 ) {
// We map the query parameter for this case
2022-06-05 02:24:14 +02:00
return qp . sync (
( str ) = > {
2022-01-08 04:22:50 +01:00
const parsed = Number ( str )
if ( isNaN ( parsed ) ) {
// Nope, not a correct number!
return undefined
}
2023-03-28 05:13:48 +02:00
return parsed
2022-01-08 04:22:50 +01:00
} ,
[ ] ,
2023-03-28 05:13:48 +02:00
( n ) = > "" + n
2022-01-08 04:22:50 +01:00
)
}
const option = this . options [ 0 ]
if ( option . fields . length > 0 ) {
2023-03-28 05:13:48 +02:00
return qp
2022-01-08 04:22:50 +01:00
}
2022-06-05 02:24:14 +02:00
return qp . sync (
2022-01-08 04:22:50 +01:00
( str ) = > {
// Only a single option exists here
if ( str === "true" ) {
2023-03-28 05:13:48 +02:00
return 0
2022-01-08 04:22:50 +01:00
}
return undefined
} ,
[ ] ,
2023-03-28 05:13:48 +02:00
( n ) = > ( n === undefined ? "false" : "true" )
2022-01-08 04:22:50 +01:00
)
}
2022-12-06 03:41:54 +01:00
public GenerateDocs ( ) : BaseUIElement {
2022-12-16 13:45:07 +01:00
const hasField = this . options . some ( ( opt ) = > opt . fields ? . length > 0 )
2022-12-06 03:41:54 +01:00
return new Table (
2022-12-16 13:45:07 +01:00
Utils . NoNull ( [ "id" , "question" , "osmTags" , hasField ? "fields" : undefined ] ) ,
2022-12-06 03:41:54 +01:00
this . options . map ( ( opt , i ) = > {
2022-12-16 13:45:07 +01:00
const isDefault = this . options . length > 1 && ( this . defaultSelection ? ? 0 ) == i
2022-12-06 03:41:54 +01:00
return Utils . NoNull ( [
this . id + "." + i ,
2022-12-16 13:45:07 +01:00
isDefault
? new Combine ( [ opt . question . SetClass ( "font-bold" ) , "(default)" ] )
: opt . question ,
2022-12-06 03:41:54 +01:00
opt . osmTags ? . asHumanString ( false , false , { } ) ? ? "" ,
2022-12-16 13:45:07 +01:00
opt . fields ? . length > 0
? new Combine ( opt . fields . map ( ( f ) = > f . name + " (" + f . type + ")" ) )
: undefined ,
] )
2022-12-06 03:41:54 +01:00
} )
2022-12-16 13:45:07 +01:00
)
2022-12-06 03:41:54 +01:00
}
2021-08-07 23:11:34 +02:00
}