From 42a6b37ca6208296f99017d49fd0b5b5a45bbb3c Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Sat, 8 Jan 2022 22:11:24 +0100 Subject: [PATCH] Add filters --- .gitignore | 1 + Logic/State/MapState.ts | 61 +++++---- Logic/Tags/TagUtils.ts | 25 ++-- Models/ThemeConfig/FilterConfig.ts | 13 +- Models/ThemeConfig/LayerConfig.ts | 1 - UI/BigComponents/FilterView.ts | 2 +- UI/Input/SimpleDatePicker.ts | 2 +- UI/Input/ValidatedTextField.ts | 3 +- assets/layers/note/note.json | 206 +++++++++++++++++++++++++++++ assets/themes/notes/notes.json | 162 +---------------------- scripts/build.sh | 10 +- test/ReplaceGeometry.spec.ts | 11 +- test/Tag.spec.ts | 9 +- 13 files changed, 287 insertions(+), 219 deletions(-) create mode 100644 assets/layers/note/note.json diff --git a/.gitignore b/.gitignore index b2ad34580..0ae99c0e0 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ missing_translations.txt Svg.ts data/ Folder.DotSettings.user +index_*.ts \ No newline at end of file diff --git a/Logic/State/MapState.ts b/Logic/State/MapState.ts index 85a216a22..b5e7bb178 100644 --- a/Logic/State/MapState.ts +++ b/Logic/State/MapState.ts @@ -52,7 +52,7 @@ export default class MapState extends UserRelatedState { * The location as delivered by the GPS */ public currentUserLocation: FeatureSourceForLayer & Tiled; - + /** * All previously visited points */ @@ -82,7 +82,7 @@ export default class MapState extends UserRelatedState { public overlayToggles: { config: TilesourceConfig, isDisplayed: UIEventSource }[] - constructor(layoutToUse: LayoutConfig, options?: {attemptLogin: true | boolean}) { + constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) { super(layoutToUse, options); this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl); @@ -97,7 +97,7 @@ export default class MapState extends UserRelatedState { const self = this this.backgroundLayer = new UIEventSource(defaultLayer) this.backgroundLayer.addCallbackAndRunD(layer => self.backgroundLayerId.setData(layer.id)) - + const attr = new Attribution( this.locationControl, this.osmConnection.userDetails, @@ -176,11 +176,11 @@ export default class MapState extends UserRelatedState { }) } } - - private initCurrentView(){ + + private initCurrentView() { let currentViewLayer: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "current_view")[0] - if(currentViewLayer === undefined){ + if (currentViewLayer === undefined) { // This layer is not needed by the theme and thus unloaded return; } @@ -188,8 +188,8 @@ export default class MapState extends UserRelatedState { let i = 0 const self = this; - const features : UIEventSource<{ feature: any, freshness: Date }[]>= this.currentBounds.map(bounds => { - if(bounds === undefined){ + const features: UIEventSource<{ feature: any, freshness: Date }[]> = this.currentBounds.map(bounds => { + if (bounds === undefined) { return [] } i++ @@ -197,14 +197,14 @@ export default class MapState extends UserRelatedState { freshness: new Date(), feature: { type: "Feature", - properties:{ - id:"current_view-"+i, - "current_view":"yes", - "zoom": ""+self.locationControl.data.zoom + properties: { + id: "current_view-" + i, + "current_view": "yes", + "zoom": "" + self.locationControl.data.zoom }, - geometry:{ - type:"Polygon", - coordinates:[[ + geometry: { + type: "Polygon", + coordinates: [[ [bounds.maxLon, bounds.maxLat], [bounds.minLon, bounds.maxLat], [bounds.minLon, bounds.minLat], @@ -216,13 +216,16 @@ export default class MapState extends UserRelatedState { } return [feature] }) - - this.currentView = new SimpleFeatureSource(currentViewLayer,0,features) + + this.currentView = new SimpleFeatureSource(currentViewLayer, 0, features) } private initGpsLocation() { // Initialize the gps layer data. This is emtpy for now, the actual writing happens in the Geolocationhandler let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_location")[0] + if(gpsLayerDef === undefined){ + return + } this.currentUserLocation = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0)); } @@ -235,7 +238,7 @@ export default class MapState extends UserRelatedState { features.ping() const self = this; let i = 0 - this.currentUserLocation.features.addCallbackAndRunD(([location]) => { + this.currentUserLocation?.features?.addCallbackAndRunD(([location]) => { if (location === undefined) { return; } @@ -266,7 +269,9 @@ export default class MapState extends UserRelatedState { let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_location_history")[0] - this.historicalUserLocations = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0), features); + if(gpsLayerDef !== undefined){ + this.historicalUserLocations = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0), features); + } const asLine = features.map(allPoints => { @@ -294,7 +299,9 @@ export default class MapState extends UserRelatedState { }] }) let gpsLineLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_track")[0] - this.historicalUserLocationsTrack = new SimpleFeatureSource(gpsLineLayerDef, Tiles.tile_index(0, 0, 0), asLine); + if(gpsLineLayerDef !== undefined){ + this.historicalUserLocationsTrack = new SimpleFeatureSource(gpsLineLayerDef, Tiles.tile_index(0, 0, 0), asLine); + } } private initHomeLocation() { @@ -331,7 +338,9 @@ export default class MapState extends UserRelatedState { }) const flayer = this.filteredLayers.data.filter(l => l.layerDef.id === "home_location")[0] - this.homeLocation = new SimpleFeatureSource(flayer, Tiles.tile_index(0, 0, 0), feature) + if (flayer !== undefined) { + this.homeLocation = new SimpleFeatureSource(flayer, Tiles.tile_index(0, 0, 0), feature) + } } @@ -349,19 +358,19 @@ export default class MapState extends UserRelatedState { } else { isDisplayed = QueryParameters.GetBooleanQueryParameter( "layer-" + layer.id, - ""+layer.shownByDefault, + "" + layer.shownByDefault, "Wether or not layer " + layer.id + " is shown" ) } - const flayer : FilteredLayer = { + const flayer: FilteredLayer = { isDisplayed: isDisplayed, layerDef: layer, - appliedFilters: new UIEventSource>(new Map()) + appliedFilters: new UIEventSource>(new Map()) }; layer.filters.forEach(filterConfig => { const stateSrc = filterConfig.initState() - - stateSrc .addCallbackAndRun(state => flayer.appliedFilters.data.set(filterConfig.id, state)) + + stateSrc.addCallbackAndRun(state => flayer.appliedFilters.data.set(filterConfig.id, state)) flayer.appliedFilters.map(dict => dict.get(filterConfig.id)) .addCallback(state => stateSrc.setData(state)) }) diff --git a/Logic/Tags/TagUtils.ts b/Logic/Tags/TagUtils.ts index e662beca6..d389bae54 100644 --- a/Logic/Tags/TagUtils.ts +++ b/Logic/Tags/TagUtils.ts @@ -56,9 +56,6 @@ export class TagUtils { /*** * Creates a hash {key --> [values : string | Regex ]}, with all the values present in the tagsfilter - * - * @param tagsFilters - * @constructor */ static SplitKeys(tagsFilters: TagsFilter[], allowRegex = false) { const keyValues = {} // Map string -> string[] @@ -189,16 +186,26 @@ export class TagUtils { if (tag.indexOf(operator) >= 0) { const split = Utils.SplitFirst(tag, operator); - const val = Number(split[1].trim()) + let val = Number(split[1].trim()) if (isNaN(val)) { - throw `Error: not a valid value for a comparison: ${split[1]}, make sure it is a number and nothing more (at ${context})` + val = new Date(split[1].trim()).getTime() } const f = (value: string | undefined) => { - const b = Number(value?.replace(/[^\d.]/g, '')) - if (isNaN(b)) { + console.log("Comparing ",value,operator,val) + if(value === undefined){ return false; } + let b = Number(value?.trim() ) + if (isNaN(b)) { + if(value.endsWith(" UTC")) { + value = value.replace(" UTC", "+00") + } + b = new Date(value).getTime() + if(isNaN(b)){ + return false + } + } return comparator(b, val) } return new ComparingTag(split[0], f, operator + val) @@ -259,8 +266,8 @@ export class TagUtils { } if (tag.indexOf("~") >= 0) { const split = Utils.SplitFirst(tag, "~"); - if(split[1] === "") { - throw "Detected a regextag with an empty regex; this is not allowed. Use '"+split[0]+"='instead (at "+context+")" + if (split[1] === "") { + throw "Detected a regextag with an empty regex; this is not allowed. Use '" + split[0] + "='instead (at " + context + ")" } if (split[1] === "*") { split[1] = "..*" diff --git a/Models/ThemeConfig/FilterConfig.ts b/Models/ThemeConfig/FilterConfig.ts index 08aeec7fa..32785a354 100644 --- a/Models/ThemeConfig/FilterConfig.ts +++ b/Models/ThemeConfig/FilterConfig.ts @@ -42,12 +42,11 @@ export default class FilterConfig { `${ctx}.question` ); let osmTags = undefined; - if (option.osmTags !== undefined) { + if ((option.fields?.length ?? 0) == 0 && option.osmTags !== undefined) { osmTags = TagUtils.Tag( option.osmTags, `${ctx}.osmTags` ); - } if (question === undefined) { throw `Invalid filter: no question given at ${ctx}` @@ -67,10 +66,6 @@ export default class FilterConfig { } }) - if (fields.length > 0) { - // erase the tags, they aren't needed - osmTags = undefined - } return {question: question, osmTags: osmTags, fields, originalTagsSpec: option.osmTags}; }); @@ -92,6 +87,7 @@ export default class FilterConfig { } return "" + state.state } + const defaultValue = this.options.length > 1 ? "0" : "" const qp = QueryParameters.GetQueryParameter("filter-" + this.id, defaultValue, "State of filter " + this.id) @@ -130,13 +126,14 @@ export default class FilterConfig { return v } for (const key in props) { - v = (v).replace("{"+key+"}", props[key]) + v = (v).replace("{" + key + "}", props[key]) } return v } ) + const parsed = TagUtils.Tag(rewrittenTags) return { - currentFilter: TagUtils.Tag(rewrittenTags), + currentFilter: parsed, state: str } } catch (e) { diff --git a/Models/ThemeConfig/LayerConfig.ts b/Models/ThemeConfig/LayerConfig.ts index b2c52c74d..c12c3df14 100644 --- a/Models/ThemeConfig/LayerConfig.ts +++ b/Models/ThemeConfig/LayerConfig.ts @@ -22,7 +22,6 @@ import Title from "../../UI/Base/Title"; import List from "../../UI/Base/List"; import Link from "../../UI/Base/Link"; import {Utils} from "../../Utils"; -import {tag} from "@turf/turf"; export default class LayerConfig extends WithContextLoader { diff --git a/UI/BigComponents/FilterView.ts b/UI/BigComponents/FilterView.ts index b51368437..e4cf5eaf7 100644 --- a/UI/BigComponents/FilterView.ts +++ b/UI/BigComponents/FilterView.ts @@ -195,7 +195,7 @@ export default class FilterView extends VariableUiElement { } const props = properties.data // Replace all the field occurences in the tags... - const tagsSpec = Utils.WalkJson(filter.originalTagsSpec, + const tagsSpec = Utils.WalkJson(filter.originalTagsSpec, v => { if (typeof v !== "string") { return v diff --git a/UI/Input/SimpleDatePicker.ts b/UI/Input/SimpleDatePicker.ts index 133d03d2e..a131e456e 100644 --- a/UI/Input/SimpleDatePicker.ts +++ b/UI/Input/SimpleDatePicker.ts @@ -35,7 +35,7 @@ export default class SimpleDatePicker extends InputElement { } IsValid(t: string): boolean { - return false; + return !isNaN(new Date(t).getTime()); } protected InnerConstructElement(): HTMLElement { diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 61e822060..4a64cbe8e 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -206,8 +206,7 @@ export default class ValidatedTextField { "date", "A date", (str) => { - const time = Date.parse(str); - return !isNaN(time); + return !isNaN(new Date(str).getTime()); }, (str) => { const d = new Date(str); diff --git a/assets/layers/note/note.json b/assets/layers/note/note.json new file mode 100644 index 000000000..b4f28782c --- /dev/null +++ b/assets/layers/note/note.json @@ -0,0 +1,206 @@ +{ + "id": "note", + "name": { + "en": "OpenStreetMap notes" + }, + "description": "This layer shows notes on OpenStreetMap.", + "source": { + "osmTags": "id~*", + "geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?closed=7&bbox={x_min},{y_min},{x_max},{y_max}", + "geoJsonZoomLevel": 12, + "maxCacheAge": 0 + }, + "minzoom": 10, + "title": { + "render": { + "en": "Note" + }, + "mappings": [ + { + "if": "closed_at~*", + "then": { + "en": "Closed note" + } + } + ] + }, + "calculatedTags": [ + "_first_comment:=feat.get('comments')[0].text.toLowerCase()", + "_opened_by_anonymous_user:=feat.get('comments')[0].user === undefined", + "_first_user:=feat.get('comments')[0].user", + "_first_user_lc:=feat.get('comments')[0].user?.toLowerCase()", + "_first_user_id:=feat.get('comments')[0].uid" + ], + "titleIcons": [ + { + "render": "" + } + ], + "tagRenderings": [ + { + "id": "conversation", + "render": "{visualize_note_comments()}" + }, + { + "id": "add_image", + "render": "{add_image_to_note()}" + }, + { + "id": "comment", + "render": "{add_note_comment()}" + }, + { + "id": "report-contributor", + "render": { + "en": "Report {_first_user} as spam" + }, + "condition": "_opened_by_anonymous_user=false" + }, + { + "id": "report-note", + "render": { + "en": "Report this note as spam or inappropriate" + } + } + ], + "mapRendering": [ + { + "location": [ + "point", + "centroid" + ], + "icon": { + "render": "./assets/svg/note.svg", + "mappings": [ + { + "if": "closed_at~*", + "then": "./assets/svg/resolved.svg" + } + ] + }, + "iconSize": "40,40,bottom" + } + ], + "filter": [ + { + "id": "search", + "options": [ + { + "osmTags": "_first_comment~.*{search}.*", + "fields": [ + { + "name": "search" + } + ], + "question": { + "en": "Should mention {search} in the first comment" + } + } + ] + }, + { + "id": "not", + "options": [ + { + "osmTags": "_first_comment!~.*{search}.*", + "fields": [ + { + "name": "search" + } + ], + "question": { + "en": "Should not mention {search} in the first comment" + } + } + ] + }, + { + "id": "opened_by", + "options": [ + { + "osmTags": "_first_user_lc~.*{search}.*", + "fields": [ + { + "name": "search" + } + ], + "question": { + "en": "Opened by {search}" + } + } + ] + }, + { + "id": "not_opened_by", + "options": [ + { + "osmTags": "_first_user_lc!~.*{search}.*", + "fields": [ + { + "name": "search" + } + ], + "question": { + "en": "Not opened by {search}" + } + } + ] + }, + { + "id": "opened_before", + "options": [ + { + "osmTags": "date_created<{search}", + "fields": [ + { + "name": "search", + "type": "date" + } + ], + "question": { + "en": "Opened before {search}" + } + } + ] + }, + { + "id": "opened_after", + "options": [ + { + "osmTags": "date_created>{search}", + "fields": [ + { + "name": "search", + "type": "date" + } + ], + "question": { + "en": "Opened after {search}" + } + } + ] + }, + { + "id": "anonymous", + "options": [ + { + "osmTags": "_opened_by_anonymous_user=true", + "question": { + "en": "Opened by anonymous user" + } + } + ] + }, + { + "id": "is_open", + "options": [ + { + "osmTags": "closed_at=", + "question": { + "en": "Only show open notes" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/assets/themes/notes/notes.json b/assets/themes/notes/notes.json index ebc13eb12..b92bbbadc 100644 --- a/assets/themes/notes/notes.json +++ b/assets/themes/notes/notes.json @@ -14,166 +14,6 @@ "clustering": false, "enableDownload": true, "layers": [ - { - "id": "notes", - "name": { - "en": "OpenStreetMap notes" - }, - "description": "This layer shows notes on OpenStreetMap.", - "source": { - "osmTags": "id~*", - "geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?closed=7&bbox={x_min},{y_min},{x_max},{y_max}", - "geoJsonZoomLevel": 12, - "maxCacheAge": 0 - }, - "minzoom": 8, - "title": { - "render": { - "en": "Note" - }, - "mappings": [ - { - "if": "closed_at~*", - "then": { - "en": "Closed note" - } - } - ] - }, - "calculatedTags": [ - "_first_comment:=feat.get('comments')[0].text.toLowerCase()", - "_opened_by_anonymous_user:=feat.get('comments')[0].user === undefined", - "_first_user:=feat.get('comments')[0].user", - "_first_user_lc:=feat.get('comments')[0].user?.toLowerCase()", - "_first_user_id:=feat.get('comments')[0].uid" - ], - "titleIcons": [ - { - "render": "" - } - ], - "tagRenderings": [ - { - "id": "conversation", - "render": "{visualize_note_comments()}" - }, - { - "id": "add_image", - "render": "{add_image_to_note()}" - }, - { - "id": "comment", - "render": "{add_note_comment()}" - }, - { - "id": "report-contributor", - "render": { - "en": "Report {_first_user} as spam" - }, - "condition": "_opened_by_anonymous_user=false" - }, - { - "id": "report-note", - "render": { - "en": "Report this note as spam or inappropriate" - } - } - ], - "mapRendering": [ - { - "location": [ - "point", - "centroid" - ], - "icon": { - "render": "./assets/svg/note.svg", - "mappings": [ - { - "if": "closed_at~*", - "then": "./assets/svg/resolved.svg" - } - ] - }, - "iconSize": "40,40,bottom" - } - ], - "filter": [ - { - "id": "search", - "options": [ - { - "osmTags": "_first_comment~.*{search}.*", - "fields": [ - { - "name": "search" - } - ], - "question": { - "en": "Should mention {search} in the first comment" - } - } - ] - }, - { - "id": "not", - "options": [ - { - "osmTags": "_first_comment!~.*{search}.*", - "fields": [ - { - "name": "search" - } - ], - "question": { - "en": "Should not mention {search} in the first comment" - } - } - ] - }, - { - "id": "opened_by", - "options": [ - { - "osmTags": "_first_user_lc~.*{search}.*", - "fields": [ - { - "name": "search" - } - ], - "question": { - "en": "Opened by {search}" - } - } - ] - }, - { - "id": "not_opened_by", - "options": [ - { - "osmTags": "_first_user_lc!~.*{search}.*", - "fields": [ - { - "name": "search" - } - ], - "question": { - "en": "Not opened by {search}" - } - } - ] - }, - { - "id": "anonymous", - "options": [ - { - "osmTags": "_opened_by_anonymous_user=true", - "question": { - "en": "Opened by anonymous user" - } - } - ] - } - ] - } + "note" ] } \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh index 498b345ee..d2e91ded7 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -8,12 +8,16 @@ rm -rf .cache mkdir dist 2> /dev/null mkdir dist/assets 2> /dev/null -npm run generate -npm run test +# This script ends every line with '&&' to chain everything. A failure will thus stop the build npm run generate:editor-layer-index -npm run generate:translations +npm run generate && +npm run test && npm run generate:layouts +if [ $? -ne 0]; then; + echo "ERROR" + exit 1 +fi # Copy the layer files, as these might contain assets (e.g. svgs) cp -r assets/layers/ dist/assets/layers/ diff --git a/test/ReplaceGeometry.spec.ts b/test/ReplaceGeometry.spec.ts index acc87d216..0e3caece9 100644 --- a/test/ReplaceGeometry.spec.ts +++ b/test/ReplaceGeometry.spec.ts @@ -1,12 +1,8 @@ import T from "./TestHelper"; import {Utils} from "../Utils"; import ReplaceGeometryAction from "../Logic/Osm/Actions/ReplaceGeometryAction"; -import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline"; -import {Tag} from "../Logic/Tags/Tag"; -import MapState from "../Logic/State/MapState"; import * as grb from "../assets/themes/grb_import/grb.json" import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; -import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; import State from "../State"; import {BBox} from "../Logic/BBox"; import Minimap from "../UI/Base/Minimap"; @@ -33,7 +29,11 @@ export default class ReplaceGeometrySpec extends T { }, "layers": [ { - "builtin": "type_node", + "id": "type_node", + source:{ + osmTags:"type=node" + }, + mapRendering: null, "override": { "calculatedTags": [ "_is_part_of_building=feat.get('parent_ways')?.some(p => p.building !== undefined && p.building !== '') ?? false", @@ -266,7 +266,6 @@ export default class ReplaceGeometrySpec extends T { } ] }, - "address", { "id": "grb", "description": "Geometry which comes from GRB with tools to import them", diff --git a/test/Tag.spec.ts b/test/Tag.spec.ts index 86ba6abda..028ba169d 100644 --- a/test/Tag.spec.ts +++ b/test/Tag.spec.ts @@ -516,7 +516,14 @@ export default class TagSpec extends T { const filter = TagUtils.Tag("_key~*") T.isTrue(filter.matchesProperties(properties), "Lazy value not matched") } - ]]); + ], + ["test date comparison",() => { + + const filter = TagUtils.Tag("date_created<2022-01-07") + T.isFalse(filter.matchesProperties({"date_created":"2022-01-08"}), "Date comparison: expected a match") + T.isTrue(filter.matchesProperties({"date_created":"2022-01-01"}), "Date comparison: didn't expect a match") + + }]]); } }