forked from MapComplete/MapComplete
Studio: more finetuning, first version working
This commit is contained in:
parent
c4d4a57a08
commit
ac1e7c7f06
39 changed files with 2971 additions and 7187 deletions
|
@ -127,31 +127,6 @@ export default class DetermineLayout {
|
|||
return layoutToUse
|
||||
}
|
||||
|
||||
public static ShowErrorOnCustomTheme(
|
||||
intro: string = "Error: could not parse the custom layout:",
|
||||
error: BaseUIElement,
|
||||
json?: any
|
||||
) {
|
||||
new Combine([
|
||||
intro,
|
||||
error.SetClass("alert"),
|
||||
new SubtleButton(Svg.back_svg(), "Go back to the theme overview", {
|
||||
url: window.location.protocol + "//" + window.location.host + "/index.html",
|
||||
newTab: false,
|
||||
}),
|
||||
json !== undefined
|
||||
? new SubtleButton(Svg.download_svg(), "Download the JSON file").onClick(() => {
|
||||
Utils.offerContentsAsDownloadableFile(
|
||||
JSON.stringify(json, null, " "),
|
||||
"theme_definition.json"
|
||||
)
|
||||
})
|
||||
: undefined,
|
||||
])
|
||||
.SetClass("flex flex-col clickable")
|
||||
.AttachTo("maindiv")
|
||||
}
|
||||
|
||||
private static getSharedTagRenderings(): Map<string, QuestionableTagRenderingConfigJson> {
|
||||
const dict = new Map<string, QuestionableTagRenderingConfigJson>()
|
||||
|
||||
|
|
|
@ -153,9 +153,6 @@ export class Pipe<TIn, TInter, TOut> extends Conversion<TIn, TOut> {
|
|||
|
||||
convert(json: TIn, context: ConversionContext): TOut {
|
||||
const r0 = this._step0.convert(json, context.inOperation(this._step0.name))
|
||||
if (context.hasErrors()) {
|
||||
return undefined
|
||||
}
|
||||
return this._step1.convert(r0, context.inOperation(this._step1.name))
|
||||
}
|
||||
}
|
||||
|
@ -308,9 +305,6 @@ export class Fuse<T> extends DesugaringStep<T> {
|
|||
if (r === undefined || r === null) {
|
||||
break
|
||||
}
|
||||
if (context.hasErrors()) {
|
||||
break
|
||||
}
|
||||
json = r
|
||||
} catch (e) {
|
||||
console.error("Step " + step.name + " failed due to ", e, e.stack)
|
||||
|
|
|
@ -731,6 +731,77 @@ export class ValidateLayer extends Conversion<
|
|||
return null
|
||||
}
|
||||
|
||||
if (typeof json === "string") {
|
||||
context.err(
|
||||
`Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (json.id === undefined) {
|
||||
context.err(`Not a valid layer: id is undefined: ${JSON.stringify(json)}`)
|
||||
}
|
||||
|
||||
if (json.source === undefined) {
|
||||
context.enter("source").err("No source section is defined")
|
||||
} else {
|
||||
if (json.source === "special" || json.source === "special:library") {
|
||||
} else if (json.source && json.source["osmTags"] === undefined) {
|
||||
context
|
||||
.enters("source", "osmTags")
|
||||
.err(
|
||||
"No osmTags defined in the source section - these should always be present, even for geojson layer"
|
||||
)
|
||||
} else {
|
||||
const osmTags = TagUtils.Tag(json.source["osmTags"], context + "source.osmTags")
|
||||
if (osmTags.isNegative()) {
|
||||
context
|
||||
.enters("source", "osmTags")
|
||||
.err(
|
||||
"The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" +
|
||||
osmTags.asHumanString(false, false, {})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (json.source["geoJsonSource"] !== undefined) {
|
||||
context
|
||||
.enters("source", "geoJsonSource")
|
||||
.err("Use 'geoJson' instead of 'geoJsonSource'")
|
||||
}
|
||||
|
||||
if (json.source["geojson"] !== undefined) {
|
||||
context
|
||||
.enters("source", "geojson")
|
||||
.err("Use 'geoJson' instead of 'geojson' (the J is a capital letter)")
|
||||
}
|
||||
}
|
||||
|
||||
if (json.id?.toLowerCase() !== json.id) {
|
||||
context.enter("id").err(`The id of a layer should be lowercase: ${json.id}`)
|
||||
}
|
||||
if (json.id?.match(/[a-z0-9-_]/) == null) {
|
||||
context.enter("id").err(`The id of a layer should match [a-z0-9-_]*: ${json.id}`)
|
||||
}
|
||||
|
||||
if (
|
||||
json.syncSelection !== undefined &&
|
||||
LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0
|
||||
) {
|
||||
context
|
||||
.enter("syncSelection")
|
||||
.err(
|
||||
"Invalid sync-selection: must be one of " +
|
||||
LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") +
|
||||
" but got '" +
|
||||
json.syncSelection +
|
||||
"'"
|
||||
)
|
||||
}
|
||||
|
||||
if (context.hasErrors()) {
|
||||
return undefined
|
||||
}
|
||||
let layerConfig: LayerConfig
|
||||
try {
|
||||
layerConfig = new LayerConfig(json, "validation", true)
|
||||
|
|
|
@ -136,7 +136,7 @@ export interface LayerConfigJson {
|
|||
* There are a few extra functions available. Refer to <a>Docs/CalculatedTags.md</a> for more information
|
||||
* The functions will be run in order, e.g.
|
||||
* [
|
||||
* "_max_overlap_m2=Math.max(...feat.overlapsWith("someOtherLayer").map(o => o.overlap))
|
||||
Not found... * "_max_overlap_m2=Math.max(...feat.overlapsWith("someOtherLayer").map(o => o.overlap))
|
||||
* "_max_overlap_ratio=Number(feat._max_overlap_m2)/feat.area
|
||||
* ]
|
||||
*
|
||||
|
@ -208,6 +208,7 @@ export interface LayerConfigJson {
|
|||
* types: use a fixed translation ; Use a dynamic tagRendering ; hidden
|
||||
* typesdefault: 1
|
||||
* type: translation
|
||||
* inline: {translated{value}}
|
||||
*/
|
||||
title?: TagRenderingConfigJson | Translatable
|
||||
|
||||
|
@ -309,7 +310,7 @@ export interface LayerConfigJson {
|
|||
* Do _not_ indicate 'new': 'add a new shop here' is incorrect, as the shop might have existed forever, it could just be unmapped!
|
||||
*
|
||||
* question: What is the word to describe this object?
|
||||
* inline: Add <b>{value}</b> here
|
||||
* inline: Add <b>{translated(value)}</b> here
|
||||
*/
|
||||
title: Translatable
|
||||
/**
|
||||
|
@ -360,7 +361,7 @@ export interface LayerConfigJson {
|
|||
* If further away, it'll be placed in the center of the location input
|
||||
* Distance in meter
|
||||
*
|
||||
* Default: 10
|
||||
* ifunset: Do not snap to a layer
|
||||
*/
|
||||
maxSnapDistance?: number
|
||||
}[]
|
||||
|
@ -463,6 +464,7 @@ export interface LayerConfigJson {
|
|||
* types: Use an advanced delete configuration ; boolean
|
||||
* iftrue: Allow deletion
|
||||
* iffalse: Do not allow deletion
|
||||
* ifunset: Do not allow deletion
|
||||
*
|
||||
**/
|
||||
deletion?: DeleteConfigJson | boolean
|
||||
|
|
|
@ -18,7 +18,6 @@ export default interface LineRenderingConfigJson {
|
|||
* types: dynamic value ; string
|
||||
* title: Line Colour
|
||||
* inline: The line colour always is <b>{value}</b>
|
||||
* ifunset: Round ending
|
||||
* type: color
|
||||
*
|
||||
*/
|
||||
|
|
|
@ -91,6 +91,7 @@ export default interface PointRenderingConfigJson {
|
|||
* If the icon is undefined, then the label is shown in the center of the feature.
|
||||
* types: Dynamic value | string
|
||||
* inline: Always show label <b>{value}</b> beneath the marker
|
||||
* ifunset: Do not show a label beneath the marker
|
||||
*/
|
||||
label?: string | TagRenderingConfigJson
|
||||
|
||||
|
@ -100,6 +101,8 @@ export default interface PointRenderingConfigJson {
|
|||
* This will be applied to the _container_ containing both the marker and the label
|
||||
* inline: Apply CSS-style <b>{value}</b> to the _entire marker_
|
||||
* types: Dynamic value ; string
|
||||
* ifunset: Do not apply extra CSS element to the entire marker
|
||||
*
|
||||
*/
|
||||
css?: string | TagRenderingConfigJson
|
||||
|
||||
|
@ -111,7 +114,9 @@ export default interface PointRenderingConfigJson {
|
|||
* You can use most Tailwind-css classes, see https://tailwindcss.com/ for more information
|
||||
* For example: `center bg-gray-500 mx-2 my-1 rounded-full`
|
||||
* inline: Apply CSS-classes <b>{value}</b> to the entire container
|
||||
* ifunset: Do not apply extra CSS-classes to the label
|
||||
* types: Dynamic value ; string
|
||||
* ifunset: Do not apply extra CSS-classes to the entire marker
|
||||
*/
|
||||
cssClasses?: string | TagRenderingConfigJson
|
||||
|
||||
|
@ -120,6 +125,8 @@ export default interface PointRenderingConfigJson {
|
|||
* You can set the css-properties here, e.g. `background: red; font-size: 12px; `
|
||||
* inline: Apply CSS-style <b>{value}</b> to the label
|
||||
* types: Dynamic value ; string
|
||||
* ifunset: Do not apply extra CSS-labels to the label
|
||||
*
|
||||
*/
|
||||
labelCss?: TagRenderingConfigJson | string
|
||||
|
||||
|
@ -131,6 +138,7 @@ export default interface PointRenderingConfigJson {
|
|||
* For example: `center bg-gray-500 mx-2 my-1 rounded-full`
|
||||
* inline: Apply CSS-classes <b>{value}</b> to the label
|
||||
* types: Dynamic value ; string
|
||||
* ifunset: Do not apply extra CSS-classes to the label
|
||||
*/
|
||||
labelCssClasses?: string | TagRenderingConfigJson
|
||||
|
||||
|
|
|
@ -187,7 +187,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
|
|||
*
|
||||
* question: Should a contributor be allowed to select multiple mappings?
|
||||
*
|
||||
* iftrue: allow to select multiple mappigns
|
||||
* iftrue: allow to select multiple mappings
|
||||
* iffalse: only allow to select a single mapping
|
||||
* ifunset: only allow to select a single mapping
|
||||
*/
|
||||
|
@ -250,7 +250,7 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
|
|||
* For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked
|
||||
* ifunset: The question will be considered answered if any value is set for the key
|
||||
*/
|
||||
invalidValues?: string[]
|
||||
invalidValues?: TagConfigJson
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -75,65 +75,17 @@ export default class LayerConfig extends WithContextLoader {
|
|||
const translationContext = "layers:" + json.id
|
||||
super(json, context)
|
||||
this.id = json.id
|
||||
if (typeof json === "string") {
|
||||
throw `Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed (at ${context})`
|
||||
}
|
||||
|
||||
if (json.id === undefined) {
|
||||
throw `Not a valid layer: id is undefined: ${JSON.stringify(json)} (At ${context})`
|
||||
}
|
||||
|
||||
if (json.source === undefined) {
|
||||
throw "Layer " + this.id + " does not define a source section (" + context + ")"
|
||||
}
|
||||
|
||||
if (json.source === "special" || json.source === "special:library") {
|
||||
this.source = null
|
||||
} else if (json.source["osmTags"] === undefined) {
|
||||
throw (
|
||||
"Layer " +
|
||||
this.id +
|
||||
" does not define a osmTags in the source section - these should always be present, even for geojson layers (" +
|
||||
context +
|
||||
")"
|
||||
)
|
||||
}
|
||||
|
||||
if (json.id.toLowerCase() !== json.id) {
|
||||
throw `${context}: The id of a layer should be lowercase: ${json.id}`
|
||||
}
|
||||
if (json.id.match(/[a-z0-9-_]/) == null) {
|
||||
throw `${context}: The id of a layer should match [a-z0-9-_]*: ${json.id}`
|
||||
}
|
||||
|
||||
if (
|
||||
json.syncSelection !== undefined &&
|
||||
LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0
|
||||
) {
|
||||
throw (
|
||||
context +
|
||||
" Invalid sync-selection: must be one of " +
|
||||
LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") +
|
||||
" but got '" +
|
||||
json.syncSelection +
|
||||
"'"
|
||||
)
|
||||
}
|
||||
this.syncSelection = json.syncSelection ?? "no"
|
||||
if (typeof json.source !== "string") {
|
||||
this.maxAgeOfCache = json.source["maxCacheAge"] ?? 24 * 60 * 60 * 30
|
||||
const osmTags = TagUtils.Tag(json.source["osmTags"], context + "source.osmTags")
|
||||
if (osmTags.isNegative()) {
|
||||
throw (
|
||||
context +
|
||||
"The source states tags which give a very wide selection: it only uses negative expressions, which will result in too much and unexpected data. Add at least one required tag. The tags are:\n\t" +
|
||||
osmTags.asHumanString(false, false, {})
|
||||
)
|
||||
}
|
||||
|
||||
this.source = new SourceConfig(
|
||||
{
|
||||
osmTags: osmTags,
|
||||
osmTags: TagUtils.Tag(json.source["osmTags"], context + "source.osmTags"),
|
||||
geojsonSource: json.source["geoJson"],
|
||||
geojsonSourceLevel: json.source["geoJsonZoomLevel"],
|
||||
overpassScript: json.source["overpassScript"],
|
||||
|
@ -145,14 +97,6 @@ export default class LayerConfig extends WithContextLoader {
|
|||
)
|
||||
}
|
||||
|
||||
if (json.source["geoJsonSource"] !== undefined) {
|
||||
throw context + "Use 'geoJson' instead of 'geoJsonSource'"
|
||||
}
|
||||
|
||||
if (json.source["geojson"] !== undefined) {
|
||||
throw context + "Use 'geoJson' instead of 'geojson' (the J is a capital letter)"
|
||||
}
|
||||
|
||||
this.allowSplit = json.allowSplit ?? false
|
||||
this.name = Translations.T(json.name, translationContext + ".name")
|
||||
if (json.units !== undefined && !Array.isArray(json.units)) {
|
||||
|
|
|
@ -53,6 +53,7 @@ export default class TagRenderingConfig {
|
|||
public readonly question?: TypedTranslation<object>
|
||||
public readonly questionhint?: TypedTranslation<object>
|
||||
public readonly condition?: TagsFilter
|
||||
public readonly invalidValues?: TagsFilter
|
||||
/**
|
||||
* Evaluated against the current 'usersettings'-state
|
||||
*/
|
||||
|
@ -133,6 +134,9 @@ export default class TagRenderingConfig {
|
|||
this.questionhint = Translations.T(json.questionHint, translationKey + ".questionHint")
|
||||
this.description = Translations.T(json.description, translationKey + ".description")
|
||||
this.condition = TagUtils.Tag(json.condition ?? { and: [] }, `${context}.condition`)
|
||||
this.invalidValues = json["invalidValues"]
|
||||
? TagUtils.Tag(json["invalidValues"], `${context}.invalidValues`)
|
||||
: undefined
|
||||
if (typeof json.icon === "string") {
|
||||
this.renderIcon = json.icon
|
||||
this.renderIconClass = "small"
|
||||
|
@ -469,9 +473,12 @@ export default class TagRenderingConfig {
|
|||
*/
|
||||
public IsKnown(tags: Record<string, string>): boolean {
|
||||
if (this.condition && !this.condition.matchesProperties(tags)) {
|
||||
// Filtered away by the condition, so it is kindof known
|
||||
// Filtered away by the condition, so it is kind of known
|
||||
return true
|
||||
}
|
||||
if (this.invalidValues && this.invalidValues.matchesProperties(tags)) {
|
||||
return false
|
||||
}
|
||||
if (this.multiAnswer) {
|
||||
for (const m of this.mappings ?? []) {
|
||||
if (TagUtils.MatchesMultiAnswer(m.if, tags)) {
|
||||
|
@ -482,6 +489,9 @@ export default class TagRenderingConfig {
|
|||
const free = this.freeform?.key
|
||||
if (free !== undefined) {
|
||||
const value = tags[free]
|
||||
if (typeof value === "object") {
|
||||
return Object.keys(value).length > 0
|
||||
}
|
||||
return value !== undefined && value !== ""
|
||||
}
|
||||
return false
|
||||
|
@ -679,7 +689,9 @@ export default class TagRenderingConfig {
|
|||
multiSelectedMapping: boolean[] | undefined,
|
||||
currentProperties: Record<string, string>
|
||||
): UploadableTag {
|
||||
freeformValue = freeformValue?.trim()
|
||||
if (typeof freeformValue === "string") {
|
||||
freeformValue = freeformValue?.trim()
|
||||
}
|
||||
const validator = Validators.get(<ValidatorType>this.freeform?.type)
|
||||
if (validator && freeformValue) {
|
||||
freeformValue = validator.reformat(freeformValue, () => currentProperties["_country"])
|
||||
|
|
|
@ -141,7 +141,7 @@
|
|||
*/
|
||||
const kv = selectedTags.asChange(tags.data);
|
||||
for (const { k, v } of kv) {
|
||||
if (v === undefined) {
|
||||
if (v === undefined || v === "") {
|
||||
delete tags.data[k];
|
||||
} else {
|
||||
tags.data[k] = v;
|
||||
|
|
|
@ -1317,6 +1317,7 @@ export default class SpecialVisualizations {
|
|||
{
|
||||
funcName: "translated",
|
||||
docs: "If the given key can be interpreted as a JSON, only show the key containing the current language (or 'en'). This specialRendering is meant to be used by MapComplete studio and is not useful in map themes",
|
||||
needsUrls: [],
|
||||
args: [
|
||||
{
|
||||
name: "key",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
|
||||
import EditLayerState, { LayerStateSender } from "./EditLayerState";
|
||||
import { LayerStateSender } from "./EditLayerState";
|
||||
import layerSchemaRaw from "../../assets/schemas/layerconfigmeta.json";
|
||||
import Region from "./Region.svelte";
|
||||
import TabbedGroup from "../Base/TabbedGroup.svelte";
|
||||
|
@ -10,12 +10,13 @@
|
|||
import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson";
|
||||
|
||||
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
|
||||
let state = new EditLayerState(layerSchema);
|
||||
|
||||
export let state;
|
||||
const messages = state.messages;
|
||||
export let initialLayerConfig: Partial<LayerConfigJson> = {};
|
||||
state.configuration.setData(initialLayerConfig);
|
||||
const configuration = state.configuration;
|
||||
new LayerStateSender("http://localhost:1235", state);
|
||||
new LayerStateSender(state);
|
||||
/**
|
||||
* Blacklist of regions for the general area tab
|
||||
* These are regions which are handled by a different tab
|
||||
|
@ -76,6 +77,7 @@
|
|||
</div>
|
||||
{#each $messages as message}
|
||||
<li>
|
||||
{message.level}
|
||||
<span class="literal-code">{message.context.path.join(".")}</span>
|
||||
{message.message}
|
||||
</li>
|
||||
|
|
|
@ -13,26 +13,21 @@ import { PrepareLayer } from "../../Models/ThemeConfig/Conversion/PrepareLayer"
|
|||
import { ValidateLayer } from "../../Models/ThemeConfig/Conversion/Validation"
|
||||
import { AllSharedLayers } from "../../Customizations/AllSharedLayers"
|
||||
import { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils"
|
||||
import StudioServer from "./StudioServer"
|
||||
|
||||
/**
|
||||
* Sends changes back to the server
|
||||
*/
|
||||
export class LayerStateSender {
|
||||
constructor(serverLocation: string, layerState: EditLayerState) {
|
||||
constructor(layerState: EditLayerState) {
|
||||
layerState.configuration.addCallback(async (config) => {
|
||||
// console.log("Current config is", Utils.Clone(config))
|
||||
const id = config.id
|
||||
if (id === undefined) {
|
||||
console.log("No id found in layer, not updating")
|
||||
console.warn("No id found in layer, not updating")
|
||||
return
|
||||
}
|
||||
const fresponse = await fetch(`${serverLocation}/layers/${id}/${id}.json`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json;charset=utf-8",
|
||||
},
|
||||
body: JSON.stringify(config, null, " "),
|
||||
})
|
||||
await layerState.server.updateLayer(<LayerConfigJson>config)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -47,9 +42,11 @@ export default class EditLayerState {
|
|||
Partial<LayerConfigJson>
|
||||
>({})
|
||||
public readonly messages: Store<ConversionMessage[]>
|
||||
public readonly server: StudioServer
|
||||
|
||||
constructor(schema: ConfigMeta[]) {
|
||||
constructor(schema: ConfigMeta[], server: StudioServer) {
|
||||
this.schema = schema
|
||||
this.server = server
|
||||
this.osmConnection = new OsmConnection({
|
||||
oauth_token: QueryParameters.GetQueryParameter(
|
||||
"oauth_token",
|
||||
|
@ -73,18 +70,36 @@ export default class EditLayerState {
|
|||
sharedLayers: layers,
|
||||
}
|
||||
}
|
||||
this.messages = this.configuration.map((config) => {
|
||||
this.messages = this.configuration.mapD((config) => {
|
||||
const context = ConversionContext.construct([], ["prepare"])
|
||||
|
||||
for (let i = 0; i < (config.tagRenderings ?? []).length; i++) {
|
||||
const tr = config.tagRenderings[i]
|
||||
if (typeof tr === "string") {
|
||||
continue
|
||||
}
|
||||
if (!tr["id"] && !tr["override"]) {
|
||||
const qtr = <QuestionableTagRenderingConfigJson>tr
|
||||
let id = "" + i
|
||||
if (qtr?.freeform?.key) {
|
||||
id = qtr?.freeform?.key
|
||||
} else if (qtr.mappings?.[0]?.if) {
|
||||
id =
|
||||
qtr.freeform?.key ??
|
||||
TagUtils.Tag(qtr.mappings[0].if).usedKeys()?.[0] ??
|
||||
"" + i
|
||||
}
|
||||
qtr["id"] = id
|
||||
}
|
||||
}
|
||||
|
||||
const prepare = new Pipe(
|
||||
new PrepareLayer(state),
|
||||
new ValidateLayer("dynamic", false, undefined)
|
||||
)
|
||||
prepare.convert(<LayerConfigJson>config, context)
|
||||
console.log(context.messages)
|
||||
return context.messages
|
||||
})
|
||||
console.log("Configuration store:", this.configuration)
|
||||
}
|
||||
|
||||
public getCurrentValueFor(path: ReadonlyArray<string | number>): any | undefined {
|
||||
|
@ -104,7 +119,6 @@ export default class EditLayerState {
|
|||
public getStoreFor(path: ReadonlyArray<string | number>): UIEventSource<any | undefined> {
|
||||
const store = new UIEventSource<any>(this.getCurrentValueFor(path))
|
||||
store.addCallback((v) => {
|
||||
console.log("UPdating store", path, v)
|
||||
this.setValueAt(path, v)
|
||||
})
|
||||
return store
|
||||
|
@ -156,7 +170,6 @@ export default class EditLayerState {
|
|||
|
||||
public setValueAt(path: ReadonlyArray<string | number>, v: any) {
|
||||
let entry = this.configuration.data
|
||||
console.log("Setting value", v, "to", path, "in entry", entry)
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const breadcrumb = path[i]
|
||||
if (entry[breadcrumb] === undefined) {
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
|
||||
const subparts: ConfigMeta = state.getSchemaStartingWith(schema.path)
|
||||
.filter(part => part.path.length - 1 === schema.path.length);
|
||||
console.log("For ", schema.path, "got subparts", subparts)
|
||||
/**
|
||||
* Store the _indices_
|
||||
*/
|
||||
|
|
|
@ -100,6 +100,7 @@
|
|||
try {
|
||||
onDestroy(state.register(path, tags.map(tgs => {
|
||||
const v = tgs["value"];
|
||||
console.log("Registering",path,"setting value to", v)
|
||||
if(typeof v !== "string"){
|
||||
return v
|
||||
}
|
||||
|
@ -116,6 +117,9 @@
|
|||
}
|
||||
}
|
||||
if (schema.type === "number") {
|
||||
if(v === ""){
|
||||
return undefined
|
||||
}
|
||||
return Number(v)
|
||||
}
|
||||
if (isTranslation) {
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
types.splice(hasBooleanOption);
|
||||
}
|
||||
|
||||
|
||||
let lastIsString = false;
|
||||
{
|
||||
const types: string | string[] = Array.isArray(schema.type) ? schema.type[schema.type.length - 1].type : [];
|
||||
|
@ -217,5 +218,4 @@
|
|||
path={[...subpath, (subschema?.path?.at(-1) ?? "???")]}></SchemaBasedInput>
|
||||
{/each}
|
||||
{/if}
|
||||
{chosenOption}
|
||||
</div>
|
||||
|
|
|
@ -3,15 +3,15 @@ import Constants from "../../Models/Constants"
|
|||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
|
||||
export default class StudioServer {
|
||||
private _url: string
|
||||
private readonly url: string
|
||||
|
||||
constructor(url: string) {
|
||||
this._url = url
|
||||
this.url = url
|
||||
}
|
||||
|
||||
public async fetchLayerOverview(): Promise<Set<string>> {
|
||||
const { allFiles } = <{ allFiles: string[] }>(
|
||||
await Utils.downloadJson(this._url + "/overview")
|
||||
await Utils.downloadJson(this.url + "/overview")
|
||||
)
|
||||
const layers = allFiles
|
||||
.filter((f) => f.startsWith("layers/"))
|
||||
|
@ -20,19 +20,27 @@ export default class StudioServer {
|
|||
return new Set<string>(layers)
|
||||
}
|
||||
|
||||
async fetchLayer(layerId: string, checkNew: boolean = false): Promise<LayerConfigJson> {
|
||||
async fetchLayer(layerId: string): Promise<LayerConfigJson> {
|
||||
try {
|
||||
return await Utils.downloadJson(
|
||||
this._url +
|
||||
"/layers/" +
|
||||
layerId +
|
||||
"/" +
|
||||
layerId +
|
||||
".json" +
|
||||
(checkNew ? ".new.json" : "")
|
||||
this.url + "/layers/" + layerId + "/" + layerId + ".json"
|
||||
)
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async updateLayer(config: LayerConfigJson) {
|
||||
const id = config.id
|
||||
if (id === undefined || id === "") {
|
||||
return
|
||||
}
|
||||
await fetch(`${this.url}/layers/${id}/${id}.json`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json;charset=utf-8",
|
||||
},
|
||||
body: JSON.stringify(config, null, " "),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,6 +82,7 @@ const freeformSchema = <ConfigMeta[]> questionableTagRenderingSchemaRaw.filter(
|
|||
|
||||
<SchemaBasedField {state} path={[...path,"question"]} schema={topLevelItems["question"]}></SchemaBasedField>
|
||||
<SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]}></SchemaBasedField>
|
||||
<SchemaBasedField {state} path={[...path,"render"]} schema={topLevelItems["render"]}></SchemaBasedField>
|
||||
|
||||
{#each ($mappings ?? []) as mapping, i (mapping)}
|
||||
<div class="flex interactive w-full">
|
||||
|
@ -102,7 +103,12 @@ const freeformSchema = <ConfigMeta[]> questionableTagRenderingSchemaRaw.filter(
|
|||
Add a mapping
|
||||
</button>
|
||||
|
||||
<SchemaBasedField {state} path={[...path,"multiAnswer"]} schema={topLevelItems["multiAnswer"]}></SchemaBasedField>
|
||||
|
||||
<div class="border border-gray-200 border-dashed">
|
||||
<h3>Text field and input element configuration</h3>
|
||||
<Region {state} {path} configs={freeformSchema}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -14,8 +14,9 @@
|
|||
import { OsmConnection } from "../Logic/Osm/OsmConnection";
|
||||
import { QueryParameters } from "../Logic/Web/QueryParameters";
|
||||
|
||||
import layerSchemaRaw from "../../src/assets/schemas/layerconfigmeta.json";
|
||||
|
||||
export let studioUrl = "http://127.0.0.1:1235";
|
||||
export let studioUrl = /*"https://studio.mapcomplete.org"; /*/ "http://127.0.0.1:1235"; //*/
|
||||
const studio = new StudioServer(studioUrl);
|
||||
let layers = UIEventSource.FromPromise(studio.fetchLayerOverview());
|
||||
let state: undefined | "edit_layer" | "new_layer" | "edit_theme" | "new_theme" | "editing_layer" | "loading" = undefined;
|
||||
|
@ -33,101 +34,121 @@
|
|||
}, [layers]);
|
||||
|
||||
|
||||
let editLayerState = new EditLayerState();
|
||||
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw;
|
||||
|
||||
let editLayerState = new EditLayerState(layerSchema, studio);
|
||||
|
||||
function fetchIconDescription(layerId): any {
|
||||
const icon = AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
|
||||
console.log(icon);
|
||||
return icon;
|
||||
return AllSharedLayers.getSharedLayersConfigs().get(layerId)?._layerIcon;
|
||||
}
|
||||
|
||||
async function createNewLayer(){
|
||||
state = "loading"
|
||||
const id = newLayerId.data
|
||||
const createdBy = osmConnection.userDetails.data.name
|
||||
|
||||
const loaded = await studio.fetchLayer(id, true)
|
||||
initialLayerConfig = loaded ?? {id, credits: createdBy};
|
||||
state = "editing_layer"}
|
||||
|
||||
let osmConnection = new OsmConnection( new OsmConnection({
|
||||
async function createNewLayer() {
|
||||
state = "loading";
|
||||
const id = newLayerId.data;
|
||||
const createdBy = osmConnection.userDetails.data.name;
|
||||
|
||||
try {
|
||||
|
||||
const loaded = await studio.fetchLayer(id);
|
||||
initialLayerConfig = loaded ?? {
|
||||
id, credits: createdBy,
|
||||
pointRendering: [
|
||||
{
|
||||
location: ["point", "centroid"],
|
||||
marker: [{
|
||||
icon: "circle",
|
||||
color: "white"
|
||||
}]
|
||||
}
|
||||
],
|
||||
lineRendering : [{
|
||||
width : 1,
|
||||
color: "blue"
|
||||
}]
|
||||
};
|
||||
} catch (e) {
|
||||
initialLayerConfig = { id, credits: createdBy };
|
||||
}
|
||||
state = "editing_layer";
|
||||
}
|
||||
|
||||
let osmConnection = new OsmConnection(new OsmConnection({
|
||||
oauth_token: QueryParameters.GetQueryParameter(
|
||||
"oauth_token",
|
||||
undefined,
|
||||
"Used to complete the login"
|
||||
),
|
||||
}))
|
||||
)
|
||||
}));
|
||||
|
||||
</script>
|
||||
|
||||
<LoginToggle state={{osmConnection}}>
|
||||
<div slot="not-logged-in" >
|
||||
<NextButton clss="primary">
|
||||
Please log in to use MapComplete Studio
|
||||
</NextButton>
|
||||
</div>
|
||||
{#if state === undefined}
|
||||
<h1>MapComplete Studio</h1>
|
||||
<div class="w-full flex flex-col">
|
||||
|
||||
<NextButton on:click={() => state = "edit_layer"}>
|
||||
Edit an existing layer
|
||||
</NextButton>
|
||||
<NextButton on:click={() => state = "new_layer"}>
|
||||
Create a new layer
|
||||
</NextButton>
|
||||
<NextButton on:click={() => state = "edit_theme"}>
|
||||
Edit a theme
|
||||
</NextButton>
|
||||
<NextButton on:click={() => state = "new_theme"}>
|
||||
Create a new theme
|
||||
<LoginToggle state={{osmConnection}} ignoreLoading={true}>
|
||||
<div slot="not-logged-in">
|
||||
<NextButton clss="primary">
|
||||
Please log in to use MapComplete Studio
|
||||
</NextButton>
|
||||
</div>
|
||||
{:else if state === "edit_layer"}
|
||||
<div class="flex flex-wrap">
|
||||
{#each Array.from($layers) as layerId}
|
||||
<NextButton clss="small" on:click={async () => {
|
||||
console.log("Editing layer",layerId)
|
||||
{#if state === undefined}
|
||||
<h1>MapComplete Studio</h1>
|
||||
<div class="w-full flex flex-col">
|
||||
|
||||
<NextButton on:click={() => state = "edit_layer"}>
|
||||
Edit an existing layer
|
||||
</NextButton>
|
||||
<NextButton on:click={() => state = "new_layer"}>
|
||||
Create a new layer
|
||||
</NextButton>
|
||||
<NextButton on:click={() => state = "edit_theme"}>
|
||||
Edit a theme
|
||||
</NextButton>
|
||||
<NextButton on:click={() => state = "new_theme"}>
|
||||
Create a new theme
|
||||
</NextButton>
|
||||
</div>
|
||||
{:else if state === "edit_layer"}
|
||||
<div class="flex flex-wrap">
|
||||
{#each Array.from($layers) as layerId}
|
||||
<NextButton clss="small" on:click={async () => {
|
||||
state = "loading"
|
||||
initialLayerConfig = await studio.fetchLayer(layerId)
|
||||
state = "editing_layer"
|
||||
}}>
|
||||
<div class="w-4 h-4 mr-1">
|
||||
<Marker icons={fetchIconDescription(layerId)} />
|
||||
<div class="w-4 h-4 mr-1">
|
||||
<Marker icons={fetchIconDescription(layerId)} />
|
||||
</div>
|
||||
{layerId}
|
||||
</NextButton>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if state === "new_layer"}
|
||||
<div class="interactive flex m-2 rounded-2xl flex-col p-2">
|
||||
<h3>Enter the ID for the new layer</h3>
|
||||
A good ID is:
|
||||
<ul>
|
||||
<li>a noun</li>
|
||||
<li>singular</li>
|
||||
<li>describes the object</li>
|
||||
<li>in English</li>
|
||||
</ul>
|
||||
<div class="m-2 p-2 w-full">
|
||||
|
||||
<ValidatedInput type="id" value={newLayerId} feedback={layerIdFeedback} on:submit={() => createNewLayer()} />
|
||||
</div>
|
||||
{#if $layerIdFeedback !== undefined}
|
||||
<div class="alert">
|
||||
{$layerIdFeedback}
|
||||
</div>
|
||||
{layerId}
|
||||
</NextButton>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if state === "new_layer"}
|
||||
<div class="interactive flex m-2 rounded-2xl flex-col p-2">
|
||||
<h3>Enter the ID for the new layer</h3>
|
||||
A good ID is:
|
||||
<ul>
|
||||
<li>a noun</li>
|
||||
<li>singular</li>
|
||||
<li>describes the object</li>
|
||||
<li>in English</li>
|
||||
</ul>
|
||||
<div class="m-2 p-2 w-full">
|
||||
|
||||
<ValidatedInput type="id" value={newLayerId} feedback={layerIdFeedback} on:submit={() => createNewLayer()}/>
|
||||
{:else }
|
||||
<NextButton clss="primary" on:click={() => createNewLayer()}>
|
||||
Create layer {$newLayerId}
|
||||
</NextButton>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $layerIdFeedback !== undefined}
|
||||
<div class="alert">
|
||||
{$layerIdFeedback}
|
||||
{:else if state === "loading"}
|
||||
<div class="w-8 h-8">
|
||||
<Loading />
|
||||
</div>
|
||||
{:else }
|
||||
<NextButton clss="primary" on:click={() => createNewLayer()}>
|
||||
Create layer {$newLayerId}
|
||||
</NextButton>
|
||||
{:else if state === "editing_layer"}
|
||||
<EditLayer {initialLayerConfig} state={editLayerState} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else if state === "loading"}
|
||||
<div class="w-8 h-8">
|
||||
<Loading />
|
||||
</div>
|
||||
{:else if state === "editing_layer"}
|
||||
<EditLayer {initialLayerConfig} />
|
||||
{/if}
|
||||
</LoginToggle>
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -639,9 +639,87 @@
|
|||
"question": "What values of the freeform key should be interpreted as 'unknown'?",
|
||||
"ifunset": "The question will be considered answered if any value is set for the key"
|
||||
},
|
||||
"type": "array",
|
||||
"type": [
|
||||
{
|
||||
"$ref": "#/definitions/{and:TagConfigJson[];}"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/{or:TagConfigJson[];}"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "For example, if a feature has `shop=yes`, the question 'what type of shop is this?' should still asked"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"freeform",
|
||||
"invalidValues",
|
||||
"and"
|
||||
],
|
||||
"required": false,
|
||||
"hints": {
|
||||
"typehint": "tag"
|
||||
},
|
||||
"type": [
|
||||
{
|
||||
"$ref": "#/definitions/{and:TagConfigJson[];}"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"or": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/TagConfigJson"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"or"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "The main representation of Tags.\nSee https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md for more documentation"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"freeform",
|
||||
"invalidValues",
|
||||
"or"
|
||||
],
|
||||
"required": false,
|
||||
"hints": {
|
||||
"typehint": "tag"
|
||||
},
|
||||
"type": [
|
||||
{
|
||||
"$ref": "#/definitions/{and:TagConfigJson[];}"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"or": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/TagConfigJson"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"or"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "The main representation of Tags.\nSee https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Tags_format.md for more documentation"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"question"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue