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 = [
|
||||
|
|
|
@ -6,7 +6,7 @@ import { AuthConfig } from "../Logic/Osm/AuthConfig"
|
|||
export type PriviligedLayerType = (typeof Constants.priviliged_layers)[number]
|
||||
|
||||
export default class Constants {
|
||||
public static vNumber : string = packagefile.version
|
||||
public static vNumber: string = packagefile.version
|
||||
/**
|
||||
* API key for Maproulette
|
||||
*
|
||||
|
|
|
@ -17,5 +17,9 @@ export interface MapProperties {
|
|||
}
|
||||
|
||||
export interface ExportableMap {
|
||||
exportAsPng(dpiFactor: number): Promise<Blob>
|
||||
/**
|
||||
* Export the current map as PNG.
|
||||
* @param markerScale: if given, the markers will be 'markerScale' bigger. This is to use in combination with a supersized canvas to have more pixels and achieve print quality
|
||||
*/
|
||||
exportAsPng(markerScale?: number): Promise<Blob>
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
|
||||
import ScriptUtils from "../../../../scripts/ScriptUtils"
|
||||
|
||||
export interface DesugaringContext {
|
||||
tagRenderings: Map<string, QuestionableTagRenderingConfigJson>
|
||||
|
@ -28,6 +29,9 @@ export class ConversionContext {
|
|||
this.operation = operation ?? []
|
||||
// Messages is shared by reference amonst all 'context'-objects for performance
|
||||
this.messages = messages
|
||||
if (this.path.some((p) => typeof p === "object" || p === "[object Object]")) {
|
||||
throw "ConversionMessage: got an object as path entry:" + JSON.stringify(path)
|
||||
}
|
||||
}
|
||||
|
||||
public static construct(path: (string | number)[], operation: string[]) {
|
||||
|
@ -105,6 +109,10 @@ export class ConversionContext {
|
|||
public hasErrors() {
|
||||
return this.messages?.find((m) => m.level === "error") !== undefined
|
||||
}
|
||||
|
||||
debug(message: string) {
|
||||
this.messages.push({ context: this, level: "debug", message })
|
||||
}
|
||||
}
|
||||
|
||||
export type ConversionMsgLevel = "debug" | "information" | "warning" | "error"
|
||||
|
@ -178,14 +186,16 @@ export class Pure<TIn, TOut> extends Conversion<TIn, TOut> {
|
|||
|
||||
export class Each<X, Y> extends Conversion<X[], Y[]> {
|
||||
private readonly _step: Conversion<X, Y>
|
||||
private readonly _msg: string
|
||||
|
||||
constructor(step: Conversion<X, Y>) {
|
||||
constructor(step: Conversion<X, Y>, msg?: string) {
|
||||
super(
|
||||
"Applies the given step on every element of the list",
|
||||
[],
|
||||
"OnEach(" + step.name + ")"
|
||||
)
|
||||
this._step = step
|
||||
this._msg = msg
|
||||
}
|
||||
|
||||
convert(values: X[], context: ConversionContext): Y[] {
|
||||
|
@ -196,6 +206,9 @@ export class Each<X, Y> extends Conversion<X[], Y[]> {
|
|||
const result: Y[] = []
|
||||
const c = context.inOperation("each")
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (this._msg) {
|
||||
ScriptUtils.erasableLog(this._msg, `: ${i + 1}/${values.length}`)
|
||||
}
|
||||
const context_ = c.enter(i - 1)
|
||||
const r = step.convert(values[i], context_)
|
||||
result.push(r)
|
||||
|
|
|
@ -91,7 +91,7 @@ export class DoesImageExist extends DesugaringStep<string> {
|
|||
}
|
||||
|
||||
if (image.indexOf("{") >= 0) {
|
||||
context.info("Ignoring image with { in the path: " + image)
|
||||
context.debug("Ignoring image with { in the path: " + image)
|
||||
return image
|
||||
}
|
||||
|
||||
|
@ -275,7 +275,8 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
|
|||
doesImageExist: DoesImageExist,
|
||||
path: string,
|
||||
isBuiltin: boolean,
|
||||
sharedTagRenderings?: Set<string>
|
||||
sharedTagRenderings?: Set<string>,
|
||||
msg?: string
|
||||
) {
|
||||
super(
|
||||
"Validates a theme and the contained layers",
|
||||
|
@ -284,9 +285,10 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
|
|||
"layers",
|
||||
new Each(
|
||||
new Pipe(
|
||||
new ValidateLayer(undefined, isBuiltin, doesImageExist),
|
||||
new ValidateLayer(undefined, isBuiltin, doesImageExist, false, true),
|
||||
new Pure((x) => x.raw)
|
||||
)
|
||||
),
|
||||
msg
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -807,18 +809,21 @@ export class ValidateLayer extends Conversion<
|
|||
private readonly _isBuiltin: boolean
|
||||
private readonly _doesImageExist: DoesImageExist
|
||||
private readonly _studioValidations: boolean
|
||||
private _skipDefaultLayers: boolean
|
||||
|
||||
constructor(
|
||||
path: string,
|
||||
isBuiltin: boolean,
|
||||
doesImageExist: DoesImageExist,
|
||||
studioValidations: boolean = false
|
||||
studioValidations: boolean = false,
|
||||
skipDefaultLayers: boolean = false
|
||||
) {
|
||||
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer")
|
||||
this._path = path
|
||||
this._isBuiltin = isBuiltin
|
||||
this._doesImageExist = doesImageExist
|
||||
this._studioValidations = studioValidations
|
||||
this._skipDefaultLayers = skipDefaultLayers
|
||||
}
|
||||
|
||||
convert(
|
||||
|
@ -831,6 +836,10 @@ export class ValidateLayer extends Conversion<
|
|||
return null
|
||||
}
|
||||
|
||||
if (this._skipDefaultLayers && Constants.added_by_default.indexOf(<any>json.id) >= 0) {
|
||||
return { parsed: undefined, raw: json }
|
||||
}
|
||||
|
||||
if (typeof json === "string") {
|
||||
context.err(
|
||||
`Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`
|
||||
|
@ -1102,7 +1111,7 @@ export class ValidateLayer extends Conversion<
|
|||
).convert(json, context)
|
||||
}
|
||||
|
||||
if (json.pointRendering !== null) {
|
||||
if (json.pointRendering !== null && json.pointRendering !== undefined) {
|
||||
if (!Array.isArray(json.pointRendering)) {
|
||||
throw (
|
||||
"pointRendering in " +
|
||||
|
@ -1111,8 +1120,11 @@ export class ValidateLayer extends Conversion<
|
|||
typeof json.pointRendering
|
||||
)
|
||||
}
|
||||
for (const pointRendering of json.pointRendering) {
|
||||
const index = json.pointRendering.indexOf(pointRendering)
|
||||
for (let i = 0; i < json.pointRendering.length; i++) {
|
||||
const pointRendering = json.pointRendering[i]
|
||||
if (pointRendering.marker === undefined) {
|
||||
continue
|
||||
}
|
||||
for (const icon of pointRendering?.marker) {
|
||||
const indexM = pointRendering?.marker.indexOf(icon)
|
||||
if (!icon.icon) {
|
||||
|
@ -1120,14 +1132,7 @@ export class ValidateLayer extends Conversion<
|
|||
}
|
||||
if (icon.icon["condition"]) {
|
||||
context
|
||||
.enters(
|
||||
"pointRendering",
|
||||
index,
|
||||
"marker",
|
||||
indexM,
|
||||
"icon",
|
||||
"condition"
|
||||
)
|
||||
.enters("pointRendering", i, "marker", indexM, "icon", "condition")
|
||||
.err(
|
||||
"Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead."
|
||||
)
|
||||
|
|
|
@ -171,7 +171,11 @@ export default class LayerConfig extends WithContextLoader {
|
|||
maxSnapDistance: undefined,
|
||||
}
|
||||
if (pr["preciseInput"] !== undefined) {
|
||||
throw "Layer " + this.id + " still uses the old 'preciseInput'-field"
|
||||
throw (
|
||||
"Layer " +
|
||||
this.id +
|
||||
" still uses the old 'preciseInput'-field. For snapping to layers, use 'snapToLayer' instead"
|
||||
)
|
||||
}
|
||||
if (pr.snapToLayer !== undefined) {
|
||||
let snapToLayers = pr.snapToLayer
|
||||
|
@ -459,7 +463,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
neededTags = this.source.osmTags["and"]
|
||||
}
|
||||
|
||||
let tableRows = Utils.NoNull(
|
||||
const tableRows = Utils.NoNull(
|
||||
this.tagRenderings
|
||||
.map((tr) => tr.FreeformValues())
|
||||
.map((values) => {
|
||||
|
|
|
@ -101,11 +101,11 @@ export default class LayoutConfig implements LayoutInformation {
|
|||
}
|
||||
}
|
||||
const context = this.id
|
||||
this.credits = typeof json.credits === "string" ? json.credits : json.credits?.join(", ")
|
||||
|
||||
this.language = Array.from(
|
||||
new Set((json.mustHaveLanguage ?? []).concat(Object.keys(json.title ?? {})))
|
||||
)
|
||||
this.credits = Array.isArray(json.credits) ? json.credits.join("; ") : json.credits
|
||||
if (!json.title) {
|
||||
throw `The theme ${json.id} does not have a title defined.`
|
||||
}
|
||||
this.language = json.mustHaveLanguage ?? Object.keys(json.title)
|
||||
this.usedImages = Array.from(
|
||||
new ExtractImages(official, undefined)
|
||||
.convertStrict(json, ConversionContext.construct([json.id], ["ExtractImages"]))
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -12,28 +12,30 @@
|
|||
let id = Math.random() * 1000000000 + ""
|
||||
</script>
|
||||
|
||||
<form on:change|preventDefault={() => {
|
||||
drawAttention = false
|
||||
dispatcher("submit", inputElement.files)
|
||||
}}
|
||||
on:dragend={() => {
|
||||
console.log("Drag end")
|
||||
drawAttention = false
|
||||
}}
|
||||
on:dragenter|preventDefault|stopPropagation={(e) => {
|
||||
console.log("Dragging enter")
|
||||
drawAttention = true
|
||||
e.dataTransfer.drop = "copy"
|
||||
}}
|
||||
on:dragstart={() => {
|
||||
console.log("DragStart")
|
||||
drawAttention = false
|
||||
}}
|
||||
on:drop|preventDefault|stopPropagation={(e) => {
|
||||
console.log("Got a 'drop'")
|
||||
drawAttention = false
|
||||
dispatcher("submit", e.dataTransfer.files)
|
||||
}}>
|
||||
<form
|
||||
on:change|preventDefault={() => {
|
||||
drawAttention = false
|
||||
dispatcher("submit", inputElement.files)
|
||||
}}
|
||||
on:dragend={() => {
|
||||
console.log("Drag end")
|
||||
drawAttention = false
|
||||
}}
|
||||
on:dragenter|preventDefault|stopPropagation={(e) => {
|
||||
console.log("Dragging enter")
|
||||
drawAttention = true
|
||||
e.dataTransfer.drop = "copy"
|
||||
}}
|
||||
on:dragstart={() => {
|
||||
console.log("DragStart")
|
||||
drawAttention = false
|
||||
}}
|
||||
on:drop|preventDefault|stopPropagation={(e) => {
|
||||
console.log("Got a 'drop'")
|
||||
drawAttention = false
|
||||
dispatcher("submit", e.dataTransfer.files)
|
||||
}}
|
||||
>
|
||||
<label class={twMerge(cls, drawAttention ? "glowing-shadow" : "")} for={"fileinput" + id}>
|
||||
<slot />
|
||||
</label>
|
||||
|
@ -44,7 +46,6 @@
|
|||
id={"fileinput" + id}
|
||||
{multiple}
|
||||
name="file-input"
|
||||
|
||||
type="file"
|
||||
/>
|
||||
</form>
|
||||
|
|
|
@ -11,7 +11,9 @@
|
|||
<div
|
||||
class="absolute top-0 right-0 h-screen w-screen p-4 md:p-6"
|
||||
style="background-color: #00000088; z-index: 20"
|
||||
on:click={() => {dispatch("close")}}
|
||||
on:click={() => {
|
||||
dispatch("close")
|
||||
}}
|
||||
>
|
||||
<div class="content normal-background" on:click|stopPropagation={() => {}}>
|
||||
<div class="h-full rounded-xl">
|
||||
|
|
|
@ -23,11 +23,11 @@ export default class Hotkeys {
|
|||
>([])
|
||||
|
||||
private static textElementSelected(event: KeyboardEvent): boolean {
|
||||
if(event.ctrlKey || event.altKey){
|
||||
if (event.ctrlKey || event.altKey) {
|
||||
// This is an event with a modifier-key, lets not ignore it
|
||||
return false
|
||||
}
|
||||
if(event.key === "Escape"){
|
||||
if (event.key === "Escape") {
|
||||
return false // Another not-printable character that should not be ignored
|
||||
}
|
||||
return ["input", "textarea"].includes(document?.activeElement?.tagName?.toLowerCase())
|
||||
|
|
|
@ -12,21 +12,20 @@
|
|||
* E.g.
|
||||
* condition3 = new ImmutableStore(false) will always hide tab3 (the fourth tab)
|
||||
*/
|
||||
let tr = new ImmutableStore(true);
|
||||
export let condition0: Store<boolean> = tr;
|
||||
export let condition1: Store<boolean> = tr;
|
||||
export let condition2: Store<boolean> = tr;
|
||||
export let condition3: Store<boolean> = tr;
|
||||
export let condition4: Store<boolean> = tr;
|
||||
export let condition5: Store<boolean> = tr;
|
||||
|
||||
export let condition6: Store<boolean> = tr;
|
||||
export let tab: UIEventSource<number> = new UIEventSource<number>(0);
|
||||
let tabElements: HTMLElement[] = [];
|
||||
$: tabElements[$tab]?.click();
|
||||
const tr = new ImmutableStore(true)
|
||||
export let condition0: Store<boolean> = tr
|
||||
export let condition1: Store<boolean> = tr
|
||||
export let condition2: Store<boolean> = tr
|
||||
export let condition3: Store<boolean> = tr
|
||||
export let condition4: Store<boolean> = tr
|
||||
export let condition5: Store<boolean> = tr
|
||||
export let condition6: Store<boolean> = tr
|
||||
export let tab: UIEventSource<number> = new UIEventSource<number>(0)
|
||||
let tabElements: HTMLElement[] = []
|
||||
$: tabElements[$tab]?.click()
|
||||
$: {
|
||||
if (tabElements[tab.data]) {
|
||||
window.setTimeout(() => tabElements[tab.data].click(), 50);
|
||||
window.setTimeout(() => tabElements[tab.data].click(), 50)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -138,44 +137,44 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.tabbedgroup {
|
||||
max-height: 100vh;
|
||||
height: 100%;
|
||||
}
|
||||
.tabbedgroup {
|
||||
max-height: 100vh;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(.tabpanel) {
|
||||
height: 100%;
|
||||
}
|
||||
:global(.tabpanel) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(.tabpanels) {
|
||||
height: calc(100% - 2rem);
|
||||
}
|
||||
:global(.tabpanels) {
|
||||
height: calc(100% - 2rem);
|
||||
}
|
||||
|
||||
:global(.tab) {
|
||||
margin: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
:global(.tab) {
|
||||
margin: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
:global(.tab .flex) {
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
:global(.tab .flex) {
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
:global(.tab span|div) {
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
display: flex;
|
||||
}
|
||||
:global(.tab span|div) {
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
:global(.tab-selected svg) {
|
||||
fill: var(--catch-detail-color-contrast);
|
||||
}
|
||||
:global(.tab-selected svg) {
|
||||
fill: var(--catch-detail-color-contrast);
|
||||
}
|
||||
|
||||
:global(.tab-unselected) {
|
||||
background-color: var(--background-color) !important;
|
||||
color: var(--foreground-color) !important;
|
||||
}
|
||||
:global(.tab-unselected) {
|
||||
background-color: var(--background-color) !important;
|
||||
color: var(--foreground-color) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
if (value.data === undefined) {
|
||||
value.setData(coordinate)
|
||||
}
|
||||
if(coordinate === undefined){
|
||||
if (coordinate === undefined) {
|
||||
coordinate = value.data
|
||||
}
|
||||
export let snapToLayers: string[] | undefined
|
||||
|
@ -47,8 +47,6 @@
|
|||
|
||||
export let snappedTo: UIEventSource<string | undefined>
|
||||
|
||||
|
||||
|
||||
let preciseLocation: UIEventSource<{ lon: number; lat: number }> = new UIEventSource<{
|
||||
lon: number
|
||||
lat: number
|
||||
|
@ -75,7 +73,7 @@
|
|||
rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer),
|
||||
}
|
||||
|
||||
if(targetLayer){
|
||||
if (targetLayer) {
|
||||
const featuresForLayer = state.perLayer.get(targetLayer.id)
|
||||
if (featuresForLayer) {
|
||||
new ShowDataLayer(map, {
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
<script lang="ts">
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
const changes: Changes = state.changes
|
||||
const isUploading: Store<boolean> = changes.isUploading
|
||||
const pendingChangesCount: Store<number> = changes.pendingChanges.map(ls => ls.length)
|
||||
const errors = changes.errors
|
||||
const changes: Changes = state.changes
|
||||
const isUploading: Store<boolean> = changes.isUploading
|
||||
const pendingChangesCount: Store<number> = changes.pendingChanges.map((ls) => ls.length)
|
||||
const errors = changes.errors
|
||||
</script>
|
||||
|
||||
|
||||
<div class="flex flex-col pointer-events-auto" on:click={() => changes.flushChanges("Pending changes indicator clicked")}>
|
||||
<div
|
||||
class="pointer-events-auto flex flex-col"
|
||||
on:click={() => changes.flushChanges("Pending changes indicator clicked")}
|
||||
>
|
||||
{#if $isUploading}
|
||||
<Loading>
|
||||
<Tr cls="thx" t={Translations.t.general.uploadingChanges} />
|
||||
|
@ -23,10 +25,13 @@
|
|||
{:else if $pendingChangesCount === 1}
|
||||
<Tr cls="alert" t={Translations.t.general.uploadPendingSingle} />
|
||||
{:else if $pendingChangesCount > 1}
|
||||
<Tr cls="alert" t={Translations.t.general.uploadPending.Subs({count: $pendingChangesCount})} />
|
||||
<Tr
|
||||
cls="alert"
|
||||
t={Translations.t.general.uploadPending.Subs({ count: $pendingChangesCount })}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#each $errors as error}
|
||||
<Tr cls="alert" t={Translations.t.general.uploadError.Subs({error})} />
|
||||
<Tr cls="alert" t={Translations.t.general.uploadError.Subs({ error })} />
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -15,19 +15,19 @@
|
|||
export let userDetails: UIEventSource<UserDetails>
|
||||
export let state: { layoutToUse?: { id: string }; osmConnection: OsmConnection }
|
||||
export let selected: boolean = false
|
||||
|
||||
|
||||
let unlockedPersonal = LocalStorageSource.GetParsed("unlocked_personal_theme", false)
|
||||
|
||||
userDetails.addCallbackAndRunD(userDetails => {
|
||||
if(!userDetails.loggedIn){
|
||||
return
|
||||
}
|
||||
if(userDetails.csCount > Constants.userJourney.personalLayoutUnlock){
|
||||
unlockedPersonal.setData(true)
|
||||
}
|
||||
return true
|
||||
userDetails.addCallbackAndRunD((userDetails) => {
|
||||
if (!userDetails.loggedIn) {
|
||||
return
|
||||
}
|
||||
if (userDetails.csCount > Constants.userJourney.personalLayoutUnlock) {
|
||||
unlockedPersonal.setData(true)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
|
||||
$: title = new Translation(
|
||||
theme.title,
|
||||
!isCustom && !theme.mustHaveLanguage ? "themes:" + theme.id + ".title" : undefined
|
||||
|
|
|
@ -1,48 +1,51 @@
|
|||
<script lang="ts">
|
||||
import Translations from "../i18n/Translations";
|
||||
import Svg from "../../Svg";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import NextButton from "../Base/NextButton.svelte";
|
||||
import Geosearch from "./Geosearch.svelte";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import ThemeViewState from "../../Models/ThemeViewState";
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { twJoin } from "tailwind-merge";
|
||||
import { Utils } from "../../Utils";
|
||||
import type { GeolocationPermissionState } from "../../Logic/State/GeoLocationState";
|
||||
import If from "../Base/If.svelte";
|
||||
import Translations from "../i18n/Translations"
|
||||
import Svg from "../../Svg"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import NextButton from "../Base/NextButton.svelte"
|
||||
import Geosearch from "./Geosearch.svelte"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { twJoin } from "tailwind-merge"
|
||||
import { Utils } from "../../Utils"
|
||||
import type { GeolocationPermissionState } from "../../Logic/State/GeoLocationState"
|
||||
import { GeoLocationState } from "../../Logic/State/GeoLocationState"
|
||||
import If from "../Base/If.svelte"
|
||||
import { ExclamationTriangleIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import type { Readable } from "svelte/store"
|
||||
|
||||
/**
|
||||
* The theme introduction panel
|
||||
*/
|
||||
export let state: ThemeViewState;
|
||||
let layout = state.layout;
|
||||
let selectedElement = state.selectedElement;
|
||||
let selectedLayer = state.selectedLayer;
|
||||
/**
|
||||
* The theme introduction panel
|
||||
*/
|
||||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
let selectedElement = state.selectedElement
|
||||
let selectedLayer = state.selectedLayer
|
||||
|
||||
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined);
|
||||
let searchEnabled = false;
|
||||
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
|
||||
let searchEnabled = false
|
||||
|
||||
let geopermission: Store<GeolocationPermissionState> =
|
||||
state.geolocation.geolocationState.permission;
|
||||
let currentGPSLocation = state.geolocation.geolocationState.currentGPSLocation;
|
||||
let geopermission: Readable<GeolocationPermissionState> =
|
||||
state.geolocation.geolocationState.permission
|
||||
let currentGPSLocation = state.geolocation.geolocationState.currentGPSLocation
|
||||
|
||||
geopermission.addCallback((perm) => console.log(">>>> Permission", perm));
|
||||
geopermission.addCallback((perm) => console.log(">>>> Permission", perm))
|
||||
|
||||
function jumpToCurrentLocation() {
|
||||
const glstate = state.geolocation.geolocationState;
|
||||
if (glstate.currentGPSLocation.data !== undefined) {
|
||||
const c: GeolocationCoordinates = glstate.currentGPSLocation.data;
|
||||
state.guistate.themeIsOpened.setData(false);
|
||||
const coor = { lon: c.longitude, lat: c.latitude };
|
||||
state.mapProperties.location.setData(coor);
|
||||
}
|
||||
if (glstate.permission.data !== "granted") {
|
||||
glstate.requestPermission();
|
||||
return;
|
||||
}
|
||||
function jumpToCurrentLocation() {
|
||||
const glstate = state.geolocation.geolocationState
|
||||
if (glstate.currentGPSLocation.data !== undefined) {
|
||||
const c: GeolocationCoordinates = glstate.currentGPSLocation.data
|
||||
state.guistate.themeIsOpened.setData(false)
|
||||
const coor = { lon: c.longitude, lat: c.latitude }
|
||||
state.mapProperties.location.setData(coor)
|
||||
}
|
||||
if (glstate.permission.data !== "granted") {
|
||||
glstate.requestPermission()
|
||||
return
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col justify-between">
|
||||
|
@ -63,7 +66,6 @@
|
|||
</div>
|
||||
</NextButton>
|
||||
|
||||
|
||||
<div class="flex w-full flex-wrap sm:flex-nowrap">
|
||||
<If condition={state.featureSwitches.featureSwitchGeolocation}>
|
||||
{#if $currentGPSLocation !== undefined || $geopermission === "prompt"}
|
||||
|
@ -73,12 +75,15 @@
|
|||
</button>
|
||||
<!-- No geolocation granted - we don't show the button -->
|
||||
{:else if $geopermission === "requested"}
|
||||
<button class="disabled flex w-full items-center gap-x-2" on:click={jumpToCurrentLocation}>
|
||||
<button
|
||||
class="disabled flex w-full items-center gap-x-2"
|
||||
on:click={jumpToCurrentLocation}
|
||||
>
|
||||
<!-- Even though disabled, when clicking we request the location again in case the contributor dismissed the location popup -->
|
||||
<ToSvelte
|
||||
construct={Svg.crosshair_svg()
|
||||
.SetClass("w-8 h-8")
|
||||
.SetStyle("animation: 3s linear 0s infinite normal none running spin;")}
|
||||
.SetClass("w-8 h-8")
|
||||
.SetStyle("animation: 3s linear 0s infinite normal none running spin;")}
|
||||
/>
|
||||
<Tr t={Translations.t.general.waitingForGeopermission} />
|
||||
</button>
|
||||
|
@ -91,24 +96,25 @@
|
|||
<button class="disabled flex w-full items-center gap-x-2">
|
||||
<ToSvelte
|
||||
construct={Svg.crosshair_svg()
|
||||
.SetClass("w-8 h-8")
|
||||
.SetStyle("animation: 3s linear 0s infinite normal none running spin;")}
|
||||
.SetClass("w-8 h-8")
|
||||
.SetStyle("animation: 3s linear 0s infinite normal none running spin;")}
|
||||
/>
|
||||
<Tr t={Translations.t.general.waitingForLocation} />
|
||||
</button>
|
||||
{/if}
|
||||
</If>
|
||||
|
||||
|
||||
<If condition={state.featureSwitches.featureSwitchSearch}>
|
||||
<div class=".button low-interaction m-1 flex w-full items-center gap-x-2 rounded border p-2">
|
||||
<div
|
||||
class=".button low-interaction m-1 flex h-fit w-full items-center gap-x-2 rounded border p-2"
|
||||
>
|
||||
<div class="w-full">
|
||||
<Geosearch
|
||||
bounds={state.mapProperties.bounds}
|
||||
on:searchCompleted={() => state.guistate.themeIsOpened.setData(false)}
|
||||
on:searchIsValid={(isValid) => {
|
||||
searchEnabled = isValid
|
||||
}}
|
||||
searchEnabled = isValid
|
||||
}}
|
||||
perLayer={state.perLayer}
|
||||
{selectedElement}
|
||||
{selectedLayer}
|
||||
|
@ -116,7 +122,10 @@
|
|||
/>
|
||||
</div>
|
||||
<button
|
||||
class={twJoin("flex items-center justify-between gap-x-2", !searchEnabled && "disabled")}
|
||||
class={twJoin(
|
||||
"flex items-center justify-between gap-x-2",
|
||||
!searchEnabled && "disabled"
|
||||
)}
|
||||
on:click={() => triggerSearch.ping()}
|
||||
>
|
||||
<Tr t={Translations.t.general.search.searchShort} />
|
||||
|
@ -125,6 +134,23 @@
|
|||
</div>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
{#if $currentGPSLocation === undefined && $geopermission === "requested" && GeoLocationState.isSafari()}
|
||||
<a
|
||||
href="https://support.apple.com/en-us/HT207092"
|
||||
class="button w-full"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<div class="link-underline m-1 flex w-full">
|
||||
<ExclamationTriangleIcon class="w-12 pr-2" />
|
||||
<div class="flex w-full flex-col">
|
||||
<Tr cls="font-normal" t={Translations.t.general.enableGeolocationForSafari} />
|
||||
<Tr t={Translations.t.general.enableGeolocationForSafariLink} />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="links-as-button links-w-full m-2 flex flex-col gap-y-1">
|
||||
|
|
|
@ -81,7 +81,7 @@
|
|||
mimetype="image/png"
|
||||
mainText={t.downloadAsPng}
|
||||
helperText={t.downloadAsPngHelper}
|
||||
construct={() => state.mapProperties.exportAsPng(4)}
|
||||
construct={() => state.mapProperties.exportAsPng(1)}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col">
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
export let state: SpecialVisualizationState
|
||||
|
||||
export let tags: Store<OsmTags>
|
||||
export let targetKey: string = undefined
|
||||
/**
|
||||
* Image to show in the button
|
||||
* NOT the image to upload!
|
||||
|
@ -35,7 +36,7 @@
|
|||
const file = files.item(i)
|
||||
console.log("Got file", file.name)
|
||||
try {
|
||||
state.imageUploadManager?.uploadImageAndApply(file, tags)
|
||||
state?.imageUploadManager.uploadImageAndApply(file, tags, targetKey)
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Shows information about how much images are uploaded for the given feature
|
||||
*
|
||||
* Either pass in a store with tags or a featureId.
|
||||
*/
|
||||
/**
|
||||
* Shows information about how much images are uploaded for the given feature
|
||||
*
|
||||
* Either pass in a store with tags or a featureId.
|
||||
*/
|
||||
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import type { OsmTags } from "../../Models/OsmFeature"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import type { OsmTags } from "../../Models/OsmFeature"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: Store<OsmTags>
|
||||
export let featureId = tags.data.id
|
||||
export let showThankYou: boolean = true
|
||||
const { uploadStarted, uploadFinished, retried, failed } =
|
||||
state.imageUploadManager.getCountsFor(featureId)
|
||||
const t = Translations.t.image
|
||||
export let state: SpecialVisualizationState
|
||||
export let tags: Store<OsmTags>
|
||||
export let featureId = tags.data.id
|
||||
export let showThankYou: boolean = true
|
||||
const { uploadStarted, uploadFinished, retried, failed } =
|
||||
state.imageUploadManager.getCountsFor(featureId)
|
||||
const t = Translations.t.image
|
||||
</script>
|
||||
|
||||
{#if $uploadStarted === 1}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<script lang="ts">/**
|
||||
* Opens the 'Opening hours input' in another top level window
|
||||
*/
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte"
|
||||
import OpeningHoursInput from "../../OpeningHours/OpeningHoursInput"
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Opens the 'Opening hours input' in another top level window
|
||||
*/
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte"
|
||||
import OpeningHoursInput from "../../OpeningHours/OpeningHoursInput"
|
||||
|
||||
export let value: UIEventSource<string>
|
||||
export let value: UIEventSource<string>
|
||||
</script>
|
||||
|
||||
<ToSvelte construct={new OpeningHoursInput(value)}></ToSvelte>
|
||||
<ToSvelte construct={new OpeningHoursInput(value)} />
|
||||
|
|
|
@ -1,30 +1,34 @@
|
|||
<script lang="ts">
|
||||
import { Utils } from "../Utils"
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import Loading from "./Base/Loading.svelte"
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||
|
||||
import { Utils } from "../Utils"
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import Loading from "./Base/Loading.svelte"
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||
|
||||
const osmConnection = new OsmConnection({
|
||||
attemptLogin: true
|
||||
})
|
||||
let loggedInContributor: Store<string> = osmConnection.userDetails.map(ud => ud.name)
|
||||
export let source = "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/picture-leaderboard.json"
|
||||
let data: Store<undefined | {
|
||||
const osmConnection = new OsmConnection({
|
||||
attemptLogin: true,
|
||||
})
|
||||
let loggedInContributor: Store<string> = osmConnection.userDetails.map((ud) => ud.name)
|
||||
export let source =
|
||||
"https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/picture-leaderboard.json"
|
||||
let data: Store<
|
||||
| undefined
|
||||
| {
|
||||
leaderboard: {
|
||||
rank: number,
|
||||
name: string,
|
||||
account: string,
|
||||
nrOfImages: number
|
||||
}[],
|
||||
median: number,
|
||||
totalAuthors: number,
|
||||
rank: number
|
||||
name: string
|
||||
account: string
|
||||
nrOfImages: number
|
||||
}[]
|
||||
median: number
|
||||
totalAuthors: number
|
||||
byLicense: {
|
||||
license: string, total: number, authors: string[]
|
||||
},
|
||||
license: string
|
||||
total: number
|
||||
authors: string[]
|
||||
}
|
||||
date: string
|
||||
}> = UIEventSource.FromPromise(Utils.downloadJsonCached(source))
|
||||
|
||||
}
|
||||
> = UIEventSource.FromPromise(Utils.downloadJsonCached(source))
|
||||
</script>
|
||||
|
||||
<h1>Contributed images with MapComplete: leaderboard</h1>
|
||||
|
@ -43,13 +47,14 @@
|
|||
</td>
|
||||
<td>
|
||||
{#if $loggedInContributor === contributor.name}
|
||||
<a class="thanks" href="{contributor.account}">{contributor.name}</a>
|
||||
<a class="thanks" href={contributor.account}>{contributor.name}</a>
|
||||
{:else}
|
||||
<a href="{contributor.account}">{contributor.name}</a>
|
||||
<a href={contributor.account}>{contributor.name}</a>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<b>{contributor.nrOfImages}</b> total images
|
||||
<b>{contributor.nrOfImages}</b>
|
||||
total images
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import type { Map as MLMap } from "maplibre-gl";
|
||||
import { Map as MlMap, SourceSpecification } from "maplibre-gl";
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers";
|
||||
import { Utils } from "../../Utils";
|
||||
import { BBox } from "../../Logic/BBox";
|
||||
import { ExportableMap, MapProperties } from "../../Models/MapProperties";
|
||||
import SvelteUIElement from "../Base/SvelteUIElement";
|
||||
import MaplibreMap from "./MaplibreMap.svelte";
|
||||
import { RasterLayerProperties } from "../../Models/RasterLayerProperties";
|
||||
import * as htmltoimage from "html-to-image";
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Map as MLMap } from "maplibre-gl"
|
||||
import { Map as MlMap, SourceSpecification } from "maplibre-gl"
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { Utils } from "../../Utils"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import { ExportableMap, MapProperties } from "../../Models/MapProperties"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import MaplibreMap from "./MaplibreMap.svelte"
|
||||
import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
||||
import * as htmltoimage from "html-to-image"
|
||||
|
||||
/**
|
||||
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
||||
|
@ -224,7 +224,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
return url
|
||||
}
|
||||
|
||||
public async exportAsPng(dpiFactor: number): Promise<Blob> {
|
||||
public async exportAsPng(markerScale: number = 1): Promise<Blob> {
|
||||
const map = this._maplibreMap.data
|
||||
if (!map) {
|
||||
return undefined
|
||||
|
@ -235,14 +235,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
|
||||
const ctx = drawOn.getContext("2d")
|
||||
// Set up CSS size.
|
||||
MapLibreAdaptor.setDpi(drawOn, ctx, dpiFactor / map.getPixelRatio())
|
||||
MapLibreAdaptor.setDpi(drawOn, ctx, markerScale / map.getPixelRatio())
|
||||
|
||||
await this.exportBackgroundOnCanvas(ctx)
|
||||
|
||||
// MapLibreAdaptor.setDpi(drawOn, ctx, 1)
|
||||
const markers = await this.drawMarkers(dpiFactor)
|
||||
const markers = await this.drawMarkers(markerScale)
|
||||
ctx.drawImage(markers, 0, 0, drawOn.width, drawOn.height)
|
||||
ctx.scale(dpiFactor, dpiFactor)
|
||||
ctx.scale(markerScale, markerScale)
|
||||
this._maplibreMap.data?.resize()
|
||||
return await new Promise<Blob>((resolve) => drawOn.toBlob((blob) => resolve(blob)))
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
|||
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource"
|
||||
import { CLIENT_RENEG_LIMIT } from "tls";
|
||||
import { CLIENT_RENEG_LIMIT } from "tls"
|
||||
|
||||
class PointRenderingLayer {
|
||||
private readonly _config: PointRenderingConfig
|
||||
|
@ -409,7 +409,7 @@ class LineRenderingLayer {
|
|||
this._listenerInstalledOn.add(id)
|
||||
tags.addCallbackAndRunD((properties) => {
|
||||
// Make sure to use 'getSource' here, the layer names are different!
|
||||
if(map.getSource(this._layername) === undefined){
|
||||
if (map.getSource(this._layername) === undefined) {
|
||||
return true
|
||||
}
|
||||
map.setFeatureState(
|
||||
|
|
|
@ -289,6 +289,14 @@ export class OH {
|
|||
* rules[0].startHour // => 11
|
||||
* rules[3].endHour // => 19
|
||||
*
|
||||
* const rules = OH.ParseRule("Mo 20:00-02:00");
|
||||
* rules.length // => 2
|
||||
* rules[0].weekday // => 0
|
||||
* rules[0].startHour // => 20
|
||||
* rules[0].endHour // => 0
|
||||
* rules[1].weekday // => 1
|
||||
* rules[1].startHour // => 0
|
||||
* rules[1].endHour // => 2
|
||||
*/
|
||||
public static ParseRule(rule: string): OpeningHour[] {
|
||||
try {
|
||||
|
@ -414,14 +422,14 @@ export class OH {
|
|||
}
|
||||
|
||||
/*
|
||||
This function converts a number of ranges (generated by OpeningHours.js) into all the hours of day that a change occurs.
|
||||
E.g.
|
||||
Monday, some business is opended from 9:00 till 17:00
|
||||
Tuesday from 9:30 till 18:00
|
||||
Wednesday from 9:30 till 12:30
|
||||
This function will extract all those moments of change and will return 9:00, 9:30, 12:30, 17:00 and 18:00
|
||||
This list will be sorted
|
||||
*/
|
||||
This function converts a number of ranges (generated by OpeningHours.js) into all the hours of day that a change occurs.
|
||||
E.g.
|
||||
Monday, some business is opended from 9:00 till 17:00
|
||||
Tuesday from 9:30 till 18:00
|
||||
Wednesday from 9:30 till 12:30
|
||||
This function will extract all those moments of change and will return 9:00, 9:30, 12:30, 17:00 and 18:00
|
||||
This list will be sorted
|
||||
*/
|
||||
public static allChangeMoments(
|
||||
ranges: {
|
||||
isOpen: boolean
|
||||
|
@ -507,9 +515,9 @@ export class OH {
|
|||
}
|
||||
|
||||
/*
|
||||
Calculates when the business is opened (or on holiday) between two dates.
|
||||
Returns a matrix of ranges, where [0] is a list of ranges when it is opened on monday, [1] is a list of ranges for tuesday, ...
|
||||
*/
|
||||
Calculates when the business is opened (or on holiday) between two dates.
|
||||
Returns a matrix of ranges, where [0] is a list of ranges when it is opened on monday, [1] is a list of ranges for tuesday, ...
|
||||
*/
|
||||
public static GetRanges(
|
||||
oh: any,
|
||||
from: Date,
|
||||
|
@ -560,6 +568,9 @@ export class OH {
|
|||
return values
|
||||
}
|
||||
|
||||
/**
|
||||
* OH.parseHHMM("12:30") // => {hours: 12, minutes: 30}
|
||||
*/
|
||||
private static parseHHMM(hhmm: string): { hours: number; minutes: number } {
|
||||
if (hhmm === undefined || hhmm == null) {
|
||||
return null
|
||||
|
@ -575,6 +586,10 @@ export class OH {
|
|||
return hm
|
||||
}
|
||||
|
||||
/**
|
||||
* OH.ParseHhmmRanges("20:00-22:15") // => [{startHour: 20, startMinutes: 0, endHour: 22, endMinutes: 15}]
|
||||
* OH.ParseHhmmRanges("20:00-02:15") // => [{startHour: 20, startMinutes: 0, endHour: 2, endMinutes: 15}]
|
||||
*/
|
||||
private static ParseHhmmRanges(hhmms: string): {
|
||||
startHour: number
|
||||
startMinutes: number
|
||||
|
@ -641,24 +656,53 @@ export class OH {
|
|||
endHour: number
|
||||
endMinutes: number
|
||||
}[]
|
||||
) {
|
||||
): {
|
||||
weekday: number
|
||||
startHour: number
|
||||
startMinutes: number
|
||||
endHour: number
|
||||
endMinutes: number
|
||||
}[] {
|
||||
if ((weekdays ?? null) == null || (timeranges ?? null) == null) {
|
||||
return null
|
||||
}
|
||||
const ohs: OpeningHour[] = []
|
||||
for (const timerange of timeranges) {
|
||||
const overMidnight =
|
||||
!(timerange.endHour === 0 && timerange.endMinutes === 0) &&
|
||||
(timerange.endHour < timerange.startHour ||
|
||||
(timerange.endHour == timerange.startHour &&
|
||||
timerange.endMinutes < timerange.startMinutes))
|
||||
for (const weekday of weekdays) {
|
||||
ohs.push({
|
||||
weekday: weekday,
|
||||
startHour: timerange.startHour,
|
||||
startMinutes: timerange.startMinutes,
|
||||
endHour: timerange.endHour,
|
||||
endMinutes: timerange.endMinutes,
|
||||
})
|
||||
if (!overMidnight) {
|
||||
ohs.push({
|
||||
weekday: weekday,
|
||||
startHour: timerange.startHour,
|
||||
startMinutes: timerange.startMinutes,
|
||||
endHour: timerange.endHour,
|
||||
endMinutes: timerange.endMinutes,
|
||||
})
|
||||
} else {
|
||||
ohs.push({
|
||||
weekday: weekday,
|
||||
startHour: timerange.startHour,
|
||||
startMinutes: timerange.startMinutes,
|
||||
endHour: 0,
|
||||
endMinutes: 0,
|
||||
})
|
||||
ohs.push({
|
||||
weekday: (weekday + 1) % 7,
|
||||
startHour: 0,
|
||||
startMinutes: 0,
|
||||
endHour: timerange.endHour,
|
||||
endMinutes: timerange.endMinutes,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return ohs
|
||||
}
|
||||
|
||||
public static getMondayBefore(d) {
|
||||
d = new Date(d)
|
||||
const day = d.getDay()
|
||||
|
|
|
@ -82,6 +82,7 @@ export default class OpeningHoursInput extends InputElement<string> {
|
|||
const rules = valueWithoutPrefix.data?.split(";") ?? []
|
||||
for (const rule of rules) {
|
||||
if (OH.ParsePHRule(rule) !== null) {
|
||||
// We found the rule containing the public holiday information
|
||||
ph = rule
|
||||
break
|
||||
}
|
||||
|
|
|
@ -162,16 +162,16 @@
|
|||
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in">
|
||||
<Tr slot="message" t={Translations.t.general.add.pleaseLogin} />
|
||||
</LoginButton>
|
||||
{#if $zoom < Constants.minZoomLevelToAddNewPoint}
|
||||
{#if $zoom < Constants.minZoomLevelToAddNewPoint}
|
||||
<div class="alert">
|
||||
<Tr t={Translations.t.general.add.zoomInFurther} />
|
||||
</div>
|
||||
{:else if $isLoading}
|
||||
<div class="alert">
|
||||
<Loading>
|
||||
<Tr t={Translations.t.general.add.stillLoading} />
|
||||
</Loading>
|
||||
</div>
|
||||
{:else if $isLoading}
|
||||
<div class="alert">
|
||||
<Loading>
|
||||
<Tr t={Translations.t.general.add.stillLoading} />
|
||||
</Loading>
|
||||
</div>
|
||||
{:else if selectedPreset === undefined}
|
||||
<!-- First, select the correct preset -->
|
||||
<PresetList
|
||||
|
|
|
@ -30,7 +30,12 @@
|
|||
if (flayer.isDisplayed.data === false) {
|
||||
// The layer is not displayed...
|
||||
if (!state.featureSwitches.featureSwitchFilter.data) {
|
||||
console.log("Not showing presets for layer", flayer.layerDef.id, "as not displayed and featureSwitchFilter.data is set",state.featureSwitches.featureSwitchFilter.data)
|
||||
console.log(
|
||||
"Not showing presets for layer",
|
||||
flayer.layerDef.id,
|
||||
"as not displayed and featureSwitchFilter.data is set",
|
||||
state.featureSwitches.featureSwitchFilter.data
|
||||
)
|
||||
// ...and we cannot enable the layer control -> we skip, as these presets can never be shown anyway
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -2,50 +2,50 @@
|
|||
/**
|
||||
* UIcomponent to create a new note at the given location
|
||||
*/
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource";
|
||||
import ValidatedInput from "../InputElement/ValidatedInput.svelte";
|
||||
import SubtleButton from "../Base/SubtleButton.svelte";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Translations from "../i18n/Translations.js";
|
||||
import type { Feature, Point } from "geojson";
|
||||
import LoginToggle from "../Base/LoginToggle.svelte";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import NewPointLocationInput from "../BigComponents/NewPointLocationInput.svelte";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import Svg from "../../Svg";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
|
||||
import ValidatedInput from "../InputElement/ValidatedInput.svelte"
|
||||
import SubtleButton from "../Base/SubtleButton.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations.js"
|
||||
import type { Feature, Point } from "geojson"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import NewPointLocationInput from "../BigComponents/NewPointLocationInput.svelte"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Svg from "../../Svg"
|
||||
|
||||
export let coordinate: UIEventSource<{ lon: number; lat: number }>;
|
||||
export let state: SpecialVisualizationState;
|
||||
export let coordinate: UIEventSource<{ lon: number; lat: number }>
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
let comment: UIEventSource<string> = LocalStorageSource.Get("note-text");
|
||||
let created = false;
|
||||
let comment: UIEventSource<string> = LocalStorageSource.Get("note-text")
|
||||
let created = false
|
||||
|
||||
let notelayer: FilteredLayer = state.layerState.filteredLayers.get("note");
|
||||
let notelayer: FilteredLayer = state.layerState.filteredLayers.get("note")
|
||||
|
||||
let hasFilter = notelayer?.hasFilter;
|
||||
let isDisplayed = notelayer?.isDisplayed;
|
||||
let hasFilter = notelayer?.hasFilter
|
||||
let isDisplayed = notelayer?.isDisplayed
|
||||
|
||||
function enableNoteLayer() {
|
||||
state.guistate.closeAll();
|
||||
isDisplayed.setData(true);
|
||||
state.guistate.closeAll()
|
||||
isDisplayed.setData(true)
|
||||
}
|
||||
|
||||
async function uploadNote() {
|
||||
let txt = comment.data;
|
||||
let txt = comment.data
|
||||
if (txt === undefined || txt === "") {
|
||||
return;
|
||||
return
|
||||
}
|
||||
const loc = coordinate.data;
|
||||
txt += "\n\n #MapComplete #" + state?.layout?.id;
|
||||
const id = await state?.osmConnection?.openNote(loc.lat, loc.lon, txt);
|
||||
console.log("Created a note, got id", id);
|
||||
const loc = coordinate.data
|
||||
txt += "\n\n #MapComplete #" + state?.layout?.id
|
||||
const id = await state?.osmConnection?.openNote(loc.lat, loc.lon, txt)
|
||||
console.log("Created a note, got id", id)
|
||||
const feature = <Feature<Point>>{
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [loc.lon, loc.lat]
|
||||
coordinates: [loc.lon, loc.lat],
|
||||
},
|
||||
properties: {
|
||||
id: "" + id.id,
|
||||
|
@ -56,22 +56,22 @@
|
|||
text: txt,
|
||||
html: txt,
|
||||
user: state.osmConnection?.userDetails?.data?.name,
|
||||
uid: state.osmConnection?.userDetails?.data?.uid
|
||||
}
|
||||
])
|
||||
}
|
||||
};
|
||||
// Normally, the 'Changes' will generate the new element. The 'notes' are an exception to this
|
||||
state.newFeatures.features.data.push(feature);
|
||||
state.newFeatures.features.ping();
|
||||
state.selectedElement?.setData(feature);
|
||||
if (state.featureProperties.trackFeature) {
|
||||
state.featureProperties.trackFeature(feature);
|
||||
uid: state.osmConnection?.userDetails?.data?.uid,
|
||||
},
|
||||
]),
|
||||
},
|
||||
}
|
||||
comment.setData("");
|
||||
created = true;
|
||||
state.selectedElement.setData(feature);
|
||||
state.selectedLayer.setData(state.layerState.filteredLayers.get("note"));
|
||||
// Normally, the 'Changes' will generate the new element. The 'notes' are an exception to this
|
||||
state.newFeatures.features.data.push(feature)
|
||||
state.newFeatures.features.ping()
|
||||
state.selectedElement?.setData(feature)
|
||||
if (state.featureProperties.trackFeature) {
|
||||
state.featureProperties.trackFeature(feature)
|
||||
}
|
||||
comment.setData("")
|
||||
created = true
|
||||
state.selectedElement.setData(feature)
|
||||
state.selectedLayer.setData(state.layerState.filteredLayers.get("note"))
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -109,15 +109,14 @@
|
|||
<ValidatedInput type="text" value={comment} />
|
||||
</div>
|
||||
|
||||
<div class="w-full h-56">
|
||||
<NewPointLocationInput value={coordinate} {state} >
|
||||
<div class="h-56 w-full">
|
||||
<NewPointLocationInput value={coordinate} {state}>
|
||||
<div class="h-20 w-full pb-10" slot="image">
|
||||
<ToSvelte construct={Svg.note_svg().SetClass("h-10 w-full")}/>
|
||||
<ToSvelte construct={Svg.note_svg().SetClass("h-10 w-full")} />
|
||||
</div>
|
||||
</NewPointLocationInput>
|
||||
</div>
|
||||
|
||||
|
||||
<LoginToggle {state}>
|
||||
<span slot="loading"><!--empty: don't show a loading message--></span>
|
||||
<div slot="not-logged-in" class="alert">
|
||||
|
|
|
@ -112,7 +112,10 @@
|
|||
<button
|
||||
slot="save-button"
|
||||
on:click={onDelete}
|
||||
class={twJoin(selectedTags === undefined && "disabled", "primary flex bg-red-600 items-center")}
|
||||
class={twJoin(
|
||||
selectedTags === undefined && "disabled",
|
||||
"primary flex items-center bg-red-600"
|
||||
)}
|
||||
>
|
||||
<TrashIcon
|
||||
class={twJoin(
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
{#if !userDetails || $userDetails.loggedIn}
|
||||
<div>
|
||||
{#if tags === undefined}
|
||||
<slot name="no-tags"><Tr cls="subtle" t={Translations.t.general.noTagsSelected}></Tr></slot>
|
||||
<slot name="no-tags"><Tr cls="subtle" t={Translations.t.general.noTagsSelected} /></slot>
|
||||
{:else if embedIn === undefined}
|
||||
<FromHtml src={tagsExplanation} />
|
||||
{:else}
|
||||
|
|
|
@ -180,13 +180,21 @@
|
|||
</script>
|
||||
|
||||
{#if config.question !== undefined}
|
||||
<div class="interactive border-interactive flex flex-col p-1 px-2 relative overflow-y-auto" style="max-height: 85vh">
|
||||
<div
|
||||
class="interactive border-interactive relative flex flex-col overflow-y-auto p-1 px-2"
|
||||
style="max-height: 85vh"
|
||||
>
|
||||
<div class="sticky top-0" style="z-index: 11">
|
||||
|
||||
<div class="flex justify-between sticky top-0 interactive">
|
||||
<span class="font-bold">
|
||||
<SpecialTranslation t={config.question} {tags} {state} {layer} feature={selectedElement} />
|
||||
</span>
|
||||
<div class="interactive sticky top-0 flex justify-between">
|
||||
<span class="font-bold">
|
||||
<SpecialTranslation
|
||||
t={config.question}
|
||||
{tags}
|
||||
{state}
|
||||
{layer}
|
||||
feature={selectedElement}
|
||||
/>
|
||||
</span>
|
||||
<slot name="upper-right" />
|
||||
</div>
|
||||
|
||||
|
@ -204,7 +212,7 @@
|
|||
</div>
|
||||
|
||||
{#if config.mappings?.length >= 8}
|
||||
<div class="flex w-full sticky">
|
||||
<div class="sticky flex w-full">
|
||||
<img src="./assets/svg/search.svg" class="h-6 w-6" />
|
||||
<input type="text" bind:value={$searchTerm} class="w-full" />
|
||||
</div>
|
||||
|
@ -318,8 +326,10 @@
|
|||
<Tr t={$feedback} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap sticky bottom-0 interactive"
|
||||
style="z-index: 11">
|
||||
<div
|
||||
class="interactive sticky bottom-0 flex flex-wrap-reverse items-stretch justify-end sm:flex-nowrap"
|
||||
style="z-index: 11"
|
||||
>
|
||||
<!-- TagRenderingQuestion-buttons -->
|
||||
<slot name="cancel" />
|
||||
<slot name="save-button" {selectedTags}>
|
||||
|
|
|
@ -680,11 +680,13 @@ export default class SpecialVisualizations {
|
|||
},
|
||||
],
|
||||
constr: (state, tags, args) => {
|
||||
const targetKey = args[0] === "" ? undefined : args[0]
|
||||
return new SvelteUIElement(UploadImage, {
|
||||
state,
|
||||
tags,
|
||||
targetKey,
|
||||
labelText: args[1],
|
||||
image: args[0],
|
||||
image: args[2],
|
||||
})
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,115 +1,115 @@
|
|||
<script lang="ts">
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte"
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||
import MapControlButton from "./Base/MapControlButton.svelte"
|
||||
import ToSvelte from "./Base/ToSvelte.svelte"
|
||||
import If from "./Base/If.svelte"
|
||||
import { GeolocationControl } from "./BigComponents/GeolocationControl"
|
||||
import type { Feature } from "geojson"
|
||||
import SelectedElementView from "./BigComponents/SelectedElementView.svelte"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import Filterview from "./BigComponents/Filterview.svelte"
|
||||
import ThemeViewState from "../Models/ThemeViewState"
|
||||
import type { MapProperties } from "../Models/MapProperties"
|
||||
import Geosearch from "./BigComponents/Geosearch.svelte"
|
||||
import Translations from "./i18n/Translations"
|
||||
import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import Tr from "./Base/Tr.svelte"
|
||||
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
|
||||
import FloatOver from "./Base/FloatOver.svelte"
|
||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy"
|
||||
import Constants from "../Models/Constants"
|
||||
import TabbedGroup from "./Base/TabbedGroup.svelte"
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState"
|
||||
import LoginToggle from "./Base/LoginToggle.svelte"
|
||||
import LoginButton from "./Base/LoginButton.svelte"
|
||||
import CopyrightPanel from "./BigComponents/CopyrightPanel"
|
||||
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"
|
||||
import ModalRight from "./Base/ModalRight.svelte"
|
||||
import { Utils } from "../Utils"
|
||||
import Hotkeys from "./Base/Hotkeys"
|
||||
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||
import SvelteUIElement from "./Base/SvelteUIElement"
|
||||
import OverlayToggle from "./BigComponents/OverlayToggle.svelte"
|
||||
import LevelSelector from "./BigComponents/LevelSelector.svelte"
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
|
||||
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"
|
||||
import Svg from "../Svg"
|
||||
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
|
||||
import type { RasterLayerPolygon } from "../Models/RasterLayers"
|
||||
import { AvailableRasterLayers } from "../Models/RasterLayers"
|
||||
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"
|
||||
import IfHidden from "./Base/IfHidden.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import { OpenJosm } from "./BigComponents/OpenJosm"
|
||||
import MapillaryLink from "./BigComponents/MapillaryLink.svelte"
|
||||
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
|
||||
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"
|
||||
import StateIndicator from "./BigComponents/StateIndicator.svelte"
|
||||
import LanguagePicker from "./LanguagePicker"
|
||||
import Locale from "./i18n/Locale"
|
||||
import ShareScreen from "./BigComponents/ShareScreen.svelte"
|
||||
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte"
|
||||
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte"
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import MaplibreMap from "./Map/MaplibreMap.svelte"
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||
import MapControlButton from "./Base/MapControlButton.svelte"
|
||||
import ToSvelte from "./Base/ToSvelte.svelte"
|
||||
import If from "./Base/If.svelte"
|
||||
import { GeolocationControl } from "./BigComponents/GeolocationControl"
|
||||
import type { Feature } from "geojson"
|
||||
import SelectedElementView from "./BigComponents/SelectedElementView.svelte"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import Filterview from "./BigComponents/Filterview.svelte"
|
||||
import ThemeViewState from "../Models/ThemeViewState"
|
||||
import type { MapProperties } from "../Models/MapProperties"
|
||||
import Geosearch from "./BigComponents/Geosearch.svelte"
|
||||
import Translations from "./i18n/Translations"
|
||||
import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import Tr from "./Base/Tr.svelte"
|
||||
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
|
||||
import FloatOver from "./Base/FloatOver.svelte"
|
||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy"
|
||||
import Constants from "../Models/Constants"
|
||||
import TabbedGroup from "./Base/TabbedGroup.svelte"
|
||||
import UserRelatedState from "../Logic/State/UserRelatedState"
|
||||
import LoginToggle from "./Base/LoginToggle.svelte"
|
||||
import LoginButton from "./Base/LoginButton.svelte"
|
||||
import CopyrightPanel from "./BigComponents/CopyrightPanel"
|
||||
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"
|
||||
import ModalRight from "./Base/ModalRight.svelte"
|
||||
import { Utils } from "../Utils"
|
||||
import Hotkeys from "./Base/Hotkeys"
|
||||
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||
import SvelteUIElement from "./Base/SvelteUIElement"
|
||||
import OverlayToggle from "./BigComponents/OverlayToggle.svelte"
|
||||
import LevelSelector from "./BigComponents/LevelSelector.svelte"
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
|
||||
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"
|
||||
import Svg from "../Svg"
|
||||
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
|
||||
import type { RasterLayerPolygon } from "../Models/RasterLayers"
|
||||
import { AvailableRasterLayers } from "../Models/RasterLayers"
|
||||
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"
|
||||
import IfHidden from "./Base/IfHidden.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import { OpenJosm } from "./BigComponents/OpenJosm"
|
||||
import MapillaryLink from "./BigComponents/MapillaryLink.svelte"
|
||||
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
|
||||
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"
|
||||
import StateIndicator from "./BigComponents/StateIndicator.svelte"
|
||||
import LanguagePicker from "./LanguagePicker"
|
||||
import Locale from "./i18n/Locale"
|
||||
import ShareScreen from "./BigComponents/ShareScreen.svelte"
|
||||
import UploadingImageCounter from "./Image/UploadingImageCounter.svelte"
|
||||
import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte"
|
||||
|
||||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
|
||||
let maplibremap: UIEventSource<MlMap> = state.map
|
||||
let selectedElement: UIEventSource<Feature> = state.selectedElement
|
||||
let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer
|
||||
let maplibremap: UIEventSource<MlMap> = state.map
|
||||
let selectedElement: UIEventSource<Feature> = state.selectedElement
|
||||
let selectedLayer: UIEventSource<LayerConfig> = state.selectedLayer
|
||||
|
||||
const selectedElementView = selectedElement.map(
|
||||
(selectedElement) => {
|
||||
// Svelte doesn't properly reload some of the legacy UI-elements
|
||||
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
|
||||
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
|
||||
const layer = selectedLayer.data
|
||||
if (selectedElement === undefined || layer === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const selectedElementView = selectedElement.map(
|
||||
(selectedElement) => {
|
||||
// Svelte doesn't properly reload some of the legacy UI-elements
|
||||
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
|
||||
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
|
||||
const layer = selectedLayer.data
|
||||
if (selectedElement === undefined || layer === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (!(layer.tagRenderings?.length > 0) || layer.title === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const tags = state.featureProperties.getStore(selectedElement.properties.id)
|
||||
return new SvelteUIElement(SelectedElementView, { state, layer, selectedElement, tags })
|
||||
},
|
||||
[selectedLayer],
|
||||
)
|
||||
const tags = state.featureProperties.getStore(selectedElement.properties.id)
|
||||
return new SvelteUIElement(SelectedElementView, { state, layer, selectedElement, tags })
|
||||
},
|
||||
[selectedLayer]
|
||||
)
|
||||
|
||||
const selectedElementTitle = selectedElement.map(
|
||||
(selectedElement) => {
|
||||
// Svelte doesn't properly reload some of the legacy UI-elements
|
||||
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
|
||||
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
|
||||
const layer = selectedLayer.data
|
||||
if (selectedElement === undefined || layer === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const selectedElementTitle = selectedElement.map(
|
||||
(selectedElement) => {
|
||||
// Svelte doesn't properly reload some of the legacy UI-elements
|
||||
// As such, we _reconstruct_ the selectedElementView every time a new feature is selected
|
||||
// This is a bit wasteful, but until everything is a svelte-component, this should do the trick
|
||||
const layer = selectedLayer.data
|
||||
if (selectedElement === undefined || layer === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const tags = state.featureProperties.getStore(selectedElement.properties.id)
|
||||
return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags })
|
||||
},
|
||||
[selectedLayer],
|
||||
)
|
||||
const tags = state.featureProperties.getStore(selectedElement.properties.id)
|
||||
return new SvelteUIElement(SelectedElementTitle, { state, layer, selectedElement, tags })
|
||||
},
|
||||
[selectedLayer]
|
||||
)
|
||||
|
||||
let mapproperties: MapProperties = state.mapProperties
|
||||
let featureSwitches: FeatureSwitchState = state.featureSwitches
|
||||
let availableLayers = state.availableLayers
|
||||
let userdetails = state.osmConnection.userDetails
|
||||
let currentViewLayer = layout.layers.find((l) => l.id === "current_view")
|
||||
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer
|
||||
let rasterLayerName =
|
||||
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name
|
||||
onDestroy(
|
||||
rasterLayer.addCallbackAndRunD((l) => {
|
||||
rasterLayerName = l.properties.name
|
||||
}),
|
||||
)
|
||||
let mapproperties: MapProperties = state.mapProperties
|
||||
let featureSwitches: FeatureSwitchState = state.featureSwitches
|
||||
let availableLayers = state.availableLayers
|
||||
let userdetails = state.osmConnection.userDetails
|
||||
let currentViewLayer = layout.layers.find((l) => l.id === "current_view")
|
||||
let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer
|
||||
let rasterLayerName =
|
||||
rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name
|
||||
onDestroy(
|
||||
rasterLayer.addCallbackAndRunD((l) => {
|
||||
rasterLayerName = l.properties.name
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="absolute top-0 left-0 h-screen w-screen overflow-hidden">
|
||||
|
@ -155,8 +155,8 @@
|
|||
<ToSvelte
|
||||
construct={() => new ExtraLinkButton(state, layout.extraLink).SetClass("pointer-events-auto")}
|
||||
/>
|
||||
<UploadingImageCounter {state} featureId="*" showThankYou={false}/>
|
||||
<PendingChangesIndicator {state}/>
|
||||
<UploadingImageCounter featureId="*" showThankYou={false} {state} />
|
||||
<PendingChangesIndicator {state} />
|
||||
<If condition={state.featureSwitchIsTesting}>
|
||||
<div class="alert w-fit">Testmode</div>
|
||||
</If>
|
||||
|
@ -174,7 +174,12 @@
|
|||
<div class="flex flex-col">
|
||||
<If condition={featureSwitches.featureSwitchEnableLogin}>
|
||||
{#if state.lastClickObject.hasPresets || state.lastClickObject.hasNoteLayer}
|
||||
<button class="w-fit pointer-events-auto" on:click={() => {state.openNewDialog()}}>
|
||||
<button
|
||||
class="pointer-events-auto w-fit"
|
||||
on:click={() => {
|
||||
state.openNewDialog()
|
||||
}}
|
||||
>
|
||||
{#if state.lastClickObject.hasPresets}
|
||||
<Tr t={Translations.t.general.add.title} />
|
||||
{:else}
|
||||
|
@ -197,9 +202,9 @@
|
|||
<a
|
||||
class="bg-black-transparent pointer-events-auto h-fit max-h-12 cursor-pointer self-end overflow-hidden rounded-2xl pl-1 pr-2 text-white opacity-50 hover:opacity-100"
|
||||
on:click={() => {
|
||||
state.guistate.themeViewTab.setData("copyright")
|
||||
state.guistate.themeIsOpened.setData(true)
|
||||
}}
|
||||
state.guistate.themeViewTab.setData("copyright")
|
||||
state.guistate.themeIsOpened.setData(true)
|
||||
}}
|
||||
>
|
||||
© OpenStreetMap, <span class="w-24">{rasterLayerName}</span>
|
||||
</a>
|
||||
|
@ -236,17 +241,18 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<LoginToggle ignoreLoading={true} {state }>
|
||||
<If condition={state.userRelatedState.showCrosshair.map(s => s === "yes")}>
|
||||
<If condition={state.mapProperties.zoom.map(z => z >= 17)}>
|
||||
<div class="absolute top-0 left-0 flex items-center justify-center pointer-events-none w-full h-full">
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
<If condition={state.userRelatedState.showCrosshair.map((s) => s === "yes")}>
|
||||
<If condition={state.mapProperties.zoom.map((z) => z >= 17)}>
|
||||
<div
|
||||
class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center justify-center"
|
||||
>
|
||||
<ToSvelte construct={Svg.cross_svg()} />
|
||||
</div>
|
||||
</If>
|
||||
</If>
|
||||
</LoginToggle>
|
||||
|
||||
|
||||
<If
|
||||
condition={selectedElementView.map(
|
||||
(v) =>
|
||||
|
@ -271,7 +277,6 @@
|
|||
</ModalRight>
|
||||
</If>
|
||||
|
||||
|
||||
<If
|
||||
condition={selectedElementView.map(
|
||||
(v) =>
|
||||
|
@ -293,7 +298,10 @@
|
|||
<!-- Theme menu -->
|
||||
<FloatOver on:close={() => state.guistate.themeIsOpened.setData(false)}>
|
||||
<span slot="close-button"><!-- Disable the close button --></span>
|
||||
<TabbedGroup condition1={state.featureSwitches.featureSwitchFilter} tab={state.guistate.themeViewTabIndex}>
|
||||
<TabbedGroup
|
||||
condition1={state.featureSwitches.featureSwitchFilter}
|
||||
tab={state.guistate.themeViewTabIndex}
|
||||
>
|
||||
<div slot="post-tablist">
|
||||
<XCircleIcon
|
||||
class="mr-2 h-8 w-8"
|
||||
|
@ -362,7 +370,11 @@
|
|||
|
||||
<IfHidden condition={state.guistate.backgroundLayerSelectionIsOpened}>
|
||||
<!-- background layer selector -->
|
||||
<FloatOver on:close={() => {state.guistate.backgroundLayerSelectionIsOpened.setData(false)}}>
|
||||
<FloatOver
|
||||
on:close={() => {
|
||||
state.guistate.backgroundLayerSelectionIsOpened.setData(false)
|
||||
}}
|
||||
>
|
||||
<div class="h-full p-2">
|
||||
<RasterLayerOverview
|
||||
{availableLayers}
|
||||
|
@ -377,11 +389,13 @@
|
|||
|
||||
<If condition={state.guistate.menuIsOpened}>
|
||||
<!-- Menu page -->
|
||||
<FloatOver on:close={() => state.guistate.menuIsOpened.setData(false) }>
|
||||
<FloatOver on:close={() => state.guistate.menuIsOpened.setData(false)}>
|
||||
<span slot="close-button"><!-- Hide the default close button --></span>
|
||||
<TabbedGroup condition1={featureSwitches.featureSwitchEnableLogin}
|
||||
condition2={state.featureSwitches. featureSwitchCommunityIndex}
|
||||
tab={state.guistate.menuViewTabIndex}>
|
||||
<TabbedGroup
|
||||
condition1={featureSwitches.featureSwitchEnableLogin}
|
||||
condition2={state.featureSwitches.featureSwitchCommunityIndex}
|
||||
tab={state.guistate.menuViewTabIndex}
|
||||
>
|
||||
<div slot="post-tablist">
|
||||
<XCircleIcon
|
||||
class="mr-2 h-8 w-8"
|
||||
|
@ -470,7 +484,7 @@
|
|||
<OpenIdEditor mapProperties={state.mapProperties} />
|
||||
<ToSvelte
|
||||
construct={() =>
|
||||
new OpenJosm(state.osmConnection, state.mapProperties.bounds).SetClass("w-full")}
|
||||
new OpenJosm(state.osmConnection, state.mapProperties.bounds).SetClass("w-full")}
|
||||
/>
|
||||
<MapillaryLink mapProperties={state.mapProperties} />
|
||||
</If>
|
||||
|
@ -480,5 +494,3 @@
|
|||
</TabbedGroup>
|
||||
</FloatOver>
|
||||
</If>
|
||||
|
||||
|
||||
|
|
|
@ -980,7 +980,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
*/
|
||||
public static downloadAdvanced(
|
||||
url: string,
|
||||
headers?: any
|
||||
headers?: any,
|
||||
method: "POST" | "GET" | "PUT" | "UPDATE" | "DELETE" | "OPTIONS" = "GET",
|
||||
content?: string
|
||||
): Promise<
|
||||
| { content: string }
|
||||
| { redirect: string }
|
||||
|
@ -1007,14 +1009,13 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
})
|
||||
}
|
||||
}
|
||||
xhr.open("GET", url)
|
||||
xhr.open(method, url)
|
||||
if (headers !== undefined) {
|
||||
for (const key in headers) {
|
||||
xhr.setRequestHeader(key, headers[key])
|
||||
}
|
||||
}
|
||||
|
||||
xhr.send()
|
||||
xhr.send(content)
|
||||
xhr.onerror = reject
|
||||
})
|
||||
}
|
||||
|
|
|
@ -671,14 +671,13 @@ class SvgToPdfPage {
|
|||
}
|
||||
|
||||
public async PrepareLanguage(language: string) {
|
||||
let host = window.location.host
|
||||
if (host.startsWith("127.0.0.1")) {
|
||||
host = "mapcomplete.org"
|
||||
}
|
||||
// Always fetch the remote data - it's cached anyway
|
||||
this.layerTranslations[language] = await Utils.downloadJsonCached(
|
||||
window.location.protocol +
|
||||
"//" +
|
||||
window.location.host +
|
||||
"/assets/langs/layers/" +
|
||||
language +
|
||||
".json",
|
||||
window.location.protocol + "//" + host + "/assets/langs/layers/" + language + ".json",
|
||||
24 * 60 * 60 * 1000
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"contributors": [
|
||||
{
|
||||
"commits": 6092,
|
||||
"commits": 6178,
|
||||
"contributor": "Pieter Vander Vennet"
|
||||
},
|
||||
{
|
||||
|
@ -30,14 +30,14 @@
|
|||
},
|
||||
{
|
||||
"commits": 30,
|
||||
"contributor": "paunofu"
|
||||
},
|
||||
{
|
||||
"commits": 29,
|
||||
"contributor": "Hosted Weblate"
|
||||
},
|
||||
{
|
||||
"commits": 27,
|
||||
"commits": 30,
|
||||
"contributor": "paunofu"
|
||||
},
|
||||
{
|
||||
"commits": 28,
|
||||
"contributor": "riQQ"
|
||||
},
|
||||
{
|
||||
|
@ -53,7 +53,7 @@
|
|||
"contributor": "Ward"
|
||||
},
|
||||
{
|
||||
"commits": 21,
|
||||
"commits": 23,
|
||||
"contributor": "dependabot[bot]"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -30,10 +30,6 @@
|
|||
"AT": [
|
||||
"de"
|
||||
],
|
||||
"AU": [
|
||||
"en",
|
||||
"en"
|
||||
],
|
||||
"AZ": [
|
||||
"az"
|
||||
],
|
||||
|
|
|
@ -3009,7 +3009,6 @@
|
|||
"_meta": {
|
||||
"countries": [
|
||||
"AG",
|
||||
"AU",
|
||||
"BB",
|
||||
"BI",
|
||||
"BN",
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"contributors": [
|
||||
{
|
||||
"commits": 311,
|
||||
"commits": 314,
|
||||
"contributor": "kjon"
|
||||
},
|
||||
{
|
||||
"commits": 288,
|
||||
"commits": 293,
|
||||
"contributor": "Pieter Vander Vennet"
|
||||
},
|
||||
{
|
||||
|
@ -17,7 +17,7 @@
|
|||
"contributor": "Allan Nordhøy"
|
||||
},
|
||||
{
|
||||
"commits": 70,
|
||||
"commits": 72,
|
||||
"contributor": "Robin van der Linde"
|
||||
},
|
||||
{
|
||||
|
@ -33,7 +33,7 @@
|
|||
"contributor": "Iago"
|
||||
},
|
||||
{
|
||||
"commits": 32,
|
||||
"commits": 33,
|
||||
"contributor": "Jiří Podhorecký"
|
||||
},
|
||||
{
|
||||
|
@ -60,6 +60,10 @@
|
|||
"commits": 22,
|
||||
"contributor": "Marco"
|
||||
},
|
||||
{
|
||||
"commits": 21,
|
||||
"contributor": "mcliquid"
|
||||
},
|
||||
{
|
||||
"commits": 21,
|
||||
"contributor": "SC"
|
||||
|
@ -68,14 +72,14 @@
|
|||
"commits": 18,
|
||||
"contributor": "el_libre como el chaval"
|
||||
},
|
||||
{
|
||||
"commits": 16,
|
||||
"contributor": "mcliquid"
|
||||
},
|
||||
{
|
||||
"commits": 15,
|
||||
"contributor": "WaldiS"
|
||||
},
|
||||
{
|
||||
"commits": 14,
|
||||
"contributor": "macpac"
|
||||
},
|
||||
{
|
||||
"commits": 14,
|
||||
"contributor": "LeJun"
|
||||
|
@ -96,6 +100,14 @@
|
|||
"commits": 13,
|
||||
"contributor": "Joost"
|
||||
},
|
||||
{
|
||||
"commits": 12,
|
||||
"contributor": "Piotr Strebski"
|
||||
},
|
||||
{
|
||||
"commits": 11,
|
||||
"contributor": "Jaime Marquínez Ferrándiz"
|
||||
},
|
||||
{
|
||||
"commits": 11,
|
||||
"contributor": "Túllio Franca"
|
||||
|
@ -132,10 +144,6 @@
|
|||
"commits": 9,
|
||||
"contributor": "deep map"
|
||||
},
|
||||
{
|
||||
"commits": 9,
|
||||
"contributor": "Jaime Marquínez Ferrándiz"
|
||||
},
|
||||
{
|
||||
"commits": 9,
|
||||
"contributor": "Fjuro"
|
||||
|
@ -148,6 +156,10 @@
|
|||
"commits": 9,
|
||||
"contributor": "Jacque Fresco"
|
||||
},
|
||||
{
|
||||
"commits": 8,
|
||||
"contributor": "gallegonovato"
|
||||
},
|
||||
{
|
||||
"commits": 8,
|
||||
"contributor": "Vinicius"
|
||||
|
@ -216,14 +228,6 @@
|
|||
"commits": 6,
|
||||
"contributor": "lvgx"
|
||||
},
|
||||
{
|
||||
"commits": 5,
|
||||
"contributor": "Piotr Strebski"
|
||||
},
|
||||
{
|
||||
"commits": 5,
|
||||
"contributor": "gallegonovato"
|
||||
},
|
||||
{
|
||||
"commits": 5,
|
||||
"contributor": "ⵣⵓⵀⵉⵔ ⴰⵎⴰⵣⵉⵖ ZOUHIR DEHBI"
|
||||
|
@ -350,7 +354,11 @@
|
|||
},
|
||||
{
|
||||
"commits": 2,
|
||||
"contributor": "macpac"
|
||||
"contributor": "Michel"
|
||||
},
|
||||
{
|
||||
"commits": 2,
|
||||
"contributor": "Kelson Vibber"
|
||||
},
|
||||
{
|
||||
"commits": 2,
|
||||
|
@ -450,11 +458,11 @@
|
|||
},
|
||||
{
|
||||
"commits": 1,
|
||||
"contributor": "Michal Čermák"
|
||||
"contributor": "Julio Salas"
|
||||
},
|
||||
{
|
||||
"commits": 1,
|
||||
"contributor": "Kelson Vibber"
|
||||
"contributor": "Michal Čermák"
|
||||
},
|
||||
{
|
||||
"commits": 1,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue