Compare commits

...

3 Commits

Author SHA1 Message Date
Harel M 5312d61598 Add globe support in Maputnik UI (#1379)
## Launch Checklist

Add a small drop down to select mercator or globe.
This isn't a fully covered field as one can set an expression there, but
I believe this is good enough for most cases.

Before:
<img width="645" height="254" alt="image"
src="https://github.com/user-attachments/assets/19a7ec50-a0bb-4ea3-b9fc-3abc5572c47e"
/>
After:
<img width="770" height="462" alt="image"
src="https://github.com/user-attachments/assets/f4774020-1cc8-45fe-88f9-f77ad7c53140"
/>


 - [x] Briefly describe the changes in this PR.
- [x] Include before/after visuals or gifs if this PR includes visual
changes.
 - [x] Write tests for all new functionality.
 - [x] Add an entry to `CHANGELOG.md` under the `## main` section.

---------

Co-authored-by: Birk Skyum <birk.skyum@pm.me>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-09-14 11:48:29 +02:00
Harel M 56cdfd23df Add react-markdown to better show the docs (#1378)
## Launch Checklist

This improves how the docs are being presented, since we added some
markdown in the docs it is better to have full support for this here as
well

Before:
<img width="645" height="254" alt="image"
src="https://github.com/user-attachments/assets/ce099ab9-eabd-4721-9550-5ea251439d93"
/>

After:
<img width="645" height="254" alt="image"
src="https://github.com/user-attachments/assets/884d1d69-4238-412e-b620-f9c0640723ca"
/>

The table below that is also stretched is fixes in the following PR:

https://github.com/maplibre/maputnik/pull/1377/files#diff-8d22a87c6893aa31e22f1db0804e1fe93534f38d9fb4433e71cd3878f8daa954

 - [x] Briefly describe the changes in this PR.
- [x] Include before/after visuals or gifs if this PR includes visual
changes.
 - [x] Write tests for all new functionality.
 - [x] Add an entry to `CHANGELOG.md` under the `## main` section.
2025-09-14 12:12:21 +03:00
Harel M 69143ea5d6 Add global state modal (#1377)
## Launch Checklist

This adds the ability to edit the global state.
I think it deserves a modal of its own since I don't think it should be
part of other modals...
Here are some images:
<img width="1274" height="254" alt="image"
src="https://github.com/user-attachments/assets/4b6f2564-6c71-47da-9f8c-3bd2b97e1163"
/>

Initial dialog with no variable:
<img width="640" height="254" alt="image"
src="https://github.com/user-attachments/assets/b813b540-cae9-4c80-b2c0-4d965c022cb8"
/>
After you click add a few times:
<img width="640" height="254" alt="image"
src="https://github.com/user-attachments/assets/125cb978-90dc-4047-9694-b0ffc6eaa469"
/>

The state is updated as you change thing in the dialog.
I didn't complicated it to select the type of the variable, but this can
be added later of if there's a requirement to do so, I meant to keep it
simple for now.

 - [x] Briefly describe the changes in this PR.
- [x] Include before/after visuals or gifs if this PR includes visual
changes.
 - [x] Write tests for all new functionality.
 - [x] Add an entry to `CHANGELOG.md` under the `## main` section.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-09-14 11:13:52 +03:00
18 changed files with 1524 additions and 60 deletions
+3
View File
@@ -5,6 +5,9 @@
- Add support for hillshade's color arrays and relief-color elevation expression - Add support for hillshade's color arrays and relief-color elevation expression
- Change layers icons to make them a bit more distinct - Change layers icons to make them a bit more distinct
- Remove `@mdi` packages in favor of `react-icons` - Remove `@mdi` packages in favor of `react-icons`
- Add ability to control the projection of the map - either globe or mercator
- Add markdown support for doc related to the style-spec fields
- Added global state modal to allow editing the global state
- _...Add new stuff here..._ - _...Add new stuff here..._
### 🐞 Bug fixes ### 🐞 Bug fixes
+14
View File
@@ -491,6 +491,20 @@ describe("layers", () => {
when.click("field-doc-button-Offset", 0); when.click("field-doc-button-Offset", 0);
then(get.elementByTestId("spec-field-doc")).shouldContainText("Offset distance"); then(get.elementByTestId("spec-field-doc")).shouldContainText("Offset distance");
}); });
it.only("should hide spec info when clicking a second time", () => {
when.modal.fillLayers({
type: "symbol",
layer: "example",
});
when.hover("spec-field-container:text-rotate");
then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
when.click("field-doc-button-Rotate", 0);
when.wait(200);
when.click("field-doc-button-Rotate", 0);
then(get.elementByTestId("spec-field-doc")).shouldNotBeVisible();
});
}); });
describe("raster", () => { describe("raster", () => {
+74
View File
@@ -236,6 +236,29 @@ describe("modals", () => {
).shouldInclude({ "maputnik:locationiq_access_token": apiKey }); ).shouldInclude({ "maputnik:locationiq_access_token": apiKey });
}); });
it("style projection mercator", () => {
when.select("modal:settings.projection", "mercator");
then(
get.styleFromLocalStorage().then((style) => style.projection)
).shouldInclude({ type: "mercator" });
});
it("style projection globe", () => {
when.select("modal:settings.projection", "globe");
then(
get.styleFromLocalStorage().then((style) => style.projection)
).shouldInclude({ type: "globe" });
});
it("style projection vertical-perspective", () => {
when.select("modal:settings.projection", "vertical-perspective");
then(
get.styleFromLocalStorage().then((style) => style.projection)
).shouldInclude({ type: "vertical-perspective" });
});
it("style renderer", () => { it("style renderer", () => {
cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
when.select("modal:settings.maputnik:renderer", "ol"); when.select("modal:settings.maputnik:renderer", "ol");
@@ -304,6 +327,57 @@ describe("modals", () => {
it("toggle"); it("toggle");
}); });
describe("global state", () => {
beforeEach(() => {
when.click("nav:global-state");
});
it("add variable", () => {
when.click("global-state-add-variable");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key1: { default: "value" } },
});
});
it("add multiple variables", () => {
when.click("global-state-add-variable");
when.click("global-state-add-variable");
when.click("global-state-add-variable");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key1: { default: "value" }, key2: { default: "value" }, key3: { default: "value" } },
});
});
it("remove variable", () => {
when.click("global-state-add-variable");
when.click("global-state-add-variable");
when.click("global-state-add-variable");
when.click("global-state-remove-variable", 0);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key2: { default: "value" }, key3: { default: "value" } },
});
});
it("edit variable key", () => {
when.click("global-state-add-variable");
when.setValue("global-state-variable-key:0", "mykey");
when.typeKeys("{enter}");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { mykey: { default: "value" } },
});
});
it("edit variable value", () => {
when.click("global-state-add-variable");
when.setValue("global-state-variable-value:0", "myvalue");
when.typeKeys("{enter}");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
state: { key1: { default: "myvalue" } },
});
});
});
describe("Handle localStorage QuotaExceededError", () => { describe("Handle localStorage QuotaExceededError", () => {
it("handles quota exceeded error when opening style from URL", () => { it("handles quota exceeded error when opening style from URL", () => {
// Clear localStorage to start fresh // Clear localStorage to start fresh
+1169 -16
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -69,6 +69,7 @@
"react-file-reader-input": "^2.0.0", "react-file-reader-input": "^2.0.0",
"react-i18next": "^15.7.3", "react-i18next": "^15.7.3",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"string-hash": "^1.1.3", "string-hash": "^1.1.3",
+21 -6
View File
@@ -25,6 +25,7 @@ import ModalSources from "./modals/ModalSources";
import ModalOpen from "./modals/ModalOpen"; import ModalOpen from "./modals/ModalOpen";
import ModalShortcuts from "./modals/ModalShortcuts"; import ModalShortcuts from "./modals/ModalShortcuts";
import ModalDebug from "./modals/ModalDebug"; import ModalDebug from "./modals/ModalDebug";
import ModalGlobalState from "./modals/ModalGlobalState";
import {downloadGlyphsMetadata, downloadSpriteMetadata} from "../libs/metadata"; import {downloadGlyphsMetadata, downloadSpriteMetadata} from "../libs/metadata";
import style from "../libs/style"; import style from "../libs/style";
@@ -126,6 +127,7 @@ type AppState = {
shortcuts: boolean shortcuts: boolean
export: boolean export: boolean
debug: boolean debug: boolean
globalState: boolean
} }
fileHandle: FileSystemFileHandle | null fileHandle: FileSystemFileHandle | null
}; };
@@ -164,6 +166,7 @@ export default class App extends React.Component<any, AppState> {
shortcuts: false, shortcuts: false,
export: false, export: false,
debug: false, debug: false,
globalState: false,
}, },
maplibreGlDebugOptions: { maplibreGlDebugOptions: {
showTileBoundaries: false, showTileBoundaries: false,
@@ -213,6 +216,12 @@ export default class App extends React.Component<any, AppState> {
this.toggleModal("settings"); this.toggleModal("settings");
} }
}, },
{
key: "g",
handler: () => {
this.toggleModal("globalState");
}
},
{ {
key: "i", key: "i",
handler: () => { handler: () => {
@@ -911,39 +920,45 @@ export default class App extends React.Component<any, AppState> {
onChangeMaplibreGlDebug={this.onChangeMaplibreGlDebug} onChangeMaplibreGlDebug={this.onChangeMaplibreGlDebug}
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug} onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
isOpen={this.state.isOpen.debug} isOpen={this.state.isOpen.debug}
onOpenToggle={this.toggleModal.bind(this, "debug")} onOpenToggle={() => this.toggleModal("debug")}
mapView={this.state.mapView} mapView={this.state.mapView}
/> />
<ModalShortcuts <ModalShortcuts
isOpen={this.state.isOpen.shortcuts} isOpen={this.state.isOpen.shortcuts}
onOpenToggle={this.toggleModal.bind(this, "shortcuts")} onOpenToggle={() => this.toggleModal("shortcuts")}
/> />
<ModalSettings <ModalSettings
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged} onStyleChanged={this.onStyleChanged}
onChangeMetadataProperty={this.onChangeMetadataProperty} onChangeMetadataProperty={this.onChangeMetadataProperty}
isOpen={this.state.isOpen.settings} isOpen={this.state.isOpen.settings}
onOpenToggle={this.toggleModal.bind(this, "settings")} onOpenToggle={() => this.toggleModal("settings")}
/> />
<ModalExport <ModalExport
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged} onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.export} isOpen={this.state.isOpen.export}
onOpenToggle={this.toggleModal.bind(this, "export")} onOpenToggle={() => this.toggleModal("export")}
fileHandle={this.state.fileHandle} fileHandle={this.state.fileHandle}
onSetFileHandle={this.onSetFileHandle} onSetFileHandle={this.onSetFileHandle}
/> />
<ModalOpen <ModalOpen
isOpen={this.state.isOpen.open} isOpen={this.state.isOpen.open}
onStyleOpen={this.openStyle} onStyleOpen={this.openStyle}
onOpenToggle={this.toggleModal.bind(this, "open")} onOpenToggle={() => this.toggleModal("open")}
fileHandle={this.state.fileHandle} fileHandle={this.state.fileHandle}
/> />
<ModalSources <ModalSources
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged} onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.sources} isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, "sources")} onOpenToggle={() => this.toggleModal("sources")}
/>
<ModalGlobalState
mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.globalState}
onOpenToggle={() => this.toggleModal("globalState")}
/> />
</div>; </div>;
+6 -1
View File
@@ -9,7 +9,8 @@ import {
MdHelpOutline, MdHelpOutline,
MdFindInPage, MdFindInPage,
MdLanguage, MdLanguage,
MdSave MdSave,
MdPublic
} from "react-icons/md"; } from "react-icons/md";
import pkgJson from "../../package.json"; import pkgJson from "../../package.json";
//@ts-ignore //@ts-ignore
@@ -236,6 +237,10 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
<MdSettings /> <MdSettings />
<IconText>{t("Style Settings")}</IconText> <IconText>{t("Style Settings")}</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction wdKey="nav:global-state" onClick={this.props.onToggleModal.bind(this, "globalState")}>
<MdPublic />
<IconText>{t("Global State")}</IconText>
</ToolbarAction>
<ToolbarSelect wdKey="nav:inspect"> <ToolbarSelect wdKey="nav:inspect">
<MdFindInPage /> <MdFindInPage />
+16 -4
View File
@@ -1,10 +1,10 @@
import React from "react"; import React from "react";
import Markdown from "react-markdown";
const headers = { const headers = {
js: "JS", js: "JS",
android: "Android", android: "Android",
ios: "iOS", ios: "iOS"
macos: "macOS",
}; };
type DocProps = { type DocProps = {
@@ -37,11 +37,23 @@ export default class Doc extends React.Component<DocProps> {
!Array.isArray(values) !Array.isArray(values)
); );
const sdkSupportToJsx = (value: string) => {
const supportValue = value.toLowerCase();
if (supportValue.startsWith("https://")) {
return <a href={supportValue} target="_blank" rel="noreferrer">{"#" + supportValue.split("/").pop()}</a>;
}
return value;
};
return ( return (
<> <>
{doc && {doc &&
<div className="SpecDoc"> <div className="SpecDoc">
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>{doc}</div> <div className="SpecDoc__doc" data-wd-key='spec-field-doc'>
<Markdown components={{
a: ({node: _node, href, children, ...props}) => <a href={href} target="_blank" {...props}>{children}</a>,
}}>{doc}</Markdown>
</div>
{renderValues && {renderValues &&
<ul className="SpecDoc__values"> <ul className="SpecDoc__values">
{Object.entries(values).map(([key, value]) => { {Object.entries(values).map(([key, value]) => {
@@ -74,7 +86,7 @@ export default class Doc extends React.Component<DocProps> {
<td>{key}</td> <td>{key}</td>
{Object.keys(headers).map((k) => { {Object.keys(headers).map((k) => {
if (Object.prototype.hasOwnProperty.call(supportObj, k)) { if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
return <td key={k}>{supportObj[k as keyof typeof headers]}</td>; return <td key={k}>{sdkSupportToJsx(supportObj[k as keyof typeof headers])}</td>;
} }
else { else {
return <td key={k}>no</td>; return <td key={k}>no</td>;
+2 -2
View File
@@ -8,7 +8,7 @@ type ModalInternalProps = PropsWithChildren & {
"data-wd-key"?: string "data-wd-key"?: string
isOpen: boolean isOpen: boolean
title: string title: string
onOpenToggle(value: boolean): unknown onOpenToggle(): void
underlayClickExits?: boolean underlayClickExits?: boolean
className?: string className?: string
} & WithTranslation; } & WithTranslation;
@@ -26,7 +26,7 @@ class ModalInternal extends React.Component<ModalInternalProps> {
} }
setTimeout(() => { setTimeout(() => {
this.props.onOpenToggle(false); this.props.onOpenToggle();
}, 0); }, 0);
}; };
+2 -2
View File
@@ -14,7 +14,7 @@ type ModalAddInternalProps = {
layers: LayerSpecification[] layers: LayerSpecification[]
onLayersChange(layers: LayerSpecification[]): unknown onLayersChange(layers: LayerSpecification[]): unknown
isOpen: boolean isOpen: boolean
onOpenToggle(open: boolean): unknown onOpenToggle(): void
// A dict of source id's and the available source layers // A dict of source id's and the available source layers
sources: Record<string, SourceSpecification & {layers: string[]}>; sources: Record<string, SourceSpecification & {layers: string[]}>;
} & WithTranslation; } & WithTranslation;
@@ -50,7 +50,7 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
changedLayers.push(layer as LayerSpecification); changedLayers.push(layer as LayerSpecification);
this.setState({ error: null }, () => { this.setState({ error: null }, () => {
this.props.onLayersChange(changedLayers); this.props.onLayersChange(changedLayers);
this.props.onOpenToggle(false); this.props.onOpenToggle();
}); });
}; };
+1 -1
View File
@@ -9,7 +9,7 @@ type ModalDebugInternalProps = {
renderer: string renderer: string
onChangeMaplibreGlDebug(key: string, checked: boolean): unknown onChangeMaplibreGlDebug(key: string, checked: boolean): unknown
onChangeOpenlayersDebug(key: string, checked: boolean): unknown onChangeOpenlayersDebug(key: string, checked: boolean): unknown
onOpenToggle(value: boolean): unknown onOpenToggle(): void
maplibreGlDebugOptions?: object maplibreGlDebugOptions?: object
openlayersDebugOptions?: object openlayersDebugOptions?: object
mapView: { mapView: {
+1 -1
View File
@@ -22,7 +22,7 @@ type ModalExportInternalProps = {
mapStyle: StyleSpecificationWithId mapStyle: StyleSpecificationWithId
onStyleChanged: OnStyleChangedCallback onStyleChanged: OnStyleChangedCallback
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(): void
onSetFileHandle(fileHandle: FileSystemFileHandle | null): unknown onSetFileHandle(fileHandle: FileSystemFileHandle | null): unknown
fileHandle: FileSystemFileHandle | null fileHandle: FileSystemFileHandle | null
} & WithTranslation; } & WithTranslation;
+155
View File
@@ -0,0 +1,155 @@
import React from "react";
import { withTranslation, type WithTranslation } from "react-i18next";
import { MdDelete } from "react-icons/md";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import Modal from "./Modal";
import FieldString from "../FieldString";
import InputButton from "../InputButton";
import { PiListPlusBold } from "react-icons/pi";
import { type StyleSpecificationWithId } from "../../libs/definitions";
import { type SchemaSpecification } from "maplibre-gl";
import Doc from "../Doc";
type ModalGlobalStateInternalProps = {
mapStyle: StyleSpecificationWithId;
isOpen: boolean;
onStyleChanged(style: StyleSpecificationWithId): void;
onOpenToggle(): void
} & WithTranslation;
type GlobalStateVariable = {
key: string;
value: any;
};
const ModalGlobalStateInternal: React.FC<ModalGlobalStateInternalProps> = (props) => {
const getGlobalStateVariables = (): GlobalStateVariable[] => {
const style = props.mapStyle;
const globalState = style.state || {};
return Object.entries(globalState).map(([key, value]) => ({
key,
value: value.default
}));
};
const setGlobalStateVariables = (variables: GlobalStateVariable[]) => {
const style = { ...props.mapStyle };
// Create the globalState object from the variables array
const globalState: Record<string, SchemaSpecification> = {};
for (const variable of variables) {
if (variable.key.trim() !== "") {
globalState[variable.key] = {
default: variable.value
};
}
}
style.state = Object.keys(globalState).length > 0 ? globalState : undefined;
props.onStyleChanged(style);
};
const onAddVariable = () => {
const variables = getGlobalStateVariables();
let index = 1;
while (variables.find(v => v.key === `key${index}`)) {
index++;
}
variables.push({ key: `key${index}`, value: "value" });
setGlobalStateVariables(variables);
};
const onRemoveVariable = (index: number) => {
const variables = getGlobalStateVariables();
variables.splice(index, 1);
setGlobalStateVariables(variables);
};
const onChangeVariableKey = (index: number, newKey: string) => {
const variables = getGlobalStateVariables();
variables[index].key = newKey || "";
setGlobalStateVariables(variables);
};
const onChangeVariableValue = (index: number, newValue: string) => {
const variables = getGlobalStateVariables();
variables[index].value = newValue || "";
setGlobalStateVariables(variables);
};
const variables = getGlobalStateVariables();
const variableFields = variables.map((variable, index) => (
<tr key={index}>
<td>
<FieldString
label={props.t("Key")}
value={variable.key}
onChange={(value) => onChangeVariableKey(index, value || "")}
data-wd-key={"global-state-variable-key:" + index}
/>
</td>
<td>
<FieldString
label={props.t("Value")}
value={variable.value}
onChange={(value) => onChangeVariableValue(index, value || "")}
data-wd-key={"global-state-variable-value:" + index}
/>
</td>
<td style={{ verticalAlign: "middle"}}>
<InputButton
onClick={() => onRemoveVariable(index)}
title={props.t("Remove variable")}
data-wd-key="global-state-remove-variable"
>
<MdDelete />
</InputButton>
</td>
</tr>
));
return (
<Modal
data-wd-key="modal:global-state"
isOpen={props.isOpen}
onOpenToggle={props.onOpenToggle}
title={props.t("Global State Variables")}
>
{variables.length === 0 &&
<div>
<p>{props.t("No global state variables defined. Add variables to create reusable values in your style.")}</p>
<div key="doc" className="maputnik-doc-inline">
<Doc fieldSpec={latest.$root.state} />
</div>
</div>
}
{variables.length > 0 &&
<table>
<thead>
</thead>
<tbody>
{variableFields}
</tbody>
</table>
}
<div>
<InputButton
onClick={onAddVariable}
data-wd-key="global-state-add-variable"
>
<PiListPlusBold />
{props.t("Add Variable")}
</InputButton>
</div>
</Modal>
);
};
const ModalGlobalState = withTranslation()(ModalGlobalStateInternal);
export default ModalGlobalState;
+1 -1
View File
@@ -45,7 +45,7 @@ class PublicStyle extends React.Component<PublicStyleProps> {
type ModalOpenInternalProps = { type ModalOpenInternalProps = {
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(): void
onStyleOpen(...args: unknown[]): unknown onStyleOpen(...args: unknown[]): unknown
fileHandle: FileSystemFileHandle | null fileHandle: FileSystemFileHandle | null
} & WithTranslation; } & WithTranslation;
+55 -23
View File
@@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json"; import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import type {LightSpecification, StyleSpecification, TerrainSpecification, TransitionSpecification} from "maplibre-gl"; import type {LightSpecification, ProjectionSpecification, StyleSpecification, TerrainSpecification, TransitionSpecification} from "maplibre-gl";
import { type WithTranslation, withTranslation } from "react-i18next"; import { type WithTranslation, withTranslation } from "react-i18next";
import FieldArray from "../FieldArray"; import FieldArray from "../FieldArray";
@@ -19,7 +19,7 @@ type ModalSettingsInternalProps = {
onStyleChanged: OnStyleChangedCallback onStyleChanged: OnStyleChangedCallback
onChangeMetadataProperty(...args: unknown[]): unknown onChangeMetadataProperty(...args: unknown[]): unknown
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(): void
} & WithTranslation; } & WithTranslation;
class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps> { class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps> {
@@ -79,6 +79,24 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
}); });
} }
changeProjectionType(value: any) {
const projection = {
...this.props.mapStyle.projection,
} as ProjectionSpecification;
if (value === undefined) {
delete projection.type;
}
else {
projection.type = value;
}
this.props.onStyleChanged({
...this.props.mapStyle,
projection,
});
}
changeStyleProperty(property: keyof StyleSpecification | "owner", value: any) { changeStyleProperty(property: keyof StyleSpecification | "owner", value: any) {
const changedStyle = { const changedStyle = {
...this.props.mapStyle, ...this.props.mapStyle,
@@ -103,6 +121,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
const light = this.props.mapStyle.light || {}; const light = this.props.mapStyle.light || {};
const transition = this.props.mapStyle.transition || {}; const transition = this.props.mapStyle.transition || {};
const terrain = this.props.mapStyle.terrain || {} as TerrainSpecification; const terrain = this.props.mapStyle.terrain || {} as TerrainSpecification;
const projection = this.props.mapStyle.projection || {} as ProjectionSpecification;
return <Modal return <Modal
data-wd-key="modal:settings" data-wd-key="modal:settings"
@@ -116,21 +135,21 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.name} fieldSpec={latest.$root.name}
data-wd-key="modal:settings.name" data-wd-key="modal:settings.name"
value={this.props.mapStyle.name} value={this.props.mapStyle.name}
onChange={this.changeStyleProperty.bind(this, "name")} onChange={(value) => this.changeStyleProperty("name", value)}
/> />
<FieldString <FieldString
label={t("Owner")} label={t("Owner")}
fieldSpec={{doc: t("Owner ID of the style. Used by Mapbox or future style APIs.")}} fieldSpec={{doc: t("Owner ID of the style. Used by Mapbox or future style APIs.")}}
data-wd-key="modal:settings.owner" data-wd-key="modal:settings.owner"
value={(this.props.mapStyle as any).owner} value={(this.props.mapStyle as any).owner}
onChange={this.changeStyleProperty.bind(this, "owner")} onChange={(value) => this.changeStyleProperty("owner", value)}
/> />
<FieldUrl <FieldUrl
fieldSpec={latest.$root.sprite} fieldSpec={latest.$root.sprite}
label={t("Sprite URL")} label={t("Sprite URL")}
data-wd-key="modal:settings.sprite" data-wd-key="modal:settings.sprite"
value={this.props.mapStyle.sprite as string} value={this.props.mapStyle.sprite as string}
onChange={this.changeStyleProperty.bind(this, "sprite")} onChange={(value) => this.changeStyleProperty("sprite", value)}
/> />
<FieldUrl <FieldUrl
@@ -138,7 +157,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.glyphs} fieldSpec={latest.$root.glyphs}
data-wd-key="modal:settings.glyphs" data-wd-key="modal:settings.glyphs"
value={this.props.mapStyle.glyphs as string} value={this.props.mapStyle.glyphs as string}
onChange={this.changeStyleProperty.bind(this, "glyphs")} onChange={(value) => this.changeStyleProperty("glyphs", value)}
/> />
<FieldString <FieldString
@@ -146,7 +165,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={fsa.maputnik.maptiler_access_token} fieldSpec={fsa.maputnik.maptiler_access_token}
data-wd-key="modal:settings.maputnik:openmaptiles_access_token" data-wd-key="modal:settings.maputnik:openmaptiles_access_token"
value={metadata["maputnik:openmaptiles_access_token"]} value={metadata["maputnik:openmaptiles_access_token"]}
onChange={onChangeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")} onChange={(value) => onChangeMetadataProperty("maputnik:openmaptiles_access_token", value)}
/> />
<FieldString <FieldString
@@ -154,7 +173,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={fsa.maputnik.thunderforest_access_token} fieldSpec={fsa.maputnik.thunderforest_access_token}
data-wd-key="modal:settings.maputnik:thunderforest_access_token" data-wd-key="modal:settings.maputnik:thunderforest_access_token"
value={metadata["maputnik:thunderforest_access_token"]} value={metadata["maputnik:thunderforest_access_token"]}
onChange={onChangeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")} onChange={(value) => onChangeMetadataProperty("maputnik:thunderforest_access_token", value)}
/> />
<FieldString <FieldString
@@ -162,7 +181,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={fsa.maputnik.stadia_access_token} fieldSpec={fsa.maputnik.stadia_access_token}
data-wd-key="modal:settings.maputnik:stadia_access_token" data-wd-key="modal:settings.maputnik:stadia_access_token"
value={metadata["maputnik:stadia_access_token"]} value={metadata["maputnik:stadia_access_token"]}
onChange={onChangeMetadataProperty.bind(this, "maputnik:stadia_access_token")} onChange={(value) => onChangeMetadataProperty("maputnik:stadia_access_token", value)}
/> />
<FieldString <FieldString
@@ -170,7 +189,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={fsa.maputnik.locationiq_access_token} fieldSpec={fsa.maputnik.locationiq_access_token}
data-wd-key="modal:settings.maputnik:locationiq_access_token" data-wd-key="modal:settings.maputnik:locationiq_access_token"
value={metadata["maputnik:locationiq_access_token"]} value={metadata["maputnik:locationiq_access_token"]}
onChange={onChangeMetadataProperty.bind(this, "maputnik:locationiq_access_token")} onChange={(value) => onChangeMetadataProperty("maputnik:locationiq_access_token", value)}
/> />
<FieldArray <FieldArray
@@ -180,7 +199,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
type="number" type="number"
value={mapStyle.center || []} value={mapStyle.center || []}
default={[0, 0]} default={[0, 0]}
onChange={this.changeStyleProperty.bind(this, "center")} onChange={(value) => this.changeStyleProperty("center", value)}
/> />
<FieldNumber <FieldNumber
@@ -188,7 +207,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.zoom} fieldSpec={latest.$root.zoom}
value={mapStyle.zoom} value={mapStyle.zoom}
default={0} default={0}
onChange={this.changeStyleProperty.bind(this, "zoom")} onChange={(value) => this.changeStyleProperty("zoom", value)}
/> />
<FieldNumber <FieldNumber
@@ -196,7 +215,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.bearing} fieldSpec={latest.$root.bearing}
value={mapStyle.bearing} value={mapStyle.bearing}
default={latest.$root.bearing.default} default={latest.$root.bearing.default}
onChange={this.changeStyleProperty.bind(this, "bearing")} onChange={(value) => this.changeStyleProperty("bearing", value)}
/> />
<FieldNumber <FieldNumber
@@ -204,7 +223,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.$root.pitch} fieldSpec={latest.$root.pitch}
value={mapStyle.pitch} value={mapStyle.pitch}
default={latest.$root.pitch.default} default={latest.$root.pitch.default}
onChange={this.changeStyleProperty.bind(this, "pitch")} onChange={(value) => this.changeStyleProperty("pitch", value)}
/> />
<FieldEnum <FieldEnum
@@ -214,7 +233,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
value={light.anchor as string} value={light.anchor as string}
options={Object.keys(latest.light.anchor.values)} options={Object.keys(latest.light.anchor.values)}
default={latest.light.anchor.default} default={latest.light.anchor.default}
onChange={this.changeLightProperty.bind(this, "anchor")} onChange={(value) => this.changeLightProperty("anchor", value)}
/> />
<FieldColor <FieldColor
@@ -222,7 +241,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.light.color} fieldSpec={latest.light.color}
value={light.color as string} value={light.color as string}
default={latest.light.color.default} default={latest.light.color.default}
onChange={this.changeLightProperty.bind(this, "color")} onChange={(value) => this.changeLightProperty("color", value)}
/> />
<FieldNumber <FieldNumber
@@ -230,7 +249,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.light.intensity} fieldSpec={latest.light.intensity}
value={light.intensity as number} value={light.intensity as number}
default={latest.light.intensity.default} default={latest.light.intensity.default}
onChange={this.changeLightProperty.bind(this, "intensity")} onChange={(value) => this.changeLightProperty("intensity", value)}
/> />
<FieldArray <FieldArray
@@ -240,7 +259,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
length={latest.light.position.length} length={latest.light.position.length}
value={light.position as number[]} value={light.position as number[]}
default={latest.light.position.default} default={latest.light.position.default}
onChange={this.changeLightProperty.bind(this, "position")} onChange={(value) => this.changeLightProperty("position", value)}
/> />
<FieldString <FieldString
@@ -248,7 +267,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.terrain.source} fieldSpec={latest.terrain.source}
data-wd-key="modal:settings.maputnik:terrain_source" data-wd-key="modal:settings.maputnik:terrain_source"
value={terrain.source} value={terrain.source}
onChange={this.changeTerrainProperty.bind(this, "source")} onChange={(value) => this.changeTerrainProperty("source", value)}
/> />
<FieldNumber <FieldNumber
@@ -256,7 +275,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.terrain.exaggeration} fieldSpec={latest.terrain.exaggeration}
value={terrain.exaggeration} value={terrain.exaggeration}
default={latest.terrain.exaggeration.default} default={latest.terrain.exaggeration.default}
onChange={this.changeTerrainProperty.bind(this, "exaggeration")} onChange={(value) => this.changeTerrainProperty("exaggeration", value)}
/> />
<FieldNumber <FieldNumber
@@ -264,7 +283,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.transition.delay} fieldSpec={latest.transition.delay}
value={transition.delay} value={transition.delay}
default={latest.transition.delay.default} default={latest.transition.delay.default}
onChange={this.changeTransitionProperty.bind(this, "delay")} onChange={(value) => this.changeTransitionProperty("delay", value)}
/> />
<FieldNumber <FieldNumber
@@ -272,7 +291,20 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
fieldSpec={latest.transition.duration} fieldSpec={latest.transition.duration}
value={transition.duration} value={transition.duration}
default={latest.transition.duration.default} default={latest.transition.duration.default}
onChange={this.changeTransitionProperty.bind(this, "duration")} onChange={(value) => this.changeTransitionProperty("duration", value)}
/>
<FieldSelect
label={t("Projection")}
data-wd-key="modal:settings.projection"
options={[
["", "Undefined"],
["mercator", "Mercator"],
["globe", "Globe"],
["vertical-perspective", "Vertical Perspective"]
]}
value={projection?.type?.toString() || ""}
onChange={(value) => this.changeProjectionType(value)}
/> />
<FieldSelect <FieldSelect
@@ -284,7 +316,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
["ol", t("Open Layers (experimental)")], ["ol", t("Open Layers (experimental)")],
]} ]}
value={metadata["maputnik:renderer"] || "mlgljs"} value={metadata["maputnik:renderer"] || "mlgljs"}
onChange={onChangeMetadataProperty.bind(this, "maputnik:renderer")} onChange={(value) => onChangeMetadataProperty("maputnik:renderer", value)}
/> />
</div> </div>
</Modal>; </Modal>;
+1 -1
View File
@@ -6,7 +6,7 @@ import Modal from "./Modal";
type ModalShortcutsInternalProps = { type ModalShortcutsInternalProps = {
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(): void
} & WithTranslation; } & WithTranslation;
+1 -1
View File
@@ -273,7 +273,7 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
type ModalSourcesInternalProps = { type ModalSourcesInternalProps = {
mapStyle: StyleSpecificationWithId mapStyle: StyleSpecificationWithId
isOpen: boolean isOpen: boolean
onOpenToggle(...args: unknown[]): unknown onOpenToggle(): void
onStyleChanged: OnStyleChangedCallback onStyleChanged: OnStyleChangedCallback
} & WithTranslation; } & WithTranslation;
+1 -1
View File
@@ -252,7 +252,7 @@
margin-top: vars.$margin-3; margin-top: vars.$margin-3;
} }
.SpecDoc__values code { .SpecDoc__values code, .SpecDoc__doc code {
background: vars.$color-midgray; background: vars.$color-midgray;
padding: 0.1em 0.3em; padding: 0.1em 0.3em;
border-radius: 2px; border-radius: 2px;