Merge develop
This commit is contained in:
commit
be5f46d8c8
321 changed files with 14942 additions and 4545 deletions
|
@ -86,7 +86,7 @@ export class Mapillary extends ImageProvider {
|
|||
|
||||
public async DownloadAttribution(url: string): Promise<LicenseInfo> {
|
||||
const license = new LicenseInfo()
|
||||
license.artist = "Contributor name unavailable"
|
||||
license.artist = undefined
|
||||
license.license = "CC BY-SA 4.0"
|
||||
// license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
|
||||
license.attributionRequired = true
|
||||
|
|
|
@ -28,7 +28,7 @@ export default class ChangeTagAction extends OsmChangeAction {
|
|||
currentTags: Record<string, string>,
|
||||
meta: {
|
||||
theme: string
|
||||
changeType: "answer" | "soft-delete" | "add-image" | string
|
||||
changeType: "answer" | "soft-delete" | "add-image" | "link-image" | string
|
||||
}
|
||||
) {
|
||||
super(elementId, true)
|
||||
|
|
32
src/Logic/Osm/Actions/LinkPicture.ts
Normal file
32
src/Logic/Osm/Actions/LinkPicture.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import ChangeTagAction from "./ChangeTagAction"
|
||||
import { Tag } from "../../Tags/Tag"
|
||||
|
||||
export default class LinkPicture extends ChangeTagAction {
|
||||
/**
|
||||
* Adds a link to an image
|
||||
* @param elementId
|
||||
* @param proposedKey: a key which might be used, typically `image`. If the key is already used with a different URL, `key+":0"` will be used instead (or a higher number if needed)
|
||||
* @param url
|
||||
* @param currentTags
|
||||
* @param meta
|
||||
*
|
||||
*/
|
||||
constructor(
|
||||
elementId: string,
|
||||
proposedKey: "image" | "mapillary" | "wiki_commons" | string,
|
||||
url: string,
|
||||
currentTags: Record<string, string>,
|
||||
meta: {
|
||||
theme: string
|
||||
changeType: "add-image" | "link-image"
|
||||
}
|
||||
) {
|
||||
let key = proposedKey
|
||||
let i = 0
|
||||
while (currentTags[key] !== undefined && currentTags[key] !== url) {
|
||||
key = proposedKey + ":" + i
|
||||
i++
|
||||
}
|
||||
super(elementId, new Tag(key, url), currentTags, meta)
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import osmAuth from "osm-auth"
|
||||
// @ts-ignore
|
||||
import { osmAuth } from "osm-auth"
|
||||
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||
import { OsmPreferences } from "./OsmPreferences"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import * as config from "../../../package.json"
|
||||
export default class UserDetails {
|
||||
public loggedIn = false
|
||||
public name = "Not logged in"
|
||||
|
@ -22,23 +24,18 @@ export default class UserDetails {
|
|||
}
|
||||
}
|
||||
|
||||
export interface AuthConfig {
|
||||
"#"?: string // optional comment
|
||||
oauth_client_id: string
|
||||
oauth_secret: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable"
|
||||
|
||||
export class OsmConnection {
|
||||
public static readonly oauth_configs = {
|
||||
osm: {
|
||||
oauth_consumer_key: "hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem",
|
||||
oauth_secret: "wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI",
|
||||
url: "https://www.openstreetmap.org",
|
||||
// OAUTH 1.0 application
|
||||
// https://www.openstreetmap.org/user/Pieter%20Vander%20Vennet/oauth_clients/7404
|
||||
},
|
||||
"osm-test": {
|
||||
oauth_consumer_key: "Zgr7EoKb93uwPv2EOFkIlf3n9NLwj5wbyfjZMhz2",
|
||||
oauth_secret: "3am1i1sykHDMZ66SGq4wI2Z7cJMKgzneCHp3nctn",
|
||||
url: "https://master.apis.dev.openstreetmap.org",
|
||||
},
|
||||
}
|
||||
public static readonly oauth_configs: Record<string, AuthConfig> =
|
||||
config.config.oauth_credentials
|
||||
public auth
|
||||
public userDetails: UIEventSource<UserDetails>
|
||||
public isLoggedIn: Store<boolean>
|
||||
|
@ -53,11 +50,7 @@ export class OsmConnection {
|
|||
"not-attempted"
|
||||
)
|
||||
public preferencesHandler: OsmPreferences
|
||||
public readonly _oauth_config: {
|
||||
oauth_consumer_key: string
|
||||
oauth_secret: string
|
||||
url: string
|
||||
}
|
||||
public readonly _oauth_config: AuthConfig
|
||||
private readonly _dryRun: Store<boolean>
|
||||
private fakeUser: boolean
|
||||
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []
|
||||
|
@ -83,6 +76,19 @@ export class OsmConnection {
|
|||
console.debug("Using backend", this._oauth_config.url)
|
||||
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top
|
||||
|
||||
// Check if there are settings available in environment variables, and if so, use those
|
||||
if (
|
||||
import.meta.env.VITE_OSM_OAUTH_CLIENT_ID !== undefined &&
|
||||
import.meta.env.VITE_OSM_OAUTH_SECRET !== undefined
|
||||
) {
|
||||
console.debug("Using environment variables for oauth config")
|
||||
this._oauth_config = {
|
||||
oauth_client_id: import.meta.env.VITE_OSM_OAUTH_CLIENT_ID,
|
||||
oauth_secret: import.meta.env.VITE_OSM_OAUTH_SECRET,
|
||||
url: "https://www.openstreetmap.org",
|
||||
}
|
||||
}
|
||||
|
||||
this.userDetails = new UIEventSource<UserDetails>(
|
||||
new UserDetails(this._oauth_config.url),
|
||||
"userDetails"
|
||||
|
@ -190,6 +196,9 @@ export class OsmConnection {
|
|||
const self = this
|
||||
console.log("Trying to log in...")
|
||||
this.updateAuthObject()
|
||||
LocalStorageSource.Get("location_before_login").setData(
|
||||
Utils.runningFromConsole ? undefined : window.location.href
|
||||
)
|
||||
this.auth.xhr(
|
||||
{
|
||||
method: "GET",
|
||||
|
@ -202,13 +211,8 @@ export class OsmConnection {
|
|||
if (err.status == 401) {
|
||||
console.log("Clearing tokens...")
|
||||
// Not authorized - our token probably got revoked
|
||||
// Reset all the tokens
|
||||
const tokens = [
|
||||
"https://www.openstreetmap.orgoauth_request_token_secret",
|
||||
"https://www.openstreetmap.orgoauth_token",
|
||||
"https://www.openstreetmap.orgoauth_token_secret",
|
||||
]
|
||||
tokens.forEach((token) => localStorage.removeItem(token))
|
||||
self.auth.logout()
|
||||
self.LogOut()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -310,6 +314,7 @@ export class OsmConnection {
|
|||
): Promise<any> {
|
||||
return await this.interact(path, "POST", header, content)
|
||||
}
|
||||
|
||||
public async put(
|
||||
path: string,
|
||||
content?: string,
|
||||
|
@ -486,15 +491,29 @@ export class OsmConnection {
|
|||
// Same for an iframe...
|
||||
|
||||
this.auth = new osmAuth({
|
||||
oauth_consumer_key: this._oauth_config.oauth_consumer_key,
|
||||
oauth_secret: this._oauth_config.oauth_secret,
|
||||
client_id: this._oauth_config.oauth_client_id,
|
||||
url: this._oauth_config.url,
|
||||
landing: standalone ? undefined : window.location.href,
|
||||
scope: "read_prefs write_prefs write_api write_gpx write_notes",
|
||||
redirect_uri: Utils.runningFromConsole
|
||||
? "https://mapcomplete.org/land.html"
|
||||
: window.location.protocol + "//" + window.location.host + "/land.html",
|
||||
singlepage: !standalone,
|
||||
auto: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* To be called by land.html
|
||||
*/
|
||||
public finishLogin(callback: (previousURL: string) => void) {
|
||||
this.auth.authenticate(function () {
|
||||
// Fully authed at this point
|
||||
console.log("Authentication successful!")
|
||||
const previousLocation = LocalStorageSource.Get("location_before_login")
|
||||
callback(previousLocation.data)
|
||||
})
|
||||
}
|
||||
|
||||
private CheckForMessagesContinuously() {
|
||||
const self = this
|
||||
if (this.isChecking) {
|
||||
|
|
222
src/Logic/Web/NearbyImagesSearch.ts
Normal file
222
src/Logic/Web/NearbyImagesSearch.ts
Normal file
|
@ -0,0 +1,222 @@
|
|||
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource"
|
||||
import { Mapillary } from "../ImageProviders/Mapillary"
|
||||
import P4C from "pic4carto"
|
||||
import { Utils } from "../../Utils"
|
||||
export interface NearbyImageOptions {
|
||||
lon: number
|
||||
lat: number
|
||||
// Radius of the upstream search
|
||||
searchRadius?: 500 | number
|
||||
maxDaysOld?: 1095 | number
|
||||
blacklist: Store<{ url: string }[]>
|
||||
shownImagesCount?: UIEventSource<number>
|
||||
towardscenter?: UIEventSource<boolean>
|
||||
allowSpherical?: UIEventSource<boolean>
|
||||
// Radius of what is shown. Useless to select a value > searchRadius; defaults to searchRadius
|
||||
shownRadius?: UIEventSource<number>
|
||||
}
|
||||
|
||||
export interface P4CPicture {
|
||||
pictureUrl: string
|
||||
date?: number
|
||||
coordinates: { lat: number; lng: number }
|
||||
provider: "Mapillary" | string
|
||||
author?
|
||||
license?
|
||||
detailsUrl?: string
|
||||
direction?
|
||||
osmTags?: object /*To copy straight into OSM!*/
|
||||
thumbUrl: string
|
||||
details: {
|
||||
isSpherical: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Pic4wCarto to fetch nearby images from various providers
|
||||
*/
|
||||
export default class NearbyImagesSearch {
|
||||
private static readonly services = [
|
||||
"mapillary",
|
||||
"flickr",
|
||||
"openstreetcam",
|
||||
"wikicommons",
|
||||
] as const
|
||||
|
||||
private individualStores
|
||||
private readonly _store: UIEventSource<P4CPicture[]> = new UIEventSource<P4CPicture[]>([])
|
||||
public readonly store: Store<P4CPicture[]> = this._store
|
||||
private readonly _options: NearbyImageOptions
|
||||
|
||||
constructor(options: NearbyImageOptions, features: IndexedFeatureSource) {
|
||||
this.individualStores = NearbyImagesSearch.services.map((s) =>
|
||||
NearbyImagesSearch.buildPictureFetcher(options, s)
|
||||
)
|
||||
this._options = options
|
||||
if (features !== undefined) {
|
||||
const osmImages = new ImagesInLoadedDataFetcher(features).fetchAround({
|
||||
lat: options.lat,
|
||||
lon: options.lon,
|
||||
searchRadius: options.searchRadius ?? 100,
|
||||
})
|
||||
this.individualStores.push(
|
||||
new ImmutableStore({ images: osmImages, beforeFilter: osmImages.length })
|
||||
)
|
||||
}
|
||||
for (const source of this.individualStores) {
|
||||
source.addCallback(() => this.update())
|
||||
}
|
||||
this.update()
|
||||
}
|
||||
|
||||
private static buildPictureFetcher(
|
||||
options: NearbyImageOptions,
|
||||
fetcher: "mapillary" | "flickr" | "openstreetcam" | "wikicommons"
|
||||
): Store<{ images: P4CPicture[]; beforeFilter: number }> {
|
||||
const picManager = new P4C.PicturesManager({ usefetchers: [fetcher] })
|
||||
const searchRadius = options.searchRadius ?? 100
|
||||
const maxAgeSeconds = (options.maxDaysOld ?? 3 * 365) * 24 * 60 * 60 * 1000
|
||||
|
||||
const p4cStore = Stores.FromPromise<P4CPicture[]>(
|
||||
picManager.startPicsRetrievalAround(
|
||||
new P4C.LatLng(options.lat, options.lon),
|
||||
searchRadius,
|
||||
{
|
||||
mindate: new Date().getTime() - maxAgeSeconds,
|
||||
towardscenter: false,
|
||||
}
|
||||
)
|
||||
)
|
||||
return p4cStore.map(
|
||||
(images) => {
|
||||
if (images === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const beforeFilterCount = images.length
|
||||
if (!options?.allowSpherical?.data) {
|
||||
images = images?.filter((i) => i.details.isSpherical !== true)
|
||||
}
|
||||
|
||||
const shownRadius = options?.shownRadius?.data ?? searchRadius
|
||||
if (shownRadius !== searchRadius) {
|
||||
images = images.filter((i) => {
|
||||
const d = GeoOperations.distanceBetween(
|
||||
[i.coordinates.lng, i.coordinates.lat],
|
||||
[options.lon, options.lat]
|
||||
)
|
||||
return d <= shownRadius
|
||||
})
|
||||
}
|
||||
if (options.towardscenter?.data) {
|
||||
images = images.filter((i) => {
|
||||
if (i.direction === undefined || isNaN(i.direction)) {
|
||||
return false
|
||||
}
|
||||
const bearing = GeoOperations.bearing(
|
||||
[i.coordinates.lng, i.coordinates.lat],
|
||||
[options.lon, options.lat]
|
||||
)
|
||||
const diff = Math.abs((i.direction - bearing) % 360)
|
||||
return diff < 40
|
||||
})
|
||||
}
|
||||
|
||||
images?.sort((a, b) => {
|
||||
const distanceA = GeoOperations.distanceBetween(
|
||||
[a.coordinates.lng, a.coordinates.lat],
|
||||
[options.lon, options.lat]
|
||||
)
|
||||
const distanceB = GeoOperations.distanceBetween(
|
||||
[b.coordinates.lng, b.coordinates.lat],
|
||||
[options.lon, options.lat]
|
||||
)
|
||||
return distanceA - distanceB
|
||||
})
|
||||
|
||||
return { images, beforeFilter: beforeFilterCount }
|
||||
},
|
||||
[options.blacklist, options.allowSpherical, options.towardscenter, options.shownRadius]
|
||||
)
|
||||
}
|
||||
|
||||
private update() {
|
||||
const seen: Set<string> = new Set<string>(this._options.blacklist.data.map((d) => d.url))
|
||||
let beforeFilter = 0
|
||||
let result: P4CPicture[] = []
|
||||
for (const source of this.individualStores) {
|
||||
const imgs = source.data
|
||||
if (imgs === undefined) {
|
||||
continue
|
||||
}
|
||||
beforeFilter = beforeFilter + imgs.beforeFilter
|
||||
for (const img of imgs.images) {
|
||||
if (seen.has(img.pictureUrl)) {
|
||||
continue
|
||||
}
|
||||
seen.add(img.pictureUrl)
|
||||
result.push(img)
|
||||
}
|
||||
}
|
||||
const c = [this._options.lon, this._options.lat]
|
||||
result.sort((a, b) => {
|
||||
const da = GeoOperations.distanceBetween([a.coordinates.lng, a.coordinates.lat], c)
|
||||
const db = GeoOperations.distanceBetween([b.coordinates.lng, b.coordinates.lat], c)
|
||||
return da - db
|
||||
})
|
||||
if (Utils.sameList(result, this._store.data)) {
|
||||
// return
|
||||
}
|
||||
this._store.setData(result)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts pictures from currently loaded features
|
||||
*/
|
||||
class ImagesInLoadedDataFetcher {
|
||||
private indexedFeatures: IndexedFeatureSource
|
||||
|
||||
constructor(indexedFeatures: IndexedFeatureSource) {
|
||||
this.indexedFeatures = indexedFeatures
|
||||
}
|
||||
|
||||
public fetchAround(loc: { lon: number; lat: number; searchRadius?: number }): P4CPicture[] {
|
||||
const foundImages: P4CPicture[] = []
|
||||
this.indexedFeatures.features.data.forEach((feature) => {
|
||||
const props = feature.properties
|
||||
const images = []
|
||||
if (props.image) {
|
||||
images.push(props.image)
|
||||
}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (props["image:" + i]) {
|
||||
images.push(props["image:" + i])
|
||||
}
|
||||
}
|
||||
if (images.length == 0) {
|
||||
return
|
||||
}
|
||||
const centerpoint = GeoOperations.centerpointCoordinates(feature)
|
||||
const d = GeoOperations.distanceBetween(centerpoint, [loc.lon, loc.lat])
|
||||
if (loc.searchRadius !== undefined && d > loc.searchRadius) {
|
||||
return
|
||||
}
|
||||
for (const image of images) {
|
||||
foundImages.push({
|
||||
pictureUrl: image,
|
||||
thumbUrl: image,
|
||||
coordinates: { lng: centerpoint[0], lat: centerpoint[1] },
|
||||
provider: "OpenStreetMap",
|
||||
details: {
|
||||
isSpherical: false,
|
||||
},
|
||||
osmTags: { image },
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return foundImages
|
||||
}
|
||||
}
|
|
@ -1,14 +1,13 @@
|
|||
import { Utils } from "../Utils"
|
||||
import * as meta from "../../package.json"
|
||||
import { Utils } from "../Utils"
|
||||
|
||||
export type PriviligedLayerType = (typeof Constants.priviliged_layers)[number]
|
||||
|
||||
export default class Constants {
|
||||
public static vNumber = meta.version
|
||||
|
||||
public static ImgurApiKey = "7070e7167f0a25a"
|
||||
public static readonly mapillary_client_token_v4 =
|
||||
"MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
|
||||
public static ImgurApiKey = meta.config.api_keys.imgur
|
||||
public static readonly mapillary_client_token_v4 = meta.config.api_keys.mapillary_v4
|
||||
|
||||
/**
|
||||
* API key for Maproulette
|
||||
|
@ -19,15 +18,7 @@ export default class Constants {
|
|||
*/
|
||||
public static readonly MaprouletteApiKey = ""
|
||||
|
||||
public static defaultOverpassUrls = [
|
||||
// The official instance, 10000 queries per day per project allowed
|
||||
"https://overpass-api.de/api/interpreter",
|
||||
// 'Fair usage'
|
||||
"https://overpass.kumi.systems/api/interpreter",
|
||||
// Offline: "https://overpass.nchc.org.tw/api/interpreter",
|
||||
"https://overpass.openstreetmap.ru/cgi/interpreter",
|
||||
// Doesn't support nwr: "https://overpass.openstreetmap.fr/api/interpreter"
|
||||
]
|
||||
public static defaultOverpassUrls = meta.config.default_overpass_urls
|
||||
|
||||
public static readonly added_by_default = [
|
||||
"selected_element",
|
||||
|
@ -100,6 +91,7 @@ export default class Constants {
|
|||
"etymology",
|
||||
"food",
|
||||
"cafes_and_pubs",
|
||||
"shops",
|
||||
"playgrounds",
|
||||
"hailhydrant",
|
||||
"toilets",
|
||||
|
@ -113,9 +105,8 @@ export default class Constants {
|
|||
* In seconds
|
||||
*/
|
||||
static zoomToLocationTimeout = 15
|
||||
static countryCoderEndpoint: string =
|
||||
"https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country"
|
||||
public static readonly OsmPreferenceKeyPicturesLicense = "pictures-license"
|
||||
static countryCoderEndpoint: string = meta.config.country_coder_host
|
||||
|
||||
/**
|
||||
* These are the values that are allowed to use as 'backdrop' icon for a map pin
|
||||
*/
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -490,9 +490,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
usingLayer = [
|
||||
new Title("Themes using this layer", 4),
|
||||
new List(
|
||||
(usedInThemes ?? []).map(
|
||||
(id) => new Link(id, "https://mapcomplete.org/" + id)
|
||||
)
|
||||
(usedInThemes ?? []).map((id) => new Link(id, "https://mapcomplete.org/" + id))
|
||||
),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -247,7 +247,10 @@ export default class TagRenderingConfig {
|
|||
if (txt === "") {
|
||||
throw context + " Rendering for language " + ln + " is empty"
|
||||
}
|
||||
if (txt.indexOf("{" + this.freeform.key + "}") >= 0) {
|
||||
if (
|
||||
txt.indexOf("{" + this.freeform.key + "}") >= 0 ||
|
||||
txt.indexOf("&LBRACE" + this.freeform.key + "&RBRACE")
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (txt.indexOf("{" + this.freeform.key + ":") >= 0) {
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
class={twMerge(options.extraClasses, "button text-ellipsis")}
|
||||
{href}
|
||||
target={newTab ? "_blank" : undefined}
|
||||
rel={newTab ? "noopener" : undefined}
|
||||
>
|
||||
<slot name="image">
|
||||
{#if imageUrl !== undefined}
|
||||
|
|
|
@ -29,7 +29,7 @@ export default class Table extends BaseUIElement {
|
|||
const header = Utils.NoNull(headerMarkdownParts).join(" | ")
|
||||
const headerSep = headerMarkdownParts.map((part) => "-".repeat(part.length + 2)).join(" | ")
|
||||
const table = this._contents
|
||||
.map((row) => row.map((el) => el?.AsMarkdown()?.replace("|", "\\|") ?? " ").join(" | "))
|
||||
.map((row) => row.map((el) => el?.AsMarkdown()?.replaceAll("\\","\\\\")?.replaceAll("|", "\\|") ?? " ").join(" | "))
|
||||
.join("\n")
|
||||
|
||||
return "\n\n" + [header, headerSep, table, ""].join("\n")
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
src={`https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/community_index/${resource.type}.svg`}
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<a href={resource.resolved.url} target="_blank" rel="noreferrer nofollow" class="font-bold">
|
||||
<a href={resource.resolved.url} target="_blank" rel="noreferrer nofollow noopener" class="font-bold">
|
||||
{resource.resolved.name ?? resource.resolved.url}
|
||||
</a>
|
||||
{resource.resolved?.description}
|
||||
|
|
|
@ -102,7 +102,7 @@ export default class CopyrightPanel extends Combine {
|
|||
let bgAttr: BaseUIElement | string = undefined
|
||||
if (attrText && attrUrl) {
|
||||
bgAttr =
|
||||
"<a href='" + attrUrl + "' target='_blank'>" + attrText + "</a>"
|
||||
"<a href='" + attrUrl + "' target='_blank' rel='noopener'>" + attrText + "</a>"
|
||||
} else if (attrUrl) {
|
||||
bgAttr = attrUrl
|
||||
} else {
|
||||
|
|
|
@ -16,7 +16,7 @@ export class OpenJosm extends Combine {
|
|||
|
||||
const josmState = new UIEventSource<string>(undefined)
|
||||
// Reset after 15s
|
||||
josmState.stabilized(15000).addCallbackD((_) => josmState.setData(undefined))
|
||||
josmState.stabilized(15000).addCallbackD(() => josmState.setData(undefined))
|
||||
|
||||
const stateIndication = new VariableUiElement(
|
||||
josmState.map((state) => {
|
||||
|
@ -45,7 +45,7 @@ export class OpenJosm extends Combine {
|
|||
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
|
||||
Utils.download(josmLink)
|
||||
.then((answer) => josmState.setData(answer.replace(/\n/g, "").trim()))
|
||||
.catch((_) => josmState.setData("ERROR"))
|
||||
.catch(() => josmState.setData("ERROR"))
|
||||
})
|
||||
.SetClass("w-full"),
|
||||
undefined,
|
||||
|
|
|
@ -1,108 +1,105 @@
|
|||
<script lang="ts">/**
|
||||
* A screen showing:
|
||||
* - A link to share the current view
|
||||
* - Some query parameters that can be enabled/disabled
|
||||
* - The code to embed MC as IFrame
|
||||
*/
|
||||
<script lang="ts">
|
||||
/**
|
||||
* A screen showing:
|
||||
* - A link to share the current view
|
||||
* - Some query parameters that can be enabled/disabled
|
||||
* - The code to embed MC as IFrame
|
||||
*/
|
||||
|
||||
import ThemeViewState from "../../Models/ThemeViewState";
|
||||
import { QueryParameters } from "../../Logic/Web/QueryParameters";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import { Utils } from "../../Utils";
|
||||
import Svg from "../../Svg";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import { DocumentDuplicateIcon } from "@rgossiaux/svelte-heroicons/outline";
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import { QueryParameters } from "../../Logic/Web/QueryParameters"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Utils } from "../../Utils"
|
||||
import Svg from "../../Svg"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import { DocumentDuplicateIcon } from "@rgossiaux/svelte-heroicons/outline"
|
||||
|
||||
export let state: ThemeViewState;
|
||||
const tr = Translations.t.general.sharescreen;
|
||||
export let state: ThemeViewState
|
||||
const tr = Translations.t.general.sharescreen
|
||||
|
||||
let url = window.location;
|
||||
let linkToShare: string = undefined;
|
||||
/**
|
||||
* In some cases (local deploys, custom themes), we need to set the URL to `/theme.html?layout=xyz` instead of `/xyz?...`
|
||||
*/
|
||||
let needsThemeRedirect = url.port !== "" || url.hostname.match(/^[0-9]/) || !state.layout.official;
|
||||
let layoutId = state.layout.id;
|
||||
let baseLink = url.protocol + "//" + url.host + "/" + (needsThemeRedirect ? "theme.html?layout=" + layoutId + "&" : layoutId + "?");
|
||||
let url = window.location
|
||||
let linkToShare: string = undefined
|
||||
/**
|
||||
* In some cases (local deploys, custom themes), we need to set the URL to `/theme.html?layout=xyz` instead of `/xyz?...`
|
||||
*/
|
||||
let needsThemeRedirect = url.port !== "" || url.hostname.match(/^[0-9]/) || !state.layout.official
|
||||
let layoutId = state.layout.id
|
||||
let baseLink =
|
||||
url.protocol +
|
||||
"//" +
|
||||
url.host +
|
||||
"/" +
|
||||
(needsThemeRedirect ? "theme.html?layout=" + layoutId + "&" : layoutId + "?")
|
||||
|
||||
let showWelcomeMessage = true;
|
||||
let enableLogin = true;
|
||||
$: {
|
||||
const layout = state.layout;
|
||||
let excluded = Utils.NoNull([
|
||||
showWelcomeMessage ? undefined : "fs-welcome-message",
|
||||
enableLogin ? undefined : "fs-enable-login"
|
||||
]);
|
||||
linkToShare = baseLink + QueryParameters.GetParts(new Set(excluded))
|
||||
.concat(excluded.map(k => k + "=" + false))
|
||||
.join("&");
|
||||
if (layout.definitionRaw !== undefined) {
|
||||
linkToShare += "&userlayout=" + (layout.definedAtUrl ?? layout.id);
|
||||
let showWelcomeMessage = true
|
||||
let enableLogin = true
|
||||
$: {
|
||||
const layout = state.layout
|
||||
let excluded = Utils.NoNull([
|
||||
showWelcomeMessage ? undefined : "fs-welcome-message",
|
||||
enableLogin ? undefined : "fs-enable-login",
|
||||
])
|
||||
linkToShare =
|
||||
baseLink +
|
||||
QueryParameters.GetParts(new Set(excluded))
|
||||
.concat(excluded.map((k) => k + "=" + false))
|
||||
.join("&")
|
||||
if (layout.definitionRaw !== undefined) {
|
||||
linkToShare += "&userlayout=" + (layout.definedAtUrl ?? layout.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function shareCurrentLink() {
|
||||
await navigator.share({
|
||||
title: Translations.W(state.layout.title)?.ConstructElement().textContent ?? "MapComplete",
|
||||
text: Translations.W(state.layout.description)?.ConstructElement().textContent ?? "",
|
||||
url: linkToShare,
|
||||
})
|
||||
}
|
||||
|
||||
async function shareCurrentLink() {
|
||||
await navigator.share({
|
||||
title: Translations.W(state.layout.title)?.ConstructElement().textContent ?? "MapComplete",
|
||||
text: Translations.W(state.layout.description)?.ConstructElement().textContent ?? "",
|
||||
url: linkToShare
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
let isCopied = false;
|
||||
|
||||
async function copyCurrentLink() {
|
||||
await navigator.clipboard.writeText(linkToShare);
|
||||
isCopied = true;
|
||||
await Utils.waitFor(5000);
|
||||
isCopied = false;
|
||||
}
|
||||
let isCopied = false
|
||||
|
||||
async function copyCurrentLink() {
|
||||
await navigator.clipboard.writeText(linkToShare)
|
||||
isCopied = true
|
||||
await Utils.waitFor(5000)
|
||||
isCopied = false
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
<Tr t={tr.intro} />
|
||||
<div class="flex">
|
||||
{#if typeof navigator?.share === "function"}
|
||||
<button class="w-8 h-8 p-1 shrink-0" on:click={shareCurrentLink}>
|
||||
<button class="h-8 w-8 shrink-0 p-1" on:click={shareCurrentLink}>
|
||||
<ToSvelte construct={Svg.share_svg()} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if navigator.clipboard !== undefined}
|
||||
<button class="w-8 h-8 p-1 shrink-0 no-image-background" on:click={copyCurrentLink}>
|
||||
<button class="no-image-background h-8 w-8 shrink-0 p-1" on:click={copyCurrentLink}>
|
||||
<DocumentDuplicateIcon />
|
||||
</button>
|
||||
{/if}
|
||||
<div class="literal-code" on:click={e => Utils.selectTextIn(e.target)}>
|
||||
<div class="literal-code" on:click={(e) => Utils.selectTextIn(e.target)}>
|
||||
{linkToShare}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
|
||||
{#if isCopied}
|
||||
<Tr t={tr.copiedToClipboard} cls="thanks m-2" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Tr t={tr.embedIntro} />
|
||||
|
||||
<Tr t={ tr.embedIntro} />
|
||||
|
||||
|
||||
<div class="flex flex-col my-1 link-underline">
|
||||
|
||||
<div class="link-underline my-1 flex flex-col">
|
||||
<label>
|
||||
<input bind:checked={showWelcomeMessage} type="checkbox" />
|
||||
<Tr t={tr.fsWelcomeMessage} />
|
||||
</label>
|
||||
|
||||
|
||||
<label>
|
||||
<input bind:checked={enableLogin} type="checkbox" />
|
||||
<Tr t={tr.fsUserbadge} />
|
||||
|
@ -111,11 +108,15 @@ async function copyCurrentLink() {
|
|||
|
||||
<div class="literal-code m-1">
|
||||
<span class="literal-code iframe-code-block"> <br />
|
||||
<iframe src="${url}" <br />
|
||||
allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px" <br />
|
||||
title="${state.layout.title?.txt ?? "MapComplete" } with MapComplete"> <br />
|
||||
</iframe> <br />
|
||||
<iframe src="${url}"
|
||||
<br />
|
||||
allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px"
|
||||
<br />
|
||||
title="${state.layout.title?.txt ?? "MapComplete"} with MapComplete">
|
||||
<br />
|
||||
</iframe>
|
||||
<br />
|
||||
</span>
|
||||
</div>
|
||||
<Tr t={tr.documentation} cls="link-underline"/>
|
||||
<Tr t={tr.documentation} cls="link-underline" />
|
||||
</div>
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
<Tr t={layout.description} />
|
||||
<Tr t={Translations.t.general.welcomeExplanation.general} />
|
||||
{#if layout.layers.some((l) => l.presets?.length > 0)}
|
||||
<Tr t={Translations.t.general.welcomeExplanation.addNew} />
|
||||
<Tr t={Translations.t.general.welcomeExplanation.addNew} />
|
||||
{/if}
|
||||
|
||||
<Tr t={layout.descriptionTail} />
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
<a
|
||||
href={osmConnection.Backend() + "/profile/edit"}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="link-no-underline flex items-center self-end"
|
||||
>
|
||||
<PencilAltIcon slot="image" class="h-8 w-8 p-2" />
|
||||
|
|
|
@ -81,7 +81,7 @@
|
|||
mimetype="image/png"
|
||||
mainText={t.downloadAsPng}
|
||||
helperText={t.downloadAsPngHelper}
|
||||
construct={(_) => state.mapProperties.exportAsPng(4)}
|
||||
construct={() => state.mapProperties.exportAsPng(4)}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col">
|
||||
|
|
|
@ -73,7 +73,7 @@ export class ImageUploadFlow extends Toggle {
|
|||
]).SetClass("w-full flex justify-center items-center")
|
||||
|
||||
const licenseStore = state?.osmConnection?.GetPreference(
|
||||
Constants.OsmPreferenceKeyPicturesLicense,
|
||||
"pictures-license",
|
||||
"CC0"
|
||||
)
|
||||
|
||||
|
|
|
@ -10,11 +10,11 @@
|
|||
import UnitInput from "../Popup/UnitInput.svelte"
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
export let type: ValidatorType
|
||||
export let feedback: UIEventSource<Translation> | undefined
|
||||
export let getCountry: () => string | undefined
|
||||
export let placeholder: string | Translation | undefined
|
||||
export let unit: Unit = undefined
|
||||
export let type: ValidatorType
|
||||
export let feedback: UIEventSource<Translation> | undefined = undefined
|
||||
export let getCountry: () => string | undefined
|
||||
export let placeholder: string | Translation | undefined
|
||||
export let unit: Unit = undefined
|
||||
|
||||
export let value: UIEventSource<string>
|
||||
/**
|
||||
|
|
|
@ -53,6 +53,7 @@ export default class Validators {
|
|||
"icon",
|
||||
"fediverse",
|
||||
"tag",
|
||||
"fediverse",
|
||||
] as const
|
||||
|
||||
public static readonly AllValidators: ReadonlyArray<Validator> = [
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import {Validator} from "../Validator"
|
||||
import {Translation} from "../../i18n/Translation";
|
||||
import Translations from "../../i18n/Translations";
|
||||
import { Validator } from "../Validator"
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
|
||||
export default class FediverseValidator extends Validator {
|
||||
|
||||
public static readonly usernameAtServer: RegExp = /^@?(\w+)@((\w|\.)+)$/
|
||||
|
||||
constructor() {
|
||||
super("fediverse", "Validates fediverse addresses and normalizes them into `@username@server`-format");
|
||||
super(
|
||||
"fediverse",
|
||||
"Validates fediverse addresses and normalizes them into `@username@server`-format"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -15,8 +17,8 @@ export default class FediverseValidator extends Validator {
|
|||
* @param s
|
||||
*/
|
||||
reformat(s: string): string {
|
||||
if(!s.startsWith("@")){
|
||||
s = "@"+s
|
||||
if (!s.startsWith("@")) {
|
||||
s = "@" + s
|
||||
}
|
||||
if (s.match(FediverseValidator.usernameAtServer)) {
|
||||
return s
|
||||
|
@ -25,39 +27,38 @@ export default class FediverseValidator extends Validator {
|
|||
const url = new URL(s)
|
||||
const path = url.pathname
|
||||
if (path.match(/^\/\w+$/)) {
|
||||
return `@${path.substring(1)}@${url.hostname}`;
|
||||
return `@${path.substring(1)}@${url.hostname}`
|
||||
}
|
||||
} catch (e) {
|
||||
// Nothing to do here
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
getFeedback(s: string): Translation | undefined {
|
||||
const match = s.match(FediverseValidator.usernameAtServer)
|
||||
console.log("Match:", match)
|
||||
if (match) {
|
||||
const host = match[2]
|
||||
getFeedback(s: string): Translation | undefined {
|
||||
const match = s.match(FediverseValidator.usernameAtServer)
|
||||
console.log("Match:", match)
|
||||
if (match) {
|
||||
const host = match[2]
|
||||
try {
|
||||
const url = new URL("https://" + host)
|
||||
return undefined
|
||||
} catch (e) {
|
||||
return Translations.t.validation.fediverse.invalidHost.Subs({ host })
|
||||
}
|
||||
}
|
||||
try {
|
||||
const url = new URL("https://" + host)
|
||||
return undefined
|
||||
const url = new URL(s)
|
||||
const path = url.pathname
|
||||
if (path.match(/^\/\w+$/)) {
|
||||
return undefined
|
||||
}
|
||||
} catch (e) {
|
||||
return Translations.t.validation.fediverse.invalidHost.Subs({host})
|
||||
// Nothing to do here
|
||||
}
|
||||
return Translations.t.validation.fediverse.feedback
|
||||
}
|
||||
try {
|
||||
const url = new URL(s)
|
||||
const path = url.pathname
|
||||
if (path.match(/^\/\w+$/)) {
|
||||
return undefined
|
||||
}
|
||||
} catch (e) {
|
||||
// Nothing to do here
|
||||
}
|
||||
return Translations.t.validation.fediverse.feedback
|
||||
}
|
||||
|
||||
isValid(s): boolean {
|
||||
return this.getFeedback(s) === undefined
|
||||
|
||||
return this.getFeedback(s) === undefined
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import { Validator } from "../Validator"
|
||||
import { ValidatorType } from "../Validators";
|
||||
import { ValidatorType } from "../Validators"
|
||||
|
||||
export default class FloatValidator extends Validator {
|
||||
inputmode: "decimal" = "decimal"
|
||||
|
|
|
@ -172,7 +172,8 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
tileSize: layer["tile-size"] ?? 256,
|
||||
minzoom: layer["min_zoom"] ?? 1,
|
||||
maxzoom: layer["max_zoom"] ?? 25,
|
||||
// scheme: background["type"] === "tms" ? "tms" : "xyz",
|
||||
// Bit of a hack, but seems to work
|
||||
scheme: layer.url.includes("{-y}") ? "tms" : "xyz",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,6 +204,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
"{width}": "" + size,
|
||||
"{height}": "" + size,
|
||||
"{zoom}": "{z}",
|
||||
"{-y}": "{y}",
|
||||
}
|
||||
|
||||
for (const key in toReplace) {
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
<script lang="ts">
|
||||
import LoginToggle from "../../Base/LoginToggle.svelte";
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
||||
import Translations from "../../i18n/Translations";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import { TrashIcon } from "@babeard/svelte-heroicons/mini";
|
||||
import type { OsmId, OsmTags } from "../../../Models/OsmFeature";
|
||||
import DeleteConfig from "../../../Models/ThemeConfig/DeleteConfig";
|
||||
import TagRenderingQuestion from "../TagRendering/TagRenderingQuestion.svelte";
|
||||
import type { Feature } from "geojson";
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter";
|
||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils";
|
||||
import OsmChangeAction from "../../../Logic/Osm/Actions/OsmChangeAction";
|
||||
import DeleteAction from "../../../Logic/Osm/Actions/DeleteAction";
|
||||
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction";
|
||||
import Loading from "../../Base/Loading.svelte";
|
||||
import { DeleteFlowState } from "./DeleteFlowState";
|
||||
import { twJoin } from "tailwind-merge";
|
||||
import LoginToggle from "../../Base/LoginToggle.svelte"
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import Tr from "../../Base/Tr.svelte"
|
||||
import { TrashIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import type { OsmId, OsmTags } from "../../../Models/OsmFeature"
|
||||
import DeleteConfig from "../../../Models/ThemeConfig/DeleteConfig"
|
||||
import TagRenderingQuestion from "../TagRendering/TagRenderingQuestion.svelte"
|
||||
import type { Feature } from "geojson"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
|
||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
import OsmChangeAction from "../../../Logic/Osm/Actions/OsmChangeAction"
|
||||
import DeleteAction from "../../../Logic/Osm/Actions/DeleteAction"
|
||||
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
|
||||
import Loading from "../../Base/Loading.svelte"
|
||||
import { DeleteFlowState } from "./DeleteFlowState"
|
||||
import { twJoin } from "tailwind-merge"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let deleteConfig: DeleteConfig
|
||||
|
|
73
src/UI/Popup/LinkableImage.svelte
Normal file
73
src/UI/Popup/LinkableImage.svelte
Normal file
|
@ -0,0 +1,73 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import type { OsmTags } from "../../Models/OsmFeature";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import { AttributedImage } from "../Image/AttributedImage";
|
||||
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders";
|
||||
import LinkPicture from "../../Logic/Osm/Actions/LinkPicture";
|
||||
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
|
||||
import { Tag } from "../../Logic/Tags/Tag";
|
||||
import { GeoOperations } from "../../Logic/GeoOperations";
|
||||
import type { Feature } from "geojson";
|
||||
import Translations from "../i18n/Translations";
|
||||
import SpecialTranslation from "./TagRendering/SpecialTranslation.svelte";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
|
||||
export let tags: Store<OsmTags>;
|
||||
export let lon: number;
|
||||
export let lat: number;
|
||||
export let state: SpecialVisualizationState;
|
||||
export let image: P4CPicture;
|
||||
export let feature: Feature;
|
||||
export let layer: LayerConfig;
|
||||
|
||||
export let linkable = true;
|
||||
let isLinked = false;
|
||||
|
||||
const t = Translations.t.image.nearby;
|
||||
const c = [lon, lat];
|
||||
let attributedImage = new AttributedImage({
|
||||
url: image.thumbUrl ?? image.pictureUrl,
|
||||
provider: AllImageProviders.byName(image.provider),
|
||||
date: new Date(image.date)
|
||||
});
|
||||
let distance = Math.round(GeoOperations.distanceBetween([image.coordinates.lng, image.coordinates.lat], c));
|
||||
$: {
|
||||
const currentTags = tags.data;
|
||||
const key = Object.keys(image.osmTags)[0];
|
||||
const url = image.osmTags[key];
|
||||
if (isLinked) {
|
||||
const action = new LinkPicture(
|
||||
currentTags.id,
|
||||
key,
|
||||
url,
|
||||
currentTags,
|
||||
{
|
||||
theme: state.layout.id,
|
||||
changeType: "link-image"
|
||||
}
|
||||
);
|
||||
state.changes.applyAction(action);
|
||||
} else {
|
||||
for (const k in currentTags) {
|
||||
const v = currentTags[k];
|
||||
if (v === url) {
|
||||
const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, { theme: state.layout.id, changeType: "remove-image" });
|
||||
state.changes.applyAction(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<div class="flex flex-col w-fit shrink-0">
|
||||
<ToSvelte construct={attributedImage.SetClass("h-48 w-fit")} />
|
||||
{#if linkable}
|
||||
<label>
|
||||
<input bind:checked={isLinked} type="checkbox">
|
||||
<SpecialTranslation t={t.link} {tags} {state} {layer} {feature} />
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
54
src/UI/Popup/NearbyImages.svelte
Normal file
54
src/UI/Popup/NearbyImages.svelte
Normal file
|
@ -0,0 +1,54 @@
|
|||
<script lang="ts">/**
|
||||
* Show nearby images which can be clicked
|
||||
*/
|
||||
import type { OsmTags } from "../../Models/OsmFeature";
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import type { P4CPicture } from "../../Logic/Web/NearbyImagesSearch";
|
||||
import NearbyImagesSearch from "../../Logic/Web/NearbyImagesSearch";
|
||||
import LinkableImage from "./LinkableImage.svelte";
|
||||
import type { Feature } from "geojson";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import Loading from "../Base/Loading.svelte";
|
||||
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
export let tags: Store<OsmTags>;
|
||||
export let state: SpecialVisualizationState;
|
||||
export let lon: number;
|
||||
export let lat: number;
|
||||
export let feature: Feature;
|
||||
|
||||
export let linkable: boolean = true;
|
||||
export let layer: LayerConfig;
|
||||
|
||||
let imagesProvider = new NearbyImagesSearch({
|
||||
lon, lat, allowSpherical: new UIEventSource<boolean>(false),
|
||||
blacklist: AllImageProviders.LoadImagesFor(tags)
|
||||
}, state.indexedFeatures);
|
||||
|
||||
let images: Store<P4CPicture[]> = imagesProvider.store.map(images => images.slice(0, 20));
|
||||
|
||||
</script>
|
||||
|
||||
<div class="interactive rounded-2xl border-interactive p-2">
|
||||
<div class="flex justify-between">
|
||||
|
||||
<h4>
|
||||
<Tr t={Translations.t.image.nearby.title} />
|
||||
</h4>
|
||||
<slot name="corner" />
|
||||
</div>
|
||||
{#if $images.length === 0}
|
||||
<Loading />
|
||||
{:else}
|
||||
<div class="overflow-x-auto w-full flex space-x-1" style="scroll-snap-type: x proximity">
|
||||
{#each $images as image (image.pictureUrl)}
|
||||
<span class="w-fit shrink-0" style="scroll-snap-align: start">
|
||||
<LinkableImage {tags} {image} {state} {lon} {lat} {feature} {layer} {linkable} />
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
|
@ -1,316 +0,0 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SlideShow } from "../Image/SlideShow"
|
||||
import { ClickableToggle } from "../Input/Toggle"
|
||||
import Loading from "../Base/Loading"
|
||||
import { AttributedImage } from "../Image/AttributedImage"
|
||||
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"
|
||||
import Svg from "../../Svg"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { InputElement } from "../Input/InputElement"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Mapillary } from "../../Logic/ImageProviders/Mapillary"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import Lazy from "../Base/Lazy"
|
||||
import P4C from "pic4carto"
|
||||
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
|
||||
export interface P4CPicture {
|
||||
pictureUrl: string
|
||||
date?: number
|
||||
coordinates: { lat: number; lng: number }
|
||||
provider: "Mapillary" | string
|
||||
author?
|
||||
license?
|
||||
detailsUrl?: string
|
||||
direction?
|
||||
osmTags?: object /*To copy straight into OSM!*/
|
||||
thumbUrl: string
|
||||
details: {
|
||||
isSpherical: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface NearbyImageOptions {
|
||||
lon: number
|
||||
lat: number
|
||||
// Radius of the upstream search
|
||||
searchRadius?: 500 | number
|
||||
maxDaysOld?: 1095 | number
|
||||
blacklist: Store<{ url: string }[]>
|
||||
shownImagesCount?: UIEventSource<number>
|
||||
towardscenter?: UIEventSource<boolean>
|
||||
allowSpherical?: UIEventSource<boolean>
|
||||
// Radius of what is shown. Useless to select a value > searchRadius; defaults to searchRadius
|
||||
shownRadius?: UIEventSource<number>
|
||||
}
|
||||
|
||||
class ImagesInLoadedDataFetcher {
|
||||
private indexedFeatures: IndexedFeatureSource
|
||||
|
||||
constructor(indexedFeatures: IndexedFeatureSource) {
|
||||
this.indexedFeatures = indexedFeatures
|
||||
}
|
||||
|
||||
public fetchAround(loc: { lon: number; lat: number; searchRadius?: number }): P4CPicture[] {
|
||||
const foundImages: P4CPicture[] = []
|
||||
this.indexedFeatures.features.data.forEach((feature) => {
|
||||
const props = feature.properties
|
||||
const images = []
|
||||
if (props.image) {
|
||||
images.push(props.image)
|
||||
}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (props["image:" + i]) {
|
||||
images.push(props["image:" + i])
|
||||
}
|
||||
}
|
||||
if (images.length == 0) {
|
||||
return
|
||||
}
|
||||
const centerpoint = GeoOperations.centerpointCoordinates(feature)
|
||||
const d = GeoOperations.distanceBetween(centerpoint, [loc.lon, loc.lat])
|
||||
if (loc.searchRadius !== undefined && d > loc.searchRadius) {
|
||||
return
|
||||
}
|
||||
for (const image of images) {
|
||||
foundImages.push({
|
||||
pictureUrl: image,
|
||||
thumbUrl: image,
|
||||
coordinates: { lng: centerpoint[0], lat: centerpoint[1] },
|
||||
provider: "OpenStreetMap",
|
||||
details: {
|
||||
isSpherical: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
const cleaned: P4CPicture[] = []
|
||||
const seen = new Set<string>()
|
||||
for (const foundImage of foundImages) {
|
||||
if (seen.has(foundImage.pictureUrl)) {
|
||||
continue
|
||||
}
|
||||
seen.add(foundImage.pictureUrl)
|
||||
cleaned.push(foundImage)
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
}
|
||||
|
||||
export default class NearbyImages extends Lazy {
|
||||
constructor(options: NearbyImageOptions, state?: IndexedFeatureSource) {
|
||||
super(() => {
|
||||
const t = Translations.t.image.nearbyPictures
|
||||
const shownImages = options.shownImagesCount ?? new UIEventSource(25)
|
||||
|
||||
const loadedPictures = NearbyImages.buildPictureFetcher(options, state)
|
||||
|
||||
const loadMoreButton = new Combine([
|
||||
new SubtleButton(Svg.add_svg(), t.loadMore).onClick(() => {
|
||||
shownImages.setData(shownImages.data + 25)
|
||||
}),
|
||||
]).SetClass("flex flex-col justify-center")
|
||||
|
||||
const imageElements = loadedPictures.map(
|
||||
(imgs) => {
|
||||
if (imgs === undefined) {
|
||||
return []
|
||||
}
|
||||
const elements = (imgs.images ?? [])
|
||||
.slice(0, shownImages.data)
|
||||
.map((i) => this.prepareElement(i))
|
||||
if (imgs.images !== undefined && elements.length < imgs.images.length) {
|
||||
// We effectively sliced some items, so we can increase the count
|
||||
elements.push(loadMoreButton)
|
||||
}
|
||||
return elements
|
||||
},
|
||||
[shownImages]
|
||||
)
|
||||
|
||||
return new VariableUiElement(
|
||||
loadedPictures.map((loaded) => {
|
||||
if (loaded?.images === undefined) {
|
||||
return NearbyImages.NoImagesView(new Loading(t.loading)).SetClass(
|
||||
"animate-pulse"
|
||||
)
|
||||
}
|
||||
const images = loaded.images
|
||||
const beforeFilter = loaded?.beforeFilter
|
||||
if (beforeFilter === 0) {
|
||||
return NearbyImages.NoImagesView(t.nothingFound.SetClass("alert block"))
|
||||
} else if (images.length === 0) {
|
||||
const removeFiltersButton = new SubtleButton(
|
||||
Svg.filter_disable_svg(),
|
||||
t.removeFilters
|
||||
).onClick(() => {
|
||||
options.shownRadius.setData(options.searchRadius)
|
||||
options.allowSpherical.setData(true)
|
||||
options.towardscenter.setData(false)
|
||||
})
|
||||
|
||||
return NearbyImages.NoImagesView(
|
||||
t.allFiltered.SetClass("font-bold"),
|
||||
removeFiltersButton
|
||||
)
|
||||
}
|
||||
|
||||
return new SlideShow(imageElements)
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private static NoImagesView(...elems: BaseUIElement[]) {
|
||||
return new Combine(elems)
|
||||
.SetClass("flex flex-col justify-center items-center bg-gray-200 mb-2 rounded-lg")
|
||||
.SetStyle(
|
||||
"height: calc( var(--image-carousel-height) - 0.5rem ) ; max-height: calc( var(--image-carousel-height) - 0.5rem );"
|
||||
)
|
||||
}
|
||||
|
||||
private static buildPictureFetcher(options: NearbyImageOptions, state?: IndexedFeatureSource) {
|
||||
const picManager = new P4C.PicturesManager({})
|
||||
const searchRadius = options.searchRadius ?? 500
|
||||
|
||||
const nearbyImages =
|
||||
state !== undefined ? new ImagesInLoadedDataFetcher(state).fetchAround(options) : []
|
||||
|
||||
return Stores.FromPromise<P4CPicture[]>(
|
||||
picManager.startPicsRetrievalAround(
|
||||
new P4C.LatLng(options.lat, options.lon),
|
||||
options.searchRadius ?? 500,
|
||||
{
|
||||
mindate:
|
||||
new Date().getTime() -
|
||||
(options.maxDaysOld ?? 3 * 365) * 24 * 60 * 60 * 1000,
|
||||
towardscenter: false,
|
||||
}
|
||||
)
|
||||
).map(
|
||||
(images) => {
|
||||
if (images === undefined) {
|
||||
return undefined
|
||||
}
|
||||
images = (images ?? []).concat(nearbyImages)
|
||||
const blacklisted = options.blacklist?.data
|
||||
images = images?.filter(
|
||||
(i) =>
|
||||
!blacklisted?.some((notAllowed) =>
|
||||
Mapillary.sameUrl(i.pictureUrl, notAllowed.url)
|
||||
)
|
||||
)
|
||||
|
||||
const beforeFilterCount = images.length
|
||||
|
||||
if (!options?.allowSpherical?.data) {
|
||||
images = images?.filter((i) => i.details.isSpherical !== true)
|
||||
}
|
||||
|
||||
const shownRadius = options?.shownRadius?.data ?? searchRadius
|
||||
if (shownRadius !== searchRadius) {
|
||||
images = images.filter((i) => {
|
||||
const d = GeoOperations.distanceBetween(
|
||||
[i.coordinates.lng, i.coordinates.lat],
|
||||
[options.lon, options.lat]
|
||||
)
|
||||
return d <= shownRadius
|
||||
})
|
||||
}
|
||||
if (options.towardscenter?.data) {
|
||||
images = images.filter((i) => {
|
||||
if (i.direction === undefined || isNaN(i.direction)) {
|
||||
return false
|
||||
}
|
||||
const bearing = GeoOperations.bearing(
|
||||
[i.coordinates.lng, i.coordinates.lat],
|
||||
[options.lon, options.lat]
|
||||
)
|
||||
const diff = Math.abs((i.direction - bearing) % 360)
|
||||
return diff < 40
|
||||
})
|
||||
}
|
||||
|
||||
images?.sort((a, b) => {
|
||||
const distanceA = GeoOperations.distanceBetween(
|
||||
[a.coordinates.lng, a.coordinates.lat],
|
||||
[options.lon, options.lat]
|
||||
)
|
||||
const distanceB = GeoOperations.distanceBetween(
|
||||
[b.coordinates.lng, b.coordinates.lat],
|
||||
[options.lon, options.lat]
|
||||
)
|
||||
return distanceA - distanceB
|
||||
})
|
||||
|
||||
return { images, beforeFilter: beforeFilterCount }
|
||||
},
|
||||
[options.blacklist, options.allowSpherical, options.towardscenter, options.shownRadius]
|
||||
)
|
||||
}
|
||||
|
||||
protected prepareElement(info: P4CPicture): BaseUIElement {
|
||||
const provider = AllImageProviders.byName(info.provider)
|
||||
return new AttributedImage({ url: info.pictureUrl, provider })
|
||||
}
|
||||
|
||||
private static asAttributedImage(info: P4CPicture): AttributedImage {
|
||||
const provider = AllImageProviders.byName(info.provider)
|
||||
return new AttributedImage({ url: info.thumbUrl, provider, date: new Date(info.date) })
|
||||
}
|
||||
|
||||
protected asToggle(info: P4CPicture): ClickableToggle {
|
||||
const imgNonSelected = NearbyImages.asAttributedImage(info)
|
||||
const imageSelected = NearbyImages.asAttributedImage(info)
|
||||
|
||||
const nonSelected = new Combine([imgNonSelected]).SetClass("relative block")
|
||||
const hoveringCheckmark = new Combine([
|
||||
Svg.confirm_svg().SetClass("block w-24 h-24 -ml-12 -mt-12"),
|
||||
]).SetClass("absolute left-1/2 top-1/2 w-0")
|
||||
const selected = new Combine([imageSelected, hoveringCheckmark]).SetClass("relative block")
|
||||
|
||||
return new ClickableToggle(selected, nonSelected).SetClass("").ToggleOnClick()
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectOneNearbyImage extends NearbyImages implements InputElement<P4CPicture> {
|
||||
private readonly value: UIEventSource<P4CPicture>
|
||||
|
||||
constructor(
|
||||
options: NearbyImageOptions & { value?: UIEventSource<P4CPicture> },
|
||||
state?: IndexedFeatureSource
|
||||
) {
|
||||
super(options, state)
|
||||
this.value = options.value ?? new UIEventSource<P4CPicture>(undefined)
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<P4CPicture> {
|
||||
return this.value
|
||||
}
|
||||
|
||||
IsValid(t: P4CPicture): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
protected prepareElement(info: P4CPicture): BaseUIElement {
|
||||
const toggle = super.asToggle(info)
|
||||
toggle.isEnabled.addCallback((enabled) => {
|
||||
if (enabled) {
|
||||
this.value.setData(info)
|
||||
} else if (this.value.data === info) {
|
||||
this.value.setData(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
this.value.addCallback((inf) => {
|
||||
if (inf !== info) {
|
||||
toggle.isEnabled.setData(false)
|
||||
}
|
||||
})
|
||||
|
||||
return toggle
|
||||
}
|
||||
}
|
36
src/UI/Popup/NearbyImagesCollapsed.svelte
Normal file
36
src/UI/Popup/NearbyImagesCollapsed.svelte
Normal file
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import type { OsmTags } from "../../Models/OsmFeature";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import type { Feature } from "geojson";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import NearbyImages from "./NearbyImages.svelte";
|
||||
import Svg from "../../Svg";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import { XCircleIcon } from "@babeard/svelte-heroicons/solid";
|
||||
import exp from "constants";
|
||||
|
||||
export let tags: Store<OsmTags>;
|
||||
export let state: SpecialVisualizationState;
|
||||
export let lon: number;
|
||||
export let lat: number;
|
||||
export let feature: Feature;
|
||||
|
||||
export let linkable: boolean = true;
|
||||
export let layer: LayerConfig;
|
||||
const t = Translations.t.image.nearby;
|
||||
|
||||
let expanded = false;
|
||||
</script>
|
||||
|
||||
{#if expanded}
|
||||
<NearbyImages {tags} {state} {lon} {lat} {feature} {linkable}>
|
||||
<XCircleIcon slot="corner" class="w-6 h-6 cursor-pointer" on:click={() => {expanded = false}}/>
|
||||
</NearbyImages>
|
||||
{:else}
|
||||
<button class="w-full flex items-center" on:click={() => { expanded = true; }}>
|
||||
<ToSvelte construct={ Svg.camera_plus_svg().SetClass("block w-8 h-8 p-1 mr-2 ")}/>
|
||||
<Tr t={t.seeNearby}/></button>
|
||||
{/if}
|
22
src/UI/Popup/SendEmail.svelte
Normal file
22
src/UI/Popup/SendEmail.svelte
Normal file
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
|
||||
import type { OsmTags } from "../../Models/OsmFeature";
|
||||
import Svg from "../../Svg";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import { Utils } from "../../Utils";
|
||||
|
||||
export let tags: Store<OsmTags>
|
||||
export let args: string[]
|
||||
|
||||
let [to, subject, body, button_text] = args.map(a => Utils.SubstituteKeys(a, $tags))
|
||||
let url = "mailto:" +
|
||||
to +
|
||||
"?subject=" +
|
||||
encodeURIComponent(subject) +
|
||||
"&body=" +
|
||||
encodeURIComponent(body)
|
||||
</script>
|
||||
<a class="button flex items-center w-full" href={url}>
|
||||
<ToSvelte construct={Svg.envelope_svg().SetClass("w-8 h-8 mr-4 shrink-0")}/>
|
||||
{button_text}
|
||||
</a>
|
|
@ -58,18 +58,6 @@ import LanguagePicker from "./LanguagePicker"
|
|||
import Link from "./Base/Link"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
|
||||
import NearbyImages, {
|
||||
NearbyImageOptions,
|
||||
P4CPicture,
|
||||
SelectOneNearbyImage,
|
||||
} from "./Popup/NearbyImages"
|
||||
import { Tag } from "../Logic/Tags/Tag"
|
||||
import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction"
|
||||
import { And } from "../Logic/Tags/And"
|
||||
import { SaveButton } from "./Popup/SaveButton"
|
||||
import Lazy from "./Base/Lazy"
|
||||
import { CheckBox } from "./Input/Checkboxes"
|
||||
import Slider from "./Input/Slider"
|
||||
import { OsmTags, WayId } from "../Models/OsmFeature"
|
||||
import MoveWizard from "./Popup/MoveWizard"
|
||||
import SplitRoadWizard from "./Popup/SplitRoadWizard"
|
||||
|
@ -83,19 +71,17 @@ import DeleteWizard from "./Popup/DeleteFlow/DeleteWizard.svelte"
|
|||
import { OpenJosm } from "./BigComponents/OpenJosm"
|
||||
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
|
||||
import FediverseValidator from "./InputElement/Validators/FediverseValidator"
|
||||
import SendEmail from "./Popup/SendEmail.svelte"
|
||||
import NearbyImages from "./Popup/NearbyImages.svelte"
|
||||
import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte"
|
||||
|
||||
class NearbyImageVis implements SpecialVisualization {
|
||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||
args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [
|
||||
{
|
||||
name: "mode",
|
||||
defaultValue: "expandable",
|
||||
doc: "Indicates how this component is initialized. Options are: \n\n- `open`: always show and load the pictures\n- `collapsable`: show the pictures, but a user can collapse them\n- `expandable`: shown by default; but a user can collapse them.",
|
||||
},
|
||||
{
|
||||
name: "mapillary",
|
||||
defaultValue: "true",
|
||||
doc: "If 'true', includes a link to mapillary on this location.",
|
||||
defaultValue: "closed",
|
||||
doc: "Either `open` or `closed`. If `open`, then the image carousel will always be shown",
|
||||
},
|
||||
]
|
||||
docs =
|
||||
|
@ -104,110 +90,21 @@ class NearbyImageVis implements SpecialVisualization {
|
|||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
const t = Translations.t.image.nearbyPictures
|
||||
const mode: "open" | "expandable" | "collapsable" = <any>args[0]
|
||||
const isOpen = args[0] === "open"
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
|
||||
const id: string = tagSource.data["id"]
|
||||
const canBeEdited: boolean = !!id?.match("(node|way|relation)/-?[0-9]+")
|
||||
const selectedImage = new UIEventSource<P4CPicture>(undefined)
|
||||
|
||||
let saveButton: BaseUIElement = undefined
|
||||
if (canBeEdited) {
|
||||
const confirmText: BaseUIElement = new SubstitutedTranslation(
|
||||
t.confirm,
|
||||
tagSource,
|
||||
state
|
||||
)
|
||||
|
||||
const onSave = async () => {
|
||||
console.log("Selected a picture...", selectedImage.data)
|
||||
const osmTags = selectedImage.data.osmTags
|
||||
const tags: Tag[] = []
|
||||
for (const key in osmTags) {
|
||||
tags.push(new Tag(key, osmTags[key]))
|
||||
}
|
||||
await state?.changes?.applyAction(
|
||||
new ChangeTagAction(id, new And(tags), tagSource.data, {
|
||||
theme: state?.layout.id,
|
||||
changeType: "link-image",
|
||||
})
|
||||
)
|
||||
}
|
||||
saveButton = new SaveButton(selectedImage, state, confirmText, t.noImageSelected)
|
||||
.onClick(onSave)
|
||||
.SetClass("flex justify-end")
|
||||
}
|
||||
|
||||
const nearby = new Lazy(() => {
|
||||
const towardsCenter = new CheckBox(t.onlyTowards, false)
|
||||
|
||||
const maxSearchRadius = 100
|
||||
const stepSize = 10
|
||||
const defaultValue = Math.floor(maxSearchRadius / (2 * stepSize)) * stepSize
|
||||
const fromOsmPreferences = state?.osmConnection
|
||||
?.GetPreference("nearby-images-radius", "" + defaultValue)
|
||||
.sync(
|
||||
(s) => Number(s),
|
||||
[],
|
||||
(i) => "" + i
|
||||
)
|
||||
const radiusValue = new UIEventSource(fromOsmPreferences.data)
|
||||
radiusValue.addCallbackAndRunD((v) => fromOsmPreferences.setData(v))
|
||||
|
||||
const radius = new Slider(stepSize, maxSearchRadius, {
|
||||
value: radiusValue,
|
||||
step: 10,
|
||||
})
|
||||
const alreadyInTheImage = AllImageProviders.LoadImagesFor(tagSource)
|
||||
const options: NearbyImageOptions & { value } = {
|
||||
lon,
|
||||
lat,
|
||||
searchRadius: maxSearchRadius,
|
||||
shownRadius: radius.GetValue(),
|
||||
value: selectedImage,
|
||||
blacklist: alreadyInTheImage,
|
||||
towardscenter: towardsCenter.GetValue(),
|
||||
maxDaysOld: 365 * 3,
|
||||
}
|
||||
const slideshow = canBeEdited
|
||||
? new SelectOneNearbyImage(options, state.indexedFeatures)
|
||||
: new NearbyImages(options, state.indexedFeatures)
|
||||
const controls = new Combine([
|
||||
towardsCenter,
|
||||
new Combine([
|
||||
new VariableUiElement(
|
||||
radius.GetValue().map((radius) => t.withinRadius.Subs({ radius }))
|
||||
),
|
||||
radius,
|
||||
]).SetClass("flex justify-between"),
|
||||
]).SetClass("flex flex-col")
|
||||
return new Combine([
|
||||
slideshow,
|
||||
controls,
|
||||
saveButton,
|
||||
new MapillaryLinkVis().constr(state, tagSource, [], feature).SetClass("mt-6"),
|
||||
])
|
||||
return new SvelteUIElement(isOpen ? NearbyImages : NearbyImagesCollapsed, {
|
||||
tags,
|
||||
state,
|
||||
lon,
|
||||
lat,
|
||||
feature,
|
||||
layer,
|
||||
})
|
||||
|
||||
let withEdit: BaseUIElement = nearby
|
||||
if (canBeEdited) {
|
||||
withEdit = new Combine([t.hasMatchingPicture, nearby]).SetClass("flex flex-col")
|
||||
}
|
||||
|
||||
if (mode === "open") {
|
||||
return withEdit
|
||||
}
|
||||
const toggleState = new UIEventSource<boolean>(mode === "collapsable")
|
||||
return new Toggle(
|
||||
new Combine([new Title(t.title), withEdit]),
|
||||
new Title(t.browseNearby).onClick(() => toggleState.setData(true)),
|
||||
toggleState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1237,28 +1134,12 @@ export default class SpecialVisualizations {
|
|||
},
|
||||
],
|
||||
constr(__, tags, args) {
|
||||
return new VariableUiElement(
|
||||
tags.map((tags) => {
|
||||
const [to, subject, body, button_text] = args.map((str) =>
|
||||
Utils.SubstituteKeys(str, tags)
|
||||
)
|
||||
const url =
|
||||
"mailto:" +
|
||||
to +
|
||||
"?subject=" +
|
||||
encodeURIComponent(subject) +
|
||||
"&body=" +
|
||||
encodeURIComponent(body)
|
||||
return new SubtleButton(Svg.envelope_svg(), button_text, {
|
||||
url,
|
||||
})
|
||||
})
|
||||
)
|
||||
return new SvelteUIElement(SendEmail, { args, tags })
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "link",
|
||||
docs: "Construct a link. By using the 'special' visualisation notation, translation should be easier",
|
||||
docs: "Construct a link. By using the 'special' visualisation notation, translations should be easier",
|
||||
args: [
|
||||
{
|
||||
name: "text",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import Svg from "../Svg"
|
||||
import Loading from "./Base/Loading.svelte"
|
||||
import ToSvelte from "./Base/ToSvelte.svelte"
|
||||
import Svg from "../Svg";
|
||||
import Loading from "./Base/Loading.svelte";
|
||||
import ToSvelte from "./Base/ToSvelte.svelte";
|
||||
</script>
|
||||
|
||||
<div>
|
||||
|
@ -43,6 +43,13 @@
|
|||
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} />
|
||||
Small button
|
||||
</button>
|
||||
|
||||
<button class="small primary">
|
||||
Small button
|
||||
</button>
|
||||
<button class="small primary disabled">
|
||||
Small, disabled button
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<button>
|
||||
|
|
|
@ -1,57 +1,57 @@
|
|||
<script lang="ts">
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource";
|
||||
import { Map as MlMap } from "maplibre-gl";
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte";
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
|
||||
import MapControlButton from "./Base/MapControlButton.svelte";
|
||||
import ToSvelte from "./Base/ToSvelte.svelte";
|
||||
import If from "./Base/If.svelte";
|
||||
import { GeolocationControl } from "./BigComponents/GeolocationControl";
|
||||
import type { Feature } from "geojson";
|
||||
import SelectedElementView from "./BigComponents/SelectedElementView.svelte";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import Filterview from "./BigComponents/Filterview.svelte";
|
||||
import ThemeViewState from "../Models/ThemeViewState";
|
||||
import type { MapProperties } from "../Models/MapProperties";
|
||||
import Geosearch from "./BigComponents/Geosearch.svelte";
|
||||
import Translations from "./i18n/Translations";
|
||||
import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte"
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||
import MapControlButton from "./Base/MapControlButton.svelte"
|
||||
import ToSvelte from "./Base/ToSvelte.svelte"
|
||||
import If from "./Base/If.svelte"
|
||||
import { GeolocationControl } from "./BigComponents/GeolocationControl"
|
||||
import type { Feature } from "geojson"
|
||||
import SelectedElementView from "./BigComponents/SelectedElementView.svelte"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import Filterview from "./BigComponents/Filterview.svelte"
|
||||
import ThemeViewState from "../Models/ThemeViewState"
|
||||
import type { MapProperties } from "../Models/MapProperties"
|
||||
import Geosearch from "./BigComponents/Geosearch.svelte"
|
||||
import Translations from "./i18n/Translations"
|
||||
import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
|
||||
import Tr from "./Base/Tr.svelte";
|
||||
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte";
|
||||
import FloatOver from "./Base/FloatOver.svelte";
|
||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy";
|
||||
import Constants from "../Models/Constants";
|
||||
import TabbedGroup from "./Base/TabbedGroup.svelte";
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState";
|
||||
import LoginToggle from "./Base/LoginToggle.svelte";
|
||||
import LoginButton from "./Base/LoginButton.svelte";
|
||||
import CopyrightPanel from "./BigComponents/CopyrightPanel";
|
||||
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte";
|
||||
import ModalRight from "./Base/ModalRight.svelte";
|
||||
import { Utils } from "../Utils";
|
||||
import Hotkeys from "./Base/Hotkeys";
|
||||
import { VariableUiElement } from "./Base/VariableUIElement";
|
||||
import SvelteUIElement from "./Base/SvelteUIElement";
|
||||
import OverlayToggle from "./BigComponents/OverlayToggle.svelte";
|
||||
import LevelSelector from "./BigComponents/LevelSelector.svelte";
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton";
|
||||
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte";
|
||||
import Svg from "../Svg";
|
||||
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte";
|
||||
import type { RasterLayerPolygon } from "../Models/RasterLayers";
|
||||
import { AvailableRasterLayers } from "../Models/RasterLayers";
|
||||
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte";
|
||||
import IfHidden from "./Base/IfHidden.svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
import { OpenJosm } from "./BigComponents/OpenJosm";
|
||||
import MapillaryLink from "./BigComponents/MapillaryLink.svelte";
|
||||
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte";
|
||||
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte";
|
||||
import StateIndicator from "./BigComponents/StateIndicator.svelte";
|
||||
import LanguagePicker from "./LanguagePicker";
|
||||
import Locale from "./i18n/Locale";
|
||||
import ShareScreen from "./BigComponents/ShareScreen.svelte";
|
||||
import Tr from "./Base/Tr.svelte"
|
||||
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
|
||||
import FloatOver from "./Base/FloatOver.svelte"
|
||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy"
|
||||
import Constants from "../Models/Constants"
|
||||
import TabbedGroup from "./Base/TabbedGroup.svelte"
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState"
|
||||
import LoginToggle from "./Base/LoginToggle.svelte"
|
||||
import LoginButton from "./Base/LoginButton.svelte"
|
||||
import CopyrightPanel from "./BigComponents/CopyrightPanel"
|
||||
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"
|
||||
import ModalRight from "./Base/ModalRight.svelte"
|
||||
import { Utils } from "../Utils"
|
||||
import Hotkeys from "./Base/Hotkeys"
|
||||
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||
import SvelteUIElement from "./Base/SvelteUIElement"
|
||||
import OverlayToggle from "./BigComponents/OverlayToggle.svelte"
|
||||
import LevelSelector from "./BigComponents/LevelSelector.svelte"
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
|
||||
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"
|
||||
import Svg from "../Svg"
|
||||
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
|
||||
import type { RasterLayerPolygon } from "../Models/RasterLayers"
|
||||
import { AvailableRasterLayers } from "../Models/RasterLayers"
|
||||
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"
|
||||
import IfHidden from "./Base/IfHidden.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import { OpenJosm } from "./BigComponents/OpenJosm"
|
||||
import MapillaryLink from "./BigComponents/MapillaryLink.svelte"
|
||||
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
|
||||
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"
|
||||
import StateIndicator from "./BigComponents/StateIndicator.svelte"
|
||||
import LanguagePicker from "./LanguagePicker"
|
||||
import Locale from "./i18n/Locale"
|
||||
import ShareScreen from "./BigComponents/ShareScreen.svelte"
|
||||
|
||||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
|
@ -319,7 +319,7 @@
|
|||
<Tr t={Translations.t.general.sharescreen.title} />
|
||||
</div>
|
||||
<div class="m-2" slot="content4">
|
||||
<ShareScreen {state}/>
|
||||
<ShareScreen {state} />
|
||||
</div>
|
||||
</TabbedGroup>
|
||||
</FloatOver>
|
||||
|
|
|
@ -226,16 +226,27 @@ export class Translation extends BaseUIElement {
|
|||
return new Translation(this.translations, this.context)
|
||||
}
|
||||
|
||||
FirstSentence() {
|
||||
/**
|
||||
* Build a new translation which only contains the first sentence of every language
|
||||
* A sentence stops at either a dot (`.`) or a HTML-break ('<br/>').
|
||||
* The dot or linebreak are _not_ returned.
|
||||
*
|
||||
* new Translation({"en": "This is a sentence. This is another sentence"}).FirstSentence().textFor("en") // "This is a sentence"
|
||||
* new Translation({"en": "This is a sentence <br/> This is another sentence"}).FirstSentence().textFor("en") // "This is a sentence"
|
||||
* new Translation({"en": "This is a sentence <br> This is another sentence"}).FirstSentence().textFor("en") // "This is a sentence"
|
||||
* new Translation({"en": "This is a sentence with a <b>bold</b> word. This is another sentence"}).FirstSentence().textFor("en") // "This is a sentence with a <b>bold</b> word"
|
||||
* @constructor
|
||||
*/
|
||||
public FirstSentence(): Translation {
|
||||
const tr = {}
|
||||
for (const lng in this.translations) {
|
||||
if (!this.translations.hasOwnProperty(lng)) {
|
||||
continue
|
||||
}
|
||||
let txt = this.translations[lng]
|
||||
txt = txt.replace(/[.<].*/, "")
|
||||
txt = txt.replace(/(\.|<br\/>|<br>).*/, "")
|
||||
txt = Utils.EllipsesAfter(txt, 255)
|
||||
tr[lng] = txt
|
||||
tr[lng] = txt.trim()
|
||||
}
|
||||
|
||||
return new Translation(tr)
|
||||
|
|
25
src/Utils.ts
25
src/Utils.ts
|
@ -1,5 +1,4 @@
|
|||
import colors from "./assets/colors.json"
|
||||
import { HTMLElement } from "node-html-parser"
|
||||
|
||||
export class Utils {
|
||||
/**
|
||||
|
@ -236,7 +235,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
}
|
||||
|
||||
public static TimesT<T>(count: number, f: (i: number) => T): T[] {
|
||||
let res: T[] = []
|
||||
const res: T[] = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
res.push(f(i))
|
||||
}
|
||||
|
@ -327,7 +326,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
enumerable: false,
|
||||
configurable: true,
|
||||
get: () => {
|
||||
console.trace("Property", name, "got requested")
|
||||
init().then((r) => {
|
||||
delete object[name]
|
||||
object[name] = r
|
||||
|
@ -444,6 +442,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
* Utils.SubstituteKeys("abc{def}ghi", {def: 'XYZ'}) // => "abcXYZghi"
|
||||
* Utils.SubstituteKeys("abc{def}{def}ghi", {def: 'XYZ'}) // => "abcXYZXYZghi"
|
||||
* Utils.SubstituteKeys("abc{def}ghi", {def: '{XYZ}'}) // => "abc{XYZ}ghi"
|
||||
* Utils.SubstituteKeys("abc\n\n{def}ghi", {def: '{XYZ}'}) // => "abc\n\n{XYZ}ghi"
|
||||
*
|
||||
* @param txt
|
||||
* @param tags
|
||||
|
@ -458,7 +457,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
if (txt === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const regex = /(.*?){([^}]*)}(.*)/
|
||||
const regex = /(.*?){([^}]*)}(.*)/s
|
||||
|
||||
let match = txt.match(regex)
|
||||
|
||||
|
@ -491,7 +490,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
"\nThe value is",
|
||||
v
|
||||
)
|
||||
v = (<HTMLElement>v.InnerConstructElement())?.textContent
|
||||
v = v.InnerConstructElement()?.textContent
|
||||
}
|
||||
|
||||
if (typeof v !== "string") {
|
||||
|
@ -830,7 +829,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
}
|
||||
|
||||
static getOrSetDefault<K, V>(dict: Map<K, V>, k: K, v: () => V) {
|
||||
let found = dict.get(k)
|
||||
const found = dict.get(k)
|
||||
if (found !== undefined) {
|
||||
return found
|
||||
}
|
||||
|
@ -851,7 +850,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
if (i >= 124) {
|
||||
code += 1 // Character 127 is our 'escape' character |
|
||||
}
|
||||
let replacement = "|" + String.fromCharCode(code)
|
||||
const replacement = "|" + String.fromCharCode(code)
|
||||
stringified = stringified.replace(new RegExp(`\"${knownKey}\":`, "g"), replacement)
|
||||
}
|
||||
|
||||
|
@ -1128,7 +1127,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
seconds = seconds % 60
|
||||
let hours = Math.floor(minutes / 60)
|
||||
minutes = minutes % 60
|
||||
let days = Math.floor(hours / 24)
|
||||
const days = Math.floor(hours / 24)
|
||||
hours = hours % 24
|
||||
if (days > 0) {
|
||||
return days + "days" + " " + hours + "h"
|
||||
|
@ -1291,7 +1290,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
|
||||
for (const k in d) {
|
||||
const vs = d[k]
|
||||
for (let v of vs) {
|
||||
for (const v of vs) {
|
||||
const list = newD[v]
|
||||
if (list === undefined) {
|
||||
newD[v] = [k] // Left: indexing; right: list with one element
|
||||
|
@ -1313,7 +1312,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
}
|
||||
|
||||
function componentToHex(n) {
|
||||
let hex = n.toString(16)
|
||||
const hex = n.toString(16)
|
||||
return hex.length == 1 ? "0" + hex : hex
|
||||
}
|
||||
|
||||
|
@ -1432,8 +1431,8 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
return false
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
let ai = a[i]
|
||||
let bi = b[i]
|
||||
const ai = a[i]
|
||||
const bi = b[i]
|
||||
if (ai == bi) {
|
||||
continue
|
||||
}
|
||||
|
@ -1527,7 +1526,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
if (matchWithFuncName) {
|
||||
;[_, functionName, path, line, column] = matchWithFuncName
|
||||
} else {
|
||||
let regexNoFuncName: RegExp = new RegExp("at ([a-zA-Z0-9/.]+):([0-9]+):([0-9]+)")
|
||||
const regexNoFuncName: RegExp = new RegExp("at ([a-zA-Z0-9/.]+):([0-9]+):([0-9]+)")
|
||||
;[_, path, line, column] = stackItem.match(regexNoFuncName)
|
||||
}
|
||||
|
||||
|
|
|
@ -209,7 +209,7 @@ class SvgToPdfInternals {
|
|||
if (element.childElementCount == 0) {
|
||||
this.drawTspan(element)
|
||||
} else {
|
||||
for (let child of Array.from(element.children)) {
|
||||
for (const child of Array.from(element.children)) {
|
||||
this.handleElement(child)
|
||||
}
|
||||
}
|
||||
|
@ -224,7 +224,7 @@ class SvgToPdfInternals {
|
|||
}
|
||||
|
||||
if (element.tagName === "g" || element.tagName === "text") {
|
||||
for (let child of Array.from(element.children)) {
|
||||
for (const child of Array.from(element.children)) {
|
||||
this.handleElement(child)
|
||||
}
|
||||
}
|
||||
|
@ -256,7 +256,7 @@ class SvgToPdfInternals {
|
|||
const css = SvgToPdfInternals.css(element)
|
||||
this.doc.saveGraphicsState()
|
||||
if (css["fill-opacity"] !== "0" && css["fill"] !== "none") {
|
||||
let color = css["fill"] ?? "black"
|
||||
const color = css["fill"] ?? "black"
|
||||
let opacity = 1
|
||||
if (css["fill-opacity"]) {
|
||||
opacity = Number(css["fill-opacity"])
|
||||
|
@ -314,13 +314,13 @@ class SvgToPdfInternals {
|
|||
console.log("Creating image with key", key, "searching rect in", x, y)
|
||||
const rectangle: SVGRectElement = this.page.findSmallestRectContaining(x, y, false)
|
||||
console.log("Got rect", rectangle)
|
||||
let w = SvgToPdfInternals.attrNumber(rectangle, "width")
|
||||
let h = SvgToPdfInternals.attrNumber(rectangle, "height")
|
||||
const w = SvgToPdfInternals.attrNumber(rectangle, "width")
|
||||
const h = SvgToPdfInternals.attrNumber(rectangle, "height")
|
||||
x = SvgToPdfInternals.attrNumber(rectangle, "x")
|
||||
y = SvgToPdfInternals.attrNumber(rectangle, "y")
|
||||
|
||||
// Actually, dots per mm, not dots per inch ;)
|
||||
let dpi = 60
|
||||
const dpi = 60
|
||||
const img = this.page.options.createImage(key, dpi * w + "px", dpi * h + "px")
|
||||
|
||||
const canvas = document.createElement("canvas")
|
||||
|
@ -363,7 +363,7 @@ class SvgToPdfInternals {
|
|||
fontFamily = "Ubuntu"
|
||||
}
|
||||
|
||||
let fontWeight = css["font-weight"] ?? "normal"
|
||||
const fontWeight = css["font-weight"] ?? "normal"
|
||||
this.doc.setFont(fontFamily, fontWeight)
|
||||
|
||||
const fontColor = css["fill"]
|
||||
|
@ -372,13 +372,13 @@ class SvgToPdfInternals {
|
|||
} else {
|
||||
this.doc.setTextColor("black")
|
||||
}
|
||||
let fontsize = parseFloat(css["font-size"])
|
||||
const fontsize = parseFloat(css["font-size"])
|
||||
this.doc.setFontSize(fontsize * 2.5)
|
||||
|
||||
let textTemplate = tspan.textContent.split(" ")
|
||||
const textTemplate = tspan.textContent.split(" ")
|
||||
let result: string = ""
|
||||
let addSpace = false
|
||||
for (let text of textTemplate) {
|
||||
for (const text of textTemplate) {
|
||||
if (text === "\\n") {
|
||||
result += "\n"
|
||||
addSpace = false
|
||||
|
@ -446,7 +446,7 @@ class SvgToPdfInternals {
|
|||
const svgWidth = SvgToPdfInternals.attrNumber(svgRoot, "width")
|
||||
const svgHeight = SvgToPdfInternals.attrNumber(svgRoot, "height")
|
||||
|
||||
let img = this.page.images[base64src]
|
||||
const img = this.page.images[base64src]
|
||||
// This is an svg image, we use the canvas to convert it to a png
|
||||
const canvas = document.createElement("canvas")
|
||||
const ctx = canvas.getContext("2d")
|
||||
|
@ -607,7 +607,7 @@ class SvgToPdfPage {
|
|||
const parts = tc.split(" ").filter((p) => p.startsWith("$") && p.indexOf("(") < 0)
|
||||
for (let part of parts) {
|
||||
part = part.substring(1) // Drop the $
|
||||
let path = part.split(".")
|
||||
const path = part.split(".")
|
||||
const importPath = this.importedTranslations[path[0]]
|
||||
if (importPath) {
|
||||
translations.add(importPath + "." + path.slice(1).join("."))
|
||||
|
@ -636,7 +636,7 @@ class SvgToPdfPage {
|
|||
|
||||
if (element.tagName === "tspan" && element.childElementCount == 0) {
|
||||
const specialValues = element.textContent.split(" ").filter((t) => t.startsWith("$"))
|
||||
for (let specialValue of specialValues) {
|
||||
for (const specialValue of specialValues) {
|
||||
const importMatch = element.textContent.match(
|
||||
/\$import ([a-zA-Z-_0-9.? ]+) as ([a-zA-Z0-9]+)/
|
||||
)
|
||||
|
@ -665,7 +665,7 @@ class SvgToPdfPage {
|
|||
element.tagName === "tspan" ||
|
||||
element.tagName === "defs"
|
||||
) {
|
||||
for (let child of Array.from(element.children)) {
|
||||
for (const child of Array.from(element.children)) {
|
||||
await this.prepareElement(child, mapTextSpecs, inDefs || element.tagName === "defs")
|
||||
}
|
||||
}
|
||||
|
@ -694,7 +694,7 @@ class SvgToPdfPage {
|
|||
}
|
||||
this._isPrepared = true
|
||||
const mapSpecs: SVGTSpanElement[] = []
|
||||
for (let child of Array.from(this._svgRoot.children)) {
|
||||
for (const child of Array.from(this._svgRoot.children)) {
|
||||
await this.prepareElement(<any>child, mapSpecs, child.tagName === "defs")
|
||||
}
|
||||
|
||||
|
@ -715,7 +715,7 @@ class SvgToPdfPage {
|
|||
const internal = new SvgToPdfInternals(advancedApi, this, (key) =>
|
||||
self.extractTranslation(key, language)
|
||||
)
|
||||
for (let child of Array.from(this._svgRoot.children)) {
|
||||
for (const child of Array.from(this._svgRoot.children)) {
|
||||
internal.handleElement(<any>child)
|
||||
}
|
||||
}
|
||||
|
@ -805,11 +805,11 @@ class SvgToPdfPage {
|
|||
|
||||
private loadImage(element: Element | string): Promise<void> {
|
||||
const xlink = typeof element === "string" ? element : element.getAttribute("xlink:href")
|
||||
let img = document.createElement("img")
|
||||
const img = document.createElement("img")
|
||||
|
||||
if (xlink.startsWith("data:image/svg+xml;")) {
|
||||
const base64src = xlink
|
||||
let svgXml = atob(
|
||||
const svgXml = atob(
|
||||
base64src.substring(base64src.indexOf(";base64,") + ";base64,".length)
|
||||
)
|
||||
const parser = new DOMParser()
|
||||
|
@ -884,7 +884,7 @@ class SvgToPdfPage {
|
|||
throw "Invalid mapspec:" + spec
|
||||
}
|
||||
const params = SvgToPdfInternals.parseCss(match[1], ",")
|
||||
let layout = AllKnownLayouts.allKnownLayouts.get(params["theme"])
|
||||
const layout = AllKnownLayouts.allKnownLayouts.get(params["theme"])
|
||||
if (layout === undefined) {
|
||||
console.error("Could not show map with parameters", params)
|
||||
throw (
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Utils } from "./Utils"
|
||||
import AllThemesGui from "./UI/AllThemesGui"
|
||||
import { QueryParameters } from "./Logic/Web/QueryParameters"
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"contributors": [
|
||||
{
|
||||
"commits": 5849,
|
||||
"commits": 5914,
|
||||
"contributor": "Pieter Vander Vennet"
|
||||
},
|
||||
{
|
||||
"commits": 388,
|
||||
"commits": 393,
|
||||
"contributor": "Robin van der Linde"
|
||||
},
|
||||
{
|
||||
|
@ -36,6 +36,10 @@
|
|||
"commits": 27,
|
||||
"contributor": "riQQ"
|
||||
},
|
||||
{
|
||||
"commits": 26,
|
||||
"contributor": "Hosted Weblate"
|
||||
},
|
||||
{
|
||||
"commits": 26,
|
||||
"contributor": "Joost"
|
||||
|
@ -48,10 +52,6 @@
|
|||
"commits": 24,
|
||||
"contributor": "Ward"
|
||||
},
|
||||
{
|
||||
"commits": 21,
|
||||
"contributor": "Hosted Weblate"
|
||||
},
|
||||
{
|
||||
"commits": 21,
|
||||
"contributor": "wjtje"
|
||||
|
@ -120,6 +120,10 @@
|
|||
"commits": 9,
|
||||
"contributor": "Midgard"
|
||||
},
|
||||
{
|
||||
"commits": 8,
|
||||
"contributor": "pelderson"
|
||||
},
|
||||
{
|
||||
"commits": 8,
|
||||
"contributor": "Codain"
|
||||
|
@ -128,10 +132,6 @@
|
|||
"commits": 8,
|
||||
"contributor": "Mateusz Konieczny"
|
||||
},
|
||||
{
|
||||
"commits": 7,
|
||||
"contributor": "pelderson"
|
||||
},
|
||||
{
|
||||
"commits": 7,
|
||||
"contributor": "OliNau"
|
||||
|
@ -232,6 +232,14 @@
|
|||
"commits": 2,
|
||||
"contributor": "Stanislas Gueniffey"
|
||||
},
|
||||
{
|
||||
"commits": 1,
|
||||
"contributor": "Ciprian"
|
||||
},
|
||||
{
|
||||
"commits": 1,
|
||||
"contributor": "redfast00"
|
||||
},
|
||||
{
|
||||
"commits": 1,
|
||||
"contributor": "Daniel McDonald"
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -246,6 +246,9 @@
|
|||
"es",
|
||||
"pt"
|
||||
],
|
||||
"GR": [
|
||||
"el"
|
||||
],
|
||||
"GT": [
|
||||
"es"
|
||||
],
|
||||
|
@ -505,7 +508,9 @@
|
|||
],
|
||||
"PL": [
|
||||
"pl",
|
||||
"pl"
|
||||
"be",
|
||||
"pl",
|
||||
"be"
|
||||
],
|
||||
"PS": [
|
||||
"ar"
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"fi": "suomi",
|
||||
"fr": "français",
|
||||
"gl": "lingua galega",
|
||||
"he": "עברית",
|
||||
"hu": "magyar",
|
||||
"id": "Bahasa Indonesia",
|
||||
"it": "italiano",
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,23 +1,23 @@
|
|||
{
|
||||
"contributors": [
|
||||
{
|
||||
"commits": 289,
|
||||
"commits": 299,
|
||||
"contributor": "kjon"
|
||||
},
|
||||
{
|
||||
"commits": 277,
|
||||
"commits": 281,
|
||||
"contributor": "Pieter Vander Vennet"
|
||||
},
|
||||
{
|
||||
"commits": 142,
|
||||
"commits": 151,
|
||||
"contributor": "paunofu"
|
||||
},
|
||||
{
|
||||
"commits": 94,
|
||||
"commits": 95,
|
||||
"contributor": "Allan Nordhøy"
|
||||
},
|
||||
{
|
||||
"commits": 69,
|
||||
"commits": 70,
|
||||
"contributor": "Robin van der Linde"
|
||||
},
|
||||
{
|
||||
|
@ -38,20 +38,20 @@
|
|||
},
|
||||
{
|
||||
"commits": 31,
|
||||
"contributor": "Supaplex"
|
||||
"contributor": "Lucas"
|
||||
},
|
||||
{
|
||||
"commits": 30,
|
||||
"commits": 31,
|
||||
"contributor": "Jiří Podhorecký"
|
||||
},
|
||||
{
|
||||
"commits": 31,
|
||||
"contributor": "Supaplex"
|
||||
},
|
||||
{
|
||||
"commits": 29,
|
||||
"contributor": "Artem"
|
||||
},
|
||||
{
|
||||
"commits": 25,
|
||||
"contributor": "Lucas"
|
||||
},
|
||||
{
|
||||
"commits": 25,
|
||||
"contributor": "Reza Almanda"
|
||||
|
@ -152,6 +152,10 @@
|
|||
"commits": 8,
|
||||
"contributor": "Vinicius"
|
||||
},
|
||||
{
|
||||
"commits": 7,
|
||||
"contributor": "NetworkedPoncho"
|
||||
},
|
||||
{
|
||||
"commits": 7,
|
||||
"contributor": "Joost Schouppe"
|
||||
|
@ -168,6 +172,10 @@
|
|||
"commits": 7,
|
||||
"contributor": "Niels Elgaard Larsen"
|
||||
},
|
||||
{
|
||||
"commits": 6,
|
||||
"contributor": "Juele juele"
|
||||
},
|
||||
{
|
||||
"commits": 6,
|
||||
"contributor": "Leonardo Gómez Berniga"
|
||||
|
@ -248,6 +256,10 @@
|
|||
"commits": 5,
|
||||
"contributor": "Alexey Shabanov"
|
||||
},
|
||||
{
|
||||
"commits": 4,
|
||||
"contributor": "Krzysztof Chorzempa"
|
||||
},
|
||||
{
|
||||
"commits": 4,
|
||||
"contributor": "Emory Shaw"
|
||||
|
@ -336,6 +348,14 @@
|
|||
"commits": 3,
|
||||
"contributor": "SiegbjornSitumeang"
|
||||
},
|
||||
{
|
||||
"commits": 2,
|
||||
"contributor": "nilocram"
|
||||
},
|
||||
{
|
||||
"commits": 2,
|
||||
"contributor": "מוימוי טרייצקי"
|
||||
},
|
||||
{
|
||||
"commits": 2,
|
||||
"contributor": "lmagreault"
|
||||
|
@ -420,6 +440,10 @@
|
|||
"commits": 2,
|
||||
"contributor": "Leo Alcaraz"
|
||||
},
|
||||
{
|
||||
"commits": 1,
|
||||
"contributor": "Traladarer"
|
||||
},
|
||||
{
|
||||
"commits": 1,
|
||||
"contributor": "LuPa"
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"languages":["ca","cs","da","de","en","eo","es","fi","fil","fr","gl","he","hu","id","it","ja","nb_NO","nl","pa_PK","pl","pt","pt_BR","ru","sl","sv","zgh","zh_Hans","zh_Hant"]}
|
||||
{"languages":["ca","cs","da","de","en","eo","es","eu","fi","fil","fr","gl","he","hu","id","it","ja","nb_NO","nl","pa_PK","pl","pt","pt_BR","ru","sl","sv","zgh","zh_Hans","zh_Hant"]}
|
15
src/index.ts
15
src/index.ts
|
@ -7,9 +7,22 @@ import Combine from "./UI/Base/Combine"
|
|||
import { SubtleButton } from "./UI/Base/SubtleButton"
|
||||
import Svg from "./Svg"
|
||||
import { Utils } from "./Utils"
|
||||
|
||||
function webgl_support() {
|
||||
try {
|
||||
var canvas = document.createElement("canvas")
|
||||
return (
|
||||
!!window.WebGLRenderingContext &&
|
||||
(canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))
|
||||
)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
try {
|
||||
if (!webgl_support()) {
|
||||
throw "WebGL is not supported or not enabled. This is essential for MapComplete to function, please enable this."
|
||||
}
|
||||
DetermineLayout.GetLayout()
|
||||
.then((layout) => {
|
||||
const state = new ThemeViewState(layout)
|
||||
|
|
12
src/land.ts
Normal file
12
src/land.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import {OsmConnection} from "./Logic/Osm/OsmConnection";
|
||||
|
||||
console.log("Authorizing...");
|
||||
new OsmConnection().finishLogin(previousURL => {
|
||||
const fallback = window.location.protocol+"//"+window.location.host+"/index.html"
|
||||
previousURL ??= fallback
|
||||
if(previousURL.indexOf("/land") > 0){
|
||||
previousURL = fallback
|
||||
}
|
||||
console.log("Redirecting to", previousURL)
|
||||
window.location.href = previousURL
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue