Add search for filters
This commit is contained in:
parent
1378c1a779
commit
c94393e825
24 changed files with 405 additions and 254 deletions
|
@ -9,6 +9,7 @@
|
|||
"id": "open_now",
|
||||
"options": [
|
||||
{
|
||||
"emoji": "⏰",
|
||||
"question": {
|
||||
"en": "Open now",
|
||||
"nl": "Nu open",
|
||||
|
@ -268,6 +269,7 @@
|
|||
{
|
||||
"question": {
|
||||
"en": "No preference towards dogs",
|
||||
"nl": "Geen voorkeur voor honden",
|
||||
"de": "Keine Bevorzugung von Hunden",
|
||||
"cs": "Bez preference psů"
|
||||
}
|
||||
|
@ -275,22 +277,27 @@
|
|||
{
|
||||
"question": {
|
||||
"en": "Dogs allowed",
|
||||
"nl": "Honden toegelaten",
|
||||
"de": "Hunde erlaubt",
|
||||
"cs": "Psi povoleny"
|
||||
},
|
||||
"emoji": "🐕",
|
||||
"osmTags": {
|
||||
"or": [
|
||||
"dog=unleashed",
|
||||
"dog=yes"
|
||||
]
|
||||
}
|
||||
},
|
||||
"icon": "./assets/layers/questions/dogs_allowed.svg"
|
||||
},
|
||||
{
|
||||
"question": {
|
||||
"en": "No dogs allowed",
|
||||
"nl": "Geen honden toegelaten",
|
||||
"de": "Keine Hunde erlaubt",
|
||||
"cs": "Psi nejsou povoleni"
|
||||
},
|
||||
"icon": "./assets/layers/questions/no_dogs.svg",
|
||||
"osmTags": "dog=no"
|
||||
}
|
||||
]
|
||||
|
@ -304,6 +311,7 @@
|
|||
"de": "Internetzugang vorhanden",
|
||||
"cs": "Nabízí internet"
|
||||
},
|
||||
"icon": "wifi",
|
||||
"osmTags": {
|
||||
"or": [
|
||||
"internet_access=wlan",
|
||||
|
@ -355,6 +363,7 @@
|
|||
"cs": "Má bezlepkovou nabídku",
|
||||
"de": "Hat glutenfreie Angebote"
|
||||
},
|
||||
"icon": "./assets/layers/questions/glutenfree.svg",
|
||||
"osmTags": {
|
||||
"or": [
|
||||
"diet:gluten_free=yes",
|
||||
|
@ -374,6 +383,7 @@
|
|||
"cs": "Má nabídku bez laktózy",
|
||||
"de": "Hat laktosefreie Angebote"
|
||||
},
|
||||
"icon": "./assets/layers/questions/lactose_free.svg",
|
||||
"osmTags": {
|
||||
"or": [
|
||||
"diet:lactose_free=yes",
|
||||
|
|
|
@ -341,6 +341,7 @@
|
|||
"mappings": [
|
||||
{
|
||||
"if": "cuisine=pizza",
|
||||
"icon": "🍕",
|
||||
"then": {
|
||||
"en": "This is a pizzeria",
|
||||
"nl": "Dit is een pizzeria",
|
||||
|
@ -354,6 +355,7 @@
|
|||
},
|
||||
{
|
||||
"if": "cuisine=friture",
|
||||
"icon": "🍟",
|
||||
"then": {
|
||||
"en": "This is a friture",
|
||||
"nl": "Dit is een frituur",
|
||||
|
@ -365,6 +367,7 @@
|
|||
},
|
||||
{
|
||||
"if": "cuisine=pasta",
|
||||
"icon": "🍝",
|
||||
"then": {
|
||||
"en": "Mainly serves pasta",
|
||||
"nl": "Dit is een pastazaak",
|
||||
|
@ -378,6 +381,7 @@
|
|||
},
|
||||
{
|
||||
"if": "cuisine=kebab",
|
||||
"icon": "🥙",
|
||||
"then": {
|
||||
"en": "This is kebab shop",
|
||||
"nl": "Dit is een kebabzaak",
|
||||
|
@ -391,6 +395,7 @@
|
|||
},
|
||||
{
|
||||
"if": "cuisine=sandwich",
|
||||
"icon": "🥪",
|
||||
"then": {
|
||||
"en": "This is a sandwich shop",
|
||||
"nl": "Dit is een broodjeszaak",
|
||||
|
@ -402,6 +407,7 @@
|
|||
},
|
||||
{
|
||||
"if": "cuisine=burger",
|
||||
"icon": "🍔",
|
||||
"then": {
|
||||
"en": "Burgers are served here",
|
||||
"nl": "Dit is een hamburgerrestaurant",
|
||||
|
@ -415,6 +421,7 @@
|
|||
},
|
||||
{
|
||||
"if": "cuisine=sushi",
|
||||
"icon": "\uD83C\uDF63",
|
||||
"then": {
|
||||
"en": "Sushi is served here",
|
||||
"nl": "Dit is een sushirestaurant",
|
||||
|
@ -427,6 +434,7 @@
|
|||
},
|
||||
{
|
||||
"if": "cuisine=coffee",
|
||||
"icon": "☕",
|
||||
"then": {
|
||||
"en": "Coffee is served here",
|
||||
"nl": "Dit is een koffiezaak",
|
||||
|
@ -439,6 +447,7 @@
|
|||
},
|
||||
{
|
||||
"if": "cuisine=italian",
|
||||
"icon": "🇮🇹",
|
||||
"then": {
|
||||
"en": "This is an Italian restaurant (which serves more than pasta and pizza)",
|
||||
"nl": "Dit is een Italiaans restaurant (dat meer dan enkel pasta of pizza verkoopt)",
|
||||
|
@ -451,6 +460,7 @@
|
|||
},
|
||||
{
|
||||
"if": "cuisine=french",
|
||||
"icon": "🇫🇷",
|
||||
"then": {
|
||||
"en": "French dishes are served here",
|
||||
"nl": "Dit is een Frans restaurant",
|
||||
|
@ -463,6 +473,7 @@
|
|||
},
|
||||
{
|
||||
"if": "cuisine=chinese",
|
||||
"icon":"🇨🇳",
|
||||
"then": {
|
||||
"en": "Chinese dishes are served here",
|
||||
"nl": "Dit is een Chinees restaurant",
|
||||
|
@ -475,6 +486,7 @@
|
|||
},
|
||||
{
|
||||
"if": "cuisine=greek",
|
||||
"icon": "🇬🇷",
|
||||
"then": {
|
||||
"en": "Greek dishes are served here",
|
||||
"nl": "Dit is een Grieks restaurant",
|
||||
|
@ -487,6 +499,7 @@
|
|||
},
|
||||
{
|
||||
"if": "cuisine=indian",
|
||||
"icon": "🇮🇳",
|
||||
"then": {
|
||||
"en": "Indian dishes are served here",
|
||||
"nl": "Dit is een Indisch restaurant",
|
||||
|
@ -499,6 +512,7 @@
|
|||
},
|
||||
{
|
||||
"if": "cuisine=turkish",
|
||||
"icon": "🇹🇷",
|
||||
"then": {
|
||||
"en": "Turkish dishes are served here",
|
||||
"nl": "Dit is een Turks restaurant (dat meer dan enkel kebab verkoopt)",
|
||||
|
@ -511,6 +525,7 @@
|
|||
},
|
||||
{
|
||||
"if": "cuisine=thai",
|
||||
"icon": "🇹🇭",
|
||||
"then": {
|
||||
"en": "Thai dishes are served here",
|
||||
"nl": "Dit is een Thaïs restaurant",
|
||||
|
@ -519,9 +534,42 @@
|
|||
"ca": "Aquí es serveixen plats tailandesos",
|
||||
"cs": "Podávají se zde thajské pokrmy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "cuisine=mexican ",
|
||||
"icon": "\uD83D\uDC14",
|
||||
"then": {
|
||||
"en": "Mexican dishes are served here",
|
||||
"nl": "Dit is een mexicaans restaurant"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "cuisine=japanese ",
|
||||
"icon": "🇯🇵",
|
||||
"then": {
|
||||
"en": "Japanese dishes are served here",
|
||||
"nl": "Dit is een japans restaurant"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "cuisine=chicken ",
|
||||
"icon": "\uD83D\uDC14",
|
||||
"then": {
|
||||
"en": "Chicken based dishes are served here",
|
||||
"nl": "Dit is een kiprestaurant"
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": "cuisine=seafood ",
|
||||
"icon": "\uD83D\uDC1F",
|
||||
"then": {
|
||||
"en": "Seafood dishes are served here",
|
||||
"nl": "Dit is een vis- en zeerestaurant"
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": "Cuisine"
|
||||
"id": "Cuisine",
|
||||
"filter": true
|
||||
},
|
||||
{
|
||||
"id": "show-menu-image",
|
||||
|
@ -1291,6 +1339,7 @@
|
|||
"es": "Tiene menú vegetariano",
|
||||
"fr": "A un menu végétarien"
|
||||
},
|
||||
"icon": "./assets/layers/food/Vegetarian-mark.svg",
|
||||
"osmTags": {
|
||||
"or": [
|
||||
"diet:vegetarian=yes",
|
||||
|
|
|
@ -569,7 +569,7 @@
|
|||
},
|
||||
{
|
||||
"if": "dog=no",
|
||||
"icon": "\uD83D\uDC15 ⃠",
|
||||
"icon": "./assets/layers/questions/no_dogs.svg",
|
||||
"then": {
|
||||
"en": "Dogs are <b>not</b> allowed",
|
||||
"nl": "honden zijn <b>niet</b> toegelaten",
|
||||
|
|
|
@ -3,14 +3,17 @@
|
|||
"title": {
|
||||
"en": "Changes made with MapComplete"
|
||||
},
|
||||
"description": {
|
||||
"en": "This maps shows all the changes made with MapComplete"
|
||||
},
|
||||
"shortDescription": {
|
||||
"en": "Shows changes made by MapComplete"
|
||||
},
|
||||
"description": {
|
||||
"en": "This maps shows all the changes made with MapComplete"
|
||||
},
|
||||
"icon": "./assets/svg/logo.svg",
|
||||
"hideFromOverview": true,
|
||||
"startLat": 0,
|
||||
"startLon": 0,
|
||||
"startZoom": 1,
|
||||
"layers": [
|
||||
{
|
||||
"id": "mapcomplete-changes",
|
||||
|
|
|
@ -3883,6 +3883,19 @@
|
|||
"question": "Gratis toegankelijk"
|
||||
}
|
||||
}
|
||||
},
|
||||
"10": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Geen voorkeur voor honden"
|
||||
},
|
||||
"1": {
|
||||
"question": "Honden toegelaten"
|
||||
},
|
||||
"2": {
|
||||
"question": "Geen honden toegelaten"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4724,6 +4724,17 @@ h2.group {
|
|||
background-color: #58cd2722;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
border-radius: 999rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
border: 1px solid var(--subtle-detail-color-light-contrast);
|
||||
background-color: var(--low-interaction-background);
|
||||
}
|
||||
|
||||
.alert {
|
||||
/* The class to convey important information, e.g. 'invalid', 'something went wrong', 'warning: testmode', ... */
|
||||
background-color: var(--alert-color);
|
||||
|
|
|
@ -13,11 +13,17 @@ export default class FilterSearch implements GeocodingProvider {
|
|||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
|
||||
return []
|
||||
return this.searchDirectly(query)
|
||||
}
|
||||
|
||||
private searchDirectly(query: string): SearchResult[] {
|
||||
const possibleFilters: SearchResult[] = []
|
||||
if (query.length === 0) {
|
||||
return []
|
||||
}
|
||||
if(!Utils.isEmoji(query)){
|
||||
query = Utils.simplifyStringForSearch(query)
|
||||
}
|
||||
for (const layer of this._state.layout.layers) {
|
||||
if (!Array.isArray(layer.filters)) {
|
||||
continue
|
||||
|
@ -26,16 +32,16 @@ export default class FilterSearch implements GeocodingProvider {
|
|||
for (let i = 0; i < filter.options.length; i++) {
|
||||
const option = filter.options[i]
|
||||
if (option === undefined) {
|
||||
console.log("No options for", filter)
|
||||
continue
|
||||
}
|
||||
const terms = [option.question.txt,
|
||||
...(option.searchTerms?.[Locale.language.data] ?? option.searchTerms?.["en"] ?? [])].flatMap(term => term.split(" "))
|
||||
let terms = ([option.question.txt,
|
||||
...(option.searchTerms?.[Locale.language.data] ?? option.searchTerms?.["en"] ?? [])]
|
||||
.flatMap(term => [term, ...term?.split(" ")]))
|
||||
terms = terms.map(t => Utils.simplifyStringForSearch(t))
|
||||
terms.push(option.emoji)
|
||||
Utils.NoNullInplace(terms)
|
||||
const levehnsteinD = Math.min(...
|
||||
terms.map(entry => {
|
||||
const simplified = Utils.simplifyStringForSearch(entry)
|
||||
return Utils.levenshteinDistance(query, simplified.slice(0, query.length))
|
||||
}))
|
||||
terms.map(entry => Utils.levenshteinDistance(query, entry.slice(0, query.length))))
|
||||
if (levehnsteinD / query.length > 0.25) {
|
||||
continue
|
||||
}
|
||||
|
@ -51,8 +57,10 @@ export default class FilterSearch implements GeocodingProvider {
|
|||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
|
||||
if (Utils.isEmoji(query)) {
|
||||
return new ImmutableStore(this.searchDirectly(query))
|
||||
}
|
||||
query = Utils.simplifyStringForSearch(query)
|
||||
|
||||
return new ImmutableStore(this.searchDirectly(query))
|
||||
}
|
||||
|
||||
|
|
|
@ -48,7 +48,6 @@ export class RecentSearch {
|
|||
}
|
||||
results.push(simple)
|
||||
}
|
||||
console.log("Setting", results)
|
||||
prefs.setData(JSON.stringify(results))
|
||||
|
||||
})
|
||||
|
@ -59,7 +58,6 @@ export class RecentSearch {
|
|||
if (!osm_id) {
|
||||
return
|
||||
}
|
||||
console.log("Selected element is", selected)
|
||||
if (["node", "way", "relation"].indexOf(osm_type) < 0) {
|
||||
return
|
||||
}
|
||||
|
@ -86,7 +84,6 @@ export class RecentSearch {
|
|||
seenIds.add(id)
|
||||
}
|
||||
}
|
||||
console.log(">>>", arr)
|
||||
this._seenThisSession.set(arr)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,10 +10,7 @@ import {
|
|||
SetDefault,
|
||||
} from "./Conversion"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import {
|
||||
MinimalTagRenderingConfigJson,
|
||||
TagRenderingConfigJson,
|
||||
} from "../Json/TagRenderingConfigJson"
|
||||
import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||
import { Utils } from "../../../Utils"
|
||||
import RewritableConfigJson from "../Json/RewritableConfigJson"
|
||||
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
|
||||
|
@ -21,7 +18,7 @@ import Translations from "../../../UI/i18n/Translations"
|
|||
import { Translation } from "../../../UI/i18n/Translation"
|
||||
import tagrenderingconfigmeta from "../../../../src/assets/schemas/tagrenderingconfigmeta.json"
|
||||
import { AddContextToTranslations } from "./AddContextToTranslations"
|
||||
import FilterConfigJson from "../Json/FilterConfigJson"
|
||||
import FilterConfigJson, { FilterConfigOptionJson } from "../Json/FilterConfigJson"
|
||||
import predifined_filters from "../../../../assets/layers/filters/filters.json"
|
||||
import { TagConfigJson } from "../Json/TagConfigJson"
|
||||
import PointRenderingConfigJson, { IconConfigJson } from "../Json/PointRenderingConfigJson"
|
||||
|
@ -33,7 +30,7 @@ import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
|
|||
import { ConversionContext } from "./ConversionContext"
|
||||
import { ExpandRewrite } from "./ExpandRewrite"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
import { Translatable } from "../Json/Translatable"
|
||||
import FilterConfig, { FilterConfigOption } from "../FilterConfig"
|
||||
|
||||
class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
||||
private static readonly predefinedFilters = ExpandFilter.load_filters()
|
||||
|
@ -41,9 +38,11 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
|
||||
constructor(state: DesugaringContext) {
|
||||
super(
|
||||
"Expands filters: replaces a shorthand by the value found in 'filters.json'. If the string is formatted 'layername.filtername, it will be looked up into that layer instead. If a tagRendering sets 'filter', this filter will also be included",
|
||||
["Expands filters: replaces a shorthand by the value found in 'filters.json'.",
|
||||
"If the string is formatted 'layername.filtername, it will be looked up into that layer instead. If a tagRendering sets 'filter', this filter will also be included",
|
||||
""].join(" "),
|
||||
["filter"],
|
||||
"ExpandFilter"
|
||||
"ExpandFilter",
|
||||
)
|
||||
this._state = state
|
||||
}
|
||||
|
@ -56,6 +55,38 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
return filters
|
||||
}
|
||||
|
||||
private static buildFilterFromTagRendering(tr: TagRenderingConfigJson, context: ConversionContext): FilterConfigJson {
|
||||
if (!(tr.mappings?.length >= 1)) {
|
||||
context.err(
|
||||
"Found a matching tagRendering to base a filter on, but this tagRendering does not contain any mappings",
|
||||
)
|
||||
}
|
||||
const options = (<QuestionableTagRenderingConfigJson>tr).mappings.map((mapping) => {
|
||||
let icon : string= mapping.icon?.["path"] ?? mapping.icon
|
||||
let emoji: string = undefined
|
||||
if(Utils.isEmoji(icon)){
|
||||
emoji = icon
|
||||
icon = undefined
|
||||
}
|
||||
return (<FilterConfigOptionJson>{
|
||||
question: mapping.then,
|
||||
osmTags: mapping.if,
|
||||
searchTerms: mapping.searchTerms,
|
||||
icon, emoji
|
||||
})
|
||||
})
|
||||
// Add default option
|
||||
options.unshift({
|
||||
question: tr["question"] ?? Translations.t.general.filterPanel.allTypes,
|
||||
osmTags: undefined,
|
||||
searchTerms: undefined,
|
||||
})
|
||||
return ({
|
||||
id: tr["id"],
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
|
||||
if (json?.filter === undefined || json?.filter === null) {
|
||||
return json // Nothing to change here
|
||||
|
@ -68,6 +99,16 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
const newFilters: FilterConfigJson[] = []
|
||||
const filters = <(FilterConfigJson | string)[]>json.filter
|
||||
|
||||
function filterExists(filterName: string): boolean{
|
||||
return filters.some((existing) => {
|
||||
const id: string = existing["id"] ?? existing
|
||||
return (
|
||||
filterName === id ||
|
||||
(filterName.startsWith("filters.") && filterName.endsWith("." + id))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks all tagRendering. If a tagrendering has 'filter' set, add this filter to the layer config
|
||||
*/
|
||||
|
@ -76,18 +117,18 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
if (!tagRendering?.filter) {
|
||||
continue
|
||||
}
|
||||
if (tagRendering.filter === true) {
|
||||
if(filterExists(tagRendering["id"])){
|
||||
continue
|
||||
}
|
||||
filters.push(ExpandFilter.buildFilterFromTagRendering(tagRendering, context.enters("tagRenderings", i, "filter")))
|
||||
continue
|
||||
}
|
||||
for (const filterName of tagRendering.filter ?? []) {
|
||||
if (typeof filterName !== "string") {
|
||||
context.enters("tagRenderings", i, "filter").err("Not a string: " + filterName)
|
||||
}
|
||||
const exists = filters.some((existing) => {
|
||||
const id: string = existing["id"] ?? existing
|
||||
return (
|
||||
filterName === id ||
|
||||
(filterName.startsWith("filters.") && filterName.endsWith("." + id))
|
||||
)
|
||||
})
|
||||
if (exists) {
|
||||
if (filterExists(filterName)) {
|
||||
continue
|
||||
}
|
||||
if (!filterName) {
|
||||
|
@ -99,7 +140,7 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create filters based on builtin filters
|
||||
* Create filters based on builtin filters or create them based on the tagRendering
|
||||
*/
|
||||
for (let i = 0; i < filters.length; i++) {
|
||||
const filter = filters[i]
|
||||
|
@ -115,28 +156,8 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
json.tagRenderings.find((tr) => !!tr && tr["id"] === filter)
|
||||
)
|
||||
if (matchingTr) {
|
||||
if (!(matchingTr.mappings?.length >= 1)) {
|
||||
context
|
||||
.enters("filter", i)
|
||||
.err(
|
||||
"Found a matching tagRendering to base a filter on, but this tagRendering does not contain any mappings"
|
||||
)
|
||||
}
|
||||
const options = (<QuestionableTagRenderingConfigJson> matchingTr).mappings.map((mapping) => ({
|
||||
question: mapping.then,
|
||||
osmTags: mapping.if,
|
||||
searchTerms: mapping.searchTerms
|
||||
|
||||
}))
|
||||
options.unshift({
|
||||
question: matchingTr["question"] ?? Translations.t.general.filterPanel.allTypes,
|
||||
osmTags: undefined,
|
||||
searchTerms: undefined
|
||||
})
|
||||
newFilters.push({
|
||||
id: filter,
|
||||
options,
|
||||
})
|
||||
const filter = ExpandFilter.buildFilterFromTagRendering(matchingTr, context.enters("filter", i))
|
||||
newFilters.push(filter)
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -145,7 +166,7 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
const split = filter.split(".")
|
||||
if (split.length > 2) {
|
||||
context.err(
|
||||
"invalid filter name: " + filter + ", expected `layername.filterid`"
|
||||
"invalid filter name: " + filter + ", expected `layername.filterid`",
|
||||
)
|
||||
}
|
||||
const layer = this._state.sharedLayers.get(split[0])
|
||||
|
@ -154,7 +175,7 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
}
|
||||
const expectedId = split[1]
|
||||
const expandedFilter = (<(FilterConfigJson | string)[]>layer.filter).find(
|
||||
(f) => typeof f !== "string" && f.id === expectedId
|
||||
(f) => typeof f !== "string" && f.id === expectedId,
|
||||
)
|
||||
if (expandedFilter === undefined) {
|
||||
context.err("Did not find filter with name " + filter)
|
||||
|
@ -172,15 +193,15 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
const suggestions = Utils.sortedByLevenshteinDistance(
|
||||
filter,
|
||||
Array.from(ExpandFilter.predefinedFilters.keys()),
|
||||
(t) => t
|
||||
(t) => t,
|
||||
)
|
||||
context
|
||||
.enter(filter)
|
||||
.err(
|
||||
"While searching for predefined filter " +
|
||||
filter +
|
||||
": this filter is not found. Perhaps you meant one of: " +
|
||||
suggestions
|
||||
filter +
|
||||
": this filter is not found. Perhaps you meant one of: " +
|
||||
suggestions,
|
||||
)
|
||||
}
|
||||
newFilters.push(found)
|
||||
|
@ -193,9 +214,9 @@ class ExpandTagRendering extends Conversion<
|
|||
| string
|
||||
| TagRenderingConfigJson
|
||||
| {
|
||||
builtin: string | string[]
|
||||
override: any
|
||||
},
|
||||
builtin: string | string[]
|
||||
override: any
|
||||
},
|
||||
TagRenderingConfigJson[]
|
||||
> {
|
||||
private readonly _state: DesugaringContext
|
||||
|
@ -217,12 +238,12 @@ class ExpandTagRendering extends Conversion<
|
|||
noHardcodedStrings?: false | boolean
|
||||
// If set, a question will be added to the 'sharedTagRenderings'. Should only be used for 'questions.json'
|
||||
addToContext?: false | boolean
|
||||
}
|
||||
},
|
||||
) {
|
||||
super(
|
||||
"Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question and reusing the builtins",
|
||||
[],
|
||||
"ExpandTagRendering"
|
||||
"ExpandTagRendering",
|
||||
)
|
||||
this._state = state
|
||||
this._self = self
|
||||
|
@ -242,7 +263,7 @@ class ExpandTagRendering extends Conversion<
|
|||
|
||||
public convert(
|
||||
spec: string | any,
|
||||
ctx: ConversionContext
|
||||
ctx: ConversionContext,
|
||||
): QuestionableTagRenderingConfigJson[] {
|
||||
const trs = this.convertOnce(spec, ctx)
|
||||
|
||||
|
@ -355,8 +376,8 @@ class ExpandTagRendering extends Conversion<
|
|||
found,
|
||||
ConversionContext.construct(
|
||||
[layer.id, "tagRenderings", found["id"]],
|
||||
["AddContextToTranslations"]
|
||||
)
|
||||
["AddContextToTranslations"],
|
||||
),
|
||||
)
|
||||
matchingTrs[i] = found
|
||||
}
|
||||
|
@ -384,17 +405,17 @@ class ExpandTagRendering extends Conversion<
|
|||
ctx.warn(
|
||||
`A literal rendering was detected: ${tr}
|
||||
Did you perhaps forgot to add a layer name as 'layername.${tr}'? ` +
|
||||
Array.from(state.sharedLayers.keys()).join(", ")
|
||||
Array.from(state.sharedLayers.keys()).join(", "),
|
||||
)
|
||||
}
|
||||
|
||||
if (this._options?.noHardcodedStrings && this._state?.sharedLayers?.size > 0) {
|
||||
ctx.err(
|
||||
"Detected an invocation to a builtin tagRendering, but this tagrendering was not found: " +
|
||||
tr +
|
||||
" \n Did you perhaps forget to add the layer as prefix, such as `icons." +
|
||||
tr +
|
||||
"`? "
|
||||
tr +
|
||||
" \n Did you perhaps forget to add the layer as prefix, such as `icons." +
|
||||
tr +
|
||||
"`? ",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -429,9 +450,9 @@ class ExpandTagRendering extends Conversion<
|
|||
}
|
||||
ctx.err(
|
||||
"An object calling a builtin can only have keys `builtin` or `override`, but a key with name `" +
|
||||
key +
|
||||
"` was found. This won't be picked up! The full object is: " +
|
||||
JSON.stringify(tr)
|
||||
key +
|
||||
"` was found. This won't be picked up! The full object is: " +
|
||||
JSON.stringify(tr),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -450,39 +471,39 @@ class ExpandTagRendering extends Conversion<
|
|||
const candidates = Utils.sortedByLevenshteinDistance(
|
||||
layerName,
|
||||
Array.from(state.sharedLayers.keys()),
|
||||
(s) => s
|
||||
(s) => s,
|
||||
)
|
||||
if (state.sharedLayers.size === 0) {
|
||||
ctx.warn(
|
||||
"BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " +
|
||||
name +
|
||||
": layer " +
|
||||
layerName +
|
||||
" not found for now, but ignoring as this is a bootstrapping run. "
|
||||
name +
|
||||
": layer " +
|
||||
layerName +
|
||||
" not found for now, but ignoring as this is a bootstrapping run. ",
|
||||
)
|
||||
} else {
|
||||
ctx.err(
|
||||
": While reusing tagrendering: " +
|
||||
name +
|
||||
": layer " +
|
||||
layerName +
|
||||
" not found. Maybe you meant one of " +
|
||||
candidates.slice(0, 3).join(", ")
|
||||
name +
|
||||
": layer " +
|
||||
layerName +
|
||||
" not found. Maybe you meant one of " +
|
||||
candidates.slice(0, 3).join(", "),
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
candidates = Utils.NoNull(layer.tagRenderings.map((tr) => tr["id"])).map(
|
||||
(id) => layerName + "." + id
|
||||
(id) => layerName + "." + id,
|
||||
)
|
||||
}
|
||||
candidates = Utils.sortedByLevenshteinDistance(name, candidates, (i) => i)
|
||||
ctx.err(
|
||||
"The tagRendering with identifier " +
|
||||
name +
|
||||
" was not found.\n\tDid you mean one of " +
|
||||
candidates.join(", ") +
|
||||
"?\n(Hint: did you add a new label and are you trying to use this label at the same time? Run 'reset:layeroverview' first"
|
||||
name +
|
||||
" was not found.\n\tDid you mean one of " +
|
||||
candidates.join(", ") +
|
||||
"?\n(Hint: did you add a new label and are you trying to use this label at the same time? Run 'reset:layeroverview' first",
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
@ -507,13 +528,13 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
|
|||
super(
|
||||
"If no 'inline' is set on the freeform key, it will be automatically added. If no special renderings are used, it'll be set to true",
|
||||
["freeform.inline"],
|
||||
"DetectInline"
|
||||
"DetectInline",
|
||||
)
|
||||
}
|
||||
|
||||
convert(
|
||||
json: QuestionableTagRenderingConfigJson,
|
||||
context: ConversionContext
|
||||
context: ConversionContext,
|
||||
): QuestionableTagRenderingConfigJson {
|
||||
if (json.freeform === undefined) {
|
||||
return json
|
||||
|
@ -536,7 +557,7 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
|
|||
if (json.freeform.inline === true) {
|
||||
context.err(
|
||||
"'inline' is set, but the rendering contains a special visualisation...\n " +
|
||||
spec[key]
|
||||
spec[key],
|
||||
)
|
||||
}
|
||||
json = JSON.parse(JSON.stringify(json))
|
||||
|
@ -559,7 +580,7 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
|
|||
super(
|
||||
"Adds a 'questions'-object if no question element is added yet",
|
||||
["tagRenderings"],
|
||||
"AddQuestionBox"
|
||||
"AddQuestionBox",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -583,18 +604,18 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
|
|||
json.tagRenderings = [...json.tagRenderings]
|
||||
const allSpecials: Exclude<RenderingSpecification, string>[] = <any>(
|
||||
ValidationUtils.getAllSpecialVisualisations(
|
||||
<QuestionableTagRenderingConfigJson[]>json.tagRenderings
|
||||
<QuestionableTagRenderingConfigJson[]>json.tagRenderings,
|
||||
).filter((spec) => typeof spec !== "string")
|
||||
)
|
||||
|
||||
const questionSpecials = allSpecials.filter((sp) => sp.func.funcName === "questions")
|
||||
const noLabels = questionSpecials.filter(
|
||||
(sp) => sp.args.length === 0 || sp.args[0].trim() === ""
|
||||
(sp) => sp.args.length === 0 || sp.args[0].trim() === "",
|
||||
)
|
||||
|
||||
if (noLabels.length > 1) {
|
||||
context.err(
|
||||
"Multiple 'questions'-visualisations found which would show _all_ questions. Don't do this"
|
||||
"Multiple 'questions'-visualisations found which would show _all_ questions. Don't do this",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -602,9 +623,9 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
|
|||
const allLabels = new Set(
|
||||
[].concat(
|
||||
...json.tagRenderings.map(
|
||||
(tr) => (<QuestionableTagRenderingConfigJson>tr).labels ?? []
|
||||
)
|
||||
)
|
||||
(tr) => (<QuestionableTagRenderingConfigJson>tr).labels ?? [],
|
||||
),
|
||||
),
|
||||
)
|
||||
const seen: Set<string> = new Set()
|
||||
for (const questionSpecial of questionSpecials) {
|
||||
|
@ -622,20 +643,20 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
|
|||
if (blacklisted?.length > 0 && used?.length > 0) {
|
||||
context.err(
|
||||
"The {questions()}-special rendering only supports either a blacklist OR a whitelist, but not both." +
|
||||
"\n Whitelisted: " +
|
||||
used.join(", ") +
|
||||
"\n Blacklisted: " +
|
||||
blacklisted.join(", ")
|
||||
"\n Whitelisted: " +
|
||||
used.join(", ") +
|
||||
"\n Blacklisted: " +
|
||||
blacklisted.join(", "),
|
||||
)
|
||||
}
|
||||
for (const usedLabel of used) {
|
||||
if (!allLabels.has(usedLabel)) {
|
||||
context.err(
|
||||
"This layers specifies a special question element for label `" +
|
||||
usedLabel +
|
||||
"`, but this label doesn't exist.\n" +
|
||||
" Available labels are " +
|
||||
Array.from(allLabels).join(", ")
|
||||
usedLabel +
|
||||
"`, but this label doesn't exist.\n" +
|
||||
" Available labels are " +
|
||||
Array.from(allLabels).join(", "),
|
||||
)
|
||||
}
|
||||
seen.add(usedLabel)
|
||||
|
@ -668,7 +689,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
|||
super(
|
||||
"Add some editing elements, such as the delete button or the move button if they are configured. These used to be handled by the feature info box, but this has been replaced by special visualisation elements",
|
||||
[],
|
||||
"AddEditingElements"
|
||||
"AddEditingElements",
|
||||
)
|
||||
this._desugaring = desugaring
|
||||
this.builtinQuestions = Array.from(this._desugaring.tagRenderings?.values() ?? [])
|
||||
|
@ -698,13 +719,13 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
|||
json.tagRenderings = [...(json.tagRenderings ?? [])]
|
||||
const allIds = new Set<string>(json.tagRenderings.map((tr) => tr["id"]))
|
||||
const specialVisualisations = ValidationUtils.getAllSpecialVisualisations(
|
||||
<any>json.tagRenderings
|
||||
<any>json.tagRenderings,
|
||||
)
|
||||
|
||||
const usedSpecialFunctions = new Set(
|
||||
specialVisualisations.map((sv) =>
|
||||
typeof sv === "string" ? undefined : sv.func.funcName
|
||||
)
|
||||
typeof sv === "string" ? undefined : sv.func.funcName,
|
||||
),
|
||||
)
|
||||
|
||||
/***** ADD TO TOP ****/
|
||||
|
@ -772,7 +793,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
super(
|
||||
"Converts a 'special' translation into a regular translation which uses parameters",
|
||||
["special"],
|
||||
"RewriteSpecial"
|
||||
"RewriteSpecial",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -863,12 +884,12 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
private static convertIfNeeded(
|
||||
input:
|
||||
| (object & {
|
||||
special: {
|
||||
type: string
|
||||
}
|
||||
})
|
||||
special: {
|
||||
type: string
|
||||
}
|
||||
})
|
||||
| any,
|
||||
context: ConversionContext
|
||||
context: ConversionContext,
|
||||
): any {
|
||||
const special = input["special"]
|
||||
if (special === undefined) {
|
||||
|
@ -878,7 +899,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
const type = special["type"]
|
||||
if (type === undefined) {
|
||||
context.err(
|
||||
"A 'special'-block should define 'type' to indicate which visualisation should be used"
|
||||
"A 'special'-block should define 'type' to indicate which visualisation should be used",
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
@ -888,10 +909,10 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
const options = Utils.sortedByLevenshteinDistance(
|
||||
type,
|
||||
SpecialVisualizations.specialVisualizations,
|
||||
(sp) => sp.funcName
|
||||
(sp) => sp.funcName,
|
||||
)
|
||||
context.err(
|
||||
`Special visualisation '${type}' not found. Did you perhaps mean ${options[0].funcName}, ${options[1].funcName} or ${options[2].funcName}?\n\tFor all known special visualisations, please see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialRenderings.md`
|
||||
`Special visualisation '${type}' not found. Did you perhaps mean ${options[0].funcName}, ${options[1].funcName} or ${options[2].funcName}?\n\tFor all known special visualisations, please see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialRenderings.md`,
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
@ -912,7 +933,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
const byDistance = Utils.sortedByLevenshteinDistance(
|
||||
wrongArg,
|
||||
argNamesList,
|
||||
(x) => x
|
||||
(x) => x,
|
||||
)
|
||||
return `Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${
|
||||
byDistance[0]
|
||||
|
@ -931,8 +952,8 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
`Obligated parameter '${arg.name}' in special rendering of type ${
|
||||
vis.funcName
|
||||
} not found.\n The full special rendering specification is: '${JSON.stringify(
|
||||
input
|
||||
)}'\n ${arg.name}: ${arg.doc}`
|
||||
input,
|
||||
)}'\n ${arg.name}: ${arg.doc}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1034,7 +1055,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
continue
|
||||
}
|
||||
Utils.WalkPath(path.path, json, (leaf, travelled) =>
|
||||
RewriteSpecial.convertIfNeeded(leaf, context.enter(travelled))
|
||||
RewriteSpecial.convertIfNeeded(leaf, context.enter(travelled)),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1068,7 +1089,7 @@ class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
|
|||
} = badgesJson[i]
|
||||
const expanded = this._expand.convert(
|
||||
<QuestionableTagRenderingConfigJson>iconBadge.then,
|
||||
context.enters("iconBadges", i)
|
||||
context.enters("iconBadges", i),
|
||||
)
|
||||
if (expanded === undefined) {
|
||||
iconBadges.push(iconBadge)
|
||||
|
@ -1079,7 +1100,7 @@ class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
|
|||
...expanded.map((resolved) => ({
|
||||
if: iconBadge.if,
|
||||
then: <MinimalTagRenderingConfigJson>resolved,
|
||||
}))
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1096,11 +1117,11 @@ class PreparePointRendering extends Fuse<PointRenderingConfigJson> {
|
|||
new Each(
|
||||
new On(
|
||||
"icon",
|
||||
new FirstOf(new ExpandTagRendering(state, layer, { applyCondition: false }))
|
||||
)
|
||||
)
|
||||
new FirstOf(new ExpandTagRendering(state, layer, { applyCondition: false })),
|
||||
),
|
||||
),
|
||||
),
|
||||
new ExpandIconBadges(state, layer)
|
||||
new ExpandIconBadges(state, layer),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1110,7 +1131,7 @@ class SetFullNodeDatabase extends DesugaringStep<LayerConfigJson> {
|
|||
super(
|
||||
"sets the fullNodeDatabase-bit if needed",
|
||||
["fullNodeDatabase"],
|
||||
"SetFullNodeDatabase"
|
||||
"SetFullNodeDatabase",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1139,7 +1160,7 @@ class ExpandMarkerRenderings extends DesugaringStep<IconConfigJson> {
|
|||
super(
|
||||
"Expands tagRenderings in the icons, if needed",
|
||||
["icon", "color"],
|
||||
"ExpandMarkerRenderings"
|
||||
"ExpandMarkerRenderings",
|
||||
)
|
||||
this._layer = layer
|
||||
this._state = state
|
||||
|
@ -1171,7 +1192,7 @@ class AddFavouriteBadges extends DesugaringStep<LayerConfigJson> {
|
|||
super(
|
||||
"Adds the favourite heart to the title and the rendering badges",
|
||||
[],
|
||||
"AddFavouriteBadges"
|
||||
"AddFavouriteBadges",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1196,7 +1217,7 @@ export class AddRatingBadge extends DesugaringStep<LayerConfigJson> {
|
|||
super(
|
||||
"Adds the 'rating'-element if a reviews-element is used in the tagRenderings",
|
||||
["titleIcons"],
|
||||
"AddRatingBadge"
|
||||
"AddRatingBadge",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1215,8 +1236,8 @@ export class AddRatingBadge extends DesugaringStep<LayerConfigJson> {
|
|||
|
||||
const specialVis: Exclude<RenderingSpecification, string>[] = <
|
||||
Exclude<RenderingSpecification, string>[]
|
||||
>ValidationUtils.getAllSpecialVisualisations(<any>json.tagRenderings).filter(
|
||||
(rs) => typeof rs !== "string"
|
||||
>ValidationUtils.getAllSpecialVisualisations(<any>json.tagRenderings).filter(
|
||||
(rs) => typeof rs !== "string",
|
||||
)
|
||||
const funcs = new Set<string>(specialVis.map((rs) => rs.func.funcName))
|
||||
|
||||
|
@ -1232,12 +1253,12 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
|
|||
super(
|
||||
"The auto-icon creates a (non-clickable) title icon based on a tagRendering which has icons",
|
||||
["titleIcons"],
|
||||
"AutoTitleIcon"
|
||||
"AutoTitleIcon",
|
||||
)
|
||||
}
|
||||
|
||||
private createTitleIconsBasedOn(
|
||||
tr: QuestionableTagRenderingConfigJson
|
||||
tr: QuestionableTagRenderingConfigJson,
|
||||
): TagRenderingConfigJson | undefined {
|
||||
const mappings: { if: TagConfigJson; then: string }[] = tr.mappings
|
||||
?.filter((m) => m.icon !== undefined)
|
||||
|
@ -1267,7 +1288,7 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
|
|||
return undefined
|
||||
}
|
||||
return this.createTitleIconsBasedOn(<any>tr)
|
||||
})
|
||||
}),
|
||||
)
|
||||
json.titleIcons.splice(allAutoIndex, 1, ...generated)
|
||||
return json
|
||||
|
@ -1296,8 +1317,8 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
|
|||
.enters("titleIcons", i)
|
||||
.warn(
|
||||
"TagRendering with id " +
|
||||
trId +
|
||||
" does not have any icons, not generating an icon for this"
|
||||
trId +
|
||||
" does not have any icons, not generating an icon for this",
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
@ -1312,7 +1333,7 @@ class DeriveSource extends DesugaringStep<LayerConfigJson> {
|
|||
super(
|
||||
"If no source is given, automatically derives the osmTags by 'or'-ing all the preset tags",
|
||||
["source"],
|
||||
"DeriveSource"
|
||||
"DeriveSource",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1322,7 +1343,7 @@ class DeriveSource extends DesugaringStep<LayerConfigJson> {
|
|||
}
|
||||
if (!json.presets) {
|
||||
context.err(
|
||||
"No source tags given. Trying to derive the source-tags based on the presets, but no presets are given"
|
||||
"No source tags given. Trying to derive the source-tags based on the presets, but no presets are given",
|
||||
)
|
||||
return json
|
||||
}
|
||||
|
@ -1348,7 +1369,7 @@ class DeriveSource extends DesugaringStep<LayerConfigJson> {
|
|||
export class PrepareLayer extends Fuse<LayerConfigJson> {
|
||||
constructor(
|
||||
state: DesugaringContext,
|
||||
options?: { addTagRenderingsToContext?: false | boolean }
|
||||
options?: { addTagRenderingsToContext?: false | boolean },
|
||||
) {
|
||||
super(
|
||||
"Fully prepares and expands a layer for the LayerConfig.",
|
||||
|
@ -1361,8 +1382,8 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
|
|||
new Concat(
|
||||
new ExpandTagRendering(state, layer, {
|
||||
addToContext: options?.addTagRenderingsToContext ?? false,
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
new On("tagRenderings", new Each(new DetectInline())),
|
||||
new AddQuestionBox(),
|
||||
|
@ -1375,11 +1396,11 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
|
|||
new On<PointRenderingConfigJson[], LayerConfigJson>(
|
||||
"pointRendering",
|
||||
(layer) =>
|
||||
new Each(new On("marker", new Each(new ExpandMarkerRenderings(state, layer))))
|
||||
new Each(new On("marker", new Each(new ExpandMarkerRenderings(state, layer)))),
|
||||
),
|
||||
new On<PointRenderingConfigJson[], LayerConfigJson>(
|
||||
"pointRendering",
|
||||
(layer) => new Each(new PreparePointRendering(state, layer))
|
||||
(layer) => new Each(new PreparePointRendering(state, layer)),
|
||||
),
|
||||
new SetDefault("titleIcons", ["icons.defaults"]),
|
||||
new AddRatingBadge(),
|
||||
|
@ -1388,9 +1409,9 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
|
|||
new On(
|
||||
"titleIcons",
|
||||
(layer) =>
|
||||
new Concat(new ExpandTagRendering(state, layer, { noHardcodedStrings: true }))
|
||||
new Concat(new ExpandTagRendering(state, layer, { noHardcodedStrings: true })),
|
||||
),
|
||||
new ExpandFilter(state)
|
||||
new ExpandFilter(state),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ export type FilterConfigOption = {
|
|||
question: Translation
|
||||
searchTerms: Record<string, string[]>
|
||||
icon?: string
|
||||
emoji?: string
|
||||
osmTags: TagsFilter | undefined
|
||||
/* Only set if fields are present. Used to create `osmTags` (which are used to _actually_ filter) when the field is written*/
|
||||
readonly originalTagsSpec: TagConfigJson
|
||||
|
@ -108,7 +109,8 @@ export default class FilterConfig {
|
|||
searchTerms: option.searchTerms,
|
||||
fields,
|
||||
originalTagsSpec: option.osmTags,
|
||||
icon: option.icon
|
||||
icon: option.icon,
|
||||
emoji: option.emoji,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
import { TagConfigJson } from "./TagConfigJson"
|
||||
import { Translatable } from "./Translatable"
|
||||
|
||||
export interface FilterConfigOptionJson {
|
||||
question: Translatable
|
||||
searchTerms?: Record<string, string[]>
|
||||
emoji?: string
|
||||
icon?: string
|
||||
osmTags?: TagConfigJson
|
||||
default?: boolean
|
||||
fields?: {
|
||||
/**
|
||||
* If name is `search`, use "_first_comment~.*{search}.*" as osmTags
|
||||
*/
|
||||
name: string
|
||||
type?: string | "string"
|
||||
}[]
|
||||
}
|
||||
export default interface FilterConfigJson {
|
||||
/**
|
||||
* An id/name for this filter, used to set the URL parameters
|
||||
|
@ -34,20 +48,7 @@ export default interface FilterConfigJson {
|
|||
* }
|
||||
* ```
|
||||
*/
|
||||
options: {
|
||||
question: Translatable
|
||||
searchTerms?: Record<string, string[]>
|
||||
icon?: string
|
||||
osmTags?: TagConfigJson
|
||||
default?: boolean
|
||||
fields?: {
|
||||
/**
|
||||
* If name is `search`, use "_first_comment~.*{search}.*" as osmTags
|
||||
*/
|
||||
name: string
|
||||
type?: string | "string"
|
||||
}[]
|
||||
}[]
|
||||
options: FilterConfigOptionJson[]
|
||||
|
||||
/**
|
||||
* Used for comments or to disable a check
|
||||
|
|
|
@ -226,5 +226,5 @@ export interface TagRenderingConfigJson {
|
|||
/**
|
||||
* This tagRendering can introduce this builtin filter
|
||||
*/
|
||||
filter?: string[]
|
||||
filter?: string[] | true
|
||||
}
|
||||
|
|
|
@ -385,11 +385,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.toCacheSavers = layout.enableCache ? this.initSaveToLocalStorage() : undefined
|
||||
|
||||
this.geosearch = new CombinedSearcher(
|
||||
new CoordinateSearch(),
|
||||
new FilterSearch(this),
|
||||
//new LocalElementSearch(this, 5),
|
||||
//new OpenStreetMapIdSearch(this),
|
||||
// new PhotonSearch(), // new NominatimGeocoding(),
|
||||
new LocalElementSearch(this, 5),
|
||||
new CoordinateSearch(),
|
||||
new OpenStreetMapIdSearch(this),
|
||||
new PhotonSearch(), // new NominatimGeocoding(),
|
||||
this.featureSwitches.featureSwitchBackToThemeOverview.data ? new ThemeSearch(this) : undefined
|
||||
)
|
||||
|
||||
|
@ -652,7 +652,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
},
|
||||
Translations.t.hotkeyDocumentation.openFilterPanel,
|
||||
() => {
|
||||
console.log("S pressed")
|
||||
if (this.featureSwitches.featureSwitchFilter.data) {
|
||||
this.guistate.openFilterView()
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Utils } from "../../Utils"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
|
||||
export let filteredLayer: FilteredLayer
|
||||
export let highlightedLayer: Store<string | undefined> = new ImmutableStore(undefined)
|
||||
|
@ -76,8 +77,8 @@
|
|||
<Dropdown value={getStateFor(filter)}>
|
||||
{#each filter.options as option, i}
|
||||
<option value={i}>
|
||||
{#if Utils.isEmoji(option.icon)}
|
||||
{option.icon}
|
||||
{#if option.emoji}
|
||||
{option.emoji}
|
||||
{/if}
|
||||
<Tr t={option.question} />
|
||||
</option>
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
<script lang="ts">
|
||||
import type { ActiveFilter } from "../../Logic/State/LayerState"
|
||||
import { Badge } from "flowbite-svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import { Badge } from "flowbite-svelte"
|
||||
import FilterOption from "./FilterOption.svelte"
|
||||
import { XMarkIcon } from "@babeard/svelte-heroicons/mini"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
export let activeFilter: ActiveFilter
|
||||
let { control, layer, filter } = activeFilter
|
||||
let option = control.map(c => {
|
||||
if (typeof c === "number") {
|
||||
return filter.options[c]
|
||||
}
|
||||
return filter.options[0]
|
||||
})
|
||||
let option = control.map(c => filter.options[c] ?? filter.options[0])
|
||||
</script>
|
||||
<Badge dismissable large border rounded color="dark" on:close={() =>{ console.log( "dismiss"); return control.setData(undefined) }}>
|
||||
<Tr cls="whitespace-nowrap" t={$option.question} />
|
||||
</Badge>
|
||||
<div class="badge">
|
||||
<FilterOption option={$option} />
|
||||
<button on:click={() => control.setData(undefined)}>
|
||||
|
||||
<XMarkIcon class="w-5 h-5 pl-1" color="gray" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
<script lang="ts">
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Badge } from "flowbite-svelte"
|
||||
import ActiveFilter from "./ActiveFilter.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
let activeFilters = state.layerState.activeFilters
|
||||
import { default as ActiveFilterSvelte } from "./ActiveFilter.svelte"
|
||||
import type { ActiveFilter } from "../../Logic/State/LayerState"
|
||||
|
||||
export let activeFilters: ActiveFilter[]
|
||||
|
||||
function clear() {
|
||||
for (const activeFilter of activeFilters) {
|
||||
activeFilter.control.setData(undefined)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<div class="flex flex-wrap gap-y-1 gap-x-1 button-unstyled">
|
||||
{#if activeFilters.length > 0}
|
||||
<div class="flex flex-wrap gap-y-1 gap-x-1 button-unstyled">
|
||||
{#each activeFilters as activeFilter (activeFilter)}
|
||||
<ActiveFilterSvelte {activeFilter} />
|
||||
{/each}
|
||||
|
||||
{#each $activeFilters as activeFilter (activeFilter)}
|
||||
<ActiveFilter {activeFilter} {state} />
|
||||
{/each}
|
||||
</div>
|
||||
<button class="as-link subtle" on:click={() => clear()}>
|
||||
Clear filters
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
|
10
src/UI/Search/FilterOption.svelte
Normal file
10
src/UI/Search/FilterOption.svelte
Normal file
|
@ -0,0 +1,10 @@
|
|||
<script lang="ts">
|
||||
import type { FilterConfigOption } from "../../Models/ThemeConfig/FilterConfig"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
|
||||
export let option : FilterConfigOption
|
||||
</script>
|
||||
|
||||
<Icon icon={option.icon ?? option.emoji} clss="w-5 h-5" emojiHeight="14px" />
|
||||
<Tr t={option.question} />
|
|
@ -1,14 +1,10 @@
|
|||
<script lang="ts">
|
||||
import type FilterConfig from "../../Models/ThemeConfig/FilterConfig"
|
||||
import type { FilterConfigOption } from "../../Models/ThemeConfig/FilterConfig"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Filter from "../../assets/svg/Filter.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import type { FilterPayload } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { FilterIcon as FilterSolid } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { FilterIcon as FilterOutline } from "@rgossiaux/svelte-heroicons/outline"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import SearchResultUtils from "./SearchResultUtils"
|
||||
|
||||
export let entry: {
|
||||
category: "filter",
|
||||
|
@ -18,38 +14,19 @@
|
|||
export let state: SpecialVisualizationState
|
||||
let dispatch = createEventDispatcher<{ select }>()
|
||||
|
||||
let flayer = state.layerState.filteredLayers.get(layer.id)
|
||||
let filtercontrol = flayer.appliedFilters.get(filter.id)
|
||||
let isActive = filtercontrol.map(c => c === index)
|
||||
|
||||
function apply() {
|
||||
|
||||
for (const [name, otherLayer] of state.layerState.filteredLayers) {
|
||||
if(name === layer.id){
|
||||
otherLayer.isDisplayed.setData(true)
|
||||
continue
|
||||
}
|
||||
otherLayer.isDisplayed.setData(false)
|
||||
}
|
||||
|
||||
if(filtercontrol.data === index){
|
||||
filtercontrol.setData(undefined)
|
||||
}else{
|
||||
filtercontrol.setData(index)
|
||||
}
|
||||
SearchResultUtils.apply(entry.payload, state)
|
||||
dispatch("select")
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
<button on:click={() => apply()}>
|
||||
{#if $isActive}
|
||||
<FilterSolid class="w-8 h-8 shrink-0" />
|
||||
{:else}
|
||||
<FilterOutline class="w-8 h-8 shrink-0" />
|
||||
{/if}
|
||||
<Tr t={option.question} />
|
||||
<div class="subtle">
|
||||
{layer.id}
|
||||
<div class="flex flex-col items-start">
|
||||
|
||||
<div class="flex items-center gap-x-1">
|
||||
<Icon icon={option.icon ?? option.emoji} clss="w-12 h-12 mr-2" emojiHeight="14px" />
|
||||
<Tr cls="whitespace-nowrap" t={option.question} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import GeocodingFeatureSource from "../../Logic/Geocoding/GeocodingFeatureSource"
|
||||
import MoreScreen from "../BigComponents/MoreScreen"
|
||||
import SearchResultUtils from "./SearchResultUtils"
|
||||
|
||||
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
|
||||
export let bounds: UIEventSource<BBox>
|
||||
|
@ -81,7 +82,6 @@
|
|||
return
|
||||
}
|
||||
const result = await searcher.search(searchContentsData, { bbox: bounds.data, limit: 10 })
|
||||
console.log("Results are", result)
|
||||
if (result.length == 0) {
|
||||
feedback = Translations.t.general.search.nothing.txt
|
||||
focusOnSearch()
|
||||
|
@ -91,11 +91,13 @@
|
|||
if (poi.category === "theme") {
|
||||
const theme = <MinimalLayoutInformation>poi.payload
|
||||
const url = MoreScreen.createUrlFor(theme, false)
|
||||
console.log("Found a theme, going to", url)
|
||||
// @ts-ignore
|
||||
window.location = url
|
||||
return
|
||||
}
|
||||
if(poi.category === "filter"){
|
||||
SearchResultUtils.apply(poi.payload, state)
|
||||
}
|
||||
if(poi.category === "filter"){
|
||||
return // Should not happen
|
||||
}
|
||||
|
@ -120,7 +122,6 @@
|
|||
continue
|
||||
}
|
||||
selectedElement?.setData(found)
|
||||
console.log("Found an element that probably matches:", selectedElement?.data)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -146,7 +147,6 @@
|
|||
return Stores.holdDefined(bounds.bindD(bbox => searcher.suggest(search, { bbox, limit: 15 })))
|
||||
}
|
||||
)
|
||||
suggestions.addCallbackAndRun(suggestions => console.log(">>> suggestions are", suggestions))
|
||||
let geocededFeatures= new GeocodingFeatureSource(suggestions.stabilized(250))
|
||||
state.featureProperties.trackFeatureSource(geocededFeatures)
|
||||
|
||||
|
|
|
@ -11,10 +11,9 @@
|
|||
</script>
|
||||
|
||||
{#if entry.category === "theme"}
|
||||
<ThemeResult {entry} />
|
||||
<ThemeResult {entry} on:select />
|
||||
{:else if entry.category === "filter"}
|
||||
<FilterResult {entry} {state} />
|
||||
<FilterResult {entry} {state} on:select />
|
||||
{:else}
|
||||
|
||||
<GeocodeResult {entry} {state} />
|
||||
<GeocodeResult {entry} {state} on:select />
|
||||
{/if}
|
||||
|
|
25
src/UI/Search/SearchResultUtils.ts
Normal file
25
src/UI/Search/SearchResultUtils.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { FilterPayload } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
|
||||
export default class SearchResultUtils {
|
||||
static apply(payload: FilterPayload, state: SpecialVisualizationState) {
|
||||
const { layer, filter, index, option } = payload
|
||||
|
||||
let flayer = state.layerState.filteredLayers.get(layer.id)
|
||||
let filtercontrol = flayer.appliedFilters.get(filter.id)
|
||||
|
||||
for (const [name, otherLayer] of state.layerState.filteredLayers) {
|
||||
if (name === layer.id) {
|
||||
otherLayer.isDisplayed.setData(true)
|
||||
continue
|
||||
}
|
||||
otherLayer.isDisplayed.setData(false)
|
||||
}
|
||||
|
||||
if (filtercontrol.data === index) {
|
||||
filtercontrol.setData(undefined)
|
||||
} else {
|
||||
filtercontrol.setData(index)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,14 +8,16 @@
|
|||
import MoreScreen from "../BigComponents/MoreScreen"
|
||||
import type { GeocodeResult, SearchResult } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import ActiveFilters from "./ActiveFilters.svelte"
|
||||
import Constants from "../../Models/Constants"
|
||||
import type { ActiveFilter } from "../../Logic/State/LayerState"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let results: SearchResult[]
|
||||
export let searchTerm: Store<string>
|
||||
export let isFocused: UIEventSource<boolean>
|
||||
let hasActiveFilters = state.layerState.activeFilters.map(af => af.length > 0)
|
||||
let activeFilters: Store<ActiveFilter[]> = state.layerState.activeFilters.map(fs => fs.filter(f => Constants.priviliged_layers.indexOf(<any>f.layer.id) < 0))
|
||||
|
||||
console.log("Results are", results)
|
||||
let hasActiveFilters = activeFilters.map(afs => afs.length > 0)
|
||||
|
||||
let recentlySeen: Store<GeocodeResult[]> = state.recentlySearched.seenThisSession
|
||||
let recentThemes = state.userRelatedState.recentlyVisitedThemes.mapD(thms => thms.filter(th => th !== state.layout.id).slice(0, 3))
|
||||
|
@ -24,7 +26,7 @@
|
|||
|
||||
<div class="relative w-full h-full collapsable " class:collapsed={!$isFocused && !$hasActiveFilters}>
|
||||
<div class="searchbox normal-background">
|
||||
<ActiveFilters {state} />
|
||||
<ActiveFilters activeFilters={$activeFilters} />
|
||||
{#if $isFocused}
|
||||
{#if $searchTerm.length > 0 && results === undefined}
|
||||
<div class="flex justify-center m-4 my-8">
|
||||
|
@ -64,7 +66,7 @@
|
|||
</h3>
|
||||
{#each $recentThemes as themeId (themeId)}
|
||||
<SearchResultSvelte
|
||||
entry={{payload: MoreScreen.officialThemesById.get(themeId), display_name: themeId, lat: 0, lon: 0}}
|
||||
entry={{payload: MoreScreen.officialThemesById.get(themeId), osm_id: themeId, category: "theme"}}
|
||||
{state}
|
||||
on:select />
|
||||
{/each}
|
||||
|
|
17
src/Utils.ts
17
src/Utils.ts
|
@ -1,4 +1,5 @@
|
|||
import DOMPurify from "dompurify"
|
||||
|
||||
export class Utils {
|
||||
/**
|
||||
* In the 'deploy'-step, some code needs to be run by ts-node.
|
||||
|
@ -1771,22 +1772,26 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
|
||||
}
|
||||
|
||||
static NoNullInplace(layers: any[]): void {
|
||||
for (let i = layers.length - 1; i >= 0; i--) {
|
||||
if (layers[i] === null || layers[i] === undefined) {
|
||||
layers.splice(i, 1)
|
||||
static NoNullInplace<T>(items: T[]): T[] {
|
||||
for (let i = items.length - 1; i >= 0; i--) {
|
||||
if (items[i] === null || items[i] === undefined) {
|
||||
items.splice(i, 1)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
private static emojiRegex = /[\p{Extended_Pictographic}🛰️]$/u
|
||||
private static emojiRegex = /[\p{Extended_Pictographic}🛰️]/u
|
||||
|
||||
/**
|
||||
* Returns 'true' if the given string contains at least one and only emoji characters
|
||||
*
|
||||
* Utils.isEmoji("⛰\uFE0F") // => true
|
||||
* Utils.isEmoji("🇧🇪") // => true
|
||||
* Utils.isEmoji("🍕") // => true
|
||||
*/
|
||||
public static isEmoji(string: string) {
|
||||
return Utils.emojiRegex.test(string)
|
||||
return Utils.emojiRegex.test(string) ||
|
||||
/[🇦-🇿]{2}/u.test(string) // flags, see https://stackoverflow.com/questions/53360006/detect-with-regex-if-emoji-is-country-flag
|
||||
}
|
||||
}
|
||||
|
|
|
@ -382,6 +382,17 @@ h2.group {
|
|||
background-color: #58cd2722;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
border-radius: 999rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
border: 1px solid var(--subtle-detail-color-light-contrast);
|
||||
background-color: var(--low-interaction-background);
|
||||
}
|
||||
|
||||
.alert {
|
||||
/* The class to convey important information, e.g. 'invalid', 'something went wrong', 'warning: testmode', ... */
|
||||
background-color: var(--alert-color);
|
||||
|
|
Loading…
Reference in a new issue