forked from MapComplete/MapComplete
Search: refactoring searching for themes, refactor allThemesGui, incidentally fix #1679
This commit is contained in:
parent
9b8c300e77
commit
d90b6d82d0
18 changed files with 421 additions and 334 deletions
|
@ -4,14 +4,14 @@
|
|||
"en": "Changes made with MapComplete",
|
||||
"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": {
|
||||
"en": "Shows changes made by MapComplete",
|
||||
"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",
|
||||
"hideFromOverview": true,
|
||||
"startLat": 0,
|
||||
|
|
|
@ -1413,11 +1413,6 @@ input[type="range"].range-lg::-moz-range-thumb {
|
|||
margin-right: auto;
|
||||
}
|
||||
|
||||
.mx-3 {
|
||||
margin-left: 0.75rem;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.my-4 {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
@ -1511,8 +1506,8 @@ input[type="range"].range-lg::-moz-range-thumb {
|
|||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.ml-4 {
|
||||
margin-left: 1rem;
|
||||
.mr-3 {
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
|
@ -1531,6 +1526,10 @@ input[type="range"].range-lg::-moz-range-thumb {
|
|||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.ml-4 {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.mt-8 {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
@ -3913,6 +3912,11 @@ input[type="range"].range-lg::-moz-range-thumb {
|
|||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.px-0 {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.px-3 {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
|
@ -3978,11 +3982,6 @@ input[type="range"].range-lg::-moz-range-thumb {
|
|||
padding-bottom: 0.875rem;
|
||||
}
|
||||
|
||||
.px-0 {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.\!px-0 {
|
||||
padding-left: 0px !important;
|
||||
padding-right: 0px !important;
|
||||
|
@ -4005,10 +4004,6 @@ input[type="range"].range-lg::-moz-range-thumb {
|
|||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.pl-1 {
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
|
||||
.pr-1 {
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
@ -4037,6 +4032,10 @@ input[type="range"].range-lg::-moz-range-thumb {
|
|||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.pl-1 {
|
||||
padding-left: 0.25rem;
|
||||
}
|
||||
|
||||
.pb-1\.5 {
|
||||
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));
|
||||
}
|
||||
|
||||
.accent-gray-600 {
|
||||
accent-color: #4B5563;
|
||||
}
|
||||
|
||||
.opacity-50 {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
@ -8113,10 +8116,6 @@ svg.apply-fill path {
|
|||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.sm\:mt-0 {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.sm\:mt-2 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
|
|
@ -29,13 +29,12 @@ import LayerConfig from "../src/Models/ThemeConfig/LayerConfig"
|
|||
import PointRenderingConfig from "../src/Models/ThemeConfig/PointRenderingConfig"
|
||||
import { ConversionContext } from "../src/Models/ThemeConfig/Conversion/ConversionContext"
|
||||
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 { Translatable } from "../src/Models/ThemeConfig/Json/Translatable"
|
||||
import { ValidateThemeAndLayers } from "../src/Models/ThemeConfig/Conversion/ValidateThemeAndLayers"
|
||||
import { ExtractImages } from "../src/Models/ThemeConfig/Conversion/FixImages"
|
||||
import {
|
||||
MinimalTagRenderingConfigJson,
|
||||
TagRenderingConfigJson,
|
||||
} from "../src/Models/ThemeConfig/Json/TagRenderingConfigJson"
|
||||
|
||||
|
@ -189,7 +188,7 @@ class LayerOverviewUtils extends Script {
|
|||
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
|
||||
}
|
||||
|
||||
|
@ -212,25 +211,17 @@ class LayerOverviewUtils extends Script {
|
|||
return false
|
||||
}
|
||||
|
||||
writeSmallOverview(
|
||||
themes: {
|
||||
id: string
|
||||
title: any
|
||||
shortDescription: any
|
||||
icon: string
|
||||
hideFromOverview: boolean
|
||||
mustHaveLanguage: boolean
|
||||
layers: (
|
||||
| LayerConfigJson
|
||||
| string
|
||||
| {
|
||||
builtin
|
||||
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]
|
||||
}
|
||||
)[]
|
||||
}[],
|
||||
) {
|
||||
const perId = new Map<string, any>()
|
||||
for (const theme of themes) {
|
||||
}
|
||||
}
|
||||
|
||||
private layerKeywords(l: LayerConfigJson): Record<string, string[]> {
|
||||
const keywords: Record<string, string[]> = {}
|
||||
|
||||
function addWord(language: string, word: string | string[]) {
|
||||
|
@ -243,7 +234,6 @@ class LayerOverviewUtils extends Script {
|
|||
if(!word){
|
||||
return
|
||||
}
|
||||
console.log(language, "--->", word)
|
||||
if (!keywords[language]) {
|
||||
keywords[language] = []
|
||||
}
|
||||
|
@ -261,7 +251,7 @@ class LayerOverviewUtils extends Script {
|
|||
if (tr["render"] !== undefined || tr["mappings"] !== undefined) {
|
||||
tr = <TagRenderingConfigJson>tr
|
||||
addWords(<Translatable>tr.render)
|
||||
for (let mapping of tr.mappings ?? []) {
|
||||
for (const mapping of tr.mappings ?? []) {
|
||||
if (typeof mapping === "string") {
|
||||
addWords(mapping)
|
||||
continue
|
||||
|
@ -274,16 +264,54 @@ class LayerOverviewUtils extends Script {
|
|||
addWord(lang, tr[lang])
|
||||
}
|
||||
}
|
||||
|
||||
for (const layer of theme.layers ?? []) {
|
||||
const l = <LayerConfigJson>layer
|
||||
addWord("*", l.id)
|
||||
addWords(l.title)
|
||||
addWords(l.description)
|
||||
addWords(l.searchTerms)
|
||||
return keywords
|
||||
}
|
||||
|
||||
const data = {
|
||||
writeSmallOverview(
|
||||
themes: {
|
||||
id: string
|
||||
title: Translatable
|
||||
shortDescription: Translatable
|
||||
icon: string
|
||||
hideFromOverview: boolean
|
||||
mustHaveLanguage: boolean
|
||||
layers: (
|
||||
| LayerConfigJson
|
||||
| string
|
||||
| {
|
||||
builtin
|
||||
}
|
||||
)[]
|
||||
}[],
|
||||
sharedLayers: Map<string, LayerConfigJson>
|
||||
) {
|
||||
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) {
|
||||
|
||||
const keywords: Record<string, string[]> = {}
|
||||
for (const layer of theme.layers ?? []) {
|
||||
const l = <LayerConfigJson>layer
|
||||
if(sharedLayers.has(l.id)){
|
||||
continue
|
||||
}
|
||||
if(l.id.startsWith("note_import")){
|
||||
continue
|
||||
}
|
||||
LayerOverviewUtils.mergeKeywords(keywords, this.layerKeywords(l))
|
||||
|
||||
}
|
||||
|
||||
const data = <MinimalLayoutInformation> {
|
||||
id: theme.id,
|
||||
title: theme.title,
|
||||
shortDescription: LayerOverviewUtils.cleanTranslation(theme.shortDescription),
|
||||
|
@ -291,6 +319,7 @@ class LayerOverviewUtils extends Script {
|
|||
hideFromOverview: theme.hideFromOverview,
|
||||
mustHaveLanguage: theme.mustHaveLanguage,
|
||||
keywords,
|
||||
layers: theme.layers.filter(l => sharedLayers.has(l["id"])).map(l => l["id"])
|
||||
}
|
||||
perId.set(theme.id, data)
|
||||
}
|
||||
|
@ -311,7 +340,7 @@ class LayerOverviewUtils extends Script {
|
|||
|
||||
writeFileSync(
|
||||
"./src/assets/generated/theme_overview.json",
|
||||
JSON.stringify(sorted, null, " "),
|
||||
JSON.stringify({ layers: layerKeywords, themes: sorted }, null, " "),
|
||||
{ encoding: "utf8" },
|
||||
)
|
||||
}
|
||||
|
@ -927,7 +956,7 @@ class LayerOverviewUtils extends Script {
|
|||
if (whitelist.size == 0) {
|
||||
this.writeSmallOverview(
|
||||
Array.from(fixed.values()).map((t) => {
|
||||
return {
|
||||
return <any> {
|
||||
...t,
|
||||
hideFromOverview: t.hideFromOverview ?? false,
|
||||
shortDescription:
|
||||
|
@ -935,6 +964,7 @@ class LayerOverviewUtils extends Script {
|
|||
mustHaveLanguage: t.mustHaveLanguage?.length > 0,
|
||||
}
|
||||
}),
|
||||
sharedLayers
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -7,11 +7,9 @@ import Constants from "../../Models/Constants"
|
|||
|
||||
export default class FilterSearch implements GeocodingProvider {
|
||||
private readonly _state: SpecialVisualizationState
|
||||
private readonly suggestions
|
||||
|
||||
constructor(state: SpecialVisualizationState) {
|
||||
this._state = state
|
||||
this.suggestions = this.getSuggestions()
|
||||
}
|
||||
|
||||
async search(query: string): Promise<SearchResult[]> {
|
||||
|
@ -58,7 +56,6 @@ export default class FilterSearch implements GeocodingProvider {
|
|||
Utils.NoNullInplace(terms)
|
||||
const distances = queries.flatMap(query => terms.map(entry => {
|
||||
const d = Utils.levenshteinDistance(query, entry.slice(0, query.length))
|
||||
console.log(query, "? +", terms, "=", d)
|
||||
const dRelative = d / query.length
|
||||
return dRelative
|
||||
}))
|
||||
|
@ -79,10 +76,10 @@ export default class FilterSearch implements GeocodingProvider {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a random list of filters
|
||||
*/
|
||||
getSuggestions(): FilterPayload[] {
|
||||
if (this.suggestions) {
|
||||
// return this.suggestions
|
||||
}
|
||||
const result: FilterPayload[] = []
|
||||
for (const [id, filteredLayer] of this._state.layerState.filteredLayers) {
|
||||
if (!Array.isArray(filteredLayer.layerDef.filters)) {
|
||||
|
|
|
@ -13,7 +13,6 @@ export class RecentSearch {
|
|||
|
||||
constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) {
|
||||
const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches")
|
||||
prefs.set(null)
|
||||
this._seenThisSession = new UIEventSource<GeocodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
|
||||
this.seenThisSession = this._seenThisSession
|
||||
|
||||
|
|
|
@ -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 { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
|
||||
import { Utils } from "../../Utils"
|
||||
import MoreScreen from "../../UI/BigComponents/MoreScreen"
|
||||
import { ImmutableStore, Store } from "../UIEventSource"
|
||||
|
||||
|
@ -12,11 +11,16 @@ export default class ThemeSearch implements GeocodingProvider {
|
|||
private readonly _state: SpecialVisualizationState
|
||||
private readonly _knownHiddenThemes: Store<Set<string>>
|
||||
private readonly _suggestionLimit: number
|
||||
private readonly _layersToIgnore: string[]
|
||||
private readonly _otherThemes: MinimalLayoutInformation[]
|
||||
|
||||
constructor(state: SpecialVisualizationState, suggestionLimit: number) {
|
||||
this._state = state
|
||||
this._layersToIgnore = state.layout.layers.map(l => l.id)
|
||||
this._suggestionLimit = suggestionLimit
|
||||
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[]> {
|
||||
|
@ -40,11 +44,11 @@ export default class ThemeSearch implements GeocodingProvider {
|
|||
if (query.length < 1) {
|
||||
return []
|
||||
}
|
||||
query = Utils.simplifyStringForSearch(query)
|
||||
return ThemeSearch.allThemes
|
||||
const sorted = MoreScreen.sortedByLowest(query, this._otherThemes, this._layersToIgnore)
|
||||
console.log(">>>", sorted)
|
||||
return sorted
|
||||
.map(th => th.theme)
|
||||
.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)
|
||||
}
|
||||
|
||||
|
|
|
@ -154,7 +154,7 @@ export default class SearchState {
|
|||
const poi = result[0]
|
||||
if (poi.category === "theme") {
|
||||
const theme = <MinimalLayoutInformation>poi.payload
|
||||
const url = MoreScreen.createUrlFor(theme, false)
|
||||
const url = MoreScreen.createUrlFor(theme)
|
||||
window.location = <any>url
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import LayoutConfig, { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { OsmConnection } from "../Osm/OsmConnection"
|
||||
import { MangroveIdentity } from "../Web/MangroveReviews"
|
||||
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 = this._recentlyVisitedThemes
|
||||
if (layout) {
|
||||
const osmConn =this.osmConnection
|
||||
const osmConn = this.osmConnection
|
||||
const recentlyVisited = this._recentlyVisitedThemes
|
||||
|
||||
function update() {
|
||||
if (!osmConn.isLoggedIn.data) {
|
||||
return
|
||||
|
@ -203,16 +204,7 @@ export default class UserRelatedState {
|
|||
}
|
||||
}
|
||||
|
||||
public GetUnofficialTheme(id: string):
|
||||
| {
|
||||
id: string
|
||||
icon: string
|
||||
title: any
|
||||
shortDescription: any
|
||||
definition?: any
|
||||
isOfficial: boolean
|
||||
}
|
||||
| undefined {
|
||||
public getUnofficialTheme(id: string): (MinimalLayoutInformation & { definition }) | undefined {
|
||||
const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id)
|
||||
const str = pref.data
|
||||
|
||||
|
@ -222,16 +214,7 @@ export default class UserRelatedState {
|
|||
}
|
||||
|
||||
try {
|
||||
const value: {
|
||||
id: string
|
||||
icon: string
|
||||
title: any
|
||||
shortDescription: any
|
||||
definition?: any
|
||||
isOfficial: boolean
|
||||
} = JSON.parse(str)
|
||||
value.isOfficial = false
|
||||
return value
|
||||
return <MinimalLayoutInformation & { definition: string }>JSON.parse(str)
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"Removing theme " +
|
||||
|
@ -464,7 +447,7 @@ export default class UserRelatedState {
|
|||
}
|
||||
if (tags[key + "-combined-0"]) {
|
||||
// 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
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -22,10 +22,10 @@ export class MinimalLayoutInformation {
|
|||
icon: string
|
||||
title: Translatable
|
||||
shortDescription: Translatable
|
||||
definition?: Translatable
|
||||
mustHaveLanguage?: boolean
|
||||
hideFromOverview?: boolean
|
||||
keywords?: Record<string, string[]>
|
||||
layers: string[]
|
||||
}
|
||||
/**
|
||||
* Minimal information about a theme
|
||||
|
|
|
@ -11,16 +11,11 @@
|
|||
import LoginToggle from "./Base/LoginToggle.svelte"
|
||||
import Pencil from "../assets/svg/Pencil.svelte"
|
||||
import Constants from "../Models/Constants"
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { placeholder } from "../Utils/placeholder"
|
||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { Store, Stores, UIEventSource } from "../Logic/UIEventSource"
|
||||
import ThemesList from "./BigComponents/ThemesList.svelte"
|
||||
import { LayoutInformation } from "../Models/ThemeConfig/LayoutConfig"
|
||||
import * as themeOverview from "../assets/generated/theme_overview.json"
|
||||
import UnofficialThemeList from "./BigComponents/UnofficialThemeList.svelte"
|
||||
import { MinimalLayoutInformation } from "../Models/ThemeConfig/LayoutConfig"
|
||||
import Eye from "../assets/svg/Eye.svelte"
|
||||
import LoginButton from "./Base/LoginButton.svelte"
|
||||
import ChevronDoubleRight from "@babeard/svelte-heroicons/mini/ChevronDoubleRight"
|
||||
import Mastodon from "../assets/svg/Mastodon.svelte"
|
||||
import Liberapay from "../assets/svg/Liberapay.svelte"
|
||||
import Bug from "../assets/svg/Bug.svelte"
|
||||
|
@ -28,6 +23,7 @@
|
|||
import { Utils } from "../Utils"
|
||||
import { ArrowTrendingUp } from "@babeard/svelte-heroicons/solid/ArrowTrendingUp"
|
||||
import Searchbar from "./Base/Searchbar.svelte"
|
||||
import ChevronDoubleRight from "@babeard/svelte-heroicons/mini/ChevronDoubleRight"
|
||||
|
||||
const featureSwitches = new OsmConnectionFeatureSwitches()
|
||||
const osmConnection = new OsmConnection({
|
||||
|
@ -40,27 +36,71 @@
|
|||
})
|
||||
const state = new UserRelatedState(osmConnection)
|
||||
const t = Translations.t.index
|
||||
const tu = Translations.t.general
|
||||
const tr = Translations.t.general.morescreen
|
||||
|
||||
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) {
|
||||
if (event.ctrlKey && event.code === "KeyF") {
|
||||
document.getElementById("theme-search")?.focus()
|
||||
searchIsFocussed.set(true)
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
let visitedHiddenThemes: Store<LayoutInformation[]>
|
||||
const hiddenThemes: LayoutInformation[] =
|
||||
(themeOverview["default"] ?? themeOverview)?.filter((layout) => layout.hideFromOverview) ?? []
|
||||
{
|
||||
visitedHiddenThemes = MoreScreen.knownHiddenThemes(state.osmConnection)
|
||||
.map((knownIds) => hiddenThemes.filter((theme) =>
|
||||
knownIds.has(theme.id) || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet"
|
||||
))
|
||||
function applySearch() {
|
||||
const didRedirect = MoreScreen.applySearch(search.data)
|
||||
console.log("Did redirect?", didRedirect)
|
||||
if (didRedirect) {
|
||||
// Just for style and readability; won't _actually_ reach this
|
||||
return
|
||||
}
|
||||
|
||||
const candidate = officialSearched.data[0] ?? hiddenSearched.data[0] ?? customSearched.data[0]
|
||||
if (!candidate) {
|
||||
return
|
||||
}
|
||||
|
||||
window.location.href = MoreScreen.createUrlFor(candidate)
|
||||
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<main>
|
||||
|
@ -92,20 +132,19 @@
|
|||
</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}>
|
||||
<LoginButton clss="primary" {osmConnection} slot="not-logged-in">
|
||||
<Tr t={t.logIn} />
|
||||
</LoginButton>
|
||||
<ThemesList
|
||||
hideThemes={false}
|
||||
isCustom={false}
|
||||
search={themeSearchText}
|
||||
{search}
|
||||
{state}
|
||||
themes={$visitedHiddenThemes}
|
||||
themes={$hiddenSearched}
|
||||
hasSelection={$officialSearched.length === 0}
|
||||
>
|
||||
<svelte:fragment slot="title">
|
||||
<h3>
|
||||
|
@ -122,7 +161,19 @@
|
|||
</svelte:fragment>
|
||||
</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>
|
||||
|
||||
<a
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import Backspace from "@babeard/svelte-heroicons/outline/Backspace"
|
||||
|
||||
export let value: UIEventSource<string>
|
||||
let _value = value.data ?? ""
|
||||
|
@ -23,8 +24,8 @@
|
|||
if (focussed) {
|
||||
requestAnimationFrame(() => {
|
||||
if (document.activeElement !== inputElement) {
|
||||
inputElement.focus()
|
||||
inputElement.select()
|
||||
inputElement?.focus()
|
||||
inputElement?.select()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -38,15 +39,17 @@
|
|||
on:submit|preventDefault={() => dispatch("search")}
|
||||
>
|
||||
<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
|
||||
bind:this={inputElement}
|
||||
on:focus={() => {isFocused?.setData(true)}}
|
||||
on:blur={() => {isFocused?.setData(false)}}
|
||||
type="search"
|
||||
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) => {
|
||||
return keypr.key === "Enter" ? dispatch("search") : undefined
|
||||
}}
|
||||
|
@ -54,7 +57,11 @@
|
|||
use:set_placeholder={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>
|
||||
</form>
|
||||
|
|
|
@ -3,26 +3,37 @@ import { Store } from "../../Logic/UIEventSource"
|
|||
import { Utils } from "../../Utils"
|
||||
import themeOverview from "../../assets/generated/theme_overview.json"
|
||||
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"
|
||||
|
||||
export type ThemeSearchScore = {
|
||||
theme: MinimalLayoutInformation,
|
||||
lowest: number,
|
||||
perLayer?: Record<string, number>,
|
||||
other: number
|
||||
}
|
||||
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>()
|
||||
static {
|
||||
for (const th of MoreScreen.officialThemes) {
|
||||
for (const th of MoreScreen.officialThemes.themes) {
|
||||
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()
|
||||
if (!searchTerm) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
if (searchTerm === "personal") {
|
||||
window.location.href = MoreScreen.createUrlFor({ id: "personal" }, false)
|
||||
window.location.href = MoreScreen.createUrlFor({ id: "personal" })
|
||||
}
|
||||
if (searchTerm === "bugs" || searchTerm === "issues") {
|
||||
window.location.href = "https://github.com/pietervdvn/MapComplete/issues"
|
||||
|
@ -39,77 +50,110 @@ export default class MoreScreen {
|
|||
if (searchTerm === "studio") {
|
||||
window.location.href = "./studio.html"
|
||||
}
|
||||
// Enter pressed -> search the first _official_ matchin theme and open it
|
||||
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)
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
public static MatchesLayout(
|
||||
layout: MinimalLayoutInformation,
|
||||
search: string,
|
||||
language?: string,
|
||||
): boolean {
|
||||
if (search === undefined) {
|
||||
return true
|
||||
}
|
||||
search = Utils.simplifyStringForSearch(search.toLocaleLowerCase()) // See #1729
|
||||
if (search.length > 3 && layout.id.toLowerCase().indexOf(search) >= 0) {
|
||||
return true
|
||||
}
|
||||
if (layout.id === "personal") {
|
||||
return false
|
||||
}
|
||||
if (Utils.simplifyStringForSearch(layout.id) === Utils.simplifyStringForSearch(search)) {
|
||||
return true
|
||||
/**
|
||||
* Searches for the smallest distance in words; will split both the query and the terms
|
||||
*
|
||||
* MoreScreen.scoreKeywords("drinking water", {"en": ["A layer with drinking water points"]}, "en") // => 0
|
||||
* MoreScreen.scoreKeywords("waste", {"en": ["A layer with drinking water points"]}, "en") // => 2
|
||||
*
|
||||
*/
|
||||
public static scoreKeywords(query: string, keywords: Record<string, string[]> | string[], language?: string): number {
|
||||
if(!keywords){
|
||||
return Infinity
|
||||
}
|
||||
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]
|
||||
for (const entity of entitiesToSearch) {
|
||||
if (entity === undefined) {
|
||||
let distanceSummed = 0
|
||||
for (let i = 0; i < queryParts.length; i++) {
|
||||
const q = queryParts[i]
|
||||
let minDistance: number = 99
|
||||
for (const term of termsAll) {
|
||||
const d = Utils.levenshteinDistance(q, Utils.simplifyStringForSearch(term))
|
||||
if (d < minDistance) {
|
||||
minDistance = d
|
||||
}
|
||||
}
|
||||
distanceSummed += minDistance
|
||||
}
|
||||
return distanceSummed
|
||||
}
|
||||
|
||||
public static scoreLayers(query: string): Record<string, number> {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
let term: string[]
|
||||
if (typeof entity === "string") {
|
||||
term = [entity]
|
||||
} else {
|
||||
const terms = [].concat(entity["*"], entity[language])
|
||||
if (Array.isArray(terms)) {
|
||||
term = terms
|
||||
} else {
|
||||
term = [terms]
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
const minLevehnstein = Math.min(...Utils.NoNull(term).map(t => Utils.levenshteinDistance(search,
|
||||
Utils.simplifyStringForSearch(t).slice(0, search.length))))
|
||||
|
||||
if (minLevehnstein < 1 || minLevehnstein / search.length < 0.2) {
|
||||
return true
|
||||
}
|
||||
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(
|
||||
layout: { id: string },
|
||||
isCustom: boolean,
|
||||
state?: { layoutToUse?: { id } },
|
||||
state?: { layoutToUse?: { id } }
|
||||
): string {
|
||||
if (layout === undefined) {
|
||||
return undefined
|
||||
|
@ -136,7 +180,7 @@ export default class MoreScreen {
|
|||
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}&`
|
||||
}
|
||||
|
||||
|
@ -155,7 +199,7 @@ export default class MoreScreen {
|
|||
new Set<string>(
|
||||
Object.keys(preferences)
|
||||
.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))
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,14 @@
|
|||
<script lang="ts">
|
||||
import * as personal from "../../../assets/themes/personal/personal.json"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
|
||||
import Marker from "../Map/Marker.svelte"
|
||||
|
||||
export let theme: MinimalLayoutInformation
|
||||
export let isCustom: boolean = false
|
||||
export let userDetails: UIEventSource<UserDetails>
|
||||
export let theme: MinimalLayoutInformation & {isOfficial?: boolean}
|
||||
let isCustom: boolean = theme.id.startsWith("https://") || theme.id.startsWith("http://")
|
||||
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(
|
||||
theme.title,
|
||||
|
@ -33,7 +16,6 @@
|
|||
)
|
||||
$: description = Translations.T(theme.shortDescription)
|
||||
|
||||
// TODO: Improve this function
|
||||
function createUrl(
|
||||
layout: { id: string; definition?: string },
|
||||
isCustom: boolean,
|
||||
|
@ -84,19 +66,12 @@
|
|||
let href = createUrl(theme, isCustom, state)
|
||||
</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}>
|
||||
<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">
|
||||
<Tr cls="" t={title} />
|
||||
<Tr cls="subtle text-base" t={description} />
|
||||
|
||||
{#if selected}
|
||||
<span class="thanks hidden-on-mobile" aria-hidden="true">
|
||||
<Tr t={Translations.t.general.morescreen.enterToOpen} />
|
||||
</span>
|
||||
{/if}
|
||||
<slot/>
|
||||
</span>
|
||||
</a>
|
||||
{/if}
|
||||
|
|
|
@ -4,46 +4,36 @@
|
|||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import ThemeButton from "./ThemeButton.svelte"
|
||||
import { LayoutInformation, MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import MoreScreen from "./MoreScreen"
|
||||
import themeOverview from "../../assets/generated/theme_overview.json"
|
||||
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
||||
export let search: UIEventSource<string>
|
||||
export let themes: MinimalLayoutInformation[]
|
||||
export let state: { osmConnection: OsmConnection }
|
||||
export let isCustom: boolean = false
|
||||
export let hideThemes: boolean = true
|
||||
|
||||
// Filter theme based on search value
|
||||
$: filteredThemes = themes.filter((theme) => MoreScreen.MatchesLayout(theme, $search))
|
||||
export let hasSelection : boolean = true
|
||||
|
||||
// Determine which is the first theme, after the search, using all themes
|
||||
$: allFilteredThemes = themeOverview.filter((theme) => MoreScreen.MatchesLayout(theme, $search))
|
||||
$: firstTheme = allFilteredThemes[0]
|
||||
</script>
|
||||
|
||||
<section class="w-full">
|
||||
<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">
|
||||
{#each filteredThemes as theme (theme.id)}
|
||||
{#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)}
|
||||
<!-- TODO: doesn't work if first theme is hidden -->
|
||||
{#if theme === firstTheme && !isCustom && $search !== "" && $search !== undefined}
|
||||
{#each themes as theme (theme.id)}
|
||||
<ThemeButton
|
||||
{theme}
|
||||
{isCustom}
|
||||
userDetails={state.osmConnection.userDetails}
|
||||
{state}
|
||||
selected={true}
|
||||
/>
|
||||
{:else}
|
||||
<ThemeButton {theme} {isCustom} userDetails={state.osmConnection.userDetails} {state} />
|
||||
{/if}
|
||||
>
|
||||
{#if $search && hasSelection && themes[0] === theme}
|
||||
<span class="thanks hidden-on-mobile" aria-hidden="true">
|
||||
<Tr t={Translations.t.general.morescreen.enterToOpen} />
|
||||
</span>
|
||||
{/if}
|
||||
</ThemeButton>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if filteredThemes.length === 0}
|
||||
{#if themes.length === 0}
|
||||
<NoThemeResultButton {search} />
|
||||
{/if}
|
||||
</section>
|
||||
|
|
|
@ -12,21 +12,8 @@
|
|||
osmConnection: OsmConnection
|
||||
}
|
||||
|
||||
const t = Translations.t.general
|
||||
const currentIds: Store<string[]> = state.installedUserThemes
|
||||
const stableIds = Stores.ListStabilized<string>(currentIds)
|
||||
|
||||
let customThemes
|
||||
$: customThemes = Utils.NoNull($stableIds.map((id) => state.GetUnofficialTheme(id)))
|
||||
$: console.log("Custom themes are", customThemes)
|
||||
</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}
|
||||
|
||||
|
|
|
@ -29,8 +29,6 @@
|
|||
</script>
|
||||
<div class="p-4 low-interaction flex gap-y-2 flex-col">
|
||||
|
||||
<h3>Search results</h3>
|
||||
|
||||
<ActiveFilters activeFilters={$activeFilters} />
|
||||
|
||||
{#if $searchTerm.length > 0 && $filterResults.length > 0}
|
||||
|
@ -79,7 +77,7 @@
|
|||
<h3>
|
||||
Other maps
|
||||
</h3>
|
||||
{#each $themeResults as entry}
|
||||
{#each $themeResults as entry (entry.id)}
|
||||
<ThemeResult {entry} />
|
||||
{/each}
|
||||
</SidebarUnit>
|
||||
|
|
|
@ -47,14 +47,34 @@
|
|||
import { CloseButton } from "flowbite-svelte"
|
||||
import Hash from "../Logic/Web/Hash"
|
||||
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
|
||||
|
||||
|
||||
let layout = state.layout
|
||||
let maplibremap: UIEventSource<MlMap> = state.map
|
||||
let state_selectedElement = state.selectedElement
|
||||
let selectedElement: UIEventSource<Feature> = new UIEventSource<Feature>(undefined)
|
||||
let compass = Orientation.singleton.alpha
|
||||
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()
|
||||
|
||||
state.selectedElement.addCallback((selected) => {
|
||||
|
@ -73,6 +93,8 @@
|
|||
})
|
||||
})
|
||||
|
||||
state.mapProperties.installCustomKeyboardHandler(viewport)
|
||||
|
||||
|
||||
let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) => {
|
||||
if (element.properties.id.startsWith("current_view")) {
|
||||
|
@ -80,21 +102,35 @@
|
|||
}
|
||||
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(
|
||||
(mz) => mapproperties.zoom.data < mz,
|
||||
[mapproperties.zoom],
|
||||
[mapproperties.zoom]
|
||||
)
|
||||
let canZoomOut = mapproperties.minzoom.map(
|
||||
(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() {
|
||||
const rect = viewport.data?.getBoundingClientRect()
|
||||
if (!rect) {
|
||||
|
@ -108,7 +144,7 @@
|
|||
const bottomRight = mlmap.unproject([rect.right, rect.bottom])
|
||||
const bbox = new BBox([
|
||||
[topLeft.lng, topLeft.lat],
|
||||
[bottomRight.lng, bottomRight.lat],
|
||||
[bottomRight.lng, bottomRight.lat]
|
||||
])
|
||||
state.visualFeedbackViewportBounds.setData(bbox)
|
||||
}
|
||||
|
@ -119,31 +155,6 @@
|
|||
mapproperties.bounds.addCallbackAndRunD(() => {
|
||||
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) {
|
||||
const mlmap = state.map.data
|
||||
|
@ -157,7 +168,6 @@
|
|||
animation?.cameraAnimation(mlmap)
|
||||
}
|
||||
|
||||
let hash = Hash.hash
|
||||
</script>
|
||||
|
||||
<main>
|
||||
|
@ -303,19 +313,10 @@
|
|||
|
||||
|
||||
<DrawerRight shown={state.searchState.showSearchDrawer}>
|
||||
<div class="relative">
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Top components -->
|
||||
<div class="pointer-events-none absolute top-0 left-0 w-full">
|
||||
|
||||
|
@ -356,9 +357,22 @@
|
|||
{/if}
|
||||
|
||||
<If condition={state.featureSwitches.featureSwitchSearch}>
|
||||
<div class="flex items-center">
|
||||
<div class="w-full sm:w-64">
|
||||
<Searchbar value={state.searchState.searchTerm} isFocused={state.searchState.searchIsFocused}/>
|
||||
<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>
|
||||
|
||||
</If>
|
||||
|
||||
</div>
|
||||
|
|
11
src/Utils.ts
11
src/Utils.ts
|
@ -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])
|
||||
}
|
||||
|
||||
|
||||
public static levenshteinDistance(str1: string, str2: string): number {
|
||||
const track: number[][] = Array(str2.length + 1)
|
||||
.fill(null)
|
||||
|
@ -1437,6 +1438,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
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> {
|
||||
const result: Record<string, T> = {}
|
||||
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[] {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue