mirror of
https://github.com/maputnik/editor.git
synced 2026-08-01 10:47:26 +00:00
Compare commits
2 Commits
v3.0.0
..
ebece40308
| Author | SHA1 | Date | |
|---|---|---|---|
| ebece40308 | |||
| 3e7e1e6ce9 |
@@ -8,3 +8,4 @@ assignees: ''
|
|||||||
---
|
---
|
||||||
|
|
||||||
<!-- Thanks for reaching out! If you are having general Maputnik mapping questions, please asking them at https://gis.stackexchange.com/ using the 'maputnik' tag https://gis.stackexchange.com/questions/tagged/maputnik and read https://gis.stackexchange.com/help/how-to-ask before you do so (please keep in mind that you're asking there in a general GIS forum, not a dedicated support channel) -->
|
<!-- Thanks for reaching out! If you are having general Maputnik mapping questions, please asking them at https://gis.stackexchange.com/ using the 'maputnik' tag https://gis.stackexchange.com/questions/tagged/maputnik and read https://gis.stackexchange.com/help/how-to-ask before you do so (please keep in mind that you're asking there in a general GIS forum, not a dedicated support channel) -->
|
||||||
|
|
||||||
|
|||||||
@@ -8,3 +8,4 @@
|
|||||||
- [ ] Include before/after visuals or gifs if this PR includes visual changes.
|
- [ ] Include before/after visuals or gifs if this PR includes visual changes.
|
||||||
- [ ] Write tests for all new functionality.
|
- [ ] Write tests for all new functionality.
|
||||||
- [ ] Add an entry to `CHANGELOG.md` under the `## main` section.
|
- [ ] Add an entry to `CHANGELOG.md` under the `## main` section.
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Dependabot metadata
|
- name: Dependabot metadata
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: dependabot/fetch-metadata@v2.4.0
|
uses: dependabot/fetch-metadata@v2.3.0
|
||||||
with:
|
with:
|
||||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
- name: Approve Dependabot PRs
|
- name: Approve Dependabot PRs
|
||||||
|
|||||||
+62
-68
@@ -7,7 +7,17 @@ on:
|
|||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
build-docker:
|
||||||
|
name: build docker
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: docker build -t test-docker-image-build .
|
||||||
|
|
||||||
|
# build the editor
|
||||||
build-node:
|
build-node:
|
||||||
name: "build on ${{ matrix.os }}"
|
name: "build on ${{ matrix.os }}"
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
@@ -17,17 +27,17 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v5
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
- run: npm run lint-css
|
- run: npm run lint-css
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -37,81 +47,65 @@ jobs:
|
|||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v5
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- name: artifacts/maputnik
|
- name: artifacts/maputnik
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maputnik
|
name: maputnik
|
||||||
path: dist
|
path: dist
|
||||||
|
|
||||||
# Build and upload desktop CLI artifacts
|
# Build and upload desktop CLI artifacts
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.23.x
|
go-version: ^1.23.x
|
||||||
cache-dependency-path: desktop/go.sum
|
cache-dependency-path: desktop/go.sum
|
||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: Build desktop artifacts
|
- name: Build desktop artifacts
|
||||||
run: npm run build-desktop
|
run: npm run build-desktop
|
||||||
|
|
||||||
- name: Artifacts/linux
|
- name: Artifacts/linux
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maputnik-linux
|
name: maputnik-linux
|
||||||
path: ./desktop/bin/linux/
|
path: ./desktop/bin/linux/
|
||||||
|
|
||||||
- name: Artifacts/darwin
|
- name: Artifacts/darwin
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maputnik-darwin
|
name: maputnik-darwin
|
||||||
path: ./desktop/bin/darwin/
|
path: ./desktop/bin/darwin/
|
||||||
|
|
||||||
- name: Artifacts/windows
|
- name: Artifacts/windows
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maputnik-windows
|
name: maputnik-windows
|
||||||
path: ./desktop/bin/windows/
|
path: ./desktop/bin/windows/
|
||||||
|
|
||||||
e2e-tests:
|
e2e-tests:
|
||||||
name: "E2E tests using chrome"
|
name: "E2E tests using ${{ matrix.browser }}"
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
browser: [chrome]
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Cypress run
|
- name: Cypress run
|
||||||
uses: cypress-io/github-action@v6
|
uses: cypress-io/github-action@v6
|
||||||
with:
|
with:
|
||||||
build: npm run build
|
build: npm run build
|
||||||
start: npm run start
|
start: npm run start
|
||||||
browser: chrome
|
browser: ${{ matrix.browser }}
|
||||||
- name: Upload coverage reports to Codecov
|
|
||||||
uses: codecov/codecov-action@v5
|
|
||||||
with:
|
|
||||||
files: ${{ github.workspace }}/.nyc_output/out.json
|
|
||||||
verbose: true
|
|
||||||
|
|
||||||
e2e-tests-docker:
|
|
||||||
name: "E2E tests using chrome and docker"
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
- run: npm ci
|
|
||||||
- name: Cypress run
|
|
||||||
uses: cypress-io/github-action@v6
|
|
||||||
with:
|
|
||||||
build: docker build -t maputnik .
|
|
||||||
start: docker run --rm --network host maputnik --port=8888
|
|
||||||
browser: chrome
|
|
||||||
- name: Upload coverage reports to Codecov
|
- name: Upload coverage reports to Codecov
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
- name: Use Node.js from nvmrc
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'push' }}
|
if: ${{ github.event_name == 'push' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
- name: Use Node.js from nvmrc
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
@@ -40,12 +40,12 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
||||||
- run: docker push ghcr.io/maplibre/maputnik:main
|
- run: docker push ghcr.io/maplibre/maputnik:main
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: Release
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [main]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -13,13 +13,13 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
- name: Use Node.js from nvmrc
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
|
|
||||||
@@ -39,19 +39,19 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
- name: Use Node.js from nvmrc
|
||||||
uses: actions/setup-node@v5
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
- name: Set up Go for desktop build
|
- name: Set up Go for desktop build
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.23.x
|
go-version: ^1.23.x
|
||||||
cache-dependency-path: desktop/go.sum
|
cache-dependency-path: desktop/go.sum
|
||||||
@@ -99,3 +99,4 @@ jobs:
|
|||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# See https://pre-commit.com for more information
|
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
|
||||||
|
|
||||||
ci:
|
|
||||||
autoupdate_schedule: monthly
|
|
||||||
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v6.0.0
|
|
||||||
hooks:
|
|
||||||
- id: check-added-large-files
|
|
||||||
- id: check-executables-have-shebangs
|
|
||||||
- id: check-json
|
|
||||||
exclude: 'tsconfig(\.node)?\.json'
|
|
||||||
- id: check-shebang-scripts-are-executable
|
|
||||||
- id: check-symlinks
|
|
||||||
- id: check-toml
|
|
||||||
- id: check-yaml
|
|
||||||
args: [ --allow-multiple-documents ]
|
|
||||||
- id: destroyed-symlinks
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- id: mixed-line-ending
|
|
||||||
args: [ --fix=lf ]
|
|
||||||
- id: trailing-whitespace
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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.
|
|
||||||
+3
-22
@@ -1,16 +1,6 @@
|
|||||||
## main
|
## main
|
||||||
|
|
||||||
### ✨ Features and improvements
|
### ✨ Features and improvements
|
||||||
- _...Add new stuff here..._
|
|
||||||
|
|
||||||
### 🐞 Bug fixes
|
|
||||||
|
|
||||||
- _...Add new stuff here..._
|
|
||||||
|
|
||||||
## 3.0.0
|
|
||||||
|
|
||||||
### ✨ Features and improvements
|
|
||||||
- Fix radio/delete filter buttons styling regression
|
|
||||||
- Add german translation
|
- Add german translation
|
||||||
- Use same version number for web and desktop versions
|
- Use same version number for web and desktop versions
|
||||||
- Add scheme type options for vector/raster tile
|
- Add scheme type options for vector/raster tile
|
||||||
@@ -20,22 +10,12 @@
|
|||||||
- 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
|
- _...Add new stuff here..._
|
||||||
- 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 LocationIQ as supported map provider with access token field and gallery style
|
|
||||||
- Use maputnik go binary for the docker image to allow file watching
|
|
||||||
- Revmove support for `debug` and `localport` url parameters
|
|
||||||
- Replace react-sortable-hoc with dnd-kit to avoid react console warnings and also use a maintained library
|
|
||||||
|
|
||||||
### 🐞 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..._
|
||||||
- Replace deprecated `ReactDOM.render` usage with `createRoot` and drop the
|
|
||||||
`DOMNodeRemoved` cleanup hack
|
|
||||||
|
|
||||||
## 2.1.1
|
## 2.1.1
|
||||||
|
|
||||||
@@ -52,3 +32,4 @@
|
|||||||
## 1.7.0
|
## 1.7.0
|
||||||
|
|
||||||
- See release notes at https://maputnik.github.io/blog/2020/04/23/release-v1.7.0
|
- See release notes at https://maputnik.github.io/blog/2020/04/23/release-v1.7.0
|
||||||
|
|
||||||
|
|||||||
+10
-8
@@ -1,14 +1,16 @@
|
|||||||
FROM golang:1.23-alpine AS builder
|
FROM node:18 as builder
|
||||||
WORKDIR /maputnik
|
WORKDIR /maputnik
|
||||||
|
|
||||||
RUN apk add --no-cache nodejs npm make git gcc g++ libc-dev
|
# Only copy package.json to prevent npm install from running on every build
|
||||||
|
COPY package.json package-lock.json .npmrc ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
# Build maputnik
|
# Build maputnik
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm ci
|
RUN npx vite build
|
||||||
RUN CGO_ENABLED=1 GOOS=linux npm run build-linux
|
|
||||||
|
|
||||||
FROM alpine:latest
|
#---------------------------------------------------------------------------
|
||||||
WORKDIR /app
|
# Create a clean nginx-alpine slim image with just the build results
|
||||||
COPY --from=builder /maputnik/desktop/bin/linux ./
|
FROM nginx:alpine-slim
|
||||||
ENTRYPOINT ["/app/maputnik"]
|
|
||||||
|
COPY --from=builder /maputnik/dist /usr/share/nginx/html/
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
|
|||||||
@@ -18,15 +18,9 @@ targeted at developers and map designers.
|
|||||||
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
|
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main
|
docker run -it --rm -p 8888:80 ghcr.io/maplibre/maputnik:main
|
||||||
```
|
```
|
||||||
|
|
||||||
To see the CLI options (for example file watching or style serving) run:
|
|
||||||
```bash
|
|
||||||
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main --help
|
|
||||||
```
|
|
||||||
You might need to mount a volume (`-v`) to be able to use these options.
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The documentation can be found in the [Wiki](https://github.com/maplibre/maputnik/wiki). You are welcome to collaborate!
|
The documentation can be found in the [Wiki](https://github.com/maplibre/maputnik/wiki). You are welcome to collaborate!
|
||||||
@@ -81,7 +75,7 @@ npm run sort-styles
|
|||||||
## Tests
|
## Tests
|
||||||
For E2E testing we use [Cypress](https://www.cypress.io/)
|
For E2E testing we use [Cypress](https://www.cypress.io/)
|
||||||
|
|
||||||
[Cypress](https://www.cypress.io/) doesn't start a server so you'll need to start one manually by running `npm run start`.
|
[Cypress](https://www.cypress.io/) doesn't starts a server so you'll need to start one manually by running `npm run start`.
|
||||||
|
|
||||||
Now open a terminal and run the following using *chrome*:
|
Now open a terminal and run the following using *chrome*:
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ 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
|
||||||
@@ -21,11 +20,4 @@ export default defineConfig({
|
|||||||
openMode: 0,
|
openMode: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
component: {
|
|
||||||
devServer: {
|
|
||||||
framework: "react",
|
|
||||||
bundler: "vite",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ describe("history", () => {
|
|||||||
it("undo/redo", () => {
|
it("undo/redo", () => {
|
||||||
when.setStyle("geojson");
|
when.setStyle("geojson");
|
||||||
when.modal.open();
|
when.modal.open();
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
||||||
|
|
||||||
when.modal.fillLayers({
|
when.modal.fillLayers({
|
||||||
id: "step 1",
|
id: "step 1",
|
||||||
|
|||||||
@@ -378,57 +378,8 @@ describe("layers", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("groups", () => {
|
it("groups", () => {
|
||||||
when.modal.open();
|
// TODO
|
||||||
const id1 = when.modal.fillLayers({
|
// Click each of the layer groups.
|
||||||
id: "aa",
|
|
||||||
type: "line",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
const id2 = when.modal.fillLayers({
|
|
||||||
id: "aa-2",
|
|
||||||
type: "line",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
const id3 = when.modal.fillLayers({
|
|
||||||
id: "b",
|
|
||||||
type: "line",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id2)).shouldNotBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
|
|
||||||
when.click("layer-list-group:aa-0");
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id2)).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
|
|
||||||
when.click("layer-list-item:" + id2);
|
|
||||||
when.click("skip-target-layer-editor");
|
|
||||||
when.click("menu-move-layer-down");
|
|
||||||
then(get.elementByTestId("layer-list-group:aa-0")).shouldNotExist();
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "aa",
|
|
||||||
type: "line",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "b",
|
|
||||||
type: "line",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "aa-2",
|
|
||||||
type: "line",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -544,7 +495,9 @@ describe("layers", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe("layereditor jsonlint should error", ()=>{
|
describe("layereditor jsonlint should error", ()=>{
|
||||||
|
|
||||||
it("add", () => {
|
it("add", () => {
|
||||||
const id = when.modal.fillLayers({
|
const id = when.modal.fillLayers({
|
||||||
type: "circle",
|
type: "circle",
|
||||||
@@ -570,42 +523,4 @@ describe("layers", () => {
|
|||||||
error.should('exist');
|
error.should('exist');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("drag and drop", () => {
|
|
||||||
it("move layer should update local storage", () => {
|
|
||||||
when.modal.open();
|
|
||||||
const firstId = when.modal.fillLayers({
|
|
||||||
id: "a",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
when.modal.open();
|
|
||||||
const secondId = when.modal.fillLayers({
|
|
||||||
id: "b",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
when.modal.open();
|
|
||||||
const thirdId = when.modal.fillLayers({
|
|
||||||
id: "c",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
when.dragAndDrop(get.elementByTestId("layer-list-item:" + firstId), get.elementByTestId("layer-list-item:" + thirdId));
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: secondId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: thirdId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: firstId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,10 +8,8 @@ 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 styleItemKey = `maputnik:style:${styleId}`;
|
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
|
||||||
const styleItem = win.localStorage.getItem(styleItemKey);
|
const obj = JSON.parse(styleItem || "");
|
||||||
if (!styleItem) throw new Error("Could not get styleItem from localStorage");
|
|
||||||
const obj = JSON.parse(styleItem);
|
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,8 +92,8 @@ export class MaputnikDriver {
|
|||||||
public when = {
|
public when = {
|
||||||
...this.helper.when,
|
...this.helper.when,
|
||||||
modal: this.modalDriver.when,
|
modal: this.modalDriver.when,
|
||||||
doWithin: (selector: string, fn: () => void) => {
|
within: (selector: string, fn: () => void) => {
|
||||||
this.helper.when.doWithin(fn, selector);
|
this.helper.when.within(fn, selector);
|
||||||
},
|
},
|
||||||
tab: () => this.helper.get.element("body").tab(),
|
tab: () => this.helper.get.element("body").tab(),
|
||||||
waitForExampleFileResponse: () => {
|
waitForExampleFileResponse: () => {
|
||||||
@@ -110,25 +108,25 @@ export class MaputnikDriver {
|
|||||||
styleProperties: "geojson" | "raster" | "both" | "layer" | "",
|
styleProperties: "geojson" | "raster" | "both" | "layer" | "",
|
||||||
zoom?: number
|
zoom?: number
|
||||||
) => {
|
) => {
|
||||||
const url = new URL(baseUrl);
|
let url = "?debug";
|
||||||
switch (styleProperties) {
|
switch (styleProperties) {
|
||||||
case "geojson":
|
case "geojson":
|
||||||
url.searchParams.set("style", baseUrl + "geojson-style.json");
|
url += `&style=${baseUrl}geojson-style.json`;
|
||||||
break;
|
break;
|
||||||
case "raster":
|
case "raster":
|
||||||
url.searchParams.set("style", baseUrl + "raster-style.json");
|
url += `&style=${baseUrl}raster-style.json`;
|
||||||
break;
|
break;
|
||||||
case "both":
|
case "both":
|
||||||
url.searchParams.set("style", baseUrl + "geojson-raster-style.json");
|
url += `&style=${baseUrl}geojson-raster-style.json`;
|
||||||
break;
|
break;
|
||||||
case "layer":
|
case "layer":
|
||||||
url.searchParams.set("style", baseUrl + "example-layer-style.json");
|
url += `&style=${baseUrl}/example-layer-style.json`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (zoom) {
|
if (zoom) {
|
||||||
url.hash = `${zoom}/41.3805/2.1635`;
|
url += `#${zoom}/41.3805/2.1635`;
|
||||||
}
|
}
|
||||||
this.helper.when.visit(url.toString());
|
this.helper.when.visit(baseUrl + url);
|
||||||
if (styleProperties) {
|
if (styleProperties) {
|
||||||
this.helper.when.acceptConfirm();
|
this.helper.when.acceptConfirm();
|
||||||
}
|
}
|
||||||
@@ -145,7 +143,7 @@ export class MaputnikDriver {
|
|||||||
},
|
},
|
||||||
|
|
||||||
selectWithin: (selector: string, value: string) => {
|
selectWithin: (selector: string, value: string) => {
|
||||||
this.when.doWithin(selector, () => {
|
this.when.within(selector, () => {
|
||||||
this.helper.get.element("select").select(value);
|
this.helper.get.element("select").select(value);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ export default class ModalDriver {
|
|||||||
this.helper.when.type("add-layer.layer-id.input", id);
|
this.helper.when.type("add-layer.layer-id.input", id);
|
||||||
|
|
||||||
if (layer) {
|
if (layer) {
|
||||||
this.helper.when.doWithin(() => {
|
this.helper.when.within(() => {
|
||||||
this.helper.get.element("input").clear().type(layer!);
|
this.helper.get.element("input").type(layer!);
|
||||||
}, "add-layer.layer-source-block");
|
}, "add-layer.layer-source-block");
|
||||||
}
|
}
|
||||||
this.helper.when.click("add-layer");
|
this.helper.when.click("add-layer");
|
||||||
|
|||||||
@@ -224,18 +224,6 @@ describe("modals", () => {
|
|||||||
).shouldInclude({ "maputnik:stadia_access_token": apiKey });
|
).shouldInclude({ "maputnik:stadia_access_token": apiKey });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("locationiq access token", () => {
|
|
||||||
const apiKey = "testing123";
|
|
||||||
when.setValue(
|
|
||||||
"modal:settings.maputnik:locationiq_access_token",
|
|
||||||
apiKey
|
|
||||||
);
|
|
||||||
when.click("modal:settings.name");
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
|
||||||
).shouldInclude({ "maputnik:locationiq_access_token": apiKey });
|
|
||||||
});
|
|
||||||
|
|
||||||
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");
|
||||||
@@ -257,7 +245,7 @@ describe("modals", () => {
|
|||||||
when.click("nav:open");
|
when.click("nav:open");
|
||||||
|
|
||||||
get.elementByAttribute('aria-label', "MapTiler Basic").should('exist').click();
|
get.elementByAttribute('aria-label', "MapTiler Basic").should('exist').click();
|
||||||
when.wait(1000);
|
|
||||||
when.click("nav:settings");
|
when.click("nav:settings");
|
||||||
|
|
||||||
when.select("modal:settings.maputnik:renderer", "mlgljs");
|
when.select("modal:settings.maputnik:renderer", "mlgljs");
|
||||||
@@ -284,63 +272,7 @@ 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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,3 +15,4 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,114 +1,12 @@
|
|||||||
{
|
{
|
||||||
"id": "test-style",
|
"id": "test-style",
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"name": "Test Style",
|
"name": "Test Style",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"maputnik:renderer": "mlgljs",
|
"maputnik:renderer": "mlgljs"
|
||||||
"data": [
|
},
|
||||||
0,
|
"sources": {},
|
||||||
1,
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
2,
|
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
3,
|
"layers": []
|
||||||
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": {},
|
|
||||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
|
||||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
|
||||||
"layers": []
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
// ***********************************************************
|
|
||||||
// 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 />)
|
|
||||||
+2
-5
@@ -2,7 +2,8 @@ SOURCEDIR=.
|
|||||||
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
|
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
|
||||||
BINARY=maputnik
|
BINARY=maputnik
|
||||||
VERSION := $(shell node -p "require('../package.json').version")
|
VERSION := $(shell node -p "require('../package.json').version")
|
||||||
GOBIN := $(or $(shell if [ -d /go/bin ]; then echo "/go/bin"; fi),$(HOME)/go/bin)
|
GOPATH := $(if $(GOPATH),$(GOPATH),$(HOME)/go)
|
||||||
|
GOBIN := $(if $(GOBIN),$(GOBIN),$(HOME)/go/bin)
|
||||||
|
|
||||||
all: $(BINARY)
|
all: $(BINARY)
|
||||||
|
|
||||||
@@ -10,10 +11,6 @@ $(BINARY): $(GOBIN)/gox $(GOBIN)/go-winres $(SOURCES) version.go rice-box.go win
|
|||||||
$(GOBIN)/go-winres make --product-version=$(VERSION)
|
$(GOBIN)/go-winres make --product-version=$(VERSION)
|
||||||
$(GOBIN)/gox -osarch "windows/amd64 linux/amd64 darwin/amd64" -output "bin/{{.OS}}/${BINARY}"
|
$(GOBIN)/gox -osarch "windows/amd64 linux/amd64 darwin/amd64" -output "bin/{{.OS}}/${BINARY}"
|
||||||
|
|
||||||
bin/linux/$(BINARY): $(GOBIN)/gox $(GOBIN)/go-winres $(SOURCES) version.go rice-box.go winres/winres.json
|
|
||||||
$(GOBIN)/go-winres make --product-version=$(VERSION)
|
|
||||||
$(GOBIN)/gox -osarch "linux/amd64" -output "bin/{{.OS}}/${BINARY}"
|
|
||||||
|
|
||||||
winres/winres.json: winres/winres_template.json
|
winres/winres.json: winres/winres_template.json
|
||||||
sed 's/{{.Version}}/$(VERSION)/g' winres/winres_template.json > $@
|
sed 's/{{.Version}}/$(VERSION)/g' winres/winres_template.json > $@
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"github.com/GeertJohan/go.rice"
|
|
||||||
|
"github.com/GeertJohan/go.rice"
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/maputnik/desktop/filewatch"
|
"github.com/maputnik/desktop/filewatch"
|
||||||
|
|||||||
Generated
+1423
-1628
File diff suppressed because it is too large
Load Diff
+42
-43
@@ -1,14 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "maputnik",
|
"name": "maputnik",
|
||||||
"version": "3.0.0",
|
"version": "2.1.1",
|
||||||
"description": "A MapLibre GL visual style editor",
|
"description": "A MapLibre GL visual style editor",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "''",
|
"main": "''",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "tsc && vite build --mode=production",
|
"build": "tsc && vite build --base=/maputnik/",
|
||||||
"build-desktop": "tsc && vite build --mode=desktop && cd desktop && make",
|
"build-desktop": "tsc && vite build --base=/ && cd desktop && make",
|
||||||
"build-linux": "tsc && vite build --mode=desktop && cd desktop && make bin/linux/maputnik",
|
|
||||||
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
|
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"test": "cypress run",
|
"test": "cypress run",
|
||||||
@@ -24,27 +23,23 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/maplibre/maputnik#readme",
|
"homepage": "https://github.com/maplibre/maputnik#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
|
||||||
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
|
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
|
||||||
"@maplibre/maplibre-gl-geocoder": "^1.9.0",
|
"@maplibre/maplibre-gl-geocoder": "^1.7.1",
|
||||||
"@maplibre/maplibre-gl-inspect": "^1.7.1",
|
"@maplibre/maplibre-gl-inspect": "^1.7.1",
|
||||||
"@maplibre/maplibre-gl-style-spec": "^23.3.0",
|
"@maplibre/maplibre-gl-style-spec": "^23.1.0",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@prantlf/jsonlint": "^16.0.0",
|
"@prantlf/jsonlint": "^16.0.0",
|
||||||
"array-move": "^4.0.0",
|
"array-move": "^4.0.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"codemirror": "^5.65.20",
|
"codemirror": "^5.65.18",
|
||||||
"color": "^5.0.0",
|
"color": "^5.0.0",
|
||||||
"detect-browser": "^5.3.0",
|
"detect-browser": "^5.3.0",
|
||||||
"downshift": "^9.0.10",
|
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"i18next": "^25.5.2",
|
"i18next": "^24.2.2",
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.0.4",
|
||||||
"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",
|
||||||
@@ -55,23 +50,26 @@
|
|||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"maplibre-gl": "^5.7.1",
|
"maplibre-gl": "^5.1.0",
|
||||||
"maputnik-design": "github:maputnik/design#172b06c",
|
"maputnik-design": "github:maputnik/design#172b06c",
|
||||||
"ol": "^10.6.1",
|
"ol": "^10.4.0",
|
||||||
"ol-mapbox-style": "^13.1.0",
|
"ol-mapbox-style": "^12.4.0",
|
||||||
"pmtiles": "^4.3.0",
|
"pmtiles": "^4.1.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-accessible-accordion": "^5.0.1",
|
"react-accessible-accordion": "^5.0.0",
|
||||||
"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",
|
||||||
|
"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.7.3",
|
"react-i18next": "^15.4.0",
|
||||||
"react-icon-base": "^2.1.2",
|
"react-icon-base": "^2.1.2",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.4.0",
|
||||||
|
"react-sortable-hoc": "^2.0.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",
|
||||||
@@ -95,14 +93,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cypress/code-coverage": "^3.14.6",
|
"@cypress/code-coverage": "^3.13.11",
|
||||||
"@eslint/js": "^9.35.0",
|
"@eslint/js": "^9.19.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": "^6.0.1",
|
"@shellygo/cypress-test-utils": "^4.1.13",
|
||||||
"@types/codemirror": "^5.60.16",
|
"@types/codemirror": "^5.60.15",
|
||||||
"@types/color": "^4.2.0",
|
"@types/color": "^4.2.0",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/geojson": "^7946.0.16",
|
"@types/geojson": "^7946.0.16",
|
||||||
"@types/json-to-ast": "^2.1.4",
|
"@types/json-to-ast": "^2.1.4",
|
||||||
@@ -116,6 +114,7 @@
|
|||||||
"@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,28 +122,28 @@
|
|||||||
"@types/react-icon-base": "^2.1.6",
|
"@types/react-icon-base": "^2.1.6",
|
||||||
"@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.5",
|
||||||
"@vitejs/plugin-react": "^5.0.2",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cypress": "^15.1.0",
|
"cypress": "^14.0.2",
|
||||||
"cypress-plugin-tab": "^1.0.5",
|
"cypress-plugin-tab": "^1.0.5",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.19.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.18",
|
||||||
"i18next-parser": "^9.3.0",
|
"i18next-parser": "^9.1.0",
|
||||||
"istanbul": "^0.4.5",
|
"istanbul": "^0.4.5",
|
||||||
"istanbul-lib-coverage": "^3.2.2",
|
"istanbul-lib-coverage": "^3.2.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.1",
|
||||||
"react-hot-loader": "^4.13.1",
|
"react-hot-loader": "^4.13.1",
|
||||||
"sass": "^1.92.1",
|
"sass": "^1.83.4",
|
||||||
"stylelint": "^16.24.0",
|
"stylelint": "^16.14.1",
|
||||||
"stylelint-config-recommended-scss": "^16.0.0",
|
"stylelint-config-recommended-scss": "^14.1.0",
|
||||||
"stylelint-scss": "^6.12.1",
|
"stylelint-scss": "^6.11.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.7.3",
|
||||||
"typescript-eslint": "^8.43.0",
|
"typescript-eslint": "^8.22.0",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^11.0.5",
|
||||||
"vite": "^7.1.5",
|
"vite": "^6.1.0",
|
||||||
"vite-plugin-istanbul": "^7.1.0"
|
"vite-plugin-istanbul": "^6.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+149
-92
@@ -1,3 +1,5 @@
|
|||||||
|
// @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'
|
||||||
@@ -27,14 +29,17 @@ import ModalDebug from './ModalDebug'
|
|||||||
|
|
||||||
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
|
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
|
||||||
import style from '../libs/style'
|
import style from '../libs/style'
|
||||||
|
import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
|
||||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
||||||
import { createStyleStore, type IStyleStore } from '../libs/store/style-store-factory'
|
import { StyleStore } from '../libs/stylestore'
|
||||||
|
import { ApiStyleStore } from '../libs/apistore'
|
||||||
import { RevisionStore } from '../libs/revisions'
|
import { RevisionStore } from '../libs/revisions'
|
||||||
import LayerWatcher from '../libs/layerwatcher'
|
import LayerWatcher from '../libs/layerwatcher'
|
||||||
import tokens from '../config/tokens.json'
|
import tokens from '../config/tokens.json'
|
||||||
import isEqual from 'lodash.isequal'
|
import isEqual from 'lodash.isequal'
|
||||||
|
import Debug from '../libs/debug'
|
||||||
|
import { SortEnd } from 'react-sortable-hoc';
|
||||||
import { MapOptions } from 'maplibre-gl';
|
import { MapOptions } from 'maplibre-gl';
|
||||||
import { OnStyleChangedOpts, StyleSpecificationWithId } from '../libs/definitions'
|
|
||||||
|
|
||||||
// Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
|
// Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
|
||||||
window.Buffer = buffer.Buffer;
|
window.Buffer = buffer.Buffer;
|
||||||
@@ -43,7 +48,6 @@ function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
|
|||||||
const matchesTilehosting = url.match(/\.tilehosting\.com/);
|
const matchesTilehosting = url.match(/\.tilehosting\.com/);
|
||||||
const matchesMaptiler = url.match(/\.maptiler\.com/);
|
const matchesMaptiler = url.match(/\.maptiler\.com/);
|
||||||
const matchesThunderforest = url.match(/\.thunderforest\.com/);
|
const matchesThunderforest = url.match(/\.thunderforest\.com/);
|
||||||
const matchesLocationIQ = url.match(/\.locationiq\.com/);
|
|
||||||
if (matchesTilehosting || matchesMaptiler) {
|
if (matchesTilehosting || matchesMaptiler) {
|
||||||
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true})
|
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true})
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
@@ -56,12 +60,6 @@ function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
|
|||||||
return url.replace('{key}', accessToken)
|
return url.replace('{key}', accessToken)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (matchesLocationIQ) {
|
|
||||||
const accessToken = style.getAccessToken("locationiq", mapStyle, {allowFallback: true})
|
|
||||||
if (accessToken) {
|
|
||||||
return url.replace('{key}', accessToken)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
else {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
@@ -80,6 +78,12 @@ function updateRootSpec(spec: any, fieldName: string, newValues: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OnStyleChangedOpts = {
|
||||||
|
save?: boolean
|
||||||
|
addRevision?: boolean
|
||||||
|
initialLoad?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
type MappedErrors = {
|
type MappedErrors = {
|
||||||
message: string
|
message: string
|
||||||
parsed?: {
|
parsed?: {
|
||||||
@@ -95,11 +99,11 @@ type MappedErrors = {
|
|||||||
type AppState = {
|
type AppState = {
|
||||||
errors: MappedErrors[],
|
errors: MappedErrors[],
|
||||||
infos: string[],
|
infos: string[],
|
||||||
mapStyle: StyleSpecificationWithId,
|
mapStyle: StyleSpecification & {id: string},
|
||||||
dirtyMapStyle?: StyleSpecification,
|
dirtyMapStyle?: StyleSpecification,
|
||||||
selectedLayerIndex: number,
|
selectedLayerIndex: number,
|
||||||
selectedLayerOriginalId?: string,
|
selectedLayerOriginalId?: string,
|
||||||
sources: {[key: string]: SourceSpecification & {layers: string[]} },
|
sources: {[key: string]: SourceSpecification},
|
||||||
vectorLayers: {},
|
vectorLayers: {},
|
||||||
spec: any,
|
spec: any,
|
||||||
mapView: {
|
mapView: {
|
||||||
@@ -131,56 +135,26 @@ type AppState = {
|
|||||||
|
|
||||||
export default class App extends React.Component<any, AppState> {
|
export default class App extends React.Component<any, AppState> {
|
||||||
revisionStore: RevisionStore;
|
revisionStore: RevisionStore;
|
||||||
styleStore: IStyleStore | null = null;
|
styleStore: StyleStore | ApiStyleStore;
|
||||||
layerWatcher: LayerWatcher;
|
layerWatcher: LayerWatcher;
|
||||||
|
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props)
|
super(props)
|
||||||
|
autoBind(this);
|
||||||
|
|
||||||
this.revisionStore = new RevisionStore();
|
this.revisionStore = new RevisionStore()
|
||||||
this.configureKeyboardShortcuts();
|
const params = new URLSearchParams(window.location.search.substring(1))
|
||||||
|
let port = params.get("localport")
|
||||||
this.state = {
|
if (port == null && (window.location.port !== "80" && window.location.port !== "443")) {
|
||||||
errors: [],
|
port = window.location.port
|
||||||
infos: [],
|
|
||||||
mapStyle: style.emptyStyle,
|
|
||||||
selectedLayerIndex: 0,
|
|
||||||
sources: {},
|
|
||||||
vectorLayers: {},
|
|
||||||
mapState: "map",
|
|
||||||
spec: latest,
|
|
||||||
mapView: {
|
|
||||||
zoom: 0,
|
|
||||||
center: {
|
|
||||||
lng: 0,
|
|
||||||
lat: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
isOpen: {
|
|
||||||
settings: false,
|
|
||||||
sources: false,
|
|
||||||
open: false,
|
|
||||||
shortcuts: false,
|
|
||||||
export: false,
|
|
||||||
debug: false,
|
|
||||||
},
|
|
||||||
maplibreGlDebugOptions: {
|
|
||||||
showTileBoundaries: false,
|
|
||||||
showCollisionBoxes: false,
|
|
||||||
showOverdrawInspector: false,
|
|
||||||
},
|
|
||||||
openlayersDebugOptions: {
|
|
||||||
debugToolbox: false,
|
|
||||||
},
|
|
||||||
fileHandle: null,
|
|
||||||
}
|
}
|
||||||
|
this.styleStore = new ApiStyleStore({
|
||||||
this.layerWatcher = new LayerWatcher({
|
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, {save: false}),
|
||||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
port: port,
|
||||||
|
host: params.get("localhost")
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
configureKeyboardShortcuts = () => {
|
|
||||||
const shortcuts = [
|
const shortcuts = [
|
||||||
{
|
{
|
||||||
key: "?",
|
key: "?",
|
||||||
@@ -250,6 +224,74 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const styleUrl = initialStyleUrl()
|
||||||
|
if(styleUrl && window.confirm("Load style from URL: " + styleUrl + " and discard current changes?")) {
|
||||||
|
this.styleStore = new StyleStore()
|
||||||
|
loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle))
|
||||||
|
removeStyleQuerystring()
|
||||||
|
} else {
|
||||||
|
if(styleUrl) {
|
||||||
|
removeStyleQuerystring()
|
||||||
|
}
|
||||||
|
this.styleStore.init(err => {
|
||||||
|
if(err) {
|
||||||
|
console.log('Falling back to local storage for storing styles')
|
||||||
|
this.styleStore = new StyleStore()
|
||||||
|
}
|
||||||
|
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle, {initialLoad: true}))
|
||||||
|
|
||||||
|
if(Debug.enabled()) {
|
||||||
|
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||||
|
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Debug.enabled()) {
|
||||||
|
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
||||||
|
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
errors: [],
|
||||||
|
infos: [],
|
||||||
|
mapStyle: style.emptyStyle,
|
||||||
|
selectedLayerIndex: 0,
|
||||||
|
sources: {},
|
||||||
|
vectorLayers: {},
|
||||||
|
mapState: "map",
|
||||||
|
spec: latest,
|
||||||
|
mapView: {
|
||||||
|
zoom: 0,
|
||||||
|
center: {
|
||||||
|
lng: 0,
|
||||||
|
lat: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isOpen: {
|
||||||
|
settings: false,
|
||||||
|
sources: false,
|
||||||
|
open: false,
|
||||||
|
shortcuts: false,
|
||||||
|
export: false,
|
||||||
|
// TODO: Disabled for now, this should be opened on the Nth visit to the editor
|
||||||
|
debug: false,
|
||||||
|
},
|
||||||
|
maplibreGlDebugOptions: {
|
||||||
|
showTileBoundaries: false,
|
||||||
|
showCollisionBoxes: false,
|
||||||
|
showOverdrawInspector: false,
|
||||||
|
},
|
||||||
|
openlayersDebugOptions: {
|
||||||
|
debugToolbox: false,
|
||||||
|
},
|
||||||
|
fileHandle: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.layerWatcher = new LayerWatcher({
|
||||||
|
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyPress = (e: KeyboardEvent) => {
|
handleKeyPress = (e: KeyboardEvent) => {
|
||||||
@@ -275,8 +317,7 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
componentDidMount() {
|
||||||
this.styleStore = await createStyleStore((mapStyle, opts) => this.onStyleChanged(mapStyle, opts));
|
|
||||||
window.addEventListener("keydown", this.handleKeyPress);
|
window.addEventListener("keydown", this.handleKeyPress);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,8 +325,8 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
window.removeEventListener("keydown", this.handleKeyPress);
|
window.removeEventListener("keydown", this.handleKeyPress);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveStyle(snapshotStyle: StyleSpecificationWithId) {
|
saveStyle(snapshotStyle: StyleSpecification & {id: string}) {
|
||||||
this.styleStore?.save(snapshotStyle)
|
this.styleStore.save(snapshotStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFonts(urlTemplate: string) {
|
updateFonts(urlTemplate: string) {
|
||||||
@@ -326,7 +367,7 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
this.onStyleChanged(changedStyle)
|
this.onStyleChanged(changedStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
onStyleChanged = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}): void => {
|
onStyleChanged = (newStyle: StyleSpecification & {id: string}, opts: OnStyleChangedOpts={}) => {
|
||||||
opts = {
|
opts = {
|
||||||
save: true,
|
save: true,
|
||||||
addRevision: true,
|
addRevision: true,
|
||||||
@@ -357,6 +398,7 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const errors: ValidationError[] = validateStyleMin(newStyle) || [];
|
const errors: ValidationError[] = validateStyleMin(newStyle) || [];
|
||||||
|
|
||||||
// The validate function doesn't give us errors for duplicate error with
|
// The validate function doesn't give us errors for duplicate error with
|
||||||
// empty string for layer.id, manually deal with that here.
|
// empty string for layer.id, manually deal with that here.
|
||||||
const layerErrors: (Error | ValidationError)[] = [];
|
const layerErrors: (Error | ValidationError)[] = [];
|
||||||
@@ -435,7 +477,7 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
dirtyMapStyle = cloneDeep(newStyle);
|
dirtyMapStyle = cloneDeep(newStyle);
|
||||||
|
|
||||||
for (const error of errors) {
|
errors.forEach(error => {
|
||||||
const {message} = error;
|
const {message} = error;
|
||||||
if (message) {
|
if (message) {
|
||||||
try {
|
try {
|
||||||
@@ -445,10 +487,10 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
unset(dirtyMapStyle, unsetPath);
|
unset(dirtyMapStyle, unsetPath);
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
console.warn(message + " " + err);
|
console.warn(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||||
@@ -462,7 +504,7 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
this.revisionStore.addRevision(newStyle);
|
this.revisionStore.addRevision(newStyle);
|
||||||
}
|
}
|
||||||
if (opts.save) {
|
if (opts.save) {
|
||||||
this.saveStyle(newStyle);
|
this.saveStyle(newStyle as StyleSpecification & {id: string});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -495,7 +537,7 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMoveLayer = (move: {oldIndex: number; newIndex: number}) => {
|
onMoveLayer = (move: SortEnd) => {
|
||||||
let { oldIndex, newIndex } = move;
|
let { oldIndex, newIndex } = move;
|
||||||
let layers = this.state.mapStyle.layers;
|
let layers = this.state.mapStyle.layers;
|
||||||
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
||||||
@@ -575,7 +617,7 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
}, this.setStateInUrl);
|
}, this.setStateInUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
setDefaultValues = (styleObj: StyleSpecificationWithId) => {
|
setDefaultValues = (styleObj: StyleSpecification & {id: string}) => {
|
||||||
const metadata: {[key: string]: string} = styleObj.metadata || {} as any
|
const metadata: {[key: string]: string} = styleObj.metadata || {} as any
|
||||||
if(metadata['maputnik:renderer'] === undefined) {
|
if(metadata['maputnik:renderer'] === undefined) {
|
||||||
const changedStyle = {
|
const changedStyle = {
|
||||||
@@ -591,28 +633,27 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openStyle = (styleObj: StyleSpecificationWithId, fileHandle: FileSystemFileHandle | null) => {
|
openStyle = (styleObj: StyleSpecification & {id: string}, fileHandle: FileSystemFileHandle | null) => {
|
||||||
this.setState({fileHandle: fileHandle});
|
this.setState({fileHandle: fileHandle});
|
||||||
styleObj = this.setDefaultValues(styleObj)
|
styleObj = this.setDefaultValues(styleObj)
|
||||||
this.onStyleChanged(styleObj)
|
this.onStyleChanged(styleObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSources() {
|
fetchSources() {
|
||||||
const sourceList: {[key: string]: SourceSpecification & {layers: string[]}} = {};
|
const sourceList: {[key: string]: any} = {};
|
||||||
for(const key of Object.keys(this.state.mapStyle.sources)) {
|
|
||||||
const source = this.state.mapStyle.sources[key];
|
for(const [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
||||||
if(source.type !== "vector" || !('url' in source)) {
|
if(
|
||||||
sourceList[key] = this.state.sources[key] || {...this.state.mapStyle.sources[key]};
|
!Object.prototype.hasOwnProperty.call(this.state.sources, key) &&
|
||||||
if (sourceList[key].layers === undefined) {
|
val.type === "vector" &&
|
||||||
sourceList[key].layers = [];
|
Object.prototype.hasOwnProperty.call(val, "url")
|
||||||
}
|
) {
|
||||||
} else {
|
|
||||||
sourceList[key] = {
|
sourceList[key] = {
|
||||||
type: source.type,
|
type: val.type,
|
||||||
layers: []
|
layers: []
|
||||||
};
|
};
|
||||||
|
|
||||||
let url = source.url;
|
let url = val.url;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
url = setFetchAccessToken(url!, this.state.mapStyle)
|
url = setFetchAccessToken(url!, this.state.mapStyle)
|
||||||
@@ -625,28 +666,44 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create new objects before setState
|
||||||
|
const sources = Object.assign({}, {
|
||||||
|
[key]: this.state.sources[key],
|
||||||
|
});
|
||||||
|
|
||||||
for(const layer of json.vector_layers) {
|
for(const layer of json.vector_layers) {
|
||||||
sourceList[key].layers.push(layer.id)
|
(sources[key] as any).layers.push(layer.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
sources: sources
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
if (url!.startsWith("pmtiles://")) {
|
||||||
if (url!.startsWith("pmtiles://")) {
|
(new PMTiles(url!.substr(10))).getTileJson("")
|
||||||
const json = await (new PMTiles(url!.substring(10))).getTileJson("");
|
.then(json => setVectorLayers(json))
|
||||||
setVectorLayers(json);
|
.catch(err => {
|
||||||
} else {
|
console.error("Failed to process sources for '%s'", url, err);
|
||||||
const response = await fetch(url!, { mode: 'cors' });
|
});
|
||||||
const json = await response.json();
|
} else {
|
||||||
setVectorLayers(json);
|
fetch(url!, {
|
||||||
}
|
mode: 'cors',
|
||||||
} catch(err) {
|
})
|
||||||
console.error(`Failed to process source for url: '${url}', ${err}`);
|
.then(response => response.json())
|
||||||
|
.then(json => setVectorLayers(json))
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Failed to process sources for '%s'", url, err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
sourceList[key] = this.state.sources[key] || this.state.mapStyle.sources[key];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!isEqual(this.state.sources, sourceList)) {
|
if(!isEqual(this.state.sources, sourceList)) {
|
||||||
console.debug("Setting sources", sourceList);
|
console.debug("Setting sources");
|
||||||
this.setState({
|
this.setState({
|
||||||
sources: sourceList
|
sources: sourceList
|
||||||
})
|
})
|
||||||
@@ -823,8 +880,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 });
|
this.setState({fileHandle: fileHandle});
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
|
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
|
||||||
|
|||||||
@@ -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,31 +13,38 @@ 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 <IconContext.Provider value={{size: '14px'}}>
|
return <div className="maputnik-layout">
|
||||||
<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">
|
{this.props.layerList}
|
||||||
{this.props.layerList}
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-layout-drawer">
|
|
||||||
<ScrollContainer>
|
|
||||||
{this.props.layerEditor}
|
|
||||||
</ScrollContainer>
|
|
||||||
</div>
|
|
||||||
{this.props.map}
|
|
||||||
</div>
|
</div>
|
||||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
<div className="maputnik-layout-drawer">
|
||||||
{this.props.bottom}
|
<ScrollContainer>
|
||||||
|
{this.props.layerEditor}
|
||||||
|
</ScrollContainer>
|
||||||
</div>
|
</div>
|
||||||
}
|
{this.props.map}
|
||||||
{this.props.modals}
|
|
||||||
</div>
|
</div>
|
||||||
</IconContext.Provider>
|
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||||
|
{this.props.bottom}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{this.props.modals}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import pkgJson from '../../package.json'
|
|||||||
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
|
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
|
||||||
import { withTranslation, WithTranslation } from 'react-i18next';
|
import { withTranslation, WithTranslation } from 'react-i18next';
|
||||||
import { supportedLanguages } from '../i18n';
|
import { supportedLanguages } from '../i18n';
|
||||||
import type { OnStyleChangedCallback } from '../libs/definitions';
|
|
||||||
|
|
||||||
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
|
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
|
||||||
const browser = detect();
|
const browser = detect();
|
||||||
@@ -94,9 +93,9 @@ export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deut
|
|||||||
type AppToolbarInternalProps = {
|
type AppToolbarInternalProps = {
|
||||||
mapStyle: object
|
mapStyle: object
|
||||||
inspectModeEnabled: boolean
|
inspectModeEnabled: boolean
|
||||||
onStyleChanged: OnStyleChangedCallback
|
onStyleChanged(...args: unknown[]): unknown
|
||||||
// A new style has been uploaded
|
// A new style has been uploaded
|
||||||
onStyleOpen: OnStyleChangedCallback
|
onStyleOpen(...args: unknown[]): unknown
|
||||||
// A dict of source id's and the available source layers
|
// A dict of source id's and the available source layers
|
||||||
sources: object
|
sources: object
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
|
|||||||
@@ -102,3 +102,4 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
|||||||
</label>
|
</label>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,3 +31,4 @@ export default class Collapse extends React.Component<CollapseProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ export default class Collapser extends React.Component<CollapserProps> {
|
|||||||
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />
|
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
@@ -8,12 +9,11 @@ type FieldArrayProps = InputArrayProps & {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const FieldArray: React.FC<FieldArrayProps> = (props) => {
|
export default class FieldArray extends React.Component<FieldArrayProps> {
|
||||||
return (
|
render() {
|
||||||
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
|
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputArray {...props} />
|
<InputArray {...this.props} />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldArray;
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
|
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
|
||||||
|
|
||||||
@@ -7,12 +8,11 @@ type FieldAutocompleteProps = InputAutocompleteProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldAutocomplete: React.FC<FieldAutocompleteProps> = (props) => {
|
export default class FieldAutocomplete extends React.Component<FieldAutocompleteProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label}>
|
return <Block label={this.props.label}>
|
||||||
<InputAutocomplete {...props} />
|
<InputAutocomplete {...this.props} />
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldAutocomplete;
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
|
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
|
||||||
|
|
||||||
@@ -7,12 +8,11 @@ type FieldCheckboxProps = InputCheckboxProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldCheckbox: React.FC<FieldCheckboxProps> = (props) => {
|
export default class FieldCheckbox extends React.Component<FieldCheckboxProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label}>
|
return <Block label={this.props.label}>
|
||||||
<InputCheckbox {...props} />
|
<InputCheckbox {...this.props} />
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldCheckbox;
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputColor, {InputColorProps} from './InputColor'
|
import InputColor, {InputColorProps} from './InputColor'
|
||||||
|
|
||||||
@@ -10,12 +11,11 @@ type FieldColorProps = InputColorProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldColor: React.FC<FieldColorProps> = (props) => {
|
export default class FieldColor extends React.Component<FieldColorProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputColor {...props} />
|
<InputColor {...this.props} />
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldColor;
|
|
||||||
|
|||||||
@@ -10,31 +10,29 @@ type FieldCommentInternalProps = {
|
|||||||
error: {message: string}
|
error: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
const FieldCommentInternal: React.FC<FieldCommentInternalProps> = (props) => {
|
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
|
||||||
const t = props.t;
|
render() {
|
||||||
const fieldSpec = {
|
const t = this.props.t;
|
||||||
doc: t(
|
const fieldSpec = {
|
||||||
"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 (
|
return <Block
|
||||||
<Block
|
|
||||||
label={t("Comments")}
|
label={t("Comments")}
|
||||||
fieldSpec={fieldSpec}
|
fieldSpec={fieldSpec}
|
||||||
data-wd-key="layer-comment"
|
data-wd-key="layer-comment"
|
||||||
error={props.error}
|
error={this.props.error}
|
||||||
>
|
>
|
||||||
<InputString
|
<InputString
|
||||||
multi={true}
|
multi={true}
|
||||||
value={props.value}
|
value={this.props.value}
|
||||||
onChange={props.onChange}
|
onChange={this.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;
|
||||||
|
|||||||
@@ -9,45 +9,57 @@ type FieldDocLabelProps = {
|
|||||||
onToggleDoc?(...args: unknown[]): unknown
|
onToggleDoc?(...args: unknown[]): unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FieldDocLabelState = {
|
||||||
|
open: boolean
|
||||||
|
};
|
||||||
|
|
||||||
const FieldDocLabel: React.FC<FieldDocLabelProps> = (props) => {
|
export default class FieldDocLabel extends React.Component<FieldDocLabelProps, FieldDocLabelState> {
|
||||||
const [open, setOpen] = React.useState(false);
|
constructor (props: FieldDocLabelProps) {
|
||||||
|
super(props);
|
||||||
const onToggleDoc = (state: boolean) => {
|
this.state = {
|
||||||
setOpen(state);
|
open: false,
|
||||||
if (props.onToggleDoc) {
|
|
||||||
props.onToggleDoc(state);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const { label, fieldSpec } = props;
|
onToggleDoc = (open: boolean) => {
|
||||||
const { doc } = fieldSpec || {};
|
this.setState({
|
||||||
|
open,
|
||||||
|
}, () => {
|
||||||
|
if (this.props.onToggleDoc) {
|
||||||
|
this.props.onToggleDoc(this.state.open);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (doc) {
|
render() {
|
||||||
return (
|
const {label, fieldSpec} = this.props;
|
||||||
<label className="maputnik-doc-wrapper">
|
const {doc} = fieldSpec || {};
|
||||||
|
|
||||||
|
if (doc) {
|
||||||
|
return <label className="maputnik-doc-wrapper">
|
||||||
<div className="maputnik-doc-target">
|
<div className="maputnik-doc-target">
|
||||||
{label}
|
{label}
|
||||||
{'\xa0'}
|
{'\xa0'}
|
||||||
<button
|
<button
|
||||||
aria-label={open ? 'close property documentation' : 'open property documentation'}
|
aria-label={this.state.open ? "close property documentation" : "open property documentation"}
|
||||||
className={`maputnik-doc-button maputnik-doc-button--${open ? 'open' : 'closed'}`}
|
className={`maputnik-doc-button maputnik-doc-button--${this.state.open ? 'open' : 'closed'}`}
|
||||||
onClick={() => onToggleDoc(!open)}
|
onClick={() => this.onToggleDoc(!this.state.open)}
|
||||||
data-wd-key={'field-doc-button-' + label}
|
data-wd-key={'field-doc-button-'+label}
|
||||||
>
|
>
|
||||||
{open ? <MdHighlightOff /> : <MdInfoOutline />}
|
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
);
|
}
|
||||||
} else if (label) {
|
else if (label) {
|
||||||
return (
|
return <label className="maputnik-doc-wrapper">
|
||||||
<label className="maputnik-doc-wrapper">
|
<div className="maputnik-doc-target">
|
||||||
<div className="maputnik-doc-target">{label}</div>
|
{label}
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
);
|
}
|
||||||
|
else {
|
||||||
|
<div />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return <div />;
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export default FieldDocLabel;
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
@@ -5,12 +6,11 @@ type FieldDynamicArrayProps = InputDynamicArrayProps & {
|
|||||||
name?: string
|
name?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
const FieldDynamicArray: React.FC<FieldDynamicArrayProps> = (props) => {
|
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
|
||||||
return (
|
render() {
|
||||||
<Fieldset label={props.label}>
|
return <Fieldset label={this.props.label}>
|
||||||
<InputDynamicArray {...props} />
|
<InputDynamicArray {...this.props} />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldDynamicArray;
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import InputEnum, {InputEnumProps} from './InputEnum'
|
import InputEnum, {InputEnumProps} from './InputEnum'
|
||||||
import Fieldset from './Fieldset';
|
import Fieldset from './Fieldset';
|
||||||
|
|
||||||
@@ -10,12 +11,10 @@ type FieldEnumProps = InputEnumProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldEnum: React.FC<FieldEnumProps> = (props) => {
|
export default class FieldEnum extends React.Component<FieldEnumProps> {
|
||||||
return (
|
render() {
|
||||||
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
|
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputEnum {...props} />
|
<InputEnum {...this.props} />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldEnum;
|
|
||||||
+247
-218
@@ -111,268 +111,297 @@ 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
|
||||||
*/
|
*/
|
||||||
const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
export default class FieldFunction extends React.Component<FieldFunctionProps, FieldFunctionState> {
|
||||||
const [dataType, setDataType] = React.useState(
|
constructor (props: FieldFunctionProps) {
|
||||||
getDataType(props.value, props.fieldSpec)
|
super(props);
|
||||||
);
|
this.state = {
|
||||||
const [isEditing, setIsEditing] = React.useState(false);
|
dataType: getDataType(props.value, props.fieldSpec),
|
||||||
|
isEditing: false,
|
||||||
React.useEffect(() => {
|
|
||||||
if (!isEditing) {
|
|
||||||
setDataType(getDataType(props.value, props.fieldSpec));
|
|
||||||
}
|
}
|
||||||
}, [props.value, props.fieldSpec, isEditing]);
|
}
|
||||||
|
|
||||||
const getFieldFunctionType = (fieldSpec: any) => {
|
static getDerivedStateFromProps(props: Readonly<FieldFunctionProps>, state: FieldFunctionState) {
|
||||||
|
// Because otherwise when editing values we end up accidentally changing field type.
|
||||||
|
if (state.isEditing) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {
|
||||||
|
isEditing: false,
|
||||||
|
dataType: getDataType(props.value, props.fieldSpec)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
};
|
}
|
||||||
|
|
||||||
const addStop = () => {
|
addStop = () => {
|
||||||
const stops = props.value.stops.slice(0);
|
const stops = this.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 {
|
}
|
||||||
stops.push([lastStop[0] + 1, lastStop[1]]);
|
else {
|
||||||
|
stops.push([lastStop[0] + 1, lastStop[1]])
|
||||||
}
|
}
|
||||||
|
|
||||||
const changedValue = {
|
const changedValue = {
|
||||||
...props.value,
|
...this.props.value,
|
||||||
stops: stops,
|
stops: stops,
|
||||||
};
|
|
||||||
|
|
||||||
props.onChange(props.fieldName, changedValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteExpression = () => {
|
|
||||||
const { fieldSpec, fieldName } = props;
|
|
||||||
props.onChange(fieldName, fieldSpec.default);
|
|
||||||
setDataType('value');
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteStop = (stopIdx: number) => {
|
|
||||||
const stops = props.value.stops.slice(0);
|
|
||||||
stops.splice(stopIdx, 1);
|
|
||||||
|
|
||||||
let changedValue: any = {
|
|
||||||
...props.value,
|
|
||||||
stops: stops,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (stops.length === 1) {
|
|
||||||
changedValue = stops[0][1];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
props.onChange(props.fieldName, changedValue);
|
this.props.onChange(this.props.fieldName, changedValue)
|
||||||
};
|
}
|
||||||
|
|
||||||
const makeZoomFunction = () => {
|
deleteExpression = () => {
|
||||||
const { value } = props;
|
const {fieldSpec, fieldName} = this.props;
|
||||||
|
this.props.onChange(fieldName, fieldSpec.default);
|
||||||
|
this.setState({
|
||||||
|
dataType: "value",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let zoomFunc: any;
|
deleteStop = (stopIdx: number) => {
|
||||||
if (typeof value === 'object') {
|
const stops = this.props.value.stops.slice(0)
|
||||||
|
stops.splice(stopIdx, 1)
|
||||||
|
|
||||||
|
let changedValue = {
|
||||||
|
...this.props.value,
|
||||||
|
stops: stops,
|
||||||
|
}
|
||||||
|
|
||||||
|
if(stops.length === 1) {
|
||||||
|
changedValue = stops[0][1]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onChange(this.props.fieldName, changedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
makeZoomFunction = () => {
|
||||||
|
const {value} = this.props;
|
||||||
|
|
||||||
|
let zoomFunc;
|
||||||
|
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(props.fieldSpec)];
|
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||||
}),
|
})
|
||||||
};
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
zoomFunc = {
|
zoomFunc = {
|
||||||
base: value.base,
|
base: value.base,
|
||||||
stops: [
|
stops: [
|
||||||
[6, findDefaultFromSpec(props.fieldSpec)],
|
[6, findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
[10, findDefaultFromSpec(props.fieldSpec)],
|
[10, findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
],
|
]
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
zoomFunc = {
|
zoomFunc = {
|
||||||
stops: [
|
stops: [
|
||||||
[6, value || findDefaultFromSpec(props.fieldSpec)],
|
[6, value || findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
[10, value || findDefaultFromSpec(props.fieldSpec)],
|
[10, value || findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
],
|
]
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
props.onChange(props.fieldName, zoomFunc);
|
this.props.onChange(this.props.fieldName, zoomFunc)
|
||||||
};
|
}
|
||||||
|
|
||||||
const undoExpression = () => {
|
undoExpression = () => {
|
||||||
const { value, fieldName } = props;
|
const {value, fieldName} = this.props;
|
||||||
|
|
||||||
if (isGetExpression(value)) {
|
if (isGetExpression(value)) {
|
||||||
props.onChange(fieldName, {
|
this.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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const canUndo = () => {
|
canUndo = () => {
|
||||||
const { value, fieldSpec } = props;
|
const {value, fieldSpec} = this.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")
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeExpression = () => {
|
|
||||||
const { value, fieldSpec } = props;
|
|
||||||
let expression;
|
|
||||||
|
|
||||||
if (typeof value === 'object' && 'stops' in value) {
|
|
||||||
expression = styleFunction.convertFunction(value, fieldSpec);
|
|
||||||
} else if (isIdentityProperty(value)) {
|
|
||||||
expression = ['get', value.property];
|
|
||||||
} else {
|
|
||||||
expression = ['literal', value || props.fieldSpec.default];
|
|
||||||
}
|
|
||||||
props.onChange(props.fieldName, expression);
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeDataFunction = () => {
|
|
||||||
const functionType = getFieldFunctionType(props.fieldSpec);
|
|
||||||
const stopValue = functionType === 'categorical' ? '' : 0;
|
|
||||||
const { value } = props;
|
|
||||||
let dataFunc;
|
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
if (value.stops) {
|
|
||||||
dataFunc = {
|
|
||||||
property: '',
|
|
||||||
type: functionType,
|
|
||||||
base: value.base,
|
|
||||||
stops: value.stops.map((stop: Stop) => {
|
|
||||||
return [{ zoom: stop[0], value: stopValue }, stop[1] || findDefaultFromSpec(props.fieldSpec)];
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
dataFunc = {
|
|
||||||
property: '',
|
|
||||||
type: functionType,
|
|
||||||
base: value.base,
|
|
||||||
stops: [
|
|
||||||
[{ zoom: 6, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
|
|
||||||
[{ zoom: 10, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dataFunc = {
|
|
||||||
property: '',
|
|
||||||
type: functionType,
|
|
||||||
base: value.base,
|
|
||||||
stops: [
|
|
||||||
[{ zoom: 6, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
|
|
||||||
[{ zoom: 10, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
props.onChange(props.fieldName, dataFunc);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMarkEditing = () => {
|
|
||||||
setIsEditing(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onUnmarkEditing = () => {
|
|
||||||
setIsEditing(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const propClass =
|
|
||||||
props.fieldSpec.default === props.value ? 'maputnik-default-property' : 'maputnik-modified-property';
|
|
||||||
|
|
||||||
let specField;
|
|
||||||
|
|
||||||
if (dataType === 'expression') {
|
|
||||||
specField = (
|
|
||||||
<ExpressionProperty
|
|
||||||
errors={props.errors}
|
|
||||||
onChange={props.onChange.bind(null, props.fieldName)}
|
|
||||||
canUndo={canUndo}
|
|
||||||
onUndo={undoExpression}
|
|
||||||
onDelete={deleteExpression}
|
|
||||||
fieldType={props.fieldType}
|
|
||||||
fieldName={props.fieldName}
|
|
||||||
fieldSpec={props.fieldSpec}
|
|
||||||
value={props.value}
|
|
||||||
onFocus={onMarkEditing}
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
makeExpression = () => {
|
||||||
<div className={propClass} data-wd-key={'spec-field-container:' + props.fieldName}>
|
const {value, fieldSpec} = this.props;
|
||||||
|
let expression;
|
||||||
|
|
||||||
|
if (typeof(value) === "object" && 'stops' in value) {
|
||||||
|
expression = styleFunction.convertFunction(value, fieldSpec);
|
||||||
|
}
|
||||||
|
else if (isIdentityProperty(value)) {
|
||||||
|
expression = ["get", value.property];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expression = ["literal", value || this.props.fieldSpec.default];
|
||||||
|
}
|
||||||
|
this.props.onChange(this.props.fieldName, expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
makeDataFunction = () => {
|
||||||
|
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
|
||||||
|
const stopValue = functionType === 'categorical' ? '' : 0;
|
||||||
|
const {value} = this.props;
|
||||||
|
let dataFunc;
|
||||||
|
|
||||||
|
if (typeof(value) === "object") {
|
||||||
|
if (value.stops) {
|
||||||
|
dataFunc = {
|
||||||
|
property: "",
|
||||||
|
type: functionType,
|
||||||
|
base: value.base,
|
||||||
|
stops: value.stops.map((stop: Stop) => {
|
||||||
|
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dataFunc = {
|
||||||
|
property: "",
|
||||||
|
type: functionType,
|
||||||
|
base: value.base,
|
||||||
|
stops: [
|
||||||
|
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
|
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dataFunc = {
|
||||||
|
property: "",
|
||||||
|
type: functionType,
|
||||||
|
base: value.base,
|
||||||
|
stops: [
|
||||||
|
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
|
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onChange(this.props.fieldName, dataFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMarkEditing = () => {
|
||||||
|
this.setState({isEditing: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmarkEditing = () => {
|
||||||
|
this.setState({isEditing: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {dataType} = this.state;
|
||||||
|
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
|
||||||
|
let specField;
|
||||||
|
|
||||||
|
if (dataType === "expression") {
|
||||||
|
specField = (
|
||||||
|
<ExpressionProperty
|
||||||
|
errors={this.props.errors}
|
||||||
|
onChange={this.props.onChange.bind(this, this.props.fieldName)}
|
||||||
|
canUndo={this.canUndo}
|
||||||
|
onUndo={this.undoExpression}
|
||||||
|
onDelete={this.deleteExpression}
|
||||||
|
fieldType={this.props.fieldType}
|
||||||
|
fieldName={this.props.fieldName}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
value={this.props.value}
|
||||||
|
onFocus={this.onMarkEditing}
|
||||||
|
onBlur={this.onUnmarkEditing}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (dataType === "zoom_function") {
|
||||||
|
specField = (
|
||||||
|
<ZoomProperty
|
||||||
|
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
@@ -1,3 +1,4 @@
|
|||||||
|
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'
|
||||||
@@ -10,19 +11,18 @@ type FieldIdProps = {
|
|||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
};
|
};
|
||||||
|
|
||||||
const FieldId: React.FC<FieldIdProps> = (props) => {
|
export default class FieldId extends React.Component<FieldIdProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label="ID" fieldSpec={latest.layer.id}
|
return <Block label="ID" fieldSpec={latest.layer.id}
|
||||||
data-wd-key={props.wdKey}
|
|
||||||
error={props.error}
|
data-wd-key={this.props.wdKey}
|
||||||
|
error={this.props.error}
|
||||||
>
|
>
|
||||||
<InputString
|
<InputString
|
||||||
value={props.value}
|
value={this.props.value}
|
||||||
onInput={props.onChange}
|
onInput={this.props.onChange}
|
||||||
data-wd-key={props.wdKey + ".input"}
|
data-wd-key={this.props.wdKey + ".input"}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldId;
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
import React from 'react'
|
||||||
import InputJson, {InputJsonProps} from './InputJson'
|
import InputJson, {InputJsonProps} from './InputJson'
|
||||||
|
|
||||||
|
|
||||||
type FieldJsonProps = InputJsonProps & {};
|
type FieldJsonProps = InputJsonProps & {};
|
||||||
|
|
||||||
|
|
||||||
const FieldJson: React.FC<FieldJsonProps> = (props) => {
|
export default class FieldJson extends React.Component<FieldJsonProps> {
|
||||||
return <InputJson {...props} />;
|
render() {
|
||||||
};
|
return <InputJson {...this.props} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default FieldJson;
|
|
||||||
|
|||||||
@@ -11,25 +11,25 @@ type FieldMaxZoomInternalProps = {
|
|||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
const FieldMaxZoomInternal: React.FC<FieldMaxZoomInternalProps> = (props) => {
|
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
|
||||||
const t = props.t;
|
render() {
|
||||||
return (
|
const t = this.props.t;
|
||||||
<Block label={t('Max Zoom')} fieldSpec={latest.layer.maxzoom}
|
return <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
|
||||||
error={props.error}
|
error={this.props.error}
|
||||||
data-wd-key="max-zoom"
|
data-wd-key="max-zoom"
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
allowRange={true}
|
allowRange={true}
|
||||||
value={props.value}
|
value={this.props.value}
|
||||||
onChange={props.onChange}
|
onChange={this.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;
|
||||||
|
|||||||
@@ -11,25 +11,25 @@ type FieldMinZoomInternalProps = {
|
|||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
const FieldMinZoomInternal: React.FC<FieldMinZoomInternalProps> = (props) => {
|
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
|
||||||
const t = props.t;
|
render() {
|
||||||
return (
|
const t = this.props.t;
|
||||||
<Block label={t('Min Zoom')} fieldSpec={latest.layer.minzoom}
|
return <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
|
||||||
error={props.error}
|
error={this.props.error}
|
||||||
data-wd-key="min-zoom"
|
data-wd-key="min-zoom"
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
allowRange={true}
|
allowRange={true}
|
||||||
value={props.value}
|
value={this.props.value}
|
||||||
onChange={props.onChange}
|
onChange={this.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;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
|
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
|
||||||
import Fieldset from './Fieldset'
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
@@ -7,12 +8,11 @@ type FieldMultiInputProps = InputMultiInputProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldMultiInput: React.FC<FieldMultiInputProps> = (props) => {
|
export default class FieldMultiInput extends React.Component<FieldMultiInputProps> {
|
||||||
return (
|
render() {
|
||||||
<Fieldset label={props.label}>
|
return <Fieldset label={this.props.label}>
|
||||||
<InputMultiInput {...props} />
|
<InputMultiInput {...this.props} />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldMultiInput;
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import InputNumber, {InputNumberProps} from './InputNumber'
|
import InputNumber, {InputNumberProps} from './InputNumber'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
|
|
||||||
@@ -10,12 +11,10 @@ type FieldNumberProps = InputNumberProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldNumber: React.FC<FieldNumberProps> = (props) => {
|
export default class FieldNumber extends React.Component<FieldNumberProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputNumber {...props} />
|
<InputNumber {...this.props} />
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldNumber;
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputSelect, {InputSelectProps} from './InputSelect'
|
import InputSelect, {InputSelectProps} from './InputSelect'
|
||||||
|
|
||||||
@@ -10,12 +11,12 @@ type FieldSelectProps = InputSelectProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldSelect: React.FC<FieldSelectProps> = (props) => {
|
export default class FieldSelect extends React.Component<FieldSelectProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputSelect {...props} />
|
<InputSelect {...this.props}/>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
export default FieldSelect;
|
|
||||||
|
|||||||
@@ -13,30 +13,28 @@ type FieldSourceInternalProps = {
|
|||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
const FieldSourceInternal: React.FC<FieldSourceInternalProps> = ({
|
class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
|
||||||
onChange = () => {},
|
static defaultProps = {
|
||||||
sourceIds = [],
|
onChange: () => {},
|
||||||
wdKey,
|
sourceIds: [],
|
||||||
value,
|
}
|
||||||
error,
|
|
||||||
t
|
render() {
|
||||||
}) => {
|
const t = this.props.t;
|
||||||
return (
|
return <Block
|
||||||
<Block
|
label={t("Source")}
|
||||||
label={t('Source')}
|
|
||||||
fieldSpec={latest.layer.source}
|
fieldSpec={latest.layer.source}
|
||||||
error={error}
|
error={this.props.error}
|
||||||
data-wd-key={wdKey}
|
data-wd-key={this.props.wdKey}
|
||||||
>
|
>
|
||||||
<InputAutocomplete
|
<InputAutocomplete
|
||||||
value={value}
|
value={this.props.value}
|
||||||
onChange={onChange}
|
onChange={this.props.onChange}
|
||||||
options={sourceIds?.map((src) => [src, src])}
|
options={this.props.sourceIds?.map(src => [src, src])}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
const FieldSource = withTranslation()(FieldSourceInternal);
|
const FieldSource = withTranslation()(FieldSourceInternal);
|
||||||
export default FieldSource;
|
export default FieldSource;
|
||||||
|
|||||||
@@ -9,31 +9,34 @@ type FieldSourceLayerInternalProps = {
|
|||||||
value?: string
|
value?: string
|
||||||
onChange?(...args: unknown[]): unknown
|
onChange?(...args: unknown[]): unknown
|
||||||
sourceLayerIds?: unknown[]
|
sourceLayerIds?: unknown[]
|
||||||
|
isFixed?: boolean
|
||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = ({
|
class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalProps> {
|
||||||
onChange = () => {},
|
static defaultProps = {
|
||||||
sourceLayerIds = [],
|
onChange: () => {},
|
||||||
value,
|
sourceLayerIds: [],
|
||||||
error,
|
isFixed: false
|
||||||
t
|
}
|
||||||
}) => {
|
|
||||||
return (
|
render() {
|
||||||
<Block
|
const t = this.props.t;
|
||||||
label={t('Source Layer')}
|
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={error}
|
error={this.props.error}
|
||||||
>
|
>
|
||||||
<InputAutocomplete
|
<InputAutocomplete
|
||||||
value={value}
|
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
||||||
onChange={onChange}
|
value={this.props.value}
|
||||||
options={sourceLayerIds?.map((l) => [l, l])}
|
onChange={this.props.onChange}
|
||||||
|
options={this.props.sourceLayerIds?.map(l => [l, l])}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
|
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
|
||||||
export default FieldSourceLayer;
|
export default FieldSourceLayer;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputString, {InputStringProps} from './InputString'
|
import InputString, {InputStringProps} from './InputString'
|
||||||
|
|
||||||
@@ -9,12 +10,10 @@ type FieldStringProps = InputStringProps & {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const FieldString: React.FC<FieldStringProps> = (props) => {
|
export default class FieldString extends React.Component<FieldStringProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputString {...props} />
|
<InputString {...this.props} />
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldString;
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {v8} from '@maplibre/maplibre-gl-style-spec'
|
|
||||||
|
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputSelect from './InputSelect'
|
import InputSelect from './InputSelect'
|
||||||
import InputString from './InputString'
|
import InputString from './InputString'
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
import { startCase } from 'lodash'
|
|
||||||
|
|
||||||
type FieldTypeInternalProps = {
|
type FieldTypeInternalProps = {
|
||||||
value: string
|
value: string
|
||||||
@@ -14,34 +14,44 @@ type FieldTypeInternalProps = {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
const FieldTypeInternal: React.FC<FieldTypeInternalProps> = ({
|
class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
|
||||||
t,
|
static defaultProps = {
|
||||||
value,
|
disabled: false,
|
||||||
wdKey,
|
}
|
||||||
onChange,
|
|
||||||
error,
|
render() {
|
||||||
disabled = false
|
const t = this.props.t;
|
||||||
}) => {
|
return <Block label={t("Type")} fieldSpec={latest.layer.type}
|
||||||
const layerstypes: [string, string][] = Object.keys(v8.layer.type.values || {}).map(v => [v, startCase(v.replace(/-/g, ' '))]);
|
data-wd-key={this.props.wdKey}
|
||||||
return (
|
error={this.props.error}
|
||||||
<Block label={t('Type')} fieldSpec={v8.layer.type}
|
|
||||||
data-wd-key={wdKey}
|
|
||||||
error={error}
|
|
||||||
>
|
>
|
||||||
{disabled && (
|
{this.props.disabled &&
|
||||||
<InputString value={value} disabled={true} />
|
<InputString
|
||||||
)}
|
value={this.props.value}
|
||||||
{!disabled && (
|
disabled={true}
|
||||||
<InputSelect
|
|
||||||
options={layerstypes}
|
|
||||||
onChange={onChange}
|
|
||||||
value={value}
|
|
||||||
data-wd-key={wdKey + '.select'}
|
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
|
{!this.props.disabled &&
|
||||||
|
<InputSelect
|
||||||
|
options={[
|
||||||
|
['background', 'Background'],
|
||||||
|
['fill', 'Fill'],
|
||||||
|
['line', 'Line'],
|
||||||
|
['symbol', 'Symbol'],
|
||||||
|
['raster', 'Raster'],
|
||||||
|
['circle', 'Circle'],
|
||||||
|
['fill-extrusion', 'Fill Extrusion'],
|
||||||
|
['hillshade', 'Hillshade'],
|
||||||
|
['heatmap', 'Heatmap'],
|
||||||
|
]}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
value={this.props.value}
|
||||||
|
data-wd-key={this.props.wdKey + ".select"}
|
||||||
|
/>
|
||||||
|
}
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const FieldType = withTranslation()(FieldTypeInternal);
|
const FieldType = withTranslation()(FieldTypeInternal);
|
||||||
export default FieldType;
|
export default FieldType;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
@@ -10,12 +11,13 @@ type FieldUrlProps = InputUrlProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldUrl: React.FC<FieldUrlProps> = (props) => {
|
export default class FieldUrl extends React.Component<FieldUrlProps> {
|
||||||
return (
|
render () {
|
||||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
return (
|
||||||
<InputUrl {...props} />
|
<Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
</Block>
|
<InputUrl {...this.props} />
|
||||||
);
|
</Block>
|
||||||
};
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default FieldUrl;
|
|
||||||
|
|||||||
+52
-35
@@ -9,40 +9,57 @@ type FieldsetProps = PropsWithChildren & {
|
|||||||
action?: ReactElement,
|
action?: ReactElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FieldsetState = {
|
||||||
const Fieldset: React.FC<FieldsetProps> = (props) => {
|
showDoc: boolean
|
||||||
const [showDoc, setShowDoc] = React.useState(false);
|
|
||||||
const labelId = React.useRef(generateUniqueId('fieldset_label_'));
|
|
||||||
|
|
||||||
const onToggleDoc = (val: boolean) => {
|
|
||||||
setShowDoc(val);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="maputnik-input-block" role="group" aria-labelledby={labelId.current}>
|
|
||||||
{props.fieldSpec && (
|
|
||||||
<div className="maputnik-input-block-label">
|
|
||||||
<FieldDocLabel
|
|
||||||
label={props.label}
|
|
||||||
onToggleDoc={onToggleDoc}
|
|
||||||
fieldSpec={props.fieldSpec}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!props.fieldSpec && (
|
|
||||||
<div className="maputnik-input-block-label">
|
|
||||||
{props.label}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="maputnik-input-block-action">{props.action}</div>
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Fieldset;
|
export default class Fieldset extends React.Component<FieldsetProps, FieldsetState> {
|
||||||
|
_labelId: string;
|
||||||
|
|
||||||
|
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">
|
||||||
|
<FieldDocLabel
|
||||||
|
label={this.props.label}
|
||||||
|
onToggleDoc={this.onToggleDoc}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{!this.props.fieldSpec &&
|
||||||
|
<div className="maputnik-input-block-label">
|
||||||
|
{this.props.label}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div className="maputnik-input-block-action">
|
||||||
|
{this.props.action}
|
||||||
|
</div>
|
||||||
|
<div className="maputnik-input-block-content">
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
{this.props.fieldSpec &&
|
||||||
|
<div
|
||||||
|
className="maputnik-doc-inline"
|
||||||
|
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||||
|
>
|
||||||
|
<Doc fieldSpec={this.props.fieldSpec} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {mdiTableRowPlusAfter} from '@mdi/js';
|
import {mdiTableRowPlusAfter} from '@mdi/js';
|
||||||
import {isEqual} from 'lodash';
|
import {isEqual} from 'lodash';
|
||||||
import {ExpressionSpecification, LegacyFilterSpecification} from 'maplibre-gl'
|
import {ExpressionSpecification, LegacyFilterSpecification, StyleSpecification} from 'maplibre-gl'
|
||||||
import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec'
|
import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec'
|
||||||
import {mdiFunctionVariant} from '@mdi/js';
|
import {mdiFunctionVariant} from '@mdi/js';
|
||||||
|
|
||||||
@@ -14,7 +14,6 @@ import InputButton from './InputButton'
|
|||||||
import Doc from './Doc'
|
import Doc from './Doc'
|
||||||
import ExpressionProperty from './_ExpressionProperty';
|
import ExpressionProperty from './_ExpressionProperty';
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
import type { StyleSpecificationWithId } from '../libs/definitions';
|
|
||||||
|
|
||||||
|
|
||||||
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
|
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
|
||||||
@@ -40,7 +39,7 @@ function migrateFilter(filter: LegacyFilterSpecification | ExpressionSpecificati
|
|||||||
return (migrate(createStyleFromFilter(filter) as any).layers[0] as any).filter;
|
return (migrate(createStyleFromFilter(filter) as any).layers[0] as any).filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpecification): StyleSpecificationWithId {
|
function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpecification): StyleSpecification & {id: string} {
|
||||||
return {
|
return {
|
||||||
"id": "tmp",
|
"id": "tmp",
|
||||||
"version": 8,
|
"version": 8,
|
||||||
|
|||||||
@@ -11,18 +11,18 @@ class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInterna
|
|||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
return <div className="maputnik-filter-editor-block">
|
return <div className="maputnik-filter-editor-block">
|
||||||
<div className="maputnik-filter-editor-block-content">
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-filter-editor-block-action">
|
<div className="maputnik-filter-editor-block-action">
|
||||||
<InputButton
|
<InputButton
|
||||||
className="maputnik-icon-button"
|
className="maputnik-delete-filter"
|
||||||
onClick={this.props.onDelete}
|
onClick={this.props.onDelete}
|
||||||
title={t("Delete filter block")}
|
title={t("Delete filter block")}
|
||||||
>
|
>
|
||||||
<MdDelete />
|
<MdDelete />
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="maputnik-filter-editor-block-content">
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ export default class IconBackground extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ export default class IconCircle extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ export default class IconFill extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,3 +30,4 @@ export default class IconLayer extends React.Component<IconLayerProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ export default class IconLine extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,6 @@ export default class IconSymbol extends React.Component {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -115,3 +115,4 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,116 +1,102 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import {useCombobox} from 'downshift'
|
import Autocomplete from 'react-autocomplete'
|
||||||
|
|
||||||
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 function InputAutocomplete({
|
export default class InputAutocomplete extends React.Component<InputAutocompleteProps> {
|
||||||
value,
|
state = {
|
||||||
options = [],
|
maxHeight: MAX_HEIGHT
|
||||||
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(() => {
|
autocompleteMenuEl: HTMLDivElement | null = null;
|
||||||
const lv = input.toLowerCase()
|
|
||||||
return options.filter((item) => item[0].toLowerCase().includes(lv))
|
|
||||||
}, [options, input])
|
|
||||||
|
|
||||||
const calcMaxHeight = React.useCallback(() => {
|
static defaultProps = {
|
||||||
if (menuRef.current) {
|
onChange: () => {},
|
||||||
const space = window.innerHeight - menuRef.current.getBoundingClientRect().top
|
options: [],
|
||||||
setMaxHeight(Math.min(space, MAX_HEIGHT))
|
}
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const {
|
calcMaxHeight() {
|
||||||
isOpen,
|
if(this.props.keepMenuWithinWindowBounds) {
|
||||||
getMenuProps,
|
const maxHeight = window.innerHeight - this.autocompleteMenuEl!.getBoundingClientRect().top;
|
||||||
getInputProps,
|
const limitedMaxHeight = Math.min(maxHeight, MAX_HEIGHT);
|
||||||
getItemProps,
|
|
||||||
highlightedIndex,
|
if(limitedMaxHeight != this.state.maxHeight) {
|
||||||
openMenu,
|
this.setState({
|
||||||
} = useCombobox({
|
maxHeight: limitedMaxHeight
|
||||||
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
|
|
||||||
},
|
|
||||||
onSelectedItemChange: ({selectedItem}) => {
|
|
||||||
const v = selectedItem ? selectedItem[0] : ''
|
|
||||||
setInput(v)
|
|
||||||
onChange(selectedItem ? selectedItem[0] : undefined)
|
|
||||||
},
|
|
||||||
onInputValueChange: ({inputValue: v}) => {
|
|
||||||
if (typeof v === 'string') {
|
|
||||||
setInput(v)
|
|
||||||
onChange(v === '' ? undefined : v)
|
|
||||||
openMenu()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
calcMaxHeight()
|
|
||||||
}
|
}
|
||||||
}, [isOpen, calcMaxHeight])
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
componentDidMount() {
|
||||||
window.addEventListener('resize', calcMaxHeight)
|
this.calcMaxHeight();
|
||||||
return () => window.removeEventListener('resize', calcMaxHeight)
|
}
|
||||||
}, [calcMaxHeight])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
componentDidUpdate() {
|
||||||
setInput(value || '')
|
this.calcMaxHeight();
|
||||||
}, [value])
|
}
|
||||||
|
|
||||||
return (
|
onChange(v: string) {
|
||||||
<div className="maputnik-autocomplete">
|
this.props.onChange(v === "" ? undefined : v);
|
||||||
<input
|
}
|
||||||
{...getInputProps({
|
|
||||||
'aria-label': ariaLabel,
|
render() {
|
||||||
className: 'maputnik-string',
|
return <div
|
||||||
spellCheck: false,
|
ref={(el) => {
|
||||||
onFocus: () => openMenu(),
|
this.autocompleteMenuEl = el;
|
||||||
})}
|
}}
|
||||||
|
>
|
||||||
|
<Autocomplete
|
||||||
|
menuStyle={{
|
||||||
|
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
|
||||||
|
key={item[0]}
|
||||||
|
className={classnames({
|
||||||
|
"maputnik-autocomplete-menu-item": true,
|
||||||
|
"maputnik-autocomplete-menu-item-selected": isHighlighted,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{item[1]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
{...getMenuProps({}, {suppressRefError: true})}
|
|
||||||
ref={menuRef}
|
|
||||||
style={{position: 'fixed', overflow: 'auto', maxHeight, zIndex: 998}}
|
|
||||||
className="maputnik-autocomplete-menu"
|
|
||||||
>
|
|
||||||
{isOpen &&
|
|
||||||
filteredItems.map((item, index) => (
|
|
||||||
<div
|
|
||||||
key={item[0]}
|
|
||||||
{...getItemProps({
|
|
||||||
item,
|
|
||||||
index,
|
|
||||||
className: classnames('maputnik-autocomplete-menu-item', {
|
|
||||||
'maputnik-autocomplete-menu-item-selected': highlightedIndex === index,
|
|
||||||
}),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item[1]}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -133,3 +133,4 @@ export default class InputColor extends React.Component<InputColorProps> {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -152,3 +152,4 @@ class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps
|
|||||||
</InputButton>
|
</InputButton>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,3 +47,4 @@ export default class InputEnum extends React.Component<InputEnumProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default class InputMultiInput extends React.Component<InputMultiInputProp
|
|||||||
const radios = options.map(([val, label])=> {
|
const radios = options.map(([val, label])=> {
|
||||||
return <label
|
return <label
|
||||||
key={val}
|
key={val}
|
||||||
className={classnames("maputnik-button", "maputnik-radio-as-button", {"maputnik-button-selected": val === selectedValue})}
|
className={classnames("maputnik-radio-as-button", {"maputnik-button-selected": val === selectedValue})}
|
||||||
>
|
>
|
||||||
<input type="radio"
|
<input type="radio"
|
||||||
name={this.props.name}
|
name={this.props.name}
|
||||||
@@ -37,3 +37,5 @@ export default class InputMultiInput extends React.Component<InputMultiInputProp
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -244,3 +244,5 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,3 +30,5 @@ export default class InputSelect extends React.Component<InputSelectProps> {
|
|||||||
</select>
|
</select>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -97,3 +97,5 @@ export default class InputString extends React.Component<InputStringProps, Input
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+88
-142
@@ -1,10 +1,9 @@
|
|||||||
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 {v8} from '@maplibre/maplibre-gl-style-spec';
|
|
||||||
|
|
||||||
import FieldJson from './FieldJson'
|
import FieldJson from './FieldJson'
|
||||||
import FilterEditor from './FilterEditor'
|
import FilterEditor from './FilterEditor'
|
||||||
@@ -18,79 +17,23 @@ import FieldComment from './FieldComment'
|
|||||||
import FieldSource from './FieldSource'
|
import FieldSource from './FieldSource'
|
||||||
import FieldSourceLayer from './FieldSourceLayer'
|
import FieldSourceLayer from './FieldSourceLayer'
|
||||||
import { changeType, changeProperty } from '../libs/layer'
|
import { changeType, changeProperty } from '../libs/layer'
|
||||||
|
import layout from '../config/layout.json'
|
||||||
import {formatLayerId} from '../libs/format';
|
import {formatLayerId} from '../libs/format';
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
import { TFunction } from 'i18next';
|
import { TFunction } from 'i18next';
|
||||||
import { NON_SOURCE_LAYERS } from '../libs/non-source-layers';
|
|
||||||
import { OnMoveLayerCallback } from '../libs/definitions';
|
|
||||||
|
|
||||||
type MaputnikLayoutGroup = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
type: string;
|
|
||||||
fields: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLayoutForSymbolType(t: TFunction): MaputnikLayoutGroup[] {
|
function getLayoutForType(type: LayerSpecification["type"], t: TFunction) {
|
||||||
const groups: MaputnikLayoutGroup[] = [];
|
return layout[type] ? {
|
||||||
groups.push({
|
...layout[type],
|
||||||
title: t("General layout properties"),
|
groups: layout[type].groups.map(group => {
|
||||||
id: "General_layout_properties",
|
return {
|
||||||
type: "properties",
|
...group,
|
||||||
fields: Object.keys(v8["layout_symbol"]).filter(f => f.startsWith("symbol-"))
|
id: group.title.replace(/ /g, "_"),
|
||||||
});
|
title: t(group.title)
|
||||||
groups.push({
|
};
|
||||||
title: t("Text layout properties"),
|
}),
|
||||||
id: "Text_layout_properties",
|
} : layout.invalid;
|
||||||
type: "properties",
|
|
||||||
fields: Object.keys(v8["layout_symbol"]).filter(f => f.startsWith("text-"))
|
|
||||||
});
|
|
||||||
groups.push({
|
|
||||||
title: t("Icon layout properties"),
|
|
||||||
id: "Icon_layout_properties",
|
|
||||||
type: "properties",
|
|
||||||
fields: Object.keys(v8["layout_symbol"]).filter(f => f.startsWith("icon-"))
|
|
||||||
});
|
|
||||||
groups.push({
|
|
||||||
title: t("Text paint properties"),
|
|
||||||
id: "Text_paint_properties",
|
|
||||||
type: "properties",
|
|
||||||
fields: Object.keys(v8["paint_symbol"]).filter(f => f.startsWith("text-"))
|
|
||||||
});
|
|
||||||
groups.push({
|
|
||||||
title: t("Icon paint properties"),
|
|
||||||
id: "Icon_paint_properties",
|
|
||||||
type: "properties",
|
|
||||||
fields: Object.keys(v8["paint_symbol"]).filter(f => f.startsWith("icon-"))
|
|
||||||
});
|
|
||||||
return groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLayoutForType(type: LayerSpecification["type"], t: TFunction): MaputnikLayoutGroup[] {
|
|
||||||
if (Object.keys(v8.layer.type.values).indexOf(type) < 0) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
if (type === "symbol") {
|
|
||||||
return getLayoutForSymbolType(t);
|
|
||||||
}
|
|
||||||
const groups: MaputnikLayoutGroup[] = [];
|
|
||||||
if (Object.keys(v8["paint_" + type]).length > 0) {
|
|
||||||
groups.push({
|
|
||||||
title: t("Paint properties"),
|
|
||||||
id: "Paint_properties",
|
|
||||||
type: "properties",
|
|
||||||
fields: Object.keys(v8["paint_" + type]),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (Object.keys(v8["layout_" + type]).length > 0) {
|
|
||||||
groups.push({
|
|
||||||
title: t("Layout properties"),
|
|
||||||
id: "Layout_properties",
|
|
||||||
type: "properties",
|
|
||||||
fields: Object.keys(v8["layout_" + type])
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return groups;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id: string, title: string, type: string, fields?: string[]}[] {
|
function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id: string, title: string, type: string, fields?: string[]}[] {
|
||||||
@@ -110,18 +53,18 @@ function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id:
|
|||||||
type: 'jsoneditor'
|
type: 'jsoneditor'
|
||||||
}
|
}
|
||||||
return [layerGroup, filterGroup]
|
return [layerGroup, filterGroup]
|
||||||
.concat(getLayoutForType(layerType, t))
|
.concat(getLayoutForType(layerType, t).groups)
|
||||||
.concat([editorGroup])
|
.concat([editorGroup])
|
||||||
}
|
}
|
||||||
|
|
||||||
type LayerEditorInternalProps = {
|
type LayerEditorInternalProps = {
|
||||||
layer: LayerSpecification
|
layer: LayerSpecification
|
||||||
sources: {[key: string]: SourceSpecification & {layers: string[]}}
|
sources: {[key: string]: SourceSpecification}
|
||||||
vectorLayers: {[key: string]: any}
|
vectorLayers: {[key: string]: any}
|
||||||
spec: object
|
spec: object
|
||||||
onLayerChanged(...args: unknown[]): unknown
|
onLayerChanged(...args: unknown[]): unknown
|
||||||
onLayerIdChange(...args: unknown[]): unknown
|
onLayerIdChange(...args: unknown[]): unknown
|
||||||
onMoveLayer: OnMoveLayerCallback
|
onMoveLayer(...args: unknown[]): unknown
|
||||||
onLayerDestroy(...args: unknown[]): unknown
|
onLayerDestroy(...args: unknown[]): unknown
|
||||||
onLayerCopy(...args: unknown[]): unknown
|
onLayerCopy(...args: unknown[]): unknown
|
||||||
onLayerVisibilityToggle(...args: unknown[]): unknown
|
onLayerVisibilityToggle(...args: unknown[]): unknown
|
||||||
@@ -143,13 +86,18 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
onLayerDestroyed: () => {},
|
onLayerDestroyed: () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static childContextTypes = {
|
||||||
|
reactIconBase: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: LayerEditorInternalProps) {
|
constructor(props: LayerEditorInternalProps) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
|
//TODO: Clean this up and refactor into function
|
||||||
const editorGroups: {[keys:string]: boolean} = {}
|
const editorGroups: {[keys:string]: boolean} = {}
|
||||||
for (const group of layoutGroups(this.props.layer.type, props.t)) {
|
layoutGroups(this.props.layer.type, props.t).forEach(group => {
|
||||||
editorGroups[group.title] = true
|
editorGroups[group.title] = true
|
||||||
}
|
})
|
||||||
|
|
||||||
this.state = { editorGroups }
|
this.state = { editorGroups }
|
||||||
}
|
}
|
||||||
@@ -157,17 +105,25 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) {
|
static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) {
|
||||||
const additionalGroups = { ...state.editorGroups }
|
const additionalGroups = { ...state.editorGroups }
|
||||||
|
|
||||||
for (const group of getLayoutForType(props.layer.type, props.t)) {
|
getLayoutForType(props.layer.type, props.t).groups.forEach(group => {
|
||||||
if(!(group.title in additionalGroups)) {
|
if(!(group.title in additionalGroups)) {
|
||||||
additionalGroups[group.title] = true
|
additionalGroups[group.title] = true
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
editorGroups: additionalGroups
|
editorGroups: additionalGroups
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
@@ -209,7 +165,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
let sourceLayerIds;
|
let sourceLayerIds;
|
||||||
const layer = this.props.layer as Exclude<LayerSpecification, BackgroundLayerSpecification>;
|
const layer = this.props.layer as Exclude<LayerSpecification, BackgroundLayerSpecification>;
|
||||||
if(Object.prototype.hasOwnProperty.call(this.props.sources, layer.source)) {
|
if(Object.prototype.hasOwnProperty.call(this.props.sources, layer.source)) {
|
||||||
sourceLayerIds = this.props.sources[layer.source].layers;
|
sourceLayerIds = (this.props.sources[layer.source] as any).layers;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(type) {
|
switch(type) {
|
||||||
@@ -236,7 +192,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
onChange={v => this.changeProperty(null, 'source', v)}
|
onChange={v => this.changeProperty(null, 'source', v)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{!NON_SOURCE_LAYERS.includes(this.props.layer.type) &&
|
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
|
||||||
<FieldSourceLayer
|
<FieldSourceLayer
|
||||||
error={errorData['source-layer']}
|
error={errorData['source-layer']}
|
||||||
sourceLayerIds={sourceLayerIds}
|
sourceLayerIds={sourceLayerIds}
|
||||||
@@ -323,38 +279,30 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
|
|
||||||
const layout = this.props.layer.layout || {}
|
const layout = this.props.layer.layout || {}
|
||||||
|
|
||||||
const items: {[key: string]: {
|
const items: {[key: string]: {text: string, handler: () => void, disabled?: boolean}} = {
|
||||||
text: string,
|
|
||||||
handler: () => void,
|
|
||||||
disabled?: boolean,
|
|
||||||
wdKey?: string
|
|
||||||
}} = {
|
|
||||||
delete: {
|
delete: {
|
||||||
text: t("Delete"),
|
text: t("Delete"),
|
||||||
handler: () => this.props.onLayerDestroy(this.props.layerIndex),
|
handler: () => this.props.onLayerDestroy(this.props.layerIndex)
|
||||||
wdKey: "menu-delete-layer"
|
|
||||||
},
|
},
|
||||||
duplicate: {
|
duplicate: {
|
||||||
text: t("Duplicate"),
|
text: t("Duplicate"),
|
||||||
handler: () => this.props.onLayerCopy(this.props.layerIndex),
|
handler: () => this.props.onLayerCopy(this.props.layerIndex)
|
||||||
wdKey: "menu-duplicate-layer"
|
|
||||||
},
|
},
|
||||||
hide: {
|
hide: {
|
||||||
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
|
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
|
||||||
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex),
|
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
|
||||||
wdKey: "menu-hide-layer"
|
|
||||||
},
|
},
|
||||||
moveLayerUp: {
|
moveLayerUp: {
|
||||||
text: t("Move layer up"),
|
text: t("Move layer up"),
|
||||||
|
// Not actually used...
|
||||||
disabled: this.props.isFirstLayer,
|
disabled: this.props.isFirstLayer,
|
||||||
handler: () => this.moveLayer(-1),
|
handler: () => this.moveLayer(-1)
|
||||||
wdKey: "menu-move-layer-up"
|
|
||||||
},
|
},
|
||||||
moveLayerDown: {
|
moveLayerDown: {
|
||||||
text: t("Move layer down"),
|
text: t("Move layer down"),
|
||||||
|
// Not actually used...
|
||||||
disabled: this.props.isLastLayer,
|
disabled: this.props.isLastLayer,
|
||||||
handler: () => this.moveLayer(+1),
|
handler: () => this.moveLayer(+1)
|
||||||
wdKey: "menu-move-layer-down"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,55 +311,53 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
items[id].handler();
|
items[id].handler();
|
||||||
}
|
}
|
||||||
|
|
||||||
return <IconContext.Provider value={{size: '14px', color: '#8e8e8e'}}>
|
return <section className="maputnik-layer-editor"
|
||||||
<section className="maputnik-layer-editor"
|
role="main"
|
||||||
role="main"
|
aria-label={t("Layer editor")}
|
||||||
aria-label={t("Layer editor")}
|
>
|
||||||
>
|
<header>
|
||||||
<header>
|
<div className="layer-header">
|
||||||
<div className="layer-header">
|
<h2 className="layer-header__title">
|
||||||
<h2 className="layer-header__title">
|
{t("Layer: {{layerId}}", { layerId: formatLayerId(this.props.layer.id) })}
|
||||||
{t("Layer: {{layerId}}", { layerId: formatLayerId(this.props.layer.id) })}
|
</h2>
|
||||||
</h2>
|
<div className="layer-header__info">
|
||||||
<div className="layer-header__info">
|
<Wrapper
|
||||||
<Wrapper
|
className='more-menu'
|
||||||
className='more-menu'
|
onSelection={handleSelection}
|
||||||
onSelection={handleSelection}
|
closeOnSelection={false}
|
||||||
closeOnSelection={false}
|
>
|
||||||
>
|
<Button
|
||||||
<Button
|
id="skip-target-layer-editor"
|
||||||
id="skip-target-layer-editor"
|
data-wd-key="skip-target-layer-editor"
|
||||||
data-wd-key="skip-target-layer-editor"
|
className='more-menu__button'
|
||||||
className='more-menu__button'
|
title={"Layer options"}>
|
||||||
title={"Layer options"}>
|
<MdMoreVert className="more-menu__button__svg" />
|
||||||
<MdMoreVert className="more-menu__button__svg" />
|
</Button>
|
||||||
</Button>
|
<Menu>
|
||||||
<Menu>
|
<ul className="more-menu__menu">
|
||||||
<ul className="more-menu__menu">
|
{Object.keys(items).map((id) => {
|
||||||
{Object.keys(items).map((id) => {
|
const item = items[id];
|
||||||
const item = items[id];
|
return <li key={id}>
|
||||||
return <li key={id}>
|
<MenuItem value={id} className='more-menu__menu__item'>
|
||||||
<MenuItem value={id} className='more-menu__menu__item' data-wd-key={item.wdKey}>
|
{item.text}
|
||||||
{item.text}
|
</MenuItem>
|
||||||
</MenuItem>
|
</li>
|
||||||
</li>
|
})}
|
||||||
})}
|
</ul>
|
||||||
</ul>
|
</Menu>
|
||||||
</Menu>
|
</Wrapper>
|
||||||
</Wrapper>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
<Accordion
|
<Accordion
|
||||||
allowMultipleExpanded={true}
|
allowMultipleExpanded={true}
|
||||||
allowZeroExpanded={true}
|
allowZeroExpanded={true}
|
||||||
preExpanded={groupIds}
|
preExpanded={groupIds}
|
||||||
>
|
>
|
||||||
{groups}
|
{groups}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</section>
|
</section>
|
||||||
</IconContext.Provider>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,16 @@
|
|||||||
import React, {type JSX} from 'react'
|
import React, {type JSX} from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
import {
|
|
||||||
DndContext,
|
|
||||||
PointerSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
closestCenter,
|
|
||||||
DragEndEvent,
|
|
||||||
} from '@dnd-kit/core';
|
|
||||||
import {
|
|
||||||
SortableContext,
|
|
||||||
verticalListSortingStrategy,
|
|
||||||
} from '@dnd-kit/sortable';
|
|
||||||
|
|
||||||
import LayerListGroup from './LayerListGroup'
|
import LayerListGroup from './LayerListGroup'
|
||||||
import LayerListItem from './LayerListItem'
|
import LayerListItem from './LayerListItem'
|
||||||
import ModalAdd from './ModalAdd'
|
import ModalAdd from './ModalAdd'
|
||||||
|
|
||||||
import type {LayerSpecification, SourceSpecification} from 'maplibre-gl';
|
import {SortEndHandler, SortableContainer} from 'react-sortable-hoc';
|
||||||
|
import type {LayerSpecification} from 'maplibre-gl';
|
||||||
import generateUniqueId from '../libs/document-uid';
|
import generateUniqueId from '../libs/document-uid';
|
||||||
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer';
|
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer';
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
import { OnMoveLayerCallback } from '../libs/definitions';
|
|
||||||
|
|
||||||
type LayerListContainerProps = {
|
type LayerListContainerProps = {
|
||||||
layers: LayerSpecification[]
|
layers: LayerSpecification[]
|
||||||
@@ -32,7 +20,7 @@ type LayerListContainerProps = {
|
|||||||
onLayerDestroy?(...args: unknown[]): unknown
|
onLayerDestroy?(...args: unknown[]): unknown
|
||||||
onLayerCopy(...args: unknown[]): unknown
|
onLayerCopy(...args: unknown[]): unknown
|
||||||
onLayerVisibilityToggle(...args: unknown[]): unknown
|
onLayerVisibilityToggle(...args: unknown[]): unknown
|
||||||
sources: Record<string, SourceSpecification & {layers: string[]}>;
|
sources: object
|
||||||
errors: any[]
|
errors: any[]
|
||||||
};
|
};
|
||||||
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
|
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
|
||||||
@@ -254,6 +242,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
|||||||
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1,
|
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1,
|
||||||
'maputnik-layer-list-item--error': !!layerError
|
'maputnik-layer-list-item--error': !!layerError
|
||||||
})}
|
})}
|
||||||
|
index={idx}
|
||||||
key={layer.key}
|
key={layer.key}
|
||||||
id={layer.key}
|
id={layer.key}
|
||||||
layerId={layer.id}
|
layerId={layer.id}
|
||||||
@@ -330,35 +319,20 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LayerListContainer = withTranslation()(LayerListContainerInternal);
|
const LayerListContainer = withTranslation()(LayerListContainerInternal);
|
||||||
|
const LayerListContainerSortable = SortableContainer((props: LayerListContainerProps) => <LayerListContainer {...props} />)
|
||||||
|
|
||||||
type LayerListProps = LayerListContainerProps & {
|
type LayerListProps = LayerListContainerProps & {
|
||||||
onMoveLayer: OnMoveLayerCallback
|
onMoveLayer: SortEndHandler
|
||||||
};
|
};
|
||||||
|
|
||||||
const LayerList: React.FC<LayerListProps> = (props) => {
|
export default class LayerList extends React.Component<LayerListProps> {
|
||||||
const sensors = useSensors(useSensor(PointerSensor));
|
render() {
|
||||||
|
return <LayerListContainerSortable
|
||||||
const handleDragEnd = (event: DragEndEvent) => {
|
{...this.props}
|
||||||
const {active, over} = event;
|
helperClass='sortableHelper'
|
||||||
if (!over) return;
|
onSortEnd={this.props.onMoveLayer.bind(this)}
|
||||||
|
useDragHandle={true}
|
||||||
const oldIndex = props.layers.findIndex(layer => layer.id === active.id);
|
shouldCancelStart={() => false}
|
||||||
const newIndex = props.layers.findIndex(layer => layer.id === over.id);
|
/>
|
||||||
|
}
|
||||||
if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {
|
|
||||||
props.onMoveLayer({oldIndex, newIndex});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const layerIds = props.layers.map(layer => layer.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
||||||
<SortableContext items={layerIds} strategy={verticalListSortingStrategy}>
|
|
||||||
<LayerListContainer {...props} />
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LayerList;
|
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
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 {useSortable} from '@dnd-kit/sortable'
|
|
||||||
import {CSS} from '@dnd-kit/utilities'
|
|
||||||
|
|
||||||
import IconLayer from './IconLayer'
|
import IconLayer from './IconLayer'
|
||||||
|
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
||||||
|
|
||||||
|
|
||||||
type DraggableLabelProps = {
|
type DraggableLabelProps = {
|
||||||
layerId: string
|
layerId: string
|
||||||
layerType: string
|
layerType: string
|
||||||
dragAttributes?: React.HTMLAttributes<HTMLElement>
|
|
||||||
dragListeners?: React.HTMLAttributes<HTMLElement>
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DraggableLabel: React.FC<DraggableLabelProps> = (props) => {
|
const DraggableLabel = SortableHandle((props: DraggableLabelProps) => {
|
||||||
const {dragAttributes, dragListeners} = props;
|
return <div className="maputnik-layer-list-item-handle">
|
||||||
return <div className="maputnik-layer-list-item-handle" {...dragAttributes} {...dragListeners}>
|
|
||||||
<IconLayer
|
<IconLayer
|
||||||
className="layer-handle__icon"
|
className="layer-handle__icon"
|
||||||
type={props.layerType}
|
type={props.layerType}
|
||||||
@@ -26,7 +23,7 @@ const DraggableLabel: React.FC<DraggableLabelProps> = (props) => {
|
|||||||
{props.layerId}
|
{props.layerId}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
};
|
});
|
||||||
|
|
||||||
type IconActionProps = {
|
type IconActionProps = {
|
||||||
action: string
|
action: string
|
||||||
@@ -85,80 +82,63 @@ type LayerListItemProps = {
|
|||||||
onLayerVisibilityToggle?(...args: unknown[]): unknown
|
onLayerVisibilityToggle?(...args: unknown[]): unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((props, ref) => {
|
class LayerListItem extends React.Component<LayerListItemProps> {
|
||||||
const {
|
static defaultProps = {
|
||||||
isSelected = false,
|
isSelected: false,
|
||||||
visibility = 'visible',
|
visibility: 'visible',
|
||||||
onLayerCopy = () => {},
|
onLayerCopy: () => {},
|
||||||
onLayerDestroy = () => {},
|
onLayerDestroy: () => {},
|
||||||
onLayerVisibilityToggle = () => {},
|
onLayerVisibilityToggle: () => {},
|
||||||
} = props;
|
}
|
||||||
|
|
||||||
const {
|
static childContextTypes = {
|
||||||
attributes,
|
reactIconBase: PropTypes.object
|
||||||
listeners,
|
}
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
isDragging,
|
|
||||||
} = useSortable({id: props.layerId});
|
|
||||||
|
|
||||||
const style = {
|
getChildContext() {
|
||||||
transform: CSS.Transform.toString(transform),
|
return {
|
||||||
transition,
|
reactIconBase: { size: 14 }
|
||||||
opacity: isDragging ? 0.5 : 1,
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const visibilityAction = visibility === 'visible' ? 'show' : 'hide';
|
render() {
|
||||||
|
const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide';
|
||||||
|
|
||||||
// Cast ref to MutableRefObject since we know from the codebase that's what's always passed
|
return <li
|
||||||
const refObject = ref as React.MutableRefObject<HTMLLIElement | null> | null;
|
id={this.props.id}
|
||||||
|
key={this.props.layerId}
|
||||||
return <IconContext.Provider value={{size: '14px'}}>
|
onClick={_e => this.props.onLayerSelect(this.props.layerIndex)}
|
||||||
<li
|
data-wd-key={"layer-list-item:"+this.props.layerId}
|
||||||
ref={(node) => {
|
|
||||||
setNodeRef(node);
|
|
||||||
if (refObject) {
|
|
||||||
refObject.current = node;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={style}
|
|
||||||
id={props.id}
|
|
||||||
onClick={_e => props.onLayerSelect(props.layerIndex)}
|
|
||||||
data-wd-key={"layer-list-item:" + props.layerId}
|
|
||||||
className={classnames({
|
className={classnames({
|
||||||
"maputnik-layer-list-item": true,
|
"maputnik-layer-list-item": true,
|
||||||
"maputnik-layer-list-item-selected": isSelected,
|
"maputnik-layer-list-item-selected": this.props.isSelected,
|
||||||
[props.className!]: true,
|
[this.props.className!]: true,
|
||||||
})}>
|
})}>
|
||||||
<DraggableLabel
|
<DraggableLabel {...this.props} />
|
||||||
layerId={props.layerId}
|
|
||||||
layerType={props.layerType}
|
|
||||||
dragAttributes={attributes}
|
|
||||||
dragListeners={listeners}
|
|
||||||
/>
|
|
||||||
<span style={{flexGrow: 1}} />
|
<span style={{flexGrow: 1}} />
|
||||||
<IconAction
|
<IconAction
|
||||||
wdKey={"layer-list-item:" + props.layerId+":delete"}
|
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
|
||||||
action={'delete'}
|
action={'delete'}
|
||||||
classBlockName="delete"
|
classBlockName="delete"
|
||||||
onClick={_e => onLayerDestroy!(props.layerIndex)}
|
onClick={_e => this.props.onLayerDestroy!(this.props.layerIndex)}
|
||||||
/>
|
/>
|
||||||
<IconAction
|
<IconAction
|
||||||
wdKey={"layer-list-item:" + props.layerId+":copy"}
|
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
|
||||||
action={'duplicate'}
|
action={'duplicate'}
|
||||||
classBlockName="duplicate"
|
classBlockName="duplicate"
|
||||||
onClick={_e => onLayerCopy!(props.layerIndex)}
|
onClick={_e => this.props.onLayerCopy!(this.props.layerIndex)}
|
||||||
/>
|
/>
|
||||||
<IconAction
|
<IconAction
|
||||||
wdKey={"layer-list-item:"+props.layerId+":toggle-visibility"}
|
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
|
||||||
action={visibilityAction}
|
action={visibilityAction}
|
||||||
classBlockName="visibility"
|
classBlockName="visibility"
|
||||||
classBlockModifier={visibilityAction}
|
classBlockModifier={visibilityAction}
|
||||||
onClick={_e => onLayerVisibilityToggle!(props.layerIndex)}
|
onClick={_e => this.props.onLayerVisibilityToggle!(this.props.layerIndex)}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</IconContext.Provider>
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
export default LayerListItem;
|
const LayerListItemSortable = SortableElement<LayerListItemProps>((props: LayerListItemProps) => <LayerListItem {...props} />);
|
||||||
|
|
||||||
|
export default LayerListItemSortable;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, {type JSX} from 'react'
|
import React, {type JSX} from 'react'
|
||||||
import {createRoot} from 'react-dom/client'
|
import ReactDOM from 'react-dom'
|
||||||
import MapLibreGl, {LayerSpecification, LngLat, Map, MapOptions, SourceSpecification, StyleSpecification} from 'maplibre-gl'
|
import MapLibreGl, {LayerSpecification, LngLat, Map, MapOptions, SourceSpecification, StyleSpecification} from 'maplibre-gl'
|
||||||
import MaplibreInspect from '@maplibre/maplibre-gl-inspect'
|
import MaplibreInspect from '@maplibre/maplibre-gl-inspect'
|
||||||
import colors from '@maplibre/maplibre-gl-inspect/lib/colors'
|
import colors from '@maplibre/maplibre-gl-inspect/lib/colors'
|
||||||
@@ -17,14 +17,8 @@ import { withTranslation, WithTranslation } from 'react-i18next'
|
|||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
import { Protocol } from "pmtiles";
|
import { Protocol } from "pmtiles";
|
||||||
|
|
||||||
function renderPopup(
|
function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container): HTMLElement {
|
||||||
popupElement: JSX.Element,
|
ReactDOM.render(popup, mountNode);
|
||||||
mountNode: HTMLElement,
|
|
||||||
popup: MapLibreGl.Popup
|
|
||||||
): HTMLElement {
|
|
||||||
const root = createRoot(mountNode);
|
|
||||||
popup.once('close', () => root.unmount());
|
|
||||||
root.render(popupElement);
|
|
||||||
return mountNode as HTMLElement;
|
return mountNode as HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,12 +174,10 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
|||||||
|
|
||||||
const tmpNode = document.createElement('div');
|
const tmpNode = document.createElement('div');
|
||||||
|
|
||||||
const inspectPopup = new MapLibreGl.Popup({
|
|
||||||
closeOnClick: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const inspect = new MaplibreInspect({
|
const inspect = new MaplibreInspect({
|
||||||
popup: inspectPopup,
|
popup: new MapLibreGl.Popup({
|
||||||
|
closeOnClick: false
|
||||||
|
}),
|
||||||
showMapPopup: true,
|
showMapPopup: true,
|
||||||
showMapPopupOnHover: false,
|
showMapPopupOnHover: false,
|
||||||
showInspectMapPopupOnHover: true,
|
showInspectMapPopupOnHover: true,
|
||||||
@@ -197,21 +189,9 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
|||||||
buildInspectStyle: (originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[]) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
|
buildInspectStyle: (originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[]) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
|
||||||
renderPopup: (features: InspectFeature[]) => {
|
renderPopup: (features: InspectFeature[]) => {
|
||||||
if(this.props.inspectModeEnabled) {
|
if(this.props.inspectModeEnabled) {
|
||||||
return renderPopup(
|
return renderPopup(<MapMaplibreGlFeaturePropertyPopup features={features} />, tmpNode);
|
||||||
<MapMaplibreGlFeaturePropertyPopup features={features} />,
|
|
||||||
tmpNode,
|
|
||||||
inspectPopup
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return renderPopup(
|
return renderPopup(<MapMaplibreGlLayerPopup features={features} onLayerSelect={this.onLayerSelectById} zoom={this.state.zoom} />, tmpNode);
|
||||||
<MapMaplibreGlLayerPopup
|
|
||||||
features={features}
|
|
||||||
onLayerSelect={this.onLayerSelectById}
|
|
||||||
zoom={this.state.zoom}
|
|
||||||
/>,
|
|
||||||
tmpNode,
|
|
||||||
inspectPopup
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+42
-56
@@ -6,9 +6,8 @@ import FieldType from './FieldType'
|
|||||||
import FieldId from './FieldId'
|
import FieldId from './FieldId'
|
||||||
import FieldSource from './FieldSource'
|
import FieldSource from './FieldSource'
|
||||||
import FieldSourceLayer from './FieldSourceLayer'
|
import FieldSourceLayer from './FieldSourceLayer'
|
||||||
import type {LayerSpecification, SourceSpecification} from 'maplibre-gl'
|
import type {LayerSpecification} from 'maplibre-gl'
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
import { NON_SOURCE_LAYERS } from '../libs/non-source-layers'
|
|
||||||
|
|
||||||
type ModalAddInternalProps = {
|
type ModalAddInternalProps = {
|
||||||
layers: LayerSpecification[]
|
layers: LayerSpecification[]
|
||||||
@@ -16,7 +15,7 @@ type ModalAddInternalProps = {
|
|||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onOpenToggle(open: boolean): unknown
|
onOpenToggle(open: boolean): unknown
|
||||||
// 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: any
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
type ModalAddState = {
|
type ModalAddState = {
|
||||||
@@ -24,16 +23,10 @@ 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,
|
||||||
@@ -42,16 +35,15 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
|||||||
|
|
||||||
if(this.state.type !== 'background') {
|
if(this.state.type !== 'background') {
|
||||||
layer.source = this.state.source
|
layer.source = this.state.source
|
||||||
if(!NON_SOURCE_LAYERS.includes(this.state.type) && this.state['source-layer']) {
|
if(this.state.type !== 'raster' && this.state['source-layer']) {
|
||||||
layer['source-layer'] = this.state['source-layer']
|
layer['source-layer'] = this.state['source-layer']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -59,15 +51,11 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
|||||||
const state: ModalAddState = {
|
const state: ModalAddState = {
|
||||||
type: 'fill',
|
type: 'fill',
|
||||||
id: '',
|
id: '',
|
||||||
error: null,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(Object.keys(props.sources).length > 0) {
|
if(props.sources.length > 0) {
|
||||||
state.source = Object.keys(this.props.sources)[0];
|
state.source = Object.keys(this.props.sources)[0];
|
||||||
const sourceLayers = this.props.sources[state.source].layers || []
|
state['source-layer'] = this.props.sources[state.source as keyof ModalAddInternalProps["sources"]][0]
|
||||||
if (sourceLayers.length > 0) {
|
|
||||||
state['source-layer'] = sourceLayers[0];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.state = state;
|
this.state = state;
|
||||||
}
|
}
|
||||||
@@ -101,26 +89,39 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
|||||||
return sourceObj.layers || [];
|
return sourceObj.layers || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getSources(type: LayerSpecification["type"]) {
|
getSources(type: string) {
|
||||||
|
const sources = [];
|
||||||
|
|
||||||
switch(type) {
|
const types = {
|
||||||
case 'background':
|
vector: [
|
||||||
return [];
|
"fill",
|
||||||
case 'hillshade':
|
"line",
|
||||||
case 'color-relief':
|
"symbol",
|
||||||
return Object.entries(this.props.sources).filter(([_, v]) => v.type === 'raster-dem').map(([k, _]) => k);
|
"circle",
|
||||||
case 'raster':
|
"fill-extrusion",
|
||||||
return Object.entries(this.props.sources).filter(([_, v]) => v.type === 'raster').map(([k, _]) => k);
|
"heatmap"
|
||||||
case 'heatmap':
|
],
|
||||||
case 'circle':
|
raster: [
|
||||||
case 'fill':
|
"raster"
|
||||||
case 'fill-extrusion':
|
],
|
||||||
case 'line':
|
geojson: [
|
||||||
case 'symbol':
|
"fill",
|
||||||
return Object.entries(this.props.sources).filter(([_, v]) => v.type === 'vector' || v.type === 'geojson').map(([k, _]) => k);
|
"line",
|
||||||
default:
|
"symbol",
|
||||||
return [];
|
"circle",
|
||||||
|
"fill-extrusion",
|
||||||
|
"heatmap"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for(const [key, val] of Object.entries(this.props.sources) as any) {
|
||||||
|
const valType = val.type as keyof typeof types;
|
||||||
|
if(types[valType] && types[valType].indexOf(type) > -1) {
|
||||||
|
sources.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -128,21 +129,6 @@ 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}
|
||||||
@@ -151,13 +137,12 @@ 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, error: null })
|
this.setState({ id: v })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FieldType
|
<FieldType
|
||||||
@@ -173,8 +158,9 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
|||||||
onChange={(v: string) => this.setState({ source: v })}
|
onChange={(v: string) => this.setState({ source: v })}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{!NON_SOURCE_LAYERS.includes(this.state.type) &&
|
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
|
||||||
<FieldSourceLayer
|
<FieldSourceLayer
|
||||||
|
isFixed={true}
|
||||||
sourceLayerIds={layers}
|
sourceLayerIds={layers}
|
||||||
value={this.state['source-layer']}
|
value={this.state['source-layer']}
|
||||||
onChange={(v: string) => this.setState({ 'source-layer': v })}
|
onChange={(v: string) => this.setState({ 'source-layer': v })}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Slugify from 'slugify'
|
|||||||
import {saveAs} from 'file-saver'
|
import {saveAs} from 'file-saver'
|
||||||
import {version} from 'maplibre-gl/package.json'
|
import {version} from 'maplibre-gl/package.json'
|
||||||
import {format} from '@maplibre/maplibre-gl-style-spec'
|
import {format} from '@maplibre/maplibre-gl-style-spec'
|
||||||
|
import type {StyleSpecification} from 'maplibre-gl'
|
||||||
import {MdMap, MdSave} from 'react-icons/md'
|
import {MdMap, MdSave} from 'react-icons/md'
|
||||||
import {WithTranslation, withTranslation} from 'react-i18next';
|
import {WithTranslation, withTranslation} from 'react-i18next';
|
||||||
|
|
||||||
@@ -11,7 +12,6 @@ import InputButton from './InputButton'
|
|||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
import style from '../libs/style'
|
import style from '../libs/style'
|
||||||
import fieldSpecAdditional from '../libs/field-spec-additional'
|
import fieldSpecAdditional from '../libs/field-spec-additional'
|
||||||
import type {OnStyleChangedCallback, StyleSpecificationWithId} from '../libs/definitions'
|
|
||||||
|
|
||||||
|
|
||||||
const MAPLIBRE_GL_VERSION = version;
|
const MAPLIBRE_GL_VERSION = version;
|
||||||
@@ -19,8 +19,8 @@ const showSaveFilePickerAvailable = typeof window.showSaveFilePicker === "functi
|
|||||||
|
|
||||||
|
|
||||||
type ModalExportInternalProps = {
|
type ModalExportInternalProps = {
|
||||||
mapStyle: StyleSpecificationWithId
|
mapStyle: StyleSpecification & { id: string }
|
||||||
onStyleChanged: OnStyleChangedCallback
|
onStyleChanged(...args: unknown[]): unknown
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onOpenToggle(...args: unknown[]): unknown
|
onOpenToggle(...args: unknown[]): unknown
|
||||||
onSetFileHandle(fileHandle: FileSystemFileHandle | null): unknown
|
onSetFileHandle(fileHandle: FileSystemFileHandle | null): unknown
|
||||||
@@ -186,12 +186,6 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
|||||||
value={(this.props.mapStyle.metadata || {} as any)['maputnik:stadia_access_token']}
|
value={(this.props.mapStyle.metadata || {} as any)['maputnik:stadia_access_token']}
|
||||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:stadia_access_token")}
|
onChange={this.changeMetadataProperty.bind(this, "maputnik:stadia_access_token")}
|
||||||
/>
|
/>
|
||||||
<FieldString
|
|
||||||
label={fsa.maputnik.locationiq_access_token.label}
|
|
||||||
fieldSpec={fsa.maputnik.locationiq_access_token}
|
|
||||||
value={(this.props.mapStyle.metadata || {} as any)['maputnik:locationiq_access_token']}
|
|
||||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:locationiq_access_token")}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="maputnik-modal-export-buttons">
|
<div className="maputnik-modal-export-buttons">
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ import FieldEnum from './FieldEnum'
|
|||||||
import FieldColor from './FieldColor'
|
import FieldColor from './FieldColor'
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
import fieldSpecAdditional from '../libs/field-spec-additional'
|
import fieldSpecAdditional from '../libs/field-spec-additional'
|
||||||
import type {OnStyleChangedCallback, StyleSpecificationWithId} from '../libs/definitions';
|
|
||||||
|
|
||||||
type ModalSettingsInternalProps = {
|
type ModalSettingsInternalProps = {
|
||||||
mapStyle: StyleSpecificationWithId
|
mapStyle: StyleSpecification
|
||||||
onStyleChanged: OnStyleChangedCallback
|
onStyleChanged(...args: unknown[]): unknown
|
||||||
onChangeMetadataProperty(...args: unknown[]): unknown
|
onChangeMetadataProperty(...args: unknown[]): unknown
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onOpenToggle(...args: unknown[]): unknown
|
onOpenToggle(...args: unknown[]): unknown
|
||||||
@@ -63,7 +62,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
changeTerrainProperty(property: keyof TerrainSpecification, value: any) {
|
changeTerrainProperty(property: keyof TerrainSpecification, value: any) {
|
||||||
const terrain = {
|
const terrain = {
|
||||||
...this.props.mapStyle.terrain,
|
...this.props.mapStyle.terrain,
|
||||||
} as TerrainSpecification;
|
}
|
||||||
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
delete terrain[property];
|
delete terrain[property];
|
||||||
@@ -165,14 +164,6 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
onChange={onChangeMetadataProperty.bind(this, "maputnik:stadia_access_token")}
|
onChange={onChangeMetadataProperty.bind(this, "maputnik:stadia_access_token")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldString
|
|
||||||
label={fsa.maputnik.locationiq_access_token.label}
|
|
||||||
fieldSpec={fsa.maputnik.locationiq_access_token}
|
|
||||||
data-wd-key="modal:settings.maputnik:locationiq_access_token"
|
|
||||||
value={metadata['maputnik:locationiq_access_token']}
|
|
||||||
onChange={onChangeMetadataProperty.bind(this, "maputnik:locationiq_access_token")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FieldArray
|
<FieldArray
|
||||||
label={t("Center")}
|
label={t("Center")}
|
||||||
fieldSpec={latest.$root.center}
|
fieldSpec={latest.$root.center}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {MdAddCircleOutline, MdDelete} from 'react-icons/md'
|
import {MdAddCircleOutline, MdDelete} from 'react-icons/md'
|
||||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||||
import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, VectorSourceSpecification} from 'maplibre-gl'
|
import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, StyleSpecification, VectorSourceSpecification} from 'maplibre-gl'
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
@@ -13,7 +13,6 @@ import ModalSourcesTypeEditor, { EditorMode } from './ModalSourcesTypeEditor'
|
|||||||
import style from '../libs/style'
|
import style from '../libs/style'
|
||||||
import { deleteSource, addSource, changeSource } from '../libs/source'
|
import { deleteSource, addSource, changeSource } from '../libs/source'
|
||||||
import publicSources from '../config/tilesets.json'
|
import publicSources from '../config/tilesets.json'
|
||||||
import { OnStyleChangedCallback, StyleSpecificationWithId } from '../libs/definitions';
|
|
||||||
|
|
||||||
|
|
||||||
type PublicSourceProps = {
|
type PublicSourceProps = {
|
||||||
@@ -271,10 +270,10 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ModalSourcesInternalProps = {
|
type ModalSourcesInternalProps = {
|
||||||
mapStyle: StyleSpecificationWithId
|
mapStyle: StyleSpecification
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onOpenToggle(...args: unknown[]): unknown
|
onOpenToggle(...args: unknown[]): unknown
|
||||||
onStyleChanged: OnStyleChangedCallback
|
onStyleChanged(...args: unknown[]): unknown
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
|
class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ export default class ScrollContainer extends React.Component<ScrollContainerProp
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,3 +90,4 @@ export default class SingleFilterEditor extends React.Component<SingleFilterEdit
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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'
|
||||||
@@ -19,25 +20,28 @@ export type SpecFieldProps = InputFieldSpecProps & {
|
|||||||
name?: string
|
name?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
const SpecField: React.FC<SpecFieldProps> = (props) => {
|
export default class SpecField extends React.Component<SpecFieldProps> {
|
||||||
const fieldType = props.fieldSpec?.type;
|
render() {
|
||||||
|
const fieldType = this.props.fieldSpec?.type;
|
||||||
|
|
||||||
const typeBlockFn = typeMap[fieldType!];
|
const typeBlockFn = typeMap[fieldType!];
|
||||||
|
|
||||||
let TypeBlock;
|
let TypeBlock;
|
||||||
if (typeBlockFn) {
|
if (typeBlockFn) {
|
||||||
TypeBlock = typeBlockFn(props);
|
TypeBlock = typeBlockFn(this.props);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.warn("No such type for '%s'", fieldType);
|
console.warn("No such type for '%s'", fieldType);
|
||||||
TypeBlock = Block;
|
TypeBlock = Block;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <TypeBlock
|
||||||
<TypeBlock label={props.label} action={props.action} fieldSpec={props.fieldSpec}>
|
label={this.props.label}
|
||||||
<InputSpec {...props} />
|
action={this.props.action}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
>
|
||||||
|
<InputSpec {...this.props} />
|
||||||
</TypeBlock>
|
</TypeBlock>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default SpecField;
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
|
|||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
let makeZoomInputButton, makeDataInputButton, expressionInputButton;
|
let makeZoomInputButton, makeDataInputButton, expressionInputButton;
|
||||||
|
|
||||||
if (this.props.fieldSpec.expression?.parameters.includes('zoom')) {
|
if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
|
||||||
expressionInputButton = (
|
expressionInputButton = (
|
||||||
<InputButton
|
<InputButton
|
||||||
className="maputnik-make-zoom-function"
|
className="maputnik-make-zoom-function"
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
{
|
||||||
|
"line": {
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"title": "Paint properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"line-opacity",
|
||||||
|
"line-color",
|
||||||
|
"line-width",
|
||||||
|
"line-offset",
|
||||||
|
"line-blur",
|
||||||
|
"line-dasharray",
|
||||||
|
"line-pattern",
|
||||||
|
"line-translate",
|
||||||
|
"line-translate-anchor",
|
||||||
|
"line-gap-width"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Layout properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"line-cap",
|
||||||
|
"line-join",
|
||||||
|
"line-miter-limit",
|
||||||
|
"line-round-limit"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"title": "Paint properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"background-color",
|
||||||
|
"background-pattern",
|
||||||
|
"background-opacity"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fill": {
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"title": "Paint properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"fill-opacity",
|
||||||
|
"fill-color",
|
||||||
|
"fill-antialias",
|
||||||
|
"fill-outline-color",
|
||||||
|
"fill-pattern",
|
||||||
|
"fill-translate",
|
||||||
|
"fill-translate-anchor"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fill-extrusion": {
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"title": "Paint properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"fill-extrusion-opacity",
|
||||||
|
"fill-extrusion-color",
|
||||||
|
"fill-extrusion-translate",
|
||||||
|
"fill-extrusion-translate-anchor",
|
||||||
|
"fill-extrusion-pattern",
|
||||||
|
"fill-extrusion-height",
|
||||||
|
"fill-extrusion-base",
|
||||||
|
"fill-extrusion-vertical-gradient"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"circle": {
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"title": "Paint properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"circle-color",
|
||||||
|
"circle-opacity",
|
||||||
|
"circle-stroke-color",
|
||||||
|
"circle-stroke-opacity",
|
||||||
|
"circle-blur",
|
||||||
|
"circle-radius",
|
||||||
|
"circle-stroke-width",
|
||||||
|
"circle-pitch-scale",
|
||||||
|
"circle-translate",
|
||||||
|
"circle-translate-anchor",
|
||||||
|
"circle-pitch-alignment"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"title": "General layout properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"symbol-placement",
|
||||||
|
"symbol-spacing",
|
||||||
|
"symbol-avoid-edges",
|
||||||
|
"symbol-z-order"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Text layout properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"text-field",
|
||||||
|
"text-font",
|
||||||
|
"text-size",
|
||||||
|
"text-line-height",
|
||||||
|
"text-padding",
|
||||||
|
"text-allow-overlap",
|
||||||
|
"text-ignore-placement",
|
||||||
|
"text-pitch-alignment",
|
||||||
|
"text-rotation-alignment",
|
||||||
|
"text-max-width",
|
||||||
|
"text-letter-spacing",
|
||||||
|
"text-justify",
|
||||||
|
"text-anchor",
|
||||||
|
"text-max-angle",
|
||||||
|
"text-writing-mode",
|
||||||
|
"text-rotate",
|
||||||
|
"text-keep-upright",
|
||||||
|
"text-transform",
|
||||||
|
"text-offset",
|
||||||
|
"text-optional",
|
||||||
|
"text-variable-anchor",
|
||||||
|
"text-radial-offset"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Icon layout properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"icon-image",
|
||||||
|
"icon-allow-overlap",
|
||||||
|
"icon-ignore-placement",
|
||||||
|
"icon-optional",
|
||||||
|
"icon-rotation-alignment",
|
||||||
|
"icon-size",
|
||||||
|
"icon-text-fit",
|
||||||
|
"icon-text-fit-padding",
|
||||||
|
"icon-rotate",
|
||||||
|
"icon-padding",
|
||||||
|
"icon-keep-upright",
|
||||||
|
"icon-offset",
|
||||||
|
"icon-anchor",
|
||||||
|
"icon-pitch-alignment"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Text paint properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"text-color",
|
||||||
|
"text-opacity",
|
||||||
|
"text-halo-color",
|
||||||
|
"text-halo-width",
|
||||||
|
"text-halo-blur",
|
||||||
|
"text-translate",
|
||||||
|
"text-translate-anchor"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Icon paint properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"icon-color",
|
||||||
|
"icon-opacity",
|
||||||
|
"icon-halo-color",
|
||||||
|
"icon-halo-width",
|
||||||
|
"icon-halo-blur",
|
||||||
|
"icon-translate",
|
||||||
|
"icon-translate-anchor"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"raster": {
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"title": "Paint properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"raster-opacity",
|
||||||
|
"raster-hue-rotate",
|
||||||
|
"raster-brightness-min",
|
||||||
|
"raster-brightness-max",
|
||||||
|
"raster-saturation",
|
||||||
|
"raster-contrast",
|
||||||
|
"raster-fade-duration",
|
||||||
|
"raster-resampling"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hillshade": {
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"title": "Paint properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"hillshade-illumination-direction",
|
||||||
|
"hillshade-illumination-anchor",
|
||||||
|
"hillshade-exaggeration",
|
||||||
|
"hillshade-shadow-color",
|
||||||
|
"hillshade-highlight-color",
|
||||||
|
"hillshade-accent-color"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"heatmap": {
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"title": "Paint properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"heatmap-radius",
|
||||||
|
"heatmap-weight",
|
||||||
|
"heatmap-intensity",
|
||||||
|
"heatmap-opacity"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"invalid": {
|
||||||
|
"groups": []
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
-19
@@ -11,30 +11,12 @@
|
|||||||
"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",
|
||||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.9/style.json",
|
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.9/style.json",
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
|
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "locationiq-streets",
|
|
||||||
"title": "LocationIQ Streets",
|
|
||||||
"url": "https://tiles.locationiq.com/v3/streets/vector.json?key={key}",
|
|
||||||
"thumbnail": "https://static-assets.locationiq.com/maputnik/locationiq-streets.png"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "maptiler-basic-gl-style",
|
"id": "maptiler-basic-gl-style",
|
||||||
"title": "MapTiler Basic",
|
"title": "MapTiler Basic",
|
||||||
@@ -47,6 +29,30 @@
|
|||||||
"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",
|
||||||
@@ -86,7 +92,7 @@
|
|||||||
{
|
{
|
||||||
"id": "versatiles-colorful",
|
"id": "versatiles-colorful",
|
||||||
"title": "Versatiles Colorful",
|
"title": "Versatiles Colorful",
|
||||||
"url": "https://tiles.versatiles.org/assets/styles/colorful/style.json",
|
"url": "https://tiles.versatiles.org/assets/styles/colorful.json",
|
||||||
"thumbnail": "https://github.com/maplibre/maputnik/assets/649392/6cd69818-c541-46e4-a920-65fb4f654931"
|
"thumbnail": "https://github.com/maplibre/maputnik/assets/649392/6cd69818-c541-46e4-a920-65fb4f654931"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -18,10 +18,5 @@
|
|||||||
"type": "vector",
|
"type": "vector",
|
||||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/data/vector/open-zoomstack/config.json",
|
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/data/vector/open-zoomstack/config.json",
|
||||||
"title": "OS Open Zoomstack v2"
|
"title": "OS Open Zoomstack v2"
|
||||||
},
|
|
||||||
"locationiq": {
|
|
||||||
"type": "vector",
|
|
||||||
"url": "https://tiles.locationiq.com/v3/pbf/tiles.json?key={key}",
|
|
||||||
"title": "LocationIQ"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"openmaptiles": "get_your_own_OpIi9ZULNHzrESv6T2vL",
|
"openmaptiles": "get_your_own_OpIi9ZULNHzrESv6T2vL",
|
||||||
"thunderforest": "b71f7f0ba4064f5eb9e903859a9cf5c6",
|
"thunderforest": "b71f7f0ba4064f5eb9e903859a9cf5c6"
|
||||||
"locationiq": "pk.put_your_api_key_here7bb23dffeb4"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,46 @@
|
|||||||
import style from '../style'
|
import style from './style.js'
|
||||||
import {format} from '@maplibre/maplibre-gl-style-spec'
|
import {format} from '@maplibre/maplibre-gl-style-spec'
|
||||||
|
import type {StyleSpecification} from 'maplibre-gl'
|
||||||
import ReconnectingWebSocket from 'reconnecting-websocket'
|
import ReconnectingWebSocket from 'reconnecting-websocket'
|
||||||
import type {IStyleStore, OnStyleChangedCallback, StyleSpecificationWithId} from '../definitions'
|
|
||||||
|
|
||||||
export type ApiStyleStoreOptions = {
|
export type ApiStyleStoreOptions = {
|
||||||
onLocalStyleChange?: OnStyleChangedCallback
|
port: string | null
|
||||||
|
host: string | null
|
||||||
|
onLocalStyleChange?: (style: any) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ApiStyleStore implements IStyleStore {
|
export class ApiStyleStore {
|
||||||
|
|
||||||
localUrl: string;
|
localUrl: string;
|
||||||
websocketUrl: string;
|
websocketUrl: string;
|
||||||
latestStyleId: string | undefined = undefined;
|
latestStyleId: string | undefined = undefined;
|
||||||
onLocalStyleChange: OnStyleChangedCallback;
|
onLocalStyleChange: (style: any) => void;
|
||||||
|
|
||||||
constructor(opts: ApiStyleStoreOptions) {
|
constructor(opts: ApiStyleStoreOptions) {
|
||||||
this.onLocalStyleChange = opts.onLocalStyleChange || (() => {})
|
this.onLocalStyleChange = opts.onLocalStyleChange || (() => {})
|
||||||
const port = window.location.port
|
const port = opts.port || '8000'
|
||||||
const host = 'localhost'
|
const host = opts.host || 'localhost'
|
||||||
this.localUrl = `http://${host}:${port}`
|
this.localUrl = `http://${host}:${port}`
|
||||||
this.websocketUrl = `ws://${host}:${port}/ws`
|
this.websocketUrl = `ws://${host}:${port}/ws`
|
||||||
this.init = this.init.bind(this)
|
this.init = this.init.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
init(cb: (...args: any[]) => void) {
|
||||||
try {
|
fetch(this.localUrl + '/styles', {
|
||||||
const response = await fetch(this.localUrl + '/styles', {mode: 'cors'});
|
mode: 'cors',
|
||||||
const body = await response.json();
|
})
|
||||||
const styleIds = body;
|
.then((response) => {
|
||||||
this.latestStyleId = styleIds[0]
|
return response.json();
|
||||||
this.notifyLocalChanges();
|
})
|
||||||
} catch {
|
.then((body) => {
|
||||||
throw new Error('Can not connect to style API');
|
const styleIds = body;
|
||||||
}
|
this.latestStyleId = styleIds[0]
|
||||||
|
this.notifyLocalChanges()
|
||||||
|
cb(null)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
cb(new Error('Can not connect to style API'))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyLocalChanges() {
|
notifyLocalChanges() {
|
||||||
@@ -51,20 +59,24 @@ export class ApiStyleStore implements IStyleStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLatestStyle(): Promise<StyleSpecificationWithId> {
|
latestStyle(cb: (...args: any[]) => void) {
|
||||||
if(this.latestStyleId) {
|
if(this.latestStyleId) {
|
||||||
const response = await fetch(this.localUrl + '/styles/' + this.latestStyleId, {
|
fetch(this.localUrl + '/styles/' + this.latestStyleId, {
|
||||||
mode: 'cors',
|
mode: 'cors',
|
||||||
});
|
})
|
||||||
const body = await response.json();
|
.then(function(response) {
|
||||||
return style.ensureStyleValidity(body);
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(body) {
|
||||||
|
cb(style.ensureStyleValidity(body))
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No latest style available. You need to init the api backend first.')
|
throw new Error('No latest style available. You need to init the api backend first.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save current style replacing previous version
|
// Save current style replacing previous version
|
||||||
save(mapStyle: StyleSpecificationWithId) {
|
save(mapStyle: StyleSpecification & { id: string }) {
|
||||||
const styleJSON = format(
|
const styleJSON = format(
|
||||||
style.stripAccessTokens(
|
style.stripAccessTokens(
|
||||||
style.replaceAccessTokens(mapStyle)
|
style.replaceAccessTokens(mapStyle)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
interface DebugStore {
|
||||||
|
[namespace: string]: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const debugStore: DebugStore = {};
|
||||||
|
|
||||||
|
function enabled() {
|
||||||
|
const qs = new URL(window.location.href).searchParams;
|
||||||
|
const debugQs = qs.get("debug");
|
||||||
|
if(debugQs) {
|
||||||
|
return !!debugQs.match(/^(|1|true)$/);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function genErr() {
|
||||||
|
return new Error("Debug not enabled, enable by appending '?debug' to your query string");
|
||||||
|
}
|
||||||
|
|
||||||
|
function set(namespace: keyof DebugStore, key: string, value: any) {
|
||||||
|
if(!enabled()) {
|
||||||
|
throw genErr();
|
||||||
|
}
|
||||||
|
debugStore[namespace] = debugStore[namespace] || {};
|
||||||
|
debugStore[namespace][key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(namespace: keyof DebugStore, key: string) {
|
||||||
|
if(!enabled()) {
|
||||||
|
throw genErr();
|
||||||
|
}
|
||||||
|
if(Object.prototype.hasOwnProperty.call(debugStore, namespace)) {
|
||||||
|
return debugStore[namespace][key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mod = {
|
||||||
|
enabled,
|
||||||
|
get,
|
||||||
|
set
|
||||||
|
};
|
||||||
|
|
||||||
|
(window as any).debug = mod;
|
||||||
|
export default mod;
|
||||||
Vendored
-18
@@ -1,18 +0,0 @@
|
|||||||
import type { StyleSpecification } from "maplibre-gl";
|
|
||||||
|
|
||||||
export type StyleSpecificationWithId = StyleSpecification & {id: string};
|
|
||||||
|
|
||||||
export type OnStyleChangedOpts = {
|
|
||||||
save?: boolean;
|
|
||||||
addRevision?: boolean;
|
|
||||||
initialLoad?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OnStyleChangedCallback = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}) => void;
|
|
||||||
|
|
||||||
export type OnMoveLayerCallback = (move: {oldIndex: number; newIndex: number}) => void;
|
|
||||||
|
|
||||||
export interface IStyleStore {
|
|
||||||
getLatestStyle(): Promise<StyleSpecificationWithId>;
|
|
||||||
save(mapStyle: StyleSpecificationWithId): StyleSpecificationWithId;
|
|
||||||
}
|
|
||||||
@@ -20,12 +20,6 @@ const spec = (t: TFunction) => ({
|
|||||||
docUrl: "https://docs.stadiamaps.com/authentication/",
|
docUrl: "https://docs.stadiamaps.com/authentication/",
|
||||||
docUrlLinkText: t("Learn More")
|
docUrlLinkText: t("Learn More")
|
||||||
},
|
},
|
||||||
locationiq_access_token: {
|
|
||||||
label: t("LocationIQ Access Token"),
|
|
||||||
doc: t("Public access token for LocationIQ services."),
|
|
||||||
docUrl: "https://docs.locationiq.com/docs/maps",
|
|
||||||
docUrlLinkText: t("Learn More")
|
|
||||||
},
|
|
||||||
style_renderer: {
|
style_renderer: {
|
||||||
label: t("Style Renderer"),
|
label: t("Style Renderer"),
|
||||||
doc: t("Choose the default Maputnik renderer for this style.")
|
doc: t("Choose the default Maputnik renderer for this style.")
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export const NON_SOURCE_LAYERS = ['background', 'raster', 'hillshade', 'heatmap', 'color-relief']
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { StyleSpecificationWithId } from "./definitions";
|
import type {StyleSpecification} from "maplibre-gl";
|
||||||
|
|
||||||
export class RevisionStore {
|
export class RevisionStore {
|
||||||
revisions: StyleSpecificationWithId[];
|
revisions: (StyleSpecification & {id: string})[];
|
||||||
currentIdx: number;
|
currentIdx: number;
|
||||||
|
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ export class RevisionStore {
|
|||||||
return this.revisions[this.currentIdx]
|
return this.revisions[this.currentIdx]
|
||||||
}
|
}
|
||||||
|
|
||||||
addRevision(revision: StyleSpecificationWithId) {
|
addRevision(revision: StyleSpecification & {id: string}) {
|
||||||
// clear any "redo" revisions once a change is made
|
// clear any "redo" revisions once a change is made
|
||||||
// and ensure current index is at end of list
|
// and ensure current index is at end of list
|
||||||
this.revisions = this.revisions.slice(0, this.currentIdx + 1);
|
this.revisions = this.revisions.slice(0, this.currentIdx + 1);
|
||||||
|
|||||||
+5
-5
@@ -1,7 +1,6 @@
|
|||||||
import type {SourceSpecification} from "maplibre-gl";
|
import type {StyleSpecification, SourceSpecification} from "maplibre-gl";
|
||||||
import type {StyleSpecificationWithId} from "./definitions";
|
|
||||||
|
|
||||||
export function deleteSource(mapStyle: StyleSpecificationWithId, sourceId: string) {
|
export function deleteSource(mapStyle: StyleSpecification, sourceId: string) {
|
||||||
const remainingSources = { ...mapStyle.sources}
|
const remainingSources = { ...mapStyle.sources}
|
||||||
delete remainingSources[sourceId]
|
delete remainingSources[sourceId]
|
||||||
return {
|
return {
|
||||||
@@ -11,11 +10,11 @@ export function deleteSource(mapStyle: StyleSpecificationWithId, sourceId: strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function addSource(mapStyle: StyleSpecificationWithId, sourceId: string, source: SourceSpecification) {
|
export function addSource(mapStyle: StyleSpecification, sourceId: string, source: SourceSpecification) {
|
||||||
return changeSource(mapStyle, sourceId, source)
|
return changeSource(mapStyle, sourceId, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changeSource(mapStyle: StyleSpecificationWithId, sourceId: string, source: SourceSpecification) {
|
export function changeSource(mapStyle: StyleSpecification, sourceId: string, source: SourceSpecification) {
|
||||||
const changedSources = {
|
const changedSources = {
|
||||||
...mapStyle.sources,
|
...mapStyle.sources,
|
||||||
[sourceId]: source
|
[sourceId]: source
|
||||||
@@ -25,3 +24,4 @@ export function changeSource(mapStyle: StyleSpecificationWithId, sourceId: strin
|
|||||||
sources: changedSources
|
sources: changedSources
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
import { IStyleStore, OnStyleChangedCallback } from "../definitions";
|
|
||||||
import { getStyleUrlFromAddressbarAndRemoveItIfNeeded, loadStyleUrl } from "../urlopen";
|
|
||||||
import { ApiStyleStore } from "./apistore";
|
|
||||||
import { StyleStore } from "./stylestore";
|
|
||||||
|
|
||||||
export async function createStyleStore(onStyleChanged: OnStyleChangedCallback): Promise<IStyleStore> {
|
|
||||||
const styleUrl = getStyleUrlFromAddressbarAndRemoveItIfNeeded();
|
|
||||||
const useStyleUrl = styleUrl && window.confirm("Load style from URL: " + styleUrl + " and discard current changes?");
|
|
||||||
let styleStore: IStyleStore;
|
|
||||||
if (import.meta.env.MODE === 'desktop' && !useStyleUrl) {
|
|
||||||
const apiStyleStore = new ApiStyleStore({
|
|
||||||
onLocalStyleChange: mapStyle => onStyleChanged(mapStyle, {save: false}),
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await apiStyleStore.init();
|
|
||||||
styleStore = apiStyleStore;
|
|
||||||
} catch {
|
|
||||||
styleStore = new StyleStore();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
styleStore = new StyleStore();
|
|
||||||
}
|
|
||||||
const styleToLoad = useStyleUrl ? await loadStyleUrl(styleUrl) : await styleStore.getLatestStyle();
|
|
||||||
onStyleChanged(styleToLoad, {initialLoad: true, save: false});
|
|
||||||
return styleStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { IStyleStore };
|
|
||||||
+6
-10
@@ -1,7 +1,6 @@
|
|||||||
import {derefLayers} from '@maplibre/maplibre-gl-style-spec'
|
import {derefLayers} from '@maplibre/maplibre-gl-style-spec'
|
||||||
import type {StyleSpecification, LayerSpecification} from 'maplibre-gl'
|
import type {StyleSpecification, LayerSpecification} from 'maplibre-gl'
|
||||||
import tokens from '../config/tokens.json'
|
import tokens from '../config/tokens.json'
|
||||||
import type {StyleSpecificationWithId} from './definitions'
|
|
||||||
|
|
||||||
// Empty style is always used if no style could be restored or fetched
|
// Empty style is always used if no style could be restored or fetched
|
||||||
const emptyStyle = ensureStyleValidity({
|
const emptyStyle = ensureStyleValidity({
|
||||||
@@ -14,14 +13,15 @@ function generateId() {
|
|||||||
return Math.random().toString(36).substring(2, 9)
|
return Math.random().toString(36).substring(2, 9)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureHasId(style: StyleSpecification & { id?: string }): StyleSpecificationWithId {
|
function ensureHasId(style: StyleSpecification & { id?: string }): StyleSpecification & { id: string } {
|
||||||
if(!('id' in style) || !style.id) {
|
if(!('id' in style) || !style.id) {
|
||||||
style.id = generateId();
|
style.id = generateId();
|
||||||
|
return style as StyleSpecification & { id: string };
|
||||||
}
|
}
|
||||||
return style as StyleSpecificationWithId;
|
return style as StyleSpecification & { id: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureHasNoInteractive(style: StyleSpecificationWithId) {
|
function ensureHasNoInteractive(style: StyleSpecification & {id: string}) {
|
||||||
const changedLayers = style.layers.map(layer => {
|
const changedLayers = style.layers.map(layer => {
|
||||||
const changedLayer: LayerSpecification & { interactive?: any } = { ...layer }
|
const changedLayer: LayerSpecification & { interactive?: any } = { ...layer }
|
||||||
delete changedLayer.interactive
|
delete changedLayer.interactive
|
||||||
@@ -34,14 +34,14 @@ function ensureHasNoInteractive(style: StyleSpecificationWithId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureHasNoRefs(style: StyleSpecificationWithId) {
|
function ensureHasNoRefs(style: StyleSpecification & {id: string}) {
|
||||||
return {
|
return {
|
||||||
...style,
|
...style,
|
||||||
layers: derefLayers(style.layers)
|
layers: derefLayers(style.layers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureStyleValidity(style: StyleSpecification): StyleSpecificationWithId {
|
function ensureStyleValidity(style: StyleSpecification): StyleSpecification & { id: string } {
|
||||||
return ensureHasNoInteractive(ensureHasNoRefs(ensureHasId(style)))
|
return ensureHasNoInteractive(ensureHasNoRefs(ensureHasId(style)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,9 +79,6 @@ function replaceSourceAccessToken(mapStyle: StyleSpecification, sourceName: stri
|
|||||||
// so we need to check the source URL.
|
// so we need to check the source URL.
|
||||||
authSourceName = "stadia"
|
authSourceName = "stadia"
|
||||||
}
|
}
|
||||||
else if (("url" in source) && source.url?.match(/\.locationiq\.com/)) {
|
|
||||||
authSourceName = "locationiq"
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessToken = getAccessToken(authSourceName, mapStyle, opts)
|
const accessToken = getAccessToken(authSourceName, mapStyle, opts)
|
||||||
|
|
||||||
@@ -141,7 +138,6 @@ function stripAccessTokens(mapStyle: StyleSpecification) {
|
|||||||
delete changedMetadata['maputnik:openmaptiles_access_token'];
|
delete changedMetadata['maputnik:openmaptiles_access_token'];
|
||||||
delete changedMetadata['maputnik:thunderforest_access_token'];
|
delete changedMetadata['maputnik:thunderforest_access_token'];
|
||||||
delete changedMetadata['maputnik:stadia_access_token'];
|
delete changedMetadata['maputnik:stadia_access_token'];
|
||||||
delete changedMetadata['maputnik:locationiq_access_token'];
|
|
||||||
return {
|
return {
|
||||||
...mapStyle,
|
...mapStyle,
|
||||||
metadata: changedMetadata
|
metadata: changedMetadata
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import style from '../style'
|
import style from './style'
|
||||||
import {loadStyleUrl} from '../urlopen'
|
import {loadStyleUrl} from './urlopen'
|
||||||
import publicSources from '../../config/styles.json'
|
import publicSources from '../config/styles.json'
|
||||||
import type {IStyleStore, StyleSpecificationWithId} from '../definitions'
|
import type {StyleSpecification} from 'maplibre-gl'
|
||||||
|
|
||||||
const storagePrefix = "maputnik"
|
const storagePrefix = "maputnik"
|
||||||
const stylePrefix = 'style'
|
const stylePrefix = 'style'
|
||||||
@@ -13,8 +13,8 @@ const storageKeys = {
|
|||||||
const defaultStyleUrl = publicSources[0].url
|
const defaultStyleUrl = publicSources[0].url
|
||||||
|
|
||||||
// Fetch a default style via URL and return it or a fallback style via callback
|
// Fetch a default style via URL and return it or a fallback style via callback
|
||||||
export function loadDefaultStyle(): Promise<StyleSpecificationWithId> {
|
export function loadDefaultStyle(cb: (...args: any[]) => void) {
|
||||||
return loadStyleUrl(defaultStyleUrl);
|
loadStyleUrl(defaultStyleUrl, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return style ids and dates of all styles stored in local storage
|
// Return style ids and dates of all styles stored in local storage
|
||||||
@@ -51,7 +51,7 @@ function styleKey(styleId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Manages many possible styles that are stored in the local storage
|
// Manages many possible styles that are stored in the local storage
|
||||||
export class StyleStore implements IStyleStore {
|
export class StyleStore {
|
||||||
/**
|
/**
|
||||||
* List of style ids
|
* List of style ids
|
||||||
*/
|
*/
|
||||||
@@ -63,6 +63,10 @@ export class StyleStore implements IStyleStore {
|
|||||||
this.mapStyles = loadStoredStyles();
|
this.mapStyles = loadStoredStyles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(cb: (...args: any[]) => void) {
|
||||||
|
cb(null)
|
||||||
|
}
|
||||||
|
|
||||||
// Delete entire style history
|
// Delete entire style history
|
||||||
purge() {
|
purge() {
|
||||||
for (let i = 0; i < window.localStorage.length; i++) {
|
for (let i = 0; i < window.localStorage.length; i++) {
|
||||||
@@ -74,45 +78,21 @@ export class StyleStore implements IStyleStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the last edited style
|
// Find the last edited style
|
||||||
async getLatestStyle(): Promise<StyleSpecificationWithId> {
|
latestStyle(cb: (...args: any[]) => void) {
|
||||||
if(this.mapStyles.length === 0) {
|
if(this.mapStyles.length === 0) return loadDefaultStyle(cb)
|
||||||
return loadDefaultStyle();
|
|
||||||
}
|
|
||||||
const styleId = window.localStorage.getItem(storageKeys.latest) as string;
|
const styleId = window.localStorage.getItem(storageKeys.latest) as string;
|
||||||
const styleItem = window.localStorage.getItem(styleKey(styleId))
|
const styleItem = window.localStorage.getItem(styleKey(styleId))
|
||||||
|
|
||||||
if (styleItem) {
|
if(styleItem) return cb(JSON.parse(styleItem))
|
||||||
return JSON.parse(styleItem) as StyleSpecificationWithId;
|
loadDefaultStyle(cb)
|
||||||
}
|
|
||||||
return loadDefaultStyle();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save current style replacing previous version
|
// Save current style replacing previous version
|
||||||
save(mapStyle: StyleSpecificationWithId) {
|
save(mapStyle: StyleSpecification & { id: string }) {
|
||||||
mapStyle = style.ensureStyleValidity(mapStyle)
|
mapStyle = style.ensureStyleValidity(mapStyle)
|
||||||
const key = styleKey(mapStyle.id)
|
const key = styleKey(mapStyle.id)
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(mapStyle))
|
||||||
const saveFn = () => {
|
window.localStorage.setItem(storageKeys.latest, mapStyle.id)
|
||||||
window.localStorage.setItem(key, JSON.stringify(mapStyle))
|
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user