Fix bbox bug, add ids to filters, add filter state to the URL

This commit is contained in:
pietervdvn 2021-09-27 18:35:32 +02:00
parent 38037014b0
commit 0a9e7c0b36
23 changed files with 248 additions and 59 deletions

View file

@ -42,6 +42,8 @@ import {Tiles} from "./Models/TileRange";
import {TileHierarchyAggregator} from "./UI/ShowDataLayer/PerTileCountAggregator";
import {BBox} from "./Logic/GeoOperations";
import StaticFeatureSource from "./Logic/FeatureSource/Sources/StaticFeatureSource";
import FilterConfig from "./Models/ThemeConfig/FilterConfig";
import FilteredLayer from "./Models/FilteredLayer";
export class InitUiElements {
static InitAll(
@ -406,8 +408,10 @@ export class InitUiElements {
private static InitLayers(): void {
const state = State.state;
const empty = []
state.filteredLayers = state.layoutToUse.map((layoutToUse) => {
const flayers = [];
const flayers: FilteredLayer[] = [];
for (const layer of layoutToUse.layers) {
const isDisplayed = QueryParameters.GetQueryParameter(
@ -422,23 +426,40 @@ export class InitUiElements {
const flayer = {
isDisplayed: isDisplayed,
layerDef: layer,
appliedFilters: new UIEventSource<TagsFilter>(undefined),
appliedFilters: new UIEventSource<{ filter: FilterConfig, selected: number }[]>([]),
};
if (layer.filters.length > 0) {
const filtersPerName = new Map<string, FilterConfig>()
layer.filters.forEach(f => filtersPerName.set(f.id, f))
const qp = QueryParameters.GetQueryParameter("filter-" + layer.id, "","Filtering state for a layer")
flayer.appliedFilters.map(filters => {
filters = filters ?? []
return filters.map(f => f.filter.id + "." + f.selected).join(",")
}, [], textual => {
if(textual.length === 0){
return empty
}
return textual.split(",").map(part => {
const [filterId, selected] = part.split(".");
return {filter: filtersPerName.get(filterId), selected: Number(selected)}
}).filter(f => f.filter !== undefined && !isNaN(f.selected))
}).syncWith(qp, true)
}
flayers.push(flayer);
}
return flayers;
});
const layers = State.state.layoutToUse.data.layers
const clusterShow = Math.min(...layers.map(layer => layer.minzoom))
const layers = State.state.layoutToUse.data.layers
const clusterCounter = TileHierarchyAggregator.createHierarchy()
new ShowDataLayer({
features: clusterCounter.getCountsForZoom(State.state.locationControl, State.state.layoutToUse.data.clustering.minNeededElements),
leafletMap: State.state.leafletMap,
layerToShow: ShowTileInfo.styling,
doShowLayer: layers.length === 1 ? undefined : State.state.locationControl.map(l => l.zoom < clusterShow)
})
State.state.featurePipeline = new FeaturePipeline(

View file

@ -5,13 +5,14 @@ import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import Hash from "../../Web/Hash";
import {BBox} from "../../GeoOperations";
export default class FilteringFeatureSource implements FeatureSourceForLayer , Tiled {
export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled {
public features: UIEventSource<{ feature: any; freshness: Date }[]> =
new UIEventSource<{ feature: any; freshness: Date }[]>([]);
public readonly name;
public readonly layer: FilteredLayer;
public readonly tileIndex : number
public readonly bbox : BBox
public readonly tileIndex: number
public readonly bbox: BBox
constructor(
state: {
locationControl: UIEventSource<{ zoom: number }>,
@ -21,7 +22,7 @@ public readonly tileIndex : number
upstream: FeatureSourceForLayer
) {
const self = this;
this.name = "FilteringFeatureSource("+upstream.name+")"
this.name = "FilteringFeatureSource(" + upstream.name + ")"
this.tileIndex = tileIndex
this.bbox = BBox.fromTileIndex(tileIndex)
@ -50,12 +51,15 @@ public readonly tileIndex : number
}
const tagsFilter = layer.appliedFilters.data;
if (tagsFilter) {
if (!tagsFilter.matchesProperties(f.feature.properties)) {
for (const filter of tagsFilter ?? []) {
const neededTags = filter.filter.options[filter.selected].osmTags
if (!neededTags.matchesProperties(f.feature.properties)) {
// Hidden by the filter on the layer itself - we want to hide it no matter wat
return false;
}
}
if (!layer.isDisplayed) {
// The layer itself is either disabled or hidden due to zoom constraints
// We should return true, but it might still match some other layer
@ -80,7 +84,7 @@ public readonly tileIndex : number
});
layer.appliedFilters.addCallback(_ => {
if(!layer.isDisplayed.data){
if (!layer.isDisplayed.data) {
// Currently not shown.
// Note that a change in 'isSHown' will trigger an update as well, so we don't have to watch it another time
return;

View file

@ -150,6 +150,7 @@ export default class MetaTagging {
for (const f of functions) {
f(params, feature);
}
State.state.allElements.getEventSourceById(feature.properties.id).ping();
} catch (e) {
console.error("While calculating a tag value: ", e)
}

View file

@ -2,7 +2,7 @@ import {Utils} from "../Utils";
export default class Constants {
public static vNumber = "0.10.0-alpha-2";
public static vNumber = "0.10.0-alpha-3";
public static ImgurApiKey = '7070e7167f0a25a'
// The user journey states thresholds when a new feature gets unlocked

View file

@ -1,9 +1,10 @@
import {UIEventSource} from "../Logic/UIEventSource";
import LayerConfig from "./ThemeConfig/LayerConfig";
import {And} from "../Logic/Tags/And";
import FilterConfig from "./ThemeConfig/FilterConfig";
export default interface FilteredLayer {
readonly isDisplayed: UIEventSource<boolean>;
readonly appliedFilters: UIEventSource<And>;
readonly appliedFilters: UIEventSource<{filter: FilterConfig, selected: number}[]>;
readonly layerDef: LayerConfig;
}

View file

@ -5,7 +5,8 @@ import Translations from "../../UI/i18n/Translations";
import {TagUtils} from "../../Logic/Tags/TagUtils";
export default class FilterConfig {
readonly options: {
public readonly id: string
public readonly options: {
question: Translation;
osmTags: TagsFilter;
}[];
@ -14,11 +15,18 @@ export default class FilterConfig {
if (json.options === undefined) {
throw `A filter without options was given at ${context}`
}
if (json.id === undefined) {
throw `A filter without id was found at ${context}`
}
if(json.id.match(/^[a-zA-Z0-9_-]*$/) === null){
throw `A filter with invalid id was found at ${context}. Ids should only contain letters, numbers or - _`
}
if (json.options.map === undefined) {
throw `A filter was given where the options aren't a list at ${context}`
}
this.id = json.id;
this.options = json.options.map((option, i) => {
const question = Translations.T(
option.question,

View file

@ -1,6 +1,10 @@
import {AndOrTagConfigJson} from "./TagConfigJson";
export default interface FilterConfigJson {
/**
* An id/name for this filter, used to set the URL parameters
*/
id: string,
/**
* The options for a filter
* If there are multiple options these will be a list of radio buttons

View file

@ -7,8 +7,6 @@ import Combine from "../Base/Combine";
import Translations from "../i18n/Translations";
import {Translation} from "../i18n/Translation";
import Svg from "../../Svg";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import {And} from "../../Logic/Tags/And";
import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement";
import State from "../../State";
@ -16,11 +14,6 @@ import FilteredLayer from "../../Models/FilteredLayer";
import BackgroundSelector from "./BackgroundSelector";
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
/**
* Shows the filter
*/
export default class FilterView extends VariableUiElement {
constructor(filteredLayer: UIEventSource<FilteredLayer[]>) {
const backgroundSelector = new Toggle(
@ -101,26 +94,52 @@ export default class FilterView extends VariableUiElement {
return undefined;
}
let listFilterElements: [BaseUIElement, UIEventSource<TagsFilter>][] = layer.filters.map(
const filterIndexes = new Map<string, number>()
layer.filters.forEach((f, i) => filterIndexes.set(f.id, i))
let listFilterElements: [BaseUIElement, UIEventSource<{ filter: FilterConfig, selected: number }>][] = layer.filters.map(
FilterView.createFilter
);
const update = () => {
let listTagsFilters = Utils.NoNull(
listFilterElements.map((input) => input[1].data)
);
flayer.appliedFilters.setData(new And(listTagsFilters));
};
listFilterElements.forEach((inputElement, i) =>
inputElement[1].addCallback((changed) => {
const oldValue = flayer.appliedFilters.data
listFilterElements.forEach((inputElement) =>
inputElement[1].addCallback((_) => update())
if(changed === undefined){
// Lets figure out which filter should be removed
// We know this inputElement corresponds with layer.filters[i]
// SO, if there is a value in 'oldValue' with this filter, we have to recalculated
if(!oldValue.some(f => f.filter === layer.filters[i])){
// The filter to remove is already gone, we can stop
return;
}
}else if(oldValue.some(f => f.filter === changed.filter && f.selected === changed.selected)){
// The changed value is already there
return;
}
const listTagsFilters = Utils.NoNull(
listFilterElements.map((input) => input[1].data)
);
console.log(listTagsFilters, oldValue)
flayer.appliedFilters.setData(listTagsFilters);
})
);
flayer.appliedFilters.addCallbackAndRun(appliedFilters => {
if (appliedFilters === undefined || appliedFilters.and.length === 0) {
listFilterElements.forEach(filter => filter[1].setData(undefined))
return
for (let i = 0; i < layer.filters.length; i++){
const filter = layer.filters[i];
let foundMatch = undefined
for (const appliedFilter of appliedFilters) {
if(appliedFilter.filter === filter){
foundMatch = appliedFilter
break;
}
}
listFilterElements[i][1].setData(foundMatch)
}
})
return new Combine(listFilterElements.map(input => input[0].SetClass("mt-3")))
@ -128,7 +147,7 @@ export default class FilterView extends VariableUiElement {
}
private static createFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<TagsFilter>] {
private static createFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<{ filter: FilterConfig, selected: number }>] {
if (filterConfig.options.length === 1) {
let option = filterConfig.options[0];
@ -142,20 +161,36 @@ export default class FilterView extends VariableUiElement {
.ToggleOnClick()
.SetClass("block m-1")
return [toggle, toggle.isEnabled.map(enabled => enabled ? option.osmTags : undefined, [], tags => tags !== undefined)]
const selected = {
filter: filterConfig,
selected: 0
}
return [toggle, toggle.isEnabled.map(enabled => enabled ? selected : undefined, [],
f => f?.filter === filterConfig && f?.selected === 0)
]
}
let options = filterConfig.options;
const values = options.map((f, i) => ({
filter: filterConfig, selected: i
}))
const radio = new RadioButton(
options.map(
(option) =>
new FixedInputElement(option.question.Clone(), option.osmTags)
(option, i) =>
new FixedInputElement(option.question.Clone(), i)
),
{
dontStyle: true
}
);
return [radio, radio.GetValue()]
return [radio,
radio.GetValue().map(
i => values[i],
[],
selected => {
return selected?.selected
}
)]
}
}

View file

@ -223,14 +223,14 @@ export default class SimpleAddUI extends Toggle {
]
).SetClass("flex flex-col")
).onClick(() => {
preset.layerToAddTo.appliedFilters.setData(new And([]))
preset.layerToAddTo.appliedFilters.setData([])
cancel()
})
const disableFiltersOrConfirm = new Toggle(
openLayerOrConfirm,
disableFilter,
preset.layerToAddTo.appliedFilters.map(filters => filters === undefined || filters.normalize().and.length === 0)
preset.layerToAddTo.appliedFilters.map(filters => filters === undefined || filters.length === 0)
)

View file

@ -258,6 +258,7 @@
"wayHandling": 1,
"filter": [
{
"id": "wheelchair",
"options": [
{
"question": {
@ -275,6 +276,7 @@
]
},
{
"id": "shelter",
"options": [
{
"question": {

View file

@ -170,6 +170,7 @@
],
"filter": [
{
"id": "opened-now",
"options": [
{
"question": {

View file

@ -2627,6 +2627,7 @@
"wayHandling": 1,
"filter": [
{
"id": "vehicle-type",
"options": [
{
"question": {
@ -2656,6 +2657,7 @@
]
},
{
"id": "working",
"options": [
{
"question": {
@ -2671,6 +2673,7 @@
]
},
{
"id": "connection_type",
"options": [
{
"question": {

View file

@ -648,6 +648,7 @@
"wayHandling": 1,
"filter": [
{
"id": "vehicle-type",
"options": [
{
"question": {
@ -677,6 +678,7 @@
]
},
{
"id": "working",
"options": [
{
"question": {

View file

@ -242,6 +242,7 @@ function run(file, protojson) {
})
proto["filter"].push({
id:"connection_type",
options: filterOptions
})

View file

@ -560,6 +560,7 @@
],
"filter": [
{
"id": "opened-now",
"options": [
{
"question": {
@ -571,6 +572,7 @@
]
},
{
"id": "vegetarian",
"options": [
{
"question": {
@ -589,6 +591,7 @@
]
},
{
"id": "vegan",
"options": [
{
"question": {
@ -605,6 +608,7 @@
]
},
{
"id": "halal",
"options": [
{
"question": {

View file

@ -423,6 +423,7 @@
],
"filter": [
{
"id": "access",
"options": [
{
"question": {
@ -433,6 +434,7 @@
]
},
{
"id": "dogs",
"options": [
{
"question": {

View file

@ -444,6 +444,7 @@
},
"filter": [
{
"id": "kid-books",
"options": [
{
"question": "Kinderboeken aanwezig?",
@ -452,6 +453,7 @@
]
},
{
"id": "adult-books",
"options": [
{
"question": "Boeken voor volwassenen aanwezig?",
@ -460,6 +462,7 @@
]
},
{
"id": "inside",
"options": [
{
"question": "Binnen of buiten",

View file

@ -411,6 +411,7 @@
],
"filter": [
{
"id": "wheelchair",
"options": [
{
"question": {
@ -421,6 +422,7 @@
]
},
{
"id": "changing_table",
"options": [
{
"question": {
@ -431,6 +433,7 @@
]
},
{
"id": "free",
"options": [
{
"question": {

View file

@ -143,6 +143,7 @@
},
"filter": [
{
"id": "name-alt",
"options": [
{
"question": "Name contains 'alt'",
@ -151,6 +152,7 @@
]
},
{
"id": "name-wenslijn",
"options": [
{
"question": "Name contains 'wenslijn'",
@ -159,6 +161,7 @@
]
},
{
"id": "name-omleiding",
"options": [
{
"question": "Name contains 'omleiding'",
@ -167,6 +170,7 @@
]
},
{
"id":"ref-alt",
"options": [
{
"question": "Reference contains 'alt'",
@ -175,6 +179,7 @@
]
},
{
"id": "missing_link",
"options": [
{
"question": "No filter"
@ -194,6 +199,7 @@
]
},
{
"id": "proposed",
"options": [
{
"question": "No filter"

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
viewBox="0 0 87.992996 87.883003"
id="svg12"
sodipodi:docname="housenumber_unknown_small.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
width="87.992996"
height="87.883003">
<metadata
id="metadata18">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs16" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1043"
id="namedview14"
showgrid="false"
inkscape:zoom="7.375"
inkscape:cx="-1.3561062"
inkscape:cy="19.621117"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg12" />
<path
d="m 42.372766,41.559418 h 3.247468 c 0.421762,0 0.76133,0.339541 0.76133,0.761329 v 3.241506 c 0,0.421761 -0.339541,0.761329 -0.76133,0.761329 h -3.247468 c -0.421762,0 -0.76133,-0.339541 -0.76133,-0.761329 v -3.241506 c 0,-0.421761 0.339541,-0.761329 0.76133,-0.761329 z"
style="fill:#495aad;stroke-width:0.0542103;paint-order:normal"
id="path6"
inkscape:connector-curvature="0" />
<path
d="m 42.085614,42.793949 v 2.289464 c 0.381581,0 0.763173,0.381581 0.763173,0.763173 h 2.289463 c 0,-0.381581 0.381581,-0.763173 0.763173,-0.763173 v -2.289464 c -0.381581,0 -0.763173,-0.381581 -0.763173,-0.763172 h -2.289463 c 0,0.38158 -0.381581,0.763172 -0.763173,0.763172 z"
id="path8"
inkscape:connector-curvature="0"
style="fill:none;stroke:#ffffff;stroke-width:0.27187553" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -39,5 +39,13 @@
"https://github.com/streetcomplete/StreetComplete/tree/master/res/graphics",
"https://f-droid.org/packages/de.westnordost.streetcomplete/"
]
},
{
"path": "housenumber_unknown_small.svg",
"license": "CC0",
"authors": [
"Pieter Vander Vennet"
],
"sources": []
}
]

View file

@ -15,11 +15,15 @@
"maintainer": "Pieter Vander Vennet, Rob Nickerson, Russ Garrett",
"icon": "./assets/themes/uk_addresses/housenumber_unknown.svg",
"version": "2021-09-17",
"startLat": -0.08528530407,
"startLon": 51.52103754846,
"startZoom": 18,
"widenFactor": 1.5,
"startLat": -0.08706,
"startLon": 51.52224,
"startZoom": 17,
"widenFactor": 1.01,
"socialImage": "",
"clustering": {
"minNeededFeatures": 25,
"maxZoom": 17
},
"layers": [
{
"id": "to_import",
@ -34,21 +38,21 @@
"minzoom": 12,
"wayHandling": 1,
"icon": {
"render": "./assets/themes/uk_addresses/housenumber_unknown.svg"
},
"iconSize": {
"render": "40,40,center",
"render": "./assets/themes/uk_addresses/housenumber_unknown.svg",
"mappings": [
{
"if": "_embedding_object:id~*",
"then": "15,15,center"
"then": "./assets/themes/uk_addresses/housenumber_unknown_small.svg"
},
{
"if": "_imported=yes",
"then": "8,8,center"
"then": "./assets/themes/uk_addresses/housenumber_unknown_small.svg"
}
]
},
"iconSize": {
"render": "40,40,center"
},
"title": {
"render": "Address to be determined"
},
@ -73,6 +77,22 @@
"_embedding_object:addr:housenumber=JSON.parse(feat.properties._embedding_object)?.['addr:housenumber']",
"_embedding_object:addr:street=JSON.parse(feat.properties._embedding_object)?.['addr:street']",
"_embedding_object:id=JSON.parse(feat.properties._embedding_object)?.id"
],
"filter": [
{
"id": "to_handle",
"options": [
{
"question": "Only show non-matched objects",
"osmTags": {
"and": [
"_imported=",
"_embedding_object:id="
]
}
}
]
}
]
},
{

View file

@ -181,10 +181,10 @@ export default class GeoOperationsSpec extends T {
["bbox bounds test",
() => {
const bbox = BBox.fromTile(16, 32754, 21785)
equal(-0.0714111328125, bbox.minLon)
equal(-0.076904296875, bbox.maxLon)
equal(51.53266860674158, bbox.minLat)
equal(51.5292513551899, bbox.maxLat)
equal(-0.076904296875, bbox.minLon)
equal(-0.0714111328125, bbox.maxLon)
equal(51.5292513551899, bbox.minLat)
equal(51.53266860674158, bbox.maxLat)
}
]
]