Merge branch 'main' into dependabot/npm_and_yarn/maplibre/maplibre-gl-style-spec-23.3.0

This commit is contained in:
Birk Skyum
2025-08-07 20:36:45 +02:00
committed by GitHub
49 changed files with 1502 additions and 1429 deletions
+47
View File
@@ -0,0 +1,47 @@
Maputnik is a MapLibre style editor written using React and TypeScript.
To get started, install all npm packages:
```
npm install
```
Verify code correctness by running ESLint:
```
npm run lint
```
Or try fixing lint issues with:
```
npm run lint -- --fix
```
The project type checked and built with:
```
npm run build
```
To run the tests make sure that xvfb is installed:
```
apt install xvfb
```
Run the development server in the background with Vite:
```
nohup npm run start &
```
Then start the Cypress tests with:
```
xvfb-run -a npm run test
```
## Pull Requests
- Pull requests should update `CHANGELOG.md` with a short description of the change.
+6
View File
@@ -11,11 +11,17 @@
- Upgrade to MapLibre LG JS v5 - Upgrade to MapLibre LG JS v5
- Upgrade Vite 6 and Cypress 14 ([#970](https://github.com/maplibre/maputnik/pull/970)) - Upgrade Vite 6 and Cypress 14 ([#970](https://github.com/maplibre/maputnik/pull/970))
- Upgrade OpenLayers from v6 to v10 - Upgrade OpenLayers from v6 to v10
- When loading a style into localStorage that causes a QuotaExceededError, purge localStorage and retry
- Remove react-autobind dependency
- Remove usage of legacy `childContextTypes` API
- Refactor Field components to use arrow function syntax
- Replace react-autocomplete with Downshift in the autocomplete component
- _...Add new stuff here..._ - _...Add new stuff here..._
### 🐞 Bug fixes ### 🐞 Bug fixes
- Fix incorrect handing of network error response (#944) - Fix incorrect handing of network error response (#944)
- Show an error when adding a layer with a duplicate ID
- _...Add new stuff here..._ - _...Add new stuff here..._
## 2.1.1 ## 2.1.1
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:18 as builder FROM node:22 as builder
WORKDIR /maputnik WORKDIR /maputnik
# Only copy package.json to prevent npm install from running on every build # Only copy package.json to prevent npm install from running on every build
+8
View File
@@ -8,6 +8,7 @@ export default defineConfig({
exclude: "cypress/**/*.*", exclude: "cypress/**/*.*",
}, },
}, },
e2e: { e2e: {
setupNodeEvents(on, config) { setupNodeEvents(on, config) {
// implement node event listeners here // implement node event listeners here
@@ -20,4 +21,11 @@ export default defineConfig({
openMode: 0, openMode: 0,
}, },
}, },
component: {
devServer: {
framework: "react",
bundler: "vite",
},
},
}); });
+4 -2
View File
@@ -8,8 +8,10 @@ const baseUrl = "http://localhost:8888/";
const styleFromWindow = (win: Window) => { const styleFromWindow = (win: Window) => {
const styleId = win.localStorage.getItem("maputnik:latest_style"); const styleId = win.localStorage.getItem("maputnik:latest_style");
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`); const styleItemKey = `maputnik:style:${styleId}`;
const obj = JSON.parse(styleItem || ""); const styleItem = win.localStorage.getItem(styleItemKey);
if (!styleItem) throw new Error("Could not get styleItem from localStorage");
const obj = JSON.parse(styleItem);
return obj; return obj;
}; };
+56
View File
@@ -272,7 +272,63 @@ describe("modals", () => {
}); });
describe("add layer", () => {
beforeEach(() => {
when.setStyle("layer");
when.modal.open();
});
it("shows duplicate id error", () => {
when.setValue("add-layer.layer-id.input", "background");
when.click("add-layer");
then(get.elementByTestId("modal:add-layer")).shouldExist();
then(get.element(".maputnik-modal-error")).shouldContainText(
"Layer ID already exists"
);
});
});
describe("sources", () => { describe("sources", () => {
it("toggle"); it("toggle");
}); });
describe("Handle localStorage QuotaExceededError", () => {
it("handles quota exceeded error when opening style from URL", () => {
// Clear localStorage to start fresh
cy.clearLocalStorage();
// fill localStorage until we get a QuotaExceededError
cy.window().then(win => {
let chunkSize = 1000;
const chunk = new Array(chunkSize).join("x");
let index = 0;
// Keep adding until we hit the quota
while (true) {
try {
const key = `maputnik:fill-${index++}`;
win.localStorage.setItem(key, chunk);
} catch (e: any) {
// Verify it's a quota error
if (e.name === 'QuotaExceededError') {
if (chunkSize <= 1) return;
else {
chunkSize /= 2;
continue;
}
}
throw e; // Unexpected error
}
}
});
// Open the style via URL input
when.click("nav:open");
when.setValue("modal:open.url.input", get.exampleFileUrl());
when.click("modal:open.url.button");
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
then(get.styleFromLocalStorage()).shouldExist();
});
});
}); });
+103 -1
View File
@@ -3,7 +3,109 @@
"version": 8, "version": 8,
"name": "Test Style", "name": "Test Style",
"metadata": { "metadata": {
"maputnik:renderer": "mlgljs" "maputnik:renderer": "mlgljs",
"data": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
92,
93,
94,
95,
96,
97,
98,
99
]
}, },
"sources": {}, "sources": {},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf", "glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
// ***********************************************************
// This example support/component.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
import { mount } from 'cypress/react'
// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a <reference path="./component" /> at the top of your spec.
declare global {
/* eslint-disable @typescript-eslint/no-namespace */
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}
Cypress.Commands.add('mount', mount)
// Example use:
// cy.mount(<MyComponent />)
+456 -641
View File
File diff suppressed because it is too large Load Diff
+17 -19
View File
@@ -38,8 +38,8 @@
"detect-browser": "^5.3.0", "detect-browser": "^5.3.0",
"events": "^3.3.0", "events": "^3.3.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"i18next": "^25.2.1", "i18next": "^25.3.2",
"i18next-browser-languagedetector": "^8.1.0", "i18next-browser-languagedetector": "^8.2.0",
"i18next-resources-to-backend": "^1.2.1", "i18next-resources-to-backend": "^1.2.1",
"json-stringify-pretty-compact": "^4.0.0", "json-stringify-pretty-compact": "^4.0.0",
"json-to-ast": "^2.1.0", "json-to-ast": "^2.1.0",
@@ -52,7 +52,7 @@
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"maplibre-gl": "^5.5.0", "maplibre-gl": "^5.5.0",
"maputnik-design": "github:maputnik/design#172b06c", "maputnik-design": "github:maputnik/design#172b06c",
"ol": "^10.5.0", "ol": "^10.6.1",
"ol-mapbox-style": "^13.0.1", "ol-mapbox-style": "^13.0.1",
"pmtiles": "^4.3.0", "pmtiles": "^4.3.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
@@ -60,13 +60,12 @@
"react-accessible-accordion": "^5.0.1", "react-accessible-accordion": "^5.0.1",
"react-aria-menubutton": "^7.0.3", "react-aria-menubutton": "^7.0.3",
"react-aria-modal": "^5.0.2", "react-aria-modal": "^5.0.2",
"react-autobind": "^1.0.6", "downshift": "^9.0.10",
"react-autocomplete": "^1.8.1",
"react-collapse": "^5.1.1", "react-collapse": "^5.1.1",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-file-reader-input": "^2.0.0", "react-file-reader-input": "^2.0.0",
"react-i18next": "^15.5.2", "react-i18next": "^15.6.1",
"react-icon-base": "^2.1.2", "react-icon-base": "^2.1.2",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-sortable-hoc": "^2.0.0", "react-sortable-hoc": "^2.0.0",
@@ -93,11 +92,11 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@cypress/code-coverage": "^3.14.4", "@cypress/code-coverage": "^3.14.5",
"@eslint/js": "^9.28.0", "@eslint/js": "^9.32.0",
"@istanbuljs/nyc-config-typescript": "^1.0.2", "@istanbuljs/nyc-config-typescript": "^1.0.2",
"@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-replace": "^6.0.2",
"@shellygo/cypress-test-utils": "^5.0.0", "@shellygo/cypress-test-utils": "^5.0.2",
"@types/codemirror": "^5.60.15", "@types/codemirror": "^5.60.15",
"@types/color": "^4.2.0", "@types/color": "^4.2.0",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
@@ -114,7 +113,6 @@
"@types/react": "^18.2.67", "@types/react": "^18.2.67",
"@types/react-aria-menubutton": "^6.2.14", "@types/react-aria-menubutton": "^6.2.14",
"@types/react-aria-modal": "^5.0.0", "@types/react-aria-modal": "^5.0.0",
"@types/react-autocomplete": "^1.8.11",
"@types/react-collapse": "^5.0.4", "@types/react-collapse": "^5.0.4",
"@types/react-color": "^3.0.13", "@types/react-color": "^3.0.13",
"@types/react-dom": "^18.2.22", "@types/react-dom": "^18.2.22",
@@ -123,27 +121,27 @@
"@types/string-hash": "^1.1.3", "@types/string-hash": "^1.1.3",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@types/wicg-file-system-access": "^2023.10.6", "@types/wicg-file-system-access": "^2023.10.6",
"@vitejs/plugin-react": "^4.5.1", "@vitejs/plugin-react": "^5.0.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"cypress": "^14.4.1", "cypress": "^14.5.3",
"cypress-plugin-tab": "^1.0.5", "cypress-plugin-tab": "^1.0.5",
"eslint": "^9.28.0", "eslint": "^9.32.0",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20", "eslint-plugin-react-refresh": "^0.4.20",
"i18next-parser": "^9.3.0", "i18next-parser": "^9.3.0",
"istanbul": "^0.4.5", "istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.2.2", "istanbul-lib-coverage": "^3.2.2",
"postcss": "^8.5.4", "postcss": "^8.5.6",
"react-hot-loader": "^4.13.1", "react-hot-loader": "^4.13.1",
"sass": "^1.89.1", "sass": "^1.90.0",
"stylelint": "^16.20.0", "stylelint": "^16.23.0",
"stylelint-config-recommended-scss": "^15.0.1", "stylelint-config-recommended-scss": "^15.0.1",
"stylelint-scss": "^6.12.0", "stylelint-scss": "^6.12.1",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.33.1", "typescript-eslint": "^8.38.0",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vite": "^6.3.5", "vite": "^6.3.5",
"vite-plugin-istanbul": "^7.0.0" "vite-plugin-istanbul": "^7.1.0"
} }
} }
+2 -5
View File
@@ -1,5 +1,3 @@
// @ts-ignore - this can be easily replaced with arrow functions
import autoBind from 'react-autobind';
import React from 'react' import React from 'react'
import cloneDeep from 'lodash.clonedeep' import cloneDeep from 'lodash.clonedeep'
import clamp from 'lodash.clamp' import clamp from 'lodash.clamp'
@@ -140,7 +138,6 @@ export default class App extends React.Component<any, AppState> {
constructor(props: any) { constructor(props: any) {
super(props) super(props)
autoBind(this);
this.revisionStore = new RevisionStore() this.revisionStore = new RevisionStore()
const params = new URLSearchParams(window.location.search.substring(1)) const params = new URLSearchParams(window.location.search.substring(1))
@@ -880,8 +877,8 @@ export default class App extends React.Component<any, AppState> {
this.setModal(modalName, !this.state.isOpen[modalName]); this.setModal(modalName, !this.state.isOpen[modalName]);
} }
onSetFileHandle(fileHandle: FileSystemFileHandle | null) { onSetFileHandle = (fileHandle: FileSystemFileHandle | null) => {
this.setState({fileHandle: fileHandle}); this.setState({ fileHandle });
} }
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => { onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
+4 -11
View File
@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types'
import ScrollContainer from './ScrollContainer' import ScrollContainer from './ScrollContainer'
import { WithTranslation, withTranslation } from 'react-i18next'; import { WithTranslation, withTranslation } from 'react-i18next';
import { IconContext } from 'react-icons';
type AppLayoutInternalProps = { type AppLayoutInternalProps = {
toolbar: React.ReactElement toolbar: React.ReactElement
@@ -13,20 +13,12 @@ type AppLayoutInternalProps = {
} & WithTranslation; } & WithTranslation;
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> { class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
static childContextTypes = {
reactIconBase: PropTypes.object
}
getChildContext() {
return {
reactIconBase: { size: 14 }
}
}
render() { render() {
document.body.dir = this.props.i18n.dir(); document.body.dir = this.props.i18n.dir();
return <div className="maputnik-layout"> return <IconContext.Provider value={{size: '14px'}}>
<div className="maputnik-layout">
{this.props.toolbar} {this.props.toolbar}
<div className="maputnik-layout-main"> <div className="maputnik-layout-main">
<div className="maputnik-layout-list"> <div className="maputnik-layout-list">
@@ -45,6 +37,7 @@ class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
} }
{this.props.modals} {this.props.modals}
</div> </div>
</IconContext.Provider>
} }
} }
+8 -7
View File
@@ -1,4 +1,3 @@
import React from 'react'
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray' import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
import Fieldset from './Fieldset' import Fieldset from './Fieldset'
@@ -9,10 +8,12 @@ type FieldArrayProps = InputArrayProps & {
} }
}; };
export default class FieldArray extends React.Component<FieldArrayProps> { const FieldArray: React.FC<FieldArrayProps> = (props) => {
render() { return (
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}> <Fieldset label={props.label} fieldSpec={props.fieldSpec}>
<InputArray {...this.props} /> <InputArray {...props} />
</Fieldset> </Fieldset>
} );
} };
export default FieldArray;
+8 -7
View File
@@ -1,4 +1,3 @@
import React from 'react'
import Block from './Block' import Block from './Block'
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete' import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
@@ -8,10 +7,12 @@ type FieldAutocompleteProps = InputAutocompleteProps & {
}; };
export default class FieldAutocomplete extends React.Component<FieldAutocompleteProps> { const FieldAutocomplete: React.FC<FieldAutocompleteProps> = (props) => {
render() { return (
return <Block label={this.props.label}> <Block label={props.label}>
<InputAutocomplete {...this.props} /> <InputAutocomplete {...props} />
</Block> </Block>
} );
} };
export default FieldAutocomplete;
+8 -7
View File
@@ -1,4 +1,3 @@
import React from 'react'
import Block from './Block' import Block from './Block'
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox' import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
@@ -8,10 +7,12 @@ type FieldCheckboxProps = InputCheckboxProps & {
}; };
export default class FieldCheckbox extends React.Component<FieldCheckboxProps> { const FieldCheckbox: React.FC<FieldCheckboxProps> = (props) => {
render() { return (
return <Block label={this.props.label}> <Block label={props.label}>
<InputCheckbox {...this.props} /> <InputCheckbox {...props} />
</Block> </Block>
} );
} };
export default FieldCheckbox;
+8 -7
View File
@@ -1,4 +1,3 @@
import React from 'react'
import Block from './Block' import Block from './Block'
import InputColor, {InputColorProps} from './InputColor' import InputColor, {InputColorProps} from './InputColor'
@@ -11,10 +10,12 @@ type FieldColorProps = InputColorProps & {
}; };
export default class FieldColor extends React.Component<FieldColorProps> { const FieldColor: React.FC<FieldColorProps> = (props) => {
render() { return (
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}> <Block label={props.label} fieldSpec={props.fieldSpec}>
<InputColor {...this.props} /> <InputColor {...props} />
</Block> </Block>
} );
} };
export default FieldColor;
+12 -10
View File
@@ -10,29 +10,31 @@ type FieldCommentInternalProps = {
error: {message: string} error: {message: string}
} & WithTranslation; } & WithTranslation;
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> { const FieldCommentInternal: React.FC<FieldCommentInternalProps> = (props) => {
render() { const t = props.t;
const t = this.props.t;
const fieldSpec = { const fieldSpec = {
doc: t("Comments for the current layer. This is non-standard and not in the spec."), doc: t(
"Comments for the current layer. This is non-standard and not in the spec."
),
}; };
return <Block return (
<Block
label={t("Comments")} label={t("Comments")}
fieldSpec={fieldSpec} fieldSpec={fieldSpec}
data-wd-key="layer-comment" data-wd-key="layer-comment"
error={this.props.error} error={props.error}
> >
<InputString <InputString
multi={true} multi={true}
value={this.props.value} value={props.value}
onChange={this.props.onChange} onChange={props.onChange}
default={t("Comment...")} default={t("Comment...")}
data-wd-key="layer-comment.input" data-wd-key="layer-comment.input"
/> />
</Block> </Block>
} );
} };
const FieldComment = withTranslation()(FieldCommentInternal); const FieldComment = withTranslation()(FieldCommentInternal);
export default FieldComment; export default FieldComment;
+26 -38
View File
@@ -9,57 +9,45 @@ type FieldDocLabelProps = {
onToggleDoc?(...args: unknown[]): unknown onToggleDoc?(...args: unknown[]): unknown
}; };
type FieldDocLabelState = {
open: boolean const FieldDocLabel: React.FC<FieldDocLabelProps> = (props) => {
const [open, setOpen] = React.useState(false);
const onToggleDoc = (state: boolean) => {
setOpen(state);
if (props.onToggleDoc) {
props.onToggleDoc(state);
}
}; };
export default class FieldDocLabel extends React.Component<FieldDocLabelProps, FieldDocLabelState> { const { label, fieldSpec } = props;
constructor (props: FieldDocLabelProps) {
super(props);
this.state = {
open: false,
}
}
onToggleDoc = (open: boolean) => {
this.setState({
open,
}, () => {
if (this.props.onToggleDoc) {
this.props.onToggleDoc(this.state.open);
}
});
}
render() {
const {label, fieldSpec} = this.props;
const { doc } = fieldSpec || {}; const { doc } = fieldSpec || {};
if (doc) { if (doc) {
return <label className="maputnik-doc-wrapper"> return (
<label className="maputnik-doc-wrapper">
<div className="maputnik-doc-target"> <div className="maputnik-doc-target">
{label} {label}
{'\xa0'} {'\xa0'}
<button <button
aria-label={this.state.open ? "close property documentation" : "open property documentation"} aria-label={open ? 'close property documentation' : 'open property documentation'}
className={`maputnik-doc-button maputnik-doc-button--${this.state.open ? 'open' : 'closed'}`} className={`maputnik-doc-button maputnik-doc-button--${open ? 'open' : 'closed'}`}
onClick={() => this.onToggleDoc(!this.state.open)} onClick={() => onToggleDoc(!open)}
data-wd-key={'field-doc-button-' + label} data-wd-key={'field-doc-button-' + label}
> >
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />} {open ? <MdHighlightOff /> : <MdInfoOutline />}
</button> </button>
</div> </div>
</label> </label>
} );
else if (label) { } else if (label) {
return <label className="maputnik-doc-wrapper"> return (
<div className="maputnik-doc-target"> <label className="maputnik-doc-wrapper">
{label} <div className="maputnik-doc-target">{label}</div>
</div>
</label> </label>
);
} }
else { return <div />;
<div /> };
}
} export default FieldDocLabel;
}
+8 -7
View File
@@ -1,4 +1,3 @@
import React from 'react'
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray' import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
import Fieldset from './Fieldset' import Fieldset from './Fieldset'
@@ -6,10 +5,12 @@ type FieldDynamicArrayProps = InputDynamicArrayProps & {
name?: string name?: string
}; };
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> { const FieldDynamicArray: React.FC<FieldDynamicArrayProps> = (props) => {
render() { return (
return <Fieldset label={this.props.label}> <Fieldset label={props.label}>
<InputDynamicArray {...this.props} /> <InputDynamicArray {...props} />
</Fieldset> </Fieldset>
} );
} };
export default FieldDynamicArray;
+8 -7
View File
@@ -1,4 +1,3 @@
import React from 'react'
import InputEnum, {InputEnumProps} from './InputEnum' import InputEnum, {InputEnumProps} from './InputEnum'
import Fieldset from './Fieldset'; import Fieldset from './Fieldset';
@@ -11,10 +10,12 @@ type FieldEnumProps = InputEnumProps & {
}; };
export default class FieldEnum extends React.Component<FieldEnumProps> { const FieldEnum: React.FC<FieldEnumProps> = (props) => {
render() { return (
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}> <Fieldset label={props.label} fieldSpec={props.fieldSpec}>
<InputEnum {...this.props} /> <InputEnum {...props} />
</Fieldset> </Fieldset>
} );
} };
export default FieldEnum;
+177 -205
View File
@@ -111,296 +111,268 @@ type FieldFunctionProps = {
value?: any value?: any
}; };
type FieldFunctionState = {
dataType: string
isEditing: boolean
}
/** Supports displaying spec field for zoom function objects /** Supports displaying spec field for zoom function objects
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property * https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
*/ */
export default class FieldFunction extends React.Component<FieldFunctionProps, FieldFunctionState> { const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
constructor (props: FieldFunctionProps) { const [dataType, setDataType] = React.useState(
super(props); getDataType(props.value, props.fieldSpec)
this.state = { );
dataType: getDataType(props.value, props.fieldSpec), const [isEditing, setIsEditing] = React.useState(false);
isEditing: false,
}
}
static getDerivedStateFromProps(props: Readonly<FieldFunctionProps>, state: FieldFunctionState) { React.useEffect(() => {
// Because otherwise when editing values we end up accidentally changing field type. if (!isEditing) {
if (state.isEditing) { setDataType(getDataType(props.value, props.fieldSpec));
return {};
}
else {
return {
isEditing: false,
dataType: getDataType(props.value, props.fieldSpec)
};
}
} }
}, [props.value, props.fieldSpec, isEditing]);
getFieldFunctionType(fieldSpec: any) { const getFieldFunctionType = (fieldSpec: any) => {
if (fieldSpec.expression.interpolated) { if (fieldSpec.expression.interpolated) {
return "exponential" return 'exponential';
} }
if (fieldSpec.type === "number") { if (fieldSpec.type === 'number') {
return "interval" return 'interval';
}
return "categorical"
} }
return 'categorical';
};
addStop = () => { const addStop = () => {
const stops = this.props.value.stops.slice(0) const stops = props.value.stops.slice(0);
const lastStop = stops[stops.length - 1] const lastStop = stops[stops.length - 1];
if (typeof lastStop[0] === "object") { if (typeof lastStop[0] === 'object') {
stops.push([ stops.push([
{ zoom: lastStop[0].zoom + 1, value: lastStop[0].value }, { zoom: lastStop[0].zoom + 1, value: lastStop[0].value },
lastStop[1] lastStop[1],
]) ]);
} } else {
else { stops.push([lastStop[0] + 1, lastStop[1]]);
stops.push([lastStop[0] + 1, lastStop[1]])
} }
const changedValue = { const changedValue = {
...this.props.value, ...props.value,
stops: stops, stops: stops,
} };
this.props.onChange(this.props.fieldName, changedValue) props.onChange(props.fieldName, changedValue);
} };
deleteExpression = () => { const deleteExpression = () => {
const {fieldSpec, fieldName} = this.props; const { fieldSpec, fieldName } = props;
this.props.onChange(fieldName, fieldSpec.default); props.onChange(fieldName, fieldSpec.default);
this.setState({ setDataType('value');
dataType: "value", };
});
}
deleteStop = (stopIdx: number) => { const deleteStop = (stopIdx: number) => {
const stops = this.props.value.stops.slice(0) const stops = props.value.stops.slice(0);
stops.splice(stopIdx, 1) stops.splice(stopIdx, 1);
let changedValue = { let changedValue: any = {
...this.props.value, ...props.value,
stops: stops, stops: stops,
} };
if (stops.length === 1) { if (stops.length === 1) {
changedValue = stops[0][1] changedValue = stops[0][1];
} }
this.props.onChange(this.props.fieldName, changedValue) props.onChange(props.fieldName, changedValue);
} };
makeZoomFunction = () => { const makeZoomFunction = () => {
const {value} = this.props; const { value } = props;
let zoomFunc; let zoomFunc: any;
if (typeof(value) === "object") { if (typeof value === 'object') {
if (value.stops) { if (value.stops) {
zoomFunc = { zoomFunc = {
base: value.base, base: value.base,
stops: value.stops.map((stop: Stop) => { stops: value.stops.map((stop: Stop) => {
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)]; return [stop[0].zoom, stop[1] || findDefaultFromSpec(props.fieldSpec)];
}) }),
} };
} } else {
else {
zoomFunc = { zoomFunc = {
base: value.base, base: value.base,
stops: [ stops: [
[6, findDefaultFromSpec(this.props.fieldSpec)], [6, findDefaultFromSpec(props.fieldSpec)],
[10, findDefaultFromSpec(this.props.fieldSpec)] [10, findDefaultFromSpec(props.fieldSpec)],
] ],
};
} }
} } else {
}
else {
zoomFunc = { zoomFunc = {
stops: [ stops: [
[6, value || findDefaultFromSpec(this.props.fieldSpec)], [6, value || findDefaultFromSpec(props.fieldSpec)],
[10, value || findDefaultFromSpec(this.props.fieldSpec)] [10, value || findDefaultFromSpec(props.fieldSpec)],
] ],
} };
} }
this.props.onChange(this.props.fieldName, zoomFunc) props.onChange(props.fieldName, zoomFunc);
} };
undoExpression = () => { const undoExpression = () => {
const {value, fieldName} = this.props; const { value, fieldName } = props;
if (isGetExpression(value)) { if (isGetExpression(value)) {
this.props.onChange(fieldName, { props.onChange(fieldName, {
"type": "identity", type: 'identity',
"property": value[1] property: value[1],
});
this.setState({
dataType: "value",
}); });
setDataType('value');
} else if (isLiteralExpression(value)) {
props.onChange(fieldName, value[1]);
setDataType('value');
} }
else if (isLiteralExpression(value)) { };
this.props.onChange(fieldName, value[1]);
this.setState({
dataType: "value",
});
}
}
canUndo = () => { const canUndo = () => {
const {value, fieldSpec} = this.props; const { value, fieldSpec } = props;
return ( return (
isGetExpression(value) || isGetExpression(value) ||
isLiteralExpression(value) || isLiteralExpression(value) ||
isPrimative(value) || isPrimative(value) ||
(Array.isArray(value) && fieldSpec.type === "array") (Array.isArray(value) && fieldSpec.type === 'array')
); );
} };
makeExpression = () => { const makeExpression = () => {
const {value, fieldSpec} = this.props; const { value, fieldSpec } = props;
let expression; let expression;
if (typeof(value) === "object" && 'stops' in value) { if (typeof value === 'object' && 'stops' in value) {
expression = styleFunction.convertFunction(value, fieldSpec); expression = styleFunction.convertFunction(value, fieldSpec);
} else if (isIdentityProperty(value)) {
expression = ['get', value.property];
} else {
expression = ['literal', value || props.fieldSpec.default];
} }
else if (isIdentityProperty(value)) { props.onChange(props.fieldName, expression);
expression = ["get", value.property]; };
}
else {
expression = ["literal", value || this.props.fieldSpec.default];
}
this.props.onChange(this.props.fieldName, expression);
}
makeDataFunction = () => { const makeDataFunction = () => {
const functionType = this.getFieldFunctionType(this.props.fieldSpec); const functionType = getFieldFunctionType(props.fieldSpec);
const stopValue = functionType === 'categorical' ? '' : 0; const stopValue = functionType === 'categorical' ? '' : 0;
const {value} = this.props; const { value } = props;
let dataFunc; let dataFunc;
if (typeof(value) === "object") { if (typeof value === 'object') {
if (value.stops) { if (value.stops) {
dataFunc = { dataFunc = {
property: "", property: '',
type: functionType, type: functionType,
base: value.base, base: value.base,
stops: value.stops.map((stop: Stop) => { stops: value.stops.map((stop: Stop) => {
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)]; return [{ zoom: stop[0], value: stopValue }, stop[1] || findDefaultFromSpec(props.fieldSpec)];
}) }),
} };
} } else {
else {
dataFunc = { dataFunc = {
property: "", property: '',
type: functionType, type: functionType,
base: value.base, base: value.base,
stops: [ stops: [
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)], [{ zoom: 6, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)] [{ zoom: 10, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
] ],
};
} }
} } else {
}
else {
dataFunc = { dataFunc = {
property: "", property: '',
type: functionType, type: functionType,
base: value.base, base: value.base,
stops: [ stops: [
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)], [{ zoom: 6, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)] [{ zoom: 10, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
] ],
} };
} }
this.props.onChange(this.props.fieldName, dataFunc) props.onChange(props.fieldName, dataFunc);
} };
onMarkEditing = () => { const onMarkEditing = () => {
this.setState({isEditing: true}); setIsEditing(true);
} };
onUnmarkEditing = () => { const onUnmarkEditing = () => {
this.setState({isEditing: false}); setIsEditing(false);
} };
const propClass =
props.fieldSpec.default === props.value ? 'maputnik-default-property' : 'maputnik-modified-property';
render() {
const {dataType} = this.state;
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
let specField; let specField;
if (dataType === "expression") { if (dataType === 'expression') {
specField = ( specField = (
<ExpressionProperty <ExpressionProperty
errors={this.props.errors} errors={props.errors}
onChange={this.props.onChange.bind(this, this.props.fieldName)} onChange={props.onChange.bind(null, props.fieldName)}
canUndo={this.canUndo} canUndo={canUndo}
onUndo={this.undoExpression} onUndo={undoExpression}
onDelete={this.deleteExpression} onDelete={deleteExpression}
fieldType={this.props.fieldType} fieldType={props.fieldType}
fieldName={this.props.fieldName} fieldName={props.fieldName}
fieldSpec={this.props.fieldSpec} fieldSpec={props.fieldSpec}
value={this.props.value} value={props.value}
onFocus={this.onMarkEditing} onFocus={onMarkEditing}
onBlur={this.onUnmarkEditing} onBlur={onUnmarkEditing}
/>
);
} else if (dataType === 'zoom_function') {
specField = (
<ZoomProperty
errors={props.errors}
onChange={props.onChange.bind(null)}
fieldType={props.fieldType}
fieldName={props.fieldName}
fieldSpec={props.fieldSpec}
value={props.value}
onDeleteStop={deleteStop}
onAddStop={addStop}
onChangeToDataFunction={makeDataFunction}
onExpressionClick={makeExpression}
/>
);
} else if (dataType === 'data_function') {
specField = (
<DataProperty
errors={props.errors}
onChange={props.onChange.bind(null)}
fieldType={props.fieldType}
fieldName={props.fieldName}
fieldSpec={props.fieldSpec}
value={props.value}
onDeleteStop={deleteStop}
onAddStop={addStop}
onChangeToZoomFunction={makeZoomFunction}
onExpressionClick={makeExpression}
/>
);
} else {
specField = (
<SpecProperty
errors={props.errors}
onChange={props.onChange.bind(null)}
fieldType={props.fieldType}
fieldName={props.fieldName}
fieldSpec={props.fieldSpec}
value={props.value}
onZoomClick={makeZoomFunction}
onDataClick={makeDataFunction}
onExpressionClick={makeExpression}
/> />
); );
} }
else if (dataType === "zoom_function") {
specField = ( return (
<ZoomProperty <div className={propClass} data-wd-key={'spec-field-container:' + props.fieldName}>
errors={this.props.errors}
onChange={this.props.onChange.bind(this)}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onDeleteStop={this.deleteStop}
onAddStop={this.addStop}
onChangeToDataFunction={this.makeDataFunction}
onExpressionClick={this.makeExpression}
/>
)
}
else if (dataType === "data_function") {
// TODO: Rename to FieldFunction **this file** shouldn't be called that
specField = (
<DataProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this)}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onDeleteStop={this.deleteStop}
onAddStop={this.addStop}
onChangeToZoomFunction={this.makeZoomFunction}
onExpressionClick={this.makeExpression}
/>
)
}
else {
specField = (
<SpecProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this)}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onZoomClick={this.makeZoomFunction}
onDataClick={this.makeDataFunction}
onExpressionClick={this.makeExpression}
/>
)
}
return <div className={propClass} data-wd-key={"spec-field-container:"+this.props.fieldName}>
{specField} {specField}
</div> </div>
} );
} };
export default FieldFunction;
+12 -12
View File
@@ -1,4 +1,3 @@
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 Block from './Block' import Block from './Block'
@@ -11,18 +10,19 @@ type FieldIdProps = {
error?: {message: string} error?: {message: string}
}; };
export default class FieldId extends React.Component<FieldIdProps> { const FieldId: React.FC<FieldIdProps> = (props) => {
render() { return (
return <Block label="ID" fieldSpec={latest.layer.id} <Block label="ID" fieldSpec={latest.layer.id}
data-wd-key={props.wdKey}
data-wd-key={this.props.wdKey} error={props.error}
error={this.props.error}
> >
<InputString <InputString
value={this.props.value} value={props.value}
onInput={this.props.onChange} onInput={props.onChange}
data-wd-key={this.props.wdKey + ".input"} data-wd-key={props.wdKey + ".input"}
/> />
</Block> </Block>
} );
} };
export default FieldId;
+5 -6
View File
@@ -1,12 +1,11 @@
import React from 'react'
import InputJson, {InputJsonProps} from './InputJson' import InputJson, {InputJsonProps} from './InputJson'
type FieldJsonProps = InputJsonProps & {}; type FieldJsonProps = InputJsonProps & {};
export default class FieldJson extends React.Component<FieldJsonProps> { const FieldJson: React.FC<FieldJsonProps> = (props) => {
render() { return <InputJson {...props} />;
return <InputJson {...this.props} /> };
}
} export default FieldJson;
+9 -9
View File
@@ -11,25 +11,25 @@ type FieldMaxZoomInternalProps = {
error?: {message: string} error?: {message: string}
} & WithTranslation; } & WithTranslation;
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> { const FieldMaxZoomInternal: React.FC<FieldMaxZoomInternalProps> = (props) => {
render() { const t = props.t;
const t = this.props.t; return (
return <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom} <Block label={t('Max Zoom')} fieldSpec={latest.layer.maxzoom}
error={this.props.error} error={props.error}
data-wd-key="max-zoom" data-wd-key="max-zoom"
> >
<InputNumber <InputNumber
allowRange={true} allowRange={true}
value={this.props.value} value={props.value}
onChange={this.props.onChange} onChange={props.onChange}
min={latest.layer.maxzoom.minimum} min={latest.layer.maxzoom.minimum}
max={latest.layer.maxzoom.maximum} max={latest.layer.maxzoom.maximum}
default={latest.layer.maxzoom.maximum} default={latest.layer.maxzoom.maximum}
data-wd-key="max-zoom.input" data-wd-key="max-zoom.input"
/> />
</Block> </Block>
} );
} };
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal); const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
export default FieldMaxZoom; export default FieldMaxZoom;
+9 -9
View File
@@ -11,25 +11,25 @@ type FieldMinZoomInternalProps = {
error?: {message: string} error?: {message: string}
} & WithTranslation; } & WithTranslation;
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> { const FieldMinZoomInternal: React.FC<FieldMinZoomInternalProps> = (props) => {
render() { const t = props.t;
const t = this.props.t; return (
return <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom} <Block label={t('Min Zoom')} fieldSpec={latest.layer.minzoom}
error={this.props.error} error={props.error}
data-wd-key="min-zoom" data-wd-key="min-zoom"
> >
<InputNumber <InputNumber
allowRange={true} allowRange={true}
value={this.props.value} value={props.value}
onChange={this.props.onChange} onChange={props.onChange}
min={latest.layer.minzoom.minimum} min={latest.layer.minzoom.minimum}
max={latest.layer.minzoom.maximum} max={latest.layer.minzoom.maximum}
default={latest.layer.minzoom.minimum} default={latest.layer.minzoom.minimum}
data-wd-key='min-zoom.input' data-wd-key='min-zoom.input'
/> />
</Block> </Block>
} );
} };
const FieldMinZoom = withTranslation()(FieldMinZoomInternal); const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
export default FieldMinZoom; export default FieldMinZoom;
+8 -7
View File
@@ -1,4 +1,3 @@
import React from 'react'
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput' import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
import Fieldset from './Fieldset' import Fieldset from './Fieldset'
@@ -8,10 +7,12 @@ type FieldMultiInputProps = InputMultiInputProps & {
}; };
export default class FieldMultiInput extends React.Component<FieldMultiInputProps> { const FieldMultiInput: React.FC<FieldMultiInputProps> = (props) => {
render() { return (
return <Fieldset label={this.props.label}> <Fieldset label={props.label}>
<InputMultiInput {...this.props} /> <InputMultiInput {...props} />
</Fieldset> </Fieldset>
} );
} };
export default FieldMultiInput;
+8 -7
View File
@@ -1,4 +1,3 @@
import React from 'react'
import InputNumber, {InputNumberProps} from './InputNumber' import InputNumber, {InputNumberProps} from './InputNumber'
import Block from './Block' import Block from './Block'
@@ -11,10 +10,12 @@ type FieldNumberProps = InputNumberProps & {
}; };
export default class FieldNumber extends React.Component<FieldNumberProps> { const FieldNumber: React.FC<FieldNumberProps> = (props) => {
render() { return (
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}> <Block label={props.label} fieldSpec={props.fieldSpec}>
<InputNumber {...this.props} /> <InputNumber {...props} />
</Block> </Block>
} );
} };
export default FieldNumber;
+8 -7
View File
@@ -1,4 +1,3 @@
import React from 'react'
import Block from './Block' import Block from './Block'
import InputSelect, {InputSelectProps} from './InputSelect' import InputSelect, {InputSelectProps} from './InputSelect'
@@ -11,10 +10,12 @@ type FieldSelectProps = InputSelectProps & {
}; };
export default class FieldSelect extends React.Component<FieldSelectProps> { const FieldSelect: React.FC<FieldSelectProps> = (props) => {
render() { return (
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}> <Block label={props.label} fieldSpec={props.fieldSpec}>
<InputSelect {...this.props}/> <InputSelect {...props} />
</Block> </Block>
} );
} };
export default FieldSelect;
+17 -17
View File
@@ -13,28 +13,28 @@ type FieldSourceInternalProps = {
error?: {message: string} error?: {message: string}
} & WithTranslation; } & WithTranslation;
class FieldSourceInternal extends React.Component<FieldSourceInternalProps> { const FieldSourceInternal: React.FC<FieldSourceInternalProps> = (props) => {
static defaultProps = { const t = props.t;
onChange: () => {}, return (
sourceIds: [], <Block
} label={t('Source')}
render() {
const t = this.props.t;
return <Block
label={t("Source")}
fieldSpec={latest.layer.source} fieldSpec={latest.layer.source}
error={this.props.error} error={props.error}
data-wd-key={this.props.wdKey} data-wd-key={props.wdKey}
> >
<InputAutocomplete <InputAutocomplete
value={this.props.value} value={props.value}
onChange={this.props.onChange} onChange={props.onChange}
options={this.props.sourceIds?.map(src => [src, src])} options={props.sourceIds?.map((src) => [src, src])}
/> />
</Block> </Block>
} );
} };
FieldSourceInternal.defaultProps = {
onChange: () => {},
sourceIds: [],
};
const FieldSource = withTranslation()(FieldSourceInternal); const FieldSource = withTranslation()(FieldSourceInternal);
export default FieldSource; export default FieldSource;
+17 -18
View File
@@ -13,30 +13,29 @@ type FieldSourceLayerInternalProps = {
error?: {message: string} error?: {message: string}
} & WithTranslation; } & WithTranslation;
class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalProps> { const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = (props) => {
static defaultProps = { const t = props.t;
onChange: () => {}, return (
sourceLayerIds: [], <Block
isFixed: false label={t('Source Layer')}
}
render() {
const t = this.props.t;
return <Block
label={t("Source Layer")}
fieldSpec={latest.layer['source-layer']} fieldSpec={latest.layer['source-layer']}
data-wd-key="layer-source-layer" data-wd-key="layer-source-layer"
error={this.props.error} error={props.error}
> >
<InputAutocomplete <InputAutocomplete
keepMenuWithinWindowBounds={!!this.props.isFixed} value={props.value}
value={this.props.value} onChange={props.onChange}
onChange={this.props.onChange} options={props.sourceLayerIds?.map((l) => [l, l])}
options={this.props.sourceLayerIds?.map(l => [l, l])}
/> />
</Block> </Block>
} );
} };
FieldSourceLayerInternal.defaultProps = {
onChange: () => {},
sourceLayerIds: [],
isFixed: false,
};
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal); const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
export default FieldSourceLayer; export default FieldSourceLayer;
+8 -7
View File
@@ -1,4 +1,3 @@
import React from 'react'
import Block from './Block' import Block from './Block'
import InputString, {InputStringProps} from './InputString' import InputString, {InputStringProps} from './InputString'
@@ -10,10 +9,12 @@ type FieldStringProps = InputStringProps & {
} }
}; };
export default class FieldString extends React.Component<FieldStringProps> { const FieldString: React.FC<FieldStringProps> = (props) => {
render() { return (
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}> <Block label={props.label} fieldSpec={props.fieldSpec}>
<InputString {...this.props} /> <InputString {...props} />
</Block> </Block>
} );
} };
export default FieldString;
+20 -23
View File
@@ -14,24 +14,17 @@ type FieldTypeInternalProps = {
disabled?: boolean disabled?: boolean
} & WithTranslation; } & WithTranslation;
class FieldTypeInternal extends React.Component<FieldTypeInternalProps> { const FieldTypeInternal: React.FC<FieldTypeInternalProps> = (props) => {
static defaultProps = { const t = props.t;
disabled: false, return (
} <Block label={t('Type')} fieldSpec={latest.layer.type}
data-wd-key={props.wdKey}
render() { error={props.error}
const t = this.props.t;
return <Block label={t("Type")} fieldSpec={latest.layer.type}
data-wd-key={this.props.wdKey}
error={this.props.error}
> >
{this.props.disabled && {props.disabled && (
<InputString <InputString value={props.value} disabled={true} />
value={this.props.value} )}
disabled={true} {!props.disabled && (
/>
}
{!this.props.disabled &&
<InputSelect <InputSelect
options={[ options={[
['background', 'Background'], ['background', 'Background'],
@@ -44,14 +37,18 @@ class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
['hillshade', 'Hillshade'], ['hillshade', 'Hillshade'],
['heatmap', 'Heatmap'], ['heatmap', 'Heatmap'],
]} ]}
onChange={this.props.onChange} onChange={props.onChange}
value={this.props.value} value={props.value}
data-wd-key={this.props.wdKey + ".select"} data-wd-key={props.wdKey + '.select'}
/> />
} )}
</Block> </Block>
} );
} };
FieldTypeInternal.defaultProps = {
disabled: false,
};
const FieldType = withTranslation()(FieldTypeInternal); const FieldType = withTranslation()(FieldTypeInternal);
export default FieldType; export default FieldType;
+6 -7
View File
@@ -1,4 +1,3 @@
import React from 'react'
import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl' import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl'
import Block from './Block' import Block from './Block'
@@ -11,12 +10,12 @@ type FieldUrlProps = InputUrlProps & {
}; };
export default class FieldUrl extends React.Component<FieldUrlProps> { const FieldUrl: React.FC<FieldUrlProps> = (props) => {
render () {
return ( return (
<Block label={this.props.label} fieldSpec={this.props.fieldSpec}> <Block label={props.label} fieldSpec={props.fieldSpec}>
<InputUrl {...this.props} /> <InputUrl {...props} />
</Block> </Block>
); );
} };
}
export default FieldUrl;
+27 -44
View File
@@ -9,57 +9,40 @@ type FieldsetProps = PropsWithChildren & {
action?: ReactElement, action?: ReactElement,
}; };
type FieldsetState = {
showDoc: boolean const Fieldset: React.FC<FieldsetProps> = (props) => {
const [showDoc, setShowDoc] = React.useState(false);
const labelId = React.useRef(generateUniqueId('fieldset_label_'));
const onToggleDoc = (val: boolean) => {
setShowDoc(val);
}; };
export default class Fieldset extends React.Component<FieldsetProps, FieldsetState> { return (
_labelId: string; <div className="maputnik-input-block" role="group" aria-labelledby={labelId.current}>
{props.fieldSpec && (
constructor (props: FieldsetProps) {
super(props);
this._labelId = generateUniqueId(`fieldset_label_`);
this.state = {
showDoc: false,
}
}
onToggleDoc = (val: boolean) => {
this.setState({
showDoc: val
});
}
render () {
return <div className="maputnik-input-block" role="group" aria-labelledby={this._labelId}>
{this.props.fieldSpec &&
<div className="maputnik-input-block-label"> <div className="maputnik-input-block-label">
<FieldDocLabel <FieldDocLabel
label={this.props.label} label={props.label}
onToggleDoc={this.onToggleDoc} onToggleDoc={onToggleDoc}
fieldSpec={this.props.fieldSpec} fieldSpec={props.fieldSpec}
/> />
</div> </div>
} )}
{!this.props.fieldSpec && {!props.fieldSpec && (
<div className="maputnik-input-block-label"> <div className="maputnik-input-block-label">
{this.props.label} {props.label}
</div> </div>
} )}
<div className="maputnik-input-block-action"> <div className="maputnik-input-block-action">{props.action}</div>
{this.props.action} <div className="maputnik-input-block-content">{props.children}</div>
{props.fieldSpec && (
<div className="maputnik-doc-inline" style={{ display: showDoc ? '' : 'none' }}>
<Doc fieldSpec={props.fieldSpec} />
</div> </div>
<div className="maputnik-input-block-content"> )}
{this.props.children}
</div> </div>
{this.props.fieldSpec && );
<div };
className="maputnik-doc-inline"
style={{display: this.state.showDoc ? '' : 'none'}} export default Fieldset;
>
<Doc fieldSpec={this.props.fieldSpec} />
</div>
}
</div>
}
}
+18
View File
@@ -0,0 +1,18 @@
import InputAutocomplete from './InputAutocomplete'
import { mount } from 'cypress/react'
const fruits = ['apple', 'banana', 'cherry'];
describe('<InputAutocomplete />', () => {
it('filters options when typing', () => {
mount(
<InputAutocomplete aria-label="Fruit" options={fruits.map(f => [f, f])} />
);
cy.get('input').focus();
cy.get('.maputnik-autocomplete-menu-item').should('have.length', 3);
cy.get('input').type('ch');
cy.get('.maputnik-autocomplete-menu-item').should('have.length', 1).and('contain', 'cherry');
cy.get('.maputnik-autocomplete-menu-item').click();
cy.get('input').should('have.value', 'cherry');
});
});
+89 -73
View File
@@ -1,100 +1,116 @@
import React from 'react' import React from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import Autocomplete from 'react-autocomplete' import {useCombobox} from 'downshift'
const MAX_HEIGHT = 140
const MAX_HEIGHT = 140;
export type InputAutocompleteProps = { export type InputAutocompleteProps = {
value?: string value?: string
options: any[] options?: any[]
onChange(value: string | undefined): unknown onChange?(value: string | undefined): unknown
keepMenuWithinWindowBounds?: boolean
'aria-label'?: string 'aria-label'?: string
}; };
export default class InputAutocomplete extends React.Component<InputAutocompleteProps> { export default function InputAutocomplete({
state = { value,
maxHeight: MAX_HEIGHT options = [],
onChange = () => {},
'aria-label': ariaLabel,
}: InputAutocompleteProps) {
const [input, setInput] = React.useState(value || '')
const menuRef = React.useRef<HTMLDivElement>(null)
const [maxHeight, setMaxHeight] = React.useState(MAX_HEIGHT)
const filteredItems = React.useMemo(() => {
const lv = input.toLowerCase()
return options.filter((item) => item[0].toLowerCase().includes(lv))
}, [options, input])
const calcMaxHeight = React.useCallback(() => {
if (menuRef.current) {
const space = window.innerHeight - menuRef.current.getBoundingClientRect().top
setMaxHeight(Math.min(space, MAX_HEIGHT))
} }
}, [])
autocompleteMenuEl: HTMLDivElement | null = null; const {
isOpen,
static defaultProps = { getMenuProps,
onChange: () => {}, getInputProps,
options: [], getItemProps,
highlightedIndex,
openMenu,
} = useCombobox({
items: filteredItems,
inputValue: input,
itemToString: (item) => (item ? item[0] : ''),
stateReducer: (_state, action) => {
if (action.type === useCombobox.stateChangeTypes.InputClick) {
return {...action.changes, isOpen: true}
} }
return action.changes
calcMaxHeight() { },
if(this.props.keepMenuWithinWindowBounds) { onSelectedItemChange: ({selectedItem}) => {
const maxHeight = window.innerHeight - this.autocompleteMenuEl!.getBoundingClientRect().top; const v = selectedItem ? selectedItem[0] : ''
const limitedMaxHeight = Math.min(maxHeight, MAX_HEIGHT); setInput(v)
onChange(selectedItem ? selectedItem[0] : undefined)
if(limitedMaxHeight != this.state.maxHeight) { },
this.setState({ onInputValueChange: ({inputValue: v}) => {
maxHeight: limitedMaxHeight if (typeof v === 'string') {
setInput(v)
onChange(v === '' ? undefined : v)
openMenu()
}
},
}) })
}
}
}
componentDidMount() { React.useEffect(() => {
this.calcMaxHeight(); if (isOpen) {
calcMaxHeight()
} }
}, [isOpen, calcMaxHeight])
componentDidUpdate() { React.useEffect(() => {
this.calcMaxHeight(); window.addEventListener('resize', calcMaxHeight)
} return () => window.removeEventListener('resize', calcMaxHeight)
}, [calcMaxHeight])
onChange(v: string) { React.useEffect(() => {
this.props.onChange(v === "" ? undefined : v); setInput(value || '')
} }, [value])
render() { return (
return <div <div className="maputnik-autocomplete">
ref={(el) => { <input
this.autocompleteMenuEl = el; {...getInputProps({
}} 'aria-label': ariaLabel,
className: 'maputnik-string',
spellCheck: false,
onFocus: () => openMenu(),
})}
/>
<div
{...getMenuProps({}, {suppressRefError: true})}
ref={menuRef}
style={{position: 'fixed', overflow: 'auto', maxHeight, zIndex: 998}}
className="maputnik-autocomplete-menu"
> >
<Autocomplete {isOpen &&
menuStyle={{ filteredItems.map((item, index) => (
position: "fixed",
overflow: "auto",
maxHeight: this.state.maxHeight,
zIndex: '998'
}}
wrapperProps={{
className: "maputnik-autocomplete",
style: {}
}}
inputProps={{
'aria-label': this.props['aria-label'],
className: "maputnik-string",
spellCheck: false
}}
value={this.props.value}
items={this.props.options}
getItemValue={(item) => item[0]}
onSelect={v => this.onChange(v)}
onChange={(_e, v) => this.onChange(v)}
shouldItemRender={(item, value="") => {
if (typeof(value) === "string") {
return item[0].toLowerCase().indexOf(value.toLowerCase()) > -1
}
return false
}}
renderItem={(item, isHighlighted) => (
<div <div
key={item[0]} key={item[0]}
className={classnames({ {...getItemProps({
"maputnik-autocomplete-menu-item": true, item,
"maputnik-autocomplete-menu-item-selected": isHighlighted, index,
className: classnames('maputnik-autocomplete-menu-item', {
'maputnik-autocomplete-menu-item-selected': highlightedIndex === index,
}),
})} })}
> >
{item[1]} {item[1]}
</div> </div>
)} ))}
/>
</div> </div>
} </div>
)
} }
+4 -14
View File
@@ -1,8 +1,8 @@
import React, {type JSX} from 'react' import React, {type JSX} from 'react'
import PropTypes from 'prop-types'
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton' import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
import {Accordion} from 'react-accessible-accordion'; import {Accordion} from 'react-accessible-accordion';
import {MdMoreVert} from 'react-icons/md' import {MdMoreVert} from 'react-icons/md'
import { IconContext } from 'react-icons'
import {BackgroundLayerSpecification, LayerSpecification, SourceSpecification} from 'maplibre-gl'; import {BackgroundLayerSpecification, LayerSpecification, SourceSpecification} from 'maplibre-gl';
import FieldJson from './FieldJson' import FieldJson from './FieldJson'
@@ -86,10 +86,6 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
onLayerDestroyed: () => {}, onLayerDestroyed: () => {},
} }
static childContextTypes = {
reactIconBase: PropTypes.object
}
constructor(props: LayerEditorInternalProps) { constructor(props: LayerEditorInternalProps) {
super(props) super(props)
@@ -116,14 +112,6 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
}; };
} }
getChildContext () {
return {
reactIconBase: {
size: 14,
color: '#8e8e8e',
}
}
}
changeProperty(group: keyof LayerSpecification | null, property: string, newValue: any) { changeProperty(group: keyof LayerSpecification | null, property: string, newValue: any) {
this.props.onLayerChanged( this.props.onLayerChanged(
@@ -311,7 +299,8 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
items[id].handler(); items[id].handler();
} }
return <section className="maputnik-layer-editor" return <IconContext.Provider value={{size: '14px', color: '#8e8e8e'}}>
<section className="maputnik-layer-editor"
role="main" role="main"
aria-label={t("Layer editor")} aria-label={t("Layer editor")}
> >
@@ -358,6 +347,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
{groups} {groups}
</Accordion> </Accordion>
</section> </section>
</IconContext.Provider>
} }
} }
+4 -12
View File
@@ -1,8 +1,8 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames' import classnames from 'classnames'
import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md' import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md'
import { IconContext } from 'react-icons'
import IconLayer from './IconLayer' import IconLayer from './IconLayer'
import {SortableElement, SortableHandle} from 'react-sortable-hoc' import {SortableElement, SortableHandle} from 'react-sortable-hoc'
@@ -91,20 +91,11 @@ class LayerListItem extends React.Component<LayerListItemProps> {
onLayerVisibilityToggle: () => {}, onLayerVisibilityToggle: () => {},
} }
static childContextTypes = {
reactIconBase: PropTypes.object
}
getChildContext() {
return {
reactIconBase: { size: 14 }
}
}
render() { render() {
const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide'; const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide';
return <li return <IconContext.Provider value={{size: '14px'}}>
<li
id={this.props.id} id={this.props.id}
key={this.props.layerId} key={this.props.layerId}
onClick={_e => this.props.onLayerSelect(this.props.layerIndex)} onClick={_e => this.props.onLayerSelect(this.props.layerIndex)}
@@ -136,6 +127,7 @@ class LayerListItem extends React.Component<LayerListItemProps> {
onClick={_e => this.props.onLayerVisibilityToggle!(this.props.layerIndex)} onClick={_e => this.props.onLayerVisibilityToggle!(this.props.layerIndex)}
/> />
</li> </li>
</IconContext.Provider>
} }
} }
+26 -2
View File
@@ -23,10 +23,16 @@ type ModalAddState = {
id: string id: string
source?: string source?: string
'source-layer'?: string 'source-layer'?: string
error?: string | null
}; };
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> { class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
addLayer = () => { addLayer = () => {
if (this.props.layers.some(l => l.id === this.state.id)) {
this.setState({ error: this.props.t('Layer ID already exists') })
return
}
const changedLayers = this.props.layers.slice(0) const changedLayers = this.props.layers.slice(0)
const layer: ModalAddState = { const layer: ModalAddState = {
id: this.state.id, id: this.state.id,
@@ -41,9 +47,10 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
} }
changedLayers.push(layer as LayerSpecification) changedLayers.push(layer as LayerSpecification)
this.setState({ error: null }, () => {
this.props.onLayersChange(changedLayers) this.props.onLayersChange(changedLayers)
this.props.onOpenToggle(false) this.props.onOpenToggle(false)
})
} }
constructor(props: ModalAddInternalProps) { constructor(props: ModalAddInternalProps) {
@@ -51,6 +58,7 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
const state: ModalAddState = { const state: ModalAddState = {
type: 'fill', type: 'fill',
id: '', id: '',
error: null,
} }
if(props.sources.length > 0) { if(props.sources.length > 0) {
@@ -129,6 +137,21 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
const t = this.props.t; const t = this.props.t;
const sources = this.getSources(this.state.type); const sources = this.getSources(this.state.type);
const layers = this.getLayersForSource(this.state.source!); const layers = this.getLayersForSource(this.state.source!);
let errorElement;
if (this.state.error) {
errorElement = (
<div className="maputnik-modal-error">
{this.state.error}
<a
href="#"
onClick={() => this.setState({ error: null })}
className="maputnik-modal-error-close"
>
×
</a>
</div>
);
}
return <Modal return <Modal
isOpen={this.props.isOpen} isOpen={this.props.isOpen}
@@ -137,12 +160,13 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
data-wd-key="modal:add-layer" data-wd-key="modal:add-layer"
className="maputnik-add-modal" className="maputnik-add-modal"
> >
{errorElement}
<div className="maputnik-add-layer"> <div className="maputnik-add-layer">
<FieldId <FieldId
value={this.state.id} value={this.state.id}
wdKey="add-layer.layer-id" wdKey="add-layer.layer-id"
onChange={(v: string) => { onChange={(v: string) => {
this.setState({ id: v }) this.setState({ id: v, error: null })
}} }}
/> />
<FieldType <FieldType
+10 -13
View File
@@ -1,4 +1,3 @@
import React from 'react'
import Block from './Block' import Block from './Block'
import InputSpec, { SpecFieldProps as InputFieldSpecProps } from './InputSpec' import InputSpec, { SpecFieldProps as InputFieldSpecProps } from './InputSpec'
import Fieldset from './Fieldset' import Fieldset from './Fieldset'
@@ -20,27 +19,25 @@ export type SpecFieldProps = InputFieldSpecProps & {
name?: string name?: string
}; };
export default class SpecField extends React.Component<SpecFieldProps> { const SpecField: React.FC<SpecFieldProps> = (props) => {
render() { const fieldType = props.fieldSpec?.type;
const fieldType = this.props.fieldSpec?.type;
const typeBlockFn = typeMap[fieldType!]; const typeBlockFn = typeMap[fieldType!];
let TypeBlock; let TypeBlock;
if (typeBlockFn) { if (typeBlockFn) {
TypeBlock = typeBlockFn(this.props); TypeBlock = typeBlockFn(props);
} }
else { else {
console.warn("No such type for '%s'", fieldType); console.warn("No such type for '%s'", fieldType);
TypeBlock = Block; TypeBlock = Block;
} }
return <TypeBlock return (
label={this.props.label} <TypeBlock label={props.label} action={props.action} fieldSpec={props.fieldSpec}>
action={this.props.action} <InputSpec {...props} />
fieldSpec={this.props.fieldSpec}
>
<InputSpec {...this.props} />
</TypeBlock> </TypeBlock>
} );
} };
export default SpecField;
+12 -24
View File
@@ -11,6 +11,18 @@
"url": "https://americanamap.org/style.json", "url": "https://americanamap.org/style.json",
"thumbnail": "https://github.com/maplibre/maputnik/assets/649392/23fa75ad-63e6-43f5-8837-03cdb0428bac" "thumbnail": "https://github.com/maplibre/maputnik/assets/649392/23fa75ad-63e6-43f5-8837-03cdb0428bac"
}, },
{
"id": "aws-hybrid",
"title": "AWS Hybrid",
"url": "https://maps.geo.eu-west-1.amazonaws.com/v2/styles/Hybrid/descriptor?key=v1.public.eyJqdGkiOiJiOTNkYjBlZi04OWUzLTQxMGUtODFhMC0zYjZjZjVmZWZmMDgifYtukap0NBaJpcrS6Vit9j03GJgK9Bn-RSu5UCe3jkdSql2kKp3IEgLPtyLssbmKUdVO11sXddjK3ZOZy8V6QG0olv0K_1tOxyMIe4DAO3IV6H4VzHWiaXlbSakGiEgFLuHBdcfLDeMotye7N6rSRxuZb0CN9ytH9VjLly6-NEBRZezO_qPQyvdTFdeZsARIpL0f9YVpxPxPVvUcAWYCk5LpaPseRCDPrY5SlCdA1ZKqUA4F9RzxSTxB73Fel_SoNDkCNaux1VposBu791-uUpDzUpr7leKckrPXrpZ2hwnFbafVxFV9vq4fLTpB5KoBksuLfGNIwAx1RLLxWuMhE4c.ZGQzZDY2OGQtMWQxMy00ZTEwLWIyZGUtOGVjYzUzMjU3OGE4&color-scheme=Light",
"thumbnail": "https://maputnik.s3.eu-west-1.amazonaws.com/thumbnails/aws-hybrid.jpg"
},
{
"id": "aws-standard",
"title": "AWS Standard",
"url": "https://maps.geo.eu-west-1.amazonaws.com/v2/styles/Standard/descriptor?key=v1.public.eyJqdGkiOiJiOTNkYjBlZi04OWUzLTQxMGUtODFhMC0zYjZjZjVmZWZmMDgifYtukap0NBaJpcrS6Vit9j03GJgK9Bn-RSu5UCe3jkdSql2kKp3IEgLPtyLssbmKUdVO11sXddjK3ZOZy8V6QG0olv0K_1tOxyMIe4DAO3IV6H4VzHWiaXlbSakGiEgFLuHBdcfLDeMotye7N6rSRxuZb0CN9ytH9VjLly6-NEBRZezO_qPQyvdTFdeZsARIpL0f9YVpxPxPVvUcAWYCk5LpaPseRCDPrY5SlCdA1ZKqUA4F9RzxSTxB73Fel_SoNDkCNaux1VposBu791-uUpDzUpr7leKckrPXrpZ2hwnFbafVxFV9vq4fLTpB5KoBksuLfGNIwAx1RLLxWuMhE4c.ZGQzZDY2OGQtMWQxMy00ZTEwLWIyZGUtOGVjYzUzMjU3OGE4&color-scheme=Light",
"thumbnail": "https://maputnik.s3.eu-west-1.amazonaws.com/thumbnails/aws-standard.jpg"
},
{ {
"id": "dark-matter", "id": "dark-matter",
"title": "Dark Matter", "title": "Dark Matter",
@@ -29,30 +41,6 @@
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@v1.0/style.json", "url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@v1.0/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/toner.png" "thumbnail": "https://maputnik.github.io/thumbnails/toner.png"
}, },
{
"id": "os-zoomstack-light",
"title": "Zoomstack Light",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-light/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-light.png"
},
{
"id": "os-zoomstack-night",
"title": "Zoomstack Night",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-night/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-night.png"
},
{
"id": "os-zoomstack-outdoor",
"title": "Zoomstack Outdoor",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-outdoor/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-outdoor.png"
},
{
"id": "os-zoomstack-road",
"title": "Zoomstack Road",
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-road/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-road.png"
},
{ {
"id": "osm-bright", "id": "osm-bright",
"title": "OSM Bright", "title": "OSM Bright",
+20
View File
@@ -91,8 +91,28 @@ export class StyleStore {
save(mapStyle: StyleSpecification & { id: string }) { save(mapStyle: StyleSpecification & { id: string }) {
mapStyle = style.ensureStyleValidity(mapStyle) mapStyle = style.ensureStyleValidity(mapStyle)
const key = styleKey(mapStyle.id) const key = styleKey(mapStyle.id)
const saveFn = () => {
window.localStorage.setItem(key, JSON.stringify(mapStyle)) window.localStorage.setItem(key, JSON.stringify(mapStyle))
window.localStorage.setItem(storageKeys.latest, mapStyle.id) window.localStorage.setItem(storageKeys.latest, mapStyle.id)
}
try {
saveFn()
} catch (e) {
// Handle quota exceeded error
if (e instanceof DOMException && (
e.code === 22 || // Firefox
e.code === 1014 || // Firefox
e.name === 'QuotaExceededError' ||
e.name === 'NS_ERROR_DOM_QUOTA_REACHED'
)) {
this.purge()
saveFn() // Retry after clearing
} else {
throw e
}
}
return mapStyle return mapStyle
} }
} }
+1
View File
@@ -72,6 +72,7 @@
"Collapse": "Einklappen", "Collapse": "Einklappen",
"Expand": "Ausklappen", "Expand": "Ausklappen",
"Add Layer": "Ebene hinzufügen", "Add Layer": "Ebene hinzufügen",
"Layer ID already exists": "Layer-ID existiert bereits",
"Search": "Suche", "Search": "Suche",
"Zoom:": "Zoom:", "Zoom:": "Zoom:",
"Close popup": "Popup schließen", "Close popup": "Popup schließen",
+1
View File
@@ -72,6 +72,7 @@
"Collapse": "Réduire", "Collapse": "Réduire",
"Expand": "Développer", "Expand": "Développer",
"Add Layer": "Ajouter un calque", "Add Layer": "Ajouter un calque",
"Layer ID already exists": "L'identifiant du calque existe déjà",
"Search": "Recherche", "Search": "Recherche",
"Zoom:": "Zoom :", "Zoom:": "Zoom :",
"Close popup": "Fermer la fenêtre", "Close popup": "Fermer la fenêtre",
+1
View File
@@ -72,6 +72,7 @@
"Collapse": "הקטנה", "Collapse": "הקטנה",
"Expand": "הגדלה", "Expand": "הגדלה",
"Add Layer": "הוספת שכבה", "Add Layer": "הוספת שכבה",
"Layer ID already exists": "מזהה השכבה כבר קיים",
"Search": "חיפוש", "Search": "חיפוש",
"Zoom:": "זום:", "Zoom:": "זום:",
"Close popup": "סגירת החלון", "Close popup": "סגירת החלון",
+1
View File
@@ -72,6 +72,7 @@
"Collapse": "Coprimi", "Collapse": "Coprimi",
"Expand": "Espandi", "Expand": "Espandi",
"Add Layer": "Aggiungi Livello", "Add Layer": "Aggiungi Livello",
"Layer ID already exists": "L'ID del layer esiste già",
"Search": "Cerca", "Search": "Cerca",
"Zoom:": "Zoom:", "Zoom:": "Zoom:",
"Close popup": "Chiudi popup", "Close popup": "Chiudi popup",
+1
View File
@@ -72,6 +72,7 @@
"Collapse": "畳む", "Collapse": "畳む",
"Expand": "展開", "Expand": "展開",
"Add Layer": "レイヤー追加", "Add Layer": "レイヤー追加",
"Layer ID already exists": "レイヤーIDは既に存在します",
"Search": "検索", "Search": "検索",
"Zoom:": "ズーム:", "Zoom:": "ズーム:",
"Close popup": "ポップアップを閉じる", "Close popup": "ポップアップを閉じる",
+6 -5
View File
@@ -72,6 +72,7 @@
"Collapse": "折叠", "Collapse": "折叠",
"Expand": "展开", "Expand": "展开",
"Add Layer": "添加图层", "Add Layer": "添加图层",
"Layer ID already exists": "图层ID已存在",
"Search": "搜索", "Search": "搜索",
"Zoom:": "缩放:", "Zoom:": "缩放:",
"Close popup": "关闭弹出窗口", "Close popup": "关闭弹出窗口",
@@ -149,7 +150,7 @@
"Raster (Tile URLs)": "栅格数据 (Tile URLs)", "Raster (Tile URLs)": "栅格数据 (Tile URLs)",
"Raster DEM (TileJSON URL)": "栅格高程数据 (TileJSON URL)", "Raster DEM (TileJSON URL)": "栅格高程数据 (TileJSON URL)",
"Raster DEM (XYZ URLs)": "栅格高程数据 (XYZ URLs)", "Raster DEM (XYZ URLs)": "栅格高程数据 (XYZ URLs)",
"Vector (PMTiles)": "__STRING_NOT_TRANSLATED__", "Vector (PMTiles)": "矢量数据 (PMTiles)",
"Image": "图像", "Image": "图像",
"Video": "视频", "Video": "视频",
"Add Source": "添加源", "Add Source": "添加源",
@@ -161,7 +162,7 @@
"Add a new source to your style. You can only choose the source type and id at creation time!": "向您的样式添加新源。在创建时,您只能选择源类型和ID!", "Add a new source to your style. You can only choose the source type and id at creation time!": "向您的样式添加新源。在创建时,您只能选择源类型和ID!",
"TileJSON URL": "TileJSON URL", "TileJSON URL": "TileJSON URL",
"Tile URL": "瓦片URL", "Tile URL": "瓦片URL",
"Scheme Type": "__STRING_NOT_TRANSLATED__", "Scheme Type": "瓦片方案",
"Coord top left": "左上角坐标", "Coord top left": "左上角坐标",
"Coord top right": "右上角坐标", "Coord top right": "右上角坐标",
"Coord bottom right": "右下角坐标", "Coord bottom right": "右下角坐标",
@@ -171,13 +172,13 @@
"GeoJSON URL": "GeoJSON URL", "GeoJSON URL": "GeoJSON URL",
"GeoJSON": "GeoJSON", "GeoJSON": "GeoJSON",
"Cluster": "聚合", "Cluster": "聚合",
"PMTiles URL": "__STRING_NOT_TRANSLATED__", "PMTiles URL": "PMTiles URL",
"Tile Size": "__STRING_NOT_TRANSLATED__", "Tile Size": "瓦片大小",
"Encoding": "编码", "Encoding": "编码",
"Error:": "错误:", "Error:": "错误:",
"MapTiler Access Token": "MapTiler 访问令牌", "MapTiler Access Token": "MapTiler 访问令牌",
"Public access token for MapTiler Cloud.": "MapTiler Cloud 的公共访问令牌。", "Public access token for MapTiler Cloud.": "MapTiler Cloud 的公共访问令牌。",
"Learn More": "__STRING_NOT_TRANSLATED__", "Learn More": "了解更多",
"Thunderforest Access Token": "Thunderforest 访问令牌", "Thunderforest Access Token": "Thunderforest 访问令牌",
"Public access token for Thunderforest services.": "Thunderforest 服务的公共访问令牌。", "Public access token for Thunderforest services.": "Thunderforest 服务的公共访问令牌。",
"Stadia Maps API Key": "Stadia Maps API 密钥", "Stadia Maps API Key": "Stadia Maps API 密钥",