forked from MapComplete/MapComplete
Chore: rework image uploading, should work better now
This commit is contained in:
parent
6f5b0622a5
commit
94ba18785d
17 changed files with 548 additions and 238 deletions
|
@ -344,8 +344,8 @@
|
||||||
},
|
},
|
||||||
"useSearch": "Use the search above to see presets",
|
"useSearch": "Use the search above to see presets",
|
||||||
"useSearchForMore": "Use the search function to search within {total} more values…",
|
"useSearchForMore": "Use the search function to search within {total} more values…",
|
||||||
"waitingForGeopermission": "Waiting for your permission to use the geolocation...",
|
"waitingForGeopermission": "Waiting for your permission to use the geolocation…",
|
||||||
"waitingForLocation": "Searching your current location...",
|
"waitingForLocation": "Searching your current location…",
|
||||||
"weekdays": {
|
"weekdays": {
|
||||||
"abbreviations": {
|
"abbreviations": {
|
||||||
"friday": "Fri",
|
"friday": "Fri",
|
||||||
|
@ -416,6 +416,22 @@
|
||||||
"pleaseLogin": "Please log in to add a picture",
|
"pleaseLogin": "Please log in to add a picture",
|
||||||
"respectPrivacy": "Do not photograph people nor license plates. Do not upload Google Maps, Google Streetview or other copyrighted sources.",
|
"respectPrivacy": "Do not photograph people nor license plates. Do not upload Google Maps, Google Streetview or other copyrighted sources.",
|
||||||
"toBig": "Your image is too large as it is {actual_size}. Please use images of at most {max_size}",
|
"toBig": "Your image is too large as it is {actual_size}. Please use images of at most {max_size}",
|
||||||
|
"upload": {
|
||||||
|
"failReasons": "You might have lost connection to the internet",
|
||||||
|
"failReasonsAdvanced": "Alternatively, make sure your browser and extensions do not block third-party API's.",
|
||||||
|
"multiple": {
|
||||||
|
"done": "{count} images are successfully uploaded. Thank you!",
|
||||||
|
"partiallyDone": "{count} images are getting uploaded, {done} images are done…",
|
||||||
|
"someFailed": "Sorry, we could not upload {count} images",
|
||||||
|
"uploading": "{count} images are getting uploaded…"
|
||||||
|
},
|
||||||
|
"one": {
|
||||||
|
"done": "Your image was successfully uploaded. Thank you!",
|
||||||
|
"failed": "Sorry, we could not upload your image",
|
||||||
|
"retrying": "Your image is getting uploaded again…",
|
||||||
|
"uploading": "Your image is getting uploaded…"
|
||||||
|
}
|
||||||
|
},
|
||||||
"uploadDone": "Your picture has been added. Thanks for helping out!",
|
"uploadDone": "Your picture has been added. Thanks for helping out!",
|
||||||
"uploadFailed": "Could not upload your picture. Are you connected to the Internet, and allow third party API's? The Brave browser or the uMatrix plugin might block them.",
|
"uploadFailed": "Could not upload your picture. Are you connected to the Internet, and allow third party API's? The Brave browser or the uMatrix plugin might block them.",
|
||||||
"uploadMultipleDone": "{count} pictures have been added. Thanks for helping out!",
|
"uploadMultipleDone": "{count} pictures have been added. Thanks for helping out!",
|
||||||
|
|
|
@ -2682,26 +2682,6 @@ a.link-underline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
@-webkit-keyframes spin {
|
|
||||||
to {
|
|
||||||
-webkit-transform: rotate(360deg);
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
-webkit-transform: rotate(360deg);
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.motion-reduce\:animate-spin {
|
|
||||||
-webkit-animation: spin 1s linear infinite;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.max-\[480px\]\:w-full {
|
.max-\[480px\]\:w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { OsmTags } from "../../../Models/OsmFeature"
|
||||||
*/
|
*/
|
||||||
export default class FeaturePropertiesStore {
|
export default class FeaturePropertiesStore {
|
||||||
private readonly _elements = new Map<string, UIEventSource<Record<string, string>>>()
|
private readonly _elements = new Map<string, UIEventSource<Record<string, string>>>()
|
||||||
|
public readonly aliases = new Map<string, string>()
|
||||||
constructor(...sources: FeatureSource[]) {
|
constructor(...sources: FeatureSource[]) {
|
||||||
for (const source of sources) {
|
for (const source of sources) {
|
||||||
this.trackFeatureSource(source)
|
this.trackFeatureSource(source)
|
||||||
|
@ -92,7 +92,6 @@ export default class FeaturePropertiesStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
|
||||||
public addAlias(oldId: string, newId: string): void {
|
public addAlias(oldId: string, newId: string): void {
|
||||||
if (newId === undefined) {
|
if (newId === undefined) {
|
||||||
// We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
|
// We removed the node/way/relation with type 'type' and id 'oldId' on openstreetmap!
|
||||||
|
@ -112,6 +111,7 @@ export default class FeaturePropertiesStore {
|
||||||
}
|
}
|
||||||
element.data.id = newId
|
element.data.id = newId
|
||||||
this._elements.set(newId, element)
|
this._elements.set(newId, element)
|
||||||
|
this.aliases.set(newId, oldId)
|
||||||
element.ping()
|
element.ping()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
import { ImageUploader } from "./ImageUploader";
|
||||||
|
import LinkImageAction from "../Osm/Actions/LinkImageAction";
|
||||||
|
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore";
|
||||||
|
import { OsmId, OsmTags } from "../../Models/OsmFeature";
|
||||||
|
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||||
|
import { Store, UIEventSource } from "../UIEventSource";
|
||||||
|
import { OsmConnection } from "../Osm/OsmConnection";
|
||||||
|
import { Changes } from "../Osm/Changes";
|
||||||
|
import Translations from "../../UI/i18n/Translations";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ImageUploadManager has a
|
||||||
|
*/
|
||||||
|
export class ImageUploadManager {
|
||||||
|
|
||||||
|
private readonly _uploader: ImageUploader;
|
||||||
|
private readonly _featureProperties: FeaturePropertiesStore;
|
||||||
|
private readonly _layout: LayoutConfig;
|
||||||
|
|
||||||
|
private readonly _uploadStarted: Map<string, UIEventSource<number>> = new Map();
|
||||||
|
private readonly _uploadFinished: Map<string, UIEventSource<number>> = new Map();
|
||||||
|
private readonly _uploadFailed: Map<string, UIEventSource<number>> = new Map();
|
||||||
|
private readonly _uploadRetried: Map<string, UIEventSource<number>> = new Map();
|
||||||
|
private readonly _uploadRetriedSuccess: Map<string, UIEventSource<number>> = new Map();
|
||||||
|
private readonly _osmConnection: OsmConnection;
|
||||||
|
private readonly _changes: Changes;
|
||||||
|
|
||||||
|
constructor(layout: LayoutConfig, uploader: ImageUploader, featureProperties: FeaturePropertiesStore, osmConnection: OsmConnection, changes: Changes) {
|
||||||
|
this._uploader = uploader;
|
||||||
|
this._featureProperties = featureProperties;
|
||||||
|
this._layout = layout;
|
||||||
|
this._osmConnection = osmConnection;
|
||||||
|
this._changes = changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets various counters.
|
||||||
|
* Note that counters can only increase
|
||||||
|
* If a retry was a success, both 'retrySuccess' _and_ 'uploadFinished' will be increased
|
||||||
|
* @param featureId: the id of the feature you want information for. '*' has a global counter
|
||||||
|
*/
|
||||||
|
public getCountsFor(featureId: string | "*"): {
|
||||||
|
retried: Store<number>;
|
||||||
|
uploadStarted: Store<number>;
|
||||||
|
retrySuccess: Store<number>;
|
||||||
|
failed: Store<number>;
|
||||||
|
uploadFinished: Store<number>
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
uploadStarted: this.getCounterFor(this._uploadStarted, featureId),
|
||||||
|
uploadFinished: this.getCounterFor(this._uploadFinished, featureId),
|
||||||
|
retried: this.getCounterFor(this._uploadRetried, featureId),
|
||||||
|
failed: this.getCounterFor(this._uploadFailed, featureId),
|
||||||
|
retrySuccess: this.getCounterFor(this._uploadRetriedSuccess, featureId)
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads the given image, applies the correct title and license for the known user
|
||||||
|
*/
|
||||||
|
public async uploadImageAndApply(file: File, tags: OsmTags) {
|
||||||
|
|
||||||
|
const sizeInBytes = file.size
|
||||||
|
const featureId = <OsmId> tags.id
|
||||||
|
console.log(file.name + " has a size of " + sizeInBytes + " Bytes, attaching to", tags.id)
|
||||||
|
const self = this
|
||||||
|
if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) {
|
||||||
|
this.increaseCountFor(this._uploadStarted, featureId)
|
||||||
|
this.increaseCountFor(this._uploadFailed, featureId)
|
||||||
|
throw(
|
||||||
|
Translations.t.image.toBig.Subs({
|
||||||
|
actual_size: Math.floor(sizeInBytes / 1000000) + "MB",
|
||||||
|
max_size: self._uploader.maxFileSizeInMegabytes + "MB",
|
||||||
|
}).txt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const licenseStore = this._osmConnection?.GetPreference("pictures-license", "CC0");
|
||||||
|
const license = licenseStore?.data ?? "CC0";
|
||||||
|
|
||||||
|
const matchingLayer = this._layout?.getMatchingLayer(tags);
|
||||||
|
|
||||||
|
const title =
|
||||||
|
matchingLayer?.title?.GetRenderValue(tags)?.Subs(tags)?.textFor("en") ??
|
||||||
|
tags.name ??
|
||||||
|
"https//osm.org/" + tags.id;
|
||||||
|
const description = [
|
||||||
|
"author:" + this._osmConnection.userDetails.data.name,
|
||||||
|
"license:" + license,
|
||||||
|
"osmid:" + tags.id
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
console.log("Upload done, creating ")
|
||||||
|
const action = await this.uploadImageWithLicense(featureId, title, description, file);
|
||||||
|
await this._changes.applyAction(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async uploadImageWithLicense(
|
||||||
|
featureId: OsmId,
|
||||||
|
title: string, description: string, blob: File
|
||||||
|
): Promise<LinkImageAction> {
|
||||||
|
this.increaseCountFor(this._uploadStarted, featureId);
|
||||||
|
const properties = this._featureProperties.getStore(featureId);
|
||||||
|
let key: string;
|
||||||
|
let value: string;
|
||||||
|
try {
|
||||||
|
({ key, value } = await this._uploader.uploadImage(title, description, blob));
|
||||||
|
} catch (e) {
|
||||||
|
this.increaseCountFor(this._uploadRetried, featureId);
|
||||||
|
console.error("Could not upload image, trying again:", e);
|
||||||
|
try {
|
||||||
|
|
||||||
|
({ key, value } = await this._uploader.uploadImage(title, description, blob));
|
||||||
|
this.increaseCountFor(this._uploadRetriedSuccess, featureId);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could again not upload image due to", e);
|
||||||
|
this.increaseCountFor(this._uploadFailed, featureId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
console.log("Uploading done, creating action for", featureId)
|
||||||
|
const action = new LinkImageAction(featureId, key, value, properties, {
|
||||||
|
theme: this._layout.id,
|
||||||
|
changeType: "add-image"
|
||||||
|
});
|
||||||
|
this.increaseCountFor(this._uploadFinished, featureId);
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCounterFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
|
||||||
|
if (this._featureProperties.aliases.has(key)) {
|
||||||
|
key = this._featureProperties.aliases.get(key);
|
||||||
|
}
|
||||||
|
if (!collection.has(key)) {
|
||||||
|
collection.set(key, new UIEventSource<number>(0));
|
||||||
|
}
|
||||||
|
return collection.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private increaseCountFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
|
||||||
|
const counter = this.getCounterFor(collection, key);
|
||||||
|
counter.setData(counter.data + 1);
|
||||||
|
const global = this.getCounterFor(collection, "*");
|
||||||
|
global.setData(counter.data + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
export interface ImageUploader {
|
||||||
|
maxFileSizeInMegabytes?: number;
|
||||||
|
/**
|
||||||
|
* Uploads the 'blob' as image, with some metadata.
|
||||||
|
* Returns the URL to be linked + the appropriate key to add this to OSM
|
||||||
|
* @param title
|
||||||
|
* @param description
|
||||||
|
* @param blob
|
||||||
|
*/
|
||||||
|
uploadImage(
|
||||||
|
title: string,
|
||||||
|
description: string,
|
||||||
|
blob: File
|
||||||
|
): Promise<{ key: string, value: string }>;
|
||||||
|
}
|
|
@ -1,60 +1,30 @@
|
||||||
import ImageProvider, { ProvidedImage } from "./ImageProvider"
|
import ImageProvider, { ProvidedImage } from "./ImageProvider";
|
||||||
import BaseUIElement from "../../UI/BaseUIElement"
|
import BaseUIElement from "../../UI/BaseUIElement";
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils";
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants";
|
||||||
import { LicenseInfo } from "./LicenseInfo"
|
import { LicenseInfo } from "./LicenseInfo";
|
||||||
|
import { ImageUploader } from "./ImageUploader";
|
||||||
|
|
||||||
export class Imgur extends ImageProvider {
|
export class Imgur extends ImageProvider implements ImageUploader{
|
||||||
public static readonly defaultValuePrefix = ["https://i.imgur.com"]
|
public static readonly defaultValuePrefix = ["https://i.imgur.com"]
|
||||||
public static readonly singleton = new Imgur()
|
public static readonly singleton = new Imgur()
|
||||||
public readonly defaultKeyPrefixes: string[] = ["image"]
|
public readonly defaultKeyPrefixes: string[] = ["image"]
|
||||||
|
public readonly maxFileSizeInMegabytes = 10
|
||||||
private constructor() {
|
private constructor() {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
static uploadMultiple(
|
/**
|
||||||
|
* Uploads an image, returns the URL where to find the image
|
||||||
|
* @param title
|
||||||
|
* @param description
|
||||||
|
* @param blob
|
||||||
|
*/
|
||||||
|
public async uploadImage(
|
||||||
title: string,
|
title: string,
|
||||||
description: string,
|
description: string,
|
||||||
blobs: FileList,
|
blob: File
|
||||||
handleSuccessfullUpload: (imageURL: string) => Promise<void>,
|
): Promise<{ key: string, value: string }> {
|
||||||
allDone: () => void,
|
|
||||||
onFail: (reason: string) => void,
|
|
||||||
offset: number = 0
|
|
||||||
) {
|
|
||||||
if (blobs.length == offset) {
|
|
||||||
allDone()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const blob = blobs.item(offset)
|
|
||||||
const self = this
|
|
||||||
this.uploadImage(
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
blob,
|
|
||||||
async (imageUrl) => {
|
|
||||||
await handleSuccessfullUpload(imageUrl)
|
|
||||||
self.uploadMultiple(
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
blobs,
|
|
||||||
handleSuccessfullUpload,
|
|
||||||
allDone,
|
|
||||||
onFail,
|
|
||||||
offset + 1
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onFail
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
static uploadImage(
|
|
||||||
title: string,
|
|
||||||
description: string,
|
|
||||||
blob: File,
|
|
||||||
handleSuccessfullUpload: (imageURL: string) => Promise<void>,
|
|
||||||
onFail: (reason: string) => void
|
|
||||||
) {
|
|
||||||
const apiUrl = "https://api.imgur.com/3/image"
|
const apiUrl = "https://api.imgur.com/3/image"
|
||||||
const apiKey = Constants.ImgurApiKey
|
const apiKey = Constants.ImgurApiKey
|
||||||
|
|
||||||
|
@ -63,6 +33,7 @@ export class Imgur extends ImageProvider {
|
||||||
formData.append("title", title)
|
formData.append("title", title)
|
||||||
formData.append("description", description)
|
formData.append("description", description)
|
||||||
|
|
||||||
|
|
||||||
const settings: RequestInit = {
|
const settings: RequestInit = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
|
@ -74,17 +45,9 @@ export class Imgur extends ImageProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response contains stringified JSON
|
// Response contains stringified JSON
|
||||||
// Image URL available at response.data.link
|
const response = await fetch(apiUrl, settings)
|
||||||
fetch(apiUrl, settings)
|
const content = await response.json()
|
||||||
.then(async function (response) {
|
return { key: "image", value: content.data.link }
|
||||||
const content = await response.json()
|
|
||||||
await handleSuccessfullUpload(content.data.link)
|
|
||||||
})
|
|
||||||
.catch((reason) => {
|
|
||||||
console.log("Uploading to IMGUR failed", reason)
|
|
||||||
// @ts-ignore
|
|
||||||
onFail(reason)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SourceIcon(): BaseUIElement {
|
SourceIcon(): BaseUIElement {
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
import ChangeTagAction from "./ChangeTagAction"
|
import ChangeTagAction from "./ChangeTagAction";
|
||||||
import { Tag } from "../../Tags/Tag"
|
import { Tag } from "../../Tags/Tag";
|
||||||
|
import OsmChangeAction from "./OsmChangeAction";
|
||||||
|
import { Changes } from "../Changes";
|
||||||
|
import { ChangeDescription } from "./ChangeDescription";
|
||||||
|
import { Store } from "../../UIEventSource";
|
||||||
|
|
||||||
|
export default class LinkImageAction extends OsmChangeAction {
|
||||||
|
private readonly _proposedKey: "image" | "mapillary" | "wiki_commons" | string;
|
||||||
|
private readonly _url: string;
|
||||||
|
private readonly _currentTags: Store<Record<string, string>>;
|
||||||
|
private readonly _meta: { theme: string; changeType: "add-image" | "link-image" };
|
||||||
|
|
||||||
export default class LinkPicture extends ChangeTagAction {
|
|
||||||
/**
|
/**
|
||||||
* Adds a link to an image
|
* Adds an image-link to a feature
|
||||||
* @param elementId
|
* @param elementId
|
||||||
* @param proposedKey: a key which might be used, typically `image`. If the key is already used with a different URL, `key+":0"` will be used instead (or a higher number if needed)
|
* @param proposedKey a key which might be used, typically `image`. If the key is already used with a different URL, `key+":0"` will be used instead (or a higher number if needed)
|
||||||
* @param url
|
* @param url
|
||||||
* @param currentTags
|
* @param currentTags
|
||||||
* @param meta
|
* @param meta
|
||||||
|
@ -15,18 +24,31 @@ export default class LinkPicture extends ChangeTagAction {
|
||||||
elementId: string,
|
elementId: string,
|
||||||
proposedKey: "image" | "mapillary" | "wiki_commons" | string,
|
proposedKey: "image" | "mapillary" | "wiki_commons" | string,
|
||||||
url: string,
|
url: string,
|
||||||
currentTags: Record<string, string>,
|
currentTags: Store<Record<string, string>>,
|
||||||
meta: {
|
meta: {
|
||||||
theme: string
|
theme: string
|
||||||
changeType: "add-image" | "link-image"
|
changeType: "add-image" | "link-image"
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
let key = proposedKey
|
super(elementId, true)
|
||||||
|
this._proposedKey = proposedKey;
|
||||||
|
this._url = url;
|
||||||
|
this._currentTags = currentTags;
|
||||||
|
this._meta = meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected CreateChangeDescriptions(): Promise<ChangeDescription[]> {
|
||||||
|
let key = this._proposedKey
|
||||||
let i = 0
|
let i = 0
|
||||||
|
const currentTags = this._currentTags.data
|
||||||
|
const url = this._url
|
||||||
while (currentTags[key] !== undefined && currentTags[key] !== url) {
|
while (currentTags[key] !== undefined && currentTags[key] !== url) {
|
||||||
key = proposedKey + ":" + i
|
key = this._proposedKey + ":" + i
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
super(elementId, new Tag(key, url), currentTags, meta)
|
const tagChangeAction = new ChangeTagAction ( this.mainObjectId, new Tag(key, url), currentTags, this._meta)
|
||||||
|
return tagChangeAction.CreateChangeDescriptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,9 @@ export default abstract class OsmChangeAction {
|
||||||
constructor(mainObjectId: string, trackStatistics: boolean = true) {
|
constructor(mainObjectId: string, trackStatistics: boolean = true) {
|
||||||
this.trackStatistics = trackStatistics
|
this.trackStatistics = trackStatistics
|
||||||
this.mainObjectId = mainObjectId
|
this.mainObjectId = mainObjectId
|
||||||
|
if(mainObjectId === undefined || mainObjectId === null){
|
||||||
|
throw "OsmObject received '"+mainObjectId+"' as mainObjectId"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Perform(changes: Changes) {
|
public async Perform(changes: Changes) {
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||||
import { OsmConnection } from "../Osm/OsmConnection"
|
import { OsmConnection } from "../Osm/OsmConnection";
|
||||||
import { MangroveIdentity } from "../Web/MangroveReviews"
|
import { MangroveIdentity } from "../Web/MangroveReviews";
|
||||||
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
import { Store, Stores, UIEventSource } from "../UIEventSource";
|
||||||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
|
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource";
|
||||||
import { FeatureSource } from "../FeatureSource/FeatureSource"
|
import { FeatureSource } from "../FeatureSource/FeatureSource";
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson";
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../Utils";
|
||||||
import translators from "../../assets/translators.json"
|
import translators from "../../assets/translators.json";
|
||||||
import codeContributors from "../../assets/contributors.json"
|
import codeContributors from "../../assets/contributors.json";
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson";
|
||||||
import usersettings from "../../../src/assets/generated/layers/usersettings.json"
|
import usersettings from "../../../src/assets/generated/layers/usersettings.json";
|
||||||
import Locale from "../../UI/i18n/Locale"
|
import Locale from "../../UI/i18n/Locale";
|
||||||
import LinkToWeblate from "../../UI/Base/LinkToWeblate"
|
import LinkToWeblate from "../../UI/Base/LinkToWeblate";
|
||||||
import FeatureSwitchState from "./FeatureSwitchState"
|
import FeatureSwitchState from "./FeatureSwitchState";
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants";
|
||||||
import { QueryParameters } from "../Web/QueryParameters"
|
import { QueryParameters } from "../Web/QueryParameters";
|
||||||
import { ThemeMetaTagging } from "./UserSettingsMetaTagging"
|
import { ThemeMetaTagging } from "./UserSettingsMetaTagging";
|
||||||
import { MapProperties } from "../../Models/MapProperties";
|
import { MapProperties } from "../../Models/MapProperties";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,7 +43,7 @@ export default class UserRelatedState {
|
||||||
public readonly homeLocation: FeatureSource
|
public readonly homeLocation: FeatureSource
|
||||||
public readonly language: UIEventSource<string>
|
public readonly language: UIEventSource<string>
|
||||||
public readonly preferredBackgroundLayer: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined>
|
public readonly preferredBackgroundLayer: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined>
|
||||||
public readonly preferredBackgroundLayerForTheme: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined>
|
public readonly imageLicense : UIEventSource<string>
|
||||||
/**
|
/**
|
||||||
* The number of seconds that the GPS-locations are stored in memory.
|
* The number of seconds that the GPS-locations are stored in memory.
|
||||||
* Time in seconds
|
* Time in seconds
|
||||||
|
@ -108,6 +108,9 @@ export default class UserRelatedState {
|
||||||
documentation: "The ID of a layer or layer category that MapComplete uses by default"
|
documentation: "The ID of a layer or layer category that MapComplete uses by default"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0", {
|
||||||
|
documentation: "The license under which new images are uploaded"
|
||||||
|
})
|
||||||
this.installedUserThemes = this.InitInstalledUserThemes()
|
this.installedUserThemes = this.InitInstalledUserThemes()
|
||||||
|
|
||||||
this.homeLocation = this.initHomeLocation()
|
this.homeLocation = this.initHomeLocation()
|
||||||
|
|
|
@ -51,6 +51,8 @@ import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor";
|
||||||
import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector";
|
import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector";
|
||||||
import FilteredLayer from "./FilteredLayer";
|
import FilteredLayer from "./FilteredLayer";
|
||||||
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector";
|
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector";
|
||||||
|
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager";
|
||||||
|
import { Imgur } from "../Logic/ImageProviders/Imgur";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -99,6 +101,8 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
readonly userRelatedState: UserRelatedState;
|
readonly userRelatedState: UserRelatedState;
|
||||||
readonly geolocation: GeoLocationHandler;
|
readonly geolocation: GeoLocationHandler;
|
||||||
|
|
||||||
|
readonly imageUploadManager: ImageUploadManager
|
||||||
|
|
||||||
readonly lastClickObject: WritableFeatureSource;
|
readonly lastClickObject: WritableFeatureSource;
|
||||||
readonly overlayLayerStates: ReadonlyMap<
|
readonly overlayLayerStates: ReadonlyMap<
|
||||||
string,
|
string,
|
||||||
|
@ -168,6 +172,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
|
|
||||||
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location);
|
this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location);
|
||||||
|
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id);
|
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id);
|
||||||
|
|
||||||
|
@ -323,6 +328,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
this.perLayerFiltered = this.showNormalDataOn(this.map);
|
this.perLayerFiltered = this.showNormalDataOn(this.map);
|
||||||
|
|
||||||
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView;
|
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView;
|
||||||
|
this.imageUploadManager = new ImageUploadManager(layout, Imgur.singleton, this.featureProperties, this.osmConnection, this.changes)
|
||||||
|
|
||||||
this.initActors();
|
this.initActors();
|
||||||
this.addLastClick(lastClick);
|
this.addLastClick(lastClick);
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export let accept: string;
|
||||||
|
export let multiple: boolean = true;
|
||||||
|
|
||||||
|
const dispatcher = createEventDispatcher<{ submit: FileList }>();
|
||||||
|
export let cls: string = "";
|
||||||
|
let drawAttention = false;
|
||||||
|
let inputElement: HTMLInputElement;
|
||||||
|
let id = Math.random() * 1000000000 + "";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<label class={twMerge(cls, drawAttention ? "glowing-shadow" : "")} for={"fileinput"+id}>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
</label>
|
||||||
|
<input {accept} bind:this={inputElement} class="hidden" id={"fileinput" + id} {multiple} name="file-input"
|
||||||
|
on:change|preventDefault={() => {
|
||||||
|
drawAttention = false;
|
||||||
|
dispatcher("submit", inputElement.files)}}
|
||||||
|
|
||||||
|
on:dragend={ () => {drawAttention = false}}
|
||||||
|
on:dragover|preventDefault|stopPropagation={(e) => {
|
||||||
|
console.log("Dragging over!")
|
||||||
|
drawAttention = true
|
||||||
|
e.dataTransfer.drop = "copy"
|
||||||
|
}}
|
||||||
|
on:dragstart={ () => {drawAttention = false}}
|
||||||
|
on:drop|preventDefault|stopPropagation={(e) => {
|
||||||
|
console.log("Got a 'drop'")
|
||||||
|
drawAttention = false
|
||||||
|
dispatcher("submit", e.dataTransfer.files)
|
||||||
|
}}
|
||||||
|
type="file"
|
||||||
|
>
|
||||||
|
</form>
|
|
@ -1,9 +1,12 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import ToSvelte from "./ToSvelte.svelte"
|
import ToSvelte from "./ToSvelte.svelte";
|
||||||
import Svg from "../../Svg"
|
import Svg from "../../Svg";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export let cls : string = undefined
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex p-1 pl-2">
|
<div class={twMerge( "flex p-1 pl-2", cls)}>
|
||||||
<div class="min-w-6 h-6 w-6 animate-spin self-center">
|
<div class="min-w-6 h-6 w-6 animate-spin self-center">
|
||||||
<ToSvelte construct={Svg.loading_svg()} />
|
<ToSvelte construct={Svg.loading_svg()} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,8 +15,9 @@ import Loading from "../Base/Loading"
|
||||||
import { LoginToggle } from "../Popup/LoginButton"
|
import { LoginToggle } from "../Popup/LoginButton"
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../Models/Constants"
|
||||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||||
|
import exp from "constants";
|
||||||
|
|
||||||
export class ImageUploadFlow extends Toggle {
|
export class ImageUploadFlow extends Combine {
|
||||||
private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>()
|
private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>()
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -129,7 +130,7 @@ export class ImageUploadFlow extends Toggle {
|
||||||
uploader.uploadMany(title, description, filelist)
|
uploader.uploadMany(title, description, filelist)
|
||||||
})
|
})
|
||||||
|
|
||||||
const uploadFlow: BaseUIElement = new Combine([
|
super([
|
||||||
new VariableUiElement(
|
new VariableUiElement(
|
||||||
uploader.queue
|
uploader.queue
|
||||||
.map((q) => q.length)
|
.map((q) => q.length)
|
||||||
|
@ -183,17 +184,9 @@ export class ImageUploadFlow extends Toggle {
|
||||||
})
|
})
|
||||||
.SetClass("underline"),
|
.SetClass("underline"),
|
||||||
]).SetStyle("font-size:small;"),
|
]).SetStyle("font-size:small;"),
|
||||||
]).SetClass("flex flex-col image-upload-flow mt-4 mb-8 text-center leading-none")
|
])
|
||||||
|
this.SetClass("flex flex-col image-upload-flow mt-4 mb-8 text-center leading-none")
|
||||||
|
|
||||||
|
|
||||||
super(
|
|
||||||
new LoginToggle(
|
|
||||||
/*We can show the actual upload button!*/
|
|
||||||
uploadFlow,
|
|
||||||
/* User not logged in*/ t.pleaseLogin.Clone(),
|
|
||||||
state
|
|
||||||
),
|
|
||||||
undefined /* Nothing as the user badge is disabled*/,
|
|
||||||
state?.featureSwitchUserbadge
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
<script lang="ts">/**
|
||||||
|
* Shows an 'upload'-button which will start the upload for this feature
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||||
|
import { Store } from "../../Logic/UIEventSource";
|
||||||
|
import type { OsmTags } from "../../Models/OsmFeature";
|
||||||
|
import LoginToggle from "../Base/LoginToggle.svelte";
|
||||||
|
import Translations from "../i18n/Translations";
|
||||||
|
import Tr from "../Base/Tr.svelte";
|
||||||
|
import UploadingImageCounter from "./UploadingImageCounter.svelte";
|
||||||
|
import FileSelector from "../Base/FileSelector.svelte";
|
||||||
|
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||||
|
import Svg from "../../Svg";
|
||||||
|
|
||||||
|
export let state: SpecialVisualizationState;
|
||||||
|
|
||||||
|
export let tags: Store<OsmTags>;
|
||||||
|
export let image: string = undefined;
|
||||||
|
if (image === "") {
|
||||||
|
image = undefined;
|
||||||
|
}
|
||||||
|
export let labelText: string = undefined;
|
||||||
|
const t = Translations.t.image;
|
||||||
|
|
||||||
|
let licenseStore = state.userRelatedState.imageLicense;
|
||||||
|
|
||||||
|
function handleFiles(files: FileList) {
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files.item(i);
|
||||||
|
console.log("Got file", file.name)
|
||||||
|
try {
|
||||||
|
state.imageUploadManager.uploadImageAndApply(file, tags.data);
|
||||||
|
} catch (e) {
|
||||||
|
alert(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<LoginToggle {state}>
|
||||||
|
|
||||||
|
<Tr slot="not-logged-in" t={t.pleaseLogin} />
|
||||||
|
<div class="flex flex-col">
|
||||||
|
|
||||||
|
<UploadingImageCounter {state} {tags} />
|
||||||
|
<FileSelector accept="image/*" cls="button border-2 text-2xl" multiple={true}
|
||||||
|
on:submit={e => handleFiles(e.detail)}>
|
||||||
|
<div class="flex items-center">
|
||||||
|
|
||||||
|
{#if image !== undefined}
|
||||||
|
<img src={image} />
|
||||||
|
{:else}
|
||||||
|
<ToSvelte construct={ Svg.camera_plus_svg().SetClass("block w-12 h-12 p-1 text-4xl ")} />
|
||||||
|
{/if}
|
||||||
|
{#if labelText}
|
||||||
|
{labelText}
|
||||||
|
{:else}
|
||||||
|
<Tr t={t.addPicture} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</FileSelector>
|
||||||
|
<div class="text-sm">
|
||||||
|
<Tr t={t.respectPrivacy} />
|
||||||
|
<a class="cursor-pointer" on:click={() => {state.guistate.openUsersettings("picture-license")}}>
|
||||||
|
<Tr t={t.currentLicense.Subs({license: $licenseStore})} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</LoginToggle>
|
|
@ -1,31 +1,67 @@
|
||||||
<script lang="ts">/**
|
<script lang="ts">/**
|
||||||
* Shows an 'upload'-button which will start the upload for this feature
|
* Shows information about how much images are uploaded for the given feature
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||||
import type { Feature } from "geojson";
|
|
||||||
import { Store } from "../../Logic/UIEventSource";
|
import { Store } from "../../Logic/UIEventSource";
|
||||||
import type { OsmTags } from "../../Models/OsmFeature";
|
import type { OsmTags } from "../../Models/OsmFeature";
|
||||||
import { ImageUploader } from "../../Logic/ImageProviders/ImageUploader";
|
|
||||||
import LoginToggle from "../Base/LoginToggle.svelte";
|
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import Tr from "../Base/Tr.svelte";
|
import Tr from "../Base/Tr.svelte";
|
||||||
import { ImageUploadManager } from "../../Logic/ImageProviders/ImageUploadManager";
|
import Loading from "../Base/Loading.svelte";
|
||||||
|
|
||||||
export let state: SpecialVisualizationState;
|
export let state: SpecialVisualizationState;
|
||||||
export let feature: Feature;
|
|
||||||
|
|
||||||
export let tags: Store<OsmTags>;
|
export let tags: Store<OsmTags>;
|
||||||
export let state: SpecialVisualizationState;
|
const featureId = tags.data.id;
|
||||||
export let lon: number;
|
const {
|
||||||
export let lat: number;
|
uploadStarted,
|
||||||
const t = Translations.t.image
|
uploadFinished,
|
||||||
|
retried,
|
||||||
|
failed
|
||||||
|
} = state.imageUploadManager.getCountsFor(featureId);
|
||||||
|
const t = Translations.t.image;
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if $uploadStarted == 1}
|
||||||
|
{#if $uploadFinished == 1 }
|
||||||
|
<Tr cls="thanks" t={t.upload.one.done} />
|
||||||
|
{:else if $failed == 1}
|
||||||
|
<div class="flex flex-col alert">
|
||||||
|
<Tr cls="self-center" t={t.upload.one.failed} />
|
||||||
|
<Tr t={t.upload.failReasons} />
|
||||||
|
<Tr t={t.upload.failReasonsAdvanced} />
|
||||||
|
</div>
|
||||||
|
{:else if $retried == 1}
|
||||||
|
<Loading cls="alert">
|
||||||
|
<Tr t={t.upload.one.retrying} />
|
||||||
|
</Loading>
|
||||||
|
{:else }
|
||||||
|
<Loading cls="alert">
|
||||||
|
<Tr t={t.upload.one.uploading} />
|
||||||
|
</Loading>
|
||||||
|
{/if}
|
||||||
|
{:else if $uploadStarted > 1}
|
||||||
|
{#if ($uploadFinished + $failed) == $uploadStarted && $uploadFinished > 0}
|
||||||
|
<Tr cls="thanks" t={t.upload.multiple.done.Subs({count: $uploadFinished})} />
|
||||||
|
{:else if $uploadFinished == 0}
|
||||||
|
<Loading cls="alert">
|
||||||
|
<Tr t={t.upload.multiple.uploading.Subs({count: $uploadStarted})} />
|
||||||
|
</Loading>
|
||||||
|
{:else if $uploadFinished > 0}
|
||||||
|
<Loading cls="alert">
|
||||||
|
<Tr t={t.upload.multiple.partiallyDone.Subs({count: $uploadStarted - $uploadFinished, done: $uploadFinished})} />
|
||||||
|
</Loading>
|
||||||
|
{/if}
|
||||||
|
{#if $failed > 0}
|
||||||
|
<div class="flex flex-col alert">
|
||||||
|
{#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})} />
|
||||||
|
|
||||||
<LoginToggle>
|
{/if}
|
||||||
|
<Tr t={t.upload.failReasons} />
|
||||||
<Tr slot="not-logged-in" t={t.pleaseLogin}/>
|
<Tr t={t.upload.failReasonsAdvanced} />
|
||||||
|
</div>
|
||||||
</LoginToggle>
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
|
@ -1,113 +1,117 @@
|
||||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
import { Store, UIEventSource } from "../Logic/UIEventSource";
|
||||||
import BaseUIElement from "./BaseUIElement"
|
import BaseUIElement from "./BaseUIElement";
|
||||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
||||||
import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
|
import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource";
|
||||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
import { OsmConnection } from "../Logic/Osm/OsmConnection";
|
||||||
import { Changes } from "../Logic/Osm/Changes"
|
import { Changes } from "../Logic/Osm/Changes";
|
||||||
import { ExportableMap, MapProperties } from "../Models/MapProperties"
|
import { ExportableMap, MapProperties } from "../Models/MapProperties";
|
||||||
import LayerState from "../Logic/State/LayerState"
|
import LayerState from "../Logic/State/LayerState";
|
||||||
import { Feature, Geometry, Point } from "geojson"
|
import { Feature, Geometry, Point } from "geojson";
|
||||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||||
import { MangroveIdentity } from "../Logic/Web/MangroveReviews"
|
import { MangroveIdentity } from "../Logic/Web/MangroveReviews";
|
||||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore";
|
||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
|
||||||
import { MenuState } from "../Models/MenuState"
|
import { MenuState } from "../Models/MenuState";
|
||||||
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
|
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader";
|
||||||
import { RasterLayerPolygon } from "../Models/RasterLayers"
|
import { RasterLayerPolygon } from "../Models/RasterLayers";
|
||||||
|
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The state needed to render a special Visualisation.
|
* The state needed to render a special Visualisation.
|
||||||
*/
|
*/
|
||||||
export interface SpecialVisualizationState {
|
export interface SpecialVisualizationState {
|
||||||
readonly guistate: MenuState
|
readonly guistate: MenuState;
|
||||||
readonly layout: LayoutConfig
|
readonly layout: LayoutConfig;
|
||||||
readonly featureSwitches: FeatureSwitchState
|
readonly featureSwitches: FeatureSwitchState;
|
||||||
|
|
||||||
readonly layerState: LayerState
|
readonly layerState: LayerState;
|
||||||
readonly featureProperties: { getStore(id: string): UIEventSource<Record<string, string>> }
|
readonly featureProperties: { getStore(id: string): UIEventSource<Record<string, string>> };
|
||||||
|
|
||||||
readonly indexedFeatures: IndexedFeatureSource
|
readonly indexedFeatures: IndexedFeatureSource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Some features will create a new element that should be displayed.
|
* Some features will create a new element that should be displayed.
|
||||||
* These can be injected by appending them to this featuresource (and pinging it)
|
* These can be injected by appending them to this featuresource (and pinging it)
|
||||||
*/
|
*/
|
||||||
readonly newFeatures: WritableFeatureSource
|
readonly newFeatures: WritableFeatureSource;
|
||||||
|
|
||||||
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>
|
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>;
|
||||||
|
|
||||||
readonly osmConnection: OsmConnection
|
readonly osmConnection: OsmConnection;
|
||||||
readonly featureSwitchUserbadge: Store<boolean>
|
readonly featureSwitchUserbadge: Store<boolean>;
|
||||||
readonly featureSwitchIsTesting: Store<boolean>
|
readonly featureSwitchIsTesting: Store<boolean>;
|
||||||
readonly changes: Changes
|
readonly changes: Changes;
|
||||||
readonly osmObjectDownloader: OsmObjectDownloader
|
readonly osmObjectDownloader: OsmObjectDownloader;
|
||||||
/**
|
/**
|
||||||
* State of the main map
|
* State of the main map
|
||||||
*/
|
*/
|
||||||
readonly mapProperties: MapProperties & ExportableMap
|
readonly mapProperties: MapProperties & ExportableMap;
|
||||||
|
|
||||||
readonly selectedElement: UIEventSource<Feature>
|
readonly selectedElement: UIEventSource<Feature>;
|
||||||
/**
|
/**
|
||||||
* Works together with 'selectedElement' to indicate what properties should be displayed
|
* Works together with 'selectedElement' to indicate what properties should be displayed
|
||||||
*/
|
*/
|
||||||
readonly selectedLayer: UIEventSource<LayerConfig>
|
readonly selectedLayer: UIEventSource<LayerConfig>;
|
||||||
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>
|
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If data is currently being fetched from external sources
|
* If data is currently being fetched from external sources
|
||||||
*/
|
*/
|
||||||
readonly dataIsLoading: Store<boolean>
|
readonly dataIsLoading: Store<boolean>;
|
||||||
/**
|
/**
|
||||||
* Only needed for 'ReplaceGeometryAction'
|
* Only needed for 'ReplaceGeometryAction'
|
||||||
*/
|
*/
|
||||||
readonly fullNodeDatabase?: FullNodeDatabaseSource
|
readonly fullNodeDatabase?: FullNodeDatabaseSource;
|
||||||
|
|
||||||
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>;
|
||||||
readonly userRelatedState: {
|
readonly userRelatedState: {
|
||||||
readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full">
|
readonly imageLicense: UIEventSource<string>;
|
||||||
readonly mangroveIdentity: MangroveIdentity
|
readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full">
|
||||||
readonly showAllQuestionsAtOnce: UIEventSource<boolean>
|
readonly mangroveIdentity: MangroveIdentity
|
||||||
readonly preferencesAsTags: Store<Record<string, string>>
|
readonly showAllQuestionsAtOnce: UIEventSource<boolean>
|
||||||
readonly language: UIEventSource<string>
|
readonly preferencesAsTags: Store<Record<string, string>>
|
||||||
}
|
readonly language: UIEventSource<string>
|
||||||
readonly lastClickObject: WritableFeatureSource
|
};
|
||||||
|
readonly lastClickObject: WritableFeatureSource;
|
||||||
|
|
||||||
readonly availableLayers: Store<RasterLayerPolygon[]>
|
readonly availableLayers: Store<RasterLayerPolygon[]>;
|
||||||
|
|
||||||
|
readonly imageUploadManager: ImageUploadManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpecialVisualization {
|
export interface SpecialVisualization {
|
||||||
readonly funcName: string
|
readonly funcName: string;
|
||||||
readonly docs: string | BaseUIElement
|
readonly docs: string | BaseUIElement;
|
||||||
readonly example?: string
|
readonly example?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included
|
* Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included
|
||||||
*/
|
*/
|
||||||
readonly needsNodeDatabase?: boolean
|
readonly needsNodeDatabase?: boolean;
|
||||||
readonly args: {
|
readonly args: {
|
||||||
name: string
|
name: string
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
doc: string
|
doc: string
|
||||||
required?: false | boolean
|
required?: false | boolean
|
||||||
}[]
|
}[];
|
||||||
readonly getLayerDependencies?: (argument: string[]) => string[]
|
readonly getLayerDependencies?: (argument: string[]) => string[];
|
||||||
|
|
||||||
structuredExamples?(): { feature: Feature<Geometry, Record<string, string>>; args: string[] }[]
|
structuredExamples?(): { feature: Feature<Geometry, Record<string, string>>; args: string[] }[];
|
||||||
|
|
||||||
constr(
|
constr(
|
||||||
state: SpecialVisualizationState,
|
state: SpecialVisualizationState,
|
||||||
tagSource: UIEventSource<Record<string, string>>,
|
tagSource: UIEventSource<Record<string, string>>,
|
||||||
argument: string[],
|
argument: string[],
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
layer: LayerConfig
|
layer: LayerConfig
|
||||||
): BaseUIElement
|
): BaseUIElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RenderingSpecification =
|
export type RenderingSpecification =
|
||||||
| string
|
| string
|
||||||
| {
|
| {
|
||||||
func: SpecialVisualization
|
func: SpecialVisualization
|
||||||
args: string[]
|
args: string[]
|
||||||
style: string
|
style: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,6 @@ import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"
|
||||||
import { SubtleButton } from "./Base/SubtleButton"
|
import { SubtleButton } from "./Base/SubtleButton"
|
||||||
import Svg from "../Svg"
|
import Svg from "../Svg"
|
||||||
import NoteCommentElement from "./Popup/NoteCommentElement"
|
import NoteCommentElement from "./Popup/NoteCommentElement"
|
||||||
import ImgurUploader from "../Logic/ImageProviders/ImgurUploader"
|
|
||||||
import FileSelectorButton from "./Input/FileSelectorButton"
|
import FileSelectorButton from "./Input/FileSelectorButton"
|
||||||
import { LoginToggle } from "./Popup/LoginButton"
|
import { LoginToggle } from "./Popup/LoginButton"
|
||||||
import Toggle from "./Input/Toggle"
|
import Toggle from "./Input/Toggle"
|
||||||
|
@ -74,6 +73,7 @@ import FediverseValidator from "./InputElement/Validators/FediverseValidator"
|
||||||
import SendEmail from "./Popup/SendEmail.svelte"
|
import SendEmail from "./Popup/SendEmail.svelte"
|
||||||
import NearbyImages from "./Popup/NearbyImages.svelte"
|
import NearbyImages from "./Popup/NearbyImages.svelte"
|
||||||
import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte"
|
import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte"
|
||||||
|
import UploadImage from "./Image/UploadImage.svelte";
|
||||||
|
|
||||||
class NearbyImageVis implements SpecialVisualization {
|
class NearbyImageVis implements SpecialVisualization {
|
||||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||||
|
@ -616,16 +616,19 @@ export default class SpecialVisualizations {
|
||||||
{
|
{
|
||||||
name: "image-key",
|
name: "image-key",
|
||||||
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
|
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
|
||||||
defaultValue: "image",
|
required: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "label",
|
name: "label",
|
||||||
doc: "The text to show on the button",
|
doc: "The text to show on the button",
|
||||||
defaultValue: "Add image",
|
required: false
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
constr: (state, tags, args) => {
|
constr: (state, tags, args) => {
|
||||||
return new ImageUploadFlow(tags, state, args[0], args[1])
|
return new SvelteUIElement(UploadImage, {
|
||||||
|
state,tags, labelText: args[1], image: args[0]
|
||||||
|
})
|
||||||
|
// return new ImageUploadFlow(tags, state, args[0], args[1])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue