Search: refactoring searching for themes, refactor allThemesGui, incidentally fix #1679

This commit is contained in:
Pieter Vander Vennet 2024-09-05 02:25:03 +02:00
parent 9b8c300e77
commit d90b6d82d0
18 changed files with 421 additions and 334 deletions

View file

@ -4,14 +4,14 @@
"en": "Changes made with MapComplete", "en": "Changes made with MapComplete",
"de": "Änderungen mit MapComplete vorgenommen" "de": "Änderungen mit MapComplete vorgenommen"
}, },
"description": {
"en": "This maps shows all the changes made with MapComplete",
"de": "Diese Karte zeigt alle mit MapComplete vorgenommenen Änderungen"
},
"shortDescription": { "shortDescription": {
"en": "Shows changes made by MapComplete", "en": "Shows changes made by MapComplete",
"de": "Änderungen von MapComplete anzeigen" "de": "Änderungen von MapComplete anzeigen"
}, },
"description": {
"en": "This maps shows all the changes made with MapComplete",
"de": "Diese Karte zeigt alle mit MapComplete vorgenommenen Änderungen"
},
"icon": "./assets/svg/logo.svg", "icon": "./assets/svg/logo.svg",
"hideFromOverview": true, "hideFromOverview": true,
"startLat": 0, "startLat": 0,
@ -730,4 +730,4 @@
} }
} }
] ]
} }

View file

@ -1413,11 +1413,6 @@ input[type="range"].range-lg::-moz-range-thumb {
margin-right: auto; margin-right: auto;
} }
.mx-3 {
margin-left: 0.75rem;
margin-right: 0.75rem;
}
.my-4 { .my-4 {
margin-top: 1rem; margin-top: 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
@ -1511,8 +1506,8 @@ input[type="range"].range-lg::-moz-range-thumb {
margin-left: 0.5rem; margin-left: 0.5rem;
} }
.ml-4 { .mr-3 {
margin-left: 1rem; margin-right: 0.75rem;
} }
.mb-2 { .mb-2 {
@ -1531,6 +1526,10 @@ input[type="range"].range-lg::-moz-range-thumb {
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
.ml-4 {
margin-left: 1rem;
}
.mt-8 { .mt-8 {
margin-top: 2rem; margin-top: 2rem;
} }
@ -3913,6 +3912,11 @@ input[type="range"].range-lg::-moz-range-thumb {
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
} }
.px-0 {
padding-left: 0px;
padding-right: 0px;
}
.px-3 { .px-3 {
padding-left: 0.75rem; padding-left: 0.75rem;
padding-right: 0.75rem; padding-right: 0.75rem;
@ -3978,11 +3982,6 @@ input[type="range"].range-lg::-moz-range-thumb {
padding-bottom: 0.875rem; padding-bottom: 0.875rem;
} }
.px-0 {
padding-left: 0px;
padding-right: 0px;
}
.\!px-0 { .\!px-0 {
padding-left: 0px !important; padding-left: 0px !important;
padding-right: 0px !important; padding-right: 0px !important;
@ -4005,10 +4004,6 @@ input[type="range"].range-lg::-moz-range-thumb {
padding-left: 0.5rem; padding-left: 0.5rem;
} }
.pl-1 {
padding-left: 0.25rem;
}
.pr-1 { .pr-1 {
padding-right: 0.25rem; padding-right: 0.25rem;
} }
@ -4037,6 +4032,10 @@ input[type="range"].range-lg::-moz-range-thumb {
padding-right: 1rem; padding-right: 1rem;
} }
.pl-1 {
padding-left: 0.25rem;
}
.pb-1\.5 { .pb-1\.5 {
padding-bottom: 0.375rem; padding-bottom: 0.375rem;
} }
@ -4664,6 +4663,10 @@ input[type="range"].range-lg::-moz-range-thumb {
color: rgb(200 30 30 / var(--tw-placeholder-opacity)); color: rgb(200 30 30 / var(--tw-placeholder-opacity));
} }
.accent-gray-600 {
accent-color: #4B5563;
}
.opacity-50 { .opacity-50 {
opacity: 0.5; opacity: 0.5;
} }
@ -8113,10 +8116,6 @@ svg.apply-fill path {
margin-right: 0.25rem; margin-right: 0.25rem;
} }
.sm\:mt-0 {
margin-top: 0px;
}
.sm\:mt-2 { .sm\:mt-2 {
margin-top: 0.5rem; margin-top: 0.5rem;
} }

View file

@ -29,13 +29,12 @@ import LayerConfig from "../src/Models/ThemeConfig/LayerConfig"
import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig" import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig"
import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext" import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext"
import { GenerateFavouritesLayer } from "./generateFavouritesLayer" import { GenerateFavouritesLayer } from "./generateFavouritesLayer"
import LayoutConfig from "../src/Models/ThemeConfig/LayoutConfig" import LayoutConfig, { MinimalLayoutInformation } from "../src/Models/ThemeConfig/LayoutConfig"
import Translations from "../src/UI/i18n/Translations" import Translations from "../src/UI/i18n/Translations"
import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable" import { Translatable } from "../src/Models/ThemeConfig/Json/Translatable"
import { ValidateThemeAndLayers } from "../src/Models/ThemeConfig/Conversion/ValidateThemeAndLayers" import { ValidateThemeAndLayers } from "../src/Models/ThemeConfig/Conversion/ValidateThemeAndLayers"
import { ExtractImages } from "../src/Models/ThemeConfig/Conversion/FixImages" import { ExtractImages } from "../src/Models/ThemeConfig/Conversion/FixImages"
import { import {
MinimalTagRenderingConfigJson,
TagRenderingConfigJson, TagRenderingConfigJson,
} from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson" } from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson"
@ -189,7 +188,7 @@ class LayerOverviewUtils extends Script {
return publicLayerIds return publicLayerIds
} }
public static cleanTranslation(t: Record<string, string> | Translation): Translatable { public static cleanTranslation(t: string | Record<string, string> | Translation): Translatable {
return Translations.T(t).OnEveryLanguage((s) => parse_html(s).textContent).translations return Translations.T(t).OnEveryLanguage((s) => parse_html(s).textContent).translations
} }
@ -212,11 +211,71 @@ class LayerOverviewUtils extends Script {
return false return false
} }
static mergeKeywords(into: Record<string, string[]>, source: Readonly<Record<string, string[]>>){
for (const key in source) {
if(into[key]){
into[key].push(...source[key])
}else{
into[key] = source[key]
}
}
}
private layerKeywords(l: LayerConfigJson): Record<string, string[]> {
const keywords: Record<string, string[]> = {}
function addWord(language: string, word: string | string[]) {
if(Array.isArray(word)){
word.forEach(w => addWord(language, w))
return
}
word = Utils.SubstituteKeys(word, {}).trim()
if(!word){
return
}
if (!keywords[language]) {
keywords[language] = []
}
keywords[language].push(word)
}
function addWords(tr: string | Record<string, string> | Record<string, string[]> | TagRenderingConfigJson) {
if(!tr){
return
}
if (typeof tr === "string") {
addWord("*", tr)
return
}
if (tr["render"] !== undefined || tr["mappings"] !== undefined) {
tr = <TagRenderingConfigJson>tr
addWords(<Translatable>tr.render)
for (const mapping of tr.mappings ?? []) {
if (typeof mapping === "string") {
addWords(mapping)
continue
}
addWords(mapping.then)
}
return
}
for (const lang in tr) {
addWord(lang, tr[lang])
}
}
addWord("*", l.id)
addWords(l.title)
addWords(l.description)
addWords(l.searchTerms)
return keywords
}
writeSmallOverview( writeSmallOverview(
themes: { themes: {
id: string id: string
title: any title: Translatable
shortDescription: any shortDescription: Translatable
icon: string icon: string
hideFromOverview: boolean hideFromOverview: boolean
mustHaveLanguage: boolean mustHaveLanguage: boolean
@ -228,62 +287,31 @@ class LayerOverviewUtils extends Script {
} }
)[] )[]
}[], }[],
sharedLayers: Map<string, LayerConfigJson>
) { ) {
const perId = new Map<string, any>() const layerKeywords : Record<string, Record<string, string[]>> = {}
sharedLayers.forEach((layer, id) => {
layerKeywords[id] = this.layerKeywords(layer)
})
const perId = new Map<string, MinimalLayoutInformation>()
for (const theme of themes) { for (const theme of themes) {
const keywords: Record<string, string[]> = {} const keywords: Record<string, string[]> = {}
function addWord(language: string, word: string | string[]) {
if(Array.isArray(word)){
word.forEach(w => addWord(language, w))
return
}
word = Utils.SubstituteKeys(word, {}).trim()
if(!word){
return
}
console.log(language, "--->", word)
if (!keywords[language]) {
keywords[language] = []
}
keywords[language].push(word)
}
function addWords(tr: string | Record<string, string> | Record<string, string[]> | TagRenderingConfigJson) {
if(!tr){
return
}
if (typeof tr === "string") {
addWord("*", tr)
return
}
if (tr["render"] !== undefined || tr["mappings"] !== undefined) {
tr = <TagRenderingConfigJson>tr
addWords(<Translatable>tr.render)
for (let mapping of tr.mappings ?? []) {
if (typeof mapping === "string") {
addWords(mapping)
continue
}
addWords(mapping.then)
}
return
}
for (const lang in tr) {
addWord(lang, tr[lang])
}
}
for (const layer of theme.layers ?? []) { for (const layer of theme.layers ?? []) {
const l = <LayerConfigJson>layer const l = <LayerConfigJson>layer
addWord("*", l.id) if(sharedLayers.has(l.id)){
addWords(l.title) continue
addWords(l.description) }
addWords(l.searchTerms) if(l.id.startsWith("note_import")){
continue
}
LayerOverviewUtils.mergeKeywords(keywords, this.layerKeywords(l))
} }
const data = { const data = <MinimalLayoutInformation> {
id: theme.id, id: theme.id,
title: theme.title, title: theme.title,
shortDescription: LayerOverviewUtils.cleanTranslation(theme.shortDescription), shortDescription: LayerOverviewUtils.cleanTranslation(theme.shortDescription),
@ -291,6 +319,7 @@ class LayerOverviewUtils extends Script {
hideFromOverview: theme.hideFromOverview, hideFromOverview: theme.hideFromOverview,
mustHaveLanguage: theme.mustHaveLanguage, mustHaveLanguage: theme.mustHaveLanguage,
keywords, keywords,
layers: theme.layers.filter(l => sharedLayers.has(l["id"])).map(l => l["id"])
} }
perId.set(theme.id, data) perId.set(theme.id, data)
} }
@ -311,7 +340,7 @@ class LayerOverviewUtils extends Script {
writeFileSync( writeFileSync(
"./src/assets/generated/theme_overview.json", "./src/assets/generated/theme_overview.json",
JSON.stringify(sorted, null, " "), JSON.stringify({ layers: layerKeywords, themes: sorted }, null, " "),
{ encoding: "utf8" }, { encoding: "utf8" },
) )
} }
@ -927,7 +956,7 @@ class LayerOverviewUtils extends Script {
if (whitelist.size == 0) { if (whitelist.size == 0) {
this.writeSmallOverview( this.writeSmallOverview(
Array.from(fixed.values()).map((t) => { Array.from(fixed.values()).map((t) => {
return { return <any> {
...t, ...t,
hideFromOverview: t.hideFromOverview ?? false, hideFromOverview: t.hideFromOverview ?? false,
shortDescription: shortDescription:
@ -935,6 +964,7 @@ class LayerOverviewUtils extends Script {
mustHaveLanguage: t.mustHaveLanguage?.length > 0, mustHaveLanguage: t.mustHaveLanguage?.length > 0,
} }
}), }),
sharedLayers
) )
} }

View file

@ -7,11 +7,9 @@ import Constants from "../../Models/Constants"
export default class FilterSearch implements GeocodingProvider { export default class FilterSearch implements GeocodingProvider {
private readonly _state: SpecialVisualizationState private readonly _state: SpecialVisualizationState
private readonly suggestions
constructor(state: SpecialVisualizationState) { constructor(state: SpecialVisualizationState) {
this._state = state this._state = state
this.suggestions = this.getSuggestions()
} }
async search(query: string): Promise<SearchResult[]> { async search(query: string): Promise<SearchResult[]> {
@ -58,7 +56,6 @@ export default class FilterSearch implements GeocodingProvider {
Utils.NoNullInplace(terms) Utils.NoNullInplace(terms)
const distances = queries.flatMap(query => terms.map(entry => { const distances = queries.flatMap(query => terms.map(entry => {
const d = Utils.levenshteinDistance(query, entry.slice(0, query.length)) const d = Utils.levenshteinDistance(query, entry.slice(0, query.length))
console.log(query, "? +", terms, "=", d)
const dRelative = d / query.length const dRelative = d / query.length
return dRelative return dRelative
})) }))
@ -79,10 +76,10 @@ export default class FilterSearch implements GeocodingProvider {
} }
/**
* Create a random list of filters
*/
getSuggestions(): FilterPayload[] { getSuggestions(): FilterPayload[] {
if (this.suggestions) {
// return this.suggestions
}
const result: FilterPayload[] = [] const result: FilterPayload[] = []
for (const [id, filteredLayer] of this._state.layerState.filteredLayers) { for (const [id, filteredLayer] of this._state.layerState.filteredLayers) {
if (!Array.isArray(filteredLayer.layerDef.filters)) { if (!Array.isArray(filteredLayer.layerDef.filters)) {

View file

@ -13,7 +13,6 @@ export class RecentSearch {
constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) { constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) {
const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches") const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches")
prefs.set(null)
this._seenThisSession = new UIEventSource<GeocodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, []) this._seenThisSession = new UIEventSource<GeocodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
this.seenThisSession = this._seenThisSession this.seenThisSession = this._seenThisSession

View file

@ -1,8 +1,7 @@
import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider" import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider"
import * as themeOverview from "../../assets/generated/theme_overview.json" import * as themeOverview from "../../assets/generated/theme_overview.json"
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import { SpecialVisualizationState } from "../../UI/SpecialVisualization" import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
import { Utils } from "../../Utils"
import MoreScreen from "../../UI/BigComponents/MoreScreen" import MoreScreen from "../../UI/BigComponents/MoreScreen"
import { ImmutableStore, Store } from "../UIEventSource" import { ImmutableStore, Store } from "../UIEventSource"
@ -12,11 +11,16 @@ export default class ThemeSearch implements GeocodingProvider {
private readonly _state: SpecialVisualizationState private readonly _state: SpecialVisualizationState
private readonly _knownHiddenThemes: Store<Set<string>> private readonly _knownHiddenThemes: Store<Set<string>>
private readonly _suggestionLimit: number private readonly _suggestionLimit: number
private readonly _layersToIgnore: string[]
private readonly _otherThemes: MinimalLayoutInformation[]
constructor(state: SpecialVisualizationState, suggestionLimit: number) { constructor(state: SpecialVisualizationState, suggestionLimit: number) {
this._state = state this._state = state
this._layersToIgnore = state.layout.layers.map(l => l.id)
this._suggestionLimit = suggestionLimit this._suggestionLimit = suggestionLimit
this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection) this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection)
this._otherThemes = MoreScreen.officialThemes.themes
.filter(th => th.id !== state.layout.id)
} }
async search(query: string): Promise<SearchResult[]> { async search(query: string): Promise<SearchResult[]> {
@ -40,11 +44,11 @@ export default class ThemeSearch implements GeocodingProvider {
if (query.length < 1) { if (query.length < 1) {
return [] return []
} }
query = Utils.simplifyStringForSearch(query) const sorted = MoreScreen.sortedByLowest(query, this._otherThemes, this._layersToIgnore)
return ThemeSearch.allThemes console.log(">>>", sorted)
return sorted
.map(th => th.theme)
.filter(th => !th.hideFromOverview || this._knownHiddenThemes.data.has(th.id)) .filter(th => !th.hideFromOverview || this._knownHiddenThemes.data.has(th.id))
.filter(th => th.id !== this._state.layout.id)
.filter(th => MoreScreen.MatchesLayout(th, query))
.slice(0, limit) .slice(0, limit)
} }

View file

@ -154,7 +154,7 @@ export default class SearchState {
const poi = result[0] const poi = result[0]
if (poi.category === "theme") { if (poi.category === "theme") {
const theme = <MinimalLayoutInformation>poi.payload const theme = <MinimalLayoutInformation>poi.payload
const url = MoreScreen.createUrlFor(theme, false) const url = MoreScreen.createUrlFor(theme)
window.location = <any>url window.location = <any>url
return true return true
} }

View file

@ -1,4 +1,4 @@
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig, { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import { OsmConnection } from "../Osm/OsmConnection" import { OsmConnection } from "../Osm/OsmConnection"
import { MangroveIdentity } from "../Web/MangroveReviews" import { MangroveIdentity } from "../Web/MangroveReviews"
import { Store, Stores, UIEventSource } from "../UIEventSource" import { Store, Stores, UIEventSource } from "../UIEventSource"
@ -141,8 +141,9 @@ export default class UserRelatedState {
this._recentlyVisitedThemes = UIEventSource.asObject(prefs.GetLongPreference("recently-visited-themes"), []) this._recentlyVisitedThemes = UIEventSource.asObject(prefs.GetLongPreference("recently-visited-themes"), [])
this.recentlyVisitedThemes = this._recentlyVisitedThemes this.recentlyVisitedThemes = this._recentlyVisitedThemes
if (layout) { if (layout) {
const osmConn =this.osmConnection const osmConn = this.osmConnection
const recentlyVisited = this._recentlyVisitedThemes const recentlyVisited = this._recentlyVisitedThemes
function update() { function update() {
if (!osmConn.isLoggedIn.data) { if (!osmConn.isLoggedIn.data) {
return return
@ -203,16 +204,7 @@ export default class UserRelatedState {
} }
} }
public GetUnofficialTheme(id: string): public getUnofficialTheme(id: string): (MinimalLayoutInformation & { definition }) | undefined {
| {
id: string
icon: string
title: any
shortDescription: any
definition?: any
isOfficial: boolean
}
| undefined {
const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id) const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id)
const str = pref.data const str = pref.data
@ -222,16 +214,7 @@ export default class UserRelatedState {
} }
try { try {
const value: { return <MinimalLayoutInformation & { definition: string }>JSON.parse(str)
id: string
icon: string
title: any
shortDescription: any
definition?: any
isOfficial: boolean
} = JSON.parse(str)
value.isOfficial = false
return value
} catch (e) { } catch (e) {
console.warn( console.warn(
"Removing theme " + "Removing theme " +
@ -464,7 +447,7 @@ export default class UserRelatedState {
} }
if (tags[key + "-combined-0"]) { if (tags[key + "-combined-0"]) {
// A combined value exists // A combined value exists
if(tags[key].startsWith("undefined")){ if (tags[key].startsWith("undefined")) {
// Sometimes, a long string of 'undefined' will show up, we ignore them // Sometimes, a long string of 'undefined' will show up, we ignore them
continue continue
} }

View file

@ -22,10 +22,10 @@ export class MinimalLayoutInformation {
icon: string icon: string
title: Translatable title: Translatable
shortDescription: Translatable shortDescription: Translatable
definition?: Translatable
mustHaveLanguage?: boolean mustHaveLanguage?: boolean
hideFromOverview?: boolean hideFromOverview?: boolean
keywords?: Record<string, string[]> keywords?: Record<string, string[]>
layers: string[]
} }
/** /**
* Minimal information about a theme * Minimal information about a theme

View file

@ -11,16 +11,11 @@
import LoginToggle from "./Base/LoginToggle.svelte" import LoginToggle from "./Base/LoginToggle.svelte"
import Pencil from "../assets/svg/Pencil.svelte" import Pencil from "../assets/svg/Pencil.svelte"
import Constants from "../Models/Constants" import Constants from "../Models/Constants"
import { Store, UIEventSource } from "../Logic/UIEventSource" import { Store, Stores, UIEventSource } from "../Logic/UIEventSource"
import { placeholder } from "../Utils/placeholder"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import ThemesList from "./BigComponents/ThemesList.svelte" import ThemesList from "./BigComponents/ThemesList.svelte"
import { LayoutInformation } from "../Models/ThemeConfig/LayoutConfig" import { MinimalLayoutInformation } from "../Models/ThemeConfig/LayoutConfig"
import * as themeOverview from "../assets/generated/theme_overview.json"
import UnofficialThemeList from "./BigComponents/UnofficialThemeList.svelte"
import Eye from "../assets/svg/Eye.svelte" import Eye from "../assets/svg/Eye.svelte"
import LoginButton from "./Base/LoginButton.svelte" import LoginButton from "./Base/LoginButton.svelte"
import ChevronDoubleRight from "@babeard/svelte-heroicons/mini/ChevronDoubleRight"
import Mastodon from "../assets/svg/Mastodon.svelte" import Mastodon from "../assets/svg/Mastodon.svelte"
import Liberapay from "../assets/svg/Liberapay.svelte" import Liberapay from "../assets/svg/Liberapay.svelte"
import Bug from "../assets/svg/Bug.svelte" import Bug from "../assets/svg/Bug.svelte"
@ -28,6 +23,7 @@
import { Utils } from "../Utils" import { Utils } from "../Utils"
import { ArrowTrendingUp } from "@babeard/svelte-heroicons/solid/ArrowTrendingUp" import { ArrowTrendingUp } from "@babeard/svelte-heroicons/solid/ArrowTrendingUp"
import Searchbar from "./Base/Searchbar.svelte" import Searchbar from "./Base/Searchbar.svelte"
import ChevronDoubleRight from "@babeard/svelte-heroicons/mini/ChevronDoubleRight"
const featureSwitches = new OsmConnectionFeatureSwitches() const featureSwitches = new OsmConnectionFeatureSwitches()
const osmConnection = new OsmConnection({ const osmConnection = new OsmConnection({
@ -40,27 +36,71 @@
}) })
const state = new UserRelatedState(osmConnection) const state = new UserRelatedState(osmConnection)
const t = Translations.t.index const t = Translations.t.index
const tu = Translations.t.general
const tr = Translations.t.general.morescreen const tr = Translations.t.general.morescreen
let userLanguages = osmConnection.userDetails.map((ud) => ud.languages) let userLanguages = osmConnection.userDetails.map((ud) => ud.languages)
let themeSearchText: UIEventSource<string | undefined> = new UIEventSource<string>("") let search: UIEventSource<string | undefined> = new UIEventSource<string>("")
let searchStable = search.stabilized(100)
const officialThemes: MinimalLayoutInformation[] = MoreScreen.officialThemes.themes.filter(th => th.hideFromOverview === false)
const hiddenThemes: MinimalLayoutInformation[] = MoreScreen.officialThemes.themes.filter(th => th.hideFromOverview === true)
let visitedHiddenThemes: Store<MinimalLayoutInformation[]> = MoreScreen.knownHiddenThemes(state.osmConnection)
.map((knownIds) => hiddenThemes.filter((theme) =>
knownIds.has(theme.id) || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet"
))
const customThemes: Store<MinimalLayoutInformation[]> = Stores.ListStabilized<string>(state.installedUserThemes)
.mapD(stableIds => stableIds.map(id => state.getUnofficialTheme(id)))
function filtered(themes: MinimalLayoutInformation[]): Store<MinimalLayoutInformation[]> {
return searchStable.map(search => {
if (!search) {
return themes
}
const scores = MoreScreen.sortedByLowest(search, themes)
const strict = scores.filter(sc => sc.lowest < 2)
if (strict.length > 0) {
return strict.map(sc => sc.theme)
}
return scores.filter(sc => sc.lowest < 4).slice(0, 6).map(sc => sc.theme)
})
}
let officialSearched = filtered(officialThemes)
let hiddenSearched = visitedHiddenThemes.bindD(visited => filtered(visited))
let customSearched = customThemes.bindD(customThemes => filtered(customThemes))
let searchIsFocussed = new UIEventSource(false)
document.addEventListener("keydown", function(event) { document.addEventListener("keydown", function(event) {
if (event.ctrlKey && event.code === "KeyF") { if (event.ctrlKey && event.code === "KeyF") {
document.getElementById("theme-search")?.focus() searchIsFocussed.set(true)
event.preventDefault() event.preventDefault()
} }
}) })
let visitedHiddenThemes: Store<LayoutInformation[]> function applySearch() {
const hiddenThemes: LayoutInformation[] = const didRedirect = MoreScreen.applySearch(search.data)
(themeOverview["default"] ?? themeOverview)?.filter((layout) => layout.hideFromOverview) ?? [] console.log("Did redirect?", didRedirect)
{ if (didRedirect) {
visitedHiddenThemes = MoreScreen.knownHiddenThemes(state.osmConnection) // Just for style and readability; won't _actually_ reach this
.map((knownIds) => hiddenThemes.filter((theme) => return
knownIds.has(theme.id) || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet" }
))
const candidate = officialSearched.data[0] ?? hiddenSearched.data[0] ?? customSearched.data[0]
if (!candidate) {
return
}
window.location.href = MoreScreen.createUrlFor(candidate)
} }
</script> </script>
<main> <main>
@ -92,20 +132,19 @@
</div> </div>
</div> </div>
<Searchbar value={themeSearchText} placeholder={tr.searchForATheme} on:search={() => MoreScreen.applySearch(themeSearchText.data)}/> <Searchbar value={search} placeholder={tr.searchForATheme} on:search={() => applySearch()} isFocused={searchIsFocussed} />
<ThemesList search={themeSearchText} {state} themes={MoreScreen.officialThemes} /> <ThemesList {search} {state} themes={$officialSearched} />
<LoginToggle {state}> <LoginToggle {state}>
<LoginButton clss="primary" {osmConnection} slot="not-logged-in"> <LoginButton clss="primary" {osmConnection} slot="not-logged-in">
<Tr t={t.logIn} /> <Tr t={t.logIn} />
</LoginButton> </LoginButton>
<ThemesList <ThemesList
hideThemes={false} {search}
isCustom={false}
search={themeSearchText}
{state} {state}
themes={$visitedHiddenThemes} themes={$hiddenSearched}
hasSelection={$officialSearched.length === 0}
> >
<svelte:fragment slot="title"> <svelte:fragment slot="title">
<h3> <h3>
@ -122,7 +161,19 @@
</svelte:fragment> </svelte:fragment>
</ThemesList> </ThemesList>
<UnofficialThemeList search={themeSearchText} {state} /> {#if $customThemes.length > 0}
<ThemesList {search} {state} themes={$customSearched}
hasSelection={$officialSearched.length === 0 && $hiddenSearched.length === 0}
>
<svelte:fragment slot="title">
<h3>
<Tr t={tu.customThemeTitle} />
</h3>
<Tr t={tu.customThemeIntro} />
</svelte:fragment>
</ThemesList>
{/if}
</LoginToggle> </LoginToggle>
<a <a

View file

@ -6,6 +6,7 @@
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import { ariaLabel } from "../../Utils/ariaLabel" import { ariaLabel } from "../../Utils/ariaLabel"
import { Translation } from "../i18n/Translation" import { Translation } from "../i18n/Translation"
import Backspace from "@babeard/svelte-heroicons/outline/Backspace"
export let value: UIEventSource<string> export let value: UIEventSource<string>
let _value = value.data ?? "" let _value = value.data ?? ""
@ -23,8 +24,8 @@
if (focussed) { if (focussed) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (document.activeElement !== inputElement) { if (document.activeElement !== inputElement) {
inputElement.focus() inputElement?.focus()
inputElement.select() inputElement?.select()
} }
}) })
} }
@ -38,15 +39,17 @@
on:submit|preventDefault={() => dispatch("search")} on:submit|preventDefault={() => dispatch("search")}
> >
<label <label
class="neutral-label normal-background flex w-full items-center rounded-full border-2 border-black box-shadow" class="neutral-label normal-background flex w-full items-center rounded-full border border-black box-shadow"
> >
<SearchIcon aria-hidden="true" class="h-8 w-8 ml-2" />
<input <input
bind:this={inputElement} bind:this={inputElement}
on:focus={() => {isFocused?.setData(true)}} on:focus={() => {isFocused?.setData(true)}}
on:blur={() => {isFocused?.setData(false)}} on:blur={() => {isFocused?.setData(false)}}
type="search" type="search"
style=" --tw-ring-color: rgb(0 0 0 / 0) !important;" style=" --tw-ring-color: rgb(0 0 0 / 0) !important;"
class="ml-4 pl-1 w-full outline-none border-none" class="px-0 ml-1 w-full outline-none border-none"
on:keypress={(keypr) => { on:keypress={(keypr) => {
return keypr.key === "Enter" ? dispatch("search") : undefined return keypr.key === "Enter" ? dispatch("search") : undefined
}} }}
@ -54,7 +57,11 @@
use:set_placeholder={placeholder} use:set_placeholder={placeholder}
use:ariaLabel={placeholder} use:ariaLabel={placeholder}
/> />
<SearchIcon aria-hidden="true" class="h-8 w-8 mx-3" />
{#if $value.length > 0}
<Backspace on:click={() => value.set("")} color="var(--button-background)" class="w-6 h-6 mr-3 cursor-pointer" />
{:else}
<div class="w-6 mr-3" />
{/if}
</label> </label>
</form> </form>

View file

@ -3,26 +3,37 @@ import { Store } from "../../Logic/UIEventSource"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import themeOverview from "../../assets/generated/theme_overview.json" import themeOverview from "../../assets/generated/theme_overview.json"
import Locale from "../i18n/Locale" import Locale from "../i18n/Locale"
import { Translatable } from "../../Models/ThemeConfig/Json/Translatable"
import { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"
import { OsmConnection } from "../../Logic/Osm/OsmConnection" import { OsmConnection } from "../../Logic/Osm/OsmConnection"
export type ThemeSearchScore = {
theme: MinimalLayoutInformation,
lowest: number,
perLayer?: Record<string, number>,
other: number
}
export default class MoreScreen { export default class MoreScreen {
public static readonly officialThemes: MinimalLayoutInformation[] = themeOverview public static readonly officialThemes: {
themes: MinimalLayoutInformation[],
layers: Record<string, Record<string, string[]>>
} = themeOverview
public static readonly officialThemesById: Map<string, MinimalLayoutInformation> = new Map<string, MinimalLayoutInformation>() public static readonly officialThemesById: Map<string, MinimalLayoutInformation> = new Map<string, MinimalLayoutInformation>()
static { static {
for (const th of MoreScreen.officialThemes) { for (const th of MoreScreen.officialThemes.themes) {
MoreScreen.officialThemesById.set(th.id, th) MoreScreen.officialThemesById.set(th.id, th)
} }
} }
public static applySearch(searchTerm: string) { /** Applies special search terms, such as 'studio', 'osmcha', ...
* Returns 'false' if nothing is matched.
* Doesn't return control flow if a match is found (navigates to another page in this case)
*/
public static applySearch(searchTerm: string, ) {
searchTerm = searchTerm.toLowerCase() searchTerm = searchTerm.toLowerCase()
if (!searchTerm) { if (!searchTerm) {
return return false
} }
if (searchTerm === "personal") { if (searchTerm === "personal") {
window.location.href = MoreScreen.createUrlFor({ id: "personal" }, false) window.location.href = MoreScreen.createUrlFor({ id: "personal" })
} }
if (searchTerm === "bugs" || searchTerm === "issues") { if (searchTerm === "bugs" || searchTerm === "issues") {
window.location.href = "https://github.com/pietervdvn/MapComplete/issues" window.location.href = "https://github.com/pietervdvn/MapComplete/issues"
@ -39,77 +50,110 @@ export default class MoreScreen {
if (searchTerm === "studio") { if (searchTerm === "studio") {
window.location.href = "./studio.html" window.location.href = "./studio.html"
} }
// Enter pressed -> search the first _official_ matchin theme and open it return false
const publicTheme = MoreScreen.officialThemes.find(
(th) =>
th.hideFromOverview == false &&
th.id !== "personal" &&
MoreScreen.MatchesLayout(th, searchTerm),
)
if (publicTheme !== undefined) {
window.location.href = MoreScreen.createUrlFor(publicTheme, false)
}
const hiddenTheme = MoreScreen.officialThemes.find(
(th) => th.id !== "personal" && MoreScreen.MatchesLayout(th, searchTerm),
)
if (hiddenTheme !== undefined) {
window.location.href = MoreScreen.createUrlFor(hiddenTheme, false)
}
} }
public static MatchesLayout( /**
layout: MinimalLayoutInformation, * Searches for the smallest distance in words; will split both the query and the terms
search: string, *
language?: string, * MoreScreen.scoreKeywords("drinking water", {"en": ["A layer with drinking water points"]}, "en") // => 0
): boolean { * MoreScreen.scoreKeywords("waste", {"en": ["A layer with drinking water points"]}, "en") // => 2
if (search === undefined) { *
return true */
} public static scoreKeywords(query: string, keywords: Record<string, string[]> | string[], language?: string): number {
search = Utils.simplifyStringForSearch(search.toLocaleLowerCase()) // See #1729 if(!keywords){
if (search.length > 3 && layout.id.toLowerCase().indexOf(search) >= 0) { return Infinity
return true
}
if (layout.id === "personal") {
return false
}
if (Utils.simplifyStringForSearch(layout.id) === Utils.simplifyStringForSearch(search)) {
return true
} }
language ??= Locale.language.data language ??= Locale.language.data
const queryParts = query.split(" ").map(q => Utils.simplifyStringForSearch(q))
let terms: string[]
if (Array.isArray(keywords)) {
terms = keywords
} else {
terms = (keywords[language] ?? []).concat(keywords["*"])
}
const termsAll = Utils.NoNullInplace(terms).flatMap(t => t.split(" "))
const entitiesToSearch: (string | Record<string, string> | Record<string, string[]>)[] = [layout.shortDescription, layout.title, layout.keywords] let distanceSummed = 0
for (const entity of entitiesToSearch) { for (let i = 0; i < queryParts.length; i++) {
if (entity === undefined) { const q = queryParts[i]
continue let minDistance: number = 99
} for (const term of termsAll) {
const d = Utils.levenshteinDistance(q, Utils.simplifyStringForSearch(term))
let term: string[] if (d < minDistance) {
if (typeof entity === "string") { minDistance = d
term = [entity]
} else {
const terms = [].concat(entity["*"], entity[language])
if (Array.isArray(terms)) {
term = terms
} else {
term = [terms]
} }
} }
distanceSummed += minDistance
}
return distanceSummed
}
const minLevehnstein = Math.min(...Utils.NoNull(term).map(t => Utils.levenshteinDistance(search, public static scoreLayers(query: string): Record<string, number> {
Utils.simplifyStringForSearch(t).slice(0, search.length)))) const result: Record<string, number> = {}
for (const id in this.officialThemes.layers) {
const keywords = this.officialThemes.layers[id]
const distance = this.scoreKeywords(query, keywords)
result[id] = distance
}
return result
}
if (minLevehnstein < 1 || minLevehnstein / search.length < 0.2) {
return true public static scoreThemes(query: string, themes: MinimalLayoutInformation[], ignoreLayers: string[] = []): Record<string, ThemeSearchScore> {
if (query?.length < 1) {
return undefined
}
themes = Utils.NoNullInplace(themes)
const layerScores = this.scoreLayers(query)
for (const ignoreLayer of ignoreLayers) {
delete layerScores[ignoreLayer]
}
const results: Record<string, ThemeSearchScore> = {}
for (const layoutInfo of themes) {
const theme = layoutInfo.id
if (theme === "personal") {
continue
}
if (Utils.simplifyStringForSearch(theme) === query) {
results[theme] = {
theme: layoutInfo,
lowest: -1,
other: 0
}
continue
}
const perLayer = Utils.asRecord(
layoutInfo.layers ?? [], layer => layerScores[layer]
)
const language = Locale.language.data
const keywords =Utils.NoNullInplace( [layoutInfo.shortDescription, layoutInfo.title])
.map(item => typeof item === "string" ? item : (item[language] ?? item["*"]))
const other = Math.min(this.scoreKeywords(query, keywords), this.scoreKeywords(query, layoutInfo.keywords))
const lowest = Math.min(other, ...Object.values(perLayer))
results[theme] = {
theme:layoutInfo,
perLayer,
other,
lowest
} }
} }
return results
}
return false public static sortedByLowest(search: string, themes: MinimalLayoutInformation[], ignoreLayers: string[] = []){
const scored = Object.values(this.scoreThemes(search, themes, ignoreLayers ))
scored.sort((a,b) => a.lowest - b.lowest)
return scored
} }
public static createUrlFor( public static createUrlFor(
layout: { id: string }, layout: { id: string },
isCustom: boolean, state?: { layoutToUse?: { id } }
state?: { layoutToUse?: { id } },
): string { ): string {
if (layout === undefined) { if (layout === undefined) {
return undefined return undefined
@ -136,7 +180,7 @@ export default class MoreScreen {
linkPrefix = `${path}/theme.html?layout=${layout.id}&` linkPrefix = `${path}/theme.html?layout=${layout.id}&`
} }
if (isCustom) { if (layout.id.startsWith("http://") || layout.id.startsWith("https://")) {
linkPrefix = `${path}/theme.html?userlayout=${layout.id}&` linkPrefix = `${path}/theme.html?userlayout=${layout.id}&`
} }
@ -155,7 +199,7 @@ export default class MoreScreen {
new Set<string>( new Set<string>(
Object.keys(preferences) Object.keys(preferences)
.filter((key) => key.startsWith(prefix)) .filter((key) => key.startsWith(prefix))
.map((key) => key.substring(prefix.length, key.length - "-enabled".length)), .map((key) => key.substring(prefix.length, key.length - "-enabled".length))
)) ))
} }
} }

View file

@ -1,31 +1,14 @@
<script lang="ts"> <script lang="ts">
import * as personal from "../../../assets/themes/personal/personal.json" import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Constants from "../../Models/Constants"
import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
import Marker from "../Map/Marker.svelte" import Marker from "../Map/Marker.svelte"
export let theme: MinimalLayoutInformation export let theme: MinimalLayoutInformation & {isOfficial?: boolean}
export let isCustom: boolean = false let isCustom: boolean = theme.id.startsWith("https://") || theme.id.startsWith("http://")
export let userDetails: UIEventSource<UserDetails>
export let state: { layoutToUse?: { id: string }; osmConnection: OsmConnection } export let state: { layoutToUse?: { id: string }; osmConnection: OsmConnection }
export let selected: boolean = false
let unlockedPersonal = LocalStorageSource.GetParsed("unlocked_personal_theme", false)
userDetails.addCallbackAndRunD((userDetails) => {
if (!userDetails.loggedIn) {
return
}
if (userDetails.csCount > Constants.userJourney.personalLayoutUnlock) {
unlockedPersonal.setData(true)
}
return true
})
$: title = Translations.T( $: title = Translations.T(
theme.title, theme.title,
@ -33,7 +16,6 @@
) )
$: description = Translations.T(theme.shortDescription) $: description = Translations.T(theme.shortDescription)
// TODO: Improve this function
function createUrl( function createUrl(
layout: { id: string; definition?: string }, layout: { id: string; definition?: string },
isCustom: boolean, isCustom: boolean,
@ -84,19 +66,12 @@
let href = createUrl(theme, isCustom, state) let href = createUrl(theme, isCustom, state)
</script> </script>
{#if theme.id !== personal.id || $unlockedPersonal}
<a class="low-interaction my-1 flex w-full items-center text-ellipsis rounded p-1" href={$href}> <a class="low-interaction my-1 flex w-full items-center text-ellipsis rounded p-1" href={$href}>
<Marker icons={theme.icon} size="block h-8 w-8 sm:h-11 sm:w-11 m-1 sm:mx-2 md:mx-4 shrink-0" /> <Marker icons={theme.icon} size="block h-8 w-8 sm:h-11 sm:w-11 m-1 sm:mx-2 md:mx-4 shrink-0" />
<span class="flex flex-col overflow-hidden text-ellipsis text-xl font-bold"> <span class="flex flex-col overflow-hidden text-ellipsis text-xl font-bold">
<Tr cls="" t={title} /> <Tr cls="" t={title} />
<Tr cls="subtle text-base" t={description} /> <Tr cls="subtle text-base" t={description} />
<slot/>
{#if selected}
<span class="thanks hidden-on-mobile" aria-hidden="true">
<Tr t={Translations.t.general.morescreen.enterToOpen} />
</span>
{/if}
</span> </span>
</a> </a>
{/if}

View file

@ -4,46 +4,36 @@
import { OsmConnection } from "../../Logic/Osm/OsmConnection" import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import ThemeButton from "./ThemeButton.svelte" import ThemeButton from "./ThemeButton.svelte"
import { LayoutInformation, MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import MoreScreen from "./MoreScreen" import Translations from "../i18n/Translations"
import themeOverview from "../../assets/generated/theme_overview.json" import Tr from "../Base/Tr.svelte"
export let search: UIEventSource<string> export let search: UIEventSource<string>
export let themes: MinimalLayoutInformation[] export let themes: MinimalLayoutInformation[]
export let state: { osmConnection: OsmConnection } export let state: { osmConnection: OsmConnection }
export let isCustom: boolean = false
export let hideThemes: boolean = true
// Filter theme based on search value export let hasSelection : boolean = true
$: filteredThemes = themes.filter((theme) => MoreScreen.MatchesLayout(theme, $search))
// Determine which is the first theme, after the search, using all themes
$: allFilteredThemes = themeOverview.filter((theme) => MoreScreen.MatchesLayout(theme, $search))
$: firstTheme = allFilteredThemes[0]
</script> </script>
<section class="w-full"> <section class="w-full">
<slot name="title" /> <slot name="title" />
<div class="theme-list my-2 gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3"> <div class="theme-list my-2 gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3">
{#each filteredThemes as theme (theme.id)} {#each themes as theme (theme.id)}
{#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)} <ThemeButton
<!-- TODO: doesn't work if first theme is hidden --> {theme}
{#if theme === firstTheme && !isCustom && $search !== "" && $search !== undefined} {state}
<ThemeButton >
{theme} {#if $search && hasSelection && themes[0] === theme}
{isCustom} <span class="thanks hidden-on-mobile" aria-hidden="true">
userDetails={state.osmConnection.userDetails} <Tr t={Translations.t.general.morescreen.enterToOpen} />
{state} </span>
selected={true}
/>
{:else}
<ThemeButton {theme} {isCustom} userDetails={state.osmConnection.userDetails} {state} />
{/if} {/if}
{/if} </ThemeButton>
{/each} {/each}
</div> </div>
{#if filteredThemes.length === 0} {#if themes.length === 0}
<NoThemeResultButton {search} /> <NoThemeResultButton {search} />
{/if} {/if}
</section> </section>

View file

@ -12,21 +12,8 @@
osmConnection: OsmConnection osmConnection: OsmConnection
} }
const t = Translations.t.general
const currentIds: Store<string[]> = state.installedUserThemes
const stableIds = Stores.ListStabilized<string>(currentIds)
let customThemes let customThemes
$: customThemes = Utils.NoNull($stableIds.map((id) => state.GetUnofficialTheme(id)))
$: console.log("Custom themes are", customThemes)
</script> </script>
{#if customThemes.length > 0}
<ThemesList {search} {state} themes={customThemes} isCustom={true} hideThemes={false}>
<svelte:fragment slot="title">
<h3>
<Tr t={t.customThemeTitle} />
</h3>
<Tr t={t.customThemeIntro} />
</svelte:fragment>
</ThemesList>
{/if}

View file

@ -29,8 +29,6 @@
</script> </script>
<div class="p-4 low-interaction flex gap-y-2 flex-col"> <div class="p-4 low-interaction flex gap-y-2 flex-col">
<h3>Search results</h3>
<ActiveFilters activeFilters={$activeFilters} /> <ActiveFilters activeFilters={$activeFilters} />
{#if $searchTerm.length > 0 && $filterResults.length > 0} {#if $searchTerm.length > 0 && $filterResults.length > 0}
@ -79,7 +77,7 @@
<h3> <h3>
Other maps Other maps
</h3> </h3>
{#each $themeResults as entry} {#each $themeResults as entry (entry.id)}
<ThemeResult {entry} /> <ThemeResult {entry} />
{/each} {/each}
</SidebarUnit> </SidebarUnit>

View file

@ -47,14 +47,34 @@
import { CloseButton } from "flowbite-svelte" import { CloseButton } from "flowbite-svelte"
import Hash from "../Logic/Web/Hash" import Hash from "../Logic/Web/Hash"
import Searchbar from "./Base/Searchbar.svelte" import Searchbar from "./Base/Searchbar.svelte"
import ChevronRight from "@babeard/svelte-heroicons/mini/ChevronRight"
import ChevronLeft from "@babeard/svelte-heroicons/solid/ChevronLeft"
export let state: ThemeViewState export let state: ThemeViewState
let layout = state.layout let layout = state.layout
let maplibremap: UIEventSource<MlMap> = state.map let maplibremap: UIEventSource<MlMap> = state.map
let state_selectedElement = state.selectedElement let state_selectedElement = state.selectedElement
let selectedElement: UIEventSource<Feature> = new UIEventSource<Feature>(undefined) let selectedElement: UIEventSource<Feature> = new UIEventSource<Feature>(undefined)
let compass = Orientation.singleton.alpha let compass = Orientation.singleton.alpha
let compassLoaded = Orientation.singleton.gotMeasurement let compassLoaded = Orientation.singleton.gotMeasurement
let hash = Hash.hash
let previewedImage = state.previewedImage
let addNewFeatureMode = state.userRelatedState.addNewFeatureMode
let gpsAvailable = state.geolocation.geolocationState.gpsAvailable
let gpsButtonAriaLabel = state.geolocation.geolocationState.gpsStateExplanation
let debug = state.featureSwitches.featureSwitchIsDebugging
let featureSwitches: FeatureSwitchState = state.featureSwitches
let currentViewLayer: LayerConfig = layout.layers.find((l) => l.id === "current_view")
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer
let currentZoom = state.mapProperties.zoom
let showCrosshair = state.userRelatedState.showCrosshair
let visualFeedback = state.visualFeedback
let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined)
let mapproperties: MapProperties = state.mapProperties
let searchOpened = state.searchState.showSearchDrawer
Orientation.singleton.startMeasurements() Orientation.singleton.startMeasurements()
state.selectedElement.addCallback((selected) => { state.selectedElement.addCallback((selected) => {
@ -73,6 +93,8 @@
}) })
}) })
state.mapProperties.installCustomKeyboardHandler(viewport)
let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) => { let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) => {
if (element.properties.id.startsWith("current_view")) { if (element.properties.id.startsWith("current_view")) {
@ -80,21 +102,35 @@
} }
return state.getMatchingLayer(element.properties) return state.getMatchingLayer(element.properties)
}) })
let currentZoom = state.mapProperties.zoom
let showCrosshair = state.userRelatedState.showCrosshair
let visualFeedback = state.visualFeedback
let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined)
let mapproperties: MapProperties = state.mapProperties
state.mapProperties.installCustomKeyboardHandler(viewport)
let canZoomIn = mapproperties.maxzoom.map( let canZoomIn = mapproperties.maxzoom.map(
(mz) => mapproperties.zoom.data < mz, (mz) => mapproperties.zoom.data < mz,
[mapproperties.zoom], [mapproperties.zoom]
) )
let canZoomOut = mapproperties.minzoom.map( let canZoomOut = mapproperties.minzoom.map(
(mz) => mapproperties.zoom.data > mz, (mz) => mapproperties.zoom.data > mz,
[mapproperties.zoom], [mapproperties.zoom]
) )
let rasterLayerName =
rasterLayer.data?.properties?.name ??
AvailableRasterLayers.defaultBackgroundLayer.properties.name
onDestroy(
rasterLayer.addCallbackAndRunD((l) => {
rasterLayerName = l.properties.name
})
)
debug.addCallbackAndRun((dbg) => {
if (dbg) {
document.body.classList.add("debug")
} else {
document.body.classList.remove("debug")
}
})
function updateViewport() { function updateViewport() {
const rect = viewport.data?.getBoundingClientRect() const rect = viewport.data?.getBoundingClientRect()
if (!rect) { if (!rect) {
@ -108,7 +144,7 @@
const bottomRight = mlmap.unproject([rect.right, rect.bottom]) const bottomRight = mlmap.unproject([rect.right, rect.bottom])
const bbox = new BBox([ const bbox = new BBox([
[topLeft.lng, topLeft.lat], [topLeft.lng, topLeft.lat],
[bottomRight.lng, bottomRight.lat], [bottomRight.lng, bottomRight.lat]
]) ])
state.visualFeedbackViewportBounds.setData(bbox) state.visualFeedbackViewportBounds.setData(bbox)
} }
@ -119,31 +155,6 @@
mapproperties.bounds.addCallbackAndRunD(() => { mapproperties.bounds.addCallbackAndRunD(() => {
updateViewport() updateViewport()
}) })
let featureSwitches: FeatureSwitchState = state.featureSwitches
let currentViewLayer: LayerConfig = layout.layers.find((l) => l.id === "current_view")
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer
let rasterLayerName =
rasterLayer.data?.properties?.name ??
AvailableRasterLayers.defaultBackgroundLayer.properties.name
onDestroy(
rasterLayer.addCallbackAndRunD((l) => {
rasterLayerName = l.properties.name
}),
)
let previewedImage = state.previewedImage
let addNewFeatureMode = state.userRelatedState.addNewFeatureMode
let gpsAvailable = state.geolocation.geolocationState.gpsAvailable
let gpsButtonAriaLabel = state.geolocation.geolocationState.gpsStateExplanation
let debug = state.featureSwitches.featureSwitchIsDebugging
debug.addCallbackAndRun((dbg) => {
if (dbg) {
document.body.classList.add("debug")
} else {
document.body.classList.remove("debug")
}
})
function forwardEventToMap(e: KeyboardEvent) { function forwardEventToMap(e: KeyboardEvent) {
const mlmap = state.map.data const mlmap = state.map.data
@ -157,7 +168,6 @@
animation?.cameraAnimation(mlmap) animation?.cameraAnimation(mlmap)
} }
let hash = Hash.hash
</script> </script>
<main> <main>
@ -303,20 +313,11 @@
<DrawerRight shown={state.searchState.showSearchDrawer}> <DrawerRight shown={state.searchState.showSearchDrawer}>
<div class="relative"> <SearchResults {state} />
<div class="absolute right-0 top-0 ">
<div class="mr-4 mt-4">
<CloseButton on:click={() => state.searchState.showSearchDrawer.set(false)} />
</div>
</div>
<SearchResults {state} />
</div>
</DrawerRight> </DrawerRight>
<!-- Top components -->
<!-- Top components -->
<div class="pointer-events-none absolute top-0 left-0 w-full"> <div class="pointer-events-none absolute top-0 left-0 w-full">
<div <div
@ -356,9 +357,22 @@
{/if} {/if}
<If condition={state.featureSwitches.featureSwitchSearch}> <If condition={state.featureSwitches.featureSwitchSearch}>
<div class="w-full sm:w-64"> <div class="flex items-center">
<Searchbar value={state.searchState.searchTerm} isFocused={state.searchState.searchIsFocused}/> <div class="w-full sm:w-64">
<Searchbar value={state.searchState.searchTerm} isFocused={state.searchState.searchIsFocused} />
</div>
<MapControlButton on:keydown={forwardEventToMap} on:click={() =>{
if(searchOpened.data){
searchOpened.set(false)
}else{
state.searchState.searchIsFocused.set(true)
}
}}>
<ChevronRight class="w-7 h-7 p-0 m-0 transition-all"
style={"rotate: " + ($searchOpened ? "0deg" : "180deg" ) } />
</MapControlButton>
</div> </div>
</If> </If>
</div> </div>

View file

@ -1287,6 +1287,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return withDistance.map((n) => n[0]) return withDistance.map((n) => n[0])
} }
public static levenshteinDistance(str1: string, str2: string): number { public static levenshteinDistance(str1: string, str2: string): number {
const track: number[][] = Array(str2.length + 1) const track: number[][] = Array(str2.length + 1)
.fill(null) .fill(null)
@ -1437,6 +1438,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return d return d
} }
public static asRecord<K extends string | number | symbol, V>(keys: K[], f: ((k: K) => V)): Record<K, V> {
const results = <Record<K, V>> {}
for (const key of keys) {
results[key] = f(key)
}
return results
}
static toIdRecord<T extends { id: string }>(ts: T[]): Record<string, T> { static toIdRecord<T extends { id: string }>(ts: T[]): Record<string, T> {
const result: Record<string, T> = {} const result: Record<string, T> = {}
for (const t of ts) { for (const t of ts) {
@ -1781,7 +1790,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static NoNullInplace<T>(items: T[]): T[] { public static NoNullInplace<T>(items: T[]): T[] {
for (let i = items.length - 1; i >= 0; i--) { for (let i = items.length - 1; i >= 0; i--) {
if (items[i] === null || items[i] === undefined) { if (items[i] === null || items[i] === undefined || items[i] === "") {
items.splice(i, 1) items.splice(i, 1)
} }
} }