2021-08-07 23:11:34 +02:00
import { Translation } from "../../UI/i18n/Translation" ;
import { TagsFilter } from "../../Logic/Tags/TagsFilter" ;
import FilterConfigJson from "./Json/FilterConfigJson" ;
import Translations from "../../UI/i18n/Translations" ;
import { TagUtils } from "../../Logic/Tags/TagUtils" ;
2022-01-07 17:31:39 +01:00
import ValidatedTextField from "../../UI/Input/ValidatedTextField" ;
2022-07-18 00:10:41 +02:00
import { TagConfigJson } from "./Json/TagConfigJson" ;
2022-01-08 04:22:50 +01:00
import { UIEventSource } from "../../Logic/UIEventSource" ;
import { FilterState } from "../FilteredLayer" ;
import { QueryParameters } from "../../Logic/Web/QueryParameters" ;
import { Utils } from "../../Utils" ;
2022-06-07 03:38:13 +02:00
import { RegexTag } from "../../Logic/Tags/RegexTag" ;
2021-08-07 23:11:34 +02:00
export default class FilterConfig {
2021-09-27 18:35:32 +02:00
public readonly id : string
public readonly options : {
2021-08-07 23:11:34 +02:00
question : Translation ;
2022-01-08 04:22:50 +01:00
osmTags : TagsFilter | undefined ;
2022-07-18 00:10:41 +02:00
originalTagsSpec : TagConfigJson
2022-01-07 17:31:39 +01:00
fields : { name : string , type : string } [ ]
2021-08-07 23:11:34 +02:00
} [ ] ;
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 ,
2022-01-07 17:31:39 +01:00
` ${ ctx } .question `
2021-08-07 23:11:34 +02:00
) ;
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 ,
2022-01-07 17:31:39 +01:00
` ${ 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"
2022-02-12 02:53:41 +01:00
if ( ! ValidatedTextField . ForType ( type ) === undefined ) {
throw ` Invalid filter: ${ type } is not a valid validated textfield type (at ${ ctx } .fields[ ${ i } ]) \ n \ tTry one of ${ Array . from ( ValidatedTextField . AvailableTypes ( ) ) . join ( "," ) } `
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 ) {
question . OnEveryLanguage ( ( txt , language ) = > {
if ( txt . indexOf ( "{" + field . name + "}" ) < 0 ) {
throw "Error in filter with fields at " + context + ".question." + language + ": The question text should contain every field, but it doesn't contain `{" + field + "}`: " + txt
}
return txt
} )
}
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-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 } ;
2021-08-07 23:11:34 +02:00
} ) ;
2022-02-11 03:57:39 +01: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"
2021-09-30 00:26:21 +02:00
}
2022-02-11 03:57:39 +01: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 == '^..*$' || 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
public initState ( ) : UIEventSource < FilterState > {
function reset ( state : FilterState ) : string {
if ( state === undefined ) {
return ""
}
return "" + state . state
}
2022-01-08 22:11:24 +01:00
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 )
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"
2022-02-11 03:57:39 +01:00
}
}
2022-01-08 04:22:50 +01:00
const qp = QueryParameters . GetQueryParameter ( "filter-" + this . id , defaultValue , "State of filter " + this . id )
if ( this . options . length > 1 ) {
// This is a multi-option filter; state should be a number which selects the correct entry
const possibleStates : FilterState [ ] = this . options . map ( ( opt , i ) = > ( {
currentFilter : opt.osmTags ,
state : i
} ) )
// 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
}
return possibleStates [ parsed ]
} , [ ] , reset )
}
const option = this . options [ 0 ]
if ( option . fields . length > 0 ) {
2022-06-05 02:24:14 +02:00
return qp . sync ( str = > {
2022-01-08 04:22:50 +01:00
// There are variables in play!
// str should encode a json-hash
try {
const props = JSON . parse ( str )
const origTags = option . originalTagsSpec
const rewrittenTags = Utils . WalkJson ( origTags ,
v = > {
if ( typeof v !== "string" ) {
return v
}
for ( const key in props ) {
2022-01-08 22:11:24 +01:00
v = ( < string > v ) . replace ( "{" + key + "}" , props [ key ] )
2022-01-08 04:22:50 +01:00
}
return v
}
)
2022-01-08 22:11:24 +01:00
const parsed = TagUtils . Tag ( rewrittenTags )
2022-01-08 04:22:50 +01:00
return < FilterState > {
2022-01-08 22:11:24 +01:00
currentFilter : parsed ,
2022-01-08 04:22:50 +01:00
state : str
}
} catch ( e ) {
return undefined
}
} , [ ] , reset )
}
// The last case is pretty boring: it is checked or it isn't
const filterState : FilterState = {
currentFilter : option.osmTags ,
state : "true"
}
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" ) {
return filterState
}
return undefined
} , [ ] ,
reset
)
}
2021-08-07 23:11:34 +02:00
}