Merge develop

This commit is contained in:
Pieter Vander Vennet 2023-10-30 16:32:43 +01:00
commit 29ff09024f
287 changed files with 14955 additions and 4036 deletions

View file

@ -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) {

View file

@ -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) {

View file

@ -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",

View file

@ -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

View file

@ -367,7 +367,7 @@ export class ChangesetHandler {
].map(([key, value]) => ({
key,
value,
aggretage: false,
aggregate: false,
}))
}

View file

@ -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() {

View file

@ -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... "

View file

@ -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,

View file

@ -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
}

View file

@ -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 = [

View file

@ -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
*

View file

@ -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>
}

View file

@ -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)

View file

@ -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."
)

View file

@ -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) => {

View file

@ -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

View file

@ -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>

View file

@ -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">

View file

@ -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())

View file

@ -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>

View file

@ -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, {

View file

@ -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>

View file

@ -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

View file

@ -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">

View file

@ -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">

View file

@ -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)
}

View file

@ -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}

View file

@ -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)} />

View file

@ -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}

View file

@ -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)))
}

View file

@ -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(

View file

@ -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()

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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">

View file

@ -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(

View file

@ -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}

View file

@ -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}>

View file

@ -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],
})
},
},

View file

@ -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>

View file

@ -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
})
}

View file

@ -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
)
}

View file

@ -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]"
},
{

View file

@ -30,10 +30,6 @@
"AT": [
"de"
],
"AU": [
"en",
"en"
],
"AZ": [
"az"
],

View file

@ -3009,7 +3009,6 @@
"_meta": {
"countries": [
"AG",
"AU",
"BB",
"BI",
"BN",

File diff suppressed because one or more lines are too long

View file

@ -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,