forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
29ff09024f
287 changed files with 14955 additions and 4036 deletions
|
@ -5,9 +5,10 @@ import { Utils } from "../../Utils"
|
|||
import { Feature } from "geojson"
|
||||
|
||||
export default class PendingChangesUploader {
|
||||
|
||||
constructor(changes: Changes, selectedFeature: UIEventSource<Feature>) {
|
||||
changes.pendingChanges.stabilized(Constants.updateTimeoutSec * 1000).addCallback(() => changes.flushChanges("Flushing changes due to timeout"))
|
||||
changes.pendingChanges
|
||||
.stabilized(Constants.updateTimeoutSec * 1000)
|
||||
.addCallback(() => changes.flushChanges("Flushing changes due to timeout"))
|
||||
|
||||
selectedFeature.stabilized(1000).addCallback((feature) => {
|
||||
if (feature === undefined) {
|
||||
|
|
|
@ -314,7 +314,7 @@ export class GeoOperations {
|
|||
return <any>way
|
||||
}
|
||||
|
||||
public static toCSV(features: any[]): string {
|
||||
public static toCSV(features: Feature[] | FeatureCollection): string {
|
||||
const headerValuesSeen = new Set<string>()
|
||||
const headerValuesOrdered: string[] = []
|
||||
|
||||
|
@ -330,7 +330,14 @@ export class GeoOperations {
|
|||
|
||||
const lines: string[] = []
|
||||
|
||||
for (const feature of features) {
|
||||
let _features
|
||||
if (Array.isArray(features)) {
|
||||
_features = features
|
||||
} else {
|
||||
_features = features.features
|
||||
}
|
||||
|
||||
for (const feature of _features) {
|
||||
const properties = feature.properties
|
||||
for (const key in properties) {
|
||||
if (!properties.hasOwnProperty(key)) {
|
||||
|
@ -340,7 +347,7 @@ export class GeoOperations {
|
|||
}
|
||||
}
|
||||
headerValuesOrdered.sort()
|
||||
for (const feature of features) {
|
||||
for (const feature of _features) {
|
||||
const properties = feature.properties
|
||||
let line = ""
|
||||
for (const key of headerValuesOrdered) {
|
||||
|
|
|
@ -64,8 +64,15 @@ export class ImageUploadManager {
|
|||
/**
|
||||
* Uploads the given image, applies the correct title and license for the known user.
|
||||
* Will then add this image to the OSM-feature or the OSM-note
|
||||
* @param file a jpg file to upload
|
||||
* @param tagsStore The tags of the feature
|
||||
* @param targetKey Use this key to save the attribute under. Default: 'image'
|
||||
*/
|
||||
public async uploadImageAndApply(file: File, tagsStore: UIEventSource<OsmTags>): Promise<void> {
|
||||
public async uploadImageAndApply(
|
||||
file: File,
|
||||
tagsStore: UIEventSource<OsmTags>,
|
||||
targetKey?: string
|
||||
): Promise<void> {
|
||||
const sizeInBytes = file.size
|
||||
const tags = tagsStore.data
|
||||
const featureId = <OsmId>tags.id
|
||||
|
@ -95,7 +102,13 @@ export class ImageUploadManager {
|
|||
].join("\n")
|
||||
|
||||
console.log("Upload done, creating ")
|
||||
const action = await this.uploadImageWithLicense(featureId, title, description, file)
|
||||
const action = await this.uploadImageWithLicense(
|
||||
featureId,
|
||||
title,
|
||||
description,
|
||||
file,
|
||||
targetKey
|
||||
)
|
||||
if (!isNaN(Number(featureId))) {
|
||||
// This is a map note
|
||||
const url = action._url
|
||||
|
@ -112,7 +125,8 @@ export class ImageUploadManager {
|
|||
featureId: OsmId,
|
||||
title: string,
|
||||
description: string,
|
||||
blob: File
|
||||
blob: File,
|
||||
targetKey: string | undefined
|
||||
): Promise<LinkImageAction> {
|
||||
this.increaseCountFor(this._uploadStarted, featureId)
|
||||
const properties = this._featureProperties.getStore(featureId)
|
||||
|
@ -132,6 +146,7 @@ export class ImageUploadManager {
|
|||
}
|
||||
}
|
||||
console.log("Uploading done, creating action for", featureId)
|
||||
key = targetKey ?? key
|
||||
const action = new LinkImageAction(featureId, key, value, properties, {
|
||||
theme: this._layout.id,
|
||||
changeType: "add-image",
|
||||
|
|
|
@ -411,7 +411,8 @@ export class Changes {
|
|||
let osmObjects = await Promise.all<{ id: string; osmObj: OsmObject | "deleted" }>(
|
||||
neededIds.map(async (id) => {
|
||||
try {
|
||||
const osmObj = await downloader.DownloadObjectAsync(id)
|
||||
// Important: we do **not** cache this request, we _always_ need a fresh version!
|
||||
const osmObj = await downloader.DownloadObjectAsync(id, 0)
|
||||
return { id, osmObj }
|
||||
} catch (e) {
|
||||
console.error(
|
||||
|
@ -579,7 +580,7 @@ export class Changes {
|
|||
)
|
||||
|
||||
const result = await self.flushSelectChanges(pendingChanges, openChangeset)
|
||||
if(result){
|
||||
if (result) {
|
||||
this.errors.setData([])
|
||||
}
|
||||
return result
|
||||
|
|
|
@ -367,7 +367,7 @@ export class ChangesetHandler {
|
|||
].map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
aggretage: false,
|
||||
aggregate: false,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Utils } from "../../Utils"
|
|||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { AuthConfig } from "./AuthConfig"
|
||||
import Constants from "../../Models/Constants"
|
||||
import OSMAuthInstance = OSMAuth.OSMAuthInstance
|
||||
|
||||
export default class UserDetails {
|
||||
public loggedIn = false
|
||||
|
@ -29,7 +30,7 @@ export default class UserDetails {
|
|||
export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable"
|
||||
|
||||
export class OsmConnection {
|
||||
public auth
|
||||
public auth: OSMAuthInstance
|
||||
public userDetails: UIEventSource<UserDetails>
|
||||
public isLoggedIn: Store<boolean>
|
||||
public gpxServiceIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
|
||||
|
@ -119,17 +120,16 @@ export class OsmConnection {
|
|||
const self = this
|
||||
this.auth.bootstrapToken(
|
||||
options.oauth_token.data,
|
||||
(x) => {
|
||||
console.log("Called back: ", x)
|
||||
(err, result) => {
|
||||
console.log("Bootstrap token called back", err, result)
|
||||
self.AttemptLogin()
|
||||
},
|
||||
this.auth
|
||||
}
|
||||
)
|
||||
|
||||
options.oauth_token.setData(undefined)
|
||||
}
|
||||
if (this.auth.authenticated() && options.attemptLogin !== false) {
|
||||
this.AttemptLogin() // Also updates the user badge
|
||||
this.AttemptLogin()
|
||||
} else {
|
||||
console.log("Not authenticated")
|
||||
}
|
||||
|
@ -268,17 +268,33 @@ export class OsmConnection {
|
|||
/**
|
||||
* Interact with the API.
|
||||
*
|
||||
* @param path: the path to query, without host and without '/api/0.6'. Example 'notes/1234/close'
|
||||
* @param path the path to query, without host and without '/api/0.6'. Example 'notes/1234/close'
|
||||
* @param method
|
||||
* @param header
|
||||
* @param content
|
||||
* @param allowAnonymous if set, will use the anonymous-connection if the main connection is not authenticated
|
||||
*/
|
||||
public async interact(
|
||||
path: string,
|
||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||
header?: Record<string, string | number>,
|
||||
content?: string
|
||||
): Promise<any> {
|
||||
content?: string,
|
||||
allowAnonymous: boolean = false
|
||||
): Promise<string> {
|
||||
|
||||
let connection: OSMAuthInstance = this.auth
|
||||
if(allowAnonymous && !this.auth.authenticated()) {
|
||||
const possibleResult = await Utils.downloadAdvanced(`${this.Backend()}/api/0.6/${path}`,header, method, content)
|
||||
if(possibleResult["content"]) {
|
||||
return possibleResult["content"]
|
||||
}
|
||||
console.error(possibleResult)
|
||||
throw "Could not interact with OSM:"+possibleResult["error"]
|
||||
}
|
||||
|
||||
return new Promise((ok, error) => {
|
||||
this.auth.xhr(
|
||||
{
|
||||
connection.xhr(
|
||||
<any> {
|
||||
method,
|
||||
options: {
|
||||
header,
|
||||
|
@ -300,9 +316,10 @@ export class OsmConnection {
|
|||
public async post(
|
||||
path: string,
|
||||
content?: string,
|
||||
header?: Record<string, string | number>
|
||||
header?: Record<string, string | number>,
|
||||
allowAnonymous: boolean = false
|
||||
): Promise<any> {
|
||||
return await this.interact(path, "POST", header, content)
|
||||
return await this.interact(path, "POST", header, content, allowAnonymous)
|
||||
}
|
||||
|
||||
public async put(
|
||||
|
@ -358,9 +375,10 @@ export class OsmConnection {
|
|||
// Lat and lon must be strings for the API to accept it
|
||||
const content = `lat=${lat}&lon=${lon}&text=${encodeURIComponent(text)}`
|
||||
const response = await this.post("notes.json", content, {
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
})
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
|
||||
}, true)
|
||||
const parsed = JSON.parse(response)
|
||||
console.log("Got result:", parsed)
|
||||
const id = parsed.properties
|
||||
console.log("OPENED NOTE", id)
|
||||
return id
|
||||
|
@ -494,13 +512,14 @@ export class OsmConnection {
|
|||
this.auth = new osmAuth({
|
||||
client_id: this._oauth_config.oauth_client_id,
|
||||
url: this._oauth_config.url,
|
||||
scope: "read_prefs write_prefs write_api write_gpx write_notes",
|
||||
scope: "read_prefs write_prefs write_api write_gpx write_notes openid",
|
||||
redirect_uri: Utils.runningFromConsole
|
||||
? "https://mapcomplete.org/land.html"
|
||||
: window.location.protocol + "//" + window.location.host + "/land.html",
|
||||
singlepage: !standalone,
|
||||
auto: true,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private CheckForMessagesContinuously() {
|
||||
|
|
|
@ -72,7 +72,10 @@ export class OsmPreferences {
|
|||
let i = 0
|
||||
while (str !== "") {
|
||||
if (str === undefined || str === "undefined") {
|
||||
throw "Got 'undefined' or a literal string containing 'undefined' for a long preference with name "+key
|
||||
throw (
|
||||
"Got 'undefined' or a literal string containing 'undefined' for a long preference with name " +
|
||||
key
|
||||
)
|
||||
}
|
||||
if (i > 100) {
|
||||
throw "This long preference is getting very long... "
|
||||
|
|
|
@ -75,6 +75,16 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
|
|||
layoutToUse?.enableUserBadge ?? true,
|
||||
"Disables/Enables logging in and thus disables editing all together. This effectively puts MapComplete into read-only mode."
|
||||
)
|
||||
{
|
||||
if (QueryParameters.wasInitialized("fs-userbadge")) {
|
||||
// userbadge is the legacy name for 'enable-login'
|
||||
this.featureSwitchEnableLogin.setData(
|
||||
QueryParameters.GetBooleanQueryParameter("fs-userbadge", undefined, "Legacy")
|
||||
.data
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.featureSwitchSearch = FeatureSwitchUtils.initSwitch(
|
||||
"fs-search",
|
||||
layoutToUse?.enableSearch ?? true,
|
||||
|
|
|
@ -102,6 +102,10 @@ export class GeoLocationState {
|
|||
this.requestPermissionAsync()
|
||||
}
|
||||
|
||||
public static isSafari(): boolean {
|
||||
return navigator.permissions === undefined && navigator.geolocation !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the user to allow access to their position.
|
||||
* When granted, will be written to the 'geolocationState'.
|
||||
|
@ -119,8 +123,12 @@ export class GeoLocationState {
|
|||
return
|
||||
}
|
||||
|
||||
if (navigator.permissions === undefined && navigator.geolocation !== undefined) {
|
||||
// This is probably safari - we just start watching right away
|
||||
if (GeoLocationState.isSafari()) {
|
||||
// This is probably safari
|
||||
// Safari does not support the 'permissions'-API for geolocation,
|
||||
// so we just start watching right away
|
||||
|
||||
this.permission.setData("requested")
|
||||
this.startWatching()
|
||||
return
|
||||
}
|
||||
|
|
|
@ -127,6 +127,7 @@ export default class Wikidata {
|
|||
"https://www.wikidata.org/",
|
||||
"https://wikidata.org/",
|
||||
"https://query.wikidata.org",
|
||||
"https://m.wikidata.org", // Important: a mobile browser will request m.wikidata.org instead of www.wikidata.org ; this URL needs to be listed for the CSP
|
||||
]
|
||||
private static readonly _identifierPrefixes = ["Q", "L"].map((str) => str.toLowerCase())
|
||||
private static readonly _prefixesToRemove = [
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue