Add filters

This commit is contained in:
pietervdvn 2022-01-08 22:11:24 +01:00
parent 965faca0e5
commit 42a6b37ca6
13 changed files with 287 additions and 219 deletions

1
.gitignore vendored
View file

@ -18,3 +18,4 @@ missing_translations.txt
Svg.ts
data/
Folder.DotSettings.user
index_*.ts

View file

@ -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<boolean> }[]
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<BaseLayer>(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<Map<string, FilterState>>(new Map<string, FilterState>())
appliedFilters: new UIEventSource<Map<string, FilterState>>(new Map<string, FilterState>())
};
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))
})

View file

@ -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] = "..*"

View file

@ -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 = (<string>v).replace("{"+key+"}", props[key])
v = (<string>v).replace("{" + key + "}", props[key])
}
return v
}
)
const parsed = TagUtils.Tag(rewrittenTags)
return <FilterState>{
currentFilter: TagUtils.Tag(rewrittenTags),
currentFilter: parsed,
state: str
}
} catch (e) {

View file

@ -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 {

View file

@ -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

View file

@ -35,7 +35,7 @@ export default class SimpleDatePicker extends InputElement<string> {
}
IsValid(t: string): boolean {
return false;
return !isNaN(new Date(t).getTime());
}
protected InnerConstructElement(): HTMLElement {

View file

@ -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);

View file

@ -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": "<a href='https://openstreetmap.org/note/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'></a>"
}
],
"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": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={_first_user_id}&reportable_type=User' target='_blank' class='subtle'>Report {_first_user} as spam</a>"
},
"condition": "_opened_by_anonymous_user=false"
},
{
"id": "report-note",
"render": {
"en": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={id}&reportable_type=Note' target='_blank'>Report this note as spam or inappropriate</a>"
}
}
],
"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 <b>not</b> 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": "<b>Not</b> 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"
}
}
]
}
]
}

View file

@ -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": "<a href='https://openstreetmap.org/note/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'></a>"
}
],
"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": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={_first_user_id}&reportable_type=User' target='_blank' class='subtle'>Report {_first_user} as spam</a>"
},
"condition": "_opened_by_anonymous_user=false"
},
{
"id": "report-note",
"render": {
"en": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={id}&reportable_type=Note' target='_blank'>Report this note as spam or inappropriate</a>"
}
}
],
"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 <b>not</b> 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": "<b>Not</b> opened by {search}"
}
}
]
},
{
"id": "anonymous",
"options": [
{
"osmTags": "_opened_by_anonymous_user=true",
"question": {
"en": "Opened by anonymous user"
}
}
]
}
]
}
"note"
]
}

View file

@ -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/

View file

@ -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",

View file

@ -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")
}]]);
}
}