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"
2022-01-07 17:31:39 +01:00
import ValidatedTextField from "../../UI/Input/ValidatedTextField"
2022-10-27 01:50:41 +02:00
import { TagConfigJson } from "./Json/TagConfigJson"
import { UIEventSource } from "../../Logic/UIEventSource"
import { FilterState } from "../FilteredLayer"
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"
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 , ` ${ 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"
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 ) {
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
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 )
2022-09-08 21:40:48 +02:00
2022-01-08 04:22:50 +01:00
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-09-08 21:40:48 +02:00
} )
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 ,
}
2022-01-08 22:11:24 +01:00
} catch ( e ) {
2022-01-08 04:22:50 +01:00
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
)
}
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
}