Add themes to search functionality, including quickswitch between recent themes
This commit is contained in:
parent
b4866cdbac
commit
329865a15e
22 changed files with 679 additions and 431 deletions
|
@ -393,6 +393,9 @@
|
||||||
"search": {
|
"search": {
|
||||||
"error": "Something went wrong…",
|
"error": "Something went wrong…",
|
||||||
"nothing": "Nothing found…",
|
"nothing": "Nothing found…",
|
||||||
|
"nothingFor": "No results found for {term}",
|
||||||
|
"recentThemes": "Recently visited maps",
|
||||||
|
"recents": "Recent searches",
|
||||||
"search": "Search a location",
|
"search": "Search a location",
|
||||||
"searchShort": "Search…",
|
"searchShort": "Search…",
|
||||||
"searching": "Searching…"
|
"searching": "Searching…"
|
||||||
|
|
|
@ -2558,11 +2558,6 @@ video {
|
||||||
border-color: rgb(209 213 219 / var(--tw-border-opacity));
|
border-color: rgb(209 213 219 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-red-500 {
|
|
||||||
--tw-border-opacity: 1;
|
|
||||||
border-color: rgb(239 68 68 / var(--tw-border-opacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-gray-800 {
|
.border-gray-800 {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(31 41 55 / var(--tw-border-opacity));
|
border-color: rgb(31 41 55 / var(--tw-border-opacity));
|
||||||
|
@ -2658,6 +2653,11 @@ video {
|
||||||
border-color: rgb(34 197 94 / var(--tw-border-opacity));
|
border-color: rgb(34 197 94 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.border-red-500 {
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(239 68 68 / var(--tw-border-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
.border-gray-700 {
|
.border-gray-700 {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(55 65 81 / var(--tw-border-opacity));
|
border-color: rgb(55 65 81 / var(--tw-border-opacity));
|
||||||
|
@ -4423,10 +4423,10 @@ video {
|
||||||
--svg-color: #000000;
|
--svg-color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face{
|
@font-face {
|
||||||
font-family:"Source Sans Pro";
|
font-family: "Source Sans Pro";
|
||||||
|
|
||||||
src:url("/assets/source-sans-pro.regular.ttf") format("woff");
|
src: url("/assets/source-sans-pro.regular.ttf") format("woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
/***********************************************************************\
|
/***********************************************************************\
|
||||||
|
@ -4674,7 +4674,7 @@ label:not(.neutral-label):not(.button) {
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
margin:0.25rem;
|
margin: 0.25rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -4887,6 +4887,10 @@ a.link-underline {
|
||||||
color: unset !important;
|
color: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
background-color: var(--low-interaction-background);
|
||||||
|
}
|
||||||
|
|
||||||
.disable-links a.must-link,
|
.disable-links a.must-link,
|
||||||
.disable-links .must-link a {
|
.disable-links .must-link a {
|
||||||
/* Hide links if they are disabled */
|
/* Hide links if they are disabled */
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
|
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||||
|
import { Utils } from "../../Utils"
|
||||||
|
|
||||||
export default class CombinedSearcher implements GeocodingProvider {
|
export default class CombinedSearcher implements GeocodingProvider {
|
||||||
private _providers: ReadonlyArray<GeocodingProvider>
|
private _providers: ReadonlyArray<GeocodingProvider>
|
||||||
private _providersWithSuggest: ReadonlyArray<GeocodingProvider>
|
private _providersWithSuggest: ReadonlyArray<GeocodingProvider>
|
||||||
|
|
||||||
constructor(...providers: ReadonlyArray<GeocodingProvider>) {
|
constructor(...providers: ReadonlyArray<GeocodingProvider>) {
|
||||||
this._providers = providers
|
this._providers = Utils.NoNull(providers)
|
||||||
this._providersWithSuggest = providers.filter(pr => pr.suggest !== undefined)
|
this._providersWithSuggest = this._providers.filter(pr => pr.suggest !== undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -24,7 +24,7 @@ export type GeoCodeResult = {
|
||||||
osm_type?: "node" | "way" | "relation"
|
osm_type?: "node" | "way" | "relation"
|
||||||
osm_id?: string,
|
osm_id?: string,
|
||||||
category?: GeocodingCategory,
|
category?: GeocodingCategory,
|
||||||
importance?: number
|
payload?: object
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GeocodingOptions {
|
export interface GeocodingOptions {
|
||||||
|
|
|
@ -7,21 +7,20 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||||
|
|
||||||
export class RecentSearch {
|
export class RecentSearch {
|
||||||
|
|
||||||
private readonly _recentSearches: UIEventSource<string[]>
|
|
||||||
public readonly recentSearches: Store<string[]>
|
|
||||||
|
|
||||||
private readonly _seenThisSession: UIEventSource<GeoCodeResult[]> = new UIEventSource<GeoCodeResult[]>([])
|
private readonly _seenThisSession: UIEventSource<GeoCodeResult[]>
|
||||||
public readonly seenThisSession: Store<GeoCodeResult[]> = this._seenThisSession
|
public readonly seenThisSession: Store<GeoCodeResult[]>
|
||||||
|
|
||||||
constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) {
|
constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) {
|
||||||
const longPref = state.osmConnection.preferencesHandler.GetLongPreference("recent-searches")
|
// const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches")
|
||||||
this._recentSearches = longPref.sync(str => !str ? [] : <string[]>JSON.parse(str), [], strs => JSON.stringify(strs))
|
this._seenThisSession = new UIEventSource<GeoCodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
|
||||||
this.recentSearches = this._recentSearches
|
this.seenThisSession = this._seenThisSession
|
||||||
|
|
||||||
|
|
||||||
state.selectedElement.addCallbackAndRunD(selected => {
|
state.selectedElement.addCallbackAndRunD(selected => {
|
||||||
const [osm_type, osm_id] = selected.properties.id.split("/")
|
const [osm_type, osm_id] = selected.properties.id.split("/")
|
||||||
const [lon, lat] = GeoOperations.centerpointCoordinates(selected)
|
const [lon, lat] = GeoOperations.centerpointCoordinates(selected)
|
||||||
const entry = <GeoCodeResult> {
|
const entry = <GeoCodeResult>{
|
||||||
feature: selected,
|
feature: selected,
|
||||||
osm_id, osm_type,
|
osm_id, osm_type,
|
||||||
description: "Viewed recently",
|
description: "Viewed recently",
|
||||||
|
@ -33,7 +32,7 @@ export class RecentSearch {
|
||||||
}
|
}
|
||||||
|
|
||||||
addSelected(entry: GeoCodeResult) {
|
addSelected(entry: GeoCodeResult) {
|
||||||
const arr = [...this.seenThisSession.data.slice(0, 20), entry]
|
const arr = [...(this.seenThisSession.data ?? []).slice(0, 20), entry]
|
||||||
|
|
||||||
const seenIds = new Set<string>()
|
const seenIds = new Set<string>()
|
||||||
for (let i = arr.length - 1; i >= 0; i--) {
|
for (let i = arr.length - 1; i >= 0; i--) {
|
||||||
|
|
43
src/Logic/Geocoding/ThemeSearch.ts
Normal file
43
src/Logic/Geocoding/ThemeSearch.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } 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 { Store } from "../UIEventSource"
|
||||||
|
|
||||||
|
export default class ThemeSearch implements GeocodingProvider {
|
||||||
|
|
||||||
|
private static allThemes: MinimalLayoutInformation[] = (themeOverview["default"] ?? themeOverview)
|
||||||
|
private readonly _state: SpecialVisualizationState
|
||||||
|
private readonly _knownHiddenThemes: Store<Set<string>>
|
||||||
|
|
||||||
|
constructor(state: SpecialVisualizationState) {
|
||||||
|
this._state = state
|
||||||
|
this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection)
|
||||||
|
}
|
||||||
|
|
||||||
|
search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||||
|
return this.suggest(query, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
async suggest?(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||||
|
if(query.length < 1){
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const limit = options?.limit ?? 4
|
||||||
|
query = Utils.simplifyStringForSearch(query)
|
||||||
|
const withMatch = ThemeSearch.allThemes
|
||||||
|
.filter(th => !th.hideFromOverview )
|
||||||
|
.filter(th => th.id !== this._state.layout.id)
|
||||||
|
.filter(th => MoreScreen.MatchesLayout(th, query))
|
||||||
|
.slice(0, limit + 1)
|
||||||
|
|
||||||
|
return withMatch.map(match => (<GeoCodeResult> {
|
||||||
|
payload: match,
|
||||||
|
osm_id: match.id
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -71,7 +71,7 @@ export class OsmPreferences {
|
||||||
}
|
}
|
||||||
if (str === null) {
|
if (str === null) {
|
||||||
console.error("Deleting " + allStartWith)
|
console.error("Deleting " + allStartWith)
|
||||||
let count = parseInt(length.data)
|
const count = parseInt(length.data)
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
// Delete all the preferences
|
// Delete all the preferences
|
||||||
self.GetPreference(allStartWith + "-" + i, "", subOptions).setData("")
|
self.GetPreference(allStartWith + "-" + i, "", subOptions).setData("")
|
||||||
|
|
|
@ -78,6 +78,10 @@ export default class UserRelatedState {
|
||||||
public readonly preferencesAsTags: UIEventSource<Record<string, string>>
|
public readonly preferencesAsTags: UIEventSource<Record<string, string>>
|
||||||
private readonly _mapProperties: MapProperties
|
private readonly _mapProperties: MapProperties
|
||||||
|
|
||||||
|
private readonly _recentlyVisitedThemes: UIEventSource<string[]>
|
||||||
|
public readonly recentlyVisitedThemes: Store<string[]>
|
||||||
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
osmConnection: OsmConnection,
|
osmConnection: OsmConnection,
|
||||||
layout?: LayoutConfig,
|
layout?: LayoutConfig,
|
||||||
|
@ -109,7 +113,7 @@ export default class UserRelatedState {
|
||||||
this.showAllQuestionsAtOnce = UIEventSource.asBoolean(
|
this.showAllQuestionsAtOnce = UIEventSource.asBoolean(
|
||||||
this.osmConnection.GetPreference("show-all-questions", "false", {
|
this.osmConnection.GetPreference("show-all-questions", "false", {
|
||||||
documentation:
|
documentation:
|
||||||
"Either 'true' or 'false'. If set, all questions will be shown all at once",
|
"Either 'true' or 'false'. If set, all questions will be shown all at once"
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
this.language = this.osmConnection.GetPreference("language")
|
this.language = this.osmConnection.GetPreference("language")
|
||||||
|
@ -129,7 +133,7 @@ export default class UserRelatedState {
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
documentation:
|
documentation:
|
||||||
"The ID of a layer or layer category that MapComplete uses by default",
|
"The ID of a layer or layer category that MapComplete uses by default"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -137,12 +141,12 @@ export default class UserRelatedState {
|
||||||
"preferences-add-new-mode",
|
"preferences-add-new-mode",
|
||||||
"button_click_right",
|
"button_click_right",
|
||||||
{
|
{
|
||||||
documentation: "How adding a new feature is done",
|
documentation: "How adding a new feature is done"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0", {
|
this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0", {
|
||||||
documentation: "The license under which new images are uploaded",
|
documentation: "The license under which new images are uploaded"
|
||||||
})
|
})
|
||||||
this.installedUserThemes = this.InitInstalledUserThemes()
|
this.installedUserThemes = this.InitInstalledUserThemes()
|
||||||
|
|
||||||
|
@ -150,6 +154,30 @@ export default class UserRelatedState {
|
||||||
|
|
||||||
this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches)
|
this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches)
|
||||||
|
|
||||||
|
const prefs = this.osmConnection
|
||||||
|
this._recentlyVisitedThemes = UIEventSource.asObject(prefs.GetLongPreference("recently-visited-themes"), [])
|
||||||
|
this.recentlyVisitedThemes = this._recentlyVisitedThemes
|
||||||
|
if (layout) {
|
||||||
|
const osmConn =this.osmConnection
|
||||||
|
const recentlyVisited = this._recentlyVisitedThemes
|
||||||
|
function update() {
|
||||||
|
if (!osmConn.isLoggedIn.data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const previously = recentlyVisited.data
|
||||||
|
if (previously[0] === layout.id) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const newThemes = Utils.Dedup([layout.id, ...previously]).slice(0, 30)
|
||||||
|
recentlyVisited.set(newThemes)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this._recentlyVisitedThemes.addCallbackAndRun(() => update())
|
||||||
|
this.osmConnection.isLoggedIn.addCallbackAndRun(() => update())
|
||||||
|
}
|
||||||
|
|
||||||
this.syncLanguage()
|
this.syncLanguage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -233,7 +261,7 @@ export default class UserRelatedState {
|
||||||
icon: layout.icon,
|
icon: layout.icon,
|
||||||
title: layout.title.translations,
|
title: layout.title.translations,
|
||||||
shortDescription: layout.shortDescription.translations,
|
shortDescription: layout.shortDescription.translations,
|
||||||
definition: layout["definition"],
|
definition: layout["definition"]
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -273,13 +301,13 @@ export default class UserRelatedState {
|
||||||
id: "home",
|
id: "home",
|
||||||
"user:home": "yes",
|
"user:home": "yes",
|
||||||
_lon: homeLonLat[0],
|
_lon: homeLonLat[0],
|
||||||
_lat: homeLonLat[1],
|
_lat: homeLonLat[1]
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
type: "Point",
|
type: "Point",
|
||||||
coordinates: homeLonLat,
|
coordinates: homeLonLat
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
return new StaticFeatureSource(feature)
|
return new StaticFeatureSource(feature)
|
||||||
|
@ -300,7 +328,7 @@ export default class UserRelatedState {
|
||||||
_applicationOpened: new Date().toISOString(),
|
_applicationOpened: new Date().toISOString(),
|
||||||
_supports_sharing:
|
_supports_sharing:
|
||||||
typeof window === "undefined" ? "no" : window.navigator.share ? "yes" : "no",
|
typeof window === "undefined" ? "no" : window.navigator.share ? "yes" : "no",
|
||||||
_iframe: Utils.isIframe ? "yes" : "no",
|
_iframe: Utils.isIframe ? "yes" : "no"
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const key in Constants.userJourney) {
|
for (const key in Constants.userJourney) {
|
||||||
|
@ -360,13 +388,13 @@ export default class UserRelatedState {
|
||||||
language,
|
language,
|
||||||
"themes",
|
"themes",
|
||||||
layout.id
|
layout.id
|
||||||
),
|
)
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
...missingLayers.map((id) => ({
|
...missingLayers.map((id) => ({
|
||||||
id: "layer:" + id,
|
id: "layer:" + id,
|
||||||
link: LinkToWeblate.hrefToWeblateZen(language, "layers", id),
|
link: LinkToWeblate.hrefToWeblateZen(language, "layers", id)
|
||||||
})),
|
}))
|
||||||
])
|
])
|
||||||
const untranslated_count = untranslated.length
|
const untranslated_count = untranslated.length
|
||||||
amendedPrefs.data["_translation_total"] = "" + total
|
amendedPrefs.data["_translation_total"] = "" + total
|
||||||
|
@ -391,7 +419,7 @@ export default class UserRelatedState {
|
||||||
for (const k in userDetails) {
|
for (const k in userDetails) {
|
||||||
amendedPrefs.data["_" + k] = "" + userDetails[k]
|
amendedPrefs.data["_" + k] = "" + userDetails[k]
|
||||||
}
|
}
|
||||||
if(userDetails.description){
|
if (userDetails.description) {
|
||||||
amendedPrefs.data["_description_html"] = Utils.purify(new Showdown.Converter()
|
amendedPrefs.data["_description_html"] = Utils.purify(new Showdown.Converter()
|
||||||
.makeHtml(userDetails.description)
|
.makeHtml(userDetails.description)
|
||||||
?.replace(/>/g, ">")
|
?.replace(/>/g, ">")
|
||||||
|
|
|
@ -104,7 +104,9 @@ export abstract class Store<T> implements Readable<T> {
|
||||||
extraStoresToWatch: Store<any>[],
|
extraStoresToWatch: Store<any>[],
|
||||||
callbackDestroyFunction: (f: () => void) => void
|
callbackDestroyFunction: (f: () => void) => void
|
||||||
): Store<J>
|
): Store<J>
|
||||||
|
|
||||||
M
|
M
|
||||||
|
|
||||||
public mapD<J>(
|
public mapD<J>(
|
||||||
f: (t: Exclude<T, undefined | null>) => J,
|
f: (t: Exclude<T, undefined | null>) => J,
|
||||||
extraStoresToWatch?: Store<any>[],
|
extraStoresToWatch?: Store<any>[],
|
||||||
|
@ -246,6 +248,7 @@ export abstract class Store<T> implements Readable<T> {
|
||||||
return f(<Exclude<T, undefined | null>>t)
|
return f(<Exclude<T, undefined | null>>t)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public stabilized(millisToStabilize): Store<T> {
|
public stabilized(millisToStabilize): Store<T> {
|
||||||
if (Utils.runningFromConsole) {
|
if (Utils.runningFromConsole) {
|
||||||
return this
|
return this
|
||||||
|
@ -311,12 +314,14 @@ export class ImmutableStore<T> extends Store<T> {
|
||||||
public readonly data: T
|
public readonly data: T
|
||||||
static FALSE = new ImmutableStore<boolean>(false)
|
static FALSE = new ImmutableStore<boolean>(false)
|
||||||
static TRUE = new ImmutableStore<boolean>(true)
|
static TRUE = new ImmutableStore<boolean>(true)
|
||||||
|
|
||||||
constructor(data: T) {
|
constructor(data: T) {
|
||||||
super()
|
super()
|
||||||
this.data = data
|
this.data = data
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly pass: () => void = () => {}
|
private static readonly pass: () => void = () => {
|
||||||
|
}
|
||||||
|
|
||||||
addCallback(_: (data: T) => void): () => void {
|
addCallback(_: (data: T) => void): () => void {
|
||||||
// pass: data will never change
|
// pass: data will never change
|
||||||
|
@ -718,6 +723,27 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static asObject<T extends object>(stringUIEventSource: UIEventSource<string>, defaultV: T): UIEventSource<T> {
|
||||||
|
return stringUIEventSource.sync(
|
||||||
|
(str) => {
|
||||||
|
if (str === undefined || str === null || str === "") {
|
||||||
|
return defaultV
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return <T> JSON.parse(str)
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not parse value", str,"due to",e)
|
||||||
|
return defaultV
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
(b) => {
|
||||||
|
console.log("Stringifying", b)
|
||||||
|
return JSON.stringify(b) ?? ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new UIEVentSource. Whenever 'source' changes, the returned UIEventSource will get this value as well.
|
* Create a new UIEVentSource. Whenever 'source' changes, the returned UIEventSource will get this value as well.
|
||||||
* However, this value can be overriden without affecting source
|
* However, this value can be overriden without affecting source
|
||||||
|
@ -863,7 +889,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
||||||
|
|
||||||
const newSource = new UIEventSource<J>(f(this.data), "map(" + this.tag + ")@" + callee)
|
const newSource = new UIEventSource<J>(f(this.data), "map(" + this.tag + ")@" + callee)
|
||||||
|
|
||||||
const update = function () {
|
const update = function() {
|
||||||
newSource.setData(f(self.data))
|
newSource.setData(f(self.data))
|
||||||
return allowUnregister && newSource._callbacks.length() === 0
|
return allowUnregister && newSource._callbacks.length() === 0
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,21 @@ import { RasterLayerProperties } from "../RasterLayerProperties"
|
||||||
|
|
||||||
import { ConversionContext } from "./Conversion/ConversionContext"
|
import { ConversionContext } from "./Conversion/ConversionContext"
|
||||||
import { Translatable } from "./Json/Translatable"
|
import { Translatable } from "./Json/Translatable"
|
||||||
|
import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal information about a theme
|
||||||
|
**/
|
||||||
|
export class MinimalLayoutInformation {
|
||||||
|
id: string
|
||||||
|
icon: string
|
||||||
|
title: Translatable
|
||||||
|
shortDescription: Translatable
|
||||||
|
definition?: Translatable
|
||||||
|
mustHaveLanguage?: boolean
|
||||||
|
hideFromOverview?: boolean
|
||||||
|
keywords?: (Translatable | TagRenderingConfigJson)[]
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Minimal information about a theme
|
* Minimal information about a theme
|
||||||
**/
|
**/
|
||||||
|
@ -27,6 +41,8 @@ export class LayoutInformation {
|
||||||
keywords?: (Translatable | Translation)[]
|
keywords?: (Translatable | Translation)[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default class LayoutConfig implements LayoutInformation {
|
export default class LayoutConfig implements LayoutInformation {
|
||||||
public static readonly defaultSocialImage = "assets/SocialImage.png"
|
public static readonly defaultSocialImage = "assets/SocialImage.png"
|
||||||
public readonly id: string
|
public readonly id: string
|
||||||
|
|
|
@ -81,6 +81,7 @@ import CoordinateSearch from "../Logic/Geocoding/CoordinateSearch"
|
||||||
import LocalElementSearch from "../Logic/Geocoding/LocalElementSearch"
|
import LocalElementSearch from "../Logic/Geocoding/LocalElementSearch"
|
||||||
import { RecentSearch } from "../Logic/Geocoding/RecentSearch"
|
import { RecentSearch } from "../Logic/Geocoding/RecentSearch"
|
||||||
import PhotonSearch from "../Logic/Geocoding/PhotonSearch"
|
import PhotonSearch from "../Logic/Geocoding/PhotonSearch"
|
||||||
|
import ThemeSearch from "../Logic/Geocoding/ThemeSearch"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -393,6 +394,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
new LocalElementSearch(this, 5),
|
new LocalElementSearch(this, 5),
|
||||||
new PhotonSearch(), // new NominatimGeocoding(),
|
new PhotonSearch(), // new NominatimGeocoding(),
|
||||||
new CoordinateSearch(),
|
new CoordinateSearch(),
|
||||||
|
this.featureSwitches.featureSwitchBackToThemeOverview.data ? new ThemeSearch(this) : undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
this.recentlySearched = new RecentSearch(this)
|
this.recentlySearched = new RecentSearch(this)
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
"oauth_token",
|
"oauth_token",
|
||||||
undefined,
|
undefined,
|
||||||
"Used to complete the login"
|
"Used to complete the login"
|
||||||
),
|
)
|
||||||
})
|
})
|
||||||
const state = new UserRelatedState(osmConnection)
|
const state = new UserRelatedState(osmConnection)
|
||||||
const t = Translations.t.index
|
const t = Translations.t.index
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
let userLanguages = osmConnection.userDetails.map((ud) => ud.languages)
|
let userLanguages = osmConnection.userDetails.map((ud) => ud.languages)
|
||||||
let themeSearchText: UIEventSource<string | undefined> = new UIEventSource<string>(undefined)
|
let themeSearchText: UIEventSource<string | undefined> = new UIEventSource<string>(undefined)
|
||||||
|
|
||||||
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()
|
document.getElementById("theme-search")?.focus()
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
@ -55,20 +55,10 @@
|
||||||
const hiddenThemes: LayoutInformation[] =
|
const hiddenThemes: LayoutInformation[] =
|
||||||
(themeOverview["default"] ?? themeOverview)?.filter((layout) => layout.hideFromOverview) ?? []
|
(themeOverview["default"] ?? themeOverview)?.filter((layout) => layout.hideFromOverview) ?? []
|
||||||
{
|
{
|
||||||
const prefix = "mapcomplete-hidden-theme-"
|
visitedHiddenThemes = MoreScreen.knownHiddenThemes(state.osmConnection)
|
||||||
const userPreferences = state.osmConnection.preferencesHandler.preferences
|
.map((knownIds) => hiddenThemes.filter((theme) =>
|
||||||
visitedHiddenThemes = userPreferences.map((preferences) => {
|
knownIds.has(theme.id) || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet"
|
||||||
const knownIds = new Set<string>(
|
))
|
||||||
Object.keys(preferences)
|
|
||||||
.filter((key) => key.startsWith(prefix))
|
|
||||||
.map((key) => key.substring(prefix.length, key.length - "-enabled".length))
|
|
||||||
)
|
|
||||||
return hiddenThemes.filter(
|
|
||||||
(theme) =>
|
|
||||||
knownIds.has(theme.id) ||
|
|
||||||
state.osmConnection.userDetails.data.name === "Pieter Vander Vennet"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -103,7 +93,7 @@
|
||||||
|
|
||||||
<form
|
<form
|
||||||
class="flex justify-center"
|
class="flex justify-center"
|
||||||
on:submit|preventDefault={(_) => MoreScreen.applySearch(themeSearchText.data)}
|
on:submit|preventDefault={() => MoreScreen.applySearch(themeSearchText.data)}
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="neutral-label my-2 flex w-full items-center rounded-full border-2 border-black sm:w-1/2"
|
class="neutral-label my-2 flex w-full items-center rounded-full border-2 border-black sm:w-1/2"
|
||||||
|
|
|
@ -116,8 +116,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let suggestions: Store<GeoCodeResult[]> = searchContents.stabilized(250).bindD(search =>
|
let suggestions: Store<{success: GeoCodeResult[]} | {error}> = searchContents.stabilized(250).bindD(search =>
|
||||||
UIEventSource.FromPromise(searcher.suggest(search), err => console.error(err))
|
UIEventSource.FromPromiseWithErr(searcher.suggest(search))
|
||||||
)
|
)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,19 +1,27 @@
|
||||||
import { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
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"
|
||||||
|
|
||||||
export default class MoreScreen {
|
export default class MoreScreen {
|
||||||
public static readonly officialThemes: LayoutInformation[] = themeOverview
|
public static readonly officialThemes: MinimalLayoutInformation[] = themeOverview
|
||||||
|
public static readonly officialThemesById: Map<string, MinimalLayoutInformation> = new Map<string, MinimalLayoutInformation>()
|
||||||
|
static {
|
||||||
|
for (const th of MoreScreen.officialThemes) {
|
||||||
|
MoreScreen.officialThemesById.set(th.id, th)
|
||||||
|
}
|
||||||
|
}
|
||||||
public static applySearch(searchTerm: string) {
|
public static applySearch(searchTerm: string) {
|
||||||
searchTerm = searchTerm.toLowerCase()
|
searchTerm = searchTerm.toLowerCase()
|
||||||
if (!searchTerm) {
|
if (!searchTerm) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (searchTerm === "personal") {
|
if (searchTerm === "personal") {
|
||||||
window.location.href = MoreScreen.createUrlFor({ id: "personal" }, false).data
|
window.location.href = MoreScreen.createUrlFor({ id: "personal" }, false)
|
||||||
}
|
}
|
||||||
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"
|
||||||
|
@ -38,22 +46,22 @@ export default class MoreScreen {
|
||||||
MoreScreen.MatchesLayout(th, searchTerm)
|
MoreScreen.MatchesLayout(th, searchTerm)
|
||||||
)
|
)
|
||||||
if (publicTheme !== undefined) {
|
if (publicTheme !== undefined) {
|
||||||
window.location.href = MoreScreen.createUrlFor(publicTheme, false).data
|
window.location.href = MoreScreen.createUrlFor(publicTheme, false)
|
||||||
}
|
}
|
||||||
const hiddenTheme = MoreScreen.officialThemes.find(
|
const hiddenTheme = MoreScreen.officialThemes.find(
|
||||||
(th) => th.id !== "personal" && MoreScreen.MatchesLayout(th, searchTerm)
|
(th) => th.id !== "personal" && MoreScreen.MatchesLayout(th, searchTerm)
|
||||||
)
|
)
|
||||||
if (hiddenTheme !== undefined) {
|
if (hiddenTheme !== undefined) {
|
||||||
window.location.href = MoreScreen.createUrlFor(hiddenTheme, false).data
|
window.location.href = MoreScreen.createUrlFor(hiddenTheme, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MatchesLayout(
|
public static MatchesLayout(
|
||||||
layout: {
|
layout: {
|
||||||
id: string
|
id: string
|
||||||
title: any
|
title: Translatable
|
||||||
shortDescription: any
|
shortDescription: Translatable
|
||||||
keywords?: any[]
|
keywords?: (Translatable | TagRenderingConfigJson)[]
|
||||||
},
|
},
|
||||||
search: string
|
search: string
|
||||||
): boolean {
|
): boolean {
|
||||||
|
@ -72,7 +80,7 @@ export default class MoreScreen {
|
||||||
if (entity === undefined) {
|
if (entity === undefined) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const term = entity["*"] ?? entity[Locale.language.data]
|
const term: string = entity["*"] ?? entity[Locale.language.data]
|
||||||
if (Utils.RemoveDiacritics(term?.toLowerCase())?.indexOf(search) >= 0) {
|
if (Utils.RemoveDiacritics(term?.toLowerCase())?.indexOf(search) >= 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -82,10 +90,10 @@ export default class MoreScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static createUrlFor(
|
public static createUrlFor(
|
||||||
layout: { id: string; definition?: string },
|
layout: { id: string },
|
||||||
isCustom: boolean,
|
isCustom: boolean,
|
||||||
state?: { layoutToUse?: { id } }
|
state?: { layoutToUse?: { id } }
|
||||||
): Store<string> {
|
): string {
|
||||||
if (layout === undefined) {
|
if (layout === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
@ -115,11 +123,22 @@ export default class MoreScreen {
|
||||||
linkPrefix = `${path}/theme.html?userlayout=${layout.id}&`
|
linkPrefix = `${path}/theme.html?userlayout=${layout.id}&`
|
||||||
}
|
}
|
||||||
|
|
||||||
let hash = ""
|
|
||||||
if (layout.definition !== undefined) {
|
return `${linkPrefix}`
|
||||||
hash = "#" + btoa(JSON.stringify(layout.definition))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ImmutableStore<string>(`${linkPrefix}${hash}`)
|
/**
|
||||||
|
* Gives all the IDs of the hidden themes which were previously visited
|
||||||
|
* @param osmConnection
|
||||||
|
*/
|
||||||
|
public static knownHiddenThemes(osmConnection: OsmConnection): Store<Set<string>> {
|
||||||
|
const prefix = "mapcomplete-hidden-theme-"
|
||||||
|
const userPreferences = osmConnection.preferencesHandler.preferences
|
||||||
|
return userPreferences.map((preferences) =>
|
||||||
|
new Set<string>(
|
||||||
|
Object.keys(preferences)
|
||||||
|
.filter((key) => key.startsWith(prefix))
|
||||||
|
.map((key) => key.substring(prefix.length, key.length - "-enabled".length))
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,15 @@
|
||||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
||||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp"
|
import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp"
|
||||||
|
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||||
|
import Tr from "../Base/Tr.svelte"
|
||||||
|
import { Translation } from "../i18n/Translation"
|
||||||
|
import MoreScreen from "./MoreScreen"
|
||||||
|
|
||||||
export let entry: GeoCodeResult
|
export let entry: GeoCodeResult
|
||||||
export let state: SpecialVisualizationState
|
export let state: SpecialVisualizationState
|
||||||
let layer: LayerConfig
|
let layer: LayerConfig
|
||||||
let tags : UIEventSource<Record<string, string>>
|
let tags: UIEventSource<Record<string, string>>
|
||||||
if (entry.feature?.properties?.id) {
|
if (entry.feature?.properties?.id) {
|
||||||
layer = state.layout.getMatchingLayer(entry.feature.properties)
|
layer = state.layout.getMatchingLayer(entry.feature.properties)
|
||||||
tags = state.featureProperties.getStore(entry.feature.properties.id)
|
tags = state.featureProperties.getStore(entry.feature.properties.id)
|
||||||
|
@ -28,6 +32,8 @@
|
||||||
let mapRotation = state.mapProperties.rotation
|
let mapRotation = state.mapProperties.rotation
|
||||||
let inView = state.mapProperties.bounds.mapD(bounds => bounds.contains([entry.lon, entry.lat]))
|
let inView = state.mapProperties.bounds.mapD(bounds => bounds.contains([entry.lon, entry.lat]))
|
||||||
|
|
||||||
|
let otherTheme: MinimalLayoutInformation | undefined = <MinimalLayoutInformation>entry.payload
|
||||||
|
|
||||||
function select() {
|
function select() {
|
||||||
console.log("Selected search entry", entry)
|
console.log("Selected search entry", entry)
|
||||||
if (entry.boundingbox) {
|
if (entry.boundingbox) {
|
||||||
|
@ -41,16 +47,29 @@
|
||||||
} else {
|
} else {
|
||||||
state.mapProperties.flyTo(entry.lon, entry.lat, GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17)
|
state.mapProperties.flyTo(entry.lon, entry.lat, GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17)
|
||||||
}
|
}
|
||||||
if (entry.feature) {
|
if (entry.feature?.properties?.id) {
|
||||||
state.selectedElement.set(entry.feature)
|
state.selectedElement.set(entry.feature)
|
||||||
}
|
}
|
||||||
state.recentlySearched.addSelected(entry)
|
state.recentlySearched.addSelected(entry)
|
||||||
dispatch("select")
|
dispatch("select")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<button class="unstyled w-full link-no-underline" on:click={() => select() }>
|
|
||||||
<div class="p-2 flex items-center w-full gap-y-2 w-full">
|
|
||||||
|
|
||||||
|
{#if otherTheme}
|
||||||
|
<a href={ MoreScreen.createUrlFor(otherTheme, false)} class="flex items-center p-2 w-full gap-y-2 rounded-xl" >
|
||||||
|
|
||||||
|
<Icon icon={otherTheme.icon} clss="w-6 h-6 m-1" />
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<b>
|
||||||
|
<Tr t={new Translation(otherTheme.title)} />
|
||||||
|
</b>
|
||||||
|
<!--<Tr t={new Translation(otherTheme.shortDescription)} /> -->
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{:else}
|
||||||
|
<button class="unstyled w-full link-no-underline" on:click={() => select() }>
|
||||||
|
<div class="p-2 flex items-center w-full gap-y-2">
|
||||||
{#if layer}
|
{#if layer}
|
||||||
<ToSvelte construct={() => layer.defaultIcon(entry.feature.properties).SetClass("w-6 h-6")} />
|
<ToSvelte construct={() => layer.defaultIcon(entry.feature.properties).SetClass("w-6 h-6")} />
|
||||||
{:else if entry.category}
|
{:else if entry.category}
|
||||||
|
@ -79,6 +98,9 @@
|
||||||
{entry.description}
|
{entry.description}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/if}
|
||||||
|
|
|
@ -3,59 +3,81 @@
|
||||||
import SearchResult from "./SearchResult.svelte"
|
import SearchResult from "./SearchResult.svelte"
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
import { XMarkIcon } from "@babeard/svelte-heroicons/solid"
|
import { XMarkIcon } from "@babeard/svelte-heroicons/solid"
|
||||||
import { Store } from "../../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import Loading from "../Base/Loading.svelte"
|
import Loading from "../Base/Loading.svelte"
|
||||||
|
import Tr from "../Base/Tr.svelte"
|
||||||
|
import Translations from "../i18n/Translations"
|
||||||
|
import MoreScreen from "./MoreScreen"
|
||||||
|
|
||||||
export let state: SpecialVisualizationState
|
export let state: SpecialVisualizationState
|
||||||
export let results: GeoCodeResult[]
|
export let results: { success: GeoCodeResult[] } | { error }
|
||||||
export let searchTerm: Store<string>
|
export let searchTerm: Store<string>
|
||||||
export let isFocused: Store<boolean>
|
export let isFocused: UIEventSource<boolean>
|
||||||
|
|
||||||
let recentlySeen: Store<GeoCodeResult[]> = state.recentlySearched.seenThisSession
|
let recentlySeen: Store<GeoCodeResult[]> = state.recentlySearched.seenThisSession
|
||||||
|
let recentThemes = state.userRelatedState.recentlyVisitedThemes.mapD(thms => thms.filter(th => th !== state.layout.id).slice(0, 3))
|
||||||
|
let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full collapsable" style="height: 50rem;" class:collapsed={!$isFocused}>
|
<div class="w-full collapsable" style="height: 50rem;" class:collapsed={!$isFocused}>
|
||||||
{#if $searchTerm.length > 0 && results === undefined}
|
{#if results?.["error"] !== undefined}
|
||||||
|
<div class="searchbox normal-background items-center">
|
||||||
|
An error occured
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{:else if $searchTerm.length > 0 && results === undefined}
|
||||||
<div class="searchbox normal-background items-center">
|
<div class="searchbox normal-background items-center">
|
||||||
<Loading />
|
<Loading />
|
||||||
</div>
|
</div>
|
||||||
{:else if results?.length > 0}
|
{:else if results?.["success"]?.length > 0}
|
||||||
<div class="relative w-full h-full">
|
<div class="relative w-full h-full">
|
||||||
<div class="absolute top-0 right-0 searchbox normal-background"
|
<div class="absolute top-0 right-0 searchbox normal-background"
|
||||||
style="width: 25rem">
|
style="width: 25rem">
|
||||||
<div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto">
|
<div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto">
|
||||||
|
|
||||||
{#each results as entry (entry)}
|
{#each results["success"] as entry (entry)}
|
||||||
<SearchResult on:select {entry} {state} />
|
<SearchResult on:select {entry} {state} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="absolute top-2 right-2 cursor-pointer" on:click={() => close()}>
|
<div class="absolute top-2 right-2 cursor-pointer" on:click={() => isFocused.setData(false)}>
|
||||||
<XMarkIcon class="w-4 h-4 hover:bg-stone-200 rounded-full" />
|
<XMarkIcon class="w-4 h-4 hover:bg-stone-200 rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else }
|
{:else if $searchTerm.length > 0 || $recentlySeen?.length > 0 || $recentThemes?.length > 0}
|
||||||
|
|
||||||
<div class="searchbox normal-background ">
|
<div class="searchbox normal-background overflow-y-auto h-full">
|
||||||
{#if $searchTerm.length > 0}
|
{#if $searchTerm.length > 0}
|
||||||
<!-- TODO add translation -->
|
<b class="flex justify-center p-4">
|
||||||
<b class="flex justify-center p-4">No results found for {$searchTerm}</b>
|
<Tr t={Translations.t.general.search.nothingFor.Subs({term: $searchTerm})} />
|
||||||
|
</b>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $recentlySeen?.length > 0}
|
{#if $recentlySeen?.length > 0}
|
||||||
<!-- TODO add translation -->
|
<h3 class="mx-2">
|
||||||
<h4>Recent searches</h4>
|
<Tr t={Translations.t.general.search.recents} />
|
||||||
|
</h3>
|
||||||
{#each $recentlySeen as entry}
|
{#each $recentlySeen as entry}
|
||||||
<SearchResult {entry} {state} on:select />
|
<SearchResult {entry} {state} on:select />
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if $recentThemes?.length > 0 && $allowOtherThemes}
|
||||||
|
<h3 class="mx-2">
|
||||||
|
<Tr t={Translations.t.general.search.recentThemes} />
|
||||||
|
</h3>
|
||||||
|
{#each $recentThemes as themeId (themeId)}
|
||||||
|
<SearchResult
|
||||||
|
entry={{payload: MoreScreen.officialThemesById.get(themeId), display_name: themeId, lat: 0, lon: 0}} {state}
|
||||||
|
on:select />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.searchbox {
|
.searchbox {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -68,7 +90,8 @@
|
||||||
|
|
||||||
.collapsable {
|
.collapsable {
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
transition: max-height 350ms ease-in-out;
|
transition: max-height 400ms linear;
|
||||||
|
transition-delay: 500ms;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Translation } from "../i18n/Translation"
|
|
||||||
import * as personal from "../../../assets/themes/personal/personal.json"
|
import * as personal from "../../../assets/themes/personal/personal.json"
|
||||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||||
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||||
|
|
|
@ -23,15 +23,16 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
"dragRotate",
|
"dragRotate",
|
||||||
"dragPan",
|
"dragPan",
|
||||||
"keyboard",
|
"keyboard",
|
||||||
"touchZoomRotate",
|
"touchZoomRotate"
|
||||||
]
|
]
|
||||||
private static maplibre_zoom_handlers = [
|
private static maplibre_zoom_handlers = [
|
||||||
"scrollZoom",
|
"scrollZoom",
|
||||||
"boxZoom",
|
"boxZoom",
|
||||||
"doubleClickZoom",
|
"doubleClickZoom",
|
||||||
"touchZoomRotate",
|
"touchZoomRotate"
|
||||||
]
|
]
|
||||||
readonly location: UIEventSource<{ lon: number; lat: number }>
|
readonly location: UIEventSource<{ lon: number; lat: number }>
|
||||||
|
private readonly isFlying = new UIEventSource(false)
|
||||||
readonly zoom: UIEventSource<number>
|
readonly zoom: UIEventSource<number>
|
||||||
readonly bounds: UIEventSource<BBox>
|
readonly bounds: UIEventSource<BBox>
|
||||||
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
|
readonly rasterLayer: UIEventSource<RasterLayerPolygon | undefined>
|
||||||
|
@ -105,6 +106,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
new RasterLayerHandler(this._maplibreMap, this.rasterLayer)
|
new RasterLayerHandler(this._maplibreMap, this.rasterLayer)
|
||||||
|
|
||||||
const clickmodes = ["left", "middle", "right"] as const
|
const clickmodes = ["left", "middle", "right"] as const
|
||||||
|
|
||||||
function handleClick(e: maplibregl.MapMouseEvent, mode?: "left" | "right" | "middle") {
|
function handleClick(e: maplibregl.MapMouseEvent, mode?: "left" | "right" | "middle") {
|
||||||
if (e.originalEvent["consumed"]) {
|
if (e.originalEvent["consumed"]) {
|
||||||
// Workaround, 'ShowPointLayer' sets this flag
|
// Workaround, 'ShowPointLayer' sets this flag
|
||||||
|
@ -144,7 +146,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
self.SetRotation(self.rotation.data)
|
self.SetRotation(self.rotation.data)
|
||||||
self.setTerrain(self.useTerrain.data)
|
self.setTerrain(self.useTerrain.data)
|
||||||
this.updateStores(true)
|
this.updateStores(true)
|
||||||
map.on("moveend", () => this.updateStores())
|
map.on("movestart", () => {
|
||||||
|
this.isFlying.setData(true)
|
||||||
|
})
|
||||||
|
map.on("moveend", () => {
|
||||||
|
this.isFlying.setData(false)
|
||||||
|
this.updateStores()
|
||||||
|
})
|
||||||
map.on("click", (e) => {
|
map.on("click", (e) => {
|
||||||
handleClick(e)
|
handleClick(e)
|
||||||
})
|
})
|
||||||
|
@ -228,9 +236,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
return {
|
return {
|
||||||
map: mlmap,
|
map: mlmap,
|
||||||
ui: new SvelteUIElement(MaplibreMap, {
|
ui: new SvelteUIElement(MaplibreMap, {
|
||||||
map: mlmap,
|
map: mlmap
|
||||||
}),
|
}),
|
||||||
mapproperties: new MapLibreAdaptor(mlmap),
|
mapproperties: new MapLibreAdaptor(mlmap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -298,7 +306,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
) {
|
) {
|
||||||
const event = {
|
const event = {
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
key: key,
|
key: key
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < this._onKeyNavigation.length; i++) {
|
for (let i = 0; i < this._onKeyNavigation.length; i++) {
|
||||||
|
@ -487,7 +495,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
const bounds = map.getBounds()
|
const bounds = map.getBounds()
|
||||||
const bbox = new BBox([
|
const bbox = new BBox([
|
||||||
[bounds.getEast(), bounds.getNorth()],
|
[bounds.getEast(), bounds.getNorth()],
|
||||||
[bounds.getWest(), bounds.getSouth()],
|
[bounds.getWest(), bounds.getSouth()]
|
||||||
])
|
])
|
||||||
if (this.bounds.data === undefined || !isSetup) {
|
if (this.bounds.data === undefined || !isSetup) {
|
||||||
this.bounds.setData(bbox)
|
this.bounds.setData(bbox)
|
||||||
|
@ -501,6 +509,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
if (!map || z === undefined) {
|
if (!map || z === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (this.isFlying.data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (Math.abs(map.getZoom() - z) > 0.01) {
|
if (Math.abs(map.getZoom() - z) > 0.01) {
|
||||||
map.setZoom(z)
|
map.setZoom(z)
|
||||||
}
|
}
|
||||||
|
@ -648,9 +659,22 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
if (!hasDiff) {
|
if (!hasDiff) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
this.lockZoom()
|
||||||
map.fitBounds(bounds.toLngLat())
|
map.fitBounds(bounds.toLngLat())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called before making an animation.
|
||||||
|
* First, 'isFlying' is set to true. This will disable the zoom control
|
||||||
|
* Then, zoom is set to '1', which is very low. This will generally disable all layers, after which this function will return
|
||||||
|
*
|
||||||
|
* Then, a zoom/pan/... animation can be made; after which a 'moveEnd'-event will trigger the 'isFlying' to be set to false and the zoom to be set correctly
|
||||||
|
*/
|
||||||
|
private lockZoom() {
|
||||||
|
this.isFlying.setData(true)
|
||||||
|
this.zoom.setData(1)
|
||||||
|
}
|
||||||
|
|
||||||
private async setTerrain(useTerrain: boolean) {
|
private async setTerrain(useTerrain: boolean) {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (!map) {
|
if (!map) {
|
||||||
|
@ -665,14 +689,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
type: "raster-dem",
|
type: "raster-dem",
|
||||||
url:
|
url:
|
||||||
"https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=" +
|
"https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=" +
|
||||||
Constants.maptilerApiKey,
|
Constants.maptilerApiKey
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
while (!map?.isStyleLoaded()) {
|
while (!map?.isStyleLoaded()) {
|
||||||
await Utils.waitFor(250)
|
await Utils.waitFor(250)
|
||||||
}
|
}
|
||||||
map.setTerrain({
|
map.setTerrain({
|
||||||
source: id,
|
source: id
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
@ -680,10 +704,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public flyTo(lon: number, lat: number, zoom: number){
|
public flyTo(lon: number, lat: number, zoom: number) {
|
||||||
|
this.lockZoom()
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
this._maplibreMap.data?.flyTo({
|
this._maplibreMap.data?.flyTo({
|
||||||
zoom,
|
zoom,
|
||||||
center: [lon, lat],
|
center: [lon, lat]
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||||
import BaseUIElement from "./BaseUIElement"
|
import BaseUIElement from "./BaseUIElement"
|
||||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
import LayoutConfig, { MinimalLayoutInformation } from "../Models/ThemeConfig/LayoutConfig"
|
||||||
import {
|
import {
|
||||||
FeatureSource,
|
FeatureSource,
|
||||||
IndexedFeatureSource,
|
IndexedFeatureSource,
|
||||||
|
@ -87,6 +87,7 @@ export interface SpecialVisualizationState {
|
||||||
readonly showAllQuestionsAtOnce: UIEventSource<boolean>
|
readonly showAllQuestionsAtOnce: UIEventSource<boolean>
|
||||||
readonly preferencesAsTags: UIEventSource<Record<string, string>>
|
readonly preferencesAsTags: UIEventSource<Record<string, string>>
|
||||||
readonly language: UIEventSource<string>
|
readonly language: UIEventSource<string>
|
||||||
|
readonly recentlyVisitedThemes: Store<string[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly availableLayers: Store<RasterLayerPolygon[]>
|
readonly availableLayers: Store<RasterLayerPolygon[]>
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
return "offline"
|
return "offline"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
message: osmApi,
|
message: osmApi
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@
|
||||||
}
|
}
|
||||||
const files: string[] = s["success"]["allFiles"]
|
const files: string[] = s["success"]["allFiles"]
|
||||||
return "Contains " + (files.length ?? "no") + " files"
|
return "Contains " + (files.length ?? "no") + " files"
|
||||||
}),
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
@ -106,7 +106,7 @@
|
||||||
return "degraded"
|
return "degraded"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
message: simpleMessage(testDownload(Constants.GeoIpServer + "/ip")),
|
message: simpleMessage(testDownload(Constants.GeoIpServer + "/ip"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +125,7 @@
|
||||||
}
|
}
|
||||||
return "degraded"
|
return "degraded"
|
||||||
}),
|
}),
|
||||||
message: simpleMessage(status),
|
message: simpleMessage(status)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@
|
||||||
}
|
}
|
||||||
return "online"
|
return "online"
|
||||||
}),
|
}),
|
||||||
message: simpleMessage(status),
|
message: simpleMessage(status)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,7 +183,7 @@
|
||||||
|
|
||||||
const json = JSON.stringify(s["success"], null, " ")
|
const json = JSON.stringify(s["success"], null, " ")
|
||||||
return "Database is " + Math.floor(timediffDays) + " days out of sync\n\n" + json
|
return "Database is " + Math.floor(timediffDays) + " days out of sync\n\n" + json
|
||||||
}),
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +202,45 @@
|
||||||
}
|
}
|
||||||
return "degraded"
|
return "degraded"
|
||||||
}),
|
}),
|
||||||
message: status.map((s) => JSON.stringify(s)),
|
message: status.map((s) => JSON.stringify(s))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const s = Constants.nominatimEndpoint
|
||||||
|
const status = testDownload(s + "/search.php?q=Brugge")
|
||||||
|
services.push({
|
||||||
|
name: s,
|
||||||
|
message: simpleMessage(status),
|
||||||
|
status: status.mapD(s => {
|
||||||
|
if (s["error"]) {
|
||||||
|
return "offline"
|
||||||
|
}
|
||||||
|
const data = s["success"]
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return "online"
|
||||||
|
}
|
||||||
|
return "degraded"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const s = Constants.photonEndpoint
|
||||||
|
const status = testDownload(s + "/api/?q=Brugge")
|
||||||
|
services.push({
|
||||||
|
name: s,
|
||||||
|
status: status.mapD(s => {
|
||||||
|
if (s["error"]) {
|
||||||
|
return "offline"
|
||||||
|
}
|
||||||
|
const data = s["success"]
|
||||||
|
if (Array.isArray(data.features) && data.features.length > 0) {
|
||||||
|
return "online"
|
||||||
|
}
|
||||||
|
return "degraded"
|
||||||
|
}),
|
||||||
|
message: simpleMessage(status)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,7 +266,7 @@
|
||||||
|
|
||||||
return "online"
|
return "online"
|
||||||
}),
|
}),
|
||||||
message: simpleMessage(status),
|
message: simpleMessage(status)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -241,7 +279,7 @@
|
||||||
return "online"
|
return "online"
|
||||||
}
|
}
|
||||||
return "offline"
|
return "offline"
|
||||||
}),
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -263,14 +263,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NoNull<T>(array: T[] | undefined): T[] | undefined
|
public static NoNull<T>(array: ReadonlyArray<T> | undefined): T[] | undefined
|
||||||
public static NoNull<T>(array: undefined): undefined
|
public static NoNull<T>(array: undefined): undefined
|
||||||
public static NoNull<T>(array: T[]): T[]
|
public static NoNull<T>(array: ReadonlyArray<T>): T[]
|
||||||
public static NoNull<T>(array: T[]): NonNullable<T>[] {
|
public static NoNull<T>(array: ReadonlyArray<T>): NonNullable<T>[] {
|
||||||
return <any>array?.filter((o) => o !== undefined && o !== null)
|
return <any>array?.filter((o) => o !== undefined && o !== null)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Hist(array: string[]): Map<string, number> {
|
public static Hist(array: ReadonlyArray<string>): Map<string, number> {
|
||||||
const hist = new Map<string, number>()
|
const hist = new Map<string, number>()
|
||||||
for (const s of array) {
|
for (const s of array) {
|
||||||
hist.set(s, 1 + (hist.get(s) ?? 0))
|
hist.set(s, 1 + (hist.get(s) ?? 0))
|
||||||
|
|
|
@ -66,9 +66,9 @@
|
||||||
--svg-color: #000000;
|
--svg-color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face{
|
@font-face {
|
||||||
font-family:"Source Sans Pro";
|
font-family: "Source Sans Pro";
|
||||||
src:url("/assets/source-sans-pro.regular.ttf") format("woff");
|
src: url("/assets/source-sans-pro.regular.ttf") format("woff");
|
||||||
}
|
}
|
||||||
|
|
||||||
/***********************************************************************\
|
/***********************************************************************\
|
||||||
|
@ -244,9 +244,11 @@ button.disabled {
|
||||||
color: var(--disabled-font);
|
color: var(--disabled-font);
|
||||||
cursor: unset;
|
cursor: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.disabled svg path {
|
button.disabled svg path {
|
||||||
transition: all 200ms;
|
transition: all 200ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.disabled svg path {
|
button.disabled svg path {
|
||||||
fill: var(--disabled-font);
|
fill: var(--disabled-font);
|
||||||
stroke: var(--disabled-font);
|
stroke: var(--disabled-font);
|
||||||
|
@ -324,7 +326,7 @@ label:not(.neutral-label):not(.button) {
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
margin:0.25rem;
|
margin: 0.25rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -537,8 +539,14 @@ a.link-underline {
|
||||||
text-decoration: underline !important;
|
text-decoration: underline !important;
|
||||||
-webkit-text-decoration: underline !important;
|
-webkit-text-decoration: underline !important;
|
||||||
color: unset !important;
|
color: unset !important;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
background-color: var(--low-interaction-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.disable-links a.must-link,
|
.disable-links a.must-link,
|
||||||
.disable-links .must-link a {
|
.disable-links .must-link a {
|
||||||
/* Hide links if they are disabled */
|
/* Hide links if they are disabled */
|
||||||
|
@ -610,7 +618,6 @@ svg.apply-fill path {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/************************* LEGACY MARKER - CLEANUP BELOW ********************************/
|
/************************* LEGACY MARKER - CLEANUP BELOW ********************************/
|
||||||
|
|
||||||
.slideshow-item img {
|
.slideshow-item img {
|
||||||
|
|
Loading…
Reference in a new issue