mirror of
https://github.com/maputnik/editor.git
synced 2026-08-03 03:37:25 +00:00
Compare commits
49 Commits
v2.0.0
...
f306d8e143
| Author | SHA1 | Date | |
|---|---|---|---|
| f306d8e143 | |||
| 6a27913114 | |||
| 285dca8d2c | |||
| 2a3b6aa770 | |||
| c84c7a7b96 | |||
| cb77c6b4e2 | |||
| ea42f434eb | |||
| 6f82c12861 | |||
| 3b95b25777 | |||
| 1da65f2116 | |||
| a62db148cd | |||
| 6ed10a862f | |||
| 123e84f19b | |||
| d9b1b6f3ae | |||
| e0cef99c07 | |||
| eb48bed32a | |||
| 87cf81d1c9 | |||
| 8e35ed97e6 | |||
| 124ae98bf3 | |||
| b784bf2b84 | |||
| 09a1f3f87b | |||
| a22476cab2 | |||
| a324ddb654 | |||
| 656264f2bc | |||
| 974dd7bfd9 | |||
| fa182e66fa | |||
| 3bf0e510e6 | |||
| e8d07fa694 | |||
| 4d1e2e6893 | |||
| f219ff1e17 | |||
| 8eabfa5519 | |||
| ad69cbdb20 | |||
| 17eaa3f204 | |||
| 1df2e36dbb | |||
| 0c46affda9 | |||
| 18f45e932b | |||
| 31d56c9fae | |||
| b7838ad6e1 | |||
| c92fd12854 | |||
| eb55796461 | |||
| 4b97f82980 | |||
| 5d0b6e3201 | |||
| 5ab9be9fdb | |||
| 9659d41b83 | |||
| 52f949e152 | |||
| e4d559f953 | |||
| 577663f706 | |||
| 393f4a38b9 | |||
| 3727c9ad5e |
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
"@babel/preset-env",
|
|
||||||
"@babel/preset-react"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"static-fs",
|
|
||||||
"react-hot-loader/babel",
|
|
||||||
"@babel/plugin-proposal-class-properties",
|
|
||||||
"@babel/transform-runtime"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"test": {
|
|
||||||
"plugins": [
|
|
||||||
["istanbul", {
|
|
||||||
"exclude": ["node_modules/**", "test/**"]
|
|
||||||
}]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"packages": [],
|
|
||||||
"sandboxes": ["/"]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2020": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react/jsx-runtime",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
],
|
||||||
|
"ignorePatterns": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"react": { "version": "16.4" }
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"react-refresh"],
|
||||||
|
"rules": {
|
||||||
|
"react-refresh/only-export-components": [
|
||||||
|
"warn",
|
||||||
|
{ "allowConstantExport": true }
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{ "argsIgnorePattern": "^_" }
|
||||||
|
],
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"react/prop-types": ["off"],
|
||||||
|
// Disable no-undef. It's covered by @typescript-eslint
|
||||||
|
"no-undef": "off",
|
||||||
|
"indent": ["error", 2],
|
||||||
|
"no-var": ["error"]
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"global": "readonly"
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -1 +1,2 @@
|
|||||||
custom: "https://maputnik.github.io/donate"
|
github: [maplibre]
|
||||||
|
open_collective: maplibre
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ assignees: ''
|
|||||||
|
|
||||||
<!-- Thanks for your feedback! Please complete the following information: -->
|
<!-- Thanks for your feedback! Please complete the following information: -->
|
||||||
|
|
||||||
**Maputnik version**:<!-- e.g v1.7.0, master -->
|
**Maputnik version**:<!-- e.g v1.7.0, main -->
|
||||||
**Browser**:
|
**Browser**:
|
||||||
**OS**:<!-- (Windows, macOS, Linux) -->
|
**OS**:<!-- (Windows, macOS, Linux) -->
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 2
|
||||||
|
versioning-strategy: increase
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 2
|
||||||
|
versioning-strategy: increase
|
||||||
+40
-114
@@ -2,44 +2,24 @@ name: ci
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ main ]
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
# post a comment linking to codesandbox with the current branch
|
|
||||||
# meta-demo-comment:
|
|
||||||
# name: meta/demo-comment
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
# if: ${{ github.event_name == 'pull_request' }}
|
|
||||||
|
|
||||||
# steps:
|
|
||||||
# - uses: unsplash/comment-on-pr@v1.2.0
|
|
||||||
# env:
|
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# with:
|
|
||||||
# msg: "Demo: <https://codesandbox.io/embed/github/${{ github.repository }}/tree/${{ github.head_ref }}?view=preview>"
|
|
||||||
|
|
||||||
build-docker:
|
build-docker:
|
||||||
name: build/docker
|
name: build docker
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:master .
|
- run: docker build -t test-docker-image-build .
|
||||||
|
|
||||||
# build the editor
|
# build the editor
|
||||||
build-node:
|
build-node:
|
||||||
name: "build/node@${{ matrix.node-version }} (${{ matrix.os }})"
|
name: "build on ${{ matrix.os }}"
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||||
@@ -48,64 +28,36 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
node-version: [16.x]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version-file: '.nvmrc'
|
||||||
- uses: actions/cache@v1
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
|
- run: npm run lint
|
||||||
|
- run: npm run lint-css
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
build-artifacts:
|
build-artifacts:
|
||||||
name: "build/artifacts (${{ matrix.os }})"
|
name: "build artifacts"
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest]
|
|
||||||
node-version: [16.x]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version-file: '.nvmrc'
|
||||||
- uses: actions/cache@v1
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm run build-storybook
|
- name: artifacts/maputnik
|
||||||
- name: artifacts/editor
|
|
||||||
uses: actions/upload-artifact@v1
|
uses: actions/upload-artifact@v1
|
||||||
with:
|
with:
|
||||||
name: editor
|
name: maputnik
|
||||||
path: build/build
|
path: dist
|
||||||
- run: npm run profiling-build
|
|
||||||
- name: artifacts/editor-profiling
|
|
||||||
uses: actions/upload-artifact@v1
|
|
||||||
with:
|
|
||||||
name: editor-profiling
|
|
||||||
path: build/profiling
|
|
||||||
- name: artifacts/storybook
|
|
||||||
uses: actions/upload-artifact@v1
|
|
||||||
with:
|
|
||||||
name: storybook
|
|
||||||
path: build/storybook
|
|
||||||
|
|
||||||
# Build and upload desktop CLI artifacts
|
# Build and upload desktop CLI artifacts
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
@@ -115,7 +67,7 @@ jobs:
|
|||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: maputnik/desktop
|
repository: maputnik/desktop
|
||||||
ref: master
|
ref: master
|
||||||
@@ -141,53 +93,27 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: maputnik-windows
|
name: maputnik-windows
|
||||||
path: ./src/github.com/maputnik/desktop/bin/windows/
|
path: ./src/github.com/maputnik/desktop/bin/windows/
|
||||||
|
|
||||||
# build and test the editor
|
e2e-tests:
|
||||||
test_selenium_standalone:
|
name: "E2E tests using ${{ matrix.browser }}"
|
||||||
name: "test/standalone-${{ matrix.browser }} (${{ matrix.os }})"
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
|
||||||
node-version: [16]
|
|
||||||
browser: [chrome, firefox]
|
browser: [chrome, firefox]
|
||||||
|
|
||||||
container:
|
runs-on: ubuntu-22.04
|
||||||
image: node:${{ matrix.node-version }}
|
|
||||||
options: --network-alias testhost
|
|
||||||
|
|
||||||
services:
|
|
||||||
selenium:
|
|
||||||
# geckodriver-0.31 seems to have problems as of 2022 May 1
|
|
||||||
image: selenium/standalone-${{ matrix.browser == 'firefox' && 'firefox:99.0-geckodriver-0.30-20220427' || matrix.browser }}
|
|
||||||
ports:
|
|
||||||
- 4444:4444
|
|
||||||
options: --shm-size=2gb
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Checkout
|
||||||
- uses: actions/cache@v1
|
uses: actions/checkout@v4
|
||||||
with:
|
- run: npm ci
|
||||||
path: ~/.npm
|
- name: Cypress run
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
uses: cypress-io/github-action@v6
|
||||||
restore-keys: |
|
with:
|
||||||
${{ runner.os }}-node-
|
build: npm run build
|
||||||
- run: npm ci
|
start: npm run start
|
||||||
- run: BROWSER=${{ matrix.browser }} TEST_NETWORK=testhost DOCKER_HOST=selenium npm run test
|
browser: ${{ matrix.browser }}
|
||||||
- if: ${{ matrix.browser == 'chrome' }}
|
- name: Upload coverage reports to Codecov
|
||||||
run: ./node_modules/.bin/istanbul report --include build/coverage/coverage.json --dir build/coverage html lcov
|
uses: codecov/codecov-action@v3
|
||||||
- if: ${{ matrix.browser == 'chrome' }}
|
with:
|
||||||
name: artifacts/coverage
|
files: ${{ github.workspace }}/.nyc_output/out.json
|
||||||
uses: actions/upload-artifact@v1
|
verbose: true
|
||||||
with:
|
|
||||||
name: coverage
|
|
||||||
path: build/coverage
|
|
||||||
- name: artifacts/screenshots
|
|
||||||
uses: actions/upload-artifact@v1
|
|
||||||
with:
|
|
||||||
name: screenshots-${{ matrix.browser }}
|
|
||||||
path: build/screenshots
|
|
||||||
|
|||||||
@@ -2,27 +2,50 @@ name: deploy
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [ main ]
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# publish docker to github registry
|
deploy-pages:
|
||||||
|
name: deploy/pages
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event_name == 'push' }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use Node.js from nvmrc
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Upload to GitHub Pages
|
||||||
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
publish_dir: dist
|
||||||
|
|
||||||
|
# publish docker to GitHub registry
|
||||||
deploy-docker:
|
deploy-docker:
|
||||||
name: deploy/docker
|
name: deploy/docker
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
if: ${{ github.event_name == 'push' }}
|
if: ${{ github.event_name == 'push' }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Login to GitHub Container Registry
|
||||||
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u orangemug --password-stdin
|
uses: docker/login-action@v3
|
||||||
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:master .
|
with:
|
||||||
- run: docker push docker.pkg.github.com/maputnik/editor/editor:master
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
||||||
|
- run: docker push ghcr.io/maplibre/maputnik:main
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ lib-cov
|
|||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
# Coverage directory used by tools like istanbul
|
||||||
coverage
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
.grunt
|
.grunt
|
||||||
@@ -33,3 +34,5 @@ public
|
|||||||
/errorShots
|
/errorShots
|
||||||
/old
|
/old
|
||||||
/build
|
/build
|
||||||
|
/cypress/screenshots
|
||||||
|
/dist/
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"all": true,
|
||||||
|
"extends": "@istanbuljs/nyc-config-typescript",
|
||||||
|
"check-coverage": false,
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||||
|
"exclude": [
|
||||||
|
"cypress/**/*.*",
|
||||||
|
"**/*.d.ts",
|
||||||
|
"**/*.cy.tsx",
|
||||||
|
"**/*.cy.ts",
|
||||||
|
"./coverage/**",
|
||||||
|
"./cypress/**",
|
||||||
|
"./dist/**",
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
|
"report-dir": "coverage",
|
||||||
|
"reporter": ["json", "lcov", "json-summary"]
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
const rules = require('../config/webpack.rules');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
stories: ['../stories/**/*.stories.js'],
|
|
||||||
addons: [
|
|
||||||
'@storybook/addon-actions',
|
|
||||||
'@storybook/addon-links',
|
|
||||||
'@storybook/addon-a11y/register',
|
|
||||||
'@storybook/addon-storysource',
|
|
||||||
],
|
|
||||||
webpackFinal: async config => {
|
|
||||||
// do mutation to the config
|
|
||||||
console.log("config.module", config.module);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
...rules,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { addons } from '@storybook/addons';
|
|
||||||
import { themes } from '@storybook/theming';
|
|
||||||
import theme from './maputnik.theme';
|
|
||||||
|
|
||||||
addons.setConfig({
|
|
||||||
theme: theme,
|
|
||||||
});
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { create } from '@storybook/theming/create';
|
|
||||||
|
|
||||||
export default create({
|
|
||||||
base: 'light',
|
|
||||||
|
|
||||||
brandTitle: 'Maputnik',
|
|
||||||
brandUrl: 'https://github.com/maputnik/editor',
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Contributor Covenant
|
||||||
|
[](https://github.com/maplibre/maplibre/blob/main/CODE_OF_CONDUCT.md)
|
||||||
+5
-11
@@ -1,4 +1,4 @@
|
|||||||
FROM node:10 as builder
|
FROM node:18 as builder
|
||||||
WORKDIR /maputnik
|
WORKDIR /maputnik
|
||||||
|
|
||||||
# Only copy package.json to prevent npm install from running on every build
|
# Only copy package.json to prevent npm install from running on every build
|
||||||
@@ -6,17 +6,11 @@ COPY package.json package-lock.json ./
|
|||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
# Build maputnik
|
# Build maputnik
|
||||||
# TODO: we should also do a npm run test here (needs more dependencies)
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npx vite build
|
||||||
|
|
||||||
#---------------------------------------------------------------------------
|
#---------------------------------------------------------------------------
|
||||||
|
# Create a clean nginx-alpine slim image with just the build results
|
||||||
|
FROM nginx:alpine-slim
|
||||||
|
|
||||||
# Create a clean python-based image with just the build results
|
COPY --from=builder /maputnik/dist /usr/share/nginx/html/
|
||||||
FROM python:3-slim
|
|
||||||
WORKDIR /maputnik
|
|
||||||
|
|
||||||
COPY --from=builder /maputnik/build/build .
|
|
||||||
|
|
||||||
EXPOSE 8888
|
|
||||||
CMD python -m http.server 8888
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2015 Lukas Martinelli
|
Copyright (c) 2015 Lukas Martinelli
|
||||||
|
Copyright (c) 2024 MapLibre contributors
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,45 +1,44 @@
|
|||||||
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
|
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
|
||||||
|
|
||||||
# Maputnik
|
# Maputnik
|
||||||
[][github-action-ci]
|
[][github-action-ci]
|
||||||
[][license]
|
[][license]
|
||||||
|
|
||||||
[github-action-ci]: https://github.com/maputnik/editor/actions?query=workflow%3Aci
|
[github-action-ci]: https://github.com/maplibre/maputnik/actions?query=workflow%3Aci
|
||||||
[license]: https://tldrlegal.com/license/mit-license
|
[license]: https://tldrlegal.com/license/mit-license
|
||||||
|
|
||||||
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
|
A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
|
||||||
targeted at developers and map designers.
|
targeted at developers and map designers.
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
|
- :link: Design your maps online at **<https://www.maplibre.org/maputnik/>** (all in local storage)
|
||||||
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
|
- :link: Use the [Maputnik CLI](https://github.com/maplibre/maputnik/wiki/Maputnik-CLI) for local style development
|
||||||
- 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:8888 maputnik/editor
|
docker run -it --rm -p 8888:8888 ghcr.io/maplibre/maputnik:main
|
||||||
```
|
```
|
||||||
|
|
||||||
## Donations
|
|
||||||
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer.
|
|
||||||
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/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!
|
||||||
|
|
||||||
- :link: **Study the [Maputnik Wiki](https://github.com/maputnik/editor/wiki)**
|
- :link: **Study the [Maputnik Wiki](https://github.com/maplibre/maputnik/wiki)**
|
||||||
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
|
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
|
||||||
|
|
||||||
[](https://youtu.be/XoDh0gEnBQo)
|
[](https://youtu.be/XoDh0gEnBQo)
|
||||||
|
|
||||||
## Develop
|
## Develop
|
||||||
|
|
||||||
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
|
Maputnik is written in typescript and is using [React](https://github.com/facebook/react) and [MapLibre GL JS](https://maplibre.org/projects/maplibre-gl-js/).
|
||||||
|
|
||||||
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
|
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
|
||||||
|
|
||||||
|
### Getting Involved
|
||||||
|
Join the #maplibre or #maputnik slack channel at OSMUS: get an invite at https://slack.openstreetmap.us/ Read the the below guide in order to get familiar with how we do things around here.
|
||||||
|
|
||||||
Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
|
Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -49,18 +48,14 @@ npm install
|
|||||||
npm run start
|
npm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want Maputnik to be accessible externally use the [`--host` option](https://webpack.js.org/configuration/dev-server/#devserverhost):
|
If you want Maputnik to be accessible externally use the [`--host` option](https://vitejs.dev/config/server-options.html#server-host):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# start externally accessible dev server
|
# start externally accessible dev server
|
||||||
npm run start -- --host 0.0.0.0
|
npm run start -- --host 0.0.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the [webpack-dev-server docs](https://webpack.js.org/configuration/dev-server/):
|
The build process will watch for changes to the filesystem, rebuild and autoreload the editor.
|
||||||
|
|
||||||
> webpack uses the file system to get notified of file changes. In some cases this does not work. For example, when using Network File System (NFS). Vagrant also has a lot of problems with this. ([snippet source](https://webpack.js.org/configuration/dev-server/#devserverwatchoptions-))
|
|
||||||
|
|
||||||
To enable polling add `export WEBPACK_DEV_SERVER_POLLING=1` to your environment.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
npm run build
|
npm run build
|
||||||
@@ -76,9 +71,9 @@ npm run lint-styles
|
|||||||
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
For testing we use [webdriverio](https://webdriver.io) and [selenium-standalone](https://github.com/webdriverio/selenium-standalone).
|
For E2E testing we use [Cypress](https://www.cypress.io/)
|
||||||
|
|
||||||
[selenium-standalone](https://github.com/webdriverio/selenium-standalone) starts a server that will launch browsers on your local machine. You need to have Java installed on your machine as well as *chrome* or *firefox*.
|
[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*:
|
||||||
|
|
||||||
@@ -87,10 +82,16 @@ npm run test
|
|||||||
```
|
```
|
||||||
or *firefox*:
|
or *firefox*:
|
||||||
```
|
```
|
||||||
BROWSER=firefox npm run test
|
npm run test -- --browser firefox
|
||||||
```
|
```
|
||||||
|
|
||||||
After some time you should see a browser launch which will be automated by the test runner.
|
See the following docs for more info: (Launching Browsers)[https://docs.cypress.io/guides/guides/launching-browsers]
|
||||||
|
|
||||||
|
You can also see the tests as they run or select which suites to run by executing:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run cy:open
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Related Projects
|
## Related Projects
|
||||||
@@ -100,61 +101,10 @@ After some time you should see a browser launch which will be automated by the t
|
|||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
|
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
|
||||||
|
You can see this file's history for previous sponsors of the original Maputnik repo.
|
||||||
### Gold
|
Read more about the MapLibre Sponsorship Program at https://maplibre.org/sponsors/.
|
||||||
|
|
||||||
- [Wemap](https://getwemap.com/)
|
|
||||||
- [Orbicon Informatik](https://www.orbiconinformatik.dk/)
|
|
||||||
- [Terranodo](http://terranodo.io/)
|
|
||||||
|
|
||||||
<a href="https://getwemap.com/">
|
|
||||||
<img width="33%" alt="Wemap" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/wemap.jpg" />
|
|
||||||
</a>
|
|
||||||
<a href="http://terranodo.io/">
|
|
||||||
<img width="33%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/terranodo.png" />
|
|
||||||
</a>
|
|
||||||
<a href="https://www.orbiconinformatik.dk/">
|
|
||||||
<img width="32%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/orbicon_informatik.png" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
### Silver
|
|
||||||
|
|
||||||
- [Klokan Technologies](https://www.klokantech.com/)
|
|
||||||
- [Geofabrik](http://www.geofabrik.de/)
|
|
||||||
- [Dreipol](https://www.dreipol.ch/)
|
|
||||||
|
|
||||||
<a href="https://www.klokantech.com/">
|
|
||||||
<img width="18%" alt="Klokan Technologies" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/klokantech.png" />
|
|
||||||
</a>
|
|
||||||
<a href="http://www.geofabrik.de/">
|
|
||||||
<img width="18%" alt="Geofabrik" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/geofabrik.png" />
|
|
||||||
</a>
|
|
||||||
<a href="https://www.dreipol.ch/">
|
|
||||||
<img width="18%" alt="Dreipol" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/dreipol.png" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
### Individuals
|
|
||||||
|
|
||||||
**Influential Stakeholder**
|
|
||||||
|
|
||||||
Alan McConchie, Odi, Mats Norén, Uli [geOps](http://geops.ch/), Helge Fahrnberger ([Toursprung](http://www.toursprung.com/)), Kirusanth Poopalasingam
|
|
||||||
|
|
||||||
**Stakeholder**
|
|
||||||
|
|
||||||
Brian Flood, Vasile Coțovanu, Andreas Kalkbrenner, Christian Mäder, Gregor Wassmann, Lee Armstrong, Rafel, Jon Burgess, Lukas Lehmann, Joachim Ungar, Alois Ackermann, Zsolt Ero, Jordan Meek
|
|
||||||
|
|
||||||
**Supporter**
|
|
||||||
|
|
||||||
Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth, Janko Mihelić, Moritz Stefaner, Sebastian Ahoi, Juerg Uhlmann, Tom Wider, Nadia Panchaud, Oliver Snowden, Stephan Heuel, Tobin Bradley, Adrian Herzog, Antti Lehto, Pascal Mages, Marc Gehling, Imre Samu, Lauri K., Visahavel Parthasarathy, Christophe Waterlot-Buisine, Max Galka, ubahnverleih, Wouter van Dam, Jakob Lobensteiner, Samuel Kurath, Brian Bancroft
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
|
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and Maplibre contributors.
|
||||||
|
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by other map studios and make your own decisions for a good style editor.
|
||||||
**Disclaimer** This project is not affiliated with Mapbox or Mapbox Studio. It is an independent style editor for the
|
|
||||||
open source technology in the Mapbox GL ecosystem.
|
|
||||||
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
For an up-to-date policy refer to
|
||||||
|
https://github.com/maplibre/maplibre/blob/main/SECURITY_POLICY.txt
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
var webpack = require("webpack");
|
|
||||||
var WebpackDevServer = require("webpack-dev-server");
|
|
||||||
var webpackConfig = require("./webpack.config");
|
|
||||||
var testConfig = require("../test/config/specs");
|
|
||||||
var artifacts = require("../test/artifacts");
|
|
||||||
|
|
||||||
|
|
||||||
var server;
|
|
||||||
var SCREENSHOT_PATH = artifacts.pathSync("screenshots");
|
|
||||||
|
|
||||||
exports.config = {
|
|
||||||
runner: 'local',
|
|
||||||
path: '/wd/hub',
|
|
||||||
specs: [
|
|
||||||
'./test/functional/index.js'
|
|
||||||
],
|
|
||||||
maxInstances: 10,
|
|
||||||
capabilities: [
|
|
||||||
{
|
|
||||||
maxInstances: 5,
|
|
||||||
browserName: (process.env.BROWSER || 'chrome'),
|
|
||||||
}
|
|
||||||
],
|
|
||||||
// geckodriver-0.31 seems to have problems as of 2022 May 1
|
|
||||||
services: process.env.DOCKER_HOST ? [] : [ ['selenium-standalone', { drivers: { firefox: '0.30.0', chrome: 'latest' } } ] ],
|
|
||||||
logLevel: 'info',
|
|
||||||
bail: 0,
|
|
||||||
screenshotPath: SCREENSHOT_PATH,
|
|
||||||
hostname: process.env.DOCKER_HOST || "0.0.0.0",
|
|
||||||
framework: 'mocha',
|
|
||||||
reporters: ['spec'],
|
|
||||||
mochaOpts: {
|
|
||||||
ui: 'bdd',
|
|
||||||
// Because we don't know how long the initial build will take...
|
|
||||||
timeout: 4*60*1000,
|
|
||||||
},
|
|
||||||
onPrepare: async function (config, capabilities) {
|
|
||||||
webpackConfig.devServer.host = testConfig.testNetwork;
|
|
||||||
webpackConfig.devServer.port = testConfig.port;
|
|
||||||
const compiler = webpack(webpackConfig);
|
|
||||||
server = new WebpackDevServer(webpackConfig.devServer, compiler);
|
|
||||||
await server.start();
|
|
||||||
},
|
|
||||||
onComplete: async function (exitCode, config, capabilities) {
|
|
||||||
await server.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var path = require('path');
|
|
||||||
var rules = require('./webpack.rules');
|
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
|
|
||||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
|
||||||
|
|
||||||
const HOST = process.env.HOST || "127.0.0.1";
|
|
||||||
const PORT = process.env.PORT || "8888";
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
target: 'web',
|
|
||||||
mode: 'development',
|
|
||||||
entry: [
|
|
||||||
`webpack-dev-server/client?http://${HOST}:${PORT}`,
|
|
||||||
`webpack/hot/only-dev-server`,
|
|
||||||
`./src/index.jsx` // Your appʼs entry point
|
|
||||||
],
|
|
||||||
devtool: process.env.WEBPACK_DEVTOOL || 'cheap-module-source-map',
|
|
||||||
output: {
|
|
||||||
path: path.join(__dirname, '..', 'public'),
|
|
||||||
filename: 'bundle.js'
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js', '.jsx']
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
noParse: [
|
|
||||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
|
||||||
],
|
|
||||||
rules: rules
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
fs: "empty",
|
|
||||||
net: 'empty',
|
|
||||||
tls: 'empty'
|
|
||||||
},
|
|
||||||
devServer: {
|
|
||||||
// enable HMR
|
|
||||||
hot: true,
|
|
||||||
// serve index.html in place of 404 responses to allow HTML5 history
|
|
||||||
historyApiFallback: true,
|
|
||||||
port: PORT,
|
|
||||||
host: HOST,
|
|
||||||
watchFiles: {
|
|
||||||
options: {
|
|
||||||
// Disabled polling by default as it causes lots of CPU usage and hence drains laptop batteries. To enable polling add WEBPACK_DEV_SERVER_POLLING to your environment
|
|
||||||
// See <https://webpack.js.org/configuration/watch/#watchoptions-poll> for details
|
|
||||||
usePolling: (!!process.env.WEBPACK_DEV_SERVER_POLLING ? true : false),
|
|
||||||
watch: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
optimization: {
|
|
||||||
noEmitOnErrors: true,
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
title: 'Maputnik',
|
|
||||||
template: './src/template.html'
|
|
||||||
}),
|
|
||||||
new HtmlWebpackInlineSVGPlugin({
|
|
||||||
runPreEmit: true,
|
|
||||||
}),
|
|
||||||
new CopyWebpackPlugin({
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
from: './src/manifest.json',
|
|
||||||
to: 'manifest.json'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
var webpack = require('webpack');
|
|
||||||
var path = require('path');
|
|
||||||
var rules = require('./webpack.rules');
|
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
|
|
||||||
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
|
||||||
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
|
||||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
|
||||||
var artifacts = require("../test/artifacts");
|
|
||||||
|
|
||||||
var OUTPATH = artifacts.pathSync("/build");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: {
|
|
||||||
app: './src/index.jsx',
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: OUTPATH,
|
|
||||||
filename: '[name].[contenthash].js',
|
|
||||||
chunkFilename: '[contenthash].js'
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js', '.jsx']
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
noParse: [
|
|
||||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
|
||||||
],
|
|
||||||
rules: rules
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
fs: "empty",
|
|
||||||
net: 'empty',
|
|
||||||
tls: 'empty'
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.NoEmitOnErrorsPlugin(),
|
|
||||||
new WebpackCleanupPlugin(),
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env': {
|
|
||||||
NODE_ENV: '"production"'
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: './src/template.html',
|
|
||||||
title: 'Maputnik'
|
|
||||||
}),
|
|
||||||
new HtmlWebpackInlineSVGPlugin({
|
|
||||||
runPreEmit: true,
|
|
||||||
}),
|
|
||||||
new CopyWebpackPlugin({
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
from: './src/manifest.json',
|
|
||||||
to: 'manifest.json'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
new BundleAnalyzerPlugin({
|
|
||||||
analyzerMode: 'static',
|
|
||||||
defaultSizes: 'gzip',
|
|
||||||
openAnalyzer: false,
|
|
||||||
generateStatsFile: true,
|
|
||||||
reportFilename: 'bundle-stats.html',
|
|
||||||
statsFilename: 'bundle-stats.json',
|
|
||||||
})
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
const webpackProdConfig = require('./webpack.production.config');
|
|
||||||
const artifacts = require("../test/artifacts");
|
|
||||||
|
|
||||||
const OUTPATH = artifacts.pathSync("/profiling");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
...webpackProdConfig,
|
|
||||||
output: {
|
|
||||||
...webpackProdConfig.output,
|
|
||||||
path: OUTPATH,
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
...webpackProdConfig.resolve,
|
|
||||||
alias: {
|
|
||||||
...webpackProdConfig.resolve.alias,
|
|
||||||
'react-dom$': 'react-dom/profiling',
|
|
||||||
'scheduler/tracing': 'scheduler/tracing-profiling',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
|
|
||||||
module.exports = [
|
|
||||||
{
|
|
||||||
test: /\.jsx?$/,
|
|
||||||
exclude: [
|
|
||||||
path.resolve(__dirname, '../node_modules')
|
|
||||||
],
|
|
||||||
use: 'babel-loader'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(eot|ttf|woff|woff2)$/,
|
|
||||||
use: 'file-loader?name=fonts/[name].[ext]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.ico$/,
|
|
||||||
use: 'file-loader?name=[name].[ext]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(gif|jpg|png)$/,
|
|
||||||
use: 'file-loader?name=img/[name].[ext]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.svg$/,
|
|
||||||
use: [
|
|
||||||
'svg-inline-loader'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
|
|
||||||
use: [
|
|
||||||
'style-loader',
|
|
||||||
"css-loader",
|
|
||||||
"sass-loader"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
|
|
||||||
use: [
|
|
||||||
'style-loader',
|
|
||||||
'css-loader'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { defineConfig } from "cypress";
|
||||||
|
import { createRequire } from "module";
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
env: {
|
||||||
|
codeCoverage: {
|
||||||
|
exclude: "cypress/**/*.*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
e2e: {
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
// implement node event listeners here
|
||||||
|
require("@cypress/code-coverage/task")(on, config);
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
baseUrl: "http://localhost:8888",
|
||||||
|
retries: {
|
||||||
|
runMode: 2,
|
||||||
|
openMode: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { MaputnikDriver } from "./maputnik-driver";
|
||||||
|
|
||||||
|
describe("accessibility", () => {
|
||||||
|
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||||
|
beforeAndAfter();
|
||||||
|
|
||||||
|
describe("skip links", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.setStyle("layer");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skip link to layer list", () => {
|
||||||
|
const selector = "root:skip:layer-list";
|
||||||
|
then(get.elementByTestId(selector)).shouldExist();
|
||||||
|
when.tab();
|
||||||
|
then(get.elementByTestId(selector)).shouldBeFocused();
|
||||||
|
when.click(selector);
|
||||||
|
then(get.skipTargetLayerList()).shouldBeFocused();
|
||||||
|
});
|
||||||
|
|
||||||
|
// This fails for some reason only in Chrome, but passes in firefox. Adding a skip here to allow merge and later on we'll decide if we want to fix this or not.
|
||||||
|
it.skip("skip link to layer editor", () => {
|
||||||
|
const selector = "root:skip:layer-editor";
|
||||||
|
then(get.elementByTestId(selector)).shouldExist();
|
||||||
|
when.tab().tab();
|
||||||
|
then(get.elementByTestId(selector)).shouldBeFocused();
|
||||||
|
when.click(selector);
|
||||||
|
then(get.skipTargetLayerEditor()).shouldBeFocused();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skip link to map view", () => {
|
||||||
|
const selector = "root:skip:map-view";
|
||||||
|
then(get.elementByTestId(selector)).shouldExist();
|
||||||
|
when.tab().tab().tab();
|
||||||
|
then(get.elementByTestId(selector)).shouldBeFocused();
|
||||||
|
when.click(selector);
|
||||||
|
then(get.canvas()).shouldBeFocused();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { MaputnikDriver } from "./maputnik-driver";
|
||||||
|
|
||||||
|
describe("history", () => {
|
||||||
|
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
||||||
|
beforeAndAfter();
|
||||||
|
|
||||||
|
let undoKeyCombo: string;
|
||||||
|
let redoKeyCombo: string;
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
const isMac = get.isMac();
|
||||||
|
undoKeyCombo = isMac ? "{meta}z" : "{ctrl}z";
|
||||||
|
redoKeyCombo = isMac ? "{meta}{shift}z" : "{ctrl}y";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("undo/redo", () => {
|
||||||
|
when.setStyle("geojson");
|
||||||
|
when.modal.open();
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
||||||
|
|
||||||
|
when.modal.fillLayers({
|
||||||
|
id: "step 1",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "step 1",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
when.modal.open();
|
||||||
|
when.modal.fillLayers({
|
||||||
|
id: "step 2",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "step 1",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "step 2",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
when.typeKeys(undoKeyCombo);
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "step 1",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
when.typeKeys(undoKeyCombo);
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
||||||
|
|
||||||
|
when.typeKeys(redoKeyCombo);
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "step 1",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
when.typeKeys(redoKeyCombo);
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "step 1",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "step 2",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not redo after undo and value change", () => {
|
||||||
|
when.setStyle("geojson");
|
||||||
|
when.modal.open();
|
||||||
|
when.modal.fillLayers({
|
||||||
|
id: "step 1",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
when.modal.open();
|
||||||
|
when.modal.fillLayers({
|
||||||
|
id: "step 2",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
when.typeKeys(undoKeyCombo);
|
||||||
|
when.typeKeys(undoKeyCombo);
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
||||||
|
|
||||||
|
when.modal.open();
|
||||||
|
when.modal.fillLayers({
|
||||||
|
id: "step 3",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
when.typeKeys(redoKeyCombo);
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "step 3",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { MaputnikDriver } from "./maputnik-driver";
|
||||||
|
|
||||||
|
describe("keyboard", () => {
|
||||||
|
let { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
|
||||||
|
beforeAndAfter();
|
||||||
|
describe("shortcuts", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
given.setupMockBackedResponses();
|
||||||
|
when.setStyle("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ESC should unfocus", () => {
|
||||||
|
const targetSelector = "maputnik-select";
|
||||||
|
when.focus(targetSelector);
|
||||||
|
then(get.elementByTestId(targetSelector)).shouldBeFocused();
|
||||||
|
when.typeKeys("{esc}");
|
||||||
|
then(get.elementByTestId(targetSelector)).shouldNotBeFocused();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'?' should show shortcuts modal", () => {
|
||||||
|
when.typeKeys("?");
|
||||||
|
then(get.elementByTestId("modal:shortcuts")).shouldBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'o' should show open modal", () => {
|
||||||
|
when.typeKeys("o");
|
||||||
|
then(get.elementByTestId("modal:open")).shouldBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'e' should show export modal", () => {
|
||||||
|
when.typeKeys("e");
|
||||||
|
then(get.elementByTestId("modal:export")).shouldBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'d' should show sources modal", () => {
|
||||||
|
when.typeKeys("d");
|
||||||
|
then(get.elementByTestId("modal:sources")).shouldBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'s' should show settings modal", () => {
|
||||||
|
when.typeKeys("s");
|
||||||
|
then(get.elementByTestId("modal:settings")).shouldBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'i' should change map to inspect mode", () => {
|
||||||
|
when.typeKeys("i");
|
||||||
|
then(get.inputValue("maputnik-select")).shouldEqual("inspect");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'m' should focus map", () => {
|
||||||
|
when.typeKeys("m");
|
||||||
|
then(get.canvas()).shouldBeFocused();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'!' should show debug modal", () => {
|
||||||
|
when.typeKeys("!");
|
||||||
|
then(get.elementByTestId("modal:debug")).shouldBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,478 @@
|
|||||||
|
import { v1 as uuid } from "uuid";
|
||||||
|
import { MaputnikDriver } from "./maputnik-driver";
|
||||||
|
|
||||||
|
describe("layers", () => {
|
||||||
|
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||||
|
beforeAndAfter();
|
||||||
|
beforeEach(() => {
|
||||||
|
when.setStyle("both");
|
||||||
|
when.modal.open();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ops", () => {
|
||||||
|
let id: string;
|
||||||
|
beforeEach(() => {
|
||||||
|
id = when.modal.fillLayers({
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update layers in local storage", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when clicking delete", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.click("layer-list-item:" + id + ":delete");
|
||||||
|
});
|
||||||
|
it("should empty layers in local storage", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when clicking duplicate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.click("layer-list-item:" + id + ":copy");
|
||||||
|
});
|
||||||
|
it("should add copy layer in local storage", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id + "-copy",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when clicking hide", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update visibility to none in local storage", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
layout: {
|
||||||
|
visibility: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when clicking show", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update visibility to visible in local storage", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
layout: {
|
||||||
|
visibility: "visible",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("background", () => {
|
||||||
|
it("add", () => {
|
||||||
|
let id = when.modal.fillLayers({
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("modify", () => {
|
||||||
|
function createBackground() {
|
||||||
|
// Setup
|
||||||
|
let id = uuid();
|
||||||
|
|
||||||
|
when.selectWithin("add-layer.layer-type", "background");
|
||||||
|
when.setValue("add-layer.layer-id.input", "background:" + id);
|
||||||
|
|
||||||
|
when.click("add-layer");
|
||||||
|
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "background:" + id,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====> THESE SHOULD BE FROM THE SPEC
|
||||||
|
describe("layer", () => {
|
||||||
|
it("expand/collapse");
|
||||||
|
it("id", () => {
|
||||||
|
let bgId = createBackground();
|
||||||
|
|
||||||
|
when.click("layer-list-item:background:" + bgId);
|
||||||
|
|
||||||
|
let id = uuid();
|
||||||
|
when.setValue("layer-editor.layer-id.input", "foobar:" + id);
|
||||||
|
when.click("min-zoom");
|
||||||
|
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "foobar:" + id,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("min-zoom", () => {
|
||||||
|
let bgId: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
bgId = createBackground();
|
||||||
|
when.click("layer-list-item:background:" + bgId);
|
||||||
|
when.setValue("min-zoom.input-text", "1");
|
||||||
|
when.click("layer-editor.layer-id");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update min-zoom in local storage", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "background:" + bgId,
|
||||||
|
type: "background",
|
||||||
|
minzoom: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when clicking next layer should update style on local storage", () => {
|
||||||
|
when.type("min-zoom.input-text", "{backspace}");
|
||||||
|
when.click("max-zoom.input-text");
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "background:" + bgId,
|
||||||
|
type: "background",
|
||||||
|
minzoom: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("max-zoom", () => {
|
||||||
|
let bgId: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
bgId = createBackground();
|
||||||
|
when.click("layer-list-item:background:" + bgId);
|
||||||
|
when.setValue("max-zoom.input-text", "1");
|
||||||
|
when.click("layer-editor.layer-id");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update style in local storage", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "background:" + bgId,
|
||||||
|
type: "background",
|
||||||
|
maxzoom: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("comments", () => {
|
||||||
|
let bgId: string;
|
||||||
|
let comment = "42";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
bgId = createBackground();
|
||||||
|
when.click("layer-list-item:background:" + bgId);
|
||||||
|
when.setValue("layer-comment.input", comment);
|
||||||
|
when.click("layer-editor.layer-id");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update style in local storage", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "background:" + bgId,
|
||||||
|
type: "background",
|
||||||
|
metadata: {
|
||||||
|
"maputnik:comment": comment,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when unsetting", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.clear("layer-comment.input");
|
||||||
|
when.click("min-zoom.input-text");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update style in local storage", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "background:" + bgId,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("color", () => {
|
||||||
|
let bgId: string;
|
||||||
|
beforeEach(() => {
|
||||||
|
bgId = createBackground();
|
||||||
|
when.click("layer-list-item:background:" + bgId);
|
||||||
|
when.click("spec-field:background-color");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update style in local storage", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: "background:" + bgId,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter", () => {
|
||||||
|
it("expand/collapse");
|
||||||
|
it("compound filter");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("paint", () => {
|
||||||
|
it("expand/collapse");
|
||||||
|
it("color");
|
||||||
|
it("pattern");
|
||||||
|
it("opacity");
|
||||||
|
});
|
||||||
|
// <=====
|
||||||
|
|
||||||
|
describe("json-editor", () => {
|
||||||
|
it("expand/collapse");
|
||||||
|
it("modify");
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
it.skip("parse error", () => {
|
||||||
|
let bgId = createBackground();
|
||||||
|
|
||||||
|
when.click("layer-list-item:background:" + bgId);
|
||||||
|
|
||||||
|
let errorSelector = ".CodeMirror-lint-marker-error";
|
||||||
|
then(get.elementByTestId(errorSelector)).shouldNotExist();
|
||||||
|
|
||||||
|
when.click(".CodeMirror");
|
||||||
|
when.typeKeys(
|
||||||
|
"\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"
|
||||||
|
);
|
||||||
|
then(get.elementByTestId(errorSelector)).shouldExist();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fill", () => {
|
||||||
|
it("add", () => {
|
||||||
|
let id = when.modal.fillLayers({
|
||||||
|
type: "fill",
|
||||||
|
layer: "example",
|
||||||
|
});
|
||||||
|
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "fill",
|
||||||
|
source: "example",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Change source
|
||||||
|
it("change source");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("line", () => {
|
||||||
|
it("add", () => {
|
||||||
|
let id = when.modal.fillLayers({
|
||||||
|
type: "line",
|
||||||
|
layer: "example",
|
||||||
|
});
|
||||||
|
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "line",
|
||||||
|
source: "example",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("groups", () => {
|
||||||
|
// TODO
|
||||||
|
// Click each of the layer groups.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("symbol", () => {
|
||||||
|
it("add", () => {
|
||||||
|
let id = when.modal.fillLayers({
|
||||||
|
type: "symbol",
|
||||||
|
layer: "example",
|
||||||
|
});
|
||||||
|
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "symbol",
|
||||||
|
source: "example",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("raster", () => {
|
||||||
|
it("add", () => {
|
||||||
|
let id = when.modal.fillLayers({
|
||||||
|
type: "raster",
|
||||||
|
layer: "raster",
|
||||||
|
});
|
||||||
|
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "raster",
|
||||||
|
source: "raster",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("circle", () => {
|
||||||
|
it("add", () => {
|
||||||
|
let id = when.modal.fillLayers({
|
||||||
|
type: "circle",
|
||||||
|
layer: "example",
|
||||||
|
});
|
||||||
|
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "circle",
|
||||||
|
source: "example",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fill extrusion", () => {
|
||||||
|
it("add", () => {
|
||||||
|
let id = when.modal.fillLayers({
|
||||||
|
type: "fill-extrusion",
|
||||||
|
layer: "example",
|
||||||
|
});
|
||||||
|
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "fill-extrusion",
|
||||||
|
source: "example",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("groups", () => {
|
||||||
|
it("simple", () => {
|
||||||
|
when.setStyle("geojson");
|
||||||
|
|
||||||
|
when.modal.open();
|
||||||
|
when.modal.fillLayers({
|
||||||
|
id: "foo",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
when.modal.open();
|
||||||
|
when.modal.fillLayers({
|
||||||
|
id: "foo_bar",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
when.modal.open();
|
||||||
|
when.modal.fillLayers({
|
||||||
|
id: "foo_bar_baz",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
||||||
|
then(get.elementByTestId("layer-list-item:foo_bar")).shouldNotBeVisible();
|
||||||
|
then(
|
||||||
|
get.elementByTestId("layer-list-item:foo_bar_baz")
|
||||||
|
).shouldNotBeVisible();
|
||||||
|
when.click("layer-list-group:foo-0");
|
||||||
|
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
||||||
|
then(get.elementByTestId("layer-list-item:foo_bar")).shouldBeVisible();
|
||||||
|
then(
|
||||||
|
get.elementByTestId("layer-list-item:foo_bar_baz")
|
||||||
|
).shouldBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { MaputnikDriver } from "./maputnik-driver";
|
||||||
|
|
||||||
|
describe("map", () => {
|
||||||
|
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||||
|
beforeAndAfter();
|
||||||
|
describe("zoom level", () => {
|
||||||
|
it("via url", () => {
|
||||||
|
let zoomLevel = 12.37;
|
||||||
|
when.setStyle("geojson", zoomLevel);
|
||||||
|
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
||||||
|
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
|
||||||
|
"Zoom: " + zoomLevel
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("via map controls", () => {
|
||||||
|
let zoomLevel = 12.37;
|
||||||
|
when.setStyle("geojson", zoomLevel);
|
||||||
|
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
||||||
|
when.clickZoomIn();
|
||||||
|
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
|
||||||
|
"Zoom: " + (zoomLevel + 1)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("search", () => {
|
||||||
|
it('should exist', () => {
|
||||||
|
then(get.searchControl()).shouldBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
||||||
|
|
||||||
|
export default class MaputnikCypressHelper {
|
||||||
|
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
|
||||||
|
|
||||||
|
public given = {
|
||||||
|
...this.helper.given,
|
||||||
|
};
|
||||||
|
|
||||||
|
public get = {
|
||||||
|
...this.helper.get,
|
||||||
|
};
|
||||||
|
|
||||||
|
public when = {
|
||||||
|
...this.helper.when,
|
||||||
|
};
|
||||||
|
|
||||||
|
public beforeAndAfter = this.helper.beforeAndAfter;
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
||||||
|
import { Assertable, then } from "@shellygo/cypress-test-utils/assertable";
|
||||||
|
import MaputnikCypressHelper from "./maputnik-cypress-helper";
|
||||||
|
import ModalDriver from "./modal-driver";
|
||||||
|
const baseUrl = "http://localhost:8888/";
|
||||||
|
|
||||||
|
const styleFromWindow = (win: Window) => {
|
||||||
|
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
||||||
|
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
|
||||||
|
const obj = JSON.parse(styleItem || "");
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MaputnikAssertable<T> extends Assertable<T> {
|
||||||
|
shouldEqualToStoredStyle = () =>
|
||||||
|
then(
|
||||||
|
new CypressHelper().get.window().then((win) => {
|
||||||
|
const style = styleFromWindow(win);
|
||||||
|
then(this.chainable).shouldDeepNestedInclude(style);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MaputnikDriver {
|
||||||
|
private helper = new MaputnikCypressHelper();
|
||||||
|
private modalDriver = new ModalDriver();
|
||||||
|
|
||||||
|
public beforeAndAfter = () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
this.given.setupMockBackedResponses();
|
||||||
|
this.when.setStyle("both");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public then = (chainable: Cypress.Chainable<any>) =>
|
||||||
|
new MaputnikAssertable(chainable);
|
||||||
|
|
||||||
|
public given = {
|
||||||
|
...this.helper.given,
|
||||||
|
setupMockBackedResponses: () => {
|
||||||
|
this.helper.given.interceptAndMockResponse({
|
||||||
|
method: "GET",
|
||||||
|
url: baseUrl + "example-style.json",
|
||||||
|
response: {
|
||||||
|
fixture: "example-style.json",
|
||||||
|
},
|
||||||
|
alias: "example-style.json",
|
||||||
|
});
|
||||||
|
this.helper.given.interceptAndMockResponse({
|
||||||
|
method: "GET",
|
||||||
|
url: baseUrl + "example-layer-style.json",
|
||||||
|
response: {
|
||||||
|
fixture: "example-layer-style.json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.helper.given.interceptAndMockResponse({
|
||||||
|
method: "GET",
|
||||||
|
url: baseUrl + "geojson-style.json",
|
||||||
|
response: {
|
||||||
|
fixture: "geojson-style.json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.helper.given.interceptAndMockResponse({
|
||||||
|
method: "GET",
|
||||||
|
url: baseUrl + "raster-style.json",
|
||||||
|
response: {
|
||||||
|
fixture: "raster-style.json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.helper.given.interceptAndMockResponse({
|
||||||
|
method: "GET",
|
||||||
|
url: baseUrl + "geojson-raster-style.json",
|
||||||
|
response: {
|
||||||
|
fixture: "geojson-raster-style.json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.helper.given.interceptAndMockResponse({
|
||||||
|
method: "GET",
|
||||||
|
url: "*example.local/*",
|
||||||
|
response: [],
|
||||||
|
});
|
||||||
|
this.helper.given.interceptAndMockResponse({
|
||||||
|
method: "GET",
|
||||||
|
url: "*example.com/*",
|
||||||
|
response: [],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public when = {
|
||||||
|
...this.helper.when,
|
||||||
|
modal: this.modalDriver.when,
|
||||||
|
within: (selector: string, fn: () => void) => {
|
||||||
|
this.helper.when.within(fn, selector);
|
||||||
|
},
|
||||||
|
tab: () => this.helper.get.element("body").tab(),
|
||||||
|
waitForExampleFileResponse: () => {
|
||||||
|
this.helper.when.waitForResponse("example-style.json");
|
||||||
|
},
|
||||||
|
chooseExampleFile: () => {
|
||||||
|
this.helper.get
|
||||||
|
.bySelector("type", "file")
|
||||||
|
.selectFile("cypress/fixtures/example-style.json", { force: true });
|
||||||
|
},
|
||||||
|
setStyle: (
|
||||||
|
styleProperties: "geojson" | "raster" | "both" | "layer" | "",
|
||||||
|
zoom?: number
|
||||||
|
) => {
|
||||||
|
let url = "?debug";
|
||||||
|
switch (styleProperties) {
|
||||||
|
case "geojson":
|
||||||
|
url += `&style=${baseUrl}geojson-style.json`;
|
||||||
|
break;
|
||||||
|
case "raster":
|
||||||
|
url += `&style=${baseUrl}raster-style.json`;
|
||||||
|
break;
|
||||||
|
case "both":
|
||||||
|
url += `&style=${baseUrl}geojson-raster-style.json`;
|
||||||
|
break;
|
||||||
|
case "layer":
|
||||||
|
url += `&style=${baseUrl}/example-layer-style.json`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (zoom) {
|
||||||
|
url += `#${zoom}/41.3805/2.1635`;
|
||||||
|
}
|
||||||
|
this.helper.when.visit(baseUrl + url);
|
||||||
|
if (styleProperties) {
|
||||||
|
this.helper.when.acceptConfirm();
|
||||||
|
}
|
||||||
|
// when methods should not include assertions
|
||||||
|
this.helper.get.elementByTestId("toolbar:link").should("be.visible");
|
||||||
|
},
|
||||||
|
|
||||||
|
typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
|
||||||
|
|
||||||
|
clickZoomIn: () => {
|
||||||
|
this.helper.get.element(".maplibregl-ctrl-zoom-in").click();
|
||||||
|
},
|
||||||
|
|
||||||
|
selectWithin: (selector: string, value: string) => {
|
||||||
|
this.when.within(selector, () => {
|
||||||
|
this.helper.get.element("select").select(value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
select: (selector: string, value: string) => {
|
||||||
|
this.helper.get.elementByTestId(selector).select(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
focus: (selector: string) => {
|
||||||
|
this.helper.when.focus(selector);
|
||||||
|
},
|
||||||
|
|
||||||
|
setValue: (selector: string, text: string) => {
|
||||||
|
this.helper.get
|
||||||
|
.elementByTestId(selector)
|
||||||
|
.clear()
|
||||||
|
.type(text, { parseSpecialCharSequences: false });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public get = {
|
||||||
|
...this.helper.get,
|
||||||
|
isMac: () => {
|
||||||
|
return Cypress.platform === "darwin";
|
||||||
|
},
|
||||||
|
|
||||||
|
styleFromLocalStorage: () =>
|
||||||
|
this.helper.get.window().then((win) => styleFromWindow(win)),
|
||||||
|
|
||||||
|
exampleFileUrl: () => {
|
||||||
|
return baseUrl + "example-style.json";
|
||||||
|
},
|
||||||
|
skipTargetLayerList: () =>
|
||||||
|
this.helper.get.elementByTestId("skip-target-layer-list"),
|
||||||
|
skipTargetLayerEditor: () =>
|
||||||
|
this.helper.get.elementByTestId("skip-target-layer-editor"),
|
||||||
|
canvas: () => this.helper.get.element("canvas"),
|
||||||
|
searchControl: () => this.helper.get.element('.maplibregl-ctrl-geocoder')
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { v1 as uuid } from "uuid";
|
||||||
|
import MaputnikCypressHelper from "./maputnik-cypress-helper";
|
||||||
|
|
||||||
|
export default class ModalDriver {
|
||||||
|
private helper = new MaputnikCypressHelper();
|
||||||
|
|
||||||
|
public when = {
|
||||||
|
fillLayers: (opts: { type: string; layer?: string; id?: string }) => {
|
||||||
|
// Having logic in test code is an anti pattern.
|
||||||
|
// This should be splitted to multiple single responsibility functions
|
||||||
|
let type = opts.type;
|
||||||
|
let layer = opts.layer;
|
||||||
|
let id;
|
||||||
|
if (opts.id) {
|
||||||
|
id = opts.id;
|
||||||
|
} else {
|
||||||
|
id = `${type}:${uuid()}`;
|
||||||
|
}
|
||||||
|
this.helper.when.selectOption("add-layer.layer-type.select", type);
|
||||||
|
this.helper.when.type("add-layer.layer-id.input", id);
|
||||||
|
|
||||||
|
if (layer) {
|
||||||
|
this.helper.when.within(() => {
|
||||||
|
this.helper.get.element("input").type(layer!);
|
||||||
|
}, "add-layer.layer-source-block");
|
||||||
|
}
|
||||||
|
this.helper.when.click("add-layer");
|
||||||
|
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
|
||||||
|
open: () => {
|
||||||
|
this.helper.when.click("layer-list:add-layer");
|
||||||
|
},
|
||||||
|
|
||||||
|
close: (key: string) => {
|
||||||
|
this.helper.when.click(key + ".close-modal");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import { MaputnikDriver } from "./maputnik-driver";
|
||||||
|
|
||||||
|
describe("modals", () => {
|
||||||
|
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
||||||
|
beforeAndAfter();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
when.setStyle("");
|
||||||
|
});
|
||||||
|
describe("open", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.click("nav:open");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("close", () => {
|
||||||
|
when.modal.close("modal:open");
|
||||||
|
then(get.elementByTestId("modal:open")).shouldNotExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip("upload", () => {
|
||||||
|
// HM: I was not able to make the following choose file actually to select a file and close the modal...
|
||||||
|
when.chooseExampleFile();
|
||||||
|
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when click open url", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
let styleFileUrl = get.exampleFileUrl();
|
||||||
|
|
||||||
|
when.setValue("modal:open.url.input", styleFileUrl);
|
||||||
|
when.click("modal:open.url.button");
|
||||||
|
when.wait(200);
|
||||||
|
});
|
||||||
|
it("load from url", () => {
|
||||||
|
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shortcuts", () => {
|
||||||
|
it("open/close", () => {
|
||||||
|
when.setStyle("");
|
||||||
|
when.typeKeys("?");
|
||||||
|
when.modal.close("modal:shortcuts");
|
||||||
|
then(get.elementByTestId("modal:shortcuts")).shouldNotExist();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("export", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.click("nav:export");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("close", () => {
|
||||||
|
when.modal.close("modal:export");
|
||||||
|
then(get.elementByTestId("modal:export")).shouldNotExist();
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Work out how to download a file and check the contents
|
||||||
|
it("download");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sources", () => {
|
||||||
|
it("active sources");
|
||||||
|
it("public source");
|
||||||
|
it("add new source");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("inspect", () => {
|
||||||
|
it("toggle", () => {
|
||||||
|
// There is no assertion in this test
|
||||||
|
when.setStyle("geojson");
|
||||||
|
when.select("maputnik-select", "inspect");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("style settings", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.click("nav:settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when click name filed spec information", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.click("field-doc-button-Name");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the spec information", () => {
|
||||||
|
then(get.elementsText("spec-field-doc")).shouldInclude(
|
||||||
|
"name for the style"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when set name and click owner", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.setValue("modal:settings.name", "foobar");
|
||||||
|
when.click("modal:settings.owner");
|
||||||
|
when.wait(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("show name specifications", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
name: "foobar",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when set owner and click name", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.setValue("modal:settings.owner", "foobar");
|
||||||
|
when.click("modal:settings.name");
|
||||||
|
when.wait(200);
|
||||||
|
});
|
||||||
|
it("should update owner in local storage", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
owner: "foobar",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sprite url", () => {
|
||||||
|
when.setValue("modal:settings.sprite", "http://example.com");
|
||||||
|
when.click("modal:settings.name");
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
sprite: "http://example.com",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("glyphs url", () => {
|
||||||
|
let glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
|
||||||
|
when.setValue("modal:settings.glyphs", glyphsUrl);
|
||||||
|
when.click("modal:settings.name");
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
glyphs: glyphsUrl,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maptiler access token", () => {
|
||||||
|
let apiKey = "testing123";
|
||||||
|
when.setValue(
|
||||||
|
"modal:settings.maputnik:openmaptiles_access_token",
|
||||||
|
apiKey
|
||||||
|
);
|
||||||
|
when.click("modal:settings.name");
|
||||||
|
then(
|
||||||
|
get.styleFromLocalStorage().pipe((style) => style.metadata)
|
||||||
|
).shouldInclude({
|
||||||
|
"maputnik:openmaptiles_access_token": apiKey,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("thunderforest access token", () => {
|
||||||
|
let apiKey = "testing123";
|
||||||
|
when.setValue(
|
||||||
|
"modal:settings.maputnik:thunderforest_access_token",
|
||||||
|
apiKey
|
||||||
|
);
|
||||||
|
when.click("modal:settings.name");
|
||||||
|
then(
|
||||||
|
get.styleFromLocalStorage().pipe((style) => style.metadata)
|
||||||
|
).shouldInclude({ "maputnik:thunderforest_access_token": apiKey });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("style renderer", () => {
|
||||||
|
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");
|
||||||
|
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
|
||||||
|
"ol"
|
||||||
|
);
|
||||||
|
|
||||||
|
when.click("modal:settings.name");
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
metadata: { "maputnik:renderer": "ol" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sources", () => {
|
||||||
|
it("toggle");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": 8,
|
"version": 8,
|
||||||
"name": "Test Style",
|
"name": "Test Style",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"maputnik:renderer": "mbgljs"
|
"maputnik:renderer": "mlgljs"
|
||||||
},
|
},
|
||||||
"sources": {},
|
"sources": {},
|
||||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": 8,
|
"version": 8,
|
||||||
"name": "Test Style",
|
"name": "Test Style",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"maputnik:renderer": "mbgljs"
|
"maputnik:renderer": "mlgljs"
|
||||||
},
|
},
|
||||||
"sources": {},
|
"sources": {},
|
||||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"id": "test-style",
|
||||||
|
"version": 8,
|
||||||
|
"name": "Test Style",
|
||||||
|
"metadata": {
|
||||||
|
"maputnik:renderer": "mlgljs"
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"example": {
|
||||||
|
"type": "vector",
|
||||||
|
"data": {
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features":[{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {
|
||||||
|
"name": "Dinagat Islands"
|
||||||
|
},
|
||||||
|
"geometry":{
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [125.6, 10.1]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"raster": {
|
||||||
|
"tileSize": 256,
|
||||||
|
"tiles": ["http://localhost/example/{x}/{y}/{z}"],
|
||||||
|
"type": "raster"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"layers": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"id": "test-style",
|
||||||
|
"version": 8,
|
||||||
|
"name": "Test Style",
|
||||||
|
"metadata": {
|
||||||
|
"maputnik:renderer": "mlgljs"
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"example": {
|
||||||
|
"type": "vector",
|
||||||
|
"data": {
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features":[{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {
|
||||||
|
"name": "Dinagat Islands"
|
||||||
|
},
|
||||||
|
"geometry":{
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [125.6, 10.1]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"layers": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"id": "test-style",
|
||||||
|
"version": 8,
|
||||||
|
"name": "Test Style",
|
||||||
|
"metadata": {
|
||||||
|
"maputnik:renderer": "mlgljs"
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"raster": {
|
||||||
|
"tileSize": 256,
|
||||||
|
"tiles": ["http://localhost/example/{x}/{y}/{z}"],
|
||||||
|
"type": "raster"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"layers": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
// ***********************************************
|
||||||
|
// This example commands.ts shows you how to
|
||||||
|
// create various custom commands and overwrite
|
||||||
|
// existing commands.
|
||||||
|
//
|
||||||
|
// For more comprehensive examples of custom
|
||||||
|
// commands please read more here:
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
// ***********************************************
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a parent command --
|
||||||
|
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a child command --
|
||||||
|
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a dual command --
|
||||||
|
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This will overwrite an existing command --
|
||||||
|
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||||
|
//
|
||||||
|
// declare global {
|
||||||
|
// namespace Cypress {
|
||||||
|
// interface Chainable {
|
||||||
|
// login(email: string, password: string): Chainable<void>
|
||||||
|
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||||
|
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||||
|
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// ***********************************************************
|
||||||
|
// This example support/e2e.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 "@cypress/code-coverage/support";
|
||||||
|
import "cypress-plugin-tab";
|
||||||
|
import "./commands";
|
||||||
|
|
||||||
|
// Alternatively you can use CommonJS syntax:
|
||||||
|
// require('./commands')
|
||||||
@@ -2,9 +2,10 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title>Maputnik</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="src/manifest.json">
|
||||||
|
<link rel="icon" href="src/favicon.ico" type="image/x-icon" />
|
||||||
<style>
|
<style>
|
||||||
html {
|
html {
|
||||||
background-color: rgb(28, 31, 36);
|
background-color: rgb(28, 31, 36);
|
||||||
@@ -23,7 +24,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading__logo svg {
|
.loading__logo img {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
@@ -38,7 +39,6 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- TODO: Import dynamically -->
|
|
||||||
<!-- From <https://github.com/hail2u/color-blindness-emulation> -->
|
<!-- From <https://github.com/hail2u/color-blindness-emulation> -->
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -123,10 +123,10 @@
|
|||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<div class="loading">
|
<div class="loading">
|
||||||
<div class="loading__logo">
|
<div class="loading__logo">
|
||||||
<!-- replaced by 'html-webpack-inline-svg-plugin' -->
|
|
||||||
<img inline src="node_modules/maputnik-design/logos/logo-loading.svg" />
|
<img inline src="node_modules/maputnik-design/logos/logo-loading.svg" />
|
||||||
</div>
|
</div>
|
||||||
<div class="loading__text">Loading…</div>
|
<div class="loading__text">Loading…</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="module" src="/src/index.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Generated
+6489
-39177
File diff suppressed because it is too large
Load Diff
+52
-89
@@ -1,35 +1,32 @@
|
|||||||
{
|
{
|
||||||
"name": "maputnik",
|
"name": "maputnik",
|
||||||
"version": "2.0.0-pre.1",
|
"version": "2.0.0",
|
||||||
"description": "A MapLibre GL visual style editor",
|
"description": "A MapLibre GL visual style editor",
|
||||||
|
"type": "module",
|
||||||
"main": "''",
|
"main": "''",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"stats": "webpack --config config/webpack.production.config.js --progress=profile --json > stats.json",
|
"start": "vite",
|
||||||
"build": "webpack --config config/webpack.production.config.js --progress=profile --color",
|
"build": "tsc && vite build --base=/maputnik/",
|
||||||
"profiling-build": "webpack --config config/webpack.profiling.config.js --progress=profile --color",
|
"lint": "eslint ./src ./cypress --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"test": "cross-env NODE_ENV=test wdio config/wdio.conf.js",
|
"test": "cypress run",
|
||||||
"test-watch": "cross-env NODE_ENV=test wdio config/wdio.conf.js --watch",
|
"cy:open": "cypress open",
|
||||||
"start": "webpack-dev-server --progress=profile --color --config config/webpack.config.js",
|
"lint-css": "stylelint \"src/styles/*.scss\""
|
||||||
"start-prod": "webpack-dev-server --progress=profile --color --config config/webpack.production.config.js",
|
|
||||||
"start-sandbox": "webpack-dev-server --disable-host-check --host 0.0.0.0 --progress=profile --color --config config/webpack.production.config.js",
|
|
||||||
"lint-js": "eslint --ext js --ext jsx src test",
|
|
||||||
"lint-css": "stylelint \"src/styles/*.scss\"",
|
|
||||||
"lint": "npm run lint-js && npm run lint-css",
|
|
||||||
"storybook": "start-storybook -h 0.0.0.0 -p 6006",
|
|
||||||
"build-storybook": "build-storybook -o build/storybook"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/maputnik/editor"
|
"url": "https://github.com/maplibre/maputnik"
|
||||||
},
|
},
|
||||||
"author": "Lukas Martinelli",
|
"author": "Lukas Martinelli",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/maputnik/editor#readme",
|
"homepage": "https://github.com/maplibre/maputnik#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.17.9",
|
|
||||||
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
|
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
|
||||||
"@maplibre/maplibre-gl-style-spec": "^17.0.1",
|
"@maplibre/maplibre-gl-geocoder": "^1.5.0",
|
||||||
|
"@maplibre/maplibre-gl-style-spec": "^20.1.0",
|
||||||
|
"@mdi/js": "^6.6.96",
|
||||||
"@mdi/react": "^1.5.0",
|
"@mdi/react": "^1.5.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||||
|
"@typescript-eslint/parser": "^6.16.0",
|
||||||
"array-move": "^4.0.0",
|
"array-move": "^4.0.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
@@ -48,7 +45,7 @@
|
|||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"mapbox-gl-inspect": "^1.3.1",
|
"mapbox-gl-inspect": "^1.3.1",
|
||||||
"maplibre-gl": "^2.4.0",
|
"maplibre-gl": "^4.0.0",
|
||||||
"maputnik-design": "github:maputnik/design#172b06c",
|
"maputnik-design": "github:maputnik/design#172b06c",
|
||||||
"ol": "^6.14.1",
|
"ol": "^6.14.1",
|
||||||
"ol-mapbox-style": "^7.1.1",
|
"ol-mapbox-style": "^7.1.1",
|
||||||
@@ -89,87 +86,53 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
|
||||||
"plugins": [
|
|
||||||
"react"
|
|
||||||
],
|
|
||||||
"extends": [
|
|
||||||
"plugin:react/recommended"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"node": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"parser": "@babel/eslint-parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 6,
|
|
||||||
"sourceType": "module",
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"impliedStrict": true,
|
|
||||||
"experimentalObjectRestSpread": true,
|
|
||||||
"jsx": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"react": {
|
|
||||||
"version": "detect"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.17.9",
|
"@cypress/code-coverage": "^3.12.15",
|
||||||
"@babel/eslint-parser": "^7.19.1",
|
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
"@rollup/plugin-replace": "^5.0.5",
|
||||||
"@babel/plugin-transform-runtime": "^7.17.0",
|
"@shellygo/cypress-test-utils": "^2.0.17",
|
||||||
"@babel/preset-env": "^7.16.11",
|
"@types/codemirror": "^5.60.15",
|
||||||
"@babel/preset-flow": "^7.16.7",
|
"@types/color": "^3.0.6",
|
||||||
"@babel/preset-react": "^7.16.7",
|
"@types/cors": "^2.8.17",
|
||||||
"@mdi/js": "^6.6.96",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@storybook/addon-a11y": "^6.4.20",
|
"@types/geojson": "^7946.0.13",
|
||||||
"@storybook/addon-actions": "^6.4.20",
|
"@types/json-to-ast": "^2.1.4",
|
||||||
"@storybook/addon-links": "^6.4.20",
|
"@types/lodash.capitalize": "^4.2.9",
|
||||||
"@storybook/addon-storysource": "^6.4.20",
|
"@types/lodash.clamp": "^4.0.9",
|
||||||
"@storybook/addons": "^6.4.20",
|
"@types/lodash.clonedeep": "^4.5.9",
|
||||||
"@storybook/react": "^6.4.20",
|
"@types/lodash.get": "^4.4.9",
|
||||||
"@storybook/theming": "^6.4.20",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@wdio/cli": "^7.19.3",
|
"@types/lodash.throttle": "^4.1.9",
|
||||||
"@wdio/local-runner": "^7.19.3",
|
"@types/react": "^16.14.52",
|
||||||
"@wdio/mocha-framework": "^7.19.3",
|
"@types/react-aria-menubutton": "^6.2.13",
|
||||||
"@wdio/selenium-standalone-service": "^7.19.1",
|
"@types/react-aria-modal": "^4.0.9",
|
||||||
"@wdio/spec-reporter": "^7.19.1",
|
"@types/react-autocomplete": "^1.8.9",
|
||||||
"babel-loader": "^8.2.4",
|
"@types/react-collapse": "^5.0.4",
|
||||||
"babel-plugin-istanbul": "^6.1.1",
|
"@types/react-color": "^3.0.10",
|
||||||
"babel-plugin-static-fs": "^3.0.0",
|
"@types/react-dom": "^16.9.24",
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"@types/react-file-reader-input": "^2.0.4",
|
||||||
|
"@types/react-icon-base": "^2.1.6",
|
||||||
|
"@types/string-hash": "^1.1.3",
|
||||||
|
"@types/uuid": "^9.0.7",
|
||||||
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-env": "^7.0.3",
|
"cypress": "^13.6.1",
|
||||||
"css-loader": "^5.2.7",
|
"eslint": "^8.56.0",
|
||||||
"eslint": "^8.12.0",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react": "^7.29.4",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
"express": "^4.17.3",
|
"express": "^4.17.3",
|
||||||
"html-webpack-inline-svg-plugin": "^2.3.0",
|
|
||||||
"html-webpack-plugin": "^4.5.2",
|
|
||||||
"istanbul": "^0.4.5",
|
"istanbul": "^0.4.5",
|
||||||
"istanbul-lib-coverage": "^3.2.0",
|
"istanbul-lib-coverage": "^3.2.0",
|
||||||
"mkdirp": "^1.0.4",
|
|
||||||
"mocha": "^9.2.2",
|
"mocha": "^9.2.2",
|
||||||
"postcss": "^8.4.12",
|
"postcss": "^8.4.12",
|
||||||
"react-hot-loader": "^4.13.0",
|
"react-hot-loader": "^4.13.0",
|
||||||
"sass-loader": "^10.2.1",
|
|
||||||
"style-loader": "^2.0.0",
|
|
||||||
"stylelint": "^14.6.1",
|
"stylelint": "^14.6.1",
|
||||||
"stylelint-config-recommended-scss": "^6.0.0",
|
"stylelint-config-recommended-scss": "^6.0.0",
|
||||||
"stylelint-scss": "^4.2.0",
|
"stylelint-scss": "^4.2.0",
|
||||||
"svg-inline-loader": "^0.8.2",
|
"typescript": "^5.3.3",
|
||||||
"transform-loader": "^0.2.4",
|
|
||||||
"typescript": "^4.6.3",
|
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"webdriverio": "^7.19.3",
|
"vite": "^5.0.12",
|
||||||
"webpack": "^4.46.0",
|
"vite-plugin-istanbul": "^5.0.0"
|
||||||
"webpack-bundle-analyzer": "^4.5.0",
|
|
||||||
"webpack-cleanup-plugin": "^0.5.1",
|
|
||||||
"webpack-cli": "^4.9.2",
|
|
||||||
"webpack-dev-server": "^4.8.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"container": {
|
|
||||||
"startScript": "start-sandbox"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-ignore - this can be easily replaced with arrow functions
|
||||||
import autoBind from 'react-autobind';
|
import autoBind from 'react-autobind';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import cloneDeep from 'lodash.clonedeep'
|
import cloneDeep from 'lodash.clonedeep'
|
||||||
@@ -6,14 +7,15 @@ import buffer from 'buffer'
|
|||||||
import get from 'lodash.get'
|
import get from 'lodash.get'
|
||||||
import {unset} from 'lodash'
|
import {unset} from 'lodash'
|
||||||
import {arrayMoveMutable} from 'array-move'
|
import {arrayMoveMutable} from 'array-move'
|
||||||
import url from 'url'
|
|
||||||
import hash from "string-hash";
|
import hash from "string-hash";
|
||||||
|
import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl'
|
||||||
|
import {latest, validateStyleMin} from '@maplibre/maplibre-gl-style-spec'
|
||||||
|
|
||||||
import MapMapboxGl from './MapMapboxGl'
|
import MapMaplibreGl from './MapMaplibreGl'
|
||||||
import MapOpenLayers from './MapOpenLayers'
|
import MapOpenLayers from './MapOpenLayers'
|
||||||
import LayerList from './LayerList'
|
import LayerList from './LayerList'
|
||||||
import LayerEditor from './LayerEditor'
|
import LayerEditor from './LayerEditor'
|
||||||
import AppToolbar from './AppToolbar'
|
import AppToolbar, { MapState } from './AppToolbar'
|
||||||
import AppLayout from './AppLayout'
|
import AppLayout from './AppLayout'
|
||||||
import MessagePanel from './AppMessagePanel'
|
import MessagePanel from './AppMessagePanel'
|
||||||
|
|
||||||
@@ -25,8 +27,7 @@ import ModalShortcuts from './ModalShortcuts'
|
|||||||
import ModalSurvey from './ModalSurvey'
|
import ModalSurvey from './ModalSurvey'
|
||||||
import ModalDebug from './ModalDebug'
|
import ModalDebug from './ModalDebug'
|
||||||
|
|
||||||
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
|
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
|
||||||
import {latest, validate} from '@maplibre/maplibre-gl-style-spec'
|
|
||||||
import style from '../libs/style'
|
import style from '../libs/style'
|
||||||
import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
|
import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
|
||||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
||||||
@@ -37,12 +38,13 @@ 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 Debug from '../libs/debug'
|
||||||
import {formatLayerId} from '../util/format';
|
import { SortEnd } from 'react-sortable-hoc';
|
||||||
|
import { MapOptions } from 'maplibre-gl';
|
||||||
|
|
||||||
// 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;
|
||||||
|
|
||||||
function setFetchAccessToken(url, mapStyle) {
|
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/);
|
||||||
@@ -63,7 +65,7 @@ function setFetchAccessToken(url, mapStyle) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRootSpec(spec, fieldName, newValues) {
|
function updateRootSpec(spec: any, fieldName: string, newValues: any) {
|
||||||
return {
|
return {
|
||||||
...spec,
|
...spec,
|
||||||
$root: {
|
$root: {
|
||||||
@@ -76,15 +78,75 @@ function updateRootSpec(spec, fieldName, newValues) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class App extends React.Component {
|
type OnStyleChangedOpts = {
|
||||||
constructor(props) {
|
save?: boolean
|
||||||
|
addRevision?: boolean
|
||||||
|
initialLoad?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type MappedErrors = {
|
||||||
|
message: string
|
||||||
|
parsed?: {
|
||||||
|
type: string
|
||||||
|
data: {
|
||||||
|
index: number
|
||||||
|
key: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppState = {
|
||||||
|
errors: MappedErrors[],
|
||||||
|
infos: string[],
|
||||||
|
mapStyle: StyleSpecification & {id: string},
|
||||||
|
dirtyMapStyle?: StyleSpecification,
|
||||||
|
selectedLayerIndex: number,
|
||||||
|
selectedLayerOriginalId?: string,
|
||||||
|
sources: {[key: string]: SourceSpecification},
|
||||||
|
vectorLayers: {},
|
||||||
|
spec: any,
|
||||||
|
mapView: {
|
||||||
|
zoom: number,
|
||||||
|
center: {
|
||||||
|
lng: number,
|
||||||
|
lat: number,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
maplibreGlDebugOptions: Partial<MapOptions> & {
|
||||||
|
showTileBoundaries: boolean,
|
||||||
|
showCollisionBoxes: boolean,
|
||||||
|
showOverdrawInspector: boolean,
|
||||||
|
},
|
||||||
|
openlayersDebugOptions: {
|
||||||
|
debugToolbox: boolean,
|
||||||
|
},
|
||||||
|
mapState: MapState
|
||||||
|
isOpen: {
|
||||||
|
settings: boolean
|
||||||
|
sources: boolean
|
||||||
|
open: boolean
|
||||||
|
shortcuts: boolean
|
||||||
|
export: boolean
|
||||||
|
survey: boolean
|
||||||
|
debug: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class App extends React.Component<any, AppState> {
|
||||||
|
revisionStore: RevisionStore;
|
||||||
|
styleStore: StyleStore | ApiStyleStore;
|
||||||
|
layerWatcher: LayerWatcher;
|
||||||
|
shortcutEl: ModalShortcuts | null = null;
|
||||||
|
|
||||||
|
constructor(props: any) {
|
||||||
super(props)
|
super(props)
|
||||||
autoBind(this);
|
autoBind(this);
|
||||||
|
|
||||||
this.revisionStore = new RevisionStore()
|
this.revisionStore = new RevisionStore()
|
||||||
const params = new URLSearchParams(window.location.search.substring(1))
|
const params = new URLSearchParams(window.location.search.substring(1))
|
||||||
let port = params.get("localport")
|
let port = params.get("localport")
|
||||||
if (port == null && (window.location.port != 80 && window.location.port != 443)) {
|
if (port == null && (window.location.port !== "80" && window.location.port !== "443")) {
|
||||||
port = window.location.port
|
port = window.location.port
|
||||||
}
|
}
|
||||||
this.styleStore = new ApiStyleStore({
|
this.styleStore = new ApiStyleStore({
|
||||||
@@ -136,7 +198,7 @@ export default class App extends React.Component {
|
|||||||
{
|
{
|
||||||
key: "m",
|
key: "m",
|
||||||
handler: () => {
|
handler: () => {
|
||||||
document.querySelector(".mapboxgl-canvas").focus();
|
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -149,7 +211,7 @@ export default class App extends React.Component {
|
|||||||
|
|
||||||
document.body.addEventListener("keyup", (e) => {
|
document.body.addEventListener("keyup", (e) => {
|
||||||
if(e.key === "Escape") {
|
if(e.key === "Escape") {
|
||||||
e.target.blur();
|
(e.target as HTMLElement).blur();
|
||||||
document.body.focus();
|
document.body.focus();
|
||||||
}
|
}
|
||||||
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
|
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
|
||||||
@@ -159,7 +221,7 @@ export default class App extends React.Component {
|
|||||||
|
|
||||||
if(shortcut) {
|
if(shortcut) {
|
||||||
this.setModal("shortcuts", false);
|
this.setModal("shortcuts", false);
|
||||||
shortcut.handler(e);
|
shortcut.handler();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -192,8 +254,6 @@ export default class App extends React.Component {
|
|||||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryObj = url.parse(window.location.href, true).query;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
errors: [],
|
errors: [],
|
||||||
infos: [],
|
infos: [],
|
||||||
@@ -220,7 +280,7 @@ export default class App extends React.Component {
|
|||||||
survey: false,
|
survey: false,
|
||||||
debug: false,
|
debug: false,
|
||||||
},
|
},
|
||||||
mapboxGlDebugOptions: {
|
maplibreGlDebugOptions: {
|
||||||
showTileBoundaries: false,
|
showTileBoundaries: false,
|
||||||
showCollisionBoxes: false,
|
showCollisionBoxes: false,
|
||||||
showOverdrawInspector: false,
|
showOverdrawInspector: false,
|
||||||
@@ -235,25 +295,25 @@ export default class App extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyPress = (e) => {
|
handleKeyPress = (e: KeyboardEvent) => {
|
||||||
if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
|
if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
|
||||||
if(e.metaKey && e.shiftKey && e.keyCode === 90) {
|
if(e.metaKey && e.shiftKey && e.keyCode === 90) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.onRedo(e);
|
this.onRedo();
|
||||||
}
|
}
|
||||||
else if(e.metaKey && e.keyCode === 90) {
|
else if(e.metaKey && e.keyCode === 90) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.onUndo(e);
|
this.onUndo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if(e.ctrlKey && e.keyCode === 90) {
|
if(e.ctrlKey && e.keyCode === 90) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.onUndo(e);
|
this.onUndo();
|
||||||
}
|
}
|
||||||
else if(e.ctrlKey && e.keyCode === 89) {
|
else if(e.ctrlKey && e.keyCode === 89) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.onRedo(e);
|
this.onRedo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,31 +326,31 @@ export default class App extends React.Component {
|
|||||||
window.removeEventListener("keydown", this.handleKeyPress);
|
window.removeEventListener("keydown", this.handleKeyPress);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveStyle(snapshotStyle) {
|
saveStyle(snapshotStyle: StyleSpecification & {id: string}) {
|
||||||
this.styleStore.save(snapshotStyle)
|
this.styleStore.save(snapshotStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFonts(urlTemplate) {
|
updateFonts(urlTemplate: string) {
|
||||||
const metadata = this.state.mapStyle.metadata || {}
|
const metadata: {[key: string]: string} = this.state.mapStyle.metadata || {} as any
|
||||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
||||||
|
|
||||||
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
|
const glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
|
||||||
downloadGlyphsMetadata(glyphUrl, fonts => {
|
downloadGlyphsMetadata(glyphUrl, fonts => {
|
||||||
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
updateIcons(baseUrl) {
|
updateIcons(baseUrl: string) {
|
||||||
downloadSpriteMetadata(baseUrl, icons => {
|
downloadSpriteMetadata(baseUrl, icons => {
|
||||||
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
|
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeMetadataProperty = (property, value) => {
|
onChangeMetadataProperty = (property: string, value: any) => {
|
||||||
// If we're changing renderer reset the map state.
|
// If we're changing renderer reset the map state.
|
||||||
if (
|
if (
|
||||||
property === 'maputnik:renderer' &&
|
property === 'maputnik:renderer' &&
|
||||||
value !== get(this.state.mapStyle, ['metadata', 'maputnik:renderer'], 'mbgljs')
|
value !== get(this.state.mapStyle, ['metadata', 'maputnik:renderer'], 'mlgljs')
|
||||||
) {
|
) {
|
||||||
this.setState({
|
this.setState({
|
||||||
mapState: 'map'
|
mapState: 'map'
|
||||||
@@ -300,14 +360,14 @@ export default class App extends React.Component {
|
|||||||
const changedStyle = {
|
const changedStyle = {
|
||||||
...this.state.mapStyle,
|
...this.state.mapStyle,
|
||||||
metadata: {
|
metadata: {
|
||||||
...this.state.mapStyle.metadata,
|
...(this.state.mapStyle as any).metadata,
|
||||||
[property]: value
|
[property]: value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.onStyleChanged(changedStyle)
|
this.onStyleChanged(changedStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
onStyleChanged = (newStyle, opts={}) => {
|
onStyleChanged = (newStyle: StyleSpecification & {id: string}, opts: OnStyleChangedOpts={}) => {
|
||||||
opts = {
|
opts = {
|
||||||
save: true,
|
save: true,
|
||||||
addRevision: true,
|
addRevision: true,
|
||||||
@@ -319,16 +379,15 @@ export default class App extends React.Component {
|
|||||||
this.getInitialStateFromUrl(newStyle);
|
this.getInitialStateFromUrl(newStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors = validate(newStyle, latest) || [];
|
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 = [];
|
const layerErrors: (Error | ValidationError)[] = [];
|
||||||
if (newStyle && newStyle.layers) {
|
if (newStyle && newStyle.layers) {
|
||||||
const foundLayers = new Map();
|
const foundLayers = new global.Map();
|
||||||
newStyle.layers.forEach((layer, index) => {
|
newStyle.layers.forEach((layer, index) => {
|
||||||
if (layer.id === "" && foundLayers.has(layer.id)) {
|
if (layer.id === "" && foundLayers.has(layer.id)) {
|
||||||
const message = `Duplicate layer: ${formatLayerId(layer.id)}`;
|
|
||||||
const error = new Error(
|
const error = new Error(
|
||||||
`layers[${index}]: duplicate layer id [empty_string], previously used`
|
`layers[${index}]: duplicate layer id [empty_string], previously used`
|
||||||
);
|
);
|
||||||
@@ -342,7 +401,7 @@ export default class App extends React.Component {
|
|||||||
// Special case: Duplicate layer id
|
// Special case: Duplicate layer id
|
||||||
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
|
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
|
||||||
if (dupMatch) {
|
if (dupMatch) {
|
||||||
const [matchStr, index, message] = dupMatch;
|
const [, index, message] = dupMatch;
|
||||||
return {
|
return {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
parsed: {
|
parsed: {
|
||||||
@@ -359,7 +418,7 @@ export default class App extends React.Component {
|
|||||||
// Special case: Invalid source
|
// Special case: Invalid source
|
||||||
const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/);
|
const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/);
|
||||||
if (invalidSourceMatch) {
|
if (invalidSourceMatch) {
|
||||||
const [matchStr, index, message] = invalidSourceMatch;
|
const [, index, message] = invalidSourceMatch;
|
||||||
return {
|
return {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
parsed: {
|
parsed: {
|
||||||
@@ -375,7 +434,7 @@ export default class App extends React.Component {
|
|||||||
|
|
||||||
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
|
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
|
||||||
if (layerMatch) {
|
if (layerMatch) {
|
||||||
const [matchStr, index, group, property, message] = layerMatch;
|
const [, index, group, property, message] = layerMatch;
|
||||||
const key = (group && property) ? [group, property].join(".") : property;
|
const key = (group && property) ? [group, property].join(".") : property;
|
||||||
return {
|
return {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
@@ -396,7 +455,7 @@ export default class App extends React.Component {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let dirtyMapStyle = undefined;
|
let dirtyMapStyle: StyleSpecification | undefined = undefined;
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
dirtyMapStyle = cloneDeep(newStyle);
|
dirtyMapStyle = cloneDeep(newStyle);
|
||||||
|
|
||||||
@@ -406,7 +465,7 @@ export default class App extends React.Component {
|
|||||||
try {
|
try {
|
||||||
const objPath = message.split(":")[0];
|
const objPath = message.split(":")[0];
|
||||||
// Errors can be deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter'
|
// Errors can be deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter'
|
||||||
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^\[]+/)[0];
|
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^[]+/)![0];
|
||||||
unset(dirtyMapStyle, unsetPath);
|
unset(dirtyMapStyle, unsetPath);
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
@@ -417,17 +476,17 @@ export default class App extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||||
this.updateFonts(newStyle.glyphs)
|
this.updateFonts(newStyle.glyphs as string)
|
||||||
}
|
}
|
||||||
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
||||||
this.updateIcons(newStyle.sprite)
|
this.updateIcons(newStyle.sprite as string)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.addRevision) {
|
if (opts.addRevision) {
|
||||||
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({
|
||||||
@@ -460,7 +519,7 @@ export default class App extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onMoveLayer = (move) => {
|
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);
|
||||||
@@ -478,7 +537,7 @@ export default class App extends React.Component {
|
|||||||
this.onLayersChange(layers);
|
this.onLayersChange(layers);
|
||||||
}
|
}
|
||||||
|
|
||||||
onLayersChange = (changedLayers) => {
|
onLayersChange = (changedLayers: LayerSpecification[]) => {
|
||||||
const changedStyle = {
|
const changedStyle = {
|
||||||
...this.state.mapStyle,
|
...this.state.mapStyle,
|
||||||
layers: changedLayers
|
layers: changedLayers
|
||||||
@@ -486,15 +545,15 @@ export default class App extends React.Component {
|
|||||||
this.onStyleChanged(changedStyle)
|
this.onStyleChanged(changedStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
onLayerDestroy = (index) => {
|
onLayerDestroy = (index: number) => {
|
||||||
let layers = this.state.mapStyle.layers;
|
const layers = this.state.mapStyle.layers;
|
||||||
const remainingLayers = layers.slice(0);
|
const remainingLayers = layers.slice(0);
|
||||||
remainingLayers.splice(index, 1);
|
remainingLayers.splice(index, 1);
|
||||||
this.onLayersChange(remainingLayers);
|
this.onLayersChange(remainingLayers);
|
||||||
}
|
}
|
||||||
|
|
||||||
onLayerCopy = (index) => {
|
onLayerCopy = (index: number) => {
|
||||||
let layers = this.state.mapStyle.layers;
|
const layers = this.state.mapStyle.layers;
|
||||||
const changedLayers = layers.slice(0)
|
const changedLayers = layers.slice(0)
|
||||||
|
|
||||||
const clonedLayer = cloneDeep(changedLayers[index])
|
const clonedLayer = cloneDeep(changedLayers[index])
|
||||||
@@ -503,8 +562,8 @@ export default class App extends React.Component {
|
|||||||
this.onLayersChange(changedLayers)
|
this.onLayersChange(changedLayers)
|
||||||
}
|
}
|
||||||
|
|
||||||
onLayerVisibilityToggle = (index) => {
|
onLayerVisibilityToggle = (index: number) => {
|
||||||
let layers = this.state.mapStyle.layers;
|
const layers = this.state.mapStyle.layers;
|
||||||
const changedLayers = layers.slice(0)
|
const changedLayers = layers.slice(0)
|
||||||
|
|
||||||
const layer = { ...changedLayers[index] }
|
const layer = { ...changedLayers[index] }
|
||||||
@@ -517,7 +576,7 @@ export default class App extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
onLayerIdChange = (index, oldId, newId) => {
|
onLayerIdChange = (index: number, _oldId: string, newId: string) => {
|
||||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||||
changedLayers[index] = {
|
changedLayers[index] = {
|
||||||
...changedLayers[index],
|
...changedLayers[index],
|
||||||
@@ -527,27 +586,27 @@ export default class App extends React.Component {
|
|||||||
this.onLayersChange(changedLayers)
|
this.onLayersChange(changedLayers)
|
||||||
}
|
}
|
||||||
|
|
||||||
onLayerChanged = (index, layer) => {
|
onLayerChanged = (index: number, layer: LayerSpecification) => {
|
||||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||||
changedLayers[index] = layer
|
changedLayers[index] = layer
|
||||||
|
|
||||||
this.onLayersChange(changedLayers)
|
this.onLayersChange(changedLayers)
|
||||||
}
|
}
|
||||||
|
|
||||||
setMapState = (newState) => {
|
setMapState = (newState: MapState) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
mapState: newState
|
mapState: newState
|
||||||
}, this.setStateInUrl);
|
}, this.setStateInUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
setDefaultValues = (styleObj) => {
|
setDefaultValues = (styleObj: StyleSpecification & {id: string}) => {
|
||||||
const metadata = styleObj.metadata || {}
|
const metadata: {[key: string]: string} = styleObj.metadata || {} as any
|
||||||
if(metadata['maputnik:renderer'] === undefined) {
|
if(metadata['maputnik:renderer'] === undefined) {
|
||||||
const changedStyle = {
|
const changedStyle = {
|
||||||
...styleObj,
|
...styleObj,
|
||||||
metadata: {
|
metadata: {
|
||||||
...styleObj.metadata,
|
...styleObj.metadata as any,
|
||||||
'maputnik:renderer': 'mbgljs'
|
'maputnik:renderer': 'mlgljs'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return changedStyle
|
return changedStyle
|
||||||
@@ -556,19 +615,19 @@ export default class App extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openStyle = (styleObj) => {
|
openStyle = (styleObj: StyleSpecification & {id: string}) => {
|
||||||
styleObj = this.setDefaultValues(styleObj)
|
styleObj = this.setDefaultValues(styleObj)
|
||||||
this.onStyleChanged(styleObj)
|
this.onStyleChanged(styleObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchSources() {
|
fetchSources() {
|
||||||
const sourceList = {};
|
const sourceList: {[key: string]: any} = {};
|
||||||
|
|
||||||
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
for(const [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
||||||
if(
|
if(
|
||||||
!this.state.sources.hasOwnProperty(key) &&
|
!Object.prototype.hasOwnProperty.call(this.state.sources, key) &&
|
||||||
val.type === "vector" &&
|
val.type === "vector" &&
|
||||||
val.hasOwnProperty("url")
|
Object.prototype.hasOwnProperty.call(val, "url")
|
||||||
) {
|
) {
|
||||||
sourceList[key] = {
|
sourceList[key] = {
|
||||||
type: val.type,
|
type: val.type,
|
||||||
@@ -578,38 +637,38 @@ export default class App extends React.Component {
|
|||||||
let url = val.url;
|
let url = val.url;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
url = setFetchAccessToken(url, this.state.mapStyle)
|
url = setFetchAccessToken(url!, this.state.mapStyle)
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.warn("Failed to setFetchAccessToken: ", err);
|
console.warn("Failed to setFetchAccessToken: ", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(url, {
|
fetch(url!, {
|
||||||
mode: 'cors',
|
mode: 'cors',
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(json => {
|
.then(json => {
|
||||||
|
|
||||||
if(!json.hasOwnProperty("vector_layers")) {
|
if(!Object.prototype.hasOwnProperty.call(json, "vector_layers")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new objects before setState
|
// Create new objects before setState
|
||||||
const sources = Object.assign({}, {
|
const sources = Object.assign({}, {
|
||||||
[key]: this.state.sources[key],
|
[key]: this.state.sources[key],
|
||||||
|
});
|
||||||
|
|
||||||
|
for(const layer of json.vector_layers) {
|
||||||
|
(sources[key] as any).layers.push(layer.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug("Updating source: "+key);
|
||||||
|
this.setState({
|
||||||
|
sources: sources
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Failed to process sources for '%s'", url, err);
|
||||||
});
|
});
|
||||||
|
|
||||||
for(let layer of json.vector_layers) {
|
|
||||||
sources[key].layers.push(layer.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug("Updating source: "+key);
|
|
||||||
this.setState({
|
|
||||||
sources: sources
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error("Failed to process sources for '%s'", url, err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
sourceList[key] = this.state.sources[key] || this.state.mapStyle.sources[key];
|
sourceList[key] = this.state.sources[key] || this.state.mapStyle.sources[key];
|
||||||
@@ -625,11 +684,17 @@ export default class App extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getRenderer () {
|
_getRenderer () {
|
||||||
const metadata = this.state.mapStyle.metadata || {};
|
const metadata: {[key:string]: string} = this.state.mapStyle.metadata || {} as any;
|
||||||
return metadata['maputnik:renderer'] || 'mbgljs';
|
return metadata['maputnik:renderer'] || 'mlgljs';
|
||||||
}
|
}
|
||||||
|
|
||||||
onMapChange = (mapView) => {
|
onMapChange = (mapView: {
|
||||||
|
zoom: number,
|
||||||
|
center: {
|
||||||
|
lng: number,
|
||||||
|
lat: number,
|
||||||
|
},
|
||||||
|
}) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
mapView,
|
mapView,
|
||||||
});
|
});
|
||||||
@@ -637,16 +702,15 @@ export default class App extends React.Component {
|
|||||||
|
|
||||||
mapRenderer() {
|
mapRenderer() {
|
||||||
const {mapStyle, dirtyMapStyle} = this.state;
|
const {mapStyle, dirtyMapStyle} = this.state;
|
||||||
const metadata = this.state.mapStyle.metadata || {};
|
|
||||||
|
|
||||||
const mapProps = {
|
const mapProps = {
|
||||||
mapStyle: (dirtyMapStyle || mapStyle),
|
mapStyle: (dirtyMapStyle || mapStyle),
|
||||||
replaceAccessTokens: (mapStyle) => {
|
replaceAccessTokens: (mapStyle: StyleSpecification) => {
|
||||||
return style.replaceAccessTokens(mapStyle, {
|
return style.replaceAccessTokens(mapStyle, {
|
||||||
allowFallback: true
|
allowFallback: true
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onDataChange: (e) => {
|
onDataChange: (e: {map: Map}) => {
|
||||||
this.layerWatcher.analyzeMap(e.map)
|
this.layerWatcher.analyzeMap(e.map)
|
||||||
this.fetchSources();
|
this.fetchSources();
|
||||||
},
|
},
|
||||||
@@ -665,9 +729,9 @@ export default class App extends React.Component {
|
|||||||
onLayerSelect={this.onLayerSelect}
|
onLayerSelect={this.onLayerSelect}
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
mapElement = <MapMapboxGl {...mapProps}
|
mapElement = <MapMaplibreGl {...mapProps}
|
||||||
onChange={this.onMapChange}
|
onChange={this.onMapChange}
|
||||||
options={this.state.mapboxGlDebugOptions}
|
options={this.state.maplibreGlDebugOptions}
|
||||||
inspectModeEnabled={this.state.mapState === "inspect"}
|
inspectModeEnabled={this.state.mapState === "inspect"}
|
||||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
||||||
onLayerSelect={this.onLayerSelect} />
|
onLayerSelect={this.onLayerSelect} />
|
||||||
@@ -677,12 +741,12 @@ export default class App extends React.Component {
|
|||||||
if(this.state.mapState.match(/^filter-/)) {
|
if(this.state.mapState.match(/^filter-/)) {
|
||||||
filterName = this.state.mapState.replace(/^filter-/, "");
|
filterName = this.state.mapState.replace(/^filter-/, "");
|
||||||
}
|
}
|
||||||
const elementStyle = {};
|
const elementStyle: {filter?: string} = {};
|
||||||
if (filterName) {
|
if (filterName) {
|
||||||
elementStyle.filter = `url('#${filterName}')`;
|
elementStyle.filter = `url('#${filterName}')`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div style={elementStyle} className="maputnik-map__container">
|
return <div style={elementStyle} className="maputnik-map__container" data-wd-key="maplibre:container">
|
||||||
{mapElement}
|
{mapElement}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -695,8 +759,8 @@ export default class App extends React.Component {
|
|||||||
url.searchParams.set("layer", `${hashVal}~${selectedLayerIndex}`);
|
url.searchParams.set("layer", `${hashVal}~${selectedLayerIndex}`);
|
||||||
|
|
||||||
const openModals = Object.entries(isOpen)
|
const openModals = Object.entries(isOpen)
|
||||||
.map(([key, val]) => (val === true ? key : null))
|
.map(([key, val]) => (val === true ? key : null))
|
||||||
.filter(val => val !== null);
|
.filter(val => val !== null);
|
||||||
|
|
||||||
if (openModals.length > 0) {
|
if (openModals.length > 0) {
|
||||||
url.searchParams.set("modal", openModals.join(","));
|
url.searchParams.set("modal", openModals.join(","));
|
||||||
@@ -715,12 +779,12 @@ export default class App extends React.Component {
|
|||||||
history.replaceState({selectedLayerIndex}, "Maputnik", url.href);
|
history.replaceState({selectedLayerIndex}, "Maputnik", url.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
getInitialStateFromUrl = (mapStyle) => {
|
getInitialStateFromUrl = (mapStyle: StyleSpecification) => {
|
||||||
const url = new URL(location.href);
|
const url = new URL(location.href);
|
||||||
const modalParam = url.searchParams.get("modal");
|
const modalParam = url.searchParams.get("modal");
|
||||||
if (modalParam && modalParam !== "") {
|
if (modalParam && modalParam !== "") {
|
||||||
const modals = modalParam.split(",");
|
const modals = modalParam.split(",");
|
||||||
const modalObj = {};
|
const modalObj: {[key: string]: boolean} = {};
|
||||||
modals.forEach(modalName => {
|
modals.forEach(modalName => {
|
||||||
modalObj[modalName] = true;
|
modalObj[modalName] = true;
|
||||||
});
|
});
|
||||||
@@ -735,7 +799,7 @@ export default class App extends React.Component {
|
|||||||
|
|
||||||
const view = url.searchParams.get("view");
|
const view = url.searchParams.get("view");
|
||||||
if (view && view !== "") {
|
if (view && view !== "") {
|
||||||
this.setMapState(view);
|
this.setMapState(view as MapState);
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = url.searchParams.get("layer");
|
const path = url.searchParams.get("layer");
|
||||||
@@ -767,14 +831,14 @@ export default class App extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onLayerSelect = (index) => {
|
onLayerSelect = (index: number) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedLayerIndex: index,
|
selectedLayerIndex: index,
|
||||||
selectedLayerOriginalId: this.state.mapStyle.layers[index].id,
|
selectedLayerOriginalId: this.state.mapStyle.layers[index].id,
|
||||||
}, this.setStateInUrl);
|
}, this.setStateInUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
setModal(modalName, value) {
|
setModal(modalName: keyof AppState["isOpen"], value: boolean) {
|
||||||
if(modalName === 'survey' && value === false) {
|
if(modalName === 'survey' && value === false) {
|
||||||
localStorage.setItem('survey', '');
|
localStorage.setItem('survey', '');
|
||||||
}
|
}
|
||||||
@@ -787,11 +851,11 @@ export default class App extends React.Component {
|
|||||||
}, this.setStateInUrl)
|
}, this.setStateInUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleModal(modalName) {
|
toggleModal(modalName: keyof AppState["isOpen"]) {
|
||||||
this.setModal(modalName, !this.state.isOpen[modalName]);
|
this.setModal(modalName, !this.state.isOpen[modalName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeOpenlayersDebug = (key, value) => {
|
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
openlayersDebugOptions: {
|
openlayersDebugOptions: {
|
||||||
...this.state.openlayersDebugOptions,
|
...this.state.openlayersDebugOptions,
|
||||||
@@ -800,10 +864,10 @@ export default class App extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeMaboxGlDebug = (key, value) => {
|
onChangeMaplibreGlDebug = (key: keyof AppState["maplibreGlDebugOptions"], value: any) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
mapboxGlDebugOptions: {
|
maplibreGlDebugOptions: {
|
||||||
...this.state.mapboxGlDebugOptions,
|
...this.state.maplibreGlDebugOptions,
|
||||||
[key]: value,
|
[key]: value,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -811,8 +875,7 @@ export default class App extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const layers = this.state.mapStyle.layers || []
|
const layers = this.state.mapStyle.layers || []
|
||||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
|
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : undefined
|
||||||
const metadata = this.state.mapStyle.metadata || {}
|
|
||||||
|
|
||||||
const toolbar = <AppToolbar
|
const toolbar = <AppToolbar
|
||||||
renderer={this._getRenderer()}
|
renderer={this._getRenderer()}
|
||||||
@@ -855,7 +918,7 @@ export default class App extends React.Component {
|
|||||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
||||||
onLayerIdChange={this.onLayerIdChange}
|
onLayerIdChange={this.onLayerIdChange}
|
||||||
errors={this.state.errors}
|
errors={this.state.errors}
|
||||||
/> : null
|
/> : undefined
|
||||||
|
|
||||||
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
||||||
currentLayer={selectedLayer}
|
currentLayer={selectedLayer}
|
||||||
@@ -864,15 +927,15 @@ export default class App extends React.Component {
|
|||||||
mapStyle={this.state.mapStyle}
|
mapStyle={this.state.mapStyle}
|
||||||
errors={this.state.errors}
|
errors={this.state.errors}
|
||||||
infos={this.state.infos}
|
infos={this.state.infos}
|
||||||
/> : null
|
/> : undefined
|
||||||
|
|
||||||
|
|
||||||
const modals = <div>
|
const modals = <div>
|
||||||
<ModalDebug
|
<ModalDebug
|
||||||
renderer={this._getRenderer()}
|
renderer={this._getRenderer()}
|
||||||
mapboxGlDebugOptions={this.state.mapboxGlDebugOptions}
|
maplibreGlDebugOptions={this.state.maplibreGlDebugOptions}
|
||||||
openlayersDebugOptions={this.state.openlayersDebugOptions}
|
openlayersDebugOptions={this.state.openlayersDebugOptions}
|
||||||
onChangeMaboxGlDebug={this.onChangeMaboxGlDebug}
|
onChangeMaplibreGlDebug={this.onChangeMaplibreGlDebug}
|
||||||
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
|
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
|
||||||
isOpen={this.state.isOpen.debug}
|
isOpen={this.state.isOpen.debug}
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'debug')}
|
onOpenToggle={this.toggleModal.bind(this, 'debug')}
|
||||||
@@ -889,7 +952,6 @@ export default class App extends React.Component {
|
|||||||
onChangeMetadataProperty={this.onChangeMetadataProperty}
|
onChangeMetadataProperty={this.onChangeMetadataProperty}
|
||||||
isOpen={this.state.isOpen.settings}
|
isOpen={this.state.isOpen.settings}
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
||||||
openlayersDebugOptions={this.state.openlayersDebugOptions}
|
|
||||||
/>
|
/>
|
||||||
<ModalExport
|
<ModalExport
|
||||||
mapStyle={this.state.mapStyle}
|
mapStyle={this.state.mapStyle}
|
||||||
@@ -2,16 +2,16 @@ import React from 'react'
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import ScrollContainer from './ScrollContainer'
|
import ScrollContainer from './ScrollContainer'
|
||||||
|
|
||||||
class AppLayout extends React.Component {
|
type AppLayoutProps = {
|
||||||
static propTypes = {
|
toolbar: React.ReactElement
|
||||||
toolbar: PropTypes.element.isRequired,
|
layerList: React.ReactElement
|
||||||
layerList: PropTypes.element.isRequired,
|
layerEditor?: React.ReactElement
|
||||||
layerEditor: PropTypes.element,
|
map: React.ReactElement
|
||||||
map: PropTypes.element.isRequired,
|
bottom?: React.ReactElement
|
||||||
bottom: PropTypes.element,
|
modals?: React.ReactNode
|
||||||
modals: PropTypes.node,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
class AppLayout extends React.Component<AppLayoutProps> {
|
||||||
static childContextTypes = {
|
static childContextTypes = {
|
||||||
reactIconBase: PropTypes.object
|
reactIconBase: PropTypes.object
|
||||||
}
|
}
|
||||||
@@ -35,8 +35,8 @@ class AppLayout extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
{this.props.map}
|
{this.props.map}
|
||||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||||
{this.props.bottom}
|
{this.props.bottom}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{this.props.modals}
|
{this.props.modals}
|
||||||
</div>
|
</div>
|
||||||
@@ -1,29 +1,28 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import {formatLayerId} from '../libs/format';
|
||||||
import {formatLayerId} from '../util/format';
|
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
|
||||||
|
|
||||||
export default class AppMessagePanel extends React.Component {
|
type AppMessagePanelProps = {
|
||||||
static propTypes = {
|
errors?: unknown[]
|
||||||
errors: PropTypes.array,
|
infos?: unknown[]
|
||||||
infos: PropTypes.array,
|
mapStyle?: StyleSpecification
|
||||||
mapStyle: PropTypes.object,
|
onLayerSelect?(...args: unknown[]): unknown
|
||||||
onLayerSelect: PropTypes.func,
|
currentLayer?: LayerSpecification
|
||||||
currentLayer: PropTypes.object,
|
selectedLayerIndex?: number
|
||||||
selectedLayerIndex: PropTypes.number,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
export default class AppMessagePanel extends React.Component<AppMessagePanelProps> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onLayerSelect: () => {},
|
onLayerSelect: () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {selectedLayerIndex} = this.props;
|
const {selectedLayerIndex} = this.props;
|
||||||
const errors = this.props.errors.map((error, idx) => {
|
const errors = this.props.errors?.map((error: any, idx) => {
|
||||||
let content;
|
let content;
|
||||||
if (error.parsed && error.parsed.type === "layer") {
|
if (error.parsed && error.parsed.type === "layer") {
|
||||||
const {parsed} = error;
|
const {parsed} = error;
|
||||||
const {mapStyle, currentLayer} = this.props;
|
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
|
||||||
const layerId = mapStyle.layers[parsed.data.index].id;
|
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||||
@@ -32,7 +31,7 @@ export default class AppMessagePanel extends React.Component {
|
|||||||
—
|
—
|
||||||
<button
|
<button
|
||||||
className="maputnik-message-panel__switch-button"
|
className="maputnik-message-panel__switch-button"
|
||||||
onClick={() => this.props.onLayerSelect(parsed.data.index)}
|
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
|
||||||
>
|
>
|
||||||
switch to layer
|
switch to layer
|
||||||
</button>
|
</button>
|
||||||
@@ -49,7 +48,7 @@ export default class AppMessagePanel extends React.Component {
|
|||||||
</p>
|
</p>
|
||||||
})
|
})
|
||||||
|
|
||||||
const infos = this.props.infos.map((m, i) => {
|
const infos = this.props.infos?.map((m, i) => {
|
||||||
return <p key={"info-"+i}>{m}</p>
|
return <p key={"info-"+i}>{m}</p>
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1,78 +1,55 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import {detect} from 'detect-browser';
|
import {detect} from 'detect-browser';
|
||||||
|
|
||||||
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdAssignmentTurnedIn} from 'react-icons/md'
|
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage} from 'react-icons/md'
|
||||||
|
|
||||||
|
|
||||||
import logoImage from 'maputnik-design/logos/logo-color.svg'
|
|
||||||
import pkgJson from '../../package.json'
|
import pkgJson from '../../package.json'
|
||||||
|
//@ts-ignore
|
||||||
|
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
|
||||||
|
|
||||||
// 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();
|
||||||
const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser.name) > -1;
|
const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser!.name) > -1;
|
||||||
|
|
||||||
|
|
||||||
class IconText extends React.Component {
|
type IconTextProps = {
|
||||||
static propTypes = {
|
children?: React.ReactNode
|
||||||
children: PropTypes.node,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
class IconText extends React.Component<IconTextProps> {
|
||||||
render() {
|
render() {
|
||||||
return <span className="maputnik-icon-text">{this.props.children}</span>
|
return <span className="maputnik-icon-text">{this.props.children}</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ToolbarLink extends React.Component {
|
type ToolbarLinkProps = {
|
||||||
static propTypes = {
|
className?: string
|
||||||
className: PropTypes.string,
|
children?: React.ReactNode
|
||||||
children: PropTypes.node,
|
href?: string
|
||||||
href: PropTypes.string,
|
onToggleModal?(...args: unknown[]): unknown
|
||||||
onToggleModal: PropTypes.func,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
class ToolbarLink extends React.Component<ToolbarLinkProps> {
|
||||||
render() {
|
render() {
|
||||||
return <a
|
return <a
|
||||||
className={classnames('maputnik-toolbar-link', this.props.className)}
|
className={classnames('maputnik-toolbar-link', this.props.className)}
|
||||||
href={this.props.href}
|
href={this.props.href}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
data-wd-key="toolbar:link"
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ToolbarLinkHighlighted extends React.Component {
|
type ToolbarSelectProps = {
|
||||||
static propTypes = {
|
children?: React.ReactNode
|
||||||
className: PropTypes.string,
|
wdKey?: string
|
||||||
children: PropTypes.node,
|
};
|
||||||
href: PropTypes.string,
|
|
||||||
onToggleModal: PropTypes.func
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <a
|
|
||||||
className={classnames('maputnik-toolbar-link', "maputnik-toolbar-link--highlighted", this.props.className)}
|
|
||||||
href={this.props.href}
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<span className="maputnik-toolbar-link-wrapper">
|
|
||||||
{this.props.children}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ToolbarSelect extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
children: PropTypes.node,
|
|
||||||
wdKey: PropTypes.string
|
|
||||||
}
|
|
||||||
|
|
||||||
|
class ToolbarSelect extends React.Component<ToolbarSelectProps> {
|
||||||
render() {
|
render() {
|
||||||
return <div
|
return <div
|
||||||
className='maputnik-toolbar-select'
|
className='maputnik-toolbar-select'
|
||||||
@@ -83,13 +60,13 @@ class ToolbarSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ToolbarAction extends React.Component {
|
type ToolbarActionProps = {
|
||||||
static propTypes = {
|
children?: React.ReactNode
|
||||||
children: PropTypes.node,
|
onClick?(...args: unknown[]): unknown
|
||||||
onClick: PropTypes.func,
|
wdKey?: string
|
||||||
wdKey: PropTypes.string
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
class ToolbarAction extends React.Component<ToolbarActionProps> {
|
||||||
render() {
|
render() {
|
||||||
return <button
|
return <button
|
||||||
className='maputnik-toolbar-action'
|
className='maputnik-toolbar-action'
|
||||||
@@ -101,22 +78,24 @@ class ToolbarAction extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AppToolbar extends React.Component {
|
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
|
||||||
static propTypes = {
|
|
||||||
mapStyle: PropTypes.object.isRequired,
|
|
||||||
inspectModeEnabled: PropTypes.bool.isRequired,
|
|
||||||
onStyleChanged: PropTypes.func.isRequired,
|
|
||||||
// A new style has been uploaded
|
|
||||||
onStyleOpen: PropTypes.func.isRequired,
|
|
||||||
// A dict of source id's and the available source layers
|
|
||||||
sources: PropTypes.object.isRequired,
|
|
||||||
children: PropTypes.node,
|
|
||||||
onToggleModal: PropTypes.func,
|
|
||||||
onSetMapState: PropTypes.func,
|
|
||||||
mapState: PropTypes.string,
|
|
||||||
renderer: PropTypes.string,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
type AppToolbarProps = {
|
||||||
|
mapStyle: object
|
||||||
|
inspectModeEnabled: boolean
|
||||||
|
onStyleChanged(...args: unknown[]): unknown
|
||||||
|
// A new style has been uploaded
|
||||||
|
onStyleOpen(...args: unknown[]): unknown
|
||||||
|
// A dict of source id's and the available source layers
|
||||||
|
sources: object
|
||||||
|
children?: React.ReactNode
|
||||||
|
onToggleModal(...args: unknown[]): unknown
|
||||||
|
onSetMapState(mapState: MapState): unknown
|
||||||
|
mapState?: MapState
|
||||||
|
renderer?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||||
state = {
|
state = {
|
||||||
isOpen: {
|
isOpen: {
|
||||||
settings: false,
|
settings: false,
|
||||||
@@ -127,16 +106,16 @@ export default class AppToolbar extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSelection(val) {
|
handleSelection(val: MapState) {
|
||||||
this.props.onSetMapState(val);
|
this.props.onSetMapState(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSkip = (target) => {
|
onSkip = (target: string) => {
|
||||||
if (target === "map") {
|
if (target === "map") {
|
||||||
document.querySelector(".mapboxgl-canvas").focus();
|
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const el = document.querySelector("#skip-target-"+target);
|
const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement;
|
||||||
el.focus();
|
el.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,7 +131,7 @@ export default class AppToolbar extends React.Component {
|
|||||||
id: "inspect",
|
id: "inspect",
|
||||||
group: "general",
|
group: "general",
|
||||||
title: "Inspect",
|
title: "Inspect",
|
||||||
disabled: this.props.renderer !== 'mbgljs',
|
disabled: this.props.renderer === 'ol',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "filter-deuteranopia",
|
id: "filter-deuteranopia",
|
||||||
@@ -193,21 +172,21 @@ export default class AppToolbar extends React.Component {
|
|||||||
<button
|
<button
|
||||||
data-wd-key="root:skip:layer-list"
|
data-wd-key="root:skip:layer-list"
|
||||||
className="maputnik-toolbar-skip"
|
className="maputnik-toolbar-skip"
|
||||||
onClick={e => this.onSkip("layer-list")}
|
onClick={_e => this.onSkip("layer-list")}
|
||||||
>
|
>
|
||||||
Layers list
|
Layers list
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
data-wd-key="root:skip:layer-editor"
|
data-wd-key="root:skip:layer-editor"
|
||||||
className="maputnik-toolbar-skip"
|
className="maputnik-toolbar-skip"
|
||||||
onClick={e => this.onSkip("layer-editor")}
|
onClick={_e => this.onSkip("layer-editor")}
|
||||||
>
|
>
|
||||||
Layer editor
|
Layer editor
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
data-wd-key="root:skip:map-view"
|
data-wd-key="root:skip:map-view"
|
||||||
className="maputnik-toolbar-skip"
|
className="maputnik-toolbar-skip"
|
||||||
onClick={e => this.onSkip("map")}
|
onClick={_e => this.onSkip("map")}
|
||||||
>
|
>
|
||||||
Map view
|
Map view
|
||||||
</button>
|
</button>
|
||||||
@@ -215,9 +194,9 @@ export default class AppToolbar extends React.Component {
|
|||||||
className="maputnik-toolbar-logo"
|
className="maputnik-toolbar-logo"
|
||||||
target="blank"
|
target="blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
href="https://github.com/maputnik/editor"
|
href="https://github.com/maplibre/maputnik"
|
||||||
>
|
>
|
||||||
<span dangerouslySetInnerHTML={{__html: logoImage}} />
|
<img src={maputnikLogo} alt="Maputnik on GitHub" />
|
||||||
<h1>
|
<h1>
|
||||||
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
|
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
|
||||||
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
||||||
@@ -247,12 +226,13 @@ export default class AppToolbar extends React.Component {
|
|||||||
<label>View
|
<label>View
|
||||||
<select
|
<select
|
||||||
className="maputnik-select"
|
className="maputnik-select"
|
||||||
onChange={(e) => this.handleSelection(e.target.value)}
|
data-wd-key="maputnik-select"
|
||||||
value={currentView.id}
|
onChange={(e) => this.handleSelection(e.target.value as MapState)}
|
||||||
|
value={currentView?.id}
|
||||||
>
|
>
|
||||||
{views.filter(v => v.group === "general").map((item) => {
|
{views.filter(v => v.group === "general").map((item) => {
|
||||||
return (
|
return (
|
||||||
<option key={item.id} value={item.id} disabled={item.disabled}>
|
<option key={item.id} value={item.id} disabled={item.disabled} data-wd-key={item.id}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
@@ -270,14 +250,10 @@ export default class AppToolbar extends React.Component {
|
|||||||
</label>
|
</label>
|
||||||
</ToolbarSelect>
|
</ToolbarSelect>
|
||||||
|
|
||||||
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
|
||||||
<MdHelpOutline />
|
<MdHelpOutline />
|
||||||
<IconText>Help</IconText>
|
<IconText>Help</IconText>
|
||||||
</ToolbarLink>
|
</ToolbarLink>
|
||||||
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
|
|
||||||
<MdAssignmentTurnedIn />
|
|
||||||
<IconText>Take the Maputnik Survey</IconText>
|
|
||||||
</ToolbarLinkHighlighted>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -1,40 +1,43 @@
|
|||||||
import React from 'react'
|
import React, {SyntheticEvent} from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import FieldDocLabel from './FieldDocLabel'
|
import FieldDocLabel from './FieldDocLabel'
|
||||||
import Doc from './Doc'
|
import Doc from './Doc'
|
||||||
|
|
||||||
|
|
||||||
/** Wrap a component with a label */
|
type BlockProps = {
|
||||||
export default class Block extends React.Component {
|
"data-wd-key"?: string
|
||||||
static propTypes = {
|
label?: string
|
||||||
"data-wd-key": PropTypes.string,
|
action?: React.ReactElement
|
||||||
label: PropTypes.oneOfType([
|
style?: object
|
||||||
PropTypes.string,
|
onChange?(...args: unknown[]): unknown
|
||||||
PropTypes.element,
|
fieldSpec?: object
|
||||||
]),
|
wideMode?: boolean
|
||||||
action: PropTypes.element,
|
error?: {message: string}
|
||||||
children: PropTypes.node.isRequired,
|
};
|
||||||
style: PropTypes.object,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
fieldSpec: PropTypes.object,
|
|
||||||
wideMode: PropTypes.bool,
|
|
||||||
error: PropTypes.array,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor (props) {
|
type BlockState = {
|
||||||
|
showDoc: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Wrap a component with a label */
|
||||||
|
export default class Block extends React.Component<BlockProps, BlockState> {
|
||||||
|
_blockEl: HTMLDivElement | null = null;
|
||||||
|
|
||||||
|
constructor (props: BlockProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
showDoc: false,
|
showDoc: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(e) {
|
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) {
|
||||||
const value = e.target.value
|
const value = e.target.value
|
||||||
return this.props.onChange(value === "" ? undefined : value)
|
if (this.props.onChange) {
|
||||||
|
return this.props.onChange(value === "" ? undefined : value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onToggleDoc = (val) => {
|
onToggleDoc = (val: boolean) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
showDoc: val
|
showDoc: val
|
||||||
});
|
});
|
||||||
@@ -46,20 +49,17 @@ export default class Block extends React.Component {
|
|||||||
* causing the picker to reopen. This causes a scenario where the picker can
|
* causing the picker to reopen. This causes a scenario where the picker can
|
||||||
* never be closed once open.
|
* never be closed once open.
|
||||||
*/
|
*/
|
||||||
onLabelClick = (event) => {
|
onLabelClick = (event: SyntheticEvent<any, any>) => {
|
||||||
const el = event.nativeEvent.target;
|
const el = event.nativeEvent.target;
|
||||||
const nativeEvent = event.nativeEvent;
|
const contains = this._blockEl?.contains(el);
|
||||||
const contains = this._blockEl.contains(el);
|
|
||||||
|
|
||||||
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
|
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const errors = [].concat(this.props.error || []);
|
|
||||||
|
|
||||||
return <label style={this.props.style}
|
return <label style={this.props.style}
|
||||||
data-wd-key={this.props["data-wd-key"]}
|
data-wd-key={this.props["data-wd-key"]}
|
||||||
className={classnames({
|
className={classnames({
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { Collapse as ReactCollapse } from 'react-collapse'
|
import { Collapse as ReactCollapse } from 'react-collapse'
|
||||||
import accessibility from '../../libs/accessibility'
|
import {reducedMotionEnabled} from '../libs/accessibility'
|
||||||
|
|
||||||
|
|
||||||
export default class Collapse extends React.Component {
|
type CollapseProps = {
|
||||||
static propTypes = {
|
isActive: boolean
|
||||||
isActive: PropTypes.bool.isRequired,
|
children: React.ReactElement
|
||||||
children: PropTypes.element.isRequired
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
export default class Collapse extends React.Component<CollapseProps> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
isActive: true
|
isActive: true
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (accessibility.reducedMotionEnabled()) {
|
if (reducedMotionEnabled()) {
|
||||||
return (
|
return (
|
||||||
<div style={{display: this.props.isActive ? "block" : "none"}}>
|
<div style={{display: this.props.isActive ? "block" : "none"}}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import {MdArrowDropDown, MdArrowDropUp} from 'react-icons/md'
|
import {MdArrowDropDown, MdArrowDropUp} from 'react-icons/md'
|
||||||
|
|
||||||
export default class Collapser extends React.Component {
|
type CollapserProps = {
|
||||||
static propTypes = {
|
isCollapsed: boolean
|
||||||
isCollapsed: PropTypes.bool.isRequired,
|
style?: object
|
||||||
style: PropTypes.object,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
export default class Collapser extends React.Component<CollapserProps> {
|
||||||
render() {
|
render() {
|
||||||
const iconStyle = {
|
const iconStyle = {
|
||||||
width: 20,
|
width: 20,
|
||||||
@@ -1,36 +1,45 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
export default class Doc extends React.Component {
|
const headers = {
|
||||||
static propTypes = {
|
js: "JS",
|
||||||
fieldSpec: PropTypes.object.isRequired,
|
android: "Android",
|
||||||
|
ios: "iOS",
|
||||||
|
macos: "macOS",
|
||||||
|
};
|
||||||
|
|
||||||
|
type DocProps = {
|
||||||
|
fieldSpec: {
|
||||||
|
doc?: string
|
||||||
|
values?: {
|
||||||
|
[key: string]: {
|
||||||
|
doc?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'sdk-support'?: {
|
||||||
|
[key: string]: typeof headers
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Doc extends React.Component<DocProps> {
|
||||||
render () {
|
render () {
|
||||||
const {fieldSpec} = this.props;
|
const {fieldSpec} = this.props;
|
||||||
|
|
||||||
const {doc, values} = fieldSpec;
|
const {doc, values} = fieldSpec;
|
||||||
const sdkSupport = fieldSpec['sdk-support'];
|
const sdkSupport = fieldSpec['sdk-support'];
|
||||||
|
|
||||||
const headers = {
|
|
||||||
js: "JS",
|
|
||||||
android: "Android",
|
|
||||||
ios: "iOS",
|
|
||||||
macos: "macOS",
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderValues = (
|
const renderValues = (
|
||||||
!!values &&
|
!!values &&
|
||||||
// HACK: Currently we merge additional values into the stylespec, so this is required
|
// HACK: Currently we merge additional values into the style spec, so this is required
|
||||||
// See <https://github.com/maputnik/editor/blob/master/src/components/fields/PropertyGroup.jsx#L16>
|
// See <https://github.com/maplibre/maputnik/blob/main/src/components/PropertyGroup.jsx#L16>
|
||||||
!Array.isArray(values)
|
!Array.isArray(values)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{doc &&
|
{doc &&
|
||||||
<div className="SpecDoc">
|
<div className="SpecDoc">
|
||||||
<div className="SpecDoc__doc">{doc}</div>
|
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>{doc}</div>
|
||||||
{renderValues &&
|
{renderValues &&
|
||||||
<ul className="SpecDoc__values">
|
<ul className="SpecDoc__values">
|
||||||
{Object.entries(values).map(([key, value]) => {
|
{Object.entries(values).map(([key, value]) => {
|
||||||
@@ -61,10 +70,9 @@ export default class Doc extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<tr key={key}>
|
<tr key={key}>
|
||||||
<td>{key}</td>
|
<td>{key}</td>
|
||||||
{Object.keys(headers).map(k => {
|
{Object.keys(headers).map((k) => {
|
||||||
const value = supportObj[k];
|
if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
|
||||||
if (supportObj.hasOwnProperty(k)) {
|
return <td key={k}>{supportObj[k as keyof typeof headers]}</td>;
|
||||||
return <td key={k}>{supportObj[k]}</td>;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return <td key={k}>no</td>;
|
return <td key={k}>no</td>;
|
||||||
@@ -80,4 +88,4 @@ export default class Doc extends React.Component {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Block from './Block'
|
|
||||||
import InputArray from './InputArray'
|
|
||||||
import Fieldset from './Fieldset'
|
|
||||||
|
|
||||||
export default class FieldArray extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
...InputArray.propTypes,
|
|
||||||
name: PropTypes.string,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {props} = this;
|
|
||||||
|
|
||||||
return <Fieldset label={props.label}>
|
|
||||||
<InputArray {...props} />
|
|
||||||
</Fieldset>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
|
||||||
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
|
type FieldArrayProps = InputArrayProps & {
|
||||||
|
name?: string
|
||||||
|
fieldSpec?: {
|
||||||
|
doc: string
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class FieldArray extends React.Component<FieldArrayProps> {
|
||||||
|
render() {
|
||||||
|
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
|
<InputArray {...this.props} />
|
||||||
|
</Fieldset>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Block from './Block'
|
|
||||||
import InputAutocomplete from './InputAutocomplete'
|
|
||||||
|
|
||||||
|
|
||||||
export default class FieldAutocomplete extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
...InputAutocomplete.propTypes,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {props} = this;
|
|
||||||
|
|
||||||
return <Block label={props.label}>
|
|
||||||
<InputAutocomplete {...props} />
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
|
||||||
|
|
||||||
|
|
||||||
|
type FieldAutocompleteProps = InputAutocompleteProps & {
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldAutocomplete extends React.Component<FieldAutocompleteProps> {
|
||||||
|
render() {
|
||||||
|
return <Block label={this.props.label}>
|
||||||
|
<InputAutocomplete {...this.props} />
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Block from './Block'
|
|
||||||
import InputCheckbox from './InputCheckbox'
|
|
||||||
|
|
||||||
|
|
||||||
export default class FieldCheckbox extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
...InputCheckbox.propTypes,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {props} = this;
|
|
||||||
|
|
||||||
return <Block label={this.props.label}>
|
|
||||||
<InputCheckbox {...props} />
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
|
||||||
|
|
||||||
|
|
||||||
|
type FieldCheckboxProps = InputCheckboxProps & {
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldCheckbox extends React.Component<FieldCheckboxProps> {
|
||||||
|
render() {
|
||||||
|
return <Block label={this.props.label}>
|
||||||
|
<InputCheckbox {...this.props} />
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Block from './Block'
|
|
||||||
import InputColor from './InputColor'
|
|
||||||
|
|
||||||
|
|
||||||
export default class FieldColor extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
...InputColor.propTypes,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {props} = this;
|
|
||||||
|
|
||||||
return <Block label={props.label}>
|
|
||||||
<InputColor {...props} />
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputColor, {InputColorProps} from './InputColor'
|
||||||
|
|
||||||
|
|
||||||
|
type FieldColorProps = InputColorProps & {
|
||||||
|
label?: string
|
||||||
|
fieldSpec?: {
|
||||||
|
doc: string
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldColor extends React.Component<FieldColorProps> {
|
||||||
|
render() {
|
||||||
|
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
|
<InputColor {...this.props} />
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputString from './InputString'
|
import InputString from './InputString'
|
||||||
|
|
||||||
export default class FieldComment extends React.Component {
|
type FieldCommentProps = {
|
||||||
static propTypes = {
|
value?: string
|
||||||
value: PropTypes.string,
|
onChange(value: string | undefined): unknown
|
||||||
onChange: PropTypes.func.isRequired,
|
error: {message: string}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default class FieldComment extends React.Component<FieldCommentProps> {
|
||||||
render() {
|
render() {
|
||||||
const fieldSpec = {
|
const fieldSpec = {
|
||||||
doc: "Comments for the current layer. This is non-standard and not in the spec."
|
doc: "Comments for the current layer. This is non-standard and not in the spec."
|
||||||
@@ -19,12 +19,14 @@ export default class FieldComment extends React.Component {
|
|||||||
label={"Comments"}
|
label={"Comments"}
|
||||||
fieldSpec={fieldSpec}
|
fieldSpec={fieldSpec}
|
||||||
data-wd-key="layer-comment"
|
data-wd-key="layer-comment"
|
||||||
|
error={this.props.error}
|
||||||
>
|
>
|
||||||
<InputString
|
<InputString
|
||||||
multi={true}
|
multi={true}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
default="Comment..."
|
default="Comment..."
|
||||||
|
data-wd-key="layer-comment.input"
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,27 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
|
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
|
||||||
|
|
||||||
export default class FieldDocLabel extends React.Component {
|
type FieldDocLabelProps = {
|
||||||
static propTypes = {
|
label: object | string | undefined
|
||||||
label: PropTypes.oneOfType([
|
fieldSpec?: {
|
||||||
PropTypes.object,
|
doc?: string
|
||||||
PropTypes.string
|
|
||||||
]).isRequired,
|
|
||||||
fieldSpec: PropTypes.object,
|
|
||||||
onToggleDoc: PropTypes.func,
|
|
||||||
}
|
}
|
||||||
|
onToggleDoc?(...args: unknown[]): unknown
|
||||||
|
};
|
||||||
|
|
||||||
constructor (props) {
|
type FieldDocLabelState = {
|
||||||
|
open: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class FieldDocLabel extends React.Component<FieldDocLabelProps, FieldDocLabelState> {
|
||||||
|
constructor (props: FieldDocLabelProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
open: false,
|
open: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onToggleDoc = (open) => {
|
onToggleDoc = (open: boolean) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
open,
|
open,
|
||||||
}, () => {
|
}, () => {
|
||||||
@@ -43,6 +44,7 @@ export default class FieldDocLabel extends React.Component {
|
|||||||
aria-label={this.state.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--${this.state.open ? 'open' : 'closed'}`}
|
className={`maputnik-doc-button maputnik-doc-button--${this.state.open ? 'open' : 'closed'}`}
|
||||||
onClick={() => this.onToggleDoc(!this.state.open)}
|
onClick={() => this.onToggleDoc(!this.state.open)}
|
||||||
|
data-wd-key={'field-doc-button-'+label}
|
||||||
>
|
>
|
||||||
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
|
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
|
||||||
</button>
|
</button>
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Block from './Block'
|
|
||||||
import InputDynamicArray from './InputDynamicArray'
|
|
||||||
import Fieldset from './Fieldset'
|
|
||||||
|
|
||||||
export default class FieldDynamicArray extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
...InputDynamicArray.propTypes,
|
|
||||||
name: PropTypes.string,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {props} = this;
|
|
||||||
|
|
||||||
return <Fieldset label={props.label}>
|
|
||||||
<InputDynamicArray {...props} />
|
|
||||||
</Fieldset>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
|
||||||
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
|
type FieldDynamicArrayProps = InputDynamicArrayProps & {
|
||||||
|
name?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
|
||||||
|
render() {
|
||||||
|
return <Fieldset label={this.props.label}>
|
||||||
|
<InputDynamicArray {...this.props} />
|
||||||
|
</Fieldset>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import InputEnum from './InputEnum'
|
|
||||||
import Block from './Block';
|
|
||||||
import Fieldset from './Fieldset';
|
|
||||||
|
|
||||||
|
|
||||||
export default class FieldEnum extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
...InputEnum.propTypes,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {props} = this;
|
|
||||||
|
|
||||||
return <Fieldset label={props.label}>
|
|
||||||
<InputEnum {...props} />
|
|
||||||
</Fieldset>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import InputEnum, {InputEnumProps} from './InputEnum'
|
||||||
|
import Fieldset from './Fieldset';
|
||||||
|
|
||||||
|
|
||||||
|
type FieldEnumProps = InputEnumProps & {
|
||||||
|
label?: string;
|
||||||
|
fieldSpec?: {
|
||||||
|
doc: string
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldEnum extends React.Component<FieldEnumProps> {
|
||||||
|
render() {
|
||||||
|
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
|
<InputEnum {...this.props} />
|
||||||
|
</Fieldset>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import SpecProperty from './_SpecProperty'
|
import SpecProperty from './_SpecProperty'
|
||||||
import DataProperty from './_DataProperty'
|
import DataProperty, { Stop } from './_DataProperty'
|
||||||
import ZoomProperty from './_ZoomProperty'
|
import ZoomProperty from './_ZoomProperty'
|
||||||
import ExpressionProperty from './_ExpressionProperty'
|
import ExpressionProperty from './_ExpressionProperty'
|
||||||
import {function as styleFunction} from '@maplibre/maplibre-gl-style-spec';
|
import {function as styleFunction} from '@maplibre/maplibre-gl-style-spec';
|
||||||
import {findDefaultFromSpec} from '../util/spec-helper';
|
import {findDefaultFromSpec} from '../libs/spec-helper';
|
||||||
|
|
||||||
|
|
||||||
function isLiteralExpression (value) {
|
function isLiteralExpression(value: any) {
|
||||||
return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
|
return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGetExpression (value) {
|
function isGetExpression(value: any) {
|
||||||
return (
|
return (
|
||||||
Array.isArray(value) &&
|
Array.isArray(value) &&
|
||||||
value.length === 2 &&
|
value.length === 2 &&
|
||||||
@@ -21,14 +20,14 @@ function isGetExpression (value) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isZoomField(value) {
|
function isZoomField(value: any) {
|
||||||
return (
|
return (
|
||||||
typeof(value) === 'object' &&
|
typeof(value) === 'object' &&
|
||||||
value.stops &&
|
value.stops &&
|
||||||
typeof(value.property) === 'undefined' &&
|
typeof(value.property) === 'undefined' &&
|
||||||
Array.isArray(value.stops) &&
|
Array.isArray(value.stops) &&
|
||||||
value.stops.length > 1 &&
|
value.stops.length > 1 &&
|
||||||
value.stops.every(stop => {
|
value.stops.every((stop: Stop) => {
|
||||||
return (
|
return (
|
||||||
Array.isArray(stop) &&
|
Array.isArray(stop) &&
|
||||||
stop.length === 2
|
stop.length === 2
|
||||||
@@ -37,22 +36,22 @@ function isZoomField(value) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isIdentityProperty (value) {
|
function isIdentityProperty(value: any) {
|
||||||
return (
|
return (
|
||||||
typeof(value) === 'object' &&
|
typeof(value) === 'object' &&
|
||||||
value.type === "identity" &&
|
value.type === "identity" &&
|
||||||
value.hasOwnProperty("property")
|
Object.prototype.hasOwnProperty.call(value, "property")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDataStopProperty (value) {
|
function isDataStopProperty(value: any) {
|
||||||
return (
|
return (
|
||||||
typeof(value) === 'object' &&
|
typeof(value) === 'object' &&
|
||||||
value.stops &&
|
value.stops &&
|
||||||
typeof(value.property) !== 'undefined' &&
|
typeof(value.property) !== 'undefined' &&
|
||||||
value.stops.length > 1 &&
|
value.stops.length > 1 &&
|
||||||
Array.isArray(value.stops) &&
|
Array.isArray(value.stops) &&
|
||||||
value.stops.every(stop => {
|
value.stops.every((stop: Stop) => {
|
||||||
return (
|
return (
|
||||||
Array.isArray(stop) &&
|
Array.isArray(stop) &&
|
||||||
stop.length === 2 &&
|
stop.length === 2 &&
|
||||||
@@ -62,26 +61,26 @@ function isDataStopProperty (value) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDataField(value) {
|
function isDataField(value: any) {
|
||||||
return (
|
return (
|
||||||
isIdentityProperty(value) ||
|
isIdentityProperty(value) ||
|
||||||
isDataStopProperty(value)
|
isDataStopProperty(value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPrimative (value) {
|
function isPrimative(value: any): value is string | boolean | number {
|
||||||
const valid = ["string", "boolean", "number"];
|
const valid = ["string", "boolean", "number"];
|
||||||
return valid.includes(typeof(value));
|
return valid.includes(typeof(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isArrayOfPrimatives (values) {
|
function isArrayOfPrimatives(values: any): values is Array<string | boolean | number> {
|
||||||
if (Array.isArray(values)) {
|
if (Array.isArray(values)) {
|
||||||
return values.every(isPrimative);
|
return values.every(isPrimative);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDataType (value, fieldSpec={}) {
|
function getDataType(value: any, fieldSpec={} as any) {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return "value";
|
return "value";
|
||||||
}
|
}
|
||||||
@@ -103,35 +102,33 @@ function getDataType (value, fieldSpec={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type FieldFunctionProps = {
|
||||||
|
onChange(fieldName: string, value: any): unknown
|
||||||
|
fieldName: string
|
||||||
|
fieldType: string
|
||||||
|
fieldSpec: any
|
||||||
|
errors?: {[key: string]: {message: string}}
|
||||||
|
value?: any
|
||||||
|
};
|
||||||
|
|
||||||
|
type FieldFunctionState = {
|
||||||
|
dataType: string
|
||||||
|
isEditing: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/** Supports displaying spec field for zoom function objects
|
/** Supports displaying spec field for zoom function objects
|
||||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||||
*/
|
*/
|
||||||
export default class FieldFunction extends React.Component {
|
export default class FieldFunction extends React.Component<FieldFunctionProps, FieldFunctionState> {
|
||||||
static propTypes = {
|
constructor (props: FieldFunctionProps) {
|
||||||
onChange: PropTypes.func.isRequired,
|
super(props);
|
||||||
fieldName: PropTypes.string.isRequired,
|
|
||||||
fieldType: PropTypes.string.isRequired,
|
|
||||||
fieldSpec: PropTypes.object.isRequired,
|
|
||||||
errors: PropTypes.object,
|
|
||||||
|
|
||||||
value: PropTypes.oneOfType([
|
|
||||||
PropTypes.object,
|
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.number,
|
|
||||||
PropTypes.bool,
|
|
||||||
PropTypes.array
|
|
||||||
]),
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor (props) {
|
|
||||||
super();
|
|
||||||
this.state = {
|
this.state = {
|
||||||
dataType: getDataType(props.value, props.fieldSpec),
|
dataType: getDataType(props.value, props.fieldSpec),
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
static getDerivedStateFromProps(props: FieldFunctionProps, state: FieldFunctionState) {
|
||||||
// Because otherwise when editing values we end up accidentally changing field type.
|
// Because otherwise when editing values we end up accidentally changing field type.
|
||||||
if (state.isEditing) {
|
if (state.isEditing) {
|
||||||
return {};
|
return {};
|
||||||
@@ -144,7 +141,7 @@ export default class FieldFunction extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getFieldFunctionType(fieldSpec) {
|
getFieldFunctionType(fieldSpec: any) {
|
||||||
if (fieldSpec.expression.interpolated) {
|
if (fieldSpec.expression.interpolated) {
|
||||||
return "exponential"
|
return "exponential"
|
||||||
}
|
}
|
||||||
@@ -183,7 +180,7 @@ export default class FieldFunction extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteStop = (stopIdx) => {
|
deleteStop = (stopIdx: number) => {
|
||||||
const stops = this.props.value.stops.slice(0)
|
const stops = this.props.value.stops.slice(0)
|
||||||
stops.splice(stopIdx, 1)
|
stops.splice(stopIdx, 1)
|
||||||
|
|
||||||
@@ -207,7 +204,7 @@ export default class FieldFunction extends React.Component {
|
|||||||
if (value.stops) {
|
if (value.stops) {
|
||||||
zoomFunc = {
|
zoomFunc = {
|
||||||
base: value.base,
|
base: value.base,
|
||||||
stops: value.stops.map(stop => {
|
stops: value.stops.map((stop: Stop) => {
|
||||||
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -292,7 +289,7 @@ export default class FieldFunction extends React.Component {
|
|||||||
property: "",
|
property: "",
|
||||||
type: functionType,
|
type: functionType,
|
||||||
base: value.base,
|
base: value.base,
|
||||||
stops: value.stops.map(stop => {
|
stops: value.stops.map((stop: Stop) => {
|
||||||
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -402,7 +399,7 @@ export default class FieldFunction extends React.Component {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return <div className={propClass} data-wd-key={"spec-field:"+this.props.fieldName}>
|
return <div className={propClass} data-wd-key={"spec-field-container:"+this.props.fieldName}>
|
||||||
{specField}
|
{specField}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import {latest} 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 InputString from './InputString'
|
import InputString from './InputString'
|
||||||
|
|
||||||
export default class FieldId extends React.Component {
|
type FieldIdProps = {
|
||||||
static propTypes = {
|
value: string
|
||||||
value: PropTypes.string.isRequired,
|
wdKey: string
|
||||||
wdKey: PropTypes.string.isRequired,
|
onChange(value: string | undefined): unknown
|
||||||
onChange: PropTypes.func.isRequired,
|
error?: {message: string}
|
||||||
error: PropTypes.object,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
export default class FieldId extends React.Component<FieldIdProps> {
|
||||||
render() {
|
render() {
|
||||||
return <Block label={"ID"} fieldSpec={latest.layer.id}
|
return <Block label={"ID"} fieldSpec={latest.layer.id}
|
||||||
data-wd-key={this.props.wdKey}
|
data-wd-key={this.props.wdKey}
|
||||||
@@ -21,6 +20,7 @@ export default class FieldId extends React.Component {
|
|||||||
<InputString
|
<InputString
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onInput={this.props.onChange}
|
onInput={this.props.onChange}
|
||||||
|
data-wd-key={this.props.wdKey + ".input"}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
}
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import InputJson from './InputJson'
|
|
||||||
|
|
||||||
|
|
||||||
export default class FieldJson extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
...InputJson.propTypes,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {props} = this;
|
|
||||||
return <InputJson {...props} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import InputJson, {InputJsonProps} from './InputJson'
|
||||||
|
|
||||||
|
|
||||||
|
type FieldJsonProps = InputJsonProps & {};
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldJson extends React.Component<FieldJsonProps> {
|
||||||
|
render() {
|
||||||
|
return <InputJson {...this.props} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import {latest} 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 InputNumber from './InputNumber'
|
import InputNumber from './InputNumber'
|
||||||
|
|
||||||
export default class FieldMaxZoom extends React.Component {
|
type FieldMaxZoomProps = {
|
||||||
static propTypes = {
|
value?: number
|
||||||
value: PropTypes.number,
|
onChange(value: number | undefined): unknown
|
||||||
onChange: PropTypes.func.isRequired,
|
error?: {message: string}
|
||||||
error: PropTypes.object,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
|
||||||
render() {
|
render() {
|
||||||
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
|
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
|
||||||
error={this.props.error}
|
error={this.props.error}
|
||||||
@@ -24,6 +23,7 @@ export default class FieldMaxZoom extends React.Component {
|
|||||||
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"
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import {latest} 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 InputNumber from './InputNumber'
|
import InputNumber from './InputNumber'
|
||||||
|
|
||||||
export default class FieldMinZoom extends React.Component {
|
type FieldMinZoomProps = {
|
||||||
static propTypes = {
|
value?: number
|
||||||
value: PropTypes.number,
|
onChange(...args: unknown[]): unknown
|
||||||
onChange: PropTypes.func.isRequired,
|
error?: {message: string}
|
||||||
error: PropTypes.object,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
|
||||||
render() {
|
render() {
|
||||||
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
|
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
|
||||||
error={this.props.error}
|
error={this.props.error}
|
||||||
@@ -24,6 +23,7 @@ export default class FieldMinZoom extends React.Component {
|
|||||||
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'
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
}
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Block from './Block'
|
|
||||||
import InputMultiInput from './InputMultiInput'
|
|
||||||
import Fieldset from './Fieldset'
|
|
||||||
|
|
||||||
|
|
||||||
export default class FieldMultiInput extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
...InputMultiInput.propTypes,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {props} = this;
|
|
||||||
|
|
||||||
return <Fieldset label={props.label}>
|
|
||||||
<InputMultiInput {...props} />
|
|
||||||
</Fieldset>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
|
||||||
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
|
|
||||||
|
type FieldMultiInputProps = InputMultiInputProps & {
|
||||||
|
label?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldMultiInput extends React.Component<FieldMultiInputProps> {
|
||||||
|
render() {
|
||||||
|
return <Fieldset label={this.props.label}>
|
||||||
|
<InputMultiInput {...this.props} />
|
||||||
|
</Fieldset>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import InputNumber from './InputNumber'
|
|
||||||
import Block from './Block'
|
|
||||||
|
|
||||||
|
|
||||||
export default class FieldNumber extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
...InputNumber.propTypes,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {props} = this;
|
|
||||||
return <Block label={props.label}>
|
|
||||||
<InputNumber {...props} />
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import InputNumber, {InputNumberProps} from './InputNumber'
|
||||||
|
import Block from './Block'
|
||||||
|
|
||||||
|
|
||||||
|
type FieldNumberProps = InputNumberProps & {
|
||||||
|
label?: string
|
||||||
|
fieldSpec?: {
|
||||||
|
doc: string
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldNumber extends React.Component<FieldNumberProps> {
|
||||||
|
render() {
|
||||||
|
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
|
<InputNumber {...this.props} />
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Block from './Block'
|
|
||||||
import InputSelect from './InputSelect'
|
|
||||||
|
|
||||||
|
|
||||||
export default class FieldSelect extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
...InputSelect.propTypes,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {props} = this;
|
|
||||||
|
|
||||||
return <Block label={props.label}>
|
|
||||||
<InputSelect {...props}/>
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputSelect, {InputSelectProps} from './InputSelect'
|
||||||
|
|
||||||
|
|
||||||
|
type FieldSelectProps = InputSelectProps & {
|
||||||
|
label?: string
|
||||||
|
fieldSpec?: {
|
||||||
|
doc: string
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldSelect extends React.Component<FieldSelectProps> {
|
||||||
|
render() {
|
||||||
|
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
|
<InputSelect {...this.props}/>
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import {latest} 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 InputAutocomplete from './InputAutocomplete'
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
|
|
||||||
export default class FieldSource extends React.Component {
|
type FieldSourceProps = {
|
||||||
static propTypes = {
|
value?: string
|
||||||
value: PropTypes.string,
|
wdKey?: string
|
||||||
wdKey: PropTypes.string,
|
onChange?(value: string| undefined): unknown
|
||||||
onChange: PropTypes.func,
|
sourceIds?: unknown[]
|
||||||
sourceIds: PropTypes.array,
|
error?: {message: string}
|
||||||
error: PropTypes.object,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
export default class FieldSource extends React.Component<FieldSourceProps> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
sourceIds: [],
|
sourceIds: [],
|
||||||
@@ -29,7 +28,7 @@ export default class FieldSource extends React.Component {
|
|||||||
<InputAutocomplete
|
<InputAutocomplete
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
options={this.props.sourceIds.map(src => [src, src])}
|
options={this.props.sourceIds?.map(src => [src, src])}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputAutocomplete from './InputAutocomplete'
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
|
|
||||||
export default class FieldSourceLayer extends React.Component {
|
type FieldSourceLayerProps = {
|
||||||
static propTypes = {
|
value?: string
|
||||||
value: PropTypes.string,
|
onChange?(...args: unknown[]): unknown
|
||||||
onChange: PropTypes.func,
|
sourceLayerIds?: unknown[]
|
||||||
sourceLayerIds: PropTypes.array,
|
isFixed?: boolean
|
||||||
isFixed: PropTypes.bool,
|
error?: {message: string}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default class FieldSourceLayer extends React.Component<FieldSourceLayerProps> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
sourceLayerIds: [],
|
sourceLayerIds: [],
|
||||||
@@ -20,14 +20,17 @@ export default class FieldSourceLayer extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <Block label={"Source Layer"} fieldSpec={latest.layer['source-layer']}
|
return <Block
|
||||||
|
label={"Source Layer"}
|
||||||
|
fieldSpec={latest.layer['source-layer']}
|
||||||
data-wd-key="layer-source-layer"
|
data-wd-key="layer-source-layer"
|
||||||
|
error={this.props.error}
|
||||||
>
|
>
|
||||||
<InputAutocomplete
|
<InputAutocomplete
|
||||||
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
options={this.props.sourceLayerIds.map(l => [l, l])}
|
options={this.props.sourceLayerIds?.map(l => [l, l])}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
}
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import Block from './Block'
|
|
||||||
import InputString from './InputString'
|
|
||||||
|
|
||||||
export default class FieldString extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
...InputString.propTypes,
|
|
||||||
name: PropTypes.string,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {props} = this;
|
|
||||||
|
|
||||||
return <Block label={props.label}>
|
|
||||||
<InputString {...props} />
|
|
||||||
</Block>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputString, {InputStringProps} from './InputString'
|
||||||
|
|
||||||
|
type FieldStringProps = InputStringProps & {
|
||||||
|
name?: string
|
||||||
|
label?: string
|
||||||
|
fieldSpec?: {
|
||||||
|
doc: string
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class FieldString extends React.Component<FieldStringProps> {
|
||||||
|
render() {
|
||||||
|
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
|
<InputString {...this.props} />
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import {latest} 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'
|
||||||
|
|
||||||
export default class FieldType extends React.Component {
|
type FieldTypeProps = {
|
||||||
static propTypes = {
|
value: string
|
||||||
value: PropTypes.string.isRequired,
|
wdKey?: string
|
||||||
wdKey: PropTypes.string,
|
onChange(value: string): unknown
|
||||||
onChange: PropTypes.func.isRequired,
|
error?: {message: string}
|
||||||
error: PropTypes.object,
|
disabled?: boolean
|
||||||
disabled: PropTypes.bool,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
export default class FieldType extends React.Component<FieldTypeProps> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
}
|
}
|
||||||
@@ -45,6 +44,7 @@ export default class FieldType extends React.Component {
|
|||||||
]}
|
]}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
|
data-wd-key={this.props.wdKey + ".select"}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</Block>
|
</Block>
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import React, {Fragment} from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import InputUrl from './InputUrl'
|
|
||||||
import Block from './Block'
|
|
||||||
|
|
||||||
|
|
||||||
export default class FieldUrl extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
...InputUrl.propTypes,
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const {props} = this;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Block label={this.props.label}>
|
|
||||||
<InputUrl {...props} />
|
|
||||||
</Block>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl'
|
||||||
|
import Block from './Block'
|
||||||
|
|
||||||
|
|
||||||
|
type FieldUrlProps = InputUrlProps & {
|
||||||
|
label: string;
|
||||||
|
fieldSpec?: {
|
||||||
|
doc: string
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldUrl extends React.Component<FieldUrlProps> {
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
|
<InputUrl {...this.props} />
|
||||||
|
</Block>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,29 +1,36 @@
|
|||||||
import React from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import FieldDocLabel from './FieldDocLabel'
|
import FieldDocLabel from './FieldDocLabel'
|
||||||
import Doc from './Doc'
|
import Doc from './Doc'
|
||||||
|
import generateUniqueId from '../libs/document-uid';
|
||||||
|
|
||||||
|
type FieldsetProps = {
|
||||||
|
label?: string,
|
||||||
|
fieldSpec?: { doc?: string },
|
||||||
|
action?: ReactElement,
|
||||||
|
};
|
||||||
|
|
||||||
let IDX = 0;
|
type FieldsetState = {
|
||||||
|
showDoc: boolean
|
||||||
|
};
|
||||||
|
|
||||||
export default class Fieldset extends React.Component {
|
export default class Fieldset extends React.Component<FieldsetProps, FieldsetState> {
|
||||||
constructor (props) {
|
_labelId: string;
|
||||||
|
|
||||||
|
constructor (props: FieldsetProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this._labelId = `fieldset_label_${(IDX++)}`;
|
this._labelId = generateUniqueId(`fieldset_label_`);
|
||||||
this.state = {
|
this.state = {
|
||||||
showDoc: false,
|
showDoc: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onToggleDoc = (val) => {
|
onToggleDoc = (val: boolean) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
showDoc: val
|
showDoc: val
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const {props} = this;
|
|
||||||
|
|
||||||
return <div className="maputnik-input-block" role="group" aria-labelledby={this._labelId}>
|
return <div className="maputnik-input-block" role="group" aria-labelledby={this._labelId}>
|
||||||
{this.props.fieldSpec &&
|
{this.props.fieldSpec &&
|
||||||
<div className="maputnik-input-block-label">
|
<div className="maputnik-input-block-label">
|
||||||
@@ -36,14 +43,14 @@ export default class Fieldset extends React.Component {
|
|||||||
}
|
}
|
||||||
{!this.props.fieldSpec &&
|
{!this.props.fieldSpec &&
|
||||||
<div className="maputnik-input-block-label">
|
<div className="maputnik-input-block-label">
|
||||||
{props.label}
|
{this.props.label}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div className="maputnik-input-block-action">
|
<div className="maputnik-input-block-action">
|
||||||
{this.props.action}
|
{this.props.action}
|
||||||
</div>
|
</div>
|
||||||
<div className="maputnik-input-block-content">
|
<div className="maputnik-input-block-content">
|
||||||
{props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
{this.props.fieldSpec &&
|
{this.props.fieldSpec &&
|
||||||
<div
|
<div
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { combiningFilterOps } from '../libs/filterops.js'
|
|
||||||
import {mdiTableRowPlusAfter} from '@mdi/js';
|
import {mdiTableRowPlusAfter} from '@mdi/js';
|
||||||
import {isEqual} from 'lodash';
|
import {isEqual} from 'lodash';
|
||||||
|
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 {combiningFilterOps} from '../libs/filterops'
|
||||||
import InputSelect from './InputSelect'
|
import InputSelect from './InputSelect'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import SingleFilterEditor from './SingleFilterEditor'
|
import SingleFilterEditor from './SingleFilterEditor'
|
||||||
@@ -12,11 +13,10 @@ import FilterEditorBlock from './FilterEditorBlock'
|
|||||||
import InputButton from './InputButton'
|
import InputButton from './InputButton'
|
||||||
import Doc from './Doc'
|
import Doc from './Doc'
|
||||||
import ExpressionProperty from './_ExpressionProperty';
|
import ExpressionProperty from './_ExpressionProperty';
|
||||||
import {mdiFunctionVariant} from '@mdi/js';
|
|
||||||
|
|
||||||
|
|
||||||
function combiningFilter (props) {
|
function combiningFilter(props: FilterEditorProps): LegacyFilterSpecification | ExpressionSpecification {
|
||||||
let filter = props.filter || ['all'];
|
const filter = props.filter || ['all'];
|
||||||
|
|
||||||
if (!Array.isArray(filter)) {
|
if (!Array.isArray(filter)) {
|
||||||
return filter;
|
return filter;
|
||||||
@@ -30,23 +30,24 @@ function combiningFilter (props) {
|
|||||||
filters = [filter.slice(0)];
|
filters = [filter.slice(0)];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [combiningOp, ...filters];
|
return [combiningOp, ...filters] as LegacyFilterSpecification | ExpressionSpecification;
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateFilter (filter) {
|
function migrateFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
|
||||||
return migrate(createStyleFromFilter(filter)).layers[0].filter;
|
// This "any" can be removed in latest version of maplibre where maplibre re-exported types from style-spec
|
||||||
|
return (migrate(createStyleFromFilter(filter) as any).layers[0] as any).filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createStyleFromFilter (filter) {
|
function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpecification): StyleSpecification & {id: string} {
|
||||||
return {
|
return {
|
||||||
"id": "tmp",
|
"id": "tmp",
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"name": "Empty Style",
|
"name": "Empty Style",
|
||||||
"metadata": {"maputnik:renderer": "mbgljs"},
|
"metadata": {"maputnik:renderer": "mlgljs"},
|
||||||
"sources": {
|
"sources": {
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"type": "geojson",
|
"type": "geojson",
|
||||||
"data": {}
|
"data": ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sprite": "",
|
"sprite": "",
|
||||||
@@ -69,7 +70,7 @@ const FILTER_OPS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
// If we convert a filter that is an expression to an expression it'll remain the same in value
|
// If we convert a filter that is an expression to an expression it'll remain the same in value
|
||||||
function checkIfSimpleFilter (filter) {
|
function checkIfSimpleFilter (filter: LegacyFilterSpecification | ExpressionSpecification) {
|
||||||
if (filter.length === 1 && FILTER_OPS.includes(filter[0])) {
|
if (filter.length === 1 && FILTER_OPS.includes(filter[0])) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -77,33 +78,38 @@ function checkIfSimpleFilter (filter) {
|
|||||||
return !isEqual(expression, filter);
|
return !isEqual(expression, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCombiningFilter(filter) {
|
function hasCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
|
||||||
return combiningFilterOps.indexOf(filter[0]) >= 0
|
return combiningFilterOps.indexOf(filter[0]) >= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasNestedCombiningFilter(filter) {
|
function hasNestedCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
|
||||||
if(hasCombiningFilter(filter)) {
|
if(hasCombiningFilter(filter)) {
|
||||||
const combinedFilters = filter.slice(1)
|
return filter.slice(1).map(f => hasCombiningFilter(f as any)).filter(f => f == true).length > 0
|
||||||
return filter.slice(1).map(f => hasCombiningFilter(f)).filter(f => f == true).length > 0
|
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class FilterEditor extends React.Component {
|
type FilterEditorProps = {
|
||||||
static propTypes = {
|
/** Properties of the vector layer and the available fields */
|
||||||
/** Properties of the vector layer and the available fields */
|
properties?: {[key:string]: any}
|
||||||
properties: PropTypes.object,
|
filter?: any[]
|
||||||
filter: PropTypes.array,
|
errors?: {[key:string]: any}
|
||||||
errors: PropTypes.object,
|
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
|
||||||
onChange: PropTypes.func.isRequired,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
type FilterEditorState = {
|
||||||
|
showDoc: boolean
|
||||||
|
displaySimpleFilter: boolean
|
||||||
|
valueIsSimpleFilter?: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class FilterEditor extends React.Component<FilterEditorProps, FilterEditorState> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
filter: ["all"],
|
filter: ["all"],
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor (props) {
|
constructor (props: FilterEditorProps) {
|
||||||
super();
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
showDoc: false,
|
showDoc: false,
|
||||||
displaySimpleFilter: checkIfSimpleFilter(combiningFilter(props)),
|
displaySimpleFilter: checkIfSimpleFilter(combiningFilter(props)),
|
||||||
@@ -111,25 +117,25 @@ export default class FilterEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert filter to combining filter
|
// Convert filter to combining filter
|
||||||
onFilterPartChanged(filterIdx, newPart) {
|
onFilterPartChanged(filterIdx: number, newPart: any[]) {
|
||||||
const newFilter = combiningFilter(this.props).slice(0)
|
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
|
||||||
newFilter[filterIdx] = newPart
|
newFilter[filterIdx] = newPart
|
||||||
this.props.onChange(newFilter)
|
this.props.onChange(newFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteFilterItem(filterIdx) {
|
deleteFilterItem(filterIdx: number) {
|
||||||
const newFilter = combiningFilter(this.props).slice(0)
|
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
|
||||||
newFilter.splice(filterIdx + 1, 1)
|
newFilter.splice(filterIdx + 1, 1)
|
||||||
this.props.onChange(newFilter)
|
this.props.onChange(newFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
addFilterItem = () => {
|
addFilterItem = () => {
|
||||||
const newFilterItem = combiningFilter(this.props).slice(0)
|
const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
|
||||||
newFilterItem.push(['==', 'name', ''])
|
(newFilterItem as any[]).push(['==', 'name', ''])
|
||||||
this.props.onChange(newFilterItem)
|
this.props.onChange(newFilterItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
onToggleDoc = (val) => {
|
onToggleDoc = (val: boolean) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
showDoc: val
|
showDoc: val
|
||||||
});
|
});
|
||||||
@@ -142,15 +148,14 @@ export default class FilterEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
makeExpression = () => {
|
makeExpression = () => {
|
||||||
let filter = combiningFilter(this.props);
|
const filter = combiningFilter(this.props);
|
||||||
this.props.onChange(migrateFilter(filter));
|
this.props.onChange(migrateFilter(filter));
|
||||||
this.setState({
|
this.setState({
|
||||||
displaySimpleFilter: false,
|
displaySimpleFilter: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps (props, currentState) {
|
static getDerivedStateFromProps(props: FilterEditorProps, currentState: FilterEditorState) {
|
||||||
const {filter} = props;
|
|
||||||
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
|
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
|
||||||
|
|
||||||
// Upgrade but never downgrade
|
// Upgrade but never downgrade
|
||||||
@@ -178,7 +183,7 @@ export default class FilterEditor extends React.Component {
|
|||||||
const fieldSpec={
|
const fieldSpec={
|
||||||
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
|
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
|
||||||
};
|
};
|
||||||
const defaultFilter = ["all"];
|
const defaultFilter = ["all"] as LegacyFilterSpecification | ExpressionSpecification;
|
||||||
|
|
||||||
const isNestedCombiningFilter = displaySimpleFilter && hasNestedCombiningFilter(combiningFilter(this.props));
|
const isNestedCombiningFilter = displaySimpleFilter && hasNestedCombiningFilter(combiningFilter(this.props));
|
||||||
|
|
||||||
@@ -200,8 +205,8 @@ export default class FilterEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
else if (displaySimpleFilter) {
|
else if (displaySimpleFilter) {
|
||||||
const filter = combiningFilter(this.props);
|
const filter = combiningFilter(this.props);
|
||||||
let combiningOp = filter[0];
|
const combiningOp = filter[0];
|
||||||
let filters = filter.slice(1)
|
const filters = filter.slice(1) as (LegacyFilterSpecification | ExpressionSpecification)[];
|
||||||
|
|
||||||
const actions = (
|
const actions = (
|
||||||
<div>
|
<div>
|
||||||
@@ -218,7 +223,7 @@ export default class FilterEditor extends React.Component {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const editorBlocks = filters.map((f, idx) => {
|
const editorBlocks = filters.map((f, idx) => {
|
||||||
const error = errors[`filter[${idx+1}]`];
|
const error = errors![`filter[${idx+1}]`];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={`block-${idx}`}>
|
<div key={`block-${idx}`}>
|
||||||
@@ -247,7 +252,7 @@ export default class FilterEditor extends React.Component {
|
|||||||
>
|
>
|
||||||
<InputSelect
|
<InputSelect
|
||||||
value={combiningOp}
|
value={combiningOp}
|
||||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
|
||||||
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
@@ -277,7 +282,7 @@ export default class FilterEditor extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let {filter} = this.props;
|
const {filter} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import InputButton from './InputButton'
|
import InputButton from './InputButton'
|
||||||
import {MdDelete} from 'react-icons/md'
|
import {MdDelete} from 'react-icons/md'
|
||||||
|
|
||||||
export default class FilterEditorBlock extends React.Component {
|
type FilterEditorBlockProps = {
|
||||||
static propTypes = {
|
onDelete(...args: unknown[]): unknown
|
||||||
onDelete: PropTypes.func.isRequired,
|
};
|
||||||
children: PropTypes.element.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export default class FilterEditorBlock extends React.Component<FilterEditorBlockProps> {
|
||||||
render() {
|
render() {
|
||||||
return <div className="maputnik-filter-editor-block">
|
return <div className="maputnik-filter-editor-block">
|
||||||
<div className="maputnik-filter-editor-block-action">
|
<div className="maputnik-filter-editor-block-action">
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
|
|
||||||
import IconLine from './IconLine.jsx'
|
|
||||||
import IconFill from './IconFill.jsx'
|
|
||||||
import IconSymbol from './IconSymbol.jsx'
|
|
||||||
import IconBackground from './IconBackground.jsx'
|
|
||||||
import IconCircle from './IconCircle.jsx'
|
|
||||||
import IconMissing from './IconMissing.jsx'
|
|
||||||
|
|
||||||
export default class IconLayer extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
type: PropTypes.string.isRequired,
|
|
||||||
style: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const iconProps = { style: this.props.style }
|
|
||||||
switch(this.props.type) {
|
|
||||||
case 'fill-extrusion': return <IconBackground {...iconProps} />
|
|
||||||
case 'raster': return <IconFill {...iconProps} />
|
|
||||||
case 'hillshade': return <IconFill {...iconProps} />
|
|
||||||
case 'heatmap': return <IconFill {...iconProps} />
|
|
||||||
case 'fill': return <IconFill {...iconProps} />
|
|
||||||
case 'background': return <IconBackground {...iconProps} />
|
|
||||||
case 'line': return <IconLine {...iconProps} />
|
|
||||||
case 'symbol': return <IconSymbol {...iconProps} />
|
|
||||||
case 'circle': return <IconCircle {...iconProps} />
|
|
||||||
default: return <IconMissing {...iconProps} />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import IconLine from './IconLine'
|
||||||
|
import IconFill from './IconFill'
|
||||||
|
import IconSymbol from './IconSymbol'
|
||||||
|
import IconBackground from './IconBackground'
|
||||||
|
import IconCircle from './IconCircle'
|
||||||
|
import IconMissing from './IconMissing'
|
||||||
|
|
||||||
|
type IconLayerProps = {
|
||||||
|
type: string
|
||||||
|
style?: object
|
||||||
|
className?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class IconLayer extends React.Component<IconLayerProps> {
|
||||||
|
render() {
|
||||||
|
const iconProps = { style: this.props.style }
|
||||||
|
switch(this.props.type) {
|
||||||
|
case 'fill-extrusion': return <IconBackground {...iconProps} />
|
||||||
|
case 'raster': return <IconFill {...iconProps} />
|
||||||
|
case 'hillshade': return <IconFill {...iconProps} />
|
||||||
|
case 'heatmap': return <IconFill {...iconProps} />
|
||||||
|
case 'fill': return <IconFill {...iconProps} />
|
||||||
|
case 'background': return <IconBackground {...iconProps} />
|
||||||
|
case 'line': return <IconLine {...iconProps} />
|
||||||
|
case 'symbol': return <IconSymbol {...iconProps} />
|
||||||
|
case 'circle': return <IconCircle {...iconProps} />
|
||||||
|
default: return <IconMissing {...iconProps} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user