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))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue