forked from MapComplete/MapComplete
Add filters
This commit is contained in:
parent
965faca0e5
commit
42a6b37ca6
13 changed files with 287 additions and 219 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -18,3 +18,4 @@ missing_translations.txt
|
||||||
Svg.ts
|
Svg.ts
|
||||||
data/
|
data/
|
||||||
Folder.DotSettings.user
|
Folder.DotSettings.user
|
||||||
|
index_*.ts
|
|
@ -52,7 +52,7 @@ export default class MapState extends UserRelatedState {
|
||||||
* The location as delivered by the GPS
|
* The location as delivered by the GPS
|
||||||
*/
|
*/
|
||||||
public currentUserLocation: FeatureSourceForLayer & Tiled;
|
public currentUserLocation: FeatureSourceForLayer & Tiled;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All previously visited points
|
* All previously visited points
|
||||||
*/
|
*/
|
||||||
|
@ -82,7 +82,7 @@ export default class MapState extends UserRelatedState {
|
||||||
public overlayToggles: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]
|
public overlayToggles: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]
|
||||||
|
|
||||||
|
|
||||||
constructor(layoutToUse: LayoutConfig, options?: {attemptLogin: true | boolean}) {
|
constructor(layoutToUse: LayoutConfig, options?: { attemptLogin: true | boolean }) {
|
||||||
super(layoutToUse, options);
|
super(layoutToUse, options);
|
||||||
|
|
||||||
this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl);
|
this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl);
|
||||||
|
@ -97,7 +97,7 @@ export default class MapState extends UserRelatedState {
|
||||||
const self = this
|
const self = this
|
||||||
this.backgroundLayer = new UIEventSource<BaseLayer>(defaultLayer)
|
this.backgroundLayer = new UIEventSource<BaseLayer>(defaultLayer)
|
||||||
this.backgroundLayer.addCallbackAndRunD(layer => self.backgroundLayerId.setData(layer.id))
|
this.backgroundLayer.addCallbackAndRunD(layer => self.backgroundLayerId.setData(layer.id))
|
||||||
|
|
||||||
const attr = new Attribution(
|
const attr = new Attribution(
|
||||||
this.locationControl,
|
this.locationControl,
|
||||||
this.osmConnection.userDetails,
|
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]
|
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
|
// This layer is not needed by the theme and thus unloaded
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -188,8 +188,8 @@ export default class MapState extends UserRelatedState {
|
||||||
|
|
||||||
let i = 0
|
let i = 0
|
||||||
const self = this;
|
const self = this;
|
||||||
const features : UIEventSource<{ feature: any, freshness: Date }[]>= this.currentBounds.map(bounds => {
|
const features: UIEventSource<{ feature: any, freshness: Date }[]> = this.currentBounds.map(bounds => {
|
||||||
if(bounds === undefined){
|
if (bounds === undefined) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
|
@ -197,14 +197,14 @@ export default class MapState extends UserRelatedState {
|
||||||
freshness: new Date(),
|
freshness: new Date(),
|
||||||
feature: {
|
feature: {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
properties:{
|
properties: {
|
||||||
id:"current_view-"+i,
|
id: "current_view-" + i,
|
||||||
"current_view":"yes",
|
"current_view": "yes",
|
||||||
"zoom": ""+self.locationControl.data.zoom
|
"zoom": "" + self.locationControl.data.zoom
|
||||||
},
|
},
|
||||||
geometry:{
|
geometry: {
|
||||||
type:"Polygon",
|
type: "Polygon",
|
||||||
coordinates:[[
|
coordinates: [[
|
||||||
[bounds.maxLon, bounds.maxLat],
|
[bounds.maxLon, bounds.maxLat],
|
||||||
[bounds.minLon, bounds.maxLat],
|
[bounds.minLon, bounds.maxLat],
|
||||||
[bounds.minLon, bounds.minLat],
|
[bounds.minLon, bounds.minLat],
|
||||||
|
@ -216,13 +216,16 @@ export default class MapState extends UserRelatedState {
|
||||||
}
|
}
|
||||||
return [feature]
|
return [feature]
|
||||||
})
|
})
|
||||||
|
|
||||||
this.currentView = new SimpleFeatureSource(currentViewLayer,0,features)
|
this.currentView = new SimpleFeatureSource(currentViewLayer, 0, features)
|
||||||
}
|
}
|
||||||
|
|
||||||
private initGpsLocation() {
|
private initGpsLocation() {
|
||||||
// Initialize the gps layer data. This is emtpy for now, the actual writing happens in the Geolocationhandler
|
// 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]
|
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));
|
this.currentUserLocation = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,7 +238,7 @@ export default class MapState extends UserRelatedState {
|
||||||
features.ping()
|
features.ping()
|
||||||
const self = this;
|
const self = this;
|
||||||
let i = 0
|
let i = 0
|
||||||
this.currentUserLocation.features.addCallbackAndRunD(([location]) => {
|
this.currentUserLocation?.features?.addCallbackAndRunD(([location]) => {
|
||||||
if (location === undefined) {
|
if (location === undefined) {
|
||||||
return;
|
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]
|
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 => {
|
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]
|
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() {
|
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]
|
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 {
|
} else {
|
||||||
isDisplayed = QueryParameters.GetBooleanQueryParameter(
|
isDisplayed = QueryParameters.GetBooleanQueryParameter(
|
||||||
"layer-" + layer.id,
|
"layer-" + layer.id,
|
||||||
""+layer.shownByDefault,
|
"" + layer.shownByDefault,
|
||||||
"Wether or not layer " + layer.id + " is shown"
|
"Wether or not layer " + layer.id + " is shown"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const flayer : FilteredLayer = {
|
const flayer: FilteredLayer = {
|
||||||
isDisplayed: isDisplayed,
|
isDisplayed: isDisplayed,
|
||||||
layerDef: layer,
|
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 => {
|
layer.filters.forEach(filterConfig => {
|
||||||
const stateSrc = filterConfig.initState()
|
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))
|
flayer.appliedFilters.map(dict => dict.get(filterConfig.id))
|
||||||
.addCallback(state => stateSrc.setData(state))
|
.addCallback(state => stateSrc.setData(state))
|
||||||
})
|
})
|
||||||
|
|
|
@ -56,9 +56,6 @@ export class TagUtils {
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* Creates a hash {key --> [values : string | Regex ]}, with all the values present in the tagsfilter
|
* Creates a hash {key --> [values : string | Regex ]}, with all the values present in the tagsfilter
|
||||||
*
|
|
||||||
* @param tagsFilters
|
|
||||||
* @constructor
|
|
||||||
*/
|
*/
|
||||||
static SplitKeys(tagsFilters: TagsFilter[], allowRegex = false) {
|
static SplitKeys(tagsFilters: TagsFilter[], allowRegex = false) {
|
||||||
const keyValues = {} // Map string -> string[]
|
const keyValues = {} // Map string -> string[]
|
||||||
|
@ -189,16 +186,26 @@ export class TagUtils {
|
||||||
if (tag.indexOf(operator) >= 0) {
|
if (tag.indexOf(operator) >= 0) {
|
||||||
const split = Utils.SplitFirst(tag, operator);
|
const split = Utils.SplitFirst(tag, operator);
|
||||||
|
|
||||||
const val = Number(split[1].trim())
|
let val = Number(split[1].trim())
|
||||||
if (isNaN(val)) {
|
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 f = (value: string | undefined) => {
|
||||||
const b = Number(value?.replace(/[^\d.]/g, ''))
|
console.log("Comparing ",value,operator,val)
|
||||||
if (isNaN(b)) {
|
if(value === undefined){
|
||||||
return false;
|
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 comparator(b, val)
|
||||||
}
|
}
|
||||||
return new ComparingTag(split[0], f, operator + val)
|
return new ComparingTag(split[0], f, operator + val)
|
||||||
|
@ -259,8 +266,8 @@ export class TagUtils {
|
||||||
}
|
}
|
||||||
if (tag.indexOf("~") >= 0) {
|
if (tag.indexOf("~") >= 0) {
|
||||||
const split = Utils.SplitFirst(tag, "~");
|
const split = Utils.SplitFirst(tag, "~");
|
||||||
if(split[1] === "") {
|
if (split[1] === "") {
|
||||||
throw "Detected a regextag with an empty regex; this is not allowed. Use '"+split[0]+"='instead (at "+context+")"
|
throw "Detected a regextag with an empty regex; this is not allowed. Use '" + split[0] + "='instead (at " + context + ")"
|
||||||
}
|
}
|
||||||
if (split[1] === "*") {
|
if (split[1] === "*") {
|
||||||
split[1] = "..*"
|
split[1] = "..*"
|
||||||
|
|
|
@ -42,12 +42,11 @@ export default class FilterConfig {
|
||||||
`${ctx}.question`
|
`${ctx}.question`
|
||||||
);
|
);
|
||||||
let osmTags = undefined;
|
let osmTags = undefined;
|
||||||
if (option.osmTags !== undefined) {
|
if ((option.fields?.length ?? 0) == 0 && option.osmTags !== undefined) {
|
||||||
osmTags = TagUtils.Tag(
|
osmTags = TagUtils.Tag(
|
||||||
option.osmTags,
|
option.osmTags,
|
||||||
`${ctx}.osmTags`
|
`${ctx}.osmTags`
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
if (question === undefined) {
|
if (question === undefined) {
|
||||||
throw `Invalid filter: no question given at ${ctx}`
|
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};
|
return {question: question, osmTags: osmTags, fields, originalTagsSpec: option.osmTags};
|
||||||
});
|
});
|
||||||
|
@ -92,6 +87,7 @@ export default class FilterConfig {
|
||||||
}
|
}
|
||||||
return "" + state.state
|
return "" + state.state
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultValue = this.options.length > 1 ? "0" : ""
|
const defaultValue = this.options.length > 1 ? "0" : ""
|
||||||
const qp = QueryParameters.GetQueryParameter("filter-" + this.id, defaultValue, "State of filter " + this.id)
|
const qp = QueryParameters.GetQueryParameter("filter-" + this.id, defaultValue, "State of filter " + this.id)
|
||||||
|
|
||||||
|
@ -130,13 +126,14 @@ export default class FilterConfig {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
for (const key in props) {
|
for (const key in props) {
|
||||||
v = (<string>v).replace("{"+key+"}", props[key])
|
v = (<string>v).replace("{" + key + "}", props[key])
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
const parsed = TagUtils.Tag(rewrittenTags)
|
||||||
return <FilterState>{
|
return <FilterState>{
|
||||||
currentFilter: TagUtils.Tag(rewrittenTags),
|
currentFilter: parsed,
|
||||||
state: str
|
state: str
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -22,7 +22,6 @@ import Title from "../../UI/Base/Title";
|
||||||
import List from "../../UI/Base/List";
|
import List from "../../UI/Base/List";
|
||||||
import Link from "../../UI/Base/Link";
|
import Link from "../../UI/Base/Link";
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
import {tag} from "@turf/turf";
|
|
||||||
|
|
||||||
export default class LayerConfig extends WithContextLoader {
|
export default class LayerConfig extends WithContextLoader {
|
||||||
|
|
||||||
|
|
|
@ -195,7 +195,7 @@ export default class FilterView extends VariableUiElement {
|
||||||
}
|
}
|
||||||
const props = properties.data
|
const props = properties.data
|
||||||
// Replace all the field occurences in the tags...
|
// Replace all the field occurences in the tags...
|
||||||
const tagsSpec = Utils.WalkJson(filter.originalTagsSpec,
|
const tagsSpec = Utils.WalkJson(filter.originalTagsSpec,
|
||||||
v => {
|
v => {
|
||||||
if (typeof v !== "string") {
|
if (typeof v !== "string") {
|
||||||
return v
|
return v
|
||||||
|
|
|
@ -35,7 +35,7 @@ export default class SimpleDatePicker extends InputElement<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
IsValid(t: string): boolean {
|
IsValid(t: string): boolean {
|
||||||
return false;
|
return !isNaN(new Date(t).getTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected InnerConstructElement(): HTMLElement {
|
protected InnerConstructElement(): HTMLElement {
|
||||||
|
|
|
@ -206,8 +206,7 @@ export default class ValidatedTextField {
|
||||||
"date",
|
"date",
|
||||||
"A date",
|
"A date",
|
||||||
(str) => {
|
(str) => {
|
||||||
const time = Date.parse(str);
|
return !isNaN(new Date(str).getTime());
|
||||||
return !isNaN(time);
|
|
||||||
},
|
},
|
||||||
(str) => {
|
(str) => {
|
||||||
const d = new Date(str);
|
const d = new Date(str);
|
||||||
|
|
206
assets/layers/note/note.json
Normal file
206
assets/layers/note/note.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -14,166 +14,6 @@
|
||||||
"clustering": false,
|
"clustering": false,
|
||||||
"enableDownload": true,
|
"enableDownload": true,
|
||||||
"layers": [
|
"layers": [
|
||||||
{
|
"note"
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -8,12 +8,16 @@ rm -rf .cache
|
||||||
mkdir dist 2> /dev/null
|
mkdir dist 2> /dev/null
|
||||||
mkdir dist/assets 2> /dev/null
|
mkdir dist/assets 2> /dev/null
|
||||||
|
|
||||||
npm run generate
|
# This script ends every line with '&&' to chain everything. A failure will thus stop the build
|
||||||
npm run test
|
|
||||||
npm run generate:editor-layer-index
|
npm run generate:editor-layer-index
|
||||||
npm run generate:translations
|
npm run generate &&
|
||||||
|
npm run test &&
|
||||||
npm run generate:layouts
|
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)
|
# Copy the layer files, as these might contain assets (e.g. svgs)
|
||||||
cp -r assets/layers/ dist/assets/layers/
|
cp -r assets/layers/ dist/assets/layers/
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
import T from "./TestHelper";
|
import T from "./TestHelper";
|
||||||
import {Utils} from "../Utils";
|
import {Utils} from "../Utils";
|
||||||
import ReplaceGeometryAction from "../Logic/Osm/Actions/ReplaceGeometryAction";
|
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 * as grb from "../assets/themes/grb_import/grb.json"
|
||||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
||||||
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
|
|
||||||
import State from "../State";
|
import State from "../State";
|
||||||
import {BBox} from "../Logic/BBox";
|
import {BBox} from "../Logic/BBox";
|
||||||
import Minimap from "../UI/Base/Minimap";
|
import Minimap from "../UI/Base/Minimap";
|
||||||
|
@ -33,7 +29,11 @@ export default class ReplaceGeometrySpec extends T {
|
||||||
},
|
},
|
||||||
"layers": [
|
"layers": [
|
||||||
{
|
{
|
||||||
"builtin": "type_node",
|
"id": "type_node",
|
||||||
|
source:{
|
||||||
|
osmTags:"type=node"
|
||||||
|
},
|
||||||
|
mapRendering: null,
|
||||||
"override": {
|
"override": {
|
||||||
"calculatedTags": [
|
"calculatedTags": [
|
||||||
"_is_part_of_building=feat.get('parent_ways')?.some(p => p.building !== undefined && p.building !== '') ?? false",
|
"_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",
|
"id": "grb",
|
||||||
"description": "Geometry which comes from GRB with tools to import them",
|
"description": "Geometry which comes from GRB with tools to import them",
|
||||||
|
|
|
@ -516,7 +516,14 @@ export default class TagSpec extends T {
|
||||||
const filter = TagUtils.Tag("_key~*")
|
const filter = TagUtils.Tag("_key~*")
|
||||||
T.isTrue(filter.matchesProperties(properties), "Lazy value not matched")
|
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")
|
||||||
|
|
||||||
|
}]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue