forked from MapComplete/MapComplete
Merge master
This commit is contained in:
commit
890816d2dd
424 changed files with 40595 additions and 3354 deletions
|
@ -3,9 +3,10 @@ import Constants from "../../Models/Constants"
|
|||
import { UIEventSource } from "../UIEventSource"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Feature } from "geojson"
|
||||
import { ImageUploadManager } from "../ImageProviders/ImageUploadManager"
|
||||
|
||||
export default class PendingChangesUploader {
|
||||
constructor(changes: Changes, selectedFeature: UIEventSource<Feature>) {
|
||||
constructor(changes: Changes, selectedFeature: UIEventSource<Feature>, uploader : ImageUploadManager) {
|
||||
changes.pendingChanges
|
||||
.stabilized(Constants.updateTimeoutSec * 1000)
|
||||
.addCallback(() => changes.flushChanges("Flushing changes due to timeout"))
|
||||
|
@ -48,7 +49,9 @@ export default class PendingChangesUploader {
|
|||
}
|
||||
|
||||
function onunload(e) {
|
||||
if (changes.pendingChanges.data.length == 0) {
|
||||
const pendingChanges = changes.pendingChanges.data.length
|
||||
const uploadingImages = uploader.isUploading.data
|
||||
if (pendingChanges == 0 && !uploadingImages) {
|
||||
return
|
||||
}
|
||||
changes.flushChanges("onbeforeunload - probably closing or something similar")
|
||||
|
|
|
@ -96,6 +96,9 @@ export default class FeaturePropertiesStore {
|
|||
if (newId === undefined) {
|
||||
// We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
|
||||
const element = this._elements.get(oldId)
|
||||
if(!element || element.data === undefined){
|
||||
return
|
||||
}
|
||||
element.data._deleted = "yes"
|
||||
element.ping()
|
||||
return
|
||||
|
|
|
@ -40,7 +40,7 @@ export default class GenericImageProvider extends ImageProvider {
|
|||
return undefined
|
||||
}
|
||||
|
||||
public DownloadAttribution(url: string) {
|
||||
public DownloadAttribution(_) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ export default abstract class ImageProvider {
|
|||
|
||||
public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]>
|
||||
|
||||
public abstract DownloadAttribution(url: string): Promise<LicenseInfo>
|
||||
public abstract DownloadAttribution(providedImage: ProvidedImage): Promise<LicenseInfo>
|
||||
|
||||
public abstract apiUrls(): string[]
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ export class ImageUploadManager {
|
|||
private readonly _uploadRetriedSuccess: Map<string, UIEventSource<number>> = new Map()
|
||||
private readonly _osmConnection: OsmConnection
|
||||
private readonly _changes: Changes
|
||||
public readonly isUploading: Store<boolean>
|
||||
|
||||
constructor(
|
||||
layout: LayoutConfig,
|
||||
|
@ -37,6 +38,13 @@ export class ImageUploadManager {
|
|||
this._layout = layout
|
||||
this._osmConnection = osmConnection
|
||||
this._changes = changes
|
||||
|
||||
const failed = this.getCounterFor(this._uploadFailed, "*")
|
||||
const done = this.getCounterFor(this._uploadFinished, "*")
|
||||
|
||||
this.isUploading = this.getCounterFor(this._uploadStarted, "*").map(startedCount => {
|
||||
return startedCount > failed.data + done.data
|
||||
}, [failed, done])
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -101,7 +109,6 @@ export class ImageUploadManager {
|
|||
"osmid:" + tags.id,
|
||||
].join("\n")
|
||||
|
||||
console.log("Upload done, creating ")
|
||||
const action = await this.uploadImageWithLicense(
|
||||
featureId,
|
||||
title,
|
||||
|
@ -110,6 +117,9 @@ export class ImageUploadManager {
|
|||
targetKey,
|
||||
tags?.data?.["_orig_theme"]
|
||||
)
|
||||
if (!action) {
|
||||
return
|
||||
}
|
||||
if (!isNaN(Number(featureId))) {
|
||||
// This is a map note
|
||||
const url = action._url
|
||||
|
@ -145,6 +155,7 @@ export class ImageUploadManager {
|
|||
} catch (e) {
|
||||
console.error("Could again not upload image due to", e)
|
||||
this.increaseCountFor(this._uploadFailed, featureId)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
console.log("Uploading done, creating action for", featureId)
|
||||
|
|
|
@ -78,16 +78,23 @@ export class Imgur extends ImageProvider implements ImageUploader {
|
|||
*
|
||||
* const data = {"data":{"id":"I9t6B7B","title":"Station Knokke","description":"author:Pieter Vander Vennet\r\nlicense:CC-BY 4.0\r\nosmid:node\/9812712386","datetime":1655052078,"type":"image\/jpeg","animated":false,"width":2400,"height":1795,"size":910872,"views":2,"bandwidth":1821744,"vote":null,"favorite":false,"nsfw":false,"section":null,"account_url":null,"account_id":null,"is_ad":false,"in_most_viral":false,"has_sound":false,"tags":[],"ad_type":0,"ad_url":"","edited":"0","in_gallery":false,"link":"https:\/\/i.imgur.com\/I9t6B7B.jpg","ad_config":{"safeFlags":["not_in_gallery","share"],"highRiskFlags":[],"unsafeFlags":["sixth_mod_unsafe"],"wallUnsafeFlags":[],"showsAds":false,"showAdLevel":1}},"success":true,"status":200}
|
||||
* Utils.injectJsonDownloadForTests("https://api.imgur.com/3/image/E0RuAK3", data)
|
||||
* const licenseInfo = await Imgur.singleton.DownloadAttribution("https://i.imgur.com/E0RuAK3.jpg")
|
||||
* const licenseInfo = await Imgur.singleton.DownloadAttribution({url: "https://i.imgur.com/E0RuAK3.jpg"})
|
||||
* const expected = new LicenseInfo()
|
||||
* expected.licenseShortName = "CC-BY 4.0"
|
||||
* expected.artist = "Pieter Vander Vennet"
|
||||
* expected.date = new Date(1655052078000)
|
||||
* expected.views = 2
|
||||
* licenseInfo // => expected
|
||||
* const licenseInfoJpeg = await Imgur.singleton.DownloadAttribution({url:"https://i.imgur.com/E0RuAK3.jpeg"})
|
||||
* licenseInfoJpeg // => expected
|
||||
* const licenseInfoUpperCase = await Imgur.singleton.DownloadAttribution({url: "https://i.imgur.com/E0RuAK3.JPEG"})
|
||||
* licenseInfoUpperCase // => expected
|
||||
*
|
||||
*
|
||||
*/
|
||||
public async DownloadAttribution(url: string): Promise<LicenseInfo> {
|
||||
const hash = url.substr("https://i.imgur.com/".length).split(".jpg")[0]
|
||||
public async DownloadAttribution(providedImage: {url: string}): Promise<LicenseInfo> {
|
||||
const url = providedImage.url
|
||||
const hash = url.substr("https://i.imgur.com/".length).split(/\.jpe?g/i)[0]
|
||||
|
||||
const apiUrl = "https://api.imgur.com/3/image/" + hash
|
||||
const response = await Utils.downloadJsonCached(apiUrl, 365 * 24 * 60 * 60, {
|
||||
|
|
|
@ -133,12 +133,21 @@ export class Mapillary extends ImageProvider {
|
|||
return [this.PrepareUrlAsync(key, value)]
|
||||
}
|
||||
|
||||
public async DownloadAttribution(_: string): Promise<LicenseInfo> {
|
||||
public async DownloadAttribution(providedImage: ProvidedImage): Promise<LicenseInfo> {
|
||||
const mapillaryId = providedImage.id
|
||||
const metadataUrl =
|
||||
"https://graph.mapillary.com/" +
|
||||
mapillaryId +
|
||||
"?fields=thumb_1024_url,thumb_original_url,captured_at,creator&access_token=" +
|
||||
Constants.mapillary_client_token_v4
|
||||
const response = await Utils.downloadJsonCached(metadataUrl, 60 * 60)
|
||||
|
||||
const license = new LicenseInfo()
|
||||
license.artist = undefined
|
||||
license.artist = response["creator"]["username"]
|
||||
license.license = "CC BY-SA 4.0"
|
||||
// license.license = "Creative Commons Attribution-ShareAlike 4.0 International License";
|
||||
license.attributionRequired = true
|
||||
license.date = new Date(response["captured_at"])
|
||||
return license
|
||||
}
|
||||
|
||||
|
@ -151,16 +160,19 @@ export class Mapillary extends ImageProvider {
|
|||
const metadataUrl =
|
||||
"https://graph.mapillary.com/" +
|
||||
mapillaryId +
|
||||
"?fields=thumb_1024_url,thumb_original_url&access_token=" +
|
||||
"?fields=thumb_1024_url,thumb_original_url,captured_at,creator&access_token=" +
|
||||
Constants.mapillary_client_token_v4
|
||||
const response = await Utils.downloadJsonCached(metadataUrl, 60 * 60)
|
||||
const url = <string>response["thumb_1024_url"]
|
||||
const url_hd = <string>response["thumb_original_url"]
|
||||
return {
|
||||
const date = new Date()
|
||||
date.setTime(response["captured_at"])
|
||||
return <ProvidedImage> {
|
||||
id: "" + mapillaryId,
|
||||
url,
|
||||
url_hd,
|
||||
provider: this,
|
||||
date,
|
||||
key,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ export class WikidataImageProvider extends ImageProvider {
|
|||
return allImages
|
||||
}
|
||||
|
||||
public DownloadAttribution(_: string): Promise<any> {
|
||||
public DownloadAttribution(_): Promise<any> {
|
||||
throw new Error("Method not implemented; shouldn't be needed!")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
public static readonly singleton = new WikimediaImageProvider()
|
||||
public static readonly apiUrls = [
|
||||
"https://commons.wikimedia.org/wiki/",
|
||||
"https://upload.wikimedia.org",
|
||||
"https://upload.wikimedia.org"
|
||||
]
|
||||
public static readonly commonsPrefixes = [...WikimediaImageProvider.apiUrls, "File:"]
|
||||
private readonly commons_key = "wikimedia_commons"
|
||||
|
@ -31,13 +31,18 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
return path.substring(path.lastIndexOf("/") + 1)
|
||||
}
|
||||
|
||||
private static PrepareUrl(value: string): string {
|
||||
private static PrepareUrl(value: string, useHd = false): string {
|
||||
if (value.toLowerCase().startsWith("https://commons.wikimedia.org/wiki/")) {
|
||||
return value
|
||||
}
|
||||
return `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(
|
||||
const baseUrl = `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(
|
||||
value
|
||||
)}?width=500&height=400`
|
||||
)}`
|
||||
if (useHd) {
|
||||
return baseUrl
|
||||
}
|
||||
return baseUrl + `?width=500&height=400`
|
||||
|
||||
}
|
||||
|
||||
private static startsWithCommonsPrefix(value: string): boolean {
|
||||
|
@ -109,8 +114,8 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
return [Promise.resolve(this.UrlForImage("File:" + value))]
|
||||
}
|
||||
|
||||
public async DownloadAttribution(filename: string): Promise<LicenseInfo> {
|
||||
filename = WikimediaImageProvider.ExtractFileName(filename)
|
||||
public async DownloadAttribution(img: ProvidedImage): Promise<LicenseInfo> {
|
||||
const filename = WikimediaImageProvider.ExtractFileName(img.url)
|
||||
|
||||
if (filename === "") {
|
||||
return undefined
|
||||
|
@ -166,9 +171,10 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
}
|
||||
return {
|
||||
url: WikimediaImageProvider.PrepareUrl(image),
|
||||
url_hd: WikimediaImageProvider.PrepareUrl(image, true),
|
||||
key: undefined,
|
||||
provider: this,
|
||||
id: image,
|
||||
id: image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,20 @@ export default class ChangeLocationAction extends OsmChangeAction {
|
|||
private readonly _id: number
|
||||
private readonly _newLonLat: [number, number]
|
||||
private readonly _meta: { theme: string; reason: string }
|
||||
static metatags: {
|
||||
readonly key?: string
|
||||
readonly value?: string
|
||||
readonly docs: string
|
||||
readonly changeType: string[]
|
||||
readonly specialMotivation?: boolean
|
||||
}[] = [
|
||||
{
|
||||
value: "relocated|improve_accuraccy|...",
|
||||
docs: "Will appear if the ",
|
||||
changeType: ["move"],
|
||||
specialMotivation: true,
|
||||
},
|
||||
]
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
|
|
|
@ -4,6 +4,27 @@ import { TagsFilter } from "../../Tags/TagsFilter"
|
|||
import { OsmTags } from "../../../Models/OsmFeature"
|
||||
|
||||
export default class ChangeTagAction extends OsmChangeAction {
|
||||
static metatags: {
|
||||
readonly key?: string
|
||||
readonly value?: string
|
||||
readonly docs: string
|
||||
readonly changeType: string[]
|
||||
readonly specialMotivation?: boolean
|
||||
}[] = [
|
||||
{
|
||||
changeType: ["answer"],
|
||||
docs: "Indicates the number of questions that have been answered",
|
||||
},
|
||||
{ changeType: ["soft-delete"], docs: "Indicates the number of soft-deleted items" },
|
||||
{
|
||||
changeType: ["add-image"],
|
||||
docs: "Indicates the number of images that have been added in this changeset",
|
||||
},
|
||||
{
|
||||
changeType: ["link-image"],
|
||||
docs: "Indicates the number of images that have been linked in this changeset",
|
||||
},
|
||||
]
|
||||
private readonly _elementId: string
|
||||
/**
|
||||
* The tags to apply onto the object
|
||||
|
|
|
@ -13,6 +13,12 @@ import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler"
|
|||
import { OsmConnection } from "./OsmConnection"
|
||||
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
|
||||
import OsmObjectDownloader from "./OsmObjectDownloader"
|
||||
import Combine from "../../UI/Base/Combine"
|
||||
import BaseUIElement from "../../UI/BaseUIElement"
|
||||
import Title from "../../UI/Base/Title"
|
||||
import Table from "../../UI/Base/Table"
|
||||
import ChangeLocationAction from "./Actions/ChangeLocationAction"
|
||||
import ChangeTagAction from "./Actions/ChangeTagAction"
|
||||
|
||||
/**
|
||||
* Handles all changes made to OSM.
|
||||
|
@ -99,6 +105,97 @@ export class Changes {
|
|||
return changes
|
||||
}
|
||||
|
||||
public static getDocs(): BaseUIElement {
|
||||
function addSource(items: any[], src: string) {
|
||||
items.forEach((i) => {
|
||||
i["source"] = src
|
||||
})
|
||||
return items
|
||||
}
|
||||
const metatagsDocs: {
|
||||
key?: string
|
||||
value?: string
|
||||
docs: string
|
||||
changeType?: string[]
|
||||
specialMotivation?: boolean
|
||||
source?: string
|
||||
}[] = [
|
||||
...addSource(
|
||||
[
|
||||
{
|
||||
key: "comment",
|
||||
docs: "The changeset comment. Will be a fixed string, mentioning the theme",
|
||||
},
|
||||
{
|
||||
key: "theme",
|
||||
docs: "The name of the theme that was used to create this change. ",
|
||||
},
|
||||
{
|
||||
key: "source",
|
||||
value: "survey",
|
||||
docs: "The contributor had their geolocation enabled while making changes",
|
||||
},
|
||||
{
|
||||
key: "change_within_{distance}",
|
||||
docs: "If the contributor enabled their geolocation, this will hint how far away they were from the objects they edited. This gives an indication of proximity and if they truly surveyed or were armchair-mapping",
|
||||
},
|
||||
{
|
||||
key: "change_over_{distance}",
|
||||
docs: "If the contributor enabled their geolocation, this will hint how far away they were from the objects they edited. If they were over 5000m away, the might have been armchair-mapping",
|
||||
},
|
||||
{
|
||||
key: "created_by",
|
||||
value: "MapComplete <version>",
|
||||
docs: "The piece of software used to create this changeset; will always start with MapComplete, followed by the version number",
|
||||
},
|
||||
{
|
||||
key: "locale",
|
||||
value: "en|nl|de|...",
|
||||
docs: "The code of the language that the contributor used MapComplete in. Hints what language the user speaks.",
|
||||
},
|
||||
{
|
||||
key: "host",
|
||||
value: "https://mapcomplete.org/<theme>",
|
||||
docs: "The URL that the contributor used to make changes. One can see the used instance with this",
|
||||
},
|
||||
{
|
||||
key: "imagery",
|
||||
docs: "The identifier of the used background layer, this will probably be an identifier from the [editor layer index](https://github.com/osmlab/editor-layer-index)",
|
||||
},
|
||||
],
|
||||
"default"
|
||||
),
|
||||
...addSource(ChangeTagAction.metatags, "ChangeTag"),
|
||||
...addSource(ChangeLocationAction.metatags, "ChangeLocation"),
|
||||
// TODO
|
||||
/*
|
||||
...DeleteAction.metatags,
|
||||
...LinkImageAction.metatags,
|
||||
...OsmChangeAction.metatags,
|
||||
...RelationSplitHandler.metatags,
|
||||
...ReplaceGeometryAction.metatags,
|
||||
...SplitAction.metatags,*/
|
||||
]
|
||||
return new Combine([
|
||||
new Title("Metatags on a changeset", 1),
|
||||
"You might encounter the following metatags on a changeset:",
|
||||
new Table(
|
||||
["key", "value", "explanation", "source"],
|
||||
metatagsDocs.map(({ key, value, docs, source, changeType, specialMotivation }) => [
|
||||
key ?? changeType?.join(", ") ?? "",
|
||||
value,
|
||||
new Combine([
|
||||
docs,
|
||||
specialMotivation
|
||||
? "This might give a reason per modified node or way"
|
||||
: "",
|
||||
]),
|
||||
source,
|
||||
])
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
private static GetNeededIds(changes: ChangeDescription[]) {
|
||||
return Utils.Dedup(changes.filter((c) => c.id >= 0).map((c) => c.type + "/" + c.id))
|
||||
}
|
||||
|
|
|
@ -6,6 +6,14 @@ import Constants from "../../Models/Constants"
|
|||
import { Changes } from "./Changes"
|
||||
import { Utils } from "../../Utils"
|
||||
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
|
||||
import ChangeLocationAction from "./Actions/ChangeLocationAction"
|
||||
import ChangeTagAction from "./Actions/ChangeTagAction"
|
||||
import DeleteAction from "./Actions/DeleteAction"
|
||||
import LinkImageAction from "./Actions/LinkImageAction"
|
||||
import OsmChangeAction from "./Actions/OsmChangeAction"
|
||||
import RelationSplitHandler from "./Actions/RelationSplitHandler"
|
||||
import ReplaceGeometryAction from "./Actions/ReplaceGeometryAction"
|
||||
import SplitAction from "./Actions/SplitAction"
|
||||
|
||||
export interface ChangesetTag {
|
||||
key: string
|
||||
|
|
|
@ -52,6 +52,7 @@ export class OsmConnection {
|
|||
private readonly _iframeMode: Boolean | boolean
|
||||
private readonly _singlePage: boolean
|
||||
private isChecking = false
|
||||
private readonly _doCheckRegularly
|
||||
|
||||
constructor(options?: {
|
||||
dryRun?: Store<boolean>
|
||||
|
@ -59,12 +60,17 @@ export class OsmConnection {
|
|||
oauth_token?: UIEventSource<string>
|
||||
// Used to keep multiple changesets open and to write to the correct changeset
|
||||
singlePage?: boolean
|
||||
attemptLogin?: true | boolean
|
||||
attemptLogin?: true | boolean,
|
||||
/**
|
||||
* If true: automatically check if we're still online every 5 minutes + fetch messages
|
||||
*/
|
||||
checkOnlineRegularly?: true | boolean
|
||||
}) {
|
||||
options ??= {}
|
||||
this.fakeUser = options?.fakeUser ?? false
|
||||
this._singlePage = options?.singlePage ?? true
|
||||
this._oauth_config = Constants.osmAuthConfig
|
||||
this._doCheckRegularly = options?.checkOnlineRegularly ?? true
|
||||
console.debug("Using backend", this._oauth_config.url)
|
||||
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top
|
||||
|
||||
|
@ -91,9 +97,11 @@ export class OsmConnection {
|
|||
ud.name = "Fake user"
|
||||
ud.totalMessages = 42
|
||||
ud.languages = ["en"]
|
||||
this.loadingStatus.setData("logged-in")
|
||||
}
|
||||
const self = this
|
||||
this.UpdateCapabilities()
|
||||
|
||||
this.isLoggedIn = this.userDetails.map(
|
||||
(user) =>
|
||||
user.loggedIn &&
|
||||
|
@ -111,11 +119,11 @@ export class OsmConnection {
|
|||
this._dryRun = options.dryRun ?? new UIEventSource<boolean>(false)
|
||||
|
||||
this.updateAuthObject()
|
||||
if(!this.fakeUser){
|
||||
self.CheckForMessagesContinuously()
|
||||
}
|
||||
|
||||
this.preferencesHandler = new OsmPreferences(
|
||||
this.auth,
|
||||
<any /*This is needed to make the tests work*/>this
|
||||
)
|
||||
this.preferencesHandler = new OsmPreferences(this.auth, this, this.fakeUser)
|
||||
|
||||
if (options.oauth_token?.data !== undefined) {
|
||||
console.log(options.oauth_token.data)
|
||||
|
@ -187,23 +195,27 @@ 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",
|
||||
path: "/api/0.6/user/details",
|
||||
path: "/api/0.6/user/details"
|
||||
},
|
||||
function (err, details: XMLDocument) {
|
||||
function(err, details: XMLDocument) {
|
||||
if (err != null) {
|
||||
console.log(err)
|
||||
console.log("Could not login due to:", err)
|
||||
self.loadingStatus.setData("error")
|
||||
if (err.status == 401) {
|
||||
console.log("Clearing tokens...")
|
||||
// Not authorized - our token probably got revoked
|
||||
self.auth.logout()
|
||||
self.LogOut()
|
||||
} else {
|
||||
console.log("Other error. Status:", err.status)
|
||||
self.apiIsOnline.setData("unreachable")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -213,8 +225,6 @@ export class OsmConnection {
|
|||
return
|
||||
}
|
||||
|
||||
self.CheckForMessagesContinuously()
|
||||
|
||||
// details is an XML DOM of user details
|
||||
let userInfo = details.getElementsByTagName("user")[0]
|
||||
|
||||
|
@ -303,12 +313,12 @@ export class OsmConnection {
|
|||
<any>{
|
||||
method,
|
||||
options: {
|
||||
header,
|
||||
header
|
||||
},
|
||||
content,
|
||||
path: `/api/0.6/${path}`,
|
||||
path: `/api/0.6/${path}`
|
||||
},
|
||||
function (err, response) {
|
||||
function(err, response) {
|
||||
if (err !== null) {
|
||||
error(err)
|
||||
} else {
|
||||
|
@ -388,7 +398,7 @@ export class OsmConnection {
|
|||
"notes.json",
|
||||
content,
|
||||
{
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
|
||||
},
|
||||
true
|
||||
)
|
||||
|
@ -400,6 +410,7 @@ export class OsmConnection {
|
|||
}
|
||||
|
||||
public static GpxTrackVisibility = ["private", "public", "trackable", "identifiable"] as const
|
||||
|
||||
public async uploadGpxTrack(
|
||||
gpx: string,
|
||||
options: {
|
||||
|
@ -428,7 +439,7 @@ export class OsmConnection {
|
|||
file: gpx,
|
||||
description: options.description,
|
||||
tags: options.labels?.join(",") ?? "",
|
||||
visibility: options.visibility,
|
||||
visibility: options.visibility
|
||||
}
|
||||
|
||||
if (!contents.description) {
|
||||
|
@ -436,9 +447,9 @@ export class OsmConnection {
|
|||
}
|
||||
const extras = {
|
||||
file:
|
||||
'; filename="' +
|
||||
"; filename=\"" +
|
||||
(options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) +
|
||||
'"\r\nContent-Type: application/gpx+xml',
|
||||
"\"\r\nContent-Type: application/gpx+xml"
|
||||
}
|
||||
|
||||
const boundary = "987654"
|
||||
|
@ -446,7 +457,7 @@ export class OsmConnection {
|
|||
let body = ""
|
||||
for (const key in contents) {
|
||||
body += "--" + boundary + "\r\n"
|
||||
body += 'Content-Disposition: form-data; name="' + key + '"'
|
||||
body += "Content-Disposition: form-data; name=\"" + key + "\""
|
||||
if (extras[key] !== undefined) {
|
||||
body += extras[key]
|
||||
}
|
||||
|
@ -457,7 +468,7 @@ export class OsmConnection {
|
|||
|
||||
const response = await this.post("gpx/create", body, {
|
||||
"Content-Type": "multipart/form-data; boundary=" + boundary,
|
||||
"Content-Length": body.length,
|
||||
"Content-Length": body.length
|
||||
})
|
||||
const parsed = JSON.parse(response)
|
||||
console.log("Uploaded GPX track", parsed)
|
||||
|
@ -480,9 +491,9 @@ export class OsmConnection {
|
|||
{
|
||||
method: "POST",
|
||||
|
||||
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`,
|
||||
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`
|
||||
},
|
||||
function (err, _) {
|
||||
function(err, _) {
|
||||
if (err !== null) {
|
||||
error(err)
|
||||
} else {
|
||||
|
@ -497,7 +508,7 @@ export class OsmConnection {
|
|||
* To be called by land.html
|
||||
*/
|
||||
public finishLogin(callback: (previousURL: string) => void) {
|
||||
this.auth.authenticate(function () {
|
||||
this.auth.authenticate(function() {
|
||||
// Fully authed at this point
|
||||
console.log("Authentication successful!")
|
||||
const previousLocation = LocalStorageSource.Get("location_before_login")
|
||||
|
@ -536,7 +547,7 @@ export class OsmConnection {
|
|||
? "https://mapcomplete.org/land.html"
|
||||
: window.location.protocol + "//" + window.location.host + "/land.html",
|
||||
singlepage: !standalone,
|
||||
auto: true,
|
||||
auto: true
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -545,26 +556,44 @@ export class OsmConnection {
|
|||
if (this.isChecking) {
|
||||
return
|
||||
}
|
||||
this.isChecking = true
|
||||
Stores.Chronic(5 * 60 * 1000).addCallback((_) => {
|
||||
if (self.isLoggedIn.data) {
|
||||
Stores.Chronic(3 * 1000).addCallback((_) => {
|
||||
if (!(self.apiIsOnline.data === "unreachable" || self.apiIsOnline.data === "offline")) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
console.log("Api is offline - trying to reconnect...")
|
||||
self.AttemptLogin()
|
||||
} catch (e) {
|
||||
console.log("Could not login due to", e)
|
||||
}
|
||||
})
|
||||
this.isChecking = true
|
||||
if (!this._doCheckRegularly) {
|
||||
return
|
||||
}
|
||||
Stores.Chronic(60 * 5 * 1000).addCallback((_) => {
|
||||
if (self.isLoggedIn.data) {
|
||||
try {
|
||||
self.AttemptLogin()
|
||||
} catch (e) {
|
||||
console.log("Could not login due to", e)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private UpdateCapabilities(): void {
|
||||
const self = this
|
||||
if (this.fakeUser) {
|
||||
return
|
||||
}
|
||||
this.FetchCapabilities().then(({ api, gpx }) => {
|
||||
self.apiIsOnline.setData(api)
|
||||
self.gpxServiceIsOnline.setData(gpx)
|
||||
this.apiIsOnline.setData(api)
|
||||
this.gpxServiceIsOnline.setData(gpx)
|
||||
})
|
||||
}
|
||||
|
||||
private readonly _userInfoCache: Record<number, any> = {}
|
||||
|
||||
public async getInformationAboutUser(id: number): Promise<{
|
||||
id: number
|
||||
display_name: string
|
||||
|
@ -587,6 +616,7 @@ export class OsmConnection {
|
|||
this._userInfoCache[id] = parsed
|
||||
return parsed
|
||||
}
|
||||
|
||||
private async FetchCapabilities(): Promise<{ api: OsmServiceState; gpx: OsmServiceState }> {
|
||||
if (Utils.runningFromConsole) {
|
||||
return { api: "online", gpx: "online" }
|
||||
|
|
|
@ -2,6 +2,9 @@ import { UIEventSource } from "../UIEventSource"
|
|||
import UserDetails, { OsmConnection } from "./OsmConnection"
|
||||
import { Utils } from "../../Utils"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
// @ts-ignore
|
||||
import { osmAuth } from "osm-auth"
|
||||
import OSMAuthInstance = OSMAuth.OSMAuthInstance
|
||||
|
||||
export class OsmPreferences {
|
||||
/**
|
||||
|
@ -17,16 +20,17 @@ export class OsmPreferences {
|
|||
* @private
|
||||
*/
|
||||
private readonly preferenceSources = new Map<string, UIEventSource<string>>()
|
||||
private auth: any
|
||||
private readonly auth: OSMAuthInstance
|
||||
private userDetails: UIEventSource<UserDetails>
|
||||
private longPreferences = {}
|
||||
private readonly _fakeUser: boolean
|
||||
|
||||
constructor(auth, osmConnection: OsmConnection) {
|
||||
constructor(auth: OSMAuthInstance, osmConnection: OsmConnection, fakeUser: boolean = false) {
|
||||
this.auth = auth
|
||||
this._fakeUser = fakeUser
|
||||
this.userDetails = osmConnection.userDetails
|
||||
const self = this
|
||||
osmConnection.OnLoggedIn(() => {
|
||||
self.UpdatePreferences(true)
|
||||
this.UpdatePreferences(true)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
@ -212,8 +216,21 @@ export class OsmPreferences {
|
|||
})
|
||||
}
|
||||
|
||||
removeAllWithPrefix(prefix: string) {
|
||||
for (const key in this.preferences.data) {
|
||||
if (key.startsWith(prefix)) {
|
||||
this.GetPreference(key, "", { prefix: "" }).setData(undefined)
|
||||
console.log("Clearing preference", key)
|
||||
}
|
||||
}
|
||||
this.preferences.ping()
|
||||
}
|
||||
|
||||
private UpdatePreferences(forceUpdate?: boolean) {
|
||||
const self = this
|
||||
if (this._fakeUser) {
|
||||
return
|
||||
}
|
||||
this.auth.xhr(
|
||||
{
|
||||
method: "GET",
|
||||
|
@ -272,13 +289,15 @@ export class OsmPreferences {
|
|||
}
|
||||
const self = this
|
||||
console.debug("Updating preference", k, " to ", Utils.EllipsesAfter(v, 15))
|
||||
|
||||
if (this._fakeUser) {
|
||||
return
|
||||
}
|
||||
if (v === undefined || v === "") {
|
||||
this.auth.xhr(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: "/api/0.6/user/preferences/" + encodeURIComponent(k),
|
||||
options: { header: { "Content-Type": "text/plain" } },
|
||||
headers: { "Content-Type": "text/plain" },
|
||||
},
|
||||
function (error) {
|
||||
if (error) {
|
||||
|
@ -297,7 +316,7 @@ export class OsmPreferences {
|
|||
{
|
||||
method: "PUT",
|
||||
path: "/api/0.6/user/preferences/" + encodeURIComponent(k),
|
||||
options: { header: { "Content-Type": "text/plain" } },
|
||||
headers: { "Content-Type": "text/plain" },
|
||||
content: v,
|
||||
},
|
||||
function (error) {
|
||||
|
@ -311,14 +330,4 @@ export class OsmPreferences {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
removeAllWithPrefix(prefix: string) {
|
||||
for (const key in this.preferences.data) {
|
||||
if (key.startsWith(prefix)) {
|
||||
this.GetPreference(key, "", { prefix: "" }).setData(undefined)
|
||||
console.log("Clearing preference", key)
|
||||
}
|
||||
}
|
||||
this.preferences.ping()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ export class And extends TagsFilter {
|
|||
return { and: this.and.map((a) => a.asJson()) }
|
||||
}
|
||||
|
||||
asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record<string, string>) {
|
||||
asHumanString(linkToWiki?: boolean, shorten?: boolean, properties?: Record<string, string>) {
|
||||
return this.and
|
||||
.map((t) => {
|
||||
let e = t.asHumanString(linkToWiki, shorten, properties)
|
||||
|
@ -159,7 +159,7 @@ export class And extends TagsFilter {
|
|||
return [].concat(...this.and.map((subkeys) => subkeys.usedTags()))
|
||||
}
|
||||
|
||||
asChange(properties: Record<string, string>): { k: string; v: string }[] {
|
||||
asChange(properties: Readonly<Record<string, string>>): { k: string; v: string }[] {
|
||||
const result = []
|
||||
for (const tagsFilter of this.and) {
|
||||
result.push(...tagsFilter.asChange(properties))
|
||||
|
|
|
@ -3,7 +3,7 @@ import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
|
|||
import { Tag } from "./Tag"
|
||||
import { ExpressionSpecification } from "maplibre-gl"
|
||||
|
||||
export default class ComparingTag implements TagsFilter {
|
||||
export default class ComparingTag extends TagsFilter {
|
||||
private readonly _key: string
|
||||
private readonly _predicate: (value: string) => boolean
|
||||
private readonly _representation: "<" | ">" | "<=" | ">="
|
||||
|
@ -15,13 +15,14 @@ export default class ComparingTag implements TagsFilter {
|
|||
representation: "<" | ">" | "<=" | ">=",
|
||||
boundary: string
|
||||
) {
|
||||
super()
|
||||
this._key = key
|
||||
this._predicate = predicate
|
||||
this._representation = representation
|
||||
this._boundary = boundary
|
||||
}
|
||||
|
||||
asChange(_: Record<string, string>): { k: string; v: string }[] {
|
||||
asChange(_: Readonly<Record<string, string>>): { k: string; v: string }[] {
|
||||
throw "A comparable tag can not be used to be uploaded to OSM"
|
||||
}
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@ export class Or extends TagsFilter {
|
|||
return [].concat(...this.or.map((subkeys) => subkeys.usedTags()))
|
||||
}
|
||||
|
||||
asChange(properties: Record<string, string>): { k: string; v: string }[] {
|
||||
asChange(properties: Readonly<Record<string, string>>): { k: string; v: string }[] {
|
||||
const result = []
|
||||
for (const tagsFilter of this.or) {
|
||||
result.push(...tagsFilter.asChange(properties))
|
||||
|
|
|
@ -13,12 +13,13 @@ import { ExpressionSpecification } from "maplibre-gl"
|
|||
* The 'key' is always fixed and should not contain substitutions.
|
||||
* This cannot be used to query features
|
||||
*/
|
||||
export default class SubstitutingTag implements TagsFilter {
|
||||
export default class SubstitutingTag extends TagsFilter {
|
||||
private readonly _key: string
|
||||
private readonly _value: string
|
||||
private readonly _invert: boolean
|
||||
|
||||
constructor(key: string, value: string, invert = false) {
|
||||
super()
|
||||
this._key = key
|
||||
this._value = value
|
||||
this._invert = invert
|
||||
|
@ -42,7 +43,7 @@ export default class SubstitutingTag implements TagsFilter {
|
|||
return new Tag(this._key, Utils.SubstituteKeys(this._value, currentProperties))
|
||||
}
|
||||
|
||||
asHumanString(linkToWiki: boolean, shorten: boolean, properties) {
|
||||
asHumanString(linkToWiki?: boolean, shorten?: boolean, properties?: Record<string, string>) {
|
||||
return (
|
||||
this._key +
|
||||
(this._invert ? "!" : "") +
|
||||
|
@ -99,7 +100,7 @@ export default class SubstitutingTag implements TagsFilter {
|
|||
return []
|
||||
}
|
||||
|
||||
asChange(properties: Record<string, string>): { k: string; v: string }[] {
|
||||
asChange(properties: Readonly<Record<string, string>>): { k: string; v: string }[] {
|
||||
if (this._invert) {
|
||||
throw "An inverted substituting tag can not be used to create a change"
|
||||
}
|
||||
|
|
|
@ -729,7 +729,7 @@ export class TagUtils {
|
|||
}
|
||||
if (typeof json != "string") {
|
||||
if (json["and"] !== undefined && json["or"] !== undefined) {
|
||||
throw `Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined. Did you override a value? Perhaps use \`"=parent": { ... }\` instead of \"parent": {...}\` to trigger a replacement and not a fuse of values`
|
||||
throw `Error while parsing a TagConfig: got an object where both 'and' and 'or' are defined. Did you override a value? Perhaps use \`"=parent": { ... }\` instead of \"parent": {...}\` to trigger a replacement and not a fuse of values. The value is ${JSON.stringify(json)}`
|
||||
}
|
||||
if (json["and"] !== undefined) {
|
||||
return new And(json["and"].map((t) => TagUtils.Tag(t, context)))
|
||||
|
|
|
@ -15,9 +15,9 @@ export abstract class TagsFilter {
|
|||
abstract matchesProperties(properties: Record<string, string>): boolean
|
||||
|
||||
abstract asHumanString(
|
||||
linkToWiki: boolean,
|
||||
shorten: boolean,
|
||||
properties: Record<string, string>
|
||||
linkToWiki?: boolean,
|
||||
shorten?: boolean,
|
||||
properties?: Record<string, string>
|
||||
): string
|
||||
|
||||
abstract asJson(): TagConfigJson
|
||||
|
@ -34,9 +34,18 @@ export abstract class TagsFilter {
|
|||
* Converts the tagsFilter into a list of key-values that should be uploaded to OSM.
|
||||
* Throws an error if not applicable.
|
||||
*
|
||||
* Note: properties are the already existing tags-object. It is only used in the substituting tag
|
||||
* @param properties are the already existing tags-object. It is only used in the substituting tag and will not be changed
|
||||
*/
|
||||
abstract asChange(properties: Record<string, string>): { k: string; v: string }[]
|
||||
abstract asChange(properties: Readonly<Record<string, string>>): { k: string; v: string }[]
|
||||
|
||||
public applyOn(properties: Readonly<Record<string, string>>): Record<string, string> {
|
||||
const copy = { ...properties }
|
||||
const changes = this.asChange(properties)
|
||||
for (const { k, v } of changes) {
|
||||
copy[k] = v
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an optimized version (or self) of this tagsFilter
|
||||
|
|
|
@ -3,34 +3,33 @@ import { MangroveReviews, Review } from "mangrove-reviews-typescript"
|
|||
import { Utils } from "../../Utils"
|
||||
import { Feature, Position } from "geojson"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import ScriptUtils from "../../../scripts/ScriptUtils"
|
||||
|
||||
export class MangroveIdentity {
|
||||
private readonly keypair: Store<CryptoKeyPair>
|
||||
private readonly keypair: UIEventSource<CryptoKeyPair> = new UIEventSource<CryptoKeyPair>(undefined)
|
||||
/**
|
||||
* Same as the one in the user settings
|
||||
*/
|
||||
public readonly mangroveIdentity: UIEventSource<string>
|
||||
private readonly key_id: Store<string>
|
||||
private readonly key_id: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||
private readonly _mangroveIdentityCreationDate: UIEventSource<string>
|
||||
|
||||
constructor(mangroveIdentity: UIEventSource<string>, mangroveIdentityCreationDate: UIEventSource<string>) {
|
||||
this.mangroveIdentity = mangroveIdentity
|
||||
this._mangroveIdentityCreationDate = mangroveIdentityCreationDate
|
||||
const key_id = new UIEventSource<string>(undefined)
|
||||
this.key_id = key_id
|
||||
const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined)
|
||||
this.keypair = keypairEventSource
|
||||
mangroveIdentity.addCallbackAndRunD(async (data) => {
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data))
|
||||
keypairEventSource.setData(keypair)
|
||||
const pem = await MangroveReviews.publicToPem(keypair.publicKey)
|
||||
key_id.setData(pem)
|
||||
await this.setKeypair(data)
|
||||
})
|
||||
}
|
||||
|
||||
private async setKeypair(data: string){
|
||||
console.log("Setting keypair from",data)
|
||||
const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data))
|
||||
this.keypair.setData(keypair)
|
||||
const pem = await MangroveReviews.publicToPem(keypair.publicKey)
|
||||
this.key_id.setData(pem)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an identity if none exists already.
|
||||
* Is written into the UIEventsource, which was passed into the constructor
|
||||
|
@ -43,7 +42,9 @@ export class MangroveIdentity {
|
|||
// Identity has been loaded via osmPreferences by now - we don't overwrite
|
||||
return
|
||||
}
|
||||
console.log("Creating a new Mangrove identity!")
|
||||
this.keypair.setData(keypair)
|
||||
const pem = await MangroveReviews.publicToPem(keypair.publicKey)
|
||||
this.key_id.setData(pem)
|
||||
this.mangroveIdentity.setData(JSON.stringify(jwk))
|
||||
this._mangroveIdentityCreationDate.setData(new Date().toISOString())
|
||||
}
|
||||
|
@ -52,7 +53,7 @@ export class MangroveIdentity {
|
|||
* Only called to create a review.
|
||||
*/
|
||||
async getKeypair(): Promise<CryptoKeyPair> {
|
||||
if (this.keypair.data ?? "" === "") {
|
||||
if (this.keypair.data === undefined) {
|
||||
// We want to create a review, but it seems like no key has been setup at this moment
|
||||
// We create the key
|
||||
try {
|
||||
|
@ -70,31 +71,51 @@ export class MangroveIdentity {
|
|||
return this.key_id
|
||||
}
|
||||
|
||||
private geoReviewsById: Store<(Review & { kid: string; signature: string })[]> =
|
||||
undefined
|
||||
|
||||
public getGeoReviews(): Store<(Review & { kid: string, signature: string })[] | undefined> {
|
||||
if (!this.geoReviewsById) {
|
||||
const all = this.getAllReviews()
|
||||
this.geoReviewsById = this.getAllReviews().mapD(reviews => reviews.filter(
|
||||
review => {
|
||||
try {
|
||||
const subjectUrl = new URL(review.sub)
|
||||
return subjectUrl.protocol === "geo:"
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
return this.geoReviewsById
|
||||
}
|
||||
|
||||
private allReviewsById: UIEventSource<(Review & { kid: string; signature: string })[]> =
|
||||
undefined
|
||||
|
||||
/**
|
||||
* Gets all reviews that are made for the current identity.
|
||||
* The returned store will contain `undefined` if still loading
|
||||
*/
|
||||
public getAllReviews(): Store<(Review & { kid: string; signature: string })[]> {
|
||||
public getAllReviews(): Store<(Review & { kid: string; signature: string })[] | undefined> {
|
||||
if (this.allReviewsById !== undefined) {
|
||||
return this.allReviewsById
|
||||
}
|
||||
this.allReviewsById = new UIEventSource([])
|
||||
this.key_id.map((pem) => {
|
||||
this.allReviewsById = new UIEventSource(undefined)
|
||||
this.key_id.map(async (pem) => {
|
||||
if (pem === undefined) {
|
||||
return []
|
||||
}
|
||||
MangroveReviews.getReviews({
|
||||
kid: pem,
|
||||
}).then((allReviews) => {
|
||||
this.allReviewsById.setData(
|
||||
allReviews.reviews.map((r) => ({
|
||||
...r,
|
||||
...r.payload,
|
||||
}))
|
||||
)
|
||||
const allReviews = await MangroveReviews.getReviews({
|
||||
kid: pem
|
||||
})
|
||||
this.allReviewsById.setData(
|
||||
allReviews.reviews.map((r) => ({
|
||||
...r,
|
||||
...r.payload
|
||||
}))
|
||||
)
|
||||
})
|
||||
return this.allReviewsById
|
||||
}
|
||||
|
@ -125,6 +146,7 @@ export default class FeatureReviews {
|
|||
private readonly _uncertainty: number
|
||||
private readonly _name: Store<string>
|
||||
private readonly _identity: MangroveIdentity
|
||||
private readonly _testmode: Store<boolean>
|
||||
|
||||
private constructor(
|
||||
feature: Feature,
|
||||
|
@ -134,11 +156,13 @@ export default class FeatureReviews {
|
|||
nameKey?: "name" | string
|
||||
fallbackName?: string
|
||||
uncertaintyRadius?: number
|
||||
}
|
||||
},
|
||||
testmode?: Store<boolean>
|
||||
) {
|
||||
const centerLonLat = GeoOperations.centerpointCoordinates(feature)
|
||||
;[this._lon, this._lat] = centerLonLat
|
||||
this._identity = mangroveIdentity
|
||||
this._testmode = testmode ?? new ImmutableStore(false)
|
||||
const nameKey = options?.nameKey ?? "name"
|
||||
|
||||
if (feature.geometry.type === "Point") {
|
||||
|
@ -210,19 +234,20 @@ export default class FeatureReviews {
|
|||
public static construct(
|
||||
feature: Feature,
|
||||
tagsSource: UIEventSource<Record<string, string>>,
|
||||
mangroveIdentity?: MangroveIdentity,
|
||||
options?: {
|
||||
mangroveIdentity: MangroveIdentity,
|
||||
options: {
|
||||
nameKey?: "name" | string
|
||||
fallbackName?: string
|
||||
uncertaintyRadius?: number
|
||||
}
|
||||
) {
|
||||
},
|
||||
testmode: Store<boolean>
|
||||
): FeatureReviews {
|
||||
const key = feature.properties.id
|
||||
const cached = FeatureReviews._featureReviewsCache[key]
|
||||
if (cached !== undefined) {
|
||||
return cached
|
||||
}
|
||||
const featureReviews = new FeatureReviews(feature, tagsSource, mangroveIdentity, options)
|
||||
const featureReviews = new FeatureReviews(feature, tagsSource, mangroveIdentity, options,testmode )
|
||||
FeatureReviews._featureReviewsCache[key] = featureReviews
|
||||
return featureReviews
|
||||
}
|
||||
|
@ -243,17 +268,22 @@ export default class FeatureReviews {
|
|||
}
|
||||
const r: Review = {
|
||||
sub: this.subjectUri.data,
|
||||
...review,
|
||||
...review
|
||||
}
|
||||
const keypair: CryptoKeyPair = await this._identity.getKeypair()
|
||||
const jwt = await MangroveReviews.signReview(keypair, r)
|
||||
const kid = await MangroveReviews.publicToPem(keypair.publicKey)
|
||||
await MangroveReviews.submitReview(jwt)
|
||||
if (!this._testmode.data) {
|
||||
await MangroveReviews.submitReview(jwt)
|
||||
} else {
|
||||
console.log("Testmode enabled - not uploading review")
|
||||
await Utils.waitFor(1000)
|
||||
}
|
||||
const reviewWithKid = {
|
||||
...r,
|
||||
kid,
|
||||
signature: jwt,
|
||||
madeByLoggedInUser: new ImmutableStore(true),
|
||||
madeByLoggedInUser: new ImmutableStore(true)
|
||||
}
|
||||
this._reviews.data.push(reviewWithKid)
|
||||
this._reviews.ping()
|
||||
|
@ -301,7 +331,7 @@ export default class FeatureReviews {
|
|||
signature: reviewData.signature,
|
||||
madeByLoggedInUser: this._identity.getKeyId().map((user_key_id) => {
|
||||
return reviewData.kid === user_key_id
|
||||
}),
|
||||
})
|
||||
})
|
||||
hasNew = true
|
||||
}
|
||||
|
@ -322,7 +352,7 @@ export default class FeatureReviews {
|
|||
// https://www.rfc-editor.org/rfc/rfc5870#section-3.4.2
|
||||
// `u` stands for `uncertainty`, https://www.rfc-editor.org/rfc/rfc5870#section-3.4.3
|
||||
const self = this
|
||||
return this._name.map(function (name) {
|
||||
return this._name.map(function(name) {
|
||||
let uri = `geo:${self._lat},${self._lon}?u=${Math.round(self._uncertainty)}`
|
||||
if (name) {
|
||||
uri += "&q=" + (dontEncodeName ? name : encodeURIComponent(name))
|
||||
|
|
|
@ -7,6 +7,10 @@ export type EliCategory =
|
|||
| "qa"
|
||||
| "elevation"
|
||||
| "other"
|
||||
|
||||
/**
|
||||
* This class has grown beyond the point of only containing Raster Layers
|
||||
*/
|
||||
export interface RasterLayerProperties {
|
||||
/**
|
||||
* The name of the imagery source
|
||||
|
@ -19,7 +23,8 @@ export interface RasterLayerProperties {
|
|||
|
||||
readonly url: string
|
||||
readonly category?: string | EliCategory
|
||||
readonly type?: "vector" | string
|
||||
readonly type?: "vector" | "raster" | string
|
||||
readonly style?: string,
|
||||
|
||||
readonly attribution?: {
|
||||
readonly url?: string
|
||||
|
|
|
@ -48,23 +48,12 @@ export class AvailableRasterLayers {
|
|||
geometry: BBox.global.asGeometry(),
|
||||
}
|
||||
|
||||
public static readonly maptilerDefaultLayer: RasterLayerPolygon = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
name: "MapTiler",
|
||||
url:
|
||||
"https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=" +
|
||||
Constants.maptilerApiKey,
|
||||
category: "osmbasedmap",
|
||||
id: "maptiler",
|
||||
type: "vector",
|
||||
attribution: {
|
||||
text: "Maptiler",
|
||||
url: "https://www.maptiler.com/copyright/",
|
||||
},
|
||||
},
|
||||
geometry: BBox.global.asGeometry(),
|
||||
}
|
||||
/**
|
||||
* The default background layer that any theme uses which does not explicitly define a background
|
||||
*/
|
||||
public static readonly defaultBackgroundLayer: RasterLayerPolygon = AvailableRasterLayers.globalLayers.find(l => {
|
||||
return l.properties.id === "protomaps.sunny"
|
||||
})
|
||||
|
||||
public static layersAvailableAt(
|
||||
location: Store<{ lon: number; lat: number }>,
|
||||
|
@ -90,7 +79,7 @@ export class AvailableRasterLayers {
|
|||
return GeoOperations.inside(lonlat, eliPolygon)
|
||||
})
|
||||
matching.unshift(AvailableRasterLayers.osmCarto)
|
||||
matching.push(AvailableRasterLayers.maptilerDefaultLayer)
|
||||
matching.push(AvailableRasterLayers.defaultBackgroundLayer)
|
||||
if (enableBing?.data) {
|
||||
matching.push(AvailableRasterLayers.bing)
|
||||
}
|
||||
|
@ -107,7 +96,7 @@ export class AvailableRasterLayers {
|
|||
all.push(...AvailableRasterLayers.globalLayers.map((l) => l.properties.id))
|
||||
all.push(...AvailableRasterLayers.EditorLayerIndex.map((l) => l.properties.id))
|
||||
all.push(this.osmCarto.properties.id)
|
||||
all.push(this.maptilerDefaultLayer.properties.id)
|
||||
all.push(this.defaultBackgroundLayer.properties.id)
|
||||
return new Set<string>(all)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -277,9 +277,11 @@ export class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
backgroundId === "photo" || backgroundId === "map" || backgroundId === "osmbasedmap"
|
||||
|
||||
if (!isCategory && !ValidateTheme._availableLayers.has(backgroundId)) {
|
||||
const options = Array.from(ValidateTheme._availableLayers)
|
||||
const nearby = Utils.sortedByLevenshteinDistance(backgroundId, options, t => t)
|
||||
context
|
||||
.enter("defaultBackgroundId")
|
||||
.err("This layer ID is not known: " + backgroundId)
|
||||
.err(`This layer ID is not known: ${backgroundId}. Perhaps you meant one of ${nearby.slice(0,5).join(", ")}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -850,7 +852,7 @@ class CheckTranslation extends DesugaringStep<Translatable> {
|
|||
for (const key of keys) {
|
||||
const lng = json[key]
|
||||
if (lng === "") {
|
||||
context.enter(lng).err("Got an empty string in translation for language " + lng)
|
||||
context.enter(lng).err("Got an empty string in translation for language " + key)
|
||||
}
|
||||
|
||||
// TODO validate that all subparts are here
|
||||
|
@ -1012,6 +1014,13 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
) {
|
||||
continue
|
||||
}
|
||||
if(json.freeform.key.indexOf("wikidata")>=0){
|
||||
context
|
||||
.enter("render")
|
||||
.err(
|
||||
`The rendering for language ${ln} does not contain \`{${json.freeform.key}}\`. Did you perhaps forget to set "freeform.type: 'wikidata'"?`
|
||||
)
|
||||
}
|
||||
context
|
||||
.enter("render")
|
||||
.err(
|
||||
|
@ -1264,7 +1273,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
// It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list
|
||||
context
|
||||
.enter("tagRenderings")
|
||||
.err("Some tagrenderings have a duplicate id: " + duplicates.join(", "))
|
||||
.err("Some tagrenderings have a duplicate id: " + duplicates.join(", ")+"\n"+JSON.stringify(json.tagRenderings.filter(tr=> duplicates.indexOf(tr["id"])>=0)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1837,6 +1846,7 @@ export class ValidateThemeEnsemble extends Conversion<
|
|||
{
|
||||
tags: TagsFilter
|
||||
foundInTheme: string[]
|
||||
isCounted: boolean
|
||||
}
|
||||
>
|
||||
> {
|
||||
|
@ -1855,10 +1865,11 @@ export class ValidateThemeEnsemble extends Conversion<
|
|||
string,
|
||||
{
|
||||
tags: TagsFilter
|
||||
foundInTheme: string[]
|
||||
foundInTheme: string[],
|
||||
isCounted: boolean
|
||||
}
|
||||
> {
|
||||
const idToSource = new Map<string, { tags: TagsFilter; foundInTheme: string[] }>()
|
||||
const idToSource = new Map<string, { tags: TagsFilter; foundInTheme: string[], isCounted: boolean }>()
|
||||
|
||||
for (const theme of json) {
|
||||
for (const layer of theme.layers) {
|
||||
|
@ -1879,7 +1890,7 @@ export class ValidateThemeEnsemble extends Conversion<
|
|||
const id = layer.id
|
||||
const tags = layer.source.osmTags
|
||||
if (!idToSource.has(id)) {
|
||||
idToSource.set(id, { tags, foundInTheme: [theme.id] })
|
||||
idToSource.set(id, { tags, foundInTheme: [theme.id], isCounted: layer.doCount })
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -1888,6 +1899,7 @@ export class ValidateThemeEnsemble extends Conversion<
|
|||
if (oldTags.shadows(tags) && tags.shadows(oldTags)) {
|
||||
// All is good, all is well
|
||||
oldTheme.push(theme.id)
|
||||
idToSource.get(id).isCounted ||= layer.doCount
|
||||
continue
|
||||
}
|
||||
context.err(
|
||||
|
|
|
@ -38,7 +38,7 @@ export default interface LineRenderingConfigJson {
|
|||
/**
|
||||
* question: Should a dasharray be used to render the lines?
|
||||
* The dasharray defines 'pixels of line, pixels of gap, pixels of line, pixels of gap, ...'. For example, `5 6` will be 5 pixels of line followed by a 6 pixel gap.
|
||||
* Cannot be a dynamic property due to a mapbox limitation
|
||||
* Cannot be a dynamic property due to a MapLibre limitation (see https://github.com/maplibre/maplibre-gl-js/issues/1235)
|
||||
* ifunset: Ways are rendered with a full line
|
||||
*/
|
||||
dashArray?: string
|
||||
|
|
|
@ -91,7 +91,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
mercatorCrs: json.source["mercatorCrs"],
|
||||
idKey: json.source["idKey"],
|
||||
},
|
||||
json.id
|
||||
json.id,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -106,8 +106,8 @@ export default class LayerConfig extends WithContextLoader {
|
|||
}
|
||||
this.units = [].concat(
|
||||
...(json.units ?? []).map((unitJson, i) =>
|
||||
Unit.fromJson(unitJson, `${context}.unit[${i}]`)
|
||||
)
|
||||
Unit.fromJson(unitJson, `${context}.unit[${i}]`),
|
||||
),
|
||||
)
|
||||
|
||||
if (json.description !== undefined) {
|
||||
|
@ -122,7 +122,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
if (json.calculatedTags !== undefined) {
|
||||
if (!official) {
|
||||
console.warn(
|
||||
`Unofficial theme ${this.id} with custom javascript! This is a security risk`
|
||||
`Unofficial theme ${this.id} with custom javascript! This is a security risk`,
|
||||
)
|
||||
}
|
||||
this.calculatedTags = []
|
||||
|
@ -159,7 +159,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
}
|
||||
this.minzoomVisible = json.minzoomVisible ?? this.minzoom
|
||||
this.shownByDefault = json.shownByDefault ?? true
|
||||
this.doCount = json.isCounted ?? true
|
||||
this.doCount = json.isCounted ?? this.shownByDefault ?? true
|
||||
this.forceLoad = json.forceLoad ?? false
|
||||
if (json.presets === null) json.presets = undefined
|
||||
if (json.presets !== undefined && json.presets?.map === undefined) {
|
||||
|
@ -191,7 +191,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
tags: pr.tags.map((t) => TagUtils.SimpleTag(t)),
|
||||
description: Translations.T(
|
||||
pr.description,
|
||||
`${translationContext}.presets.${i}.description`
|
||||
`${translationContext}.presets.${i}.description`,
|
||||
),
|
||||
preciseInput: preciseInput,
|
||||
exampleImages: pr.exampleImages,
|
||||
|
@ -205,7 +205,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
|
||||
if (json.lineRendering) {
|
||||
this.lineRendering = Utils.NoNull(json.lineRendering).map(
|
||||
(r, i) => new LineRenderingConfig(r, `${context}[${i}]`)
|
||||
(r, i) => new LineRenderingConfig(r, `${context}[${i}]`),
|
||||
)
|
||||
} else {
|
||||
this.lineRendering = []
|
||||
|
@ -213,7 +213,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
|
||||
if (json.pointRendering) {
|
||||
this.mapRendering = Utils.NoNull(json.pointRendering).map(
|
||||
(r, i) => new PointRenderingConfig(r, `${context}[${i}](${this.id})`)
|
||||
(r, i) => new PointRenderingConfig(r, `${context}[${i}](${this.id})`),
|
||||
)
|
||||
} else {
|
||||
this.mapRendering = []
|
||||
|
@ -225,7 +225,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
r.location.has("centroid") ||
|
||||
r.location.has("projected_centerpoint") ||
|
||||
r.location.has("start") ||
|
||||
r.location.has("end")
|
||||
r.location.has("end"),
|
||||
)
|
||||
|
||||
if (
|
||||
|
@ -247,7 +247,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
Constants.priviliged_layers.indexOf(<any>this.id) < 0 &&
|
||||
this.source !== null /*library layer*/ &&
|
||||
!this.source?.geojsonSource?.startsWith(
|
||||
"https://api.openstreetmap.org/api/0.6/notes.json"
|
||||
"https://api.openstreetmap.org/api/0.6/notes.json",
|
||||
)
|
||||
) {
|
||||
throw (
|
||||
|
@ -266,7 +266,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
typeof tr !== "string" &&
|
||||
tr["builtin"] === undefined &&
|
||||
tr["id"] === undefined &&
|
||||
tr["rewrite"] === undefined
|
||||
tr["rewrite"] === undefined,
|
||||
) ?? []
|
||||
if (missingIds?.length > 0 && official) {
|
||||
console.error("Some tagRenderings of", this.id, "are missing an id:", missingIds)
|
||||
|
@ -277,8 +277,8 @@ export default class LayerConfig extends WithContextLoader {
|
|||
(tr, i) =>
|
||||
new TagRenderingConfig(
|
||||
<QuestionableTagRenderingConfigJson>tr,
|
||||
this.id + ".tagRenderings[" + i + "]"
|
||||
)
|
||||
this.id + ".tagRenderings[" + i + "]",
|
||||
),
|
||||
)
|
||||
|
||||
if (
|
||||
|
@ -352,7 +352,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
|
||||
public GetBaseTags(): Record<string, string> {
|
||||
return TagUtils.changeAsProperties(
|
||||
this.source?.osmTags?.asChange({ id: "node/-1" }) ?? [{ k: "id", v: "node/-1" }]
|
||||
this.source?.osmTags?.asChange({ id: "node/-1" }) ?? [{ k: "id", v: "node/-1" }],
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -365,7 +365,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
neededLayer: string
|
||||
}[] = [],
|
||||
addedByDefault = false,
|
||||
canBeIncluded = true
|
||||
canBeIncluded = true,
|
||||
): BaseUIElement {
|
||||
const extraProps: (string | BaseUIElement)[] = []
|
||||
|
||||
|
@ -374,32 +374,32 @@ export default class LayerConfig extends WithContextLoader {
|
|||
if (canBeIncluded) {
|
||||
if (addedByDefault) {
|
||||
extraProps.push(
|
||||
"**This layer is included automatically in every theme. This layer might contain no points**"
|
||||
"**This layer is included automatically in every theme. This layer might contain no points**",
|
||||
)
|
||||
}
|
||||
if (this.shownByDefault === false) {
|
||||
extraProps.push(
|
||||
"This layer is not visible by default and must be enabled in the filter by the user. "
|
||||
"This layer is not visible by default and must be enabled in the filter by the user. ",
|
||||
)
|
||||
}
|
||||
if (this.title === undefined) {
|
||||
extraProps.push(
|
||||
"Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable."
|
||||
"Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable.",
|
||||
)
|
||||
}
|
||||
if (this.name === undefined && this.shownByDefault === false) {
|
||||
extraProps.push(
|
||||
"This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-<id>=true"
|
||||
"This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-<id>=true",
|
||||
)
|
||||
}
|
||||
if (this.name === undefined) {
|
||||
extraProps.push(
|
||||
"Not visible in the layer selection by default. If you want to make this layer toggable, override `name`"
|
||||
"Not visible in the layer selection by default. If you want to make this layer toggable, override `name`",
|
||||
)
|
||||
}
|
||||
if (this.mapRendering.length === 0) {
|
||||
extraProps.push(
|
||||
"Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`"
|
||||
"Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -411,23 +411,28 @@ export default class LayerConfig extends WithContextLoader {
|
|||
: undefined,
|
||||
"This layer is loaded from an external source, namely ",
|
||||
new FixedUiElement(this.source.geojsonSource).SetClass("code"),
|
||||
])
|
||||
]),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
extraProps.push(
|
||||
"This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data."
|
||||
"This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data.",
|
||||
)
|
||||
}
|
||||
|
||||
let usingLayer: BaseUIElement[] = []
|
||||
if (usedInThemes?.length > 0 && !addedByDefault) {
|
||||
usingLayer = [
|
||||
new Title("Themes using this layer", 4),
|
||||
new List(
|
||||
(usedInThemes ?? []).map((id) => new Link(id, "https://mapcomplete.org/" + id))
|
||||
),
|
||||
]
|
||||
if (!addedByDefault) {
|
||||
|
||||
if (usedInThemes?.length > 0) {
|
||||
usingLayer = [
|
||||
new Title("Themes using this layer", 4),
|
||||
new List(
|
||||
(usedInThemes ?? []).map((id) => new Link(id, "https://mapcomplete.org/" + id)),
|
||||
),
|
||||
]
|
||||
} else if(this.source !== null) {
|
||||
usingLayer = [new FixedUiElement("No themes use this layer")]
|
||||
}
|
||||
}
|
||||
|
||||
for (const dep of dependencies) {
|
||||
|
@ -438,7 +443,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
" into the layout as it depends on it: ",
|
||||
dep.reason,
|
||||
"(" + dep.context + ")",
|
||||
])
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -447,7 +452,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
new Combine([
|
||||
"This layer is needed as dependency for layer",
|
||||
new Link(revDep, "#" + revDep),
|
||||
])
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -459,14 +464,14 @@ export default class LayerConfig extends WithContextLoader {
|
|||
return undefined
|
||||
}
|
||||
const embedded: (Link | string)[] = values.values?.map((v) =>
|
||||
Link.OsmWiki(values.key, v, true).SetClass("mr-2")
|
||||
Link.OsmWiki(values.key, v, true).SetClass("mr-2"),
|
||||
) ?? ["_no preset options defined, or no values in them_"]
|
||||
return [
|
||||
new Combine([
|
||||
new Link(
|
||||
"<img src='https://mapcomplete.org/assets/svg/statistics.svg' height='18px'>",
|
||||
"https://taginfo.openstreetmap.org/keys/" + values.key + "#values",
|
||||
true
|
||||
true,
|
||||
),
|
||||
Link.OsmWiki(values.key),
|
||||
]).SetClass("flex"),
|
||||
|
@ -475,7 +480,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
: new Link(values.type, "../SpecialInputElements.md#" + values.type),
|
||||
new Combine(embedded).SetClass("flex"),
|
||||
]
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
let quickOverview: BaseUIElement = undefined
|
||||
|
@ -485,7 +490,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
"this quick overview is incomplete",
|
||||
new Table(
|
||||
["attribute", "type", "values which are supported by this layer"],
|
||||
tableRows
|
||||
tableRows,
|
||||
).SetClass("zebra-table"),
|
||||
]).SetClass("flex-col flex")
|
||||
}
|
||||
|
@ -499,7 +504,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
(mr) =>
|
||||
mr.RenderIcon(new ImmutableStore<OsmTags>({ id: "node/-1" }), {
|
||||
includeBadges: false,
|
||||
}).html
|
||||
}).html,
|
||||
)
|
||||
.find((i) => i !== undefined)
|
||||
}
|
||||
|
@ -511,7 +516,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
"Execute on overpass",
|
||||
Overpass.AsOverpassTurboLink(<TagsFilter>this.source.osmTags.optimize())
|
||||
.replaceAll("(", "%28")
|
||||
.replaceAll(")", "%29")
|
||||
.replaceAll(")", "%29"),
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("Could not generate overpasslink for " + this.id)
|
||||
|
@ -533,19 +538,19 @@ export default class LayerConfig extends WithContextLoader {
|
|||
const parts = neededTags["and"]
|
||||
tagsDescription.push(
|
||||
"Elements must match **all** of the following expressions:",
|
||||
parts.map((p, i) => i + ". " + p.asHumanString(true, false, {})).join("\n")
|
||||
parts.map((p, i) => i + ". " + p.asHumanString(true, false, {})).join("\n"),
|
||||
)
|
||||
} else if (neededTags["or"]) {
|
||||
const parts = neededTags["or"]
|
||||
tagsDescription.push(
|
||||
"Elements must match **any** of the following expressions:",
|
||||
parts.map((p) => " - " + p.asHumanString(true, false, {})).join("\n")
|
||||
parts.map((p) => " - " + p.asHumanString(true, false, {})).join("\n"),
|
||||
)
|
||||
} else {
|
||||
tagsDescription.push(
|
||||
"Elements must match the expression **" +
|
||||
neededTags.asHumanString(true, false, {}) +
|
||||
"**"
|
||||
neededTags.asHumanString(true, false, {}) +
|
||||
"**",
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -556,7 +561,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
|
||||
return new Combine([
|
||||
new Combine([new Title(this.id, 1), iconImg, this.description, "\n"]).SetClass(
|
||||
"flex flex-col"
|
||||
"flex flex-col",
|
||||
),
|
||||
new List(extraProps),
|
||||
...usingLayer,
|
||||
|
|
|
@ -313,6 +313,9 @@ export default class LayoutConfig implements LayoutInformation {
|
|||
if (tags === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if(tags.id.startsWith("current_view")){
|
||||
return this.getLayer("current_view")
|
||||
}
|
||||
for (const layer of this.layers) {
|
||||
if (!layer.source) {
|
||||
if (layer.isShown?.matchesProperties(tags)) {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { TagsFilter } from "../../Logic/Tags/TagsFilter"
|
|||
export default class LineRenderingConfig extends WithContextLoader {
|
||||
public readonly color: TagRenderingConfig
|
||||
public readonly width: TagRenderingConfig
|
||||
public readonly dashArray: TagRenderingConfig
|
||||
public readonly dashArray: string
|
||||
public readonly lineCap: TagRenderingConfig
|
||||
public readonly offset: TagRenderingConfig
|
||||
public readonly fill: TagRenderingConfig
|
||||
|
@ -19,7 +19,7 @@ export default class LineRenderingConfig extends WithContextLoader {
|
|||
super(json, context)
|
||||
this.color = this.tr("color", "#0000ff")
|
||||
this.width = this.tr("width", "7")
|
||||
this.dashArray = this.tr("dashArray", "")
|
||||
this.dashArray = json.dashArray
|
||||
this.lineCap = this.tr("lineCap", "round")
|
||||
this.fill = this.tr("fill", undefined)
|
||||
this.fillColor = this.tr("fillColor", undefined)
|
||||
|
|
|
@ -628,6 +628,22 @@ export default class TagRenderingConfig {
|
|||
* config.constructChangeSpecification("", undefined, undefined, {}) // => undefined
|
||||
* config.constructChangeSpecification("5", undefined, undefined, {}).optimize() // => new Tag("capacity", "5")
|
||||
*
|
||||
* // Should pick a mapping, even if freeform is used
|
||||
* const config = new TagRenderingConfig({"id": "shop-types", render: "Shop type is {shop}", freeform: {key: "shop", addExtraTags:["fixme=freeform shop type used"]}, mappings:[{if: "shop=second_hand", then: "Second hand shop"}]})
|
||||
* config.constructChangeSpecification("freeform", 1, undefined, {}).asHumanString(false, false, {}) // => "shop=freeform & fixme=freeform shop type used"
|
||||
* config.constructChangeSpecification("freeform", undefined, undefined, {}).asHumanString(false, false, {}) // => "shop=freeform & fixme=freeform shop type used"
|
||||
* config.constructChangeSpecification("second_hand", 1, undefined, {}).asHumanString(false, false, {}) // => "shop=second_hand"
|
||||
*
|
||||
*
|
||||
* const config = new TagRenderingConfig({id: "oh", render: "{opening_hours}", question: {"en":"When open?"}, freeform: {key: "opening_hours"},
|
||||
* mappings: [{ "if": "opening_hours=closed",
|
||||
* "then": {
|
||||
* "en": "Marked as closed for an unspecified time",
|
||||
* },
|
||||
* "hideInAnswer": true}] }
|
||||
* const tags = config.constructChangeSpecification("Tu-Fr 05:30-09:30", undefined, undefined, { }}
|
||||
* tags // =>new And([ new Tag("opening_hours", "Tu-Fr 05:30-09:30")])
|
||||
*
|
||||
* @param freeformValue The freeform value which will be applied as 'freeform.key'. Ignored if 'freeform.key' is not set
|
||||
*
|
||||
* @param singleSelectedMapping (Only used if multiAnswer == false): the single mapping to apply. Use (mappings.length) for the freeform
|
||||
|
@ -640,6 +656,12 @@ export default class TagRenderingConfig {
|
|||
multiSelectedMapping: boolean[] | undefined,
|
||||
currentProperties: Record<string, string>
|
||||
): UploadableTag {
|
||||
console.log("constructChangeSpecification:", {
|
||||
freeformValue,
|
||||
singleSelectedMapping,
|
||||
multiSelectedMapping,
|
||||
currentProperties,
|
||||
})
|
||||
if (typeof freeformValue === "string") {
|
||||
freeformValue = freeformValue?.trim()
|
||||
}
|
||||
|
@ -667,11 +689,22 @@ export default class TagRenderingConfig {
|
|||
this.mappings.length == 0 ||
|
||||
(singleSelectedMapping === this.mappings.length && !this.multiAnswer))
|
||||
) {
|
||||
const freeformOnly = { [this.freeform.key]: freeformValue }
|
||||
const matchingMapping = this.mappings?.find((m) => m.if.matchesProperties(freeformOnly))
|
||||
if (matchingMapping) {
|
||||
return new And([matchingMapping.if, ...(matchingMapping.addExtraTags ?? [])])
|
||||
}
|
||||
// Either no mappings, or this is a radio-button selected freeform value
|
||||
return new And([
|
||||
const tag = new And([
|
||||
new Tag(this.freeform.key, freeformValue),
|
||||
...(this.freeform.addExtraTags ?? []),
|
||||
])
|
||||
const newProperties = tag.applyOn(currentProperties)
|
||||
if (this.invalidValues?.matchesProperties(newProperties)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return tag
|
||||
}
|
||||
|
||||
if (this.multiAnswer) {
|
||||
|
|
|
@ -450,6 +450,19 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.selectedElement.setData(feature)
|
||||
}
|
||||
|
||||
public showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer {
|
||||
const id = "gps_location"
|
||||
const flayerGps = this.layerState.filteredLayers.get(id)
|
||||
const features = this.geolocation.currentUserLocation
|
||||
return new ShowDataLayer(map, {
|
||||
features,
|
||||
doShowLayer: flayerGps.isDisplayed,
|
||||
layer: flayerGps.layerDef,
|
||||
metaTags: this.userRelatedState.preferencesAsTags,
|
||||
selectedElement: this.selectedElement,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Various small methods that need to be called
|
||||
*/
|
||||
|
@ -674,7 +687,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
const summaryTileSource = new SummaryTileSource(
|
||||
url.protocol + "//" + url.host + "/summary",
|
||||
layers.map((l) => l.id),
|
||||
this.mapProperties.zoom.map((z) => Math.max(Math.ceil(z), 0)),
|
||||
this.mapProperties.zoom.map((z) => Math.max(Math.floor(z), 0)),
|
||||
this.mapProperties,
|
||||
{
|
||||
isActive: this.mapProperties.zoom.map((z) => z <= maxzoom),
|
||||
|
@ -682,6 +695,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
)
|
||||
return new SummaryTileSourceRewriter(summaryTileSource, this.layerState.filteredLayers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the special layers to the map
|
||||
*/
|
||||
|
@ -796,7 +810,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
new MetaTagging(this)
|
||||
new TitleHandler(this.selectedElement, this.featureProperties, this)
|
||||
new ChangeToElementsActor(this.changes, this.featureProperties)
|
||||
new PendingChangesUploader(this.changes, this.selectedElement)
|
||||
new PendingChangesUploader(this.changes, this.selectedElement, this.imageUploadManager)
|
||||
new SelectedElementTagsUpdater(this)
|
||||
new BackgroundLayerResetter(this.mapProperties.rasterLayer, this.availableLayers)
|
||||
new PreferredRasterLayerSelector(
|
||||
|
|
|
@ -141,7 +141,7 @@
|
|||
>
|
||||
<Center class=" h-6 w-6" />
|
||||
</div>
|
||||
{:else}
|
||||
{:else if !!$label}
|
||||
<div
|
||||
class={twMerge("soft relative rounded-full border border-black", size)}
|
||||
on:click={() => focusMap()}
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
import { twMerge } from "tailwind-merge"
|
||||
import Loading from "../../assets/svg/Loading.svelte"
|
||||
|
||||
export let cls: string = undefined
|
||||
export let cls: string = "flex p-1 pl-2"
|
||||
</script>
|
||||
|
||||
<div class={twMerge("flex p-1 pl-2", cls)}>
|
||||
<div class={cls}>
|
||||
<div class="min-w-6 h-6 w-6 shrink-0 animate-spin self-center">
|
||||
<Loading />
|
||||
</div>
|
||||
|
|
|
@ -3,12 +3,14 @@
|
|||
import { twJoin } from "tailwind-merge"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||
|
||||
/**
|
||||
* A round button with an icon and possible a small text, which hovers above the map
|
||||
*/
|
||||
const dispatch = createEventDispatcher()
|
||||
export let cls = "m-0.5 p-0.5 sm:p-1 md:m-1"
|
||||
export let enabled : Store<boolean> = new ImmutableStore(true)
|
||||
export let arialabel: Translation = undefined
|
||||
</script>
|
||||
|
||||
|
@ -16,7 +18,7 @@
|
|||
on:click={(e) => dispatch("click", e)}
|
||||
on:keydown
|
||||
use:ariaLabel={arialabel}
|
||||
class={twJoin("pointer-events-auto relative h-fit w-fit rounded-full", cls)}
|
||||
class={twJoin("pointer-events-auto relative h-fit w-fit rounded-full", cls, $enabled ? "" : "disabled")}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
|
|
|
@ -1,31 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Img from "./Img"
|
||||
import { twJoin, twMerge } from "tailwind-merge"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export let imageUrl: string | BaseUIElement = undefined
|
||||
export const message: string | BaseUIElement = undefined
|
||||
export let options: {
|
||||
imgSize?: string
|
||||
extraClasses?: string
|
||||
} = {}
|
||||
|
||||
let imgClasses = twJoin("block justify-center shrink-0 mr-4", options?.imgSize ?? "h-11 w-11")
|
||||
const dispatch = createEventDispatcher<{ click }>()
|
||||
</script>
|
||||
|
||||
<button
|
||||
class={twMerge(options.extraClasses, "secondary no-image-background")}
|
||||
on:click={(e) => dispatch("click", e)}
|
||||
on:click
|
||||
>
|
||||
<slot name="image">
|
||||
{#if imageUrl !== undefined}
|
||||
{#if typeof imageUrl === "string"}
|
||||
<Img src={imageUrl} class={imgClasses} />
|
||||
{/if}
|
||||
{/if}
|
||||
</slot>
|
||||
|
||||
<slot name="image" />
|
||||
<slot name="message" />
|
||||
</button>
|
||||
|
|
|
@ -17,7 +17,8 @@ export default class Title extends BaseUIElement {
|
|||
constructor(embedded: string | BaseUIElement, level: number = 3) {
|
||||
super()
|
||||
if (embedded === undefined) {
|
||||
throw "A title should have some content. Undefined is not allowed"
|
||||
console.warn("A title should have some content. Undefined is not allowed")
|
||||
embedded = ""
|
||||
}
|
||||
if (typeof embedded === "string") {
|
||||
this.title = new FixedUiElement(embedded)
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
/>
|
||||
</If>
|
||||
|
||||
{filteredLayer.layerDef.name}
|
||||
<Tr t={filteredLayer.layerDef.name}/>
|
||||
|
||||
{#if $zoomlevel < layer.minzoom}
|
||||
<span class="alert">
|
||||
|
@ -82,7 +82,7 @@
|
|||
<!-- There are three (and a half) modes of filters: a single checkbox, a radio button/dropdown or with searchable fields -->
|
||||
{#if filter.options.length === 1 && filter.options[0].fields.length === 0}
|
||||
<Checkbox selected={getBooleanStateFor(filter)}>
|
||||
{filter.options[0].question}
|
||||
<Tr t={filter.options[0].question}/>
|
||||
</Checkbox>
|
||||
{/if}
|
||||
|
||||
|
@ -94,7 +94,7 @@
|
|||
<Dropdown value={getStateFor(filter)}>
|
||||
{#each filter.options as option, i}
|
||||
<option value={i}>
|
||||
{option.question}
|
||||
<Tr t={option.question}/>
|
||||
</option>
|
||||
{/each}
|
||||
</Dropdown>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { onDestroy } from "svelte"
|
||||
import { Utils } from "../../Utils"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
||||
export let filteredLayer: FilteredLayer
|
||||
export let option: FilterConfigOption
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import LocationInput from "../InputElement/Helpers/LocationInput.svelte"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Tiles } from "../../Models/TileRange"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import type { MapProperties } from "../../Models/MapProperties"
|
||||
|
@ -15,7 +14,6 @@
|
|||
import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { Utils } from "../../Utils"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Move_arrows from "../../assets/svg/Move_arrows.svelte"
|
||||
|
||||
/**
|
||||
|
@ -53,9 +51,6 @@
|
|||
lat: number
|
||||
}>(undefined)
|
||||
|
||||
const dispatch = createEventDispatcher<{ click: { lon: number; lat: number } }>()
|
||||
|
||||
const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16)
|
||||
const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
|
||||
let initialMapProperties: Partial<MapProperties> & { location } = {
|
||||
zoom: new UIEventSource<number>(19),
|
||||
|
@ -73,6 +68,7 @@
|
|||
minzoom: new UIEventSource<number>(18),
|
||||
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer),
|
||||
}
|
||||
state?.showCurrentLocationOn(map)
|
||||
|
||||
if (targetLayer) {
|
||||
const featuresForLayer = state.perLayer.get(targetLayer.id)
|
||||
|
@ -120,7 +116,7 @@
|
|||
|
||||
<LocationInput
|
||||
{map}
|
||||
on:click={(data) => dispatch("click", data)}
|
||||
on:click
|
||||
mapProperties={initialMapProperties}
|
||||
value={preciseLocation}
|
||||
initialCoordinate={coordinate}
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* A mapcontrol button which allows the user to select a different background.
|
||||
* Even though the component is very small, it gets it's own class as it is often reused
|
||||
* Even though the component is very small, it gets its own class as it is often reused
|
||||
*/
|
||||
import { Square3Stack3dIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import Translations from "../i18n/Translations"
|
||||
import MapControlButton from "../Base/MapControlButton.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import StyleLoadingIndicator from "../Map/StyleLoadingIndicator.svelte"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let state: ThemeViewState
|
||||
export let map: Store<MlMap> = undefined
|
||||
export let hideTooltip = false
|
||||
</script>
|
||||
|
||||
|
@ -17,7 +22,10 @@
|
|||
arialabel={Translations.t.general.labels.background}
|
||||
on:click={() => state.guistate.backgroundLayerSelectionIsOpened.setData(true)}
|
||||
>
|
||||
<Square3Stack3dIcon class="h-6 w-6" />
|
||||
|
||||
<StyleLoadingIndicator map={map ?? state.map} rasterLayer={state.mapProperties.rasterLayer} >
|
||||
<Square3Stack3dIcon class="h-6 w-6" />
|
||||
</StyleLoadingIndicator>
|
||||
{#if !hideTooltip}
|
||||
<Tr cls="mx-2" t={Translations.t.general.backgroundSwitch} />
|
||||
{/if}
|
||||
|
|
|
@ -13,20 +13,25 @@
|
|||
export let layer: LayerConfig
|
||||
export let selectedElement: Feature
|
||||
let tags: UIEventSource<Record<string, string>> = state.featureProperties.getStore(
|
||||
selectedElement.properties.id
|
||||
selectedElement.properties.id,
|
||||
)
|
||||
$: {
|
||||
tags = state.featureProperties.getStore(selectedElement.properties.id)
|
||||
}
|
||||
|
||||
let isTesting = state.featureSwitchIsTesting
|
||||
let isDebugging = state.featureSwitches.featureSwitchIsDebugging
|
||||
|
||||
let metatags: Store<Record<string, string>> = state.userRelatedState.preferencesAsTags
|
||||
</script>
|
||||
|
||||
{#if $tags._deleted === "yes"}
|
||||
<Tr t={Translations.t.delete.isDeleted} />
|
||||
{:else}
|
||||
<div class="low-interaction flex border-b-2 border-black px-3 drop-shadow-md">
|
||||
<div class="h-fit w-full overflow-auto sm:p-2" style="max-height: 20vh;">
|
||||
<div class="low-interaction flex border-b-2 border-black px-3 drop-shadow-md">
|
||||
<div class="h-fit w-full overflow-auto sm:p-2" style="max-height: 20vh;">
|
||||
{#if $tags._deleted === "yes"}
|
||||
<h3 class="p-4">
|
||||
<Tr t={Translations.t.delete.deletedTitle} />
|
||||
</h3>
|
||||
{:else}
|
||||
<div class="flex h-full w-full flex-grow flex-col">
|
||||
<!-- Title element and title icons-->
|
||||
<h3 class="m-0">
|
||||
|
@ -34,12 +39,11 @@
|
|||
<TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags} {layer} />
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<div
|
||||
class="no-weblate title-icons links-as-button mr-2 flex flex-row flex-wrap items-center gap-x-0.5 pt-0.5 sm:pt-1"
|
||||
>
|
||||
{#each layer.titleIcons as titleIconConfig}
|
||||
{#if (titleIconConfig.condition?.matchesProperties($tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ...$metatags, ...$tags } ) ?? true) && titleIconConfig.IsKnown($tags)}
|
||||
{#if (titleIconConfig.condition?.matchesProperties($tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties({ ...$metatags, ...$tags }) ?? true) && titleIconConfig.IsKnown($tags)}
|
||||
<div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}>
|
||||
<TagRenderingAnswer
|
||||
config={titleIconConfig}
|
||||
|
@ -52,23 +56,27 @@
|
|||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if $isTesting || $isDebugging}
|
||||
<a class="subtle" href="https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Layers/{layer.id}.md"
|
||||
target="_blank" rel="noreferrer noopener ">{layer.id}</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
on:click={() => state.selectedElement.setData(undefined)}
|
||||
use:ariaLabel={Translations.t.general.backToMap}
|
||||
class="mt-2 h-fit shrink-0 rounded-full border-none p-0"
|
||||
style="border: 0 !important; padding: 0 !important;"
|
||||
>
|
||||
<XCircleIcon aria-hidden={true} class="h-8 w-8" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
class="mt-2 h-fit shrink-0 rounded-full border-none p-0"
|
||||
on:click={() => state.selectedElement.setData(undefined)}
|
||||
style="border: 0 !important; padding: 0 !important;"
|
||||
use:ariaLabel={Translations.t.general.backToMap}
|
||||
>
|
||||
<XCircleIcon aria-hidden={true} class="h-8 w-8" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.title-icons a) {
|
||||
display: block !important;
|
||||
}
|
||||
:global(.title-icons a) {
|
||||
display: block !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,17 +8,21 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import UserRelatedState from "../../Logic/State/UserRelatedState"
|
||||
import Delete_icon from "../../assets/svg/Delete_icon.svelte"
|
||||
import BackButton from "../Base/BackButton.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let layer: LayerConfig
|
||||
export let selectedElement: Feature
|
||||
export let highlightedRendering: UIEventSource<string> = undefined
|
||||
|
||||
export let tags: UIEventSource<Record<string, string>> = state?.featureProperties?.getStore(
|
||||
selectedElement.properties.id
|
||||
)
|
||||
|
||||
|
||||
|
||||
let layer: LayerConfig = selectedElement.properties.id === "settings" ? UserRelatedState.usersettingsConfig : state.layout.getMatchingLayer(tags.data)
|
||||
|
||||
|
||||
let stillMatches = tags.map(tags => !layer?.source?.osmTags || layer.source.osmTags?.matchesProperties(tags))
|
||||
|
||||
let _metatags: Record<string, string>
|
||||
|
@ -27,7 +31,7 @@
|
|||
onDestroy(
|
||||
state.userRelatedState.preferencesAsTags.addCallbackAndRun((tags) => {
|
||||
_metatags = tags
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -36,22 +40,26 @@
|
|||
(config) =>
|
||||
(config.condition?.matchesProperties(tgs) ?? true) &&
|
||||
(config.metacondition?.matchesProperties({ ...tgs, ..._metatags }) ?? true) &&
|
||||
config.IsKnown(tgs)
|
||||
)
|
||||
config.IsKnown(tgs),
|
||||
),
|
||||
)
|
||||
</script>
|
||||
|
||||
{#if !$stillMatches}
|
||||
<div class="alert" aria-live="assertive">
|
||||
<Tr t={Translations.t.delete.isChanged}/>
|
||||
<div class="alert" aria-live="assertive">
|
||||
<Tr t={Translations.t.delete.isChanged} />
|
||||
</div>
|
||||
{:else if $tags._deleted === "yes"}
|
||||
<div aria-live="assertive">
|
||||
<Tr t={Translations.t.delete.isDeleted} />
|
||||
<div class="flex w-full flex-col p-2">
|
||||
<div aria-live="assertive" class="alert flex items-center justify-center self-stretch">
|
||||
<Delete_icon class="w-8 h-8 m-2" />
|
||||
<Tr t={Translations.t.delete.isDeleted} />
|
||||
</div>
|
||||
<BackButton clss="self-stretch mt-4" on:click={() => state.selectedElement.setData(undefined)}>
|
||||
<Tr t={Translations.t.general.returnToTheMap} />
|
||||
</BackButton>
|
||||
|
||||
</div>
|
||||
<button class="w-full" on:click={() => state.selectedElement.setData(undefined)}>
|
||||
<Tr t={Translations.t.general.returnToTheMap} />
|
||||
</button>
|
||||
{:else}
|
||||
<div
|
||||
class="selected-element-view flex h-full w-full flex-col gap-y-2 overflow-y-auto p-1 px-4"
|
||||
|
|
|
@ -11,10 +11,11 @@
|
|||
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 Share from "../../assets/svg/Share.svelte"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Img from "../Base/Img"
|
||||
import Qr from "../../Utils/Qr"
|
||||
|
||||
export let state: ThemeViewState
|
||||
const tr = Translations.t.general.sharescreen
|
||||
|
@ -69,22 +70,32 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Tr t={tr.intro} />
|
||||
<div class="flex">
|
||||
{#if typeof navigator?.share === "function"}
|
||||
<button class="h-8 w-8 shrink-0 p-1" on:click={shareCurrentLink}>
|
||||
<Share />
|
||||
</button>
|
||||
{/if}
|
||||
{#if navigator.clipboard !== undefined}
|
||||
<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)}>
|
||||
{linkToShare}
|
||||
<div class="flex flex-col">
|
||||
<div class="flex justify-between items-start">
|
||||
|
||||
<div class="flex flex-col">
|
||||
|
||||
<Tr t={tr.intro} />
|
||||
<div class="flex">
|
||||
{#if typeof navigator?.share === "function"}
|
||||
<button class="h-8 w-8 shrink-0 p-1" on:click={shareCurrentLink}>
|
||||
<Share />
|
||||
</button>
|
||||
{/if}
|
||||
{#if navigator.clipboard !== undefined}
|
||||
<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)}>
|
||||
{linkToShare}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ToSvelte construct={() => new Img(new Qr(linkToShare).toImageElement(125)).SetStyle(
|
||||
"width: 125px"
|
||||
)} />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
|
@ -95,29 +106,31 @@
|
|||
|
||||
<Tr t={tr.embedIntro} />
|
||||
|
||||
<div class="link-underline my-1 flex flex-col">
|
||||
<label>
|
||||
<input bind:checked={showWelcomeMessage} type="checkbox" />
|
||||
<Tr t={tr.fsWelcomeMessage} />
|
||||
</label>
|
||||
<div class="flex flex-col interactive p-1">
|
||||
|
||||
<label>
|
||||
<input bind:checked={enableLogin} type="checkbox" />
|
||||
<Tr t={tr.fsUserbadge} />
|
||||
</label>
|
||||
</div>
|
||||
<div class="literal-code m-1">
|
||||
<span class="literal-code iframe-code-block"> <br />
|
||||
<iframe src="{linkToShare}"
|
||||
<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>
|
||||
<div class="link-underline my-1 flex flex-col">
|
||||
<label>
|
||||
<input bind:checked={showWelcomeMessage} type="checkbox" id="share_show_welcome" />
|
||||
<Tr t={tr.fsWelcomeMessage} />
|
||||
</label>
|
||||
|
||||
<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 />
|
||||
</span>
|
||||
<label>
|
||||
<input bind:checked={enableLogin} type="checkbox" id="share_enable_login"/>
|
||||
<Tr t={tr.fsUserbadge} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<Tr t={tr.documentation} cls="link-underline" />
|
||||
<Tr cls="link-underline" t={tr.documentation} />
|
||||
</div>
|
||||
|
|
|
@ -17,14 +17,17 @@ export default class StatisticsForLayerPanel extends VariableUiElement {
|
|||
return new Loading("Loading data")
|
||||
}
|
||||
if (features.length === 0) {
|
||||
return "No elements in view"
|
||||
return new Combine([
|
||||
"No elements in view for layer ",
|
||||
layer.id
|
||||
]).SetClass("block")
|
||||
}
|
||||
const els: BaseUIElement[] = []
|
||||
const featuresForLayer = features
|
||||
if (featuresForLayer.length === 0) {
|
||||
return
|
||||
}
|
||||
els.push(new Title(layer.name.Clone(), 1).SetClass("mt-8"))
|
||||
els.push(new Title(layer.name, 1).SetClass("mt-8"))
|
||||
|
||||
const layerStats = []
|
||||
for (const tagRendering of layer?.tagRenderings ?? []) {
|
||||
|
|
|
@ -8,7 +8,9 @@ import { OsmFeature } from "../../Models/OsmFeature"
|
|||
|
||||
export interface TagRenderingChartOptions {
|
||||
groupToOtherCutoff?: 3 | number
|
||||
sort?: boolean
|
||||
sort?: boolean,
|
||||
hideUnkown?: boolean,
|
||||
hideNotApplicable?: boolean
|
||||
}
|
||||
|
||||
export class StackedRenderingChart extends ChartJs {
|
||||
|
@ -19,12 +21,16 @@ export class StackedRenderingChart extends ChartJs {
|
|||
period: "day" | "month"
|
||||
groupToOtherCutoff?: 3 | number
|
||||
// If given, take the sum of these fields to get the feature weight
|
||||
sumFields?: string[]
|
||||
sumFields?: string[],
|
||||
hideUnknown?: boolean,
|
||||
hideNotApplicable?: boolean
|
||||
}
|
||||
) {
|
||||
const { labels, data } = TagRenderingChart.extractDataAndLabels(tr, features, {
|
||||
sort: true,
|
||||
groupToOtherCutoff: options?.groupToOtherCutoff,
|
||||
hideNotApplicable: options?.hideNotApplicable,
|
||||
hideUnkown: options?.hideUnknown
|
||||
})
|
||||
if (labels === undefined || data === undefined) {
|
||||
console.error(
|
||||
|
@ -36,7 +42,6 @@ export class StackedRenderingChart extends ChartJs {
|
|||
)
|
||||
throw "No labels or data given..."
|
||||
}
|
||||
// labels: ["cyclofix", "buurtnatuur", ...]; data : [ ["cyclofix-changeset", "cyclofix-changeset", ...], ["buurtnatuur-cs", "buurtnatuur-cs"], ... ]
|
||||
|
||||
for (let i = labels.length; i >= 0; i--) {
|
||||
if (data[i]?.length != 0) {
|
||||
|
@ -116,13 +121,13 @@ export class StackedRenderingChart extends ChartJs {
|
|||
datasets.push({
|
||||
data: countsPerDay,
|
||||
backgroundColor,
|
||||
label,
|
||||
label
|
||||
})
|
||||
}
|
||||
|
||||
const perDayData = {
|
||||
labels: trimmedDays,
|
||||
datasets,
|
||||
datasets
|
||||
}
|
||||
|
||||
const config = <ChartConfiguration>{
|
||||
|
@ -131,17 +136,17 @@ export class StackedRenderingChart extends ChartJs {
|
|||
options: {
|
||||
responsive: true,
|
||||
legend: {
|
||||
display: false,
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
stacked: true
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
stacked: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
super(config)
|
||||
}
|
||||
|
@ -194,7 +199,7 @@ export default class TagRenderingChart extends Combine {
|
|||
"rgba(255, 206, 86, 0.2)",
|
||||
"rgba(75, 192, 192, 0.2)",
|
||||
"rgba(153, 102, 255, 0.2)",
|
||||
"rgba(255, 159, 64, 0.2)",
|
||||
"rgba(255, 159, 64, 0.2)"
|
||||
]
|
||||
|
||||
public static readonly borderColors = [
|
||||
|
@ -203,7 +208,7 @@ export default class TagRenderingChart extends Combine {
|
|||
"rgba(255, 206, 86, 1)",
|
||||
"rgba(75, 192, 192, 1)",
|
||||
"rgba(153, 102, 255, 1)",
|
||||
"rgba(255, 159, 64, 1)",
|
||||
"rgba(255, 159, 64, 1)"
|
||||
]
|
||||
|
||||
/**
|
||||
|
@ -239,12 +244,12 @@ export default class TagRenderingChart extends Combine {
|
|||
const borderColor = [
|
||||
TagRenderingChart.unkownBorderColor,
|
||||
TagRenderingChart.otherBorderColor,
|
||||
TagRenderingChart.notApplicableBorderColor,
|
||||
TagRenderingChart.notApplicableBorderColor
|
||||
]
|
||||
const backgroundColor = [
|
||||
TagRenderingChart.unkownColor,
|
||||
TagRenderingChart.otherColor,
|
||||
TagRenderingChart.notApplicableColor,
|
||||
TagRenderingChart.notApplicableColor
|
||||
]
|
||||
|
||||
while (borderColor.length < data.length) {
|
||||
|
@ -276,17 +281,17 @@ export default class TagRenderingChart extends Combine {
|
|||
backgroundColor,
|
||||
borderColor,
|
||||
borderWidth: 1,
|
||||
label: undefined,
|
||||
},
|
||||
],
|
||||
label: undefined
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
legend: {
|
||||
display: !barchartMode,
|
||||
},
|
||||
},
|
||||
},
|
||||
display: !barchartMode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const chart = new ChartJs(config).SetClass(options?.chartclasses ?? "w-32 h-32")
|
||||
|
@ -297,7 +302,7 @@ export default class TagRenderingChart extends Combine {
|
|||
|
||||
super([
|
||||
options?.includeTitle ? tagRendering.question.Clone() ?? tagRendering.id : undefined,
|
||||
chart,
|
||||
chart
|
||||
])
|
||||
|
||||
this.SetClass("block")
|
||||
|
@ -386,20 +391,26 @@ export default class TagRenderingChart extends Combine {
|
|||
}
|
||||
}
|
||||
|
||||
const labels = [
|
||||
"Unknown",
|
||||
"Other",
|
||||
"Not applicable",
|
||||
const labels = []
|
||||
const data: T[][] = []
|
||||
|
||||
if (!options.hideUnkown) {
|
||||
data.push(unknownCount)
|
||||
labels.push("Unknown")
|
||||
}
|
||||
data.push(otherGrouped)
|
||||
labels.push("Other")
|
||||
if (!options.hideNotApplicable) {
|
||||
data.push(notApplicable)
|
||||
labels.push(
|
||||
"Not applicable"
|
||||
)
|
||||
}
|
||||
data.push(...categoryCounts,
|
||||
...otherData)
|
||||
labels.push(
|
||||
...(mappings?.map((m) => m.then.txt) ?? []),
|
||||
...otherLabels,
|
||||
]
|
||||
const data: T[][] = [
|
||||
unknownCount,
|
||||
otherGrouped,
|
||||
notApplicable,
|
||||
...categoryCounts,
|
||||
...otherData,
|
||||
]
|
||||
...otherLabels)
|
||||
|
||||
return { labels, data }
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
import Constants from "../../Models/Constants"
|
||||
import type { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import SubtleLink from "../Base/SubtleLink.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
|
||||
|
||||
|
@ -86,8 +85,10 @@
|
|||
</script>
|
||||
|
||||
{#if theme.id !== personal.id || $unlockedPersonal}
|
||||
<SubtleLink href={$href} options={{ extraClasses: "w-full" }}>
|
||||
<img slot="image" src={theme.icon} class="m-1 mr-2 block h-11 w-11 sm:m-2 sm:mr-4" alt="" />
|
||||
<a
|
||||
class={"w-full button text-ellipsis"}
|
||||
href={$href}
|
||||
> <img src={theme.icon} class="m-1 mr-2 block h-11 w-11 sm:m-2 sm:mr-4" alt="" />
|
||||
<span class="flex flex-col overflow-hidden text-ellipsis">
|
||||
<Tr t={title} />
|
||||
|
||||
|
@ -96,6 +97,5 @@
|
|||
<Tr t={Translations.t.general.morescreen.enterToOpen} />
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</SubtleLink>
|
||||
</span></a>
|
||||
{/if}
|
||||
|
|
|
@ -23,18 +23,20 @@
|
|||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import type { Feature, LineString, Point } from "geojson"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import SmallZoomButtons from "../Map/SmallZoomButtons.svelte"
|
||||
|
||||
const splitpoint_style = new LayerConfig(
|
||||
<LayerConfigJson>split_point,
|
||||
"(BUILTIN) SplitRoadWizard.ts",
|
||||
true
|
||||
) as const
|
||||
true,
|
||||
)
|
||||
|
||||
const splitroad_style = new LayerConfig(
|
||||
<LayerConfigJson>split_road,
|
||||
"(BUILTIN) SplitRoadWizard.ts",
|
||||
true
|
||||
) as const
|
||||
true,
|
||||
)
|
||||
|
||||
/**
|
||||
* The way to focus on
|
||||
|
@ -45,6 +47,7 @@
|
|||
* A default is given
|
||||
*/
|
||||
export let layer: LayerConfig = splitroad_style
|
||||
export let state: SpecialVisualizationState | undefined = undefined
|
||||
/**
|
||||
* Optional: use these properties to set e.g. background layer
|
||||
*/
|
||||
|
@ -58,6 +61,7 @@
|
|||
adaptor.bounds.setData(BBox.get(wayGeojson).pad(2))
|
||||
adaptor.maxbounds.setData(BBox.get(wayGeojson).pad(2))
|
||||
|
||||
state?.showCurrentLocationOn(map)
|
||||
new ShowDataLayer(map, {
|
||||
features: new StaticFeatureSource([wayGeojson]),
|
||||
drawMarkers: false,
|
||||
|
@ -101,6 +105,7 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<div class="h-full w-full">
|
||||
<MaplibreMap {map} />
|
||||
<div class="h-full w-full relative">
|
||||
<MaplibreMap {map} mapProperties={adaptor} />
|
||||
<SmallZoomButtons {adaptor} />
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
import { SvgToPdf } from "../../Utils/svgToPdf"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import DownloadPdf from "./DownloadPdf.svelte"
|
||||
import { PngMapCreator } from "../../Utils/pngMapCreator"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import ValidatedInput from "../InputElement/ValidatedInput.svelte"
|
||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
|
||||
|
||||
export let state: ThemeViewState
|
||||
let isLoading = state.dataIsLoading
|
||||
|
@ -34,9 +38,21 @@
|
|||
mapExtent: state.mapProperties.bounds.data,
|
||||
width: maindiv.offsetWidth,
|
||||
height: maindiv.offsetHeight,
|
||||
noSelfIntersectingLines,
|
||||
noSelfIntersectingLines
|
||||
})
|
||||
}
|
||||
|
||||
let customWidth = LocalStorageSource.Get("custom-png-width", "20")
|
||||
let customHeight = LocalStorageSource.Get("custom-png-height", "20")
|
||||
|
||||
async function offerCustomPng(): Promise<Blob> {
|
||||
console.log("Creating a custom size png with dimensions", customWidth.data + "mm *", customHeight.data + "mm")
|
||||
const creator = new PngMapCreator(state, {
|
||||
height: Number(customHeight.data), width: Number(customWidth.data)
|
||||
})
|
||||
return await creator.CreatePng("belowmap")
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#if $isLoading}
|
||||
|
@ -107,5 +123,26 @@
|
|||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="low-interaction p-2 mt-4">
|
||||
<h3 class="m-0 mb-2">
|
||||
<Tr t={t.custom.title}/></h3>
|
||||
<div class="flex">
|
||||
<Tr t={t.custom.width} />
|
||||
<ValidatedInput {state} type="pnat" value={customWidth} />
|
||||
</div>
|
||||
<div class="flex">
|
||||
<Tr t={t.custom.height} />
|
||||
<ValidatedInput {state} type="pnat" value={customHeight} />
|
||||
</div>
|
||||
<DownloadButton
|
||||
mainText={t.custom.download.Subs({width: $customWidth, height: $customHeight})}
|
||||
helperText={t.custom.downloadHelper}
|
||||
extension="png"
|
||||
construct={() => offerCustomPng()}
|
||||
{state}
|
||||
mimetype="image/png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tr cls="link-underline" t={t.licenseInfo} />
|
||||
{/if}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
const templateUrls = SvgToPdf.templates[templateName].pages
|
||||
const templates: string[] = await Promise.all(templateUrls.map((url) => Utils.download(url)))
|
||||
console.log("Templates are", templates)
|
||||
const bg = state.mapProperties.rasterLayer.data ?? AvailableRasterLayers.maptilerDefaultLayer
|
||||
const bg = state.mapProperties.rasterLayer.data ?? AvailableRasterLayers.defaultBackgroundLayer
|
||||
const creator = new SvgToPdf(title, templates, {
|
||||
state,
|
||||
freeComponentId: "belowmap",
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
*/
|
||||
export let image: ProvidedImage
|
||||
let license: Store<LicenseInfo> = UIEventSource.FromPromise(
|
||||
image.provider?.DownloadAttribution(image.url)
|
||||
image.provider?.DownloadAttribution(image)
|
||||
)
|
||||
let icon = image.provider?.SourceIcon(image.id)?.SetClass("block h-8 w-8 pr-2")
|
||||
</script>
|
||||
|
@ -38,26 +38,28 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-between">
|
||||
<div class="flex justify-between w-full gap-x-1">
|
||||
{#if $license.license !== undefined || $license.licenseShortName !== undefined}
|
||||
<div>
|
||||
{$license?.license ?? $license?.licenseShortName}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $license.date}
|
||||
<div>
|
||||
{$license.date.toLocaleDateString()}
|
||||
{#if $license.views}
|
||||
<div class="flex justify-around self-center">
|
||||
<EyeIcon class="h-4 w-4 pr-1" />
|
||||
{$license.views}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $license.views}
|
||||
<div class="flex justify-around self-center">
|
||||
<EyeIcon class="h-4 w-4 pr-1" />
|
||||
{$license.views}
|
||||
{#if $license.date}
|
||||
<div>
|
||||
{$license.date.toLocaleDateString()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
29
src/UI/Image/UploadFailedMessage.svelte
Normal file
29
src/UI/Image/UploadFailedMessage.svelte
Normal file
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
|
||||
|
||||
export let failed: number
|
||||
const t = Translations.t.image
|
||||
|
||||
</script>
|
||||
<div class="alert flex">
|
||||
|
||||
<div class="flex flex-col items-start">
|
||||
{#if failed === 1}
|
||||
<Tr t={t.upload.one.failed} />
|
||||
{:else}
|
||||
<Tr t={t.upload.multiple.someFailed.Subs({ count: failed })} />
|
||||
{/if}
|
||||
<Tr cls="text-normal" t={t.upload.failReasons} />
|
||||
<Tr cls="text-xs" t={t.upload.failReasonsAdvanced} />
|
||||
</div>
|
||||
<button
|
||||
class="mt-2 h-fit shrink-0 rounded-full border-none p-0 pointer-events-auto"
|
||||
on:click
|
||||
style="border: 0 !important; padding: 0 !important;"
|
||||
>
|
||||
<XCircleIcon aria-hidden={true} class="h-8 w-8" />
|
||||
</button>
|
||||
</div>
|
|
@ -11,6 +11,8 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import UploadFailedMessage from "./UploadFailedMessage.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: Store<OsmTags> = undefined
|
||||
|
@ -22,31 +24,40 @@
|
|||
const { uploadStarted, uploadFinished, retried, failed } =
|
||||
state.imageUploadManager.getCountsFor(featureId)
|
||||
const t = Translations.t.image
|
||||
const debugging = state.featureSwitches.featureSwitchIsDebugging
|
||||
let dismissed = 0
|
||||
</script>
|
||||
|
||||
{#if $uploadStarted === 1}
|
||||
{#if $debugging}
|
||||
<div class="low-interaction">Started {$uploadStarted} Done {$uploadFinished} Retry {$retried} Err {$failed}</div>
|
||||
{/if}
|
||||
{#if dismissed === $uploadStarted}
|
||||
<!-- We don't show anything as we ignore this number of failed items-->
|
||||
{:else if $uploadStarted === 1}
|
||||
{#if $uploadFinished === 1}
|
||||
{#if showThankYou}
|
||||
<Tr cls="thanks" t={t.upload.one.done} />
|
||||
{/if}
|
||||
{:else if $failed === 1}
|
||||
<div class="alert flex flex-col">
|
||||
<Tr cls="self-center" t={t.upload.one.failed} />
|
||||
<Tr t={t.upload.failReasons} />
|
||||
<Tr t={t.upload.failReasonsAdvanced} />
|
||||
</div>
|
||||
<UploadFailedMessage failed={$failed} on:click={() => dismissed = $failed}/>
|
||||
{:else if $retried === 1}
|
||||
<Loading cls="alert">
|
||||
<div class="alert">
|
||||
<Loading>
|
||||
<Tr t={t.upload.one.retrying} />
|
||||
</Loading>
|
||||
</div>
|
||||
{:else}
|
||||
<Loading cls="alert">
|
||||
<div class="alert">
|
||||
<Loading>
|
||||
<Tr t={t.upload.one.uploading} />
|
||||
</Loading>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if $uploadStarted > 1}
|
||||
{#if $uploadFinished + $failed === $uploadStarted && $uploadFinished > 0}
|
||||
{#if showThankYou}
|
||||
{#if $uploadFinished + $failed === $uploadStarted}
|
||||
{#if $uploadFinished === 0}
|
||||
<!-- pass -->
|
||||
{:else if showThankYou}
|
||||
<Tr cls="thanks" t={t.upload.multiple.done.Subs({ count: $uploadFinished })} />
|
||||
{/if}
|
||||
{:else if $uploadFinished === 0}
|
||||
|
@ -64,14 +75,7 @@
|
|||
</Loading>
|
||||
{/if}
|
||||
{#if $failed > 0}
|
||||
<div class="alert flex flex-col">
|
||||
{#if $failed === 1}
|
||||
<Tr cls="self-center" t={t.upload.one.failed} />
|
||||
{:else}
|
||||
<Tr cls="self-center" t={t.upload.multiple.someFailed.Subs({ count: $failed })} />
|
||||
{/if}
|
||||
<Tr t={t.upload.failReasons} />
|
||||
<Tr t={t.upload.failReasonsAdvanced} />
|
||||
</div>
|
||||
|
||||
<UploadFailedMessage failed={$failed} on:click={() => dismissed = $failed}/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
on:touchstart={(e) => onPosChange(e.touches[0].clientX, e.touches[0].clientY)}
|
||||
>
|
||||
<div class="absolute top-0 left-0 h-full w-full cursor-pointer">
|
||||
<MaplibreMap attribution={false} {map} />
|
||||
<MaplibreMap mapProperties={mla} {map} />
|
||||
</div>
|
||||
|
||||
<div bind:this={directionElem} class="absolute top-0 left-0 h-full w-full">
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { createEventDispatcher, onDestroy } from "svelte"
|
||||
import Move_arrows from "../../../assets/svg/Move_arrows.svelte"
|
||||
import SmallZoomButtons from "../../Map/SmallZoomButtons.svelte"
|
||||
|
||||
/**
|
||||
* A visualisation to pick a location on a map background
|
||||
|
@ -83,7 +84,7 @@
|
|||
|
||||
<div class="min-h-32 relative h-full cursor-pointer overflow-hidden">
|
||||
<div class="absolute top-0 left-0 h-full w-full cursor-pointer">
|
||||
<MaplibreMap center={{ lng: initialCoordinate.lon, lat: initialCoordinate.lat }} {map} />
|
||||
<MaplibreMap center={{ lng: initialCoordinate.lon, lat: initialCoordinate.lat }} {map} mapProperties={mla}/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
@ -95,4 +96,5 @@
|
|||
</div>
|
||||
|
||||
<DragInvitation hideSignal={mla.location} />
|
||||
<SmallZoomButtons adaptor={mla} />
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import LanguageUtils from "../../../Utils/LanguageUtils"
|
||||
import { createEventDispatcher, onDestroy } from "svelte"
|
||||
import ValidatedInput from "../ValidatedInput.svelte"
|
||||
import { del } from "idb-keyval"
|
||||
|
||||
export let value: UIEventSource<Record<string, string>> = new UIEventSource<
|
||||
Record<string, string>
|
||||
|
@ -18,14 +19,25 @@
|
|||
const allLanguages: string[] = LanguageUtils.usedLanguagesSorted
|
||||
let currentLang = new UIEventSource("en")
|
||||
const currentVal = new UIEventSource<string>("")
|
||||
/**
|
||||
* Mostly the same as currentVal, but might be the empty string as well
|
||||
*/
|
||||
const currentValRaw = new UIEventSource<string>("")
|
||||
|
||||
let dispatch = createEventDispatcher<{ submit }>()
|
||||
|
||||
function update() {
|
||||
const v = currentVal.data
|
||||
let v = currentValRaw.data
|
||||
const l = currentLang.data
|
||||
console.log("Updating translation input for value", v, " and language", l)
|
||||
if (<any>translations.data === "" || translations.data === undefined) {
|
||||
translations.data = {}
|
||||
}
|
||||
if (v === "") {
|
||||
delete translations.data[l]
|
||||
translations.ping()
|
||||
return
|
||||
}
|
||||
if (translations.data[l] === v) {
|
||||
return
|
||||
}
|
||||
|
@ -39,35 +51,52 @@
|
|||
translations.data = {}
|
||||
}
|
||||
translations.data[currentLang] = translations.data[currentLang] ?? ""
|
||||
currentVal.setData(translations.data[currentLang])
|
||||
if (translations.data[currentLang] === "") {
|
||||
delete translations.data[currentLang]
|
||||
}
|
||||
currentVal.setData(translations.data[currentLang] ?? "")
|
||||
currentValRaw.setData(translations.data[currentLang])
|
||||
})
|
||||
)
|
||||
|
||||
onDestroy(
|
||||
currentVal.addCallbackAndRunD(() => {
|
||||
currentValRaw.addCallbackAndRunD(() => {
|
||||
update()
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="interactive m-1 mt-2 flex space-x-1 font-bold">
|
||||
</script>
|
||||
<div class="flex flex-col gap-y-1">
|
||||
<div class="interactive m-1 mt-2 flex space-x-1 font-bold">
|
||||
<span>
|
||||
{prefix}
|
||||
</span>
|
||||
<select bind:value={$currentLang}>
|
||||
{#each allLanguages as language}
|
||||
<option value={language}>
|
||||
{language}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<ValidatedInput
|
||||
type="string"
|
||||
cls="w-full"
|
||||
value={currentVal}
|
||||
on:submit={() => dispatch("submit")}
|
||||
/>
|
||||
<span>
|
||||
<select bind:value={$currentLang}>
|
||||
{#each allLanguages as language}
|
||||
<option value={language}>
|
||||
{language}
|
||||
{#if $translations[language] !== undefined}
|
||||
*
|
||||
{/if}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<ValidatedInput
|
||||
type="string"
|
||||
cls="w-full"
|
||||
value={currentVal}
|
||||
unvalidatedText={currentValRaw}
|
||||
on:submit={() => dispatch("submit")}
|
||||
/>
|
||||
<span>
|
||||
{postfix}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
You have currently set translations for
|
||||
<ul>
|
||||
{#each Object.keys($translations) as l}
|
||||
<li><button class="small" on:click={() => currentLang.setData(l)}><b>{l}:</b> {$translations[l]}</button></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -60,6 +60,11 @@ export default class InputHelpers {
|
|||
if (!mapProperties.zoom) {
|
||||
mapProperties = { ...mapProperties, zoom: new UIEventSource<number>(zoom) }
|
||||
}
|
||||
if (!mapProperties.rasterLayer) {
|
||||
/* mapProperties = {
|
||||
...mapProperties, rasterLayer: properties?.mapProperties?.rasterLayer
|
||||
}*/
|
||||
}
|
||||
return mapProperties
|
||||
}
|
||||
|
||||
|
@ -69,11 +74,10 @@ export default class InputHelpers {
|
|||
) {
|
||||
const inputHelperOptions = props
|
||||
const args = inputHelperOptions.args ?? []
|
||||
const searchKey = <string>args[0] ?? "name"
|
||||
const searchKey: string = <string>args[0] ?? "name"
|
||||
|
||||
const searchFor = <string>(
|
||||
(inputHelperOptions.feature?.properties[searchKey]?.toLowerCase() ?? "")
|
||||
)
|
||||
const searchFor: string = searchKey.split(";").map(k => inputHelperOptions.feature?.properties[k]?.toLowerCase())
|
||||
.find(foundValue => !!foundValue) ?? ""
|
||||
|
||||
let searchForValue: UIEventSource<string> = new UIEventSource(searchFor)
|
||||
const options: any = args[1]
|
||||
|
@ -121,7 +125,7 @@ export default class InputHelpers {
|
|||
value,
|
||||
searchText: searchForValue,
|
||||
instanceOf,
|
||||
notInstanceOf,
|
||||
notInstanceOf
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,11 +40,14 @@
|
|||
{#if availableLanguages?.length > 1}
|
||||
<form class={twMerge("flex max-w-full items-center pr-4", clss)}>
|
||||
<label
|
||||
for="pick-language"
|
||||
class="neutral-label flex max-w-full"
|
||||
use:ariaLabel={Translations.t.general.pickLanguage}
|
||||
>
|
||||
<LanguageIcon class="mr-1 h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
<Dropdown cls="max-w-full" value={assignTo}>
|
||||
</label>
|
||||
|
||||
<Dropdown cls="max-w-full" value={assignTo} id="pick-language">
|
||||
{#if preferredFiltered}
|
||||
{#each preferredFiltered as language}
|
||||
<option value={language} class="font-bold">
|
||||
|
@ -70,6 +73,5 @@
|
|||
</option>
|
||||
{/each}
|
||||
</Dropdown>
|
||||
</label>
|
||||
</form>
|
||||
{/if}
|
||||
|
|
|
@ -57,8 +57,8 @@ export abstract class Validator {
|
|||
*
|
||||
* Returns 'undefined' if the element is valid
|
||||
*/
|
||||
public getFeedback(s: string, _?: () => string): Translation | undefined {
|
||||
if (this.isValid(s)) {
|
||||
public getFeedback(s: string, getCountry?: () => string): Translation | undefined {
|
||||
if (this.isValid(s, getCountry)) {
|
||||
return undefined
|
||||
}
|
||||
const tr = Translations.t.validation[this.name]
|
||||
|
@ -71,7 +71,7 @@ export abstract class Validator {
|
|||
return Translations.t.validation[this.name].description
|
||||
}
|
||||
|
||||
public isValid(_: string): boolean {
|
||||
public isValid(_: string, getCountry?: () => string): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,4 @@ export default class OpeningHoursValidator extends Validator {
|
|||
)
|
||||
}
|
||||
|
||||
reformat(s: string, _?: () => string): string {
|
||||
return super.reformat(s, _)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Map as MLMap } from "maplibre-gl"
|
||||
import { Map as MLMap } from "maplibre-gl"
|
||||
import { Map as MlMap, SourceSpecification } from "maplibre-gl"
|
||||
import maplibregl from "maplibre-gl"
|
||||
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { Utils } from "../../Utils"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
|
@ -11,6 +12,8 @@ import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
|||
import * as htmltoimage from "html-to-image"
|
||||
import RasterLayerHandler from "./RasterLayerHandler"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { Protocol } from "pmtiles"
|
||||
import { bool } from "sharp"
|
||||
|
||||
/**
|
||||
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
||||
|
@ -23,13 +26,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
"dragRotate",
|
||||
"dragPan",
|
||||
"keyboard",
|
||||
"touchZoomRotate",
|
||||
"touchZoomRotate"
|
||||
]
|
||||
private static maplibre_zoom_handlers = [
|
||||
"scrollZoom",
|
||||
"boxZoom",
|
||||
"doubleClickZoom",
|
||||
"touchZoomRotate",
|
||||
"touchZoomRotate"
|
||||
]
|
||||
readonly location: UIEventSource<{ lon: number; lat: number }>
|
||||
readonly zoom: UIEventSource<number>
|
||||
|
@ -46,6 +49,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
readonly pitch: UIEventSource<number>
|
||||
readonly useTerrain: Store<boolean>
|
||||
|
||||
private static pmtilesInited = false
|
||||
/**
|
||||
* Functions that are called when one of those actions has happened
|
||||
* @private
|
||||
|
@ -55,6 +59,12 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
private readonly _maplibreMap: Store<MLMap>
|
||||
|
||||
constructor(maplibreMap: Store<MLMap>, state?: Partial<MapProperties>) {
|
||||
if (!MapLibreAdaptor.pmtilesInited) {
|
||||
maplibregl.addProtocol("pmtiles", new Protocol().tile)
|
||||
MapLibreAdaptor.pmtilesInited = true
|
||||
console.log("PM-tiles protocol added" +
|
||||
"")
|
||||
}
|
||||
this._maplibreMap = maplibreMap
|
||||
|
||||
this.location = state?.location ?? new UIEventSource(undefined)
|
||||
|
@ -103,6 +113,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
}
|
||||
|
||||
maplibreMap.addCallbackAndRunD((map) => {
|
||||
|
||||
map.on("load", () => {
|
||||
self.MoveMapToCurrentLoc(self.location.data)
|
||||
self.SetZoom(self.zoom.data)
|
||||
|
@ -205,14 +216,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
return {
|
||||
map: mlmap,
|
||||
ui: new SvelteUIElement(MaplibreMap, {
|
||||
map: mlmap,
|
||||
map: mlmap
|
||||
}),
|
||||
mapproperties: new MapLibreAdaptor(mlmap),
|
||||
mapproperties: new MapLibreAdaptor(mlmap)
|
||||
}
|
||||
}
|
||||
|
||||
public static prepareWmsSource(layer: RasterLayerProperties): SourceSpecification {
|
||||
return RasterLayerHandler.prepareWmsSource(layer)
|
||||
return RasterLayerHandler.prepareSource(layer)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -275,7 +286,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
) {
|
||||
const event = {
|
||||
date: new Date(),
|
||||
key: key,
|
||||
key: key
|
||||
}
|
||||
|
||||
for (let i = 0; i < this._onKeyNavigation.length; i++) {
|
||||
|
@ -319,22 +330,51 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
rescaleIcons: number,
|
||||
pixelRatio: number
|
||||
) {
|
||||
|
||||
{
|
||||
const allimages = element.getElementsByTagName("img")
|
||||
for (const img of Array.from(allimages)) {
|
||||
let isLoaded: boolean = false
|
||||
while (!isLoaded) {
|
||||
console.log("Waiting for image", img.src, "to load", img.complete, img.naturalWidth, img)
|
||||
await Utils.waitFor(250)
|
||||
isLoaded = img.complete && img.width > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const style = element.style.transform
|
||||
let x = element.getBoundingClientRect().x
|
||||
let y = element.getBoundingClientRect().y
|
||||
element.style.transform = ""
|
||||
const offset = style.match(/translate\(([-0-9]+)%, ?([-0-9]+)%\)/)
|
||||
|
||||
let labels =<HTMLElement[]> Array.from(element.getElementsByClassName("marker-label"))
|
||||
const origLabelTransforms = labels.map(l => l.style.transform)
|
||||
// We save the original width (`w`) and height (`h`) in order to restore them later on
|
||||
const w = element.style.width
|
||||
const h = element.style.height
|
||||
const h = Number(element.style.height)
|
||||
const targetW = Math.max(element.getBoundingClientRect().width * 4,
|
||||
...labels.map(l => l.getBoundingClientRect().width))
|
||||
const targetH = element.getBoundingClientRect().height +
|
||||
Math.max(...labels.map(l => l.getBoundingClientRect().height * 2 /* A bit of buffer to catch eventual 'margin-top'*/))
|
||||
|
||||
// Force a wider view for icon badges
|
||||
element.style.width = element.getBoundingClientRect().width * 4 + "px"
|
||||
element.style.height = element.getBoundingClientRect().height + "px"
|
||||
element.style.width = targetW + "px"
|
||||
// Force more height to include labels
|
||||
element.style.height = targetH + "px"
|
||||
element.classList.add("w-full", "flex", "flex-col", "items-center")
|
||||
labels.forEach(l => {
|
||||
l.style.transform = ""
|
||||
})
|
||||
await Utils.awaitAnimationFrame()
|
||||
const svgSource = await htmltoimage.toSvg(element)
|
||||
const img = await MapLibreAdaptor.createImage(svgSource)
|
||||
element.style.width = w
|
||||
element.style.height = h
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
labels[i].style.transform = origLabelTransforms[i]
|
||||
}
|
||||
element.style.width = "" + w
|
||||
element.style.height = "" + h
|
||||
|
||||
if (offset && rescaleIcons !== 1) {
|
||||
const [_, __, relYStr] = offset
|
||||
|
@ -346,10 +386,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
y *= pixelRatio
|
||||
|
||||
try {
|
||||
drawOn.drawImage(img, x, y, img.width * rescaleIcons, img.height * rescaleIcons)
|
||||
const xdiff = img.width * rescaleIcons / 2
|
||||
drawOn.drawImage(img, x - xdiff, y, img.width * rescaleIcons, img.height * rescaleIcons)
|
||||
} catch (e) {
|
||||
console.log("Could not draw image because of", e)
|
||||
}
|
||||
element.classList.remove("w-full", "flex", "flex-col", "items-center")
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -384,19 +427,12 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
const markers = Array.from(container.getElementsByClassName("marker"))
|
||||
for (let i = 0; i < markers.length; i++) {
|
||||
const marker = <HTMLElement>markers[i]
|
||||
const labels = Array.from(marker.getElementsByClassName("marker-label"))
|
||||
const style = marker.style.transform
|
||||
|
||||
if (isDisplayed(marker)) {
|
||||
await this.drawElement(drawOn, marker, rescaleIcons, pixelRatio)
|
||||
}
|
||||
|
||||
for (const label of labels) {
|
||||
if (isDisplayed(label)) {
|
||||
await this.drawElement(drawOn, <HTMLElement>label, rescaleIcons, pixelRatio)
|
||||
}
|
||||
}
|
||||
|
||||
if (progress) {
|
||||
progress.setData({ current: i, total: markers.length })
|
||||
}
|
||||
|
@ -425,7 +461,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
const bounds = map.getBounds()
|
||||
const bbox = new BBox([
|
||||
[bounds.getEast(), bounds.getNorth()],
|
||||
[bounds.getWest(), bounds.getSouth()],
|
||||
[bounds.getWest(), bounds.getSouth()]
|
||||
])
|
||||
if (this.bounds.data === undefined || !isSetup) {
|
||||
this.bounds.setData(bbox)
|
||||
|
@ -603,14 +639,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
type: "raster-dem",
|
||||
url:
|
||||
"https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=" +
|
||||
Constants.maptilerApiKey,
|
||||
Constants.maptilerApiKey
|
||||
})
|
||||
try {
|
||||
while (!map?.isStyleLoaded()) {
|
||||
await Utils.waitFor(250)
|
||||
}
|
||||
map.setTerrain({
|
||||
source: id,
|
||||
source: id
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte"
|
||||
import type { Map } from "maplibre-gl"
|
||||
import type { Map, MapOptions } from "maplibre-gl"
|
||||
import * as maplibre from "maplibre-gl"
|
||||
import type { Readable, Writable } from "svelte/store"
|
||||
import { get, writable } from "svelte/store"
|
||||
import type { Writable } from "svelte/store"
|
||||
import { AvailableRasterLayers } from "../../Models/RasterLayers"
|
||||
import { Utils } from "../../Utils"
|
||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||
import Translations from "../i18n/Translations"
|
||||
import type { MapProperties } from "../../Models/MapProperties"
|
||||
import type { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
||||
|
||||
/**
|
||||
* The 'MaplibreMap' maps various event sources onto MapLibre.
|
||||
|
@ -17,40 +18,43 @@
|
|||
* Beware: this map will _only_ be set by this component
|
||||
* It should thus be treated as a 'store' by external parties
|
||||
*/
|
||||
export let map: Writable<Map>
|
||||
export let map: Writable<Map> = undefined
|
||||
export let mapProperties: MapProperties = undefined
|
||||
|
||||
export let interactive: boolean = true
|
||||
|
||||
let container: HTMLElement
|
||||
|
||||
export let center: { lng: number; lat: number } | Readable<{ lng: number; lat: number }> =
|
||||
writable({ lng: 0, lat: 0 })
|
||||
export let zoom: Readable<number> = writable(1)
|
||||
|
||||
const styleUrl = AvailableRasterLayers.maptilerDefaultLayer.properties.url
|
||||
|
||||
let _map: Map
|
||||
onMount(() => {
|
||||
let _center: { lng: number; lat: number }
|
||||
if (typeof center["lng"] === "number" && typeof center["lat"] === "number") {
|
||||
_center = <any>center
|
||||
const { lon, lat } = mapProperties?.location?.data ?? { lon: 0, lat: 0 }
|
||||
|
||||
const rasterLayer: RasterLayerProperties = mapProperties?.rasterLayer?.data?.properties
|
||||
let styleUrl: string
|
||||
if (rasterLayer?.type === "vector") {
|
||||
styleUrl = rasterLayer?.style ?? rasterLayer?.url ?? AvailableRasterLayers.defaultBackgroundLayer.properties.url
|
||||
} else {
|
||||
_center = get(<any>center)
|
||||
const defaultLayer = AvailableRasterLayers.defaultBackgroundLayer.properties
|
||||
styleUrl = defaultLayer.style ?? defaultLayer.url
|
||||
}
|
||||
|
||||
_map = new maplibre.Map({
|
||||
console.log("Initing mapLIbremap with style", styleUrl)
|
||||
|
||||
const options: MapOptions = {
|
||||
container,
|
||||
style: styleUrl,
|
||||
zoom: get(zoom),
|
||||
center: _center,
|
||||
zoom: mapProperties?.zoom?.data ?? 1,
|
||||
center: { lng: lon, lat },
|
||||
maxZoom: 24,
|
||||
interactive: true,
|
||||
attributionControl: false,
|
||||
})
|
||||
attributionControl: false
|
||||
}
|
||||
_map = new maplibre.Map(options)
|
||||
window.requestAnimationFrame(() => {
|
||||
_map.resize()
|
||||
})
|
||||
_map.on("load", function () {
|
||||
_map.on("load", function() {
|
||||
_map.resize()
|
||||
const canvas = _map.getCanvas()
|
||||
if (interactive) {
|
||||
|
|
|
@ -74,4 +74,4 @@
|
|||
style="z-index: 100">
|
||||
<StyleLoadingIndicator map={altmap} />
|
||||
</div>
|
||||
<MaplibreMap {interactive} map={altmap} />
|
||||
<MaplibreMap {interactive} map={altmap} mapProperties={altproperties} />
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { Map as MLMap, SourceSpecification } from "maplibre-gl"
|
||||
import { Map as MLMap, RasterSourceSpecification, VectorTileSource } from "maplibre-gl"
|
||||
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
||||
import { Utils } from "../../Utils"
|
||||
import { VectorSourceSpecification } from "@maplibre/maplibre-gl-style-spec"
|
||||
|
||||
class SingleBackgroundHandler {
|
||||
// Value between 0 and 1.0
|
||||
|
@ -17,6 +18,7 @@ class SingleBackgroundHandler {
|
|||
*/
|
||||
public static readonly DEACTIVATE_AFTER = 60
|
||||
private fadeStep = 0.1
|
||||
|
||||
constructor(
|
||||
map: Store<MLMap>,
|
||||
targetLayer: RasterLayerPolygon,
|
||||
|
@ -75,6 +77,7 @@ class SingleBackgroundHandler {
|
|||
this.fadeIn()
|
||||
}
|
||||
}
|
||||
|
||||
private async awaitStyleIsLoaded(): Promise<void> {
|
||||
const map = this._map.data
|
||||
if (!map) {
|
||||
|
@ -85,11 +88,11 @@ class SingleBackgroundHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private async enable(){
|
||||
private async enable() {
|
||||
let ttl = 15
|
||||
await this.awaitStyleIsLoaded()
|
||||
while(!this.tryEnable() && ttl > 0){
|
||||
ttl --;
|
||||
while (!this.tryEnable() && ttl > 0) {
|
||||
ttl--
|
||||
await Utils.waitFor(250)
|
||||
}
|
||||
}
|
||||
|
@ -105,14 +108,19 @@ class SingleBackgroundHandler {
|
|||
}
|
||||
const background = this._targetLayer.properties
|
||||
console.debug("Enabling", background.id)
|
||||
let addLayerBeforeId = "aeroway_fill" // this is the first non-landuse item in the stylesheet, we add the raster layer before the roads but above the landuse
|
||||
let addLayerBeforeId = "transit_pier" // this is the first non-landuse item in the stylesheet, we add the raster layer before the roads but above the landuse
|
||||
if(!map.getLayer(addLayerBeforeId)){
|
||||
console.warn("Layer", addLayerBeforeId,"not foundhttp://127.0.0.1:1234/theme.html?layout=cyclofix&z=14.8&lat=51.05282501324558&lon=3.720591622281745&layer-range=true")
|
||||
addLayerBeforeId = undefined
|
||||
}
|
||||
if (background.category === "osmbasedmap" || background.category === "map") {
|
||||
// The background layer is already an OSM-based map or another map, so we don't want anything from the baselayer
|
||||
addLayerBeforeId = undefined
|
||||
}
|
||||
|
||||
if (!map.getSource(background.id)) {
|
||||
try {
|
||||
map.addSource(background.id, RasterLayerHandler.prepareWmsSource(background))
|
||||
map.addSource(background.id, RasterLayerHandler.prepareSource(background))
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
|
@ -122,21 +130,30 @@ class SingleBackgroundHandler {
|
|||
.getStyle()
|
||||
.layers.find((l) => l.id.startsWith("mapcomplete_"))?.id
|
||||
|
||||
map.addLayer(
|
||||
{
|
||||
id: background.id,
|
||||
type: "raster",
|
||||
source: background.id,
|
||||
paint: {
|
||||
"raster-opacity": 0,
|
||||
if (background.type === "vector") {
|
||||
const styleToSet = background.style ?? background.url
|
||||
map.setStyle(styleToSet)
|
||||
} else {
|
||||
map.addLayer(
|
||||
{
|
||||
id: background.id,
|
||||
type: "raster",
|
||||
source: background.id,
|
||||
paint: {
|
||||
"raster-opacity": 0
|
||||
}
|
||||
},
|
||||
},
|
||||
addLayerBeforeId
|
||||
)
|
||||
|
||||
this.opacity.addCallbackAndRun((o) => {
|
||||
map.setPaintProperty(background.id, "raster-opacity", o)
|
||||
})
|
||||
addLayerBeforeId
|
||||
)
|
||||
this.opacity.addCallbackAndRun((o) => {
|
||||
try{
|
||||
map.setPaintProperty(background.id, "raster-opacity", o)
|
||||
}catch (e) {
|
||||
console.debug("Could not set raster-opacity of", background.id)
|
||||
return true // This layer probably doesn't exist anymore, so we unregister
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -168,7 +185,14 @@ export default class RasterLayerHandler {
|
|||
})
|
||||
}
|
||||
|
||||
public static prepareWmsSource(layer: RasterLayerProperties): SourceSpecification {
|
||||
public static prepareSource(layer: RasterLayerProperties): RasterSourceSpecification | VectorSourceSpecification {
|
||||
if (layer.type === "vector") {
|
||||
const vs: VectorSourceSpecification = {
|
||||
type: "vector",
|
||||
url: layer.url
|
||||
}
|
||||
return vs
|
||||
}
|
||||
return {
|
||||
type: "raster",
|
||||
// use the tiles option to specify a 256WMS tile source URL
|
||||
|
@ -178,7 +202,7 @@ export default class RasterLayerHandler {
|
|||
minzoom: layer["min_zoom"] ?? 1,
|
||||
maxzoom: layer["max_zoom"] ?? 25,
|
||||
// Bit of a hack, but seems to work
|
||||
scheme: layer.url.includes("{-y}") ? "tms" : "xyz",
|
||||
scheme: layer.url.includes("{-y}") ? "tms" : "xyz"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,7 +216,7 @@ export default class RasterLayerHandler {
|
|||
"{width}": "" + size,
|
||||
"{height}": "" + size,
|
||||
"{zoom}": "{z}",
|
||||
"{-y}": "{y}",
|
||||
"{-y}": "{y}"
|
||||
}
|
||||
|
||||
for (const key in toReplace) {
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import { createEventDispatcher, onDestroy } from "svelte"
|
||||
import StyleLoadingIndicator from "./StyleLoadingIndicator.svelte"
|
||||
|
||||
/***
|
||||
* Chooses a background-layer out of available options
|
||||
|
|
|
@ -154,7 +154,7 @@ class PointRenderingLayer {
|
|||
|
||||
if (this._onClick) {
|
||||
const self = this
|
||||
el.addEventListener("click", function (ev) {
|
||||
el.addEventListener("click", function(ev) {
|
||||
ev.preventDefault()
|
||||
self._onClick(feature)
|
||||
// Workaround to signal the MapLibreAdaptor to ignore this click
|
||||
|
@ -200,7 +200,7 @@ class LineRenderingLayer {
|
|||
"lineCap",
|
||||
"offset",
|
||||
"fill",
|
||||
"fillColor",
|
||||
"fillColor"
|
||||
] as const
|
||||
|
||||
private static readonly lineConfigKeysColor = ["color", "fillColor"] as const
|
||||
|
@ -249,16 +249,8 @@ class LineRenderingLayer {
|
|||
imageAlongWay.map(async (img, i) => {
|
||||
const imgId = img.then.replaceAll(/[/.-]/g, "_")
|
||||
if (map.getImage(imgId) === undefined) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
map.loadImage(img.then, (err, image) => {
|
||||
if (err) {
|
||||
console.error("Could not add symbol layer to line due to", err)
|
||||
return
|
||||
}
|
||||
map.addImage(imgId, image)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
const loadedImage = await map.loadImage(img.then)
|
||||
map.addImage(imgId, loadedImage.data)
|
||||
}
|
||||
|
||||
const spec: AddLayerObject = {
|
||||
|
@ -272,11 +264,10 @@ class LineRenderingLayer {
|
|||
"icon-rotation-alignment": "map",
|
||||
"icon-pitch-alignment": "map",
|
||||
"icon-image": imgId,
|
||||
"icon-size": 0.055,
|
||||
},
|
||||
"icon-size": 0.055
|
||||
}
|
||||
}
|
||||
const filter = img.if?.asMapboxExpression()
|
||||
console.log(">>>", this._layername, imgId, img.if, "-->", filter)
|
||||
if (filter) {
|
||||
spec.filter = filter
|
||||
}
|
||||
|
@ -347,12 +338,12 @@ class LineRenderingLayer {
|
|||
type: "geojson",
|
||||
data: {
|
||||
type: "FeatureCollection",
|
||||
features,
|
||||
features
|
||||
},
|
||||
promoteId: "id",
|
||||
promoteId: "id"
|
||||
})
|
||||
const linelayer = this._layername + "_line"
|
||||
map.addLayer({
|
||||
const layer: AddLayerObject = {
|
||||
source: this._layername,
|
||||
id: linelayer,
|
||||
type: "line",
|
||||
|
@ -360,12 +351,17 @@ class LineRenderingLayer {
|
|||
"line-color": ["feature-state", "color"],
|
||||
"line-opacity": ["feature-state", "color-opacity"],
|
||||
"line-width": ["feature-state", "width"],
|
||||
"line-offset": ["feature-state", "offset"],
|
||||
"line-offset": ["feature-state", "offset"]
|
||||
},
|
||||
layout: {
|
||||
"line-cap": "round",
|
||||
},
|
||||
})
|
||||
"line-cap": "round"
|
||||
}
|
||||
}
|
||||
if (this._config.dashArray) {
|
||||
|
||||
layer.paint["line-dasharray"] = this._config.dashArray?.split(" ")?.map(s => Number(s)) ?? null
|
||||
}
|
||||
map.addLayer(layer)
|
||||
|
||||
if (this._config.imageAlongWay) {
|
||||
this.addSymbolLayer(this._layername, this._config.imageAlongWay)
|
||||
|
@ -397,8 +393,8 @@ class LineRenderingLayer {
|
|||
layout: {},
|
||||
paint: {
|
||||
"fill-color": ["feature-state", "fillColor"],
|
||||
"fill-opacity": ["feature-state", "fillColor-opacity"],
|
||||
},
|
||||
"fill-opacity": ["feature-state", "fillColor-opacity"]
|
||||
}
|
||||
})
|
||||
if (this._onClick) {
|
||||
map.on("click", polylayer, (e) => {
|
||||
|
@ -429,7 +425,7 @@ class LineRenderingLayer {
|
|||
this.currentSourceData = features
|
||||
src.setData({
|
||||
type: "FeatureCollection",
|
||||
features: this.currentSourceData,
|
||||
features: this.currentSourceData
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -513,14 +509,14 @@ export default class ShowDataLayer {
|
|||
layers.filter((l) => l.source !== null).map((l) => new FilteredLayer(l)),
|
||||
features,
|
||||
{
|
||||
constructStore: (features, layer) => new SimpleFeatureSource(layer, features),
|
||||
constructStore: (features, layer) => new SimpleFeatureSource(layer, features)
|
||||
}
|
||||
)
|
||||
perLayer.forEach((fs) => {
|
||||
new ShowDataLayer(mlmap, {
|
||||
layer: fs.layer.layerDef,
|
||||
features: fs,
|
||||
...(options ?? {}),
|
||||
...(options ?? {})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -533,11 +529,12 @@ export default class ShowDataLayer {
|
|||
return new ShowDataLayer(map, {
|
||||
layer: ShowDataLayer.rangeLayer,
|
||||
features,
|
||||
doShowLayer,
|
||||
doShowLayer
|
||||
})
|
||||
}
|
||||
|
||||
public destruct() {}
|
||||
public destruct() {
|
||||
}
|
||||
|
||||
private zoomToCurrentFeatures(map: MlMap) {
|
||||
if (this._options.zoomToFeatures) {
|
||||
|
@ -546,21 +543,21 @@ export default class ShowDataLayer {
|
|||
map.resize()
|
||||
map.fitBounds(bbox.toLngLat(), {
|
||||
padding: { top: 10, bottom: 10, left: 10, right: 10 },
|
||||
animate: false,
|
||||
animate: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private initDrawFeatures(map: MlMap) {
|
||||
let { features, doShowLayer, fetchStore, selectedElement, selectedLayer } = this._options
|
||||
const onClick =
|
||||
this._options.onClick ??
|
||||
(this._options.layer.title === undefined
|
||||
let { features, doShowLayer, fetchStore, selectedElement } = this._options
|
||||
let onClick = this._options.onClick
|
||||
if (!onClick && selectedElement) {
|
||||
onClick = (this._options.layer.title === undefined
|
||||
? undefined
|
||||
: (feature: Feature) => {
|
||||
selectedElement?.setData(feature)
|
||||
selectedLayer?.setData(this._options.layer)
|
||||
})
|
||||
selectedElement?.setData(feature)
|
||||
})
|
||||
}
|
||||
if (this._options.drawLines !== false) {
|
||||
for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
|
||||
const lineRenderingConfig = this._options.layer.lineRendering[i]
|
||||
|
|
29
src/UI/Map/SmallZoomButtons.svelte
Normal file
29
src/UI/Map/SmallZoomButtons.svelte
Normal file
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import Translations from "../i18n/Translations.js";
|
||||
import Min from "../../assets/svg/Min.svelte";
|
||||
import MapControlButton from "../Base/MapControlButton.svelte";
|
||||
import Plus from "../../assets/svg/Plus.svelte";
|
||||
import type { MapProperties } from "../../Models/MapProperties"
|
||||
|
||||
export let adaptor: MapProperties
|
||||
let canZoomIn = adaptor.maxzoom.map(mz => adaptor.zoom.data < mz, [adaptor.zoom] )
|
||||
let canZoomOut = adaptor.minzoom.map(mz => adaptor.zoom.data > mz, [adaptor.zoom] )
|
||||
</script>
|
||||
<div class="absolute bottom-0 right-0 pointer-events-none flex flex-col">
|
||||
<MapControlButton
|
||||
enabled={canZoomIn}
|
||||
cls="m-0.5 p-1"
|
||||
arialabel={Translations.t.general.labels.zoomIn}
|
||||
on:click={() => adaptor.zoom.update((z) => z + 1)}
|
||||
>
|
||||
<Plus class="h-5 w-5" />
|
||||
</MapControlButton>
|
||||
<MapControlButton
|
||||
enabled={canZoomOut}
|
||||
cls={"m-0.5 p-1"}
|
||||
arialabel={Translations.t.general.labels.zoomOut}
|
||||
on:click={() => adaptor.zoom.update((z) => z - 1)}
|
||||
>
|
||||
<Min class="h-5 w-5" />
|
||||
</MapControlButton>
|
||||
</div>
|
|
@ -6,14 +6,30 @@
|
|||
|
||||
let isLoading = false
|
||||
export let map: UIEventSource<MlMap>
|
||||
/**
|
||||
* Optional. Only used for the 'global' change indicator so that it won't spin on pan/zoom but only when a change _actually_ occured
|
||||
*/
|
||||
export let rasterLayer: UIEventSource<any> = undefined
|
||||
|
||||
let didChange = undefined
|
||||
onDestroy(rasterLayer?.addCallback(() => {
|
||||
didChange = true
|
||||
}) ??( () => {}))
|
||||
|
||||
onDestroy(Stores.Chronic(250).addCallback(
|
||||
() => {
|
||||
isLoading = !map.data?.isStyleLoaded()
|
||||
const mapIsLoading = !map.data?.isStyleLoaded()
|
||||
isLoading = mapIsLoading && (didChange || rasterLayer === undefined)
|
||||
if(didChange && !mapIsLoading){
|
||||
didChange = false
|
||||
}
|
||||
},
|
||||
))
|
||||
</script>
|
||||
|
||||
|
||||
{#if isLoading}
|
||||
<Loading />
|
||||
<Loading cls="h-6 w-6" />
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
|
|
|
@ -364,7 +364,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Loading>Creating point...</Loading>
|
||||
<Loading><Tr t={Translations.t.general.add.creating}/> </Loading>
|
||||
{/if}
|
||||
</div>
|
||||
</LoginToggle>
|
||||
|
|
|
@ -110,7 +110,7 @@ class ApplyButton extends UIElement {
|
|||
mla.allowZooming.setData(false)
|
||||
mla.allowMoving.setData(false)
|
||||
|
||||
const previewMap = new SvelteUIElement(MaplibreMap, { map: mlmap }).SetClass("h-48")
|
||||
const previewMap = new SvelteUIElement(MaplibreMap, { mapProperties: mla, map: mlmap }).SetClass("h-48")
|
||||
|
||||
const features = this.target_feature_ids.map((id) =>
|
||||
this.state.indexedFeatures.featuresById.data.get(id)
|
||||
|
|
|
@ -48,10 +48,10 @@
|
|||
<ImportFlow {importFlow} on:confirm={() => importFlow.onConfirm()}>
|
||||
<div slot="map" class="relative">
|
||||
<div class="h-32">
|
||||
<MaplibreMap {map} />
|
||||
<MaplibreMap {map} mapProperties={mla} />
|
||||
</div>
|
||||
<div class="absolute bottom-0">
|
||||
<OpenBackgroundSelectorButton />
|
||||
<OpenBackgroundSelectorButton {state} {map} />
|
||||
</div>
|
||||
</div>
|
||||
</ImportFlow>
|
||||
|
|
|
@ -109,7 +109,7 @@ export class MinimapViz implements SpecialVisualization {
|
|||
state.layout.layers
|
||||
)
|
||||
|
||||
return new SvelteUIElement(MaplibreMap, { interactive: false, map: mlmap })
|
||||
return new SvelteUIElement(MaplibreMap, { interactive: false, map: mlmap, mapProperties: mla })
|
||||
.SetClass("h-40 rounded")
|
||||
.SetStyle("overflow: hidden; pointer-events: none;")
|
||||
}
|
||||
|
|
|
@ -130,6 +130,16 @@ export class MoveWizardState {
|
|||
this.moveDisallowedReason.setData(t.partOfRelation)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// This is a new point. Check if it was snapped to an existing way due to the '_referencing_ways'-tag
|
||||
const store = this._state.featureProperties.getStore(id)
|
||||
store?.addCallbackAndRunD((tags) => {
|
||||
if (tags._referencing_ways !== undefined && tags._referencing_ways !== "[]") {
|
||||
console.log("Got referencing ways according to the tags")
|
||||
this.moveDisallowedReason.setData(t.partOfAWay)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
112
src/UI/Popup/SplitRoadWizard.svelte
Normal file
112
src/UI/Popup/SplitRoadWizard.svelte
Normal file
|
@ -0,0 +1,112 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Feature, Point } from "geojson"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Scissors from "../../assets/svg/Scissors.svelte"
|
||||
import WaySplitMap from "../BigComponents/WaySplitMap.svelte"
|
||||
import BackButton from "../Base/BackButton.svelte"
|
||||
import SplitAction from "../../Logic/Osm/Actions/SplitAction"
|
||||
import Translations from "../i18n/Translations"
|
||||
import NextButton from "../Base/NextButton.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import { OsmWay } from "../../Logic/Osm/OsmObject"
|
||||
import type { WayId } from "../../Models/OsmFeature"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let id: WayId
|
||||
const t = Translations.t.split
|
||||
let step: "initial" | "loading_way" | "splitting" | "applying_split" | "has_been_split" | "deleted" = "initial"
|
||||
// Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring
|
||||
let splitPoints = new UIEventSource<Feature<
|
||||
Point,
|
||||
{
|
||||
id: number
|
||||
index: number
|
||||
dist: number
|
||||
location: number
|
||||
}
|
||||
>[]>([])
|
||||
let splitpointsNotEmpty = splitPoints.map(sp => sp.length > 0)
|
||||
|
||||
let osmWay: OsmWay
|
||||
|
||||
async function downloadWay() {
|
||||
step = "loading_way"
|
||||
const dloaded = await state.osmObjectDownloader.DownloadObjectAsync(id)
|
||||
if (dloaded === "deleted") {
|
||||
step = "deleted"
|
||||
return
|
||||
}
|
||||
osmWay = dloaded
|
||||
|
||||
step = "splitting"
|
||||
}
|
||||
|
||||
async function doSplit() {
|
||||
step = "applying_split"
|
||||
const splitAction = new SplitAction(
|
||||
id,
|
||||
splitPoints.data.map((ff) => <[number, number]>(<Point>ff.geometry).coordinates),
|
||||
{
|
||||
theme: state?.layout?.id,
|
||||
},
|
||||
5,
|
||||
)
|
||||
await state.changes?.applyAction(splitAction)
|
||||
// We throw away the old map and splitpoints, and create a new map from scratch
|
||||
splitPoints.setData([])
|
||||
|
||||
// Close the popup. The contributor has to select a segment again to make sure they continue editing the correct segment; see #1219
|
||||
state.selectedElement?.setData(undefined)
|
||||
step = "has_been_split"
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
<Tr slot="not-logged-in" t={t.loginToSplit} />
|
||||
|
||||
{#if step === "deleted"}
|
||||
<!-- Empty -->
|
||||
{:else if step === "initial"}
|
||||
<button on:click={() => downloadWay()}>
|
||||
<Scissors class="w-6 h-6 shrink-0" />
|
||||
<Tr t={t.inviteToSplit} />
|
||||
</button>
|
||||
{:else if step === "loading_way"}
|
||||
<Loading />
|
||||
|
||||
{:else if step === "splitting"}
|
||||
<div class="flex flex-col interactive border-interactive p-2">
|
||||
<div class="w-full h-80">
|
||||
<WaySplitMap {state} {splitPoints} {osmWay} />
|
||||
</div>
|
||||
<div class="flex flex-wrap-reverse md:flex-nowrap w-full">
|
||||
<BackButton clss="w-full" on:click={() => {
|
||||
splitPoints.set([])
|
||||
step = "initial"
|
||||
}}>
|
||||
<Tr t={Translations.t.general.cancel} />
|
||||
</BackButton>
|
||||
<NextButton clss={ ($splitpointsNotEmpty ? "": "disabled ") + "w-full primary"} on:click={() => doSplit()}>
|
||||
<Tr t={t.split} />
|
||||
</NextButton>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{:else if step === "has_been_split"}
|
||||
<Tr cls="thanks" t={ t.hasBeenSplit.Clone().SetClass("font-bold thanks block w-full")} />
|
||||
<button on:click={() => downloadWay()}>
|
||||
<Scissors class="w-6 h-6" />
|
||||
<Tr t={t.splitAgain} />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
</LoginToggle>
|
||||
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
import Toggle from "../Input/Toggle"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Combine from "../Base/Combine"
|
||||
import { Button } from "../Base/Button"
|
||||
import Translations from "../i18n/Translations"
|
||||
import SplitAction from "../../Logic/Osm/Actions/SplitAction"
|
||||
import Title from "../Base/Title"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { LoginToggle } from "./LoginButton"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import WaySplitMap from "../BigComponents/WaySplitMap.svelte"
|
||||
import { Feature, Point } from "geojson"
|
||||
import { WayId } from "../../Models/OsmFeature"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import OsmObjectDownloader from "../../Logic/Osm/OsmObjectDownloader"
|
||||
import Scissors from "../../assets/svg/Scissors.svelte"
|
||||
|
||||
export default class SplitRoadWizard extends Combine {
|
||||
public dialogIsOpened: UIEventSource<boolean>
|
||||
|
||||
/**
|
||||
* A UI Element used for splitting roads
|
||||
*
|
||||
* @param id The id of the road to remove
|
||||
* @param state the state of the application
|
||||
*/
|
||||
constructor(
|
||||
id: WayId,
|
||||
state: {
|
||||
layout?: LayoutConfig
|
||||
osmConnection?: OsmConnection
|
||||
osmObjectDownloader?: OsmObjectDownloader
|
||||
changes?: Changes
|
||||
indexedFeatures?: IndexedFeatureSource
|
||||
selectedElement?: UIEventSource<Feature>
|
||||
}
|
||||
) {
|
||||
const t = Translations.t.split
|
||||
|
||||
// Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring
|
||||
const splitPoints = new UIEventSource<Feature<Point>[]>([])
|
||||
|
||||
const hasBeenSplit = new UIEventSource(false)
|
||||
|
||||
// Toggle variable between show split button and map
|
||||
const splitClicked = new UIEventSource<boolean>(false)
|
||||
|
||||
const leafletMap = new UIEventSource<BaseUIElement>(undefined)
|
||||
|
||||
function initMap() {
|
||||
;(async function (
|
||||
id: WayId,
|
||||
splitPoints: UIEventSource<Feature[]>
|
||||
): Promise<BaseUIElement> {
|
||||
return new SvelteUIElement(WaySplitMap, {
|
||||
osmWay: await state.osmObjectDownloader.DownloadObjectAsync(id),
|
||||
splitPoints,
|
||||
})
|
||||
})(id, splitPoints).then((mapComponent) =>
|
||||
leafletMap.setData(mapComponent.SetClass("w-full h-80"))
|
||||
)
|
||||
}
|
||||
|
||||
// Toggle between splitmap
|
||||
const splitButton = new SubtleButton(
|
||||
new SvelteUIElement(Scissors).SetClass("h-6 w-6"),
|
||||
new Toggle(
|
||||
t.splitAgain.Clone().SetClass("text-lg font-bold"),
|
||||
t.inviteToSplit.Clone().SetClass("text-lg font-bold"),
|
||||
hasBeenSplit
|
||||
)
|
||||
)
|
||||
|
||||
const splitToggle = new LoginToggle(splitButton, t.loginToSplit.Clone(), state)
|
||||
|
||||
// Save button
|
||||
const saveButton = new Button(t.split.Clone(), async () => {
|
||||
hasBeenSplit.setData(true)
|
||||
splitClicked.setData(false)
|
||||
const splitAction = new SplitAction(
|
||||
id,
|
||||
splitPoints.data.map((ff) => <[number, number]>(<Point>ff.geometry).coordinates),
|
||||
{
|
||||
theme: state?.layout?.id,
|
||||
},
|
||||
5
|
||||
)
|
||||
await state.changes?.applyAction(splitAction)
|
||||
// We throw away the old map and splitpoints, and create a new map from scratch
|
||||
splitPoints.setData([])
|
||||
|
||||
// Close the popup. The contributor has to select a segment again to make sure they continue editing the correct segment; see #1219
|
||||
state.selectedElement?.setData(undefined)
|
||||
})
|
||||
|
||||
saveButton.SetClass("btn btn-primary mr-3")
|
||||
const disabledSaveButton = new Button(t.split.Clone(), undefined)
|
||||
disabledSaveButton.SetClass("btn btn-disabled mr-3")
|
||||
// Only show the save button if there are split points defined
|
||||
const saveToggle = new Toggle(
|
||||
disabledSaveButton,
|
||||
saveButton,
|
||||
splitPoints.map((data) => data.length === 0)
|
||||
)
|
||||
|
||||
const cancelButton = Translations.t.general.cancel
|
||||
.Clone() // Not using Button() element to prevent full width button
|
||||
.SetClass("btn btn-secondary mr-3")
|
||||
.onClick(() => {
|
||||
splitPoints.setData([])
|
||||
splitClicked.setData(false)
|
||||
})
|
||||
|
||||
cancelButton.SetClass("btn btn-secondary block")
|
||||
|
||||
const splitTitle = new Title(t.splitTitle)
|
||||
|
||||
const mapView = new Combine([
|
||||
splitTitle,
|
||||
new VariableUiElement(leafletMap),
|
||||
new Combine([cancelButton, saveToggle]).SetClass("flex flex-row"),
|
||||
])
|
||||
mapView.SetClass("question")
|
||||
super([
|
||||
Toggle.If(hasBeenSplit, () =>
|
||||
t.hasBeenSplit.Clone().SetClass("font-bold thanks block w-full")
|
||||
),
|
||||
new Toggle(mapView, splitToggle, splitClicked),
|
||||
])
|
||||
splitClicked.addCallback((view) => {
|
||||
if (view) {
|
||||
initMap()
|
||||
}
|
||||
})
|
||||
this.dialogIsOpened = splitClicked
|
||||
const self = this
|
||||
splitButton.onClick(() => {
|
||||
splitClicked.setData(true)
|
||||
self.ScrollIntoView()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@
|
|||
</script>
|
||||
|
||||
{#if !userDetails || $userDetails.loggedIn}
|
||||
<div>
|
||||
<div class="break-words" style="word-break: break-word">
|
||||
{#if tags === undefined}
|
||||
<slot name="no-tags"><Tr cls="subtle" t={Translations.t.general.noTagsSelected} /></slot>
|
||||
{:else if embedIn === undefined}
|
||||
|
|
|
@ -76,6 +76,5 @@
|
|||
{value}
|
||||
{state}
|
||||
on:submit
|
||||
{unvalidatedText}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* The questions can either be shown all at once or one at a time (in which case they can be skipped)
|
||||
*/
|
||||
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import type { Feature } from "geojson"
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
|
@ -12,6 +12,7 @@
|
|||
import Tr from "../../Base/Tr.svelte"
|
||||
import Translations from "../../i18n/Translations.js"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
export let layer: LayerConfig
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
|
@ -67,8 +68,25 @@
|
|||
},
|
||||
[skippedQuestions]
|
||||
)
|
||||
let firstQuestion = questionsToAsk.map((qta) => qta[0])
|
||||
let firstQuestion: UIEventSource<TagRenderingConfig> = new UIEventSource<TagRenderingConfig>(undefined)
|
||||
let allQuestionsToAsk : UIEventSource<TagRenderingConfig[]> = new UIEventSource<TagRenderingConfig[]>([])
|
||||
|
||||
function calculateQuestions(){
|
||||
console.log("Applying questions to ask")
|
||||
const qta = questionsToAsk.data
|
||||
firstQuestion.setData(undefined)
|
||||
firstQuestion.setData(qta[0])
|
||||
|
||||
allQuestionsToAsk.setData([])
|
||||
allQuestionsToAsk.setData(qta)
|
||||
}
|
||||
|
||||
|
||||
onDestroy(questionsToAsk.addCallback(() =>calculateQuestions()))
|
||||
onDestroy(showAllQuestionsAtOnce.addCallback(() => calculateQuestions()))
|
||||
calculateQuestions()
|
||||
|
||||
|
||||
let answered: number = 0
|
||||
let skipped: number = 0
|
||||
|
||||
|
@ -92,7 +110,7 @@
|
|||
class="marker-questionbox-root"
|
||||
class:hidden={$questionsToAsk.length === 0 && skipped === 0 && answered === 0}
|
||||
>
|
||||
{#if $questionsToAsk.length === 0}
|
||||
{#if $allQuestionsToAsk.length === 0}
|
||||
{#if skipped + answered > 0}
|
||||
<div class="thanks">
|
||||
<Tr t={Translations.t.general.questionBox.done} />
|
||||
|
@ -140,11 +158,11 @@
|
|||
<div>
|
||||
{#if $showAllQuestionsAtOnce}
|
||||
<div class="flex flex-col gap-y-1">
|
||||
{#each $questionsToAsk as question (question.id)}
|
||||
{#each $allQuestionsToAsk as question (question.id)}
|
||||
<TagRenderingQuestion config={question} {tags} {selectedElement} {state} {layer} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
{:else if $firstQuestion !== undefined}
|
||||
<TagRenderingQuestion
|
||||
config={$firstQuestion}
|
||||
{layer}
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
import { placeholder } from "../../../Utils/placeholder"
|
||||
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { Tag } from "../../../Logic/Tags/Tag"
|
||||
import { get, writable } from "svelte/store"
|
||||
|
||||
export let config: TagRenderingConfig
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
|
@ -46,7 +47,7 @@
|
|||
|
||||
// Will be bound if a freeform is available
|
||||
let freeformInput = new UIEventSource<string>(tags?.[config.freeform?.key])
|
||||
let freeformInputUnvalidated = new UIEventSource<string>(freeformInput.data)
|
||||
let freeformInputUnvalidated = new UIEventSource<string>(get(freeformInput))
|
||||
|
||||
let selectedMapping: number = undefined
|
||||
/**
|
||||
|
@ -112,7 +113,7 @@
|
|||
unseenFreeformValues.splice(index, 1)
|
||||
}
|
||||
// TODO this has _to much_ values
|
||||
freeformInput.setData(unseenFreeformValues.join(";"))
|
||||
freeformInput.set(unseenFreeformValues.join(";"))
|
||||
if (checkedMappings.length + 1 < mappings.length) {
|
||||
checkedMappings.push(unseenFreeformValues.length > 0)
|
||||
}
|
||||
|
@ -121,10 +122,10 @@
|
|||
if (confg.freeform?.key) {
|
||||
if (!confg.multiAnswer) {
|
||||
// Somehow, setting multi-answer freeform values is broken if this is not set
|
||||
freeformInput.setData(tgs[confg.freeform.key])
|
||||
freeformInput.set(tgs[confg.freeform.key])
|
||||
}
|
||||
} else {
|
||||
freeformInput.setData(undefined)
|
||||
freeformInput.set(undefined)
|
||||
}
|
||||
feedback.setData(undefined)
|
||||
}
|
||||
|
@ -134,8 +135,8 @@
|
|||
// We want to (re)-initialize whenever the 'tags' or 'config' change - but not when 'checkedConfig' changes
|
||||
initialize($tags, config)
|
||||
}
|
||||
|
||||
freeformInput.addCallbackAndRun((freeformValue) => {
|
||||
onDestroy(
|
||||
freeformInput.subscribe((freeformValue) => {
|
||||
if (!mappings || mappings?.length == 0 || config.freeform?.key === undefined) {
|
||||
return
|
||||
}
|
||||
|
@ -151,7 +152,8 @@
|
|||
if (freeformValue?.length > 0) {
|
||||
selectedMapping = mappings.length
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
$: {
|
||||
if (
|
||||
allowDeleteOfFreeform &&
|
||||
|
@ -202,7 +204,7 @@
|
|||
theme: tags.data["_orig_theme"] ?? state.layout.id,
|
||||
changeType: "answer",
|
||||
})
|
||||
freeformInput.setData(undefined)
|
||||
freeformInput.set(undefined)
|
||||
selectedMapping = undefined
|
||||
selectedTags = undefined
|
||||
|
||||
|
@ -241,7 +243,7 @@
|
|||
<form
|
||||
class="interactive border-interactive relative flex flex-col overflow-y-auto px-2"
|
||||
style="max-height: 75vh"
|
||||
on:submit|preventDefault={() => onSave()}
|
||||
on:submit|preventDefault={() =>{ /*onSave(); This submit is not needed and triggers to early, causing bugs: see #1808*/}}
|
||||
>
|
||||
<fieldset>
|
||||
<legend>
|
||||
|
@ -285,7 +287,7 @@
|
|||
feature={selectedElement}
|
||||
value={freeformInput}
|
||||
unvalidatedText={freeformInputUnvalidated}
|
||||
on:submit={onSave}
|
||||
on:submit={() => onSave()}
|
||||
/>
|
||||
{:else if mappings !== undefined && !config.multiAnswer}
|
||||
<!-- Simple radiobuttons as mapping -->
|
||||
|
@ -329,7 +331,7 @@
|
|||
value={freeformInput}
|
||||
unvalidatedText={freeformInputUnvalidated}
|
||||
on:selected={() => (selectedMapping = config.mappings?.length)}
|
||||
on:submit={onSave}
|
||||
on:submit={() => onSave()}
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
|
@ -372,7 +374,7 @@
|
|||
feature={selectedElement}
|
||||
value={freeformInput}
|
||||
unvalidatedText={freeformInputUnvalidated}
|
||||
on:submit={onSave}
|
||||
on:submit={() => onSave()}
|
||||
/>
|
||||
</label>
|
||||
{/if}
|
||||
|
@ -397,13 +399,13 @@
|
|||
<slot name="cancel" />
|
||||
<slot name="save-button" {selectedTags}>
|
||||
{#if allowDeleteOfFreeform && (mappings?.length ?? 0) === 0 && $freeformInput === undefined && $freeformInputUnvalidated === ""}
|
||||
<button class="primary flex" on:click|stopPropagation|preventDefault={onSave}>
|
||||
<button class="primary flex" on:click|stopPropagation|preventDefault={() => onSave()}>
|
||||
<TrashIcon class="h-6 w-6 text-red-500" />
|
||||
<Tr t={Translations.t.general.eraseValue} />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
on:click={onSave}
|
||||
on:click={() => onSave()}
|
||||
class={twJoin(
|
||||
selectedTags === undefined ? "disabled" : "button-shadow",
|
||||
"primary"
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
let lang = (
|
||||
(navigator.languages && navigator.languages[0]) ||
|
||||
navigator.language ||
|
||||
navigator["userLanguage"] ||
|
||||
"en"
|
||||
).substr(0, 2)
|
||||
|
||||
function filterLangs(maindiv) {
|
||||
let foundLangs = 0
|
||||
for (const child of Array.from(maindiv.children)) {
|
||||
if (child.attributes.getNamedItem("lang")?.value === lang) {
|
||||
foundLangs++
|
||||
}
|
||||
}
|
||||
if (foundLangs === 0) {
|
||||
lang = "en"
|
||||
}
|
||||
for (const child of Array.from(maindiv.children)) {
|
||||
const childLang = child.attributes.getNamedItem("lang")
|
||||
if (childLang === undefined) {
|
||||
continue
|
||||
}
|
||||
if (childLang.value === lang) {
|
||||
continue
|
||||
}
|
||||
child.parentElement.removeChild(child)
|
||||
}
|
||||
}
|
||||
|
||||
filterLangs(document.getElementById("descriptions-while-loading"))
|
||||
filterLangs(document.getElementById("default-title"))
|
32
src/UI/RemoveOtherLanguages.ts
Normal file
32
src/UI/RemoveOtherLanguages.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
export {}
|
||||
let lang = (
|
||||
(navigator.languages && navigator.languages[0]) ||
|
||||
navigator.language ||
|
||||
navigator["userLanguage"] ||
|
||||
"en"
|
||||
).substr(0, 2)
|
||||
|
||||
function filterLangs(maindiv) {
|
||||
let foundLangs = 0
|
||||
for (const child of Array.from(maindiv.children)) {
|
||||
if (child.attributes.getNamedItem("lang")?.value === lang) {
|
||||
foundLangs++
|
||||
}
|
||||
}
|
||||
if (foundLangs === 0) {
|
||||
lang = "en"
|
||||
}
|
||||
for (const child of Array.from(maindiv.children)) {
|
||||
const childLang = child.attributes.getNamedItem("lang")
|
||||
if (childLang === undefined) {
|
||||
continue
|
||||
}
|
||||
if (childLang.value === lang) {
|
||||
continue
|
||||
}
|
||||
child.parentElement.removeChild(child)
|
||||
}
|
||||
}
|
||||
|
||||
filterLangs(document.getElementById("descriptions-while-loading"))
|
||||
filterLangs(document.getElementById("default-title"))
|
|
@ -61,17 +61,12 @@
|
|||
opinion: opinion.data,
|
||||
metadata: { nickname, is_affiliated: isAffiliated.data },
|
||||
}
|
||||
if (state.featureSwitchIsTesting?.data ?? true) {
|
||||
console.log("Testing - not actually saving review", review)
|
||||
await Utils.waitFor(1000)
|
||||
} else {
|
||||
try {
|
||||
await reviews.createReview(review)
|
||||
} catch (e) {
|
||||
console.error("Could not create review due to", e)
|
||||
uploadFailed = "" + e
|
||||
}
|
||||
}
|
||||
_state = "done"
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -6,12 +6,15 @@
|
|||
import LoginButton from "../Base/LoginButton.svelte"
|
||||
import SingleReview from "./SingleReview.svelte"
|
||||
import Mangrove_logo from "../../assets/svg/Mangrove_logo.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
|
||||
/**
|
||||
* A panel showing all the reviews by the logged-in user
|
||||
*/
|
||||
export let state: SpecialVisualizationState
|
||||
let reviews = state.userRelatedState.mangroveIdentity.getAllReviews()
|
||||
let allReviews = state.userRelatedState.mangroveIdentity.getAllReviews()
|
||||
let reviews = state.userRelatedState.mangroveIdentity.getGeoReviews()
|
||||
let kid = state.userRelatedState.mangroveIdentity.getKeyId()
|
||||
const t = Translations.t.reviews
|
||||
</script>
|
||||
|
||||
|
@ -22,23 +25,42 @@
|
|||
</LoginButton>
|
||||
</div>
|
||||
|
||||
{#if $reviews?.length > 0}
|
||||
<div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}>
|
||||
{#each $reviews as review (review.sub)}
|
||||
<SingleReview {review} showSub={true} {state} />
|
||||
{/each}
|
||||
</div>
|
||||
{#if $reviews === undefined}
|
||||
<Loading />
|
||||
{:else}
|
||||
<Tr t={t.your_reviews_empty} />
|
||||
{#if $reviews?.length > 0}
|
||||
<div class="flex flex-col gap-y-1" on:keypress={(e) => console.log("Got keypress", e)}>
|
||||
{#each $reviews as review (review.sub)}
|
||||
<SingleReview {review} showSub={true} {state} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Tr t={t.your_reviews_empty} />
|
||||
{/if}
|
||||
|
||||
{#if $allReviews?.length > $reviews?.length}
|
||||
{#if $allReviews?.length - $reviews?.length === 1}
|
||||
<Tr t={t.non_place_review} />
|
||||
{:else}
|
||||
<Tr t={t.non_place_reviews.Subs({n:$allReviews?.length - $reviews?.length })} />
|
||||
{/if}
|
||||
<a target="_blank"
|
||||
class="link-underline"
|
||||
rel="noopener nofollow"
|
||||
href={`https://mangrove.reviews/list?kid=${encodeURIComponent($kid)}`}
|
||||
>
|
||||
<Tr t={t.see_all} />
|
||||
</a>
|
||||
{/if}
|
||||
<a
|
||||
class="link-underline"
|
||||
href="https://github.com/pietervdvn/MapComplete/issues/1782"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Tr t={t.reviews_bug} />
|
||||
</a>
|
||||
{/if}
|
||||
<a
|
||||
class="link-underline"
|
||||
href="https://github.com/pietervdvn/MapComplete/issues/1782"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Tr t={t.reviews_bug} />
|
||||
</a>
|
||||
<div class="flex justify-end">
|
||||
<Mangrove_logo class="h-12 w-12 shrink-0 p-1" />
|
||||
<Tr cls="text-sm subtle" t={t.attribution} />
|
||||
|
|
|
@ -22,6 +22,8 @@ import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
|
|||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||
import { SummaryTileSourceRewriter } from "../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource"
|
||||
import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import ShowDataLayer from "./Map/ShowDataLayer"
|
||||
|
||||
/**
|
||||
* The state needed to render a special Visualisation.
|
||||
|
@ -86,6 +88,8 @@ export interface SpecialVisualizationState {
|
|||
|
||||
readonly previewedImage: UIEventSource<ProvidedImage>
|
||||
readonly geolocation: GeoLocationHandler
|
||||
|
||||
showCurrentLocationOn(map: Store<MlMap>): ShowDataLayer
|
||||
}
|
||||
|
||||
export interface SpecialVisualization {
|
||||
|
|
|
@ -42,8 +42,6 @@ import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte"
|
|||
import UserProfile from "./BigComponents/UserProfile.svelte"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
|
||||
import { WayId } from "../Models/OsmFeature"
|
||||
import SplitRoadWizard from "./Popup/SplitRoadWizard"
|
||||
import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz"
|
||||
import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"
|
||||
import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte"
|
||||
|
@ -90,6 +88,7 @@ import LoginButton from "./Base/LoginButton.svelte"
|
|||
import Toggle from "./Input/Toggle"
|
||||
import ImportReviewIdentity from "./Reviews/ImportReviewIdentity.svelte"
|
||||
import LinkedDataLoader from "../Logic/Web/LinkedDataLoader"
|
||||
import SplitRoadWizard from "./Popup/SplitRoadWizard.svelte"
|
||||
|
||||
class NearbyImageVis implements SpecialVisualization {
|
||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||
|
@ -429,7 +428,7 @@ export default class SpecialVisualizations {
|
|||
.map((tags) => tags.id)
|
||||
.map((id) => {
|
||||
if (id.startsWith("way/")) {
|
||||
return new SplitRoadWizard(<WayId>id, state)
|
||||
return new SvelteUIElement(SplitRoadWizard, { id, state })
|
||||
}
|
||||
return undefined
|
||||
}),
|
||||
|
@ -666,6 +665,7 @@ export default class SpecialVisualizations {
|
|||
nameKey: nameKey,
|
||||
fallbackName,
|
||||
},
|
||||
state.featureSwitchIsTesting
|
||||
)
|
||||
return new SvelteUIElement(StarsBarIcon, {
|
||||
score: reviews.average,
|
||||
|
@ -699,6 +699,7 @@ export default class SpecialVisualizations {
|
|||
nameKey: nameKey,
|
||||
fallbackName,
|
||||
},
|
||||
state.featureSwitchIsTesting
|
||||
)
|
||||
return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer })
|
||||
},
|
||||
|
@ -731,6 +732,7 @@ export default class SpecialVisualizations {
|
|||
nameKey: nameKey,
|
||||
fallbackName,
|
||||
},
|
||||
state.featureSwitchIsTesting
|
||||
)
|
||||
return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer })
|
||||
},
|
||||
|
@ -750,7 +752,7 @@ export default class SpecialVisualizations {
|
|||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
const [text] = argument
|
||||
return new SvelteUIElement(ImportReviewIdentity, { state, text })
|
||||
|
@ -1151,10 +1153,11 @@ export default class SpecialVisualizations {
|
|||
constr: (state) => {
|
||||
return new Combine(
|
||||
state.layout.layers
|
||||
.filter((l) => l.name !== null)
|
||||
.filter((l) => l.name !== null && l.title && state.perLayer.get(l.id) !== undefined )
|
||||
.map(
|
||||
(l) => {
|
||||
const fs = state.perLayer.get(l.id)
|
||||
console.log(">>>", l.id, fs)
|
||||
const bbox = state.mapProperties.bounds
|
||||
const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox)
|
||||
return new StatisticsPanel(fsBboxed)
|
||||
|
@ -1596,6 +1599,9 @@ export default class SpecialVisualizations {
|
|||
feature: Feature,
|
||||
layer: LayerConfig,
|
||||
): BaseUIElement {
|
||||
const smallSize = 100
|
||||
const bigSize = 200
|
||||
const size = new UIEventSource(smallSize)
|
||||
return new VariableUiElement(
|
||||
tagSource
|
||||
.map((tags) => tags.id)
|
||||
|
@ -1615,11 +1621,17 @@ export default class SpecialVisualizations {
|
|||
const url =
|
||||
`${window.location.protocol}//${window.location.host}${window.location.pathname}?${layout}lat=${lat}&lon=${lon}&z=15` +
|
||||
`#${id}`
|
||||
return new Img(new Qr(url).toImageElement(75)).SetStyle(
|
||||
"width: 75px",
|
||||
return new Img(new Qr(url).toImageElement(size.data)).SetStyle(
|
||||
`width: ${size.data}px`
|
||||
)
|
||||
}),
|
||||
)
|
||||
}, [size])
|
||||
).onClick(()=> {
|
||||
if(size.data !== bigSize){
|
||||
size.setData(bigSize)
|
||||
}else{
|
||||
size.setData(smallSize)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
)
|
||||
let osmConnection = new OsmConnection({
|
||||
oauth_token,
|
||||
checkOnlineRegularly: true
|
||||
})
|
||||
const expertMode = UIEventSource.asBoolean(
|
||||
osmConnection.GetPreference("studio-expert-mode", "false", {
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
</script>
|
||||
|
||||
No tests
|
||||
|
||||
|
|
|
@ -118,7 +118,8 @@
|
|||
let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined)
|
||||
let mapproperties: MapProperties = state.mapProperties
|
||||
state.mapProperties.installCustomKeyboardHandler(viewport)
|
||||
|
||||
let canZoomIn = mapproperties.maxzoom.map(mz => mapproperties.zoom.data < mz, [mapproperties.zoom] )
|
||||
let canZoomOut = mapproperties.minzoom.map(mz => mapproperties.zoom.data > mz, [mapproperties.zoom] )
|
||||
function updateViewport() {
|
||||
const rect = viewport.data?.getBoundingClientRect()
|
||||
if (!rect) {
|
||||
|
@ -148,7 +149,7 @@
|
|||
let currentViewLayer = layout.layers.find((l) => l.id === "current_view")
|
||||
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer
|
||||
let rasterLayerName =
|
||||
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name
|
||||
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.defaultBackgroundLayer.properties.name
|
||||
onDestroy(
|
||||
rasterLayer.addCallbackAndRunD((l) => {
|
||||
rasterLayerName = l.properties.name
|
||||
|
@ -179,7 +180,7 @@
|
|||
</script>
|
||||
|
||||
<div class="absolute top-0 left-0 h-screen w-screen overflow-hidden">
|
||||
<MaplibreMap map={maplibremap} />
|
||||
<MaplibreMap map={maplibremap} mapProperties={mapproperties} />
|
||||
</div>
|
||||
|
||||
{#if $visualFeedback}
|
||||
|
@ -256,6 +257,9 @@
|
|||
<If condition={state.featureSwitchIsTesting}>
|
||||
<div class="alert w-fit">Testmode</div>
|
||||
</If>
|
||||
<If condition={state.featureSwitches.featureSwitchFakeUser}>
|
||||
<div class="alert w-fit">Faking a user (Testmode)</div>
|
||||
</If>
|
||||
</div>
|
||||
<div class="flex w-full flex-col items-center justify-center">
|
||||
<!-- Flex and w-full are needed for the positioning -->
|
||||
|
@ -329,12 +333,14 @@
|
|||
</If>
|
||||
<MapControlButton
|
||||
arialabel={Translations.t.general.labels.zoomIn}
|
||||
enabled={canZoomIn}
|
||||
on:click={() => mapproperties.zoom.update((z) => z + 1)}
|
||||
on:keydown={forwardEventToMap}
|
||||
>
|
||||
<Plus class="h-8 w-8" />
|
||||
</MapControlButton>
|
||||
<MapControlButton
|
||||
enabled={canZoomOut}
|
||||
arialabel={Translations.t.general.labels.zoomOut}
|
||||
on:click={() => mapproperties.zoom.update((z) => z - 1)}
|
||||
on:keydown={forwardEventToMap}
|
||||
|
@ -402,7 +408,7 @@
|
|||
<div slot="close-button" />
|
||||
<div class="normal-background absolute flex h-full w-full flex-col">
|
||||
<SelectedElementTitle {state} layer={$selectedLayer} selectedElement={$selectedElement} />
|
||||
<SelectedElementView {state} layer={$selectedLayer} selectedElement={$selectedElement} />
|
||||
<SelectedElementView {state} selectedElement={$selectedElement} />
|
||||
</div>
|
||||
</ModalRight>
|
||||
{/if}
|
||||
|
@ -580,10 +586,9 @@
|
|||
</div>
|
||||
<SelectedElementView
|
||||
highlightedRendering={state.guistate.highlightedUserSetting}
|
||||
layer={UserRelatedState.usersettingsConfig}
|
||||
selectedElement={{
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
properties: {id:"settings"},
|
||||
geometry: { type: "Point", coordinates: [0, 0] },
|
||||
}}
|
||||
{state}
|
||||
|
|
|
@ -20,7 +20,7 @@ export default class WikidataSearchBox extends InputElement<string> {
|
|||
new Table(
|
||||
["name", "doc"],
|
||||
[
|
||||
["key", "the value of this tag will initialize search (default: name)"],
|
||||
["key", "the value of this tag will initialize search (default: name). This can be a ';'-separated list in which case every key will be inspected. The non-null value will be used as search"],
|
||||
[
|
||||
"options",
|
||||
new Combine([
|
||||
|
|
13
src/Utils.ts
13
src/Utils.ts
|
@ -1059,6 +1059,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
throw result["error"]
|
||||
}
|
||||
|
||||
public static awaitAnimationFrame(): Promise<void>{
|
||||
return new Promise<void>((resolve) => {
|
||||
window.requestAnimationFrame(() => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public static async downloadJsonAdvanced(
|
||||
url: string,
|
||||
headers?: any
|
||||
|
@ -1388,7 +1396,10 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
d.setUTCMinutes(0)
|
||||
}
|
||||
|
||||
public static scrollIntoView(element: HTMLBaseElement | HTMLDivElement) {
|
||||
public static scrollIntoView(element: HTMLBaseElement | HTMLDivElement): void {
|
||||
if (!element) {
|
||||
return
|
||||
}
|
||||
// Is the element completely in the view?
|
||||
const parentRect = Utils.findParentWithScrolling(element)?.getBoundingClientRect()
|
||||
if (!parentRect) {
|
||||
|
|
|
@ -65,9 +65,11 @@ export class PngMapCreator {
|
|||
|
||||
document.getElementById(freeComponentId).appendChild(div)
|
||||
const newZoom = settings.zoom.data + Math.log2(pixelRatio) - 1
|
||||
const rasterLayerProperties = settings.rasterLayer.data?.properties ?? AvailableRasterLayers.defaultBackgroundLayer.properties
|
||||
const style = rasterLayerProperties?.style ?? rasterLayerProperties?.url
|
||||
const mapElem = new MlMap({
|
||||
container: div.id,
|
||||
style: AvailableRasterLayers.maptilerDefaultLayer.properties.url,
|
||||
style,
|
||||
center: [l.lon, l.lat],
|
||||
zoom: newZoom,
|
||||
pixelRatio,
|
||||
|
|
|
@ -881,107 +881,7 @@ class SvgToPdfPage {
|
|||
width,
|
||||
height,
|
||||
}).CreatePng(this.options.freeComponentId, this._state)
|
||||
} /* else {
|
||||
const match = spec.match(/\$map\(([^)]*)\)$/)
|
||||
if (match === null) {
|
||||
throw "Invalid mapspec:" + spec
|
||||
}
|
||||
const params = SvgToPdfInternals.parseCss(match[1], ",")
|
||||
const layout = AllKnownLayouts.allKnownLayouts.get(params["theme"])
|
||||
if (layout === undefined) {
|
||||
console.error("Could not show map with parameters", params)
|
||||
throw (
|
||||
"Theme not found:" +
|
||||
params["theme"] +
|
||||
". Use theme: to define which theme to use. "
|
||||
)
|
||||
}
|
||||
layout.widenFactor = 0
|
||||
layout.overpassTimeout = 600
|
||||
layout.defaultBackgroundId = params["background"] ?? layout.defaultBackgroundId
|
||||
for (const paramsKey in params) {
|
||||
if (paramsKey.startsWith("layer-")) {
|
||||
const layerName = paramsKey.substring("layer-".length)
|
||||
const key = params[paramsKey].toLowerCase().trim()
|
||||
const layer = layout.layers.find((l) => l.id === layerName)
|
||||
if (layer === undefined) {
|
||||
throw "No layer found for " + paramsKey
|
||||
}
|
||||
if (key === "force") {
|
||||
layer.minzoom = 0
|
||||
layer.minzoomVisible = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
const zoom = Number(params["zoom"] ?? params["z"] ?? 14)
|
||||
|
||||
const state = new ThemeViewState(layout)
|
||||
state.mapProperties.location.setData({
|
||||
lat: this.options?.overrideLocation?.lat ?? Number(params["lat"] ?? 51.05016),
|
||||
lon: this.options?.overrideLocation?.lon ?? Number(params["lon"] ?? 3.717842),
|
||||
})
|
||||
state.mapProperties.zoom.setData(zoom)
|
||||
|
||||
const fl = Array.from(state.layerState.filteredLayers.values())
|
||||
for (const filteredLayer of fl) {
|
||||
if (params["layer-" + filteredLayer.layerDef.id] !== undefined) {
|
||||
filteredLayer.isDisplayed.setData(
|
||||
loadData &&
|
||||
params["layer-" + filteredLayer.layerDef.id].trim().toLowerCase() !==
|
||||
"false"
|
||||
)
|
||||
} else if (params["layers"] === "none") {
|
||||
filteredLayer.isDisplayed.setData(false)
|
||||
} else if (filteredLayer.layerDef.id.startsWith("note_import")) {
|
||||
filteredLayer.isDisplayed.setData(false)
|
||||
}
|
||||
}
|
||||
|
||||
for (const paramsKey in params) {
|
||||
if (paramsKey.startsWith("layer-")) {
|
||||
const layerName = paramsKey.substring("layer-".length)
|
||||
const key = params[paramsKey].toLowerCase().trim()
|
||||
const isDisplayed = loadData && (key === "true" || key === "force")
|
||||
const layer = fl.find((l) => l.layerDef.id === layerName)
|
||||
if (!loadData) {
|
||||
console.log(
|
||||
"Not loading map data as 'loadData' is falsed, this is probably a test run"
|
||||
)
|
||||
} else {
|
||||
console.log(
|
||||
"Setting ",
|
||||
layer?.layerDef?.id,
|
||||
" to visibility",
|
||||
isDisplayed,
|
||||
"(minzoom:",
|
||||
layer?.layerDef?.minzoomVisible,
|
||||
layer?.layerDef?.minzoom,
|
||||
")"
|
||||
)
|
||||
}
|
||||
layer.isDisplayed.setData(loadData && isDisplayed)
|
||||
if (key === "force" && loadData) {
|
||||
layer.layerDef.minzoom = 0
|
||||
layer.layerDef.minzoomVisible = 0
|
||||
layer.isDisplayed.addCallback((isDisplayed) => {
|
||||
if (!isDisplayed) {
|
||||
console.warn("Forcing layer " + paramsKey + " as true")
|
||||
layer.isDisplayed.setData(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
const pngCreator = new PngMapCreator(state, {
|
||||
width: 4 * width,
|
||||
height: 4 * height,
|
||||
})
|
||||
png = await pngCreator.CreatePng(this.options.freeComponentId, this._state)
|
||||
if (!png) {
|
||||
throw "PngCreator did not output anything..."
|
||||
}
|
||||
}
|
||||
//*/
|
||||
svgImage.setAttribute("xlink:href", await SvgToPdfPage.blobToBase64(png))
|
||||
svgImage.style.width = width + "mm"
|
||||
svgImage.style.height = height + "mm"
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -12,6 +12,112 @@
|
|||
"url": "https://osm.org/copyright"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "pmtiles://https://api.protomaps.com/tiles/v3.json?key=2af8b969a9e8b692",
|
||||
"style": "https://api.protomaps.com/styles/v2/white.json?key=2af8b969a9e8b692",
|
||||
"connect-src": [
|
||||
"https://protomaps.github.io"
|
||||
],
|
||||
"id": "protomaps.white",
|
||||
"name": "Protomaps White",
|
||||
"type": "vector",
|
||||
"category": "osmbasedmap",
|
||||
"attribution": {
|
||||
"text": "Protomaps",
|
||||
"url": "https://protomaps.com/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "pmtiles://https://api.protomaps.com/tiles/v3.json?key=2af8b969a9e8b692",
|
||||
"style": "https://api.protomaps.com/styles/v2/light.json?key=2af8b969a9e8b692",
|
||||
"connect-src": [
|
||||
"https://protomaps.github.io"
|
||||
],
|
||||
"id": "protomaps.light",
|
||||
"name": "Protomaps Light",
|
||||
"type": "vector",
|
||||
"category": "osmbasedmap",
|
||||
"attribution": {
|
||||
"text": "Protomaps",
|
||||
"url": "https://protomaps.com/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "pmtiles://https://api.protomaps.com/tiles/v3.json?key=2af8b969a9e8b692",
|
||||
"connect-src": [
|
||||
"https://protomaps.github.io"
|
||||
],
|
||||
"style": "https://api.protomaps.com/styles/v2/grayscale.json?key=2af8b969a9e8b692",
|
||||
"id": "protomaps.grayscale",
|
||||
"name": "Protomaps Grayscale",
|
||||
"type": "vector",
|
||||
"category": "osmbasedmap",
|
||||
"attribution": {
|
||||
"text": "Protomaps",
|
||||
"url": "https://protomaps.com/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "pmtiles://https://api.protomaps.com/tiles/v3.json?key=2af8b969a9e8b692",
|
||||
"connect-src": [
|
||||
"https://protomaps.github.io"
|
||||
],
|
||||
"style": "https://api.protomaps.com/styles/v2/dark.json?key=2af8b969a9e8b692",
|
||||
"id": "protomaps.dark",
|
||||
"name": "Protomaps Dark",
|
||||
"type": "vector",
|
||||
"category": "osmbasedmap",
|
||||
"attribution": {
|
||||
"text": "Protomaps",
|
||||
"url": "https://protomaps.com/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "pmtiles://https://api.protomaps.com/tiles/v3.json?key=2af8b969a9e8b692",
|
||||
"style": "https://api.protomaps.com/styles/v2/black.json?key=2af8b969a9e8b692",
|
||||
"connect-src": [
|
||||
"https://protomaps.github.io"
|
||||
],
|
||||
"id": "protomaps.black",
|
||||
"name": "Protomaps Black",
|
||||
"type": "vector",
|
||||
"category": "osmbasedmap",
|
||||
"attribution": {
|
||||
"text": "Protomaps",
|
||||
"url": "https://protomaps.com/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "pmtiles://https://api.protomaps.com/tiles/v3.json?key=2af8b969a9e8b692",
|
||||
"style": "assets/sunny.json",
|
||||
"connect-src": [
|
||||
"https://protomaps.github.io"
|
||||
],
|
||||
"id": "protomaps.sunny",
|
||||
"name": "Protomaps Sunny",
|
||||
"type": "vector",
|
||||
"category": "osmbasedmap",
|
||||
"attribution": {
|
||||
"text": "Protomaps",
|
||||
"url": "https://protomaps.com/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "pmtiles://https://api.protomaps.com/tiles/v3.json?key=2af8b969a9e8b692",
|
||||
"style": "assets/sunny-unlabeled.json",
|
||||
"connect-src": [
|
||||
"https://protomaps.github.io"
|
||||
],
|
||||
"id": "protomaps.sunny_unlabeled",
|
||||
"name": "Protomaps Sunny Unlabeled",
|
||||
"type": "vector",
|
||||
"category": "osmbasedmap",
|
||||
"attribution": {
|
||||
"text": "Protomaps",
|
||||
"url": "https://protomaps.com/"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Americana",
|
||||
"url": "https://zelonewolf.github.io/openstreetmap-americana/style.json",
|
||||
|
@ -24,10 +130,11 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"name": "MapTiler Backdrop",
|
||||
"url": "https://api.maptiler.com/maps/backdrop/style.json?key=GvoVAJgu46I5rZapJuAy",
|
||||
"name": "MapTiler",
|
||||
"url": "https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=GvoVAJgu46I5rZapJuAy",
|
||||
"style": "https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=GvoVAJgu46I5rZapJuAy",
|
||||
"category": "osmbasedmap",
|
||||
"id": "maptiler.backdrop",
|
||||
"id": "maptiler",
|
||||
"type": "vector",
|
||||
"attribution": {
|
||||
"text": "Maptiler",
|
||||
|
@ -88,7 +195,8 @@
|
|||
"text": "Stamen/Stadiamaps",
|
||||
"url": "https://stadiamaps.com/"
|
||||
}
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"name": "Stamen Watercolor",
|
||||
"url": "https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg?key=14c5a900-7137-42f7-9cb9-fff0f4696f75",
|
||||
"category": "osmbasedmap",
|
||||
|
@ -142,8 +250,6 @@
|
|||
"url": "https://carto.com/"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"url": "https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json?key=eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfdW4ybmhlbTciLCJqdGkiOiIwZGQxNjJmNyJ9.uATJpa6QcrtXhph3Bzvk2nX3QsxEw-Q8dj5khUG6hGk",
|
||||
"name": "Carto Positron (no labels)",
|
||||
|
|
36
src/index.ts
36
src/index.ts
|
@ -20,16 +20,26 @@ function webgl_support() {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function timeout(timeMS: number): Promise<{ layers: string[] }> {
|
||||
await Utils.waitFor(timeMS)
|
||||
return { layers: [] }
|
||||
}
|
||||
|
||||
async function getAvailableLayers(): Promise<Set<string>> {
|
||||
try {
|
||||
const host = new URL(Constants.VectorTileServer).host
|
||||
const status = await Utils.downloadJson("https://" + host + "/summary/status.json")
|
||||
const status: { layers: string[] } = await Promise.any([
|
||||
// Utils.downloadJson("https://" + host + "/summary/status.json"),
|
||||
timeout(0)
|
||||
])
|
||||
return new Set<string>(status.layers)
|
||||
} catch (e) {
|
||||
console.error("Could not get MVT available layers due to", e)
|
||||
return new Set<string>()
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// @ts-ignore
|
||||
try {
|
||||
|
@ -38,12 +48,15 @@ async function main() {
|
|||
}
|
||||
const [layout, availableLayers] = await Promise.all([
|
||||
DetermineLayout.GetLayout(),
|
||||
await getAvailableLayers(),
|
||||
await getAvailableLayers()
|
||||
])
|
||||
console.log("The available layers on server are", Array.from(availableLayers))
|
||||
const state = new ThemeViewState(layout, availableLayers)
|
||||
const main = new SvelteUIElement(ThemeViewGUI, { state })
|
||||
main.AttachTo("maindiv")
|
||||
Array.from(document.getElementsByClassName("delete-on-load")).forEach(el => {
|
||||
el.parentElement.removeChild(el)
|
||||
})
|
||||
} catch (err) {
|
||||
console.error("Error while initializing: ", err, err.stack)
|
||||
const customDefinition = DetermineLayout.getCustomDefinition()
|
||||
|
@ -52,16 +65,17 @@ async function main() {
|
|||
|
||||
customDefinition?.length > 0
|
||||
? new SubtleButton(new SvelteUIElement(Download), "Download the raw file").onClick(
|
||||
() =>
|
||||
Utils.offerContentsAsDownloadableFile(
|
||||
DetermineLayout.getCustomDefinition(),
|
||||
"mapcomplete-theme.json",
|
||||
{ mimetype: "application/json" }
|
||||
)
|
||||
)
|
||||
: undefined,
|
||||
() =>
|
||||
Utils.offerContentsAsDownloadableFile(
|
||||
DetermineLayout.getCustomDefinition(),
|
||||
"mapcomplete-theme.json",
|
||||
{ mimetype: "application/json" }
|
||||
)
|
||||
)
|
||||
: undefined
|
||||
]).AttachTo("maindiv")
|
||||
}
|
||||
}
|
||||
|
||||
main().then((_) => {})
|
||||
main().then((_) => {
|
||||
})
|
||||
|
|
|
@ -18,10 +18,20 @@ function webgl_support() {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function timeout(timeMS: number): Promise<{ layers: string[] }> {
|
||||
await Utils.waitFor(timeMS)
|
||||
return { layers: [] }
|
||||
}
|
||||
|
||||
|
||||
async function getAvailableLayers(): Promise<Set<string>> {
|
||||
try {
|
||||
const host = new URL(Constants.VectorTileServer).host
|
||||
const status = await Utils.downloadJson("https://" + host + "/summary/status.json")
|
||||
const status = await Promise.any([
|
||||
// Utils.downloadJson("https://" + host + "/summary/status.json"),
|
||||
timeout(0)
|
||||
])
|
||||
return new Set<string>(status.layers)
|
||||
} catch (e) {
|
||||
console.error("Could not get MVT available layers due to", e)
|
||||
|
@ -39,6 +49,9 @@ async function main() {
|
|||
const state = new ThemeViewState(new LayoutConfig(<any> layout), availableLayers)
|
||||
const main = new SvelteUIElement(ThemeViewGUI, { state })
|
||||
main.AttachTo("maindiv")
|
||||
Array.from(document.getElementsByClassName("delete-on-load")).forEach(el => {
|
||||
el.parentElement.removeChild(el)
|
||||
})
|
||||
}
|
||||
}
|
||||
main()
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
import SvelteUIElement from "./UI/Base/SvelteUIElement"
|
||||
import Test from "./UI/Test.svelte"
|
||||
import MvtSource from "./Logic/FeatureSource/Sources/MvtSource"
|
||||
|
||||
new MvtSource("https://example.org", undefined, undefined, undefined)
|
||||
|
||||
new SvelteUIElement(Test, {}).AttachTo("maindiv")
|
||||
new SvelteUIElement(Test).AttachTo("maindiv")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue