forked from MapComplete/MapComplete
Merge branch 'master' into feature/studio
This commit is contained in:
commit
6ff2c629f0
994 changed files with 5917 additions and 4262 deletions
|
@ -21,11 +21,9 @@ import questions from "../assets/generated/layers/questions.json"
|
|||
import {
|
||||
DoesImageExist,
|
||||
PrevalidateTheme,
|
||||
ValidateTagRenderings,
|
||||
ValidateThemeAndLayers,
|
||||
} from "../Models/ThemeConfig/Conversion/Validation"
|
||||
import { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion"
|
||||
import { RewriteSpecial } from "../Models/ThemeConfig/Conversion/PrepareLayer"
|
||||
import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"
|
||||
import Hash from "./Web/Hash"
|
||||
|
||||
|
|
|
@ -10,7 +10,12 @@ import { Utils } from "../../Utils"
|
|||
class FeatureSwitchUtils {
|
||||
static initSwitch(key: string, deflt: boolean, documentation: string): UIEventSource<boolean> {
|
||||
const defaultValue = deflt
|
||||
const queryParam = QueryParameters.GetQueryParameter(key, "" + defaultValue, documentation)
|
||||
const queryParam = QueryParameters.GetQueryParameter(
|
||||
key,
|
||||
"" + defaultValue,
|
||||
documentation,
|
||||
{ stackOffset: -1 }
|
||||
)
|
||||
|
||||
// It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened
|
||||
return queryParam.sync(
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { UIEventSource } from "../UIEventSource"
|
||||
import Hash from "./Hash"
|
||||
import { Utils } from "../../Utils"
|
||||
import doc = Mocha.reporters.doc
|
||||
|
||||
export class QueryParameters {
|
||||
static defaults: Record<string, string> = {}
|
||||
|
@ -16,11 +17,27 @@ export class QueryParameters {
|
|||
public static GetQueryParameter(
|
||||
key: string,
|
||||
deflt: string,
|
||||
documentation?: string
|
||||
documentation?: string,
|
||||
options?: {
|
||||
stackOffset?: number
|
||||
}
|
||||
): UIEventSource<string> {
|
||||
if (!this.initialized) {
|
||||
this.init()
|
||||
}
|
||||
|
||||
if (Utils.runningFromConsole) {
|
||||
const location = Utils.getLocationInCode(-1 + (options?.stackOffset ?? 0))
|
||||
|
||||
documentation +=
|
||||
"\n\nThis documentation is defined in the source code at [" +
|
||||
location.filename +
|
||||
"](" +
|
||||
location.markdownLocation +
|
||||
")" +
|
||||
"\n\n"
|
||||
}
|
||||
|
||||
QueryParameters.documentation.set(key, documentation)
|
||||
if (deflt !== undefined) {
|
||||
QueryParameters.defaults[key] = deflt
|
||||
|
@ -49,7 +66,7 @@ export class QueryParameters {
|
|||
documentation?: string
|
||||
): UIEventSource<boolean> {
|
||||
return UIEventSource.asBoolean(
|
||||
QueryParameters.GetQueryParameter(key, "" + deflt, documentation)
|
||||
QueryParameters.GetQueryParameter(key, "" + deflt, documentation, { stackOffset: -1 })
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import {Store, UIEventSource} from "../Logic/UIEventSource"
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import LayerConfig from "./ThemeConfig/LayerConfig"
|
||||
import {OsmConnection} from "../Logic/Osm/OsmConnection"
|
||||
import {LocalStorageSource} from "../Logic/Web/LocalStorageSource"
|
||||
import {QueryParameters} from "../Logic/Web/QueryParameters"
|
||||
import {FilterConfigOption} from "./ThemeConfig/FilterConfig"
|
||||
import {TagsFilter} from "../Logic/Tags/TagsFilter"
|
||||
import {Utils} from "../Utils"
|
||||
import {TagUtils} from "../Logic/Tags/TagUtils"
|
||||
import {And} from "../Logic/Tags/And"
|
||||
import {GlobalFilter} from "./GlobalFilter"
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
|
||||
import { QueryParameters } from "../Logic/Web/QueryParameters"
|
||||
import { FilterConfigOption } from "./ThemeConfig/FilterConfig"
|
||||
import { TagsFilter } from "../Logic/Tags/TagsFilter"
|
||||
import { Utils } from "../Utils"
|
||||
import { TagUtils } from "../Logic/Tags/TagUtils"
|
||||
import { And } from "../Logic/Tags/And"
|
||||
import { GlobalFilter } from "./GlobalFilter"
|
||||
|
||||
export default class FilteredLayer {
|
||||
/**
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
import { DesugaringStep, Each, Fuse, On } from "./Conversion"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
import {DesugaringStep, Each, Fuse, On} from "./Conversion"
|
||||
import {LayerConfigJson} from "../Json/LayerConfigJson"
|
||||
import LayerConfig from "../LayerConfig"
|
||||
import { Utils } from "../../../Utils"
|
||||
import {Utils} from "../../../Utils"
|
||||
import Constants from "../../Constants"
|
||||
import { Translation } from "../../../UI/i18n/Translation"
|
||||
import { LayoutConfigJson } from "../Json/LayoutConfigJson"
|
||||
import {Translation} from "../../../UI/i18n/Translation"
|
||||
import {LayoutConfigJson} from "../Json/LayoutConfigJson"
|
||||
import LayoutConfig from "../LayoutConfig"
|
||||
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
import { ExtractImages } from "./FixImages"
|
||||
import { And } from "../../../Logic/Tags/And"
|
||||
import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson"
|
||||
import {TagUtils} from "../../../Logic/Tags/TagUtils"
|
||||
import {ExtractImages} from "./FixImages"
|
||||
import {And} from "../../../Logic/Tags/And"
|
||||
import Translations from "../../../UI/i18n/Translations"
|
||||
import Svg from "../../../Svg"
|
||||
import FilterConfigJson from "../Json/FilterConfigJson"
|
||||
import DeleteConfig from "../DeleteConfig"
|
||||
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
|
||||
import {QuestionableTagRenderingConfigJson} from "../Json/QuestionableTagRenderingConfigJson"
|
||||
import Validators from "../../../UI/InputElement/Validators"
|
||||
import TagRenderingConfig from "../TagRenderingConfig";
|
||||
|
||||
class ValidateLanguageCompleteness extends DesugaringStep<any> {
|
||||
private readonly _languages: string[]
|
||||
|
@ -46,12 +47,12 @@ class ValidateLanguageCompleteness extends DesugaringStep<any> {
|
|||
.forEach((missing) => {
|
||||
errors.push(
|
||||
context +
|
||||
"A theme should be translation-complete for " +
|
||||
neededLanguage +
|
||||
", but it lacks a translation for " +
|
||||
missing.context +
|
||||
".\n\tThe known translation is " +
|
||||
missing.tr.textFor("en")
|
||||
"A theme should be translation-complete for " +
|
||||
neededLanguage +
|
||||
", but it lacks a translation for " +
|
||||
missing.context +
|
||||
".\n\tThe known translation is " +
|
||||
missing.tr.textFor("en")
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -85,7 +86,7 @@ export class DoesImageExist extends DesugaringStep<string> {
|
|||
context: string
|
||||
): { result: string; errors?: string[]; warnings?: string[]; information?: string[] } {
|
||||
if (this._ignore?.has(image)) {
|
||||
return { result: image }
|
||||
return {result: image}
|
||||
}
|
||||
|
||||
const errors = []
|
||||
|
@ -93,22 +94,22 @@ export class DoesImageExist extends DesugaringStep<string> {
|
|||
const information = []
|
||||
if (image.indexOf("{") >= 0) {
|
||||
information.push("Ignoring image with { in the path: " + image)
|
||||
return { result: image }
|
||||
return {result: image}
|
||||
}
|
||||
|
||||
if (image === "assets/SocialImage.png") {
|
||||
return { result: image }
|
||||
return {result: image}
|
||||
}
|
||||
if (image.match(/[a-z]*/)) {
|
||||
if (Svg.All[image + ".svg"] !== undefined) {
|
||||
// This is a builtin img, e.g. 'checkmark' or 'crosshair'
|
||||
return { result: image }
|
||||
return {result: image}
|
||||
}
|
||||
}
|
||||
|
||||
if (image.startsWith("<") && image.endsWith(">")) {
|
||||
// This is probably HTML, you're on your own here
|
||||
return { result: image }
|
||||
return {result: image}
|
||||
}
|
||||
|
||||
if (!this._knownImagePaths.has(image)) {
|
||||
|
@ -177,15 +178,15 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
if (json["units"] !== undefined) {
|
||||
errors.push(
|
||||
"The theme " +
|
||||
json.id +
|
||||
" has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) "
|
||||
json.id +
|
||||
" has units defined - these should be defined on the layer instead. (Hint: use overrideAll: { '+units': ... }) "
|
||||
)
|
||||
}
|
||||
if (json["roamingRenderings"] !== undefined) {
|
||||
errors.push(
|
||||
"Theme " +
|
||||
json.id +
|
||||
" contains an old 'roamingRenderings'. Use an 'overrideAll' instead"
|
||||
json.id +
|
||||
" contains an old 'roamingRenderings'. Use an 'overrideAll' instead"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -197,10 +198,10 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
for (const remoteImage of remoteImages) {
|
||||
errors.push(
|
||||
"Found a remote image: " +
|
||||
remoteImage +
|
||||
" in theme " +
|
||||
json.id +
|
||||
", please download it."
|
||||
remoteImage +
|
||||
" in theme " +
|
||||
json.id +
|
||||
", please download it."
|
||||
)
|
||||
}
|
||||
for (const image of images) {
|
||||
|
@ -227,12 +228,12 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
|
|||
if (theme.id !== filename) {
|
||||
errors.push(
|
||||
"Theme ids should be the same as the name.json, but we got id: " +
|
||||
theme.id +
|
||||
" and filename " +
|
||||
filename +
|
||||
" (" +
|
||||
this._path +
|
||||
")"
|
||||
theme.id +
|
||||
" and filename " +
|
||||
filename +
|
||||
" (" +
|
||||
this._path +
|
||||
")"
|
||||
)
|
||||
}
|
||||
this._validateImage.convertJoin(
|
||||
|
@ -312,7 +313,7 @@ class OverrideShadowingCheck extends DesugaringStep<LayoutConfigJson> {
|
|||
): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } {
|
||||
const overrideAll = json.overrideAll
|
||||
if (overrideAll === undefined) {
|
||||
return { result: json }
|
||||
return {result: json}
|
||||
}
|
||||
|
||||
const errors = []
|
||||
|
@ -339,7 +340,7 @@ class OverrideShadowingCheck extends DesugaringStep<LayoutConfigJson> {
|
|||
}
|
||||
}
|
||||
|
||||
return { result: json, errors }
|
||||
return {result: json, errors}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -383,6 +384,51 @@ export class PrevalidateTheme extends Fuse<LayoutConfigJson> {
|
|||
}
|
||||
}
|
||||
|
||||
export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingConfigJson> {
|
||||
constructor() {
|
||||
super("The `if`-part in a mapping might set some keys. Those key are not allowed to be set in the `addExtraTags`, as this might result in conflicting values", [], "DetectConflictingAddExtraTags");
|
||||
}
|
||||
|
||||
convert(json: TagRenderingConfigJson, context: string): {
|
||||
result: TagRenderingConfigJson;
|
||||
errors?: string[];
|
||||
warnings?: string[];
|
||||
information?: string[]
|
||||
} {
|
||||
|
||||
if (!(json.mappings?.length > 0)) {
|
||||
return {result: json}
|
||||
}
|
||||
|
||||
const tagRendering = new TagRenderingConfig(json)
|
||||
|
||||
const errors = []
|
||||
for (let i = 0; i < tagRendering.mappings.length; i++) {
|
||||
const mapping = tagRendering.mappings[i];
|
||||
if (!mapping.addExtraTags) {
|
||||
continue
|
||||
}
|
||||
const keysInMapping = new Set(mapping.if.usedKeys())
|
||||
|
||||
const keysInAddExtraTags = mapping.addExtraTags.map(t => t.key)
|
||||
|
||||
const duplicateKeys = keysInAddExtraTags.filter(k => keysInMapping.has(k))
|
||||
if (duplicateKeys.length > 0) {
|
||||
errors.push(
|
||||
"At " + context + ".mappings[" + i + "]: AddExtraTags overrides a key that is set in the `if`-clause of this mapping. Selecting this answer might thus first set one value (needed to match as answer) and then override it with a different value, resulting in an unsaveable question. The offending `addExtraTags` is " + duplicateKeys.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
result: json,
|
||||
errors
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJson> {
|
||||
private readonly _calculatedTagNames: string[]
|
||||
|
||||
|
@ -449,7 +495,7 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
|
|||
const errors = []
|
||||
const warnings = []
|
||||
if (json.mappings === undefined || json.mappings.length === 0) {
|
||||
return { result: json }
|
||||
return {result: json}
|
||||
}
|
||||
const defaultProperties = {}
|
||||
for (const calculatedTagName of this._calculatedTagNames) {
|
||||
|
@ -475,7 +521,7 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
|
|||
}
|
||||
const keyValues = parsedConditions[i].asChange(defaultProperties)
|
||||
const properties = {}
|
||||
keyValues.forEach(({ k, v }) => {
|
||||
keyValues.forEach(({k, v}) => {
|
||||
properties[k] = v
|
||||
})
|
||||
for (let j = 0; j < i; j++) {
|
||||
|
@ -492,10 +538,10 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
|
|||
// The current mapping is shadowed!
|
||||
errors.push(`At ${context}: Mapping ${i} is shadowed by mapping ${j} and will thus never be shown:
|
||||
The mapping ${parsedConditions[i].asHumanString(
|
||||
false,
|
||||
false,
|
||||
{}
|
||||
)} is fully matched by a previous mapping (namely ${j}), which matches:
|
||||
false,
|
||||
false,
|
||||
{}
|
||||
)} is fully matched by a previous mapping (namely ${j}), which matches:
|
||||
${parsedConditions[j].asHumanString(false, false, {})}.
|
||||
|
||||
To fix this problem, you can try to:
|
||||
|
@ -564,7 +610,7 @@ export class DetectMappingsWithImages extends DesugaringStep<TagRenderingConfigJ
|
|||
const warnings: string[] = []
|
||||
const information: string[] = []
|
||||
if (json.mappings === undefined || json.mappings.length === 0) {
|
||||
return { result: json }
|
||||
return {result: json}
|
||||
}
|
||||
const ignoreToken = "ignore-image-in-then"
|
||||
for (let i = 0; i < json.mappings.length; i++) {
|
||||
|
@ -626,17 +672,17 @@ class MiscTagRenderingChecks extends DesugaringStep<TagRenderingConfigJson> {
|
|||
if (json["special"] !== undefined) {
|
||||
errors.push(
|
||||
"At " +
|
||||
context +
|
||||
': detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`'
|
||||
context +
|
||||
': detected `special` on the top level. Did you mean `{"render":{ "special": ... }}`'
|
||||
)
|
||||
}
|
||||
if (json["group"]) {
|
||||
errors.push(
|
||||
"At " +
|
||||
context +
|
||||
': groups are deprecated, use `"label": ["' +
|
||||
json["group"] +
|
||||
'"]` instead'
|
||||
context +
|
||||
': groups are deprecated, use `"label": ["' +
|
||||
json["group"] +
|
||||
'"]` instead'
|
||||
)
|
||||
}
|
||||
const freeformType = json["freeform"]?.["type"]
|
||||
|
@ -669,6 +715,7 @@ export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
|
|||
super(
|
||||
"Various validation on tagRenderingConfigs",
|
||||
new DetectShadowedMappings(layerConfig),
|
||||
new DetectConflictingAddExtraTags(),
|
||||
new DetectMappingsWithImages(doesImageExist),
|
||||
new MiscTagRenderingChecks(options)
|
||||
)
|
||||
|
@ -711,9 +758,9 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
if (!Constants.priviliged_layers.find((x) => x == json.id)) {
|
||||
errors.push(
|
||||
context +
|
||||
": layer " +
|
||||
json.id +
|
||||
" uses 'special' as source.osmTags. However, this layer is not a priviliged layer"
|
||||
": layer " +
|
||||
json.id +
|
||||
" uses 'special' as source.osmTags. However, this layer is not a priviliged layer"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -722,13 +769,13 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
if (json.title === undefined && json.source !== "special:library") {
|
||||
errors.push(
|
||||
context +
|
||||
": this layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error."
|
||||
": this layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error."
|
||||
)
|
||||
}
|
||||
if (json.title === null) {
|
||||
information.push(
|
||||
context +
|
||||
": title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set."
|
||||
": title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -755,9 +802,9 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
console.log(json.tagRenderings)
|
||||
errors.push(
|
||||
"At " +
|
||||
context +
|
||||
": some tagrenderings have a duplicate id: " +
|
||||
duplicates.join(", ")
|
||||
context +
|
||||
": some tagrenderings have a duplicate id: " +
|
||||
duplicates.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -775,8 +822,8 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
if (json["overpassTags"] !== undefined) {
|
||||
errors.push(
|
||||
"Layer " +
|
||||
json.id +
|
||||
'still uses the old \'overpassTags\'-format. Please use "source": {"osmTags": <tags>}\' instead of "overpassTags": <tags> (note: this isn\'t your fault, the custom theme generator still spits out the old format)'
|
||||
json.id +
|
||||
'still uses the old \'overpassTags\'-format. Please use "source": {"osmTags": <tags>}\' instead of "overpassTags": <tags> (note: this isn\'t your fault, the custom theme generator still spits out the old format)'
|
||||
)
|
||||
}
|
||||
const forbiddenTopLevel = [
|
||||
|
@ -794,18 +841,18 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
if (json[forbiddenKey] !== undefined)
|
||||
errors.push(
|
||||
context +
|
||||
": layer " +
|
||||
json.id +
|
||||
" still has a forbidden key " +
|
||||
forbiddenKey
|
||||
": layer " +
|
||||
json.id +
|
||||
" still has a forbidden key " +
|
||||
forbiddenKey
|
||||
)
|
||||
}
|
||||
if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
|
||||
errors.push(
|
||||
context +
|
||||
": layer " +
|
||||
json.id +
|
||||
" contains an old 'hideUnderlayingFeaturesMinPercentage'"
|
||||
": layer " +
|
||||
json.id +
|
||||
" contains an old 'hideUnderlayingFeaturesMinPercentage'"
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -822,9 +869,9 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
if (this._path != undefined && this._path.indexOf(expected) < 0) {
|
||||
errors.push(
|
||||
"Layer is in an incorrect place. The path is " +
|
||||
this._path +
|
||||
", but expected " +
|
||||
expected
|
||||
this._path +
|
||||
", but expected " +
|
||||
expected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -865,6 +912,13 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
}
|
||||
}
|
||||
|
||||
if (json.filter) {
|
||||
const r = new On("filter", new Each( new ValidateFilter())).convert(json, context)
|
||||
warnings.push(...(r.warnings ?? []))
|
||||
errors.push(...(r.errors ?? []))
|
||||
information.push(...(r.information ?? []))
|
||||
}
|
||||
|
||||
if (json.tagRenderings !== undefined) {
|
||||
const r = new On(
|
||||
"tagRenderings",
|
||||
|
@ -886,9 +940,9 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
if (hasCondition?.length > 0) {
|
||||
errors.push(
|
||||
"At " +
|
||||
context +
|
||||
":\n One or more icons in the mapRenderings have a condition set. Don't do this, as this will result in an invisible but clickable element. Use extra filters in the source instead. The offending mapRenderings are:\n" +
|
||||
JSON.stringify(hasCondition, null, " ")
|
||||
context +
|
||||
":\n One or more icons in the mapRenderings have a condition set. Don't do this, as this will result in an invisible but clickable element. Use extra filters in the source instead. The offending mapRenderings are:\n" +
|
||||
JSON.stringify(hasCondition, null, " ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -903,7 +957,7 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
const preset = json.presets[i]
|
||||
const tags: { k: string; v: string }[] = new And(
|
||||
preset.tags.map((t) => TagUtils.Tag(t))
|
||||
).asChange({ id: "node/-1" })
|
||||
).asChange({id: "node/-1"})
|
||||
const properties = {}
|
||||
for (const tag of tags) {
|
||||
properties[tag.k] = tag.v
|
||||
|
@ -912,12 +966,12 @@ export class ValidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
if (!doMatch) {
|
||||
errors.push(
|
||||
context +
|
||||
".presets[" +
|
||||
i +
|
||||
"]: This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " +
|
||||
JSON.stringify(properties) +
|
||||
"\n The required tags are: " +
|
||||
baseTags.asHumanString(false, false, {})
|
||||
".presets[" +
|
||||
i +
|
||||
"]: This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n A newly created point will have properties: " +
|
||||
JSON.stringify(properties) +
|
||||
"\n The required tags are: " +
|
||||
baseTags.asHumanString(false, false, {})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -949,9 +1003,14 @@ export class ValidateFilter extends DesugaringStep<FilterConfigJson> {
|
|||
warnings?: string[]
|
||||
information?: string[]
|
||||
} {
|
||||
if (typeof filter === "string") {
|
||||
// Calling another filter, we skip
|
||||
return {result: filter}
|
||||
}
|
||||
const errors = []
|
||||
for (const option of filter.options) {
|
||||
for (let i = 0; i < option.fields.length; i++) {
|
||||
|
||||
for (let i = 0; i < option.fields?.length ?? 0; i++) {
|
||||
const field = option.fields[i]
|
||||
const type = field.type ?? "string"
|
||||
if (Validators.availableTypes.find((t) => t === type) === undefined) {
|
||||
|
@ -962,7 +1021,7 @@ export class ValidateFilter extends DesugaringStep<FilterConfigJson> {
|
|||
}
|
||||
}
|
||||
}
|
||||
return { result: filter, errors }
|
||||
return {result: filter, errors}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -991,7 +1050,7 @@ export class DetectDuplicateFilters extends DesugaringStep<{
|
|||
const warnings: string[] = []
|
||||
const information: string[] = []
|
||||
|
||||
const { layers, themes } = json
|
||||
const {layers, themes} = json
|
||||
const perOsmTag = new Map<
|
||||
string,
|
||||
{
|
||||
|
@ -1027,7 +1086,7 @@ export class DetectDuplicateFilters extends DesugaringStep<{
|
|||
return
|
||||
}
|
||||
let msg = "Possible duplicate filter: " + key
|
||||
for (const { filter, layer, layout } of value) {
|
||||
for (const {filter, layer, layout} of value) {
|
||||
let id = ""
|
||||
if (layout !== undefined) {
|
||||
id = layout.id + ":"
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import type { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import SubtleLink from "../Base/SubtleLink.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
export let theme: LayoutInformation
|
||||
export let isCustom: boolean = false
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<div class="gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each filteredThemes as theme (theme.id)}
|
||||
{#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)}
|
||||
<!-- TODO: doesn't work if first theme is hidden -->
|
||||
<!-- TODO: doesn't work if first theme is hidden -->
|
||||
{#if theme === firstTheme && !isCustom && $search !== "" && $search !== undefined}
|
||||
<ThemeButton
|
||||
{theme}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import {InputElement} from "./InputElement"
|
||||
import {UIEventSource} from "../../Logic/UIEventSource"
|
||||
import { InputElement } from "./InputElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
|
@ -67,20 +67,18 @@ export default class FileSelectorButton extends InputElement<FileList> {
|
|||
if (actualInputElement.files !== null) {
|
||||
self._value.setData(actualInputElement.files)
|
||||
}
|
||||
actualInputElement.classList.remove("glowing-shadow");
|
||||
actualInputElement.classList.remove("glowing-shadow")
|
||||
|
||||
e.preventDefault()
|
||||
})
|
||||
|
||||
el.appendChild(actualInputElement)
|
||||
|
||||
function setDrawAttention(isOn: boolean){
|
||||
if(isOn){
|
||||
function setDrawAttention(isOn: boolean) {
|
||||
if (isOn) {
|
||||
label.classList.add("glowing-shadow")
|
||||
|
||||
}else{
|
||||
} else {
|
||||
label.classList.remove("glowing-shadow")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,10 +88,9 @@ export default class FileSelectorButton extends InputElement<FileList> {
|
|||
setDrawAttention(true)
|
||||
// Style the drag-and-drop as a "copy file" operation.
|
||||
event.dataTransfer.dropEffect = "copy"
|
||||
|
||||
})
|
||||
|
||||
window.document.addEventListener("dragenter", () =>{
|
||||
window.document.addEventListener("dragenter", () => {
|
||||
setDrawAttention(true)
|
||||
})
|
||||
|
||||
|
@ -101,7 +98,6 @@ export default class FileSelectorButton extends InputElement<FileList> {
|
|||
setDrawAttention(false)
|
||||
})
|
||||
|
||||
|
||||
el.addEventListener("drop", (event) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
|
|
|
@ -1,79 +1,73 @@
|
|||
<script lang="ts">
|
||||
import {onDestroy, onMount} from "svelte"
|
||||
import * as maplibre from "maplibre-gl"
|
||||
import type {Map} from "maplibre-gl"
|
||||
import type {Readable, Writable} from "svelte/store"
|
||||
import {get, writable} from "svelte/store"
|
||||
import {AvailableRasterLayers} from "../../Models/RasterLayers"
|
||||
import {Utils} from "../../Utils";
|
||||
import { onDestroy, onMount } from "svelte"
|
||||
import * as maplibre from "maplibre-gl"
|
||||
import type { Map } from "maplibre-gl"
|
||||
import type { Readable, Writable } from "svelte/store"
|
||||
import { get, writable } from "svelte/store"
|
||||
import { AvailableRasterLayers } from "../../Models/RasterLayers"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
/**
|
||||
* The 'MaplibreMap' maps various event sources onto MapLibre.
|
||||
*/
|
||||
/**
|
||||
* The 'MaplibreMap' maps various event sources onto MapLibre.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Beware: this map will _only_ be set by this component
|
||||
* It should thus be treated as a 'store' by external parties
|
||||
*/
|
||||
export let map: Writable<Map>
|
||||
|
||||
/**
|
||||
* Beware: this map will _only_ be set by this component
|
||||
* It should thus be treated as a 'store' by external parties
|
||||
*/
|
||||
export let map: Writable<Map>
|
||||
let container: HTMLElement
|
||||
|
||||
let container: HTMLElement
|
||||
export let attribution = false
|
||||
export let center: { lng: number; lat: number } | Readable<{ lng: number; lat: number }> =
|
||||
writable({ lng: 0, lat: 0 })
|
||||
export let zoom: Readable<number> = writable(1)
|
||||
|
||||
const styleUrl = AvailableRasterLayers.maplibre.properties.url
|
||||
|
||||
export let attribution = false
|
||||
export let center: {lng: number, lat: number} | Readable<{ lng: number; lat: number }> = writable({lng: 0, lat: 0})
|
||||
export let zoom: Readable<number> = writable(1)
|
||||
|
||||
const styleUrl = AvailableRasterLayers.maplibre.properties.url
|
||||
|
||||
let _map: Map
|
||||
onMount(() => {
|
||||
|
||||
let _center: {lng: number, lat: number}
|
||||
if(typeof center["lng"] === "number" && typeof center["lat"] === "number"){
|
||||
_center = <any> center
|
||||
}else{
|
||||
_center = get(<any> center)
|
||||
}
|
||||
|
||||
|
||||
_map = new maplibre.Map({
|
||||
container,
|
||||
style: styleUrl,
|
||||
zoom: get(zoom),
|
||||
center: _center,
|
||||
maxZoom: 24,
|
||||
interactive: true,
|
||||
attributionControl: false,
|
||||
|
||||
});
|
||||
|
||||
_map.on("load", function () {
|
||||
_map.resize()
|
||||
})
|
||||
map.set(_map)
|
||||
let _map: Map
|
||||
onMount(() => {
|
||||
let _center: { lng: number; lat: number }
|
||||
if (typeof center["lng"] === "number" && typeof center["lat"] === "number") {
|
||||
_center = <any>center
|
||||
} else {
|
||||
_center = get(<any>center)
|
||||
}
|
||||
|
||||
_map = new maplibre.Map({
|
||||
container,
|
||||
style: styleUrl,
|
||||
zoom: get(zoom),
|
||||
center: _center,
|
||||
maxZoom: 24,
|
||||
interactive: true,
|
||||
attributionControl: false,
|
||||
})
|
||||
onDestroy(async () => {
|
||||
await Utils.waitFor(250);
|
||||
if (_map) _map.remove();
|
||||
map = null;
|
||||
});
|
||||
|
||||
_map.on("load", function () {
|
||||
_map.resize()
|
||||
})
|
||||
map.set(_map)
|
||||
})
|
||||
onDestroy(async () => {
|
||||
await Utils.waitFor(250)
|
||||
if (_map) _map.remove()
|
||||
map = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link
|
||||
href="./maplibre-gl.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link href="./maplibre-gl.css" rel="stylesheet" />
|
||||
</svelte:head>
|
||||
|
||||
<div bind:this={container} class="map" id="map" style=" position: relative;
|
||||
<div
|
||||
bind:this={container}
|
||||
class="map"
|
||||
id="map"
|
||||
style=" position: relative;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;"></div>
|
||||
|
||||
|
||||
height: 100%;"
|
||||
/>
|
||||
|
|
|
@ -161,13 +161,16 @@ class PointRenderingLayer {
|
|||
})
|
||||
}
|
||||
|
||||
const marker = new Marker({ element: el}).setLngLat(loc).setOffset(iconAnchor).addTo(this._map)
|
||||
const marker = new Marker({ element: el })
|
||||
.setLngLat(loc)
|
||||
.setOffset(iconAnchor)
|
||||
.addTo(this._map)
|
||||
store
|
||||
.map((tags) => this._config.pitchAlignment.GetRenderValue(tags).Subs(tags).txt)
|
||||
.addCallbackAndRun((pitchAligment) => marker.setPitchAlignment(<any> pitchAligment))
|
||||
.addCallbackAndRun((pitchAligment) => marker.setPitchAlignment(<any>pitchAligment))
|
||||
store
|
||||
.map((tags) => this._config.rotationAlignment.GetRenderValue(tags).Subs(tags).txt)
|
||||
.addCallbackAndRun((pitchAligment) => marker.setRotationAlignment(<any> pitchAligment))
|
||||
.addCallbackAndRun((pitchAligment) => marker.setRotationAlignment(<any>pitchAligment))
|
||||
if (feature.geometry.type === "Point") {
|
||||
// When the tags get 'pinged', check that the location didn't change
|
||||
store.addCallbackAndRunD(() => {
|
||||
|
@ -458,7 +461,6 @@ export default class ShowDataLayer {
|
|||
features: FeatureSource,
|
||||
doShowLayer?: Store<boolean>
|
||||
): ShowDataLayer {
|
||||
|
||||
return new ShowDataLayer(map, {
|
||||
layer: ShowDataLayer.rangeLayer,
|
||||
features,
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="inline-flex flex-col w-full">
|
||||
<div class="inline-flex w-full flex-col">
|
||||
{#if inline}
|
||||
<Inline key={config.freeform.key} {tags} template={config.render}>
|
||||
<ValidatedInput
|
||||
|
|
|
@ -413,7 +413,7 @@
|
|||
|
||||
<div class="flex" slot="title2">
|
||||
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} />
|
||||
<Tr t={Translations.t.communityIndex.title}/>
|
||||
<Tr t={Translations.t.communityIndex.title} />
|
||||
</div>
|
||||
<div class="m-2" slot="content2">
|
||||
<CommunityIndexView location={state.mapProperties.location} />
|
||||
|
|
|
@ -39,7 +39,7 @@ export default class Locale {
|
|||
"The language to display MapComplete in.",
|
||||
"The user display language is determined in the following order:",
|
||||
"",
|
||||
"1. Use the language as set by the URL-parameter `language`. This will _disable_ setting the language by the user",
|
||||
"1. Use the language as set by the URL-parameter `language` (following ISO 639-1 | ex. `language=nl`). This will _disable_ setting the language by the user",
|
||||
"2. If the user did log in and did set their language before with MapComplete, use this language. This language selection is synchronized accross devices using the openstreetmap.org user preferences.",
|
||||
"3. If the user visited MapComplete before and did change their language manually, this changed language will be saved in local storage. Use the language from local storage",
|
||||
"4. Use the navigator-language (if available)",
|
||||
|
|
89
src/Utils.ts
89
src/Utils.ts
|
@ -1,5 +1,5 @@
|
|||
import colors from "./assets/colors.json"
|
||||
import {HTMLElement} from "node-html-parser"
|
||||
import { HTMLElement } from "node-html-parser"
|
||||
|
||||
export class Utils {
|
||||
/**
|
||||
|
@ -459,7 +459,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
|
||||
let match = txt.match(regex)
|
||||
|
||||
if(!match){
|
||||
if (!match) {
|
||||
return txt
|
||||
}
|
||||
let result = ""
|
||||
|
@ -502,7 +502,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
|
||||
result += normal + v
|
||||
match = leftover.match(regex)
|
||||
if(!match){
|
||||
if (!match) {
|
||||
result += leftover
|
||||
}
|
||||
}
|
||||
|
@ -699,10 +699,10 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
if (Array.isArray(leaf)) {
|
||||
for (let i = 0; i < (<any[]>leaf).length; i++) {
|
||||
const l = (<any[]>leaf)[i]
|
||||
collectedList.push({leaf: l, path: [...travelledPath, "" + i]})
|
||||
collectedList.push({ leaf: l, path: [...travelledPath, "" + i] })
|
||||
}
|
||||
} else {
|
||||
collectedList.push({leaf, path: travelledPath})
|
||||
collectedList.push({ leaf, path: travelledPath })
|
||||
}
|
||||
return collectedList
|
||||
}
|
||||
|
@ -780,7 +780,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
})
|
||||
}
|
||||
|
||||
const cp = {...json}
|
||||
const cp = { ...json }
|
||||
for (const key in json) {
|
||||
cp[key] = Utils.WalkJson(json[key], f, isLeaf, [...path, key])
|
||||
}
|
||||
|
@ -910,11 +910,11 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
const xhr = new XMLHttpRequest()
|
||||
xhr.onload = () => {
|
||||
if (xhr.status == 200) {
|
||||
resolve({content: xhr.response})
|
||||
resolve({ content: xhr.response })
|
||||
} else if (xhr.status === 302) {
|
||||
resolve({redirect: xhr.getResponseHeader("location")})
|
||||
resolve({ redirect: xhr.getResponseHeader("location") })
|
||||
} else if (xhr.status === 509 || xhr.status === 429) {
|
||||
resolve({error: "rate limited", url, statuscode: xhr.status})
|
||||
resolve({ error: "rate limited", url, statuscode: xhr.status })
|
||||
} else {
|
||||
resolve({
|
||||
error: "other error: " + xhr.statusText,
|
||||
|
@ -984,10 +984,10 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
}
|
||||
const promise =
|
||||
/*NO AWAIT as we work with the promise directly */ Utils.downloadJsonAdvanced(
|
||||
url,
|
||||
headers
|
||||
)
|
||||
Utils._download_cache.set(url, {promise, timestamp: new Date().getTime()})
|
||||
url,
|
||||
headers
|
||||
)
|
||||
Utils._download_cache.set(url, { promise, timestamp: new Date().getTime() })
|
||||
return await promise
|
||||
}
|
||||
|
||||
|
@ -1006,11 +1006,11 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
const injected = Utils.injectedDownloads[url]
|
||||
if (injected !== undefined) {
|
||||
console.log("Using injected resource for test for URL", url)
|
||||
return new Promise((resolve, _) => resolve({content: injected}))
|
||||
return new Promise((resolve, _) => resolve({ content: injected }))
|
||||
}
|
||||
const result = await Utils.downloadAdvanced(
|
||||
url,
|
||||
Utils.Merge({accept: "application/json"}, headers ?? {})
|
||||
Utils.Merge({ accept: "application/json" }, headers ?? {})
|
||||
)
|
||||
if (result["error"] !== undefined) {
|
||||
return <{ error: string; url: string; statuscode?: number }>result
|
||||
|
@ -1018,12 +1018,12 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
const data = result["content"]
|
||||
try {
|
||||
if (typeof data === "string") {
|
||||
return {content: JSON.parse(data)}
|
||||
return { content: JSON.parse(data) }
|
||||
}
|
||||
return {content: data}
|
||||
return { content: data }
|
||||
} catch (e) {
|
||||
console.error("Could not parse ", data, "due to", e, "\n", e.stack)
|
||||
return {error: "malformed", url}
|
||||
return { error: "malformed", url }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1047,7 +1047,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
const element = document.createElement("a")
|
||||
let file
|
||||
if (typeof contents === "string") {
|
||||
file = new Blob([contents], {type: options?.mimetype ?? "text/plain"})
|
||||
file = new Blob([contents], { type: options?.mimetype ?? "text/plain" })
|
||||
} else {
|
||||
file = contents
|
||||
}
|
||||
|
@ -1318,7 +1318,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
if (match == undefined) {
|
||||
return undefined
|
||||
}
|
||||
return {r: Number(match[1]), g: Number(match[2]), b: Number(match[3])}
|
||||
return { r: Number(match[1]), g: Number(match[2]), b: Number(match[3]) }
|
||||
}
|
||||
|
||||
if (!hex.startsWith("#")) {
|
||||
|
@ -1378,7 +1378,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
if (inView) {
|
||||
return
|
||||
}
|
||||
element.scrollIntoView({behavior: "smooth", block: "nearest"})
|
||||
element.scrollIntoView({ behavior: "smooth", block: "nearest" })
|
||||
}
|
||||
|
||||
public static findParentWithScrolling(element: HTMLBaseElement): HTMLBaseElement {
|
||||
|
@ -1470,18 +1470,59 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
const postParts = prepart.split("}")
|
||||
if (postParts.length === 1) {
|
||||
// This was a normal part
|
||||
spec.push({message: postParts[0]})
|
||||
spec.push({ message: postParts[0] })
|
||||
} else {
|
||||
const [subs, message] = postParts
|
||||
spec.push({subs})
|
||||
spec.push({ subs })
|
||||
if (message !== "") {
|
||||
spec.push({message})
|
||||
spec.push({ message })
|
||||
}
|
||||
}
|
||||
}
|
||||
return spec
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file and line number of the code calling this
|
||||
*/
|
||||
public static getLocationInCode(offset: number = 0): {
|
||||
path: string
|
||||
line: number
|
||||
column: number
|
||||
markdownLocation: string
|
||||
filename: string
|
||||
functionName: string
|
||||
} {
|
||||
const error = new Error("No error")
|
||||
const stack = error.stack.split("\n")
|
||||
stack.shift() // Remove "Error: No error"
|
||||
const regex = /at (.*) \(([a-zA-Z0-9/.]+):([0-9]+):([0-9]+)\)/
|
||||
const stackItem = stack[Math.abs(offset) + 1]
|
||||
|
||||
let functionName: string
|
||||
let path: string
|
||||
let line: string
|
||||
let column: string
|
||||
let _: string
|
||||
const matchWithFuncName = stackItem.match(regex)
|
||||
if (matchWithFuncName) {
|
||||
;[_, functionName, path, line, column] = matchWithFuncName
|
||||
} else {
|
||||
let regexNoFuncName: RegExp = new RegExp("at ([a-zA-Z0-9/.]+):([0-9]+):([0-9]+)")
|
||||
;[_, path, line, column] = stackItem.match(regexNoFuncName)
|
||||
}
|
||||
|
||||
const markdownLocation = path.substring(path.indexOf("MapComplete/src") + 11) + "#L" + line
|
||||
return {
|
||||
path,
|
||||
functionName,
|
||||
line: Number(line),
|
||||
column: Number(column),
|
||||
markdownLocation,
|
||||
filename: path.substring(path.lastIndexOf("/") + 1),
|
||||
}
|
||||
}
|
||||
|
||||
private static colorDiff(
|
||||
c0: { r: number; g: number; b: number },
|
||||
c1: { r: number; g: number; b: number }
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"contributors": [
|
||||
{
|
||||
"commits": 5753,
|
||||
"commits": 5849,
|
||||
"contributor": "Pieter Vander Vennet"
|
||||
},
|
||||
{
|
||||
"commits": 371,
|
||||
"commits": 388,
|
||||
"contributor": "Robin van der Linde"
|
||||
},
|
||||
{
|
||||
|
@ -61,7 +61,7 @@
|
|||
"contributor": "AlexanderRebai"
|
||||
},
|
||||
{
|
||||
"commits": 19,
|
||||
"commits": 20,
|
||||
"contributor": "dependabot[bot]"
|
||||
},
|
||||
{
|
||||
|
@ -110,11 +110,11 @@
|
|||
},
|
||||
{
|
||||
"commits": 10,
|
||||
"contributor": "LiamSimons"
|
||||
"contributor": "Thibault Molleman"
|
||||
},
|
||||
{
|
||||
"commits": 9,
|
||||
"contributor": "Thibault Molleman"
|
||||
"commits": 10,
|
||||
"contributor": "LiamSimons"
|
||||
},
|
||||
{
|
||||
"commits": 9,
|
||||
|
@ -128,6 +128,10 @@
|
|||
"commits": 8,
|
||||
"contributor": "Mateusz Konieczny"
|
||||
},
|
||||
{
|
||||
"commits": 7,
|
||||
"contributor": "pelderson"
|
||||
},
|
||||
{
|
||||
"commits": 7,
|
||||
"contributor": "OliNau"
|
||||
|
@ -148,10 +152,6 @@
|
|||
"commits": 6,
|
||||
"contributor": "danieldegroot2"
|
||||
},
|
||||
{
|
||||
"commits": 6,
|
||||
"contributor": "pelderson"
|
||||
},
|
||||
{
|
||||
"commits": 4,
|
||||
"contributor": "Nadhem"
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"fr": "français",
|
||||
"gl": "lingua galega",
|
||||
"hu": "magyar",
|
||||
"id": "bahasa Indonesia",
|
||||
"id": "Bahasa Indonesia",
|
||||
"it": "italiano",
|
||||
"ja": "日本語",
|
||||
"nb_NO": "bokmål",
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"contributors": [
|
||||
{
|
||||
"commits": 283,
|
||||
"commits": 289,
|
||||
"contributor": "kjon"
|
||||
},
|
||||
{
|
||||
"commits": 275,
|
||||
"commits": 277,
|
||||
"contributor": "Pieter Vander Vennet"
|
||||
},
|
||||
{
|
||||
|
@ -50,11 +50,11 @@
|
|||
},
|
||||
{
|
||||
"commits": 25,
|
||||
"contributor": "Reza Almanda"
|
||||
"contributor": "Lucas"
|
||||
},
|
||||
{
|
||||
"commits": 23,
|
||||
"contributor": "Lucas"
|
||||
"commits": 25,
|
||||
"contributor": "Reza Almanda"
|
||||
},
|
||||
{
|
||||
"commits": 22,
|
||||
|
@ -124,6 +124,10 @@
|
|||
"commits": 10,
|
||||
"contributor": "Irina"
|
||||
},
|
||||
{
|
||||
"commits": 9,
|
||||
"contributor": "deep map"
|
||||
},
|
||||
{
|
||||
"commits": 9,
|
||||
"contributor": "Jaime Marquínez Ferrándiz"
|
||||
|
@ -244,6 +248,10 @@
|
|||
"commits": 5,
|
||||
"contributor": "Alexey Shabanov"
|
||||
},
|
||||
{
|
||||
"commits": 4,
|
||||
"contributor": "Emory Shaw"
|
||||
},
|
||||
{
|
||||
"commits": 4,
|
||||
"contributor": "André Marcelo Alvarenga"
|
||||
|
@ -328,10 +336,6 @@
|
|||
"commits": 3,
|
||||
"contributor": "SiegbjornSitumeang"
|
||||
},
|
||||
{
|
||||
"commits": 2,
|
||||
"contributor": "Emory Shaw"
|
||||
},
|
||||
{
|
||||
"commits": 2,
|
||||
"contributor": "lmagreault"
|
||||
|
@ -440,10 +444,6 @@
|
|||
"commits": 1,
|
||||
"contributor": "Stéphane De Greef"
|
||||
},
|
||||
{
|
||||
"commits": 1,
|
||||
"contributor": "deep map"
|
||||
},
|
||||
{
|
||||
"commits": 1,
|
||||
"contributor": "Falk Rund"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue