Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 59ccf0d391 Bump loader-utils from 1.4.0 to 1.4.2
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.2.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.2)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-18 03:53:19 +00:00
284 changed files with 17347 additions and 12338 deletions
+4
View File
@@ -0,0 +1,4 @@
{
"packages": [],
"sandboxes": ["/"]
}
-46
View File
@@ -1,46 +0,0 @@
{
"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"
}
}
+20
View File
@@ -0,0 +1,20 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
+1 -2
View File
@@ -1,2 +1 @@
github: [maplibre] custom: "https://maputnik.github.io/donate"
open_collective: maplibre
-11
View File
@@ -1,11 +0,0 @@
## Launch Checklist
<!-- Thanks for the PR! Feel free to add or remove items from the checklist. -->
- [ ] Briefly describe the changes in this PR.
- [ ] Link to related issues.
- [ ] Include before/after visuals or gifs if this PR includes visual changes.
- [ ] Write tests for all new functionality.
- [ ] Add an entry to `CHANGELOG.md` under the `## main` section.
+61 -34
View File
@@ -8,18 +8,23 @@ on:
jobs: jobs:
build-docker: build-docker:
name: build docker name: build/docker
runs-on: ubuntu-latest runs-on: ${{ matrix.os }}
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@v4 - uses: actions/checkout@v4
- run: docker build -t test-docker-image-build . - run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
# build the editor # build the editor
build-node: build-node:
name: "build on ${{ matrix.os }}" name: "build/node@${{ matrix.node-version }} (${{ 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' }}
@@ -28,68 +33,95 @@ 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: [18.x]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v1
with: with:
node-version-file: '.nvmrc' node-version: ${{ matrix.node-version }}
- 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" name: "build/artifacts (${{ matrix.os }})"
runs-on: ubuntu-latest runs-on: ${{ matrix.os }}
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: [18.x]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v1
with: with:
node-version-file: '.nvmrc' node-version: ${{ matrix.node-version }}
- 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
- name: artifacts/maputnik - run: npm run build-storybook
uses: actions/upload-artifact@v4 - name: artifacts/editor
uses: actions/upload-artifact@v1
with: with:
name: maputnik name: editor
path: dist path: dist
- 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
uses: actions/setup-go@v5 uses: actions/setup-go@v3
with: with:
go-version: ^1.23.x go-version: ^1.19.x
cache-dependency-path: desktop/go.sum
id: go id: go
- name: Build desktop artifacts - name: Check out code into the Go module directory
run: npm run build-desktop uses: actions/checkout@v4
with:
repository: maputnik/desktop
ref: master
path: ./src/github.com/maputnik/desktop/
- name: Make
run: cd src/github.com/maputnik/desktop/ && make
- name: Artifacts/linux - name: Artifacts/linux
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v1
with: with:
name: maputnik-linux name: maputnik-linux
path: ./desktop/bin/linux/ path: ./src/github.com/maputnik/desktop/bin/linux/
- name: Artifacts/darwin - name: Artifacts/darwin
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v1
with: with:
name: maputnik-darwin name: maputnik-darwin
path: ./desktop/bin/darwin/ path: ./src/github.com/maputnik/desktop/bin/darwin/
- name: Artifacts/windows - name: Artifacts/windows
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v1
with: with:
name: maputnik-windows name: maputnik-windows
path: ./desktop/bin/windows/ path: ./src/github.com/maputnik/desktop/bin/windows/
e2e-tests: cypress-run:
name: "E2E tests using ${{ matrix.browser }}"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -106,8 +138,3 @@ jobs:
build: npm run build build: npm run build
start: npm run start start: npm run start
browser: ${{ matrix.browser }} browser: ${{ matrix.browser }}
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
files: ${{ github.workspace }}/.nyc_output/out.json
verbose: true
@@ -1,39 +0,0 @@
name: Create bump version PR
on:
workflow_dispatch:
inputs:
version:
description: Version to change to.
required: true
type: string
jobs:
bump-version-pr:
name: Bump version PR
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Bump version
run: |
npm version --commit-hooks false --git-tag-version false ${{ inputs.version }}
./build/bump-version-changelog.js ${{ inputs.version }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
commit-message: Bump version to ${{ inputs.version }}
branch: bump-version-to-${{ inputs.version }}
title: Bump version to ${{ inputs.version }}
+9 -33
View File
@@ -3,49 +3,25 @@ name: deploy
on: on:
push: push:
branches: [ main ] branches: [ main ]
push:
tags:
- 'v*'
jobs: jobs:
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 # publish docker to GitHub registry
deploy-docker: deploy-docker:
name: deploy/docker name: deploy/docker
runs-on: ubuntu-latest runs-on: ${{ matrix.os }}
if: ${{ github.event_name == 'push' }} if: ${{ github.event_name == 'push' }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix:
os: [ubuntu-latest]
steps: steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: docker build -t ghcr.io/maplibre/maputnik:main . - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u orangemug --password-stdin
- run: docker push ghcr.io/maplibre/maputnik:main - run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
- run: docker push docker.pkg.github.com/maputnik/editor/editor:main
-104
View File
@@ -1,104 +0,0 @@
name: Release
on:
push:
branches: [main]
workflow_dispatch:
jobs:
release-check:
name: Check if version changed
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Check if version has been updated
id: check
uses: EndBug/version-check@v2
outputs:
publish: ${{ steps.check.outputs.changed }}
release:
name: Release
needs: release-check
if: ${{ needs.release-check.outputs.publish == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
registry-url: "https://registry.npmjs.org"
- name: Set up Go for desktop build
uses: actions/setup-go@v5
with:
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
id: go
- name: Get version
id: package-version
uses: martinbeentjes/npm-get-version-action@v1.3.1
- name: Install
run: npm ci
- name: Build
run: |
npm run build
npm run build-desktop
- name: Tag commit and push
id: tag_version
uses: mathieudutour/github-tag-action@v6.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
custom_tag: ${{ steps.package-version.outputs.current-version }}
- name: Create Archives
run: |
zip -r dist dist
zip -r desktop desktop/bin/
- name: Build Release Notes
id: release_notes
run: |
RELEASE_NOTES_PATH="${PWD}/release_notes.txt"
./build/release-notes.js > ${RELEASE_NOTES_PATH}
echo "release_notes=${RELEASE_NOTES_PATH}" >> $GITHUB_OUTPUT
- name: Create GitHub Release
id: create_regular_release
uses: ncipollo/release-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag: ${{ steps.tag_version.outputs.new_tag }}
name: ${{ steps.tag_version.outputs.new_tag }}
bodyFile: ${{ steps.release_notes.outputs.release_notes }}
artifacts: "dist.zip,desktop.zip"
allowUpdates: true
draft: false
prerelease: false
+1 -2
View File
@@ -14,7 +14,6 @@ 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,6 +32,6 @@ node_modules
public public
/errorShots /errorShots
/old /old
/build
/cypress/screenshots /cypress/screenshots
/dist/ /dist/
/desktop/version.go
-1
View File
@@ -1 +0,0 @@
legacy-peer-deps = true
-1
View File
@@ -1 +0,0 @@
18.19
-18
View File
@@ -1,18 +0,0 @@
{
"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"]
}
+9
View File
@@ -0,0 +1,9 @@
const config = {
stories: ['../stories/**/*.stories.jsx'],
addons: ['@storybook/addon-actions', '@storybook/addon-links', '@storybook/addon-a11y/register', '@storybook/addon-storysource'],
framework: {
name: '@storybook/react-vite',
options: {}
}
};
export default config;
+7
View File
@@ -0,0 +1,7 @@
import { addons } from '@storybook/addons';
import { themes } from '@storybook/theming';
import theme from './maputnik.theme';
addons.setConfig({
theme: theme,
});
+8
View File
@@ -0,0 +1,8 @@
import { create } from '@storybook/theming/create';
export default create({
base: 'light',
brandTitle: 'Maputnik',
brandUrl: 'https://github.com/maputnik/editor',
});
-24
View File
@@ -1,24 +0,0 @@
## main
### ✨ Features and improvements
- _...Add new stuff here..._
### 🐞 Bug fixes
- _...Add new stuff here..._
## 2.1.0
### ✨ Features and improvements
- Add GitHub workflows for releasing new versions
- Update desktop build to pull from this repo (#922)
## 2.0.0
- Update MapLibre to version 4 (#872)
- Start continuous deployment of maputnik website
## 1.7.0
- See release notes at https://maputnik.github.io/blog/2020/04/23/release-v1.7.0
-2
View File
@@ -1,2 +0,0 @@
# Contributor Covenant
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/maplibre/maplibre/blob/main/CODE_OF_CONDUCT.md)
+12 -6
View File
@@ -2,15 +2,21 @@ 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
COPY package.json package-lock.json .npmrc ./ COPY package.json package-lock.json ./
RUN npm ci RUN npm install
# Build maputnik # Build maputnik
# TODO: we should also do a npm run test here (needs more dependencies)
COPY . . COPY . .
RUN npx vite build RUN npm run build
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Create a clean nginx-alpine slim image with just the build results
FROM nginx:alpine-slim
COPY --from=builder /maputnik/dist /usr/share/nginx/html/ # Create a clean python-based image with just the build results
FROM python:3-slim
WORKDIR /maputnik
COPY --from=builder /maputnik/dist .
EXPOSE 8888
CMD python -m http.server 8888
-1
View File
@@ -1,7 +1,6 @@
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
+69 -24
View File
@@ -1,10 +1,10 @@
<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 CI status](https://github.com/maplibre/maputnik/workflows/ci/badge.svg)][github-action-ci] [![GitHub CI status](https://github.com/maputnik/editor/workflows/ci/badge.svg)][github-action-ci]
[![License](https://img.shields.io/badge/license-MIT-blue.svg)][license] [![License](https://img.shields.io/badge/license-MIT-blue.svg)][license]
[github-action-ci]: https://github.com/maplibre/maputnik/actions?query=workflow%3Aci [github-action-ci]: https://github.com/maputnik/editor/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 [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/) A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
@@ -13,32 +13,33 @@ targeted at developers and map designers.
## Usage ## Usage
- :link: Design your maps online at **<https://www.maplibre.org/maputnik/>** (all in local storage) - :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
- :link: Use the [Maputnik CLI](https://github.com/maplibre/maputnik/wiki/Maputnik-CLI) for local style development - :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/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:80 ghcr.io/maplibre/maputnik:main docker run -it --rm -p 8888:8888 maputnik/editor
``` ```
## 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/maplibre/maputnik/wiki). You are welcome to collaborate! The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
- :link: **Study the [Maputnik Wiki](https://github.com/maplibre/maputnik/wiki)** - :link: **Study the [Maputnik Wiki](https://github.com/maputnik/editor/wiki)**
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo - :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
[![Design Map from Scratch](https://j.gifs.com/g5XMgl.gif)](https://youtu.be/XoDh0gEnBQo) [![Design Map from Scratch](https://j.gifs.com/g5XMgl.gif)](https://youtu.be/XoDh0gEnBQo)
## Develop ## Develop
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/). Maputnik is written in ES6 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
@@ -66,8 +67,7 @@ Lint the JavaScript code.
``` ```
# run linter # run linter
npm run lint npm run lint
npm run lint-css npm run lint-styles
npm run sort-styles
``` ```
@@ -94,23 +94,68 @@ You can also see the tests as they run or select which suites to run by executin
npm run cy:open npm run cy:open
``` ```
## Release process
1. Review [`CHANGELOG.md`](/CHANGELOG.md) ## Related Projects
- Double-check that all changes included in the release are appropriately documented.
- To-be-released changes should be under the "main" header.
- Commit any final changes to the changelog.
2. Run [Create bump version PR](https://github.com/maplibre/maputnik/actions/workflows/create-bump-version-pr.yml) by manual workflow dispatch and set the version number in the input. This will create a PR that changes the changelog and `package.json` file to review and merge.
3. Once merged, an automatic process will kick in and creates a GitHub release and uploads release assets.
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
## 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.
Read more about the MapLibre Sponsorship Program at https://maplibre.org/sponsors/. ### Gold
- [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 Maplibre contributors. Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and 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 is an independent style editor.
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.
-2
View File
@@ -1,2 +0,0 @@
For an up-to-date policy refer to
https://github.com/maplibre/maplibre/blob/main/SECURITY_POLICY.txt
-11
View File
@@ -1,11 +0,0 @@
# Build Scripts
This folder holds common build scripts used by some of the Github workflows.
The scripts are borrowed from [maplibre/maplibre-gl-js](https://github.com/maplibre/maplibre-gl-js/tree/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build).
## Generate Release Notes
`bump-version-changelog.js` Used to update the changelog with the current notes, and set up a space for new notes
`release-notes.js` Used to generate release notes when releasing a new version
-29
View File
@@ -1,29 +0,0 @@
#!/usr/bin/env node
/**
* This script updates the changelog.md file with the version given in the arguments
* It replaces ## main with ## <version>
* Removes _...Add new stuff here..._
* And adds on top a ## main with add stuff here.
*
* Copied from maplibre/maplibre-gl-js
* https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
*/
import * as fs from 'fs';
const changelogPath = 'CHANGELOG.md';
let changelog = fs.readFileSync(changelogPath, 'utf8');
changelog = changelog.replace('## main', `## ${process.argv[2]}`);
changelog = changelog.replaceAll('- _...Add new stuff here..._\n', '');
changelog = `## main
### ✨ Features and improvements
- _...Add new stuff here..._
### 🐞 Bug fixes
- _...Add new stuff here..._
` + changelog;
fs.writeFileSync(changelogPath, changelog, 'utf8');
-48
View File
@@ -1,48 +0,0 @@
#!/usr/bin/env node
// Copied from maplibre/maplibre-gl-js
// https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
import * as fs from 'fs';
const changelogPath = 'CHANGELOG.md';
const changelog = fs.readFileSync(changelogPath, 'utf8');
/*
Parse the raw changelog text and split it into individual releases.
This regular expression:
- Matches lines starting with "## x.x.x".
- Groups the version number.
- Skips the (optional) release date.
- Groups the changelog content.
- Ends when another "## x.x.x" is found.
*/
const regex = /^## (\d+\.\d+\.\d+.*?)\n(.+?)(?=\n^## \d+\.\d+\.\d+.*?\n)/gms;
let releaseNotes = [];
let match;
// eslint-disable-next-line no-cond-assign
while (match = regex.exec(changelog)) {
releaseNotes.push({
'version': match[1],
'changelog': match[2].trim(),
});
}
const latest = releaseNotes[0];
const previous = releaseNotes[1];
// Print the release notes template.
let header = 'Changes since previous version'
if (previous) {
header = `https://github.com/maplibre/maputnik
[Changes](https://github.com/maplibre/maputnik/compare/v${previous.version}...v${latest.version}) since [Maputnik v${previous.version}](https://github.com/maplibre/maputnik/releases/tag/v${previous.version})`
}
const templatedReleaseNotes = `${header}
${latest.changelog}
// eslint-disable-next-line eol-last
process.stdout.write(templatedReleaseNotes.trimEnd());
-14
View File
@@ -1,23 +1,9 @@
import { defineConfig } from "cypress"; import { defineConfig } from "cypress";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
export default defineConfig({ export default defineConfig({
env: {
codeCoverage: {
exclude: "cypress/**/*.*",
},
},
e2e: { e2e: {
setupNodeEvents(on, config) { setupNodeEvents(on, config) {
// implement node event listeners here // implement node event listeners here
require("@cypress/code-coverage/task")(on, config);
return config;
},
baseUrl: "http://localhost:8888",
retries: {
runMode: 2,
openMode: 0,
}, },
}, },
}); });
+28 -27
View File
@@ -1,40 +1,41 @@
import { MaputnikDriver } from "./maputnik-driver"; import driver from "./driver";
describe("accessibility", () => { describe("accessibility", () => {
let { beforeAndAfter, get, when, then } = new MaputnikDriver(); // skipped due to the following issue with cypress: https://github.com/cypress-io/cypress/issues/299
beforeAndAfter(); describe.skip("skip links", () => {
describe("skip links", () => {
beforeEach(() => { beforeEach(() => {
when.setStyle("layer"); driver.beforeEach();
driver.setStyle("layer");
}); });
it("skip link to layer list", () => { it("skip link to layer list", () => {
const selector = "root:skip:layer-list"; const selector = driver.getDataAttribute("root:skip:layer-list");
then(get.elementByTestId(selector)).shouldExist(); driver.isExists(selector);
when.tab(); driver.typeKeys('{tab}');
then(get.elementByTestId(selector)).shouldBeFocused(); driver.isFocused(selector);
when.click(selector); driver.click(selector);
then(get.skipTargetLayerList()).shouldBeFocused();
driver.isFocused("#skip-target-layer-list");
}); });
// 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 link to layer editor", () => {
it.skip("skip link to layer editor", () => { const selector = driver.getDataAttribute("root:skip:layer-editor");
const selector = "root:skip:layer-editor"; driver.isExists(selector);
then(get.elementByTestId(selector)).shouldExist(); driver.typeKeys('{tab}{tab}');
when.tab().tab(); driver.isFocused(selector);
then(get.elementByTestId(selector)).shouldBeFocused(); driver.click(selector);
when.click(selector);
then(get.skipTargetLayerEditor()).shouldBeFocused(); driver.isFocused("#skip-target-layer-editor");
}); });
it("skip link to map view", () => { it("skip link to map view", () => {
const selector = "root:skip:map-view"; const selector = driver.getDataAttribute("root:skip:map-view");
then(get.elementByTestId(selector)).shouldExist(); driver.isExists(selector);
when.tab().tab().tab(); driver.typeKeys('{tab}{tab}{tab}');
then(get.elementByTestId(selector)).shouldBeFocused(); driver.isFocused(selector);
when.click(selector); driver.click(selector);
then(get.canvas()).shouldBeFocused();
driver.isFocused(".maplibregl-canvas");
}); });
}); });
}); })
+170
View File
@@ -0,0 +1,170 @@
import {v1 as uuid} from "uuid";
export default {
isMac() {
return Cypress.platform === "darwin";
},
beforeEach() {
this.setupInterception();
this.setStyle('both');
},
setupInterception() {
cy.intercept('GET', 'http://localhost:8888/example-style.json', { fixture: 'example-style.json' }).as('example-style.json');
cy.intercept('GET', 'http://localhost:8888/example-layer-style.json', { fixture: 'example-layer-style.json' });
cy.intercept('GET', 'http://localhost:8888/geojson-style.json', { fixture: 'geojson-style.json' });
cy.intercept('GET', 'http://localhost:8888/raster-style.json', { fixture: 'raster-style.json' });
cy.intercept('GET', 'http://localhost:8888/geojson-raster-style.json', { fixture: 'geojson-raster-style.json' });
cy.intercept({method: 'GET', url: '*example.local/*' }, []);
cy.intercept({method: 'GET', url: '*example.com/*' }, []);
},
setStyle(styleProperties: 'geojson' | 'raster' | 'both' | 'layer' | '', zoom? : number) {
let url = "?debug";
switch (styleProperties) {
case "geojson":
url += "&style=http://localhost:8888/geojson-style.json";
break;
case "raster":
url += "&style=http://localhost:8888/raster-style.json";
break;
case "both":
url += "&style=http://localhost:8888/geojson-raster-style.json";
break;
case "layer":
url += "&style=http://localhost:8888/example-layer-style.json";
break;
}
if (zoom) {
url += "#" + zoom + "/41.3805/2.1635";
}
cy.visit("http://localhost:8888/" + url);
if (styleProperties) {
cy.on('window:confirm', () => true)
}
cy.get(".maputnik-toolbar-link").should("be.visible");
},
getDataAttribute(key: string, selector?: string) {
return `*[data-wd-key='${key}'] ${selector || ''}`;
},
closeModal(key: string) {
const selector = this.getDataAttribute(key);
this.isDisplayedInViewport(selector);
this.click(this.getDataAttribute(key + ".close-modal"));
this.doesNotExists(selector);
},
openLayersModal() {
cy.get(this.getDataAttribute('layer-list:add-layer')).click();
cy.get(this.getDataAttribute('modal:add-layer')).should('exist');
cy.get(this.getDataAttribute('modal:add-layer')).should('be.visible');
},
getStyleFromWindow(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;
},
isStyleStoreEqual(getter: (obj:any) => any, styleObj: any) {
cy.window().then((win: any) => {
const obj = this.getStyleFromWindow(win);
assert.deepEqual(getter(obj), styleObj);
});
},
isStyleStoreEqualToExampleFileData() {
cy.window().then((win: any) => {
const obj = this.getStyleFromWindow(win);
cy.fixture('example-style.json').should('deep.equal', obj);
});
},
fillLayersModal(opts: any) {
var type = opts.type;
var layer = opts.layer;
var id;
if(opts.id) {
id = opts.id
}
else {
id = `${type}:${uuid()}`;
}
cy.get(this.getDataAttribute('add-layer.layer-type', "select")).select(type);
cy.get(this.getDataAttribute("add-layer.layer-id", "input")).type(id);
if(layer) {
cy.get(this.getDataAttribute("add-layer.layer-source-block", "input")).type(layer);
}
cy.get(this.getDataAttribute("add-layer")).click();
return id;
},
typeKeys(keys: string) {
cy.get('body').type(keys);
},
click(selector: string) {
cy.get(selector).click();
},
select(selector: string, value: string) {
cy.get(selector).select(value);
},
isSelected(selector: string, value: string) {
cy.get(selector).find(`option[value="${value}"]`).should("be.selected");
},
focus(selector: string) {
cy.get(selector).focus();
},
isFocused(selector: string) {
cy.get(selector).should('have.focus');
},
isDisplayedInViewport(selector: string) {
cy.get(selector).should('be.visible');
},
isNotDisplayedInViewport(selector: string) {
cy.get(selector).should('not.be.visible');
},
setValue(selector: string, text: string) {
cy.get(selector).clear().type(text, {parseSpecialCharSequences: false});
},
isExists(selector: string) {
cy.get(selector).should('exist');
},
doesNotExists(selector: string) {
cy.get(selector).should('not.exist');
},
chooseExampleFile() {
cy.get("input[type='file']").selectFile('cypress/fixtures/example-style.json', {force: true});
},
getExampleFileUrl() {
return "http://localhost:8888/example-style.json";
},
waitForExampleFileRequset() {
cy.wait('@example-style.json');
}
}
+61 -106
View File
@@ -1,125 +1,80 @@
import { MaputnikDriver } from "./maputnik-driver"; import driver from "./driver";
describe("history", () => { describe("history", () => {
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
beforeAndAfter();
let undoKeyCombo: string; let undoKeyCombo: string;
let redoKeyCombo: string; let redoKeyCombo: string;
before(() => { before(() => {
const isMac = get.isMac(); const isMac = driver.isMac();
undoKeyCombo = isMac ? "{meta}z" : "{ctrl}z"; undoKeyCombo = isMac ? '{meta}z' : '{ctrl}z';
redoKeyCombo = isMac ? "{meta}{shift}z" : "{ctrl}y"; redoKeyCombo = isMac ? '{meta}{shift}z' : '{ctrl}y';
driver.beforeEach();
}); });
it("undo/redo", () => { it("undo/redo", () => {
when.setStyle("geojson"); driver.setStyle('geojson');
when.modal.open(); driver.openLayersModal();
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
when.modal.fillLayers({ driver.isStyleStoreEqual((a: any) => a.layers, []);
driver.fillLayersModal({
id: "step 1", id: "step 1",
type: "background", type: "background"
}); })
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 1",
type: "background",
},
],
});
when.modal.open(); driver.isStyleStoreEqual((a: any) => a.layers, [
when.modal.fillLayers({ {
"id": "step 1",
"type": 'background'
}
]);
driver.openLayersModal();
driver.fillLayersModal({
id: "step 2", id: "step 2",
type: "background", type: "background"
}); })
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ driver.isStyleStoreEqual((a: any) => a.layers, [
layers: [ {
{ "id": "step 1",
id: "step 1", "type": 'background'
type: "background", },
}, {
{ "id": "step 2",
id: "step 2", "type": 'background'
type: "background", }
}, ]);
],
});
when.typeKeys(undoKeyCombo); driver.typeKeys(undoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ driver.isStyleStoreEqual((a: any) => a.layers, [
layers: [ {
{ "id": "step 1",
id: "step 1", "type": 'background'
type: "background", }
}, ]);
],
});
when.typeKeys(undoKeyCombo); driver.typeKeys(undoKeyCombo)
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] }); driver.isStyleStoreEqual((a: any) => a.layers, []);
when.typeKeys(redoKeyCombo); driver.typeKeys(redoKeyCombo)
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ driver.isStyleStoreEqual((a: any) => a.layers, [
layers: [ {
{ "id": "step 1",
id: "step 1", "type": 'background'
type: "background", }
}, ]);
],
});
when.typeKeys(redoKeyCombo); driver.typeKeys(redoKeyCombo)
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ driver.isStyleStoreEqual((a: any) => a.layers, [
layers: [ {
{ "id": "step 1",
id: "step 1", "type": 'background'
type: "background", },
}, {
{ "id": "step 2",
id: "step 2", "type": 'background'
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",
},
],
});
});
});
-35
View File
@@ -1,35 +0,0 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("i18n", () => {
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
describe("language detector", () => {
it("English", () => {
const url = "?lng=en";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
});
it("Japanese", () => {
const url = "?lng=ja";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
});
});
describe("language switcher", () => {
beforeEach(() => {
when.setStyle("layer");
});
it("the language switcher switches to Japanese", () => {
const selector = "maputnik-lang-select";
then(get.elementByTestId(selector)).shouldExist();
when.select(selector, "ja");
then(get.elementByTestId(selector)).shouldHaveValue("ja");
then(get.elementByTestId("nav:settings")).shouldHaveText("スタイル設定");
});
});
});
+27 -27
View File
@@ -1,60 +1,60 @@
import { MaputnikDriver } from "./maputnik-driver"; import driver from "./driver";
describe("keyboard", () => { describe("keyboard", () => {
let { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
beforeAndAfter();
describe("shortcuts", () => { describe("shortcuts", () => {
beforeEach(() => { beforeEach(() => {
given.setupMockBackedResponses(); driver.setupInterception();
when.setStyle(""); driver.setStyle('');
}); })
it("ESC should unfocus", () => { it("ESC should unfocus", () => {
const targetSelector = "maputnik-select"; const targetSelector = driver.getDataAttribute("nav:inspect") + " select";
when.focus(targetSelector); driver.focus(targetSelector);
then(get.elementByTestId(targetSelector)).shouldBeFocused(); driver.isFocused(targetSelector);
when.typeKeys("{esc}");
then(get.elementByTestId(targetSelector)).shouldNotBeFocused(); //driver.typeKeys("{esc}");
//driver.isFocused('body');
}); });
it("'?' should show shortcuts modal", () => { it("'?' should show shortcuts modal", () => {
when.typeKeys("?"); driver.typeKeys("?");
then(get.elementByTestId("modal:shortcuts")).shouldBeVisible(); driver.isDisplayedInViewport(driver.getDataAttribute("modal:shortcuts"));
}); });
it("'o' should show open modal", () => { it("'o' should show open modal", () => {
when.typeKeys("o"); driver.typeKeys("o");
then(get.elementByTestId("modal:open")).shouldBeVisible(); driver.isDisplayedInViewport(driver.getDataAttribute("modal:open"));
}); });
it("'e' should show export modal", () => { it("'e' should show export modal", () => {
when.typeKeys("e"); driver.typeKeys("e");
then(get.elementByTestId("modal:export")).shouldBeVisible(); driver.isDisplayedInViewport(driver.getDataAttribute("modal:export"));
}); });
it("'d' should show sources modal", () => { it("'d' should show sources modal", () => {
when.typeKeys("d"); driver.typeKeys("d");
then(get.elementByTestId("modal:sources")).shouldBeVisible(); driver.isDisplayedInViewport(driver.getDataAttribute("modal:sources"));
}); });
it("'s' should show settings modal", () => { it("'s' should show settings modal", () => {
when.typeKeys("s"); driver.typeKeys("s");
then(get.elementByTestId("modal:settings")).shouldBeVisible(); driver.isDisplayedInViewport(driver.getDataAttribute("modal:settings"));
}); });
it("'i' should change map to inspect mode", () => { it("'i' should change map to inspect mode", () => {
when.typeKeys("i"); driver.typeKeys("i");
then(get.inputValue("maputnik-select")).shouldEqual("inspect"); driver.isSelected(driver.getDataAttribute("nav:inspect"), "inspect");
}); });
it("'m' should focus map", () => { it("'m' should focus map", () => {
when.typeKeys("m"); driver.typeKeys("m");
then(get.canvas()).shouldBeFocused(); driver.isFocused(".maplibregl-canvas");
}); });
it("'!' should show debug modal", () => { it("'!' should show debug modal", () => {
when.typeKeys("!"); driver.typeKeys("!");
then(get.elementByTestId("modal:debug")).shouldBeVisible(); driver.isDisplayedInViewport(driver.getDataAttribute("modal:debug"));
}); });
}); });
}); });
+290 -360
View File
@@ -1,137 +1,130 @@
import { v1 as uuid } from "uuid"; var assert = require("assert");
import { MaputnikDriver } from "./maputnik-driver"; import driver from "./driver";
import { v1 as uuid } from 'uuid';
describe("layers", () => { describe("layers", () => {
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
beforeEach(() => { beforeEach(() => {
when.setStyle("both"); driver.beforeEach();
when.modal.open(); driver.setStyle('both');
driver.openLayersModal();
}); });
describe("ops", () => { describe("ops", () => {
let id: string; it("delete", () => {
beforeEach(() => { var id = driver.fillLayersModal({
id = when.modal.fillLayers({ type: "background"
type: "background", })
});
driver.isStyleStoreEqual((a: any) => a.layers, [
{
"id": id,
"type": 'background'
},
]);
driver.click(driver.getDataAttribute("layer-list-item:"+id+":delete", ""))
driver.isStyleStoreEqual((a: any) => a.layers, []);
}); });
it("should update layers in local storage", () => { it("duplicate", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ var styleObj;
layers: [ var id = driver.fillLayersModal({
{ type: "background"
id: id, })
type: "background",
}, driver.isStyleStoreEqual((a: any) => a.layers, [
], {
}); "id": id,
"type": 'background'
},
]);
driver.click(driver.getDataAttribute("layer-list-item:"+id+":copy", ""));
driver.isStyleStoreEqual((a: any) => a.layers, [
{
"id": id+"-copy",
"type": "background"
},
{
"id": id,
"type": "background"
},
]);
}); });
describe("when clicking delete", () => { it("hide", () => {
beforeEach(() => { var styleObj;
when.click("layer-list-item:" + id + ":delete"); var id = driver.fillLayersModal({
}); type: "background"
it("should empty layers in local storage", () => { })
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [],
});
});
});
describe("when clicking duplicate", () => { driver.isStyleStoreEqual((a: any) => a.layers, [
beforeEach(() => { {
when.click("layer-list-item:" + id + ":copy"); "id": id,
}); "type": 'background'
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", () => { driver.click(driver.getDataAttribute("layer-list-item:"+id+":toggle-visibility", ""));
beforeEach(() => {
when.click("layer-list-item:" + id + ":toggle-visibility");
});
it("should update visibility to none in local storage", () => { driver.isStyleStoreEqual((a: any) => a.layers, [
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ {
layers: [ "id": id,
{ "type": "background",
id: id, "layout": {
type: "background", "visibility": "none"
layout: { }
visibility: "none", },
}, ]);
},
],
});
});
describe("when clicking show", () => { driver.click(driver.getDataAttribute("layer-list-item:"+id+":toggle-visibility", ""));
beforeEach(() => {
when.click("layer-list-item:" + id + ":toggle-visibility"); driver.isStyleStoreEqual((a: any) => a.layers, [
}); {
"id": id,
"type": "background",
"layout": {
"visibility": "visible"
}
},
]);
})
})
describe('background', () => {
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", () => { it("add", () => {
let id = when.modal.fillLayers({ var id = driver.fillLayersModal({
type: "background", type: "background"
}); })
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [ driver.isStyleStoreEqual((a: any) => a.layers, [
{ {
id: id, "id": id,
type: "background", "type": 'background'
}, }
], ]);
});
}); });
describe("modify", () => { describe("modify", () => {
function createBackground() { function createBackground() {
// Setup // Setup
let id = uuid(); var id = uuid();
when.selectWithin("add-layer.layer-type", "background"); driver.select(driver.getDataAttribute("add-layer.layer-type", "select"), "background");
when.setValue("add-layer.layer-id.input", "background:" + id); driver.setValue(driver.getDataAttribute("add-layer.layer-id", "input"), "background:"+id);
when.click("add-layer"); driver.click(driver.getDataAttribute("add-layer"));
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ driver.isStyleStoreEqual((a: any) => a.layers, [
layers: [ {
{ "id": 'background:'+id,
id: "background:" + id, "type": 'background'
type: "background", }
}, ]);
],
});
return id; return id;
} }
@@ -139,179 +132,127 @@ describe("layers", () => {
describe("layer", () => { describe("layer", () => {
it("expand/collapse"); it("expand/collapse");
it("id", () => { it("id", () => {
let bgId = createBackground(); var bgId = createBackground();
when.click("layer-list-item:background:" + bgId); driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId));
let id = uuid(); var id = uuid();
when.setValue("layer-editor.layer-id.input", "foobar:" + id); driver.setValue(driver.getDataAttribute("layer-editor.layer-id", "input"), "foobar:"+id)
when.click("min-zoom"); driver.click(driver.getDataAttribute("min-zoom"));
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ driver.isStyleStoreEqual((a: any) => a.layers, [
layers: [ {
{ "id": 'foobar:'+id,
id: "foobar:" + id, "type": 'background'
type: "background", }
}, ]);
],
});
}); });
describe("min-zoom", () => { it("min-zoom", () => {
let bgId: string; var bgId = createBackground();
beforeEach(() => { driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId));
bgId = createBackground(); driver.setValue(driver.getDataAttribute("min-zoom", 'input[type="text"]'), "1");
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", () => { driver.click(driver.getDataAttribute("layer-editor.layer-id", "input"));
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
minzoom: 1,
},
],
});
});
it("when clicking next layer should update style on local storage", () => { driver.isStyleStoreEqual((a: any) => a.layers, [
when.type("min-zoom.input-text", "{backspace}"); {
when.click("max-zoom.input-text"); "id": 'background:'+bgId,
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ "type": 'background',
layers: [ "minzoom": 1
{ }
id: "background:" + bgId, ]);
type: "background",
minzoom: 1, // AND RESET!
}, // driver.setValue(driver.getDataAttribute("min-zoom", "input"), "")
], // driver.click(driver.getDataAttribute("max-zoom", "input"));
});
}); // driver.isStyleStoreEqual((a: any) => a.layers, [
// {
// "id": 'background:'+bgId,
// "type": 'background'
// }
// ]);
}); });
describe("max-zoom", () => { it("max-zoom", () => {
let bgId: string; var bgId = createBackground();
beforeEach(() => { driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId));
bgId = createBackground(); driver.setValue(driver.getDataAttribute("max-zoom", 'input[type="text"]'), "1")
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", () => { driver.click(driver.getDataAttribute("layer-editor.layer-id", "input"));
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [ driver.isStyleStoreEqual((a: any) => a.layers, [
{ {
id: "background:" + bgId, "id": 'background:'+bgId,
type: "background", "type": 'background',
maxzoom: 1, "maxzoom": 1
}, }
], ]);
});
});
}); });
describe("comments", () => { it("comments", () => {
let bgId: string; var bgId = createBackground();
let comment = "42"; var id = uuid();
beforeEach(() => { driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId));
bgId = createBackground(); driver.setValue(driver.getDataAttribute("layer-comment", "textarea"), id);
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", () => { driver.click(driver.getDataAttribute("layer-editor.layer-id", "input"));
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
metadata: {
"maputnik:comment": comment,
},
},
],
});
});
describe("when unsetting", () => { driver.isStyleStoreEqual((a: any) => a.layers, [
beforeEach(() => { {
when.clear("layer-comment.input"); "id": 'background:'+bgId,
when.click("min-zoom.input-text"); "type": 'background',
}); metadata: {
'maputnik:comment': id
}
}
]);
it("should update style in local storage", () => { // Unset it again.
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ // TODO: This fails
layers: [ // driver.setValue(driver.getDataAttribute("layer-comment", "textarea"), "");
{ // driver.click(driver.getDataAttribute("min-zoom", "input"));
id: "background:" + bgId,
type: "background", // driver.isStyleStoreEqual((a: any) => a.layers, [
}, // {
], // "id": 'background:'+bgId,
}); // "type": 'background'
}); // }
}); // ]);
}); });
describe("color", () => { it("color", () => {
let bgId: string; var bgId = createBackground();
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.click("spec-field:background-color");
});
it("should update style in local storage", () => { driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId));
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
},
],
});
});
});
describe("opacity", () => { driver.click(driver.getDataAttribute("spec-field:background-color", "input"));
let bgId: string;
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.type("spec-field-input:background-opacity", "0.");
});
it("should keep '.' in the input field", () => { driver.isStyleStoreEqual((a: any) => a.layers, [
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0."); {
}); "id": 'background:'+bgId,
"type": 'background'
}
]);
it("should revert to a valid value when focus out", () => { })
when.click("layer-list-item:background:" + bgId); })
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue('0');
});
});
});
describe("filter", () => { describe("filter", () => {
it("expand/collapse"); it("expand/collapse");
it("compound filter"); it("compound filter");
}); })
describe("paint", () => { describe("paint", () => {
it("expand/collapse"); it("expand/collapse");
it("color"); it("color");
it("pattern"); it("pattern");
it("opacity"); it("opacity");
}); })
// <===== // <=====
describe("json-editor", () => { describe("json-editor", () => {
@@ -320,178 +261,167 @@ describe("layers", () => {
// TODO // TODO
it.skip("parse error", () => { it.skip("parse error", () => {
let bgId = createBackground(); var bgId = createBackground();
when.click("layer-list-item:background:" + bgId); driver.click(driver.getDataAttribute("layer-list-item:background:"+bgId));
let errorSelector = ".CodeMirror-lint-marker-error"; var errorSelector = ".CodeMirror-lint-marker-error";
then(get.elementByTestId(errorSelector)).shouldNotExist(); driver.doesNotExists(errorSelector);
when.click(".CodeMirror"); driver.click(".CodeMirror");
when.typeKeys( driver.typeKeys("\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {");
"\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {" driver.isExists(errorSelector);
);
then(get.elementByTestId(errorSelector)).shouldExist(); driver.click(driver.getDataAttribute("layer-editor.layer-id"));
}); });
}); });
}); })
}); });
describe("fill", () => { describe('fill', () => {
it("add", () => { it("add", () => {
let id = when.modal.fillLayers({
var id = driver.fillLayersModal({
type: "fill", type: "fill",
layer: "example", layer: "example"
}); });
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ driver.isStyleStoreEqual((a: any) => a.layers, [
layers: [ {
{ "id": id,
id: id, "type": 'fill',
type: "fill", "source": "example"
source: "example", }
}, ]);
], })
});
});
// TODO: Change source // TODO: Change source
it("change source"); it("change source")
}); });
describe("line", () => { describe('line', () => {
it("add", () => { it("add", () => {
let id = when.modal.fillLayers({ var id = driver.fillLayersModal({
type: "line", type: "line",
layer: "example", layer: "example"
}); });
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ driver.isStyleStoreEqual((a: any) => a.layers, [
layers: [ {
{ "id": id,
id: id, "type": "line",
type: "line", "source": "example",
source: "example", }
}, ]);
],
});
}); });
it("groups", () => { it("groups", () => {
// TODO // TODO
// Click each of the layer groups. // Click each of the layer groups.
}); })
}); });
describe("symbol", () => { describe('symbol', () => {
it("add", () => { it("add", () => {
let id = when.modal.fillLayers({ var id = driver.fillLayersModal({
type: "symbol", type: "symbol",
layer: "example", layer: "example"
}); });
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ driver.isStyleStoreEqual((a: any) => a.layers, [
layers: [ {
{ "id": id,
id: id, "type": "symbol",
type: "symbol", "source": "example",
source: "example", }
}, ]);
],
});
}); });
}); });
describe("raster", () => { describe('raster', () => {
it("add", () => { it("add", () => {
let id = when.modal.fillLayers({ var id = driver.fillLayersModal({
type: "raster", type: "raster",
layer: "raster", layer: "raster"
}); });
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ driver.isStyleStoreEqual((a: any) => a.layers, [
layers: [ {
{ "id": id,
id: id, "type": "raster",
type: "raster", "source": "raster",
source: "raster", }
}, ]);
],
});
}); });
}); });
describe("circle", () => { describe('circle', () => {
it("add", () => { it("add", () => {
let id = when.modal.fillLayers({ var id = driver.fillLayersModal({
type: "circle", type: "circle",
layer: "example", layer: "example"
}); });
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ driver.isStyleStoreEqual((a: any) => a.layers, [
layers: [ {
{ "id": id,
id: id, "type": "circle",
type: "circle", "source": "example",
source: "example", }
}, ]);
],
});
}); });
}); });
describe("fill extrusion", () => { describe('fill extrusion', () => {
it("add", () => { it("add", () => {
let id = when.modal.fillLayers({ var id = driver.fillLayersModal({
type: "fill-extrusion", type: "fill-extrusion",
layer: "example", layer: "example"
}); });
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ driver.isStyleStoreEqual((a: any) => a.layers, [
layers: [ {
{ "id": id,
id: id, "type": 'fill-extrusion',
type: "fill-extrusion", "source": "example"
source: "example", }
}, ]);
],
});
}); });
}); });
describe("groups", () => { describe("groups", () => {
it("simple", () => { it("simple", () => {
when.setStyle("geojson"); driver.setStyle("geojson");
when.modal.open(); driver.openLayersModal();
when.modal.fillLayers({ driver.fillLayersModal({
id: "foo", id: "foo",
type: "background", type: "background"
}); })
when.modal.open(); driver.openLayersModal();
when.modal.fillLayers({ driver.fillLayersModal({
id: "foo_bar", id: "foo_bar",
type: "background", type: "background"
}); })
when.modal.open(); driver.openLayersModal();
when.modal.fillLayers({ driver.fillLayersModal({
id: "foo_bar_baz", id: "foo_bar_baz",
type: "background", type: "background"
}); })
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible(); driver.isDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo"));
then(get.elementByTestId("layer-list-item:foo_bar")).shouldNotBeVisible(); driver.isNotDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo_bar"));
then( driver.isNotDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo_bar_baz"));
get.elementByTestId("layer-list-item:foo_bar_baz")
).shouldNotBeVisible(); driver.click(driver.getDataAttribute("layer-list-group:foo-0"));
when.click("layer-list-group:foo-0");
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible(); driver.isDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo"));
then(get.elementByTestId("layer-list-item:foo_bar")).shouldBeVisible(); driver.isDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo_bar"));
then( driver.isDisplayedInViewport(driver.getDataAttribute("layer-list-item:foo_bar_baz"));
get.elementByTestId("layer-list-item:foo_bar_baz") })
).shouldBeVisible(); })
});
});
}); });
+22 -29
View File
@@ -1,32 +1,25 @@
import { MaputnikDriver } from "./maputnik-driver"; import driver from "./driver";
describe("map", () => { describe("map", () => {
let { beforeAndAfter, get, when, then } = new MaputnikDriver(); describe("zoom level", () => {
beforeAndAfter(); beforeEach(() => {
describe("zoom level", () => { driver.beforeEach();
it("via url", () => { });
let zoomLevel = 12.37; it("via url", () => {
when.setStyle("geojson", zoomLevel); var zoomLevel = 12.37;
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible(); driver.setStyle("geojson", zoomLevel);
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText( driver.isDisplayedInViewport(".maplibregl-ctrl-zoom");
"Zoom: " + zoomLevel // HM TODO
); //driver.getText(".maplibregl-ctrl-zoom") === "Zoom "+(zoomLevel);
}); })
it("via map controls", () => {
var zoomLevel = 12.37;
driver.setStyle("geojson", zoomLevel);
it("via map controls", () => { driver.click(".maplibregl-ctrl-zoom-in");
let zoomLevel = 12.37; driver.isDisplayedInViewport(".maplibregl-ctrl-zoom");
when.setStyle("geojson", zoomLevel); // HM TODO
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible(); //driver.getText(".maplibregl-ctrl-zoom") === "Zoom "+(zoomLevel + 1);
when.clickZoomIn(); })
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText( })
"Zoom: " + (zoomLevel + 1) })
);
});
});
describe("search", () => {
it('should exist', () => {
then(get.searchControl()).shouldBeVisible();
});
});
});
-19
View File
@@ -1,19 +0,0 @@
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;
}
-186
View File
@@ -1,186 +0,0 @@
/// <reference types="cypress-plugin-tab" />
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: Window) => {
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
const toolbarLink = this.helper.get.elementByTestId("toolbar:link")
toolbarLink.scrollIntoView();
toolbarLink.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')
};
}
-40
View File
@@ -1,40 +0,0 @@
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");
},
};
}
+83 -126
View File
@@ -1,180 +1,137 @@
import { MaputnikDriver } from "./maputnik-driver"; import driver from "./driver";
describe("modals", () => { describe("modals", () => {
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
beforeAndAfter();
beforeEach(() => { beforeEach(() => {
when.setStyle(""); driver.beforeEach();
driver.setStyle('');
}); });
describe("open", () => { describe("open", () => {
beforeEach(() => { beforeEach(() => {
when.click("nav:open"); driver.click(driver.getDataAttribute("nav:open"));
}); });
it("close", () => { it("close", () => {
when.modal.close("modal:open"); driver.closeModal("modal:open");
then(get.elementByTestId("modal:open")).shouldNotExist();
}); });
it.skip("upload", () => { it.skip("upload", () => {
// HM: I was not able to make the following choose file actually to select a file and close the modal... // HM: I was not able to make the following choose file actually to select a file and close the modal...
when.chooseExampleFile(); driver.chooseExampleFile();
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
driver.isStyleStoreEqualToExampleFileData();
}); });
describe("when click open url", () => { it("load from url", () => {
beforeEach(() => { var styleFileUrl = driver.getExampleFileUrl();
let styleFileUrl = get.exampleFileUrl();
when.setValue("modal:open.url.input", styleFileUrl); driver.setValue(driver.getDataAttribute("modal:open.url.input"), styleFileUrl);
when.click("modal:open.url.button"); driver.click(driver.getDataAttribute("modal:open.url.button"))
when.wait(200); driver.waitForExampleFileRequset();
});
it("load from url", () => { driver.isStyleStoreEqualToExampleFileData();
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
});
}); });
}); })
describe("shortcuts", () => { describe("shortcuts", () => {
it("open/close", () => { it("open/close", () => {
when.setStyle(""); driver.setStyle('');
when.typeKeys("?");
when.modal.close("modal:shortcuts"); driver.typeKeys("?");
then(get.elementByTestId("modal:shortcuts")).shouldNotExist();
driver.isDisplayedInViewport(driver.getDataAttribute("modal:shortcuts"));
driver.closeModal("modal:shortcuts");
}); });
}); });
describe("export", () => { describe("export", () => {
beforeEach(() => { beforeEach(() => {
when.click("nav:export"); driver.click(driver.getDataAttribute("nav:export"));
}); });
it("close", () => { it("close", () => {
when.modal.close("modal:export"); driver.closeModal("modal:export");
then(get.elementByTestId("modal:export")).shouldNotExist();
}); });
// TODO: Work out how to download a file and check the contents // TODO: Work out how to download a file and check the contents
it("download"); it("download")
});
})
describe("sources", () => { describe("sources", () => {
it("active sources"); it("active sources")
it("public source"); it("public source")
it("add new source"); it("add new source")
}); })
describe("inspect", () => { describe("inspect", () => {
it("toggle", () => { it("toggle", () => {
// There is no assertion in this test driver.setStyle('geojson');
when.setStyle("geojson");
when.select("maputnik-select", "inspect"); driver.select(driver.getDataAttribute("nav:inspect", "select"), "inspect");
}); })
}); })
describe("style settings", () => { describe("style settings", () => {
beforeEach(() => { beforeEach(() => {
when.click("nav:settings"); driver.click(driver.getDataAttribute("nav:settings"));
}); });
describe("when click name filed spec information", () => { it("name", () => {
beforeEach(() => { driver.setValue(driver.getDataAttribute("modal:settings.name"), "foobar");
when.click("field-doc-button-Name"); driver.click(driver.getDataAttribute("modal:settings.owner"));
});
it("should show the spec information", () => { driver.isStyleStoreEqual((obj) => obj.name, "foobar");
then(get.elementsText("spec-field-doc")).shouldInclude( })
"name for the style" it("owner", () => {
); driver.setValue(driver.getDataAttribute("modal:settings.owner"), "foobar")
}); driver.click(driver.getDataAttribute("modal:settings.name"));
});
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",
});
});
});
driver.isStyleStoreEqual((obj) => obj.owner, "foobar");
})
it("sprite url", () => { it("sprite url", () => {
when.setValue("modal:settings.sprite", "http://example.com"); driver.setValue(driver.getDataAttribute("modal:settings.sprite"), "http://example.com")
when.click("modal:settings.name"); driver.click(driver.getDataAttribute("modal:settings.name"));
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sprite: "http://example.com", driver.isStyleStoreEqual((obj) => obj.sprite, "http://example.com");
}); })
});
it("glyphs url", () => { it("glyphs url", () => {
let glyphsUrl = "http://example.com/{fontstack}/{range}.pbf"; var glyphsUrl = "http://example.com/{fontstack}/{range}.pbf"
when.setValue("modal:settings.glyphs", glyphsUrl); driver.setValue(driver.getDataAttribute("modal:settings.glyphs"), glyphsUrl);
when.click("modal:settings.name"); driver.click(driver.getDataAttribute("modal:settings.name"));
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
glyphs: glyphsUrl, driver.isStyleStoreEqual((obj) => obj.glyphs, glyphsUrl);
}); })
});
it("maptiler access token", () => { it("maptiler access token", () => {
let apiKey = "testing123"; var apiKey = "testing123";
when.setValue( driver.setValue(driver.getDataAttribute("modal:settings.maputnik:openmaptiles_access_token"), apiKey);
"modal:settings.maputnik:openmaptiles_access_token", driver.click(driver.getDataAttribute("modal:settings.name"));
apiKey
); driver.isStyleStoreEqual((obj) => obj.metadata["maputnik:openmaptiles_access_token"], apiKey);
when.click("modal:settings.name"); })
then(
get.styleFromLocalStorage().then((style) => style.metadata)
).shouldInclude({
"maputnik:openmaptiles_access_token": apiKey,
});
});
it("thunderforest access token", () => { it("thunderforest access token", () => {
let apiKey = "testing123"; var apiKey = "testing123";
when.setValue( driver.setValue(driver.getDataAttribute("modal:settings.maputnik:thunderforest_access_token"), apiKey);
"modal:settings.maputnik:thunderforest_access_token", driver.click(driver.getDataAttribute("modal:settings.name"));
apiKey
); driver.isStyleStoreEqual((obj) => obj.metadata["maputnik:thunderforest_access_token"], apiKey);
when.click("modal:settings.name"); })
then(
get.styleFromLocalStorage().then((style) => style.metadata)
).shouldInclude({ "maputnik:thunderforest_access_token": apiKey });
});
it("style renderer", () => { it("style renderer", () => {
cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers cy.on('uncaught:exception', () => false); // this is due to the fact that this is an invalid style for openlayers
when.select("modal:settings.maputnik:renderer", "ol"); driver.select(driver.getDataAttribute("modal:settings.maputnik:renderer"), "ol");
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual( driver.isSelected(driver.getDataAttribute("modal:settings.maputnik:renderer"), "ol");
"ol"
);
when.click("modal:settings.name"); driver.click(driver.getDataAttribute("modal:settings.name"));
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
metadata: { "maputnik:renderer": "ol" }, driver.isStyleStoreEqual((obj) => obj.metadata["maputnik:renderer"], "ol");
}); })
}); })
});
describe("sources", () => { describe("sources", () => {
it("toggle"); it("toggle")
}); })
}); })
+1 -3
View File
@@ -14,9 +14,7 @@
// *********************************************************** // ***********************************************************
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import "@cypress/code-coverage/support"; import './commands'
import "cypress-plugin-tab";
import "./commands";
// Alternatively you can use CommonJS syntax: // Alternatively you can use CommonJS syntax:
// require('./commands') // require('./commands')
-31
View File
@@ -1,31 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
editor
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
# Binary version of pubilic/editor
rice-box.go
# Built binary
maputnik
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2016 Maputnik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-39
View File
@@ -1,39 +0,0 @@
SOURCEDIR=.
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
BINARY=maputnik
DESKTOP_VERSION := 1.1.1
EDITOR_VERSION := $(shell node -p "require('../package.json').version")
GOPATH := $(if $(GOPATH),$(GOPATH),$(HOME)/go)
GOBIN := $(if $(GOBIN),$(GOBIN),$(HOME)/go/bin)
all: $(BINARY)
$(BINARY): $(GOBIN)/gox $(SOURCES) version.go rice-box.go
$(GOBIN)/gox -osarch "windows/amd64 linux/amd64 darwin/amd64" -output "bin/{{.OS}}/${BINARY}"
# Copy the current release into ./editor/maputnik so it can be
# embedded in the binary
editor/pull_release:
mkdir -p editor
cp -r ../dist/* editor
$(GOBIN)/gox:
go install github.com/mitchellh/gox@v1.0.1
$(GOBIN)/rice:
go install github.com/GeertJohan/go.rice/rice@v1.0.3
# Embed the current version numbers in the executable by writing version.go
.PHONY: version.go
version.go:
@echo "// DO NOT EDIT: Autogenerated by Makefile\n" > version.go
@echo "package main\n" >> version.go
@echo "const DesktopVersion = \"$(DESKTOP_VERSION)\"" >> version.go
@echo "const EditorVersion = \"$(EDITOR_VERSION)\"" >> version.go
rice-box.go: $(GOBIN)/rice editor/pull_release
$(GOBIN)/rice embed-go
.PHONY: clean
clean:
rm -rf editor && rm -f rice-box.go && rm -rf bin
-72
View File
@@ -1,72 +0,0 @@
# Maputnik Desktop [![GitHub CI status](https://github.com/maplibre/maputnik/workflows/ci/badge.svg)][github-action-ci]
---
A Golang based cross platform executable for integrating Maputnik locally.
This binary packages up the JavaScript and CSS bundle produced by maputnik
and embeds it in the program for easy distribution. It also allows
exposing a local style file and work on it both in Maputnik and with your favorite
editor.
Report issues on [maplibre/maputnik](https://github.com/maplibre/maputnik).
## Install
You can download a single binary for Linux, OSX or Windows from [the latest releases of **maplibre/maputnik**](https://github.com/maplibre/maputnik/editor/releases/latest).
### Usage
Simply start up a web server and access the Maputnik editor GUI at `localhost:8000`.
```bash
maputnik
```
Expose a local style file to Maputnik allowing the web based editor
to save to the local filesystem.
```bash
maputnik --file basic-v9.json
```
Watch the local style for changes and inform the editor via web socket.
This makes it possible to edit the style with a local text editor and still
use Maputnik.
```bash
maputnik --watch --file basic-v9.json
```
Choose a local port to listen on, instead of using the default port 8000.
```bash
maputnik --port 8001
```
Specify a path to a directory which, if it exists, will be served under http://localhost:8000/static/ .
Could be used to serve sprites and glyphs.
```bash
maputnik --static ./localFolder
```
### API
`maputnik` exposes the configured styles via a HTTP API.
| Method | Description
|---------------------------------|---------------------------------------
| `GET /styles` | List the ID of all configured style files
| `GET /styles/{filename}` | Get contents of a single style file
| `PUT /styles/{filename}` | Update contents of a style file
| `WEBSOCKET /ws` | Listen to change events for the configured style files
### Build
From the root of the [maplibre/maputnik](https://github.com/maplibre/maputnik) project, install the deps and run the desktop-build command.
```
npm install
npm run build-desktop
```
You should now find the `maputnik` binary in your `bin` directory.
-81
View File
@@ -1,81 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/gorilla/mux"
)
func StyleFileAccessor(filename string) styleFileAccessor {
return styleFileAccessor{filename, styleId(filename)}
}
func styleId(filename string) string {
raw, err := ioutil.ReadFile(filename)
if err != nil {
log.Panicln(err)
}
var spec styleSpec
err = json.Unmarshal(raw, &spec)
if err != nil {
log.Panicln(err)
}
if spec.Id == "" {
fmt.Println("No id in style")
}
return spec.Id
}
type styleSpec struct {
Id string `json:"id"`
}
// Allows access to a single style file
type styleFileAccessor struct {
filename string
id string
}
func (fa styleFileAccessor) ListFiles(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.Encode([]string{fa.id})
}
func (fa styleFileAccessor) ReadFile(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
_ = vars["styleId"]
//TODO: Choose right file
// right now we just return the single file we know of
w.Header().Set("Content-Type", "application/json")
raw, err := ioutil.ReadFile(fa.filename)
if err != nil {
log.Panicln(err)
}
w.Write(raw)
}
func (fa styleFileAccessor) SaveFile(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
_ = vars["styleId"]
//TODO: Save to right file
w.Header().Set("Content-Type", "application/json")
body, _ := ioutil.ReadAll(r.Body)
var out bytes.Buffer
json.Indent(&out, body, "", " ")
if err := ioutil.WriteFile(fa.filename, out.Bytes(), 0666); err != nil {
log.Fatalf("Can not copy from request to file: %s", err.Error())
}
}
-69
View File
@@ -1,69 +0,0 @@
package filewatch
import (
"io/ioutil"
"log"
"net/http"
"github.com/fsnotify/fsnotify"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
func writer(ws *websocket.Conn, filename string) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event := <-watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("Modified file:", event.Name)
var p []byte
var err error
p, err = ioutil.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
if p != nil {
if err := ws.WriteMessage(websocket.TextMessage, p); err != nil {
return
}
}
}
case err := <-watcher.Errors:
log.Println("Watch error:", err)
}
}
}()
if err = watcher.Add(filename); err != nil {
log.Fatal(err)
}
<-done
}
func ServeWebsocketFileWatcher(filename string, w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
if _, ok := err.(websocket.HandshakeError); !ok {
log.Println(err)
}
return
}
writer(ws, filename)
defer ws.Close()
}
-27
View File
@@ -1,27 +0,0 @@
module maputnik/desktop
go 1.19
require (
github.com/GeertJohan/go.rice v1.0.3
github.com/fsnotify/fsnotify v1.6.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0
github.com/maputnik/desktop v1.0.7
github.com/urfave/cli v1.22.12
)
require (
github.com/GeertJohan/go.incremental v1.0.0 // indirect
github.com/akavel/rsrc v0.8.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/daaku/go.zipexe v1.0.2 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/jessevdk/go-flags v1.4.0 // indirect
github.com/nkovacs/streamquote v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.0.1 // indirect
golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect
)
-54
View File
@@ -1,54 +0,0 @@
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI=
github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4=
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/daaku/go.zipexe v1.0.2 h1:Zg55YLYTr7M9wjKn8SY/WcpuuEi+kR2u4E8RhvpyXmk=
github.com/daaku/go.zipexe v1.0.2/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/maputnik/desktop v1.0.7 h1:rdFg7emIJOT3YsZpwqSChmWtMOvu+T4h6WwVQAZP9n4=
github.com/maputnik/desktop v1.0.7/go.mod h1:wmDjHUztx9jOBz0I22589yWguAGdV/sEM57YANpN8oQ=
github.com/nkovacs/streamquote v1.0.0 h1:PmVIV08Zlx2lZK5fFZlMZ04eHcDTIFJCv/5/0twVUow=
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-80
View File
@@ -1,80 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/GeertJohan/go.rice"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/maputnik/desktop/filewatch"
"github.com/urfave/cli"
)
func main() {
app := cli.NewApp()
app.Name = "maputnik"
app.Usage = "Server for integrating Maputnik locally"
app.Version = "Editor: " + EditorVersion + "; Desktop: " + DesktopVersion
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "file, f",
Usage: "Allow access to JSON style from web client",
},
&cli.BoolFlag{
Name: "watch",
Usage: "Notify web client about JSON style file changes",
},
&cli.IntFlag{
Name: "port",
Value: 8000,
Usage: "TCP port to listen on",
},
&cli.StringFlag{
Name: "static",
Usage: "Serve directory under /static/",
},
}
app.Action = func(c *cli.Context) error {
gui := http.FileServer(rice.MustFindBox("editor").HTTPBox())
router := mux.NewRouter().StrictSlash(true)
filename := c.String("file")
if filename != "" {
fmt.Printf("%s is accessible via Maputnik\n", filename)
// Allow access to reading and writing file on the local system
path, _ := filepath.Abs(filename)
accessor := StyleFileAccessor(path)
router.Path("/styles").Methods("GET").HandlerFunc(accessor.ListFiles)
router.Path("/styles/{styleId}").Methods("GET").HandlerFunc(accessor.ReadFile)
router.Path("/styles/{styleId}").Methods("PUT").HandlerFunc(accessor.SaveFile)
// Register websocket to notify we clients about file changes
if c.Bool("watch") {
router.Path("/ws").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
filewatch.ServeWebsocketFileWatcher(filename, w, r)
})
}
}
staticDir := c.String("static")
if staticDir != "" {
h := http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))
router.PathPrefix("/static/").Handler(h)
}
router.PathPrefix("/").Handler(http.StripPrefix("/", gui))
loggedRouter := handlers.LoggingHandler(os.Stdout, router)
corsRouter := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}), handlers.AllowedMethods([]string{"GET", "PUT"}), handlers.AllowedOrigins([]string{"*"}), handlers.AllowCredentials())(loggedRouter)
fmt.Printf("Exposing Maputnik on http://localhost:%d\n", c.Int("port"))
return http.ListenAndServe(fmt.Sprintf(":%d", c.Int("port")), corsRouter)
}
app.Run(os.Args)
}
-17
View File
@@ -1,17 +0,0 @@
export default {
output: 'src/locales/$LOCALE/$NAMESPACE.json',
locales: [ 'ja', 'he','zh' ],
// Because some keys are dynamically generated, i18next-parser can't detect them.
// We add these keys manually, so we don't want to remove them.
keepRemoved: true,
// We use plain English keys, so we disable key and namespace separators.
keySeparator: false,
namespaceSeparator: false,
defaultValue: (locale, ns, key) => {
// The default value is a string that indicates that the string is not translated.
return '__STRING_NOT_TRANSLATED__';
}
}
-1
View File
@@ -5,7 +5,6 @@
<title>Maputnik</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="src/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);
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+9864 -4906
View File
File diff suppressed because it is too large Load Diff
+51 -77
View File
@@ -1,49 +1,38 @@
{ {
"name": "maputnik", "name": "maputnik",
"version": "2.1.0", "version": "2.0.0-pre.2",
"description": "A MapLibre GL visual style editor", "description": "A MapLibre GL visual style editor",
"type": "module",
"main": "''", "main": "''",
"scripts": { "scripts": {
"start": "vite", "start": "vite",
"build": "tsc && vite build --base=/maputnik/", "build": "tsc && vite build",
"build-desktop": "tsc && vite build --base=/ && cd desktop && make", "lint": "eslint ./src --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0 && npm run lint-css",
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
"lint": "eslint ./src ./cypress --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
"test": "cypress run", "test": "cypress run",
"cy:open": "cypress open", "cy:open": "cypress open",
"lint-css": "stylelint \"src/styles/*.scss\"", "lint-css": "stylelint \"src/styles/*.scss\"",
"sort-styles": "jq 'sort_by(.id)' src/config/styles.json > tmp.json && mv tmp.json src/config/styles.json" "storybook": "storybook dev -h 0.0.0.0 -p 6006",
"build-storybook": "storybook build -o build/storybook"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/maplibre/maputnik" "url": "https://github.com/maputnik/editor"
}, },
"author": "Lukas Martinelli", "author": "Lukas Martinelli",
"license": "MIT", "license": "MIT",
"homepage": "https://github.com/maplibre/maputnik#readme", "homepage": "https://github.com/maputnik/editor#readme",
"dependencies": { "dependencies": {
"@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@mapbox/mapbox-gl-rtl-text": "^0.2.3",
"@maplibre/maplibre-gl-geocoder": "^1.6.0", "@maplibre/maplibre-gl-style-spec": "^17.0.1",
"@maplibre/maplibre-gl-inspect": "^1.6.3", "@mdi/js": "^6.6.96",
"@maplibre/maplibre-gl-style-spec": "^20.1.1", "@mdi/react": "^1.5.0",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"array-move": "^4.0.0", "array-move": "^4.0.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"classnames": "^2.5.1", "classnames": "^2.3.1",
"codemirror": "^5.65.2", "codemirror": "^5.65.2",
"color": "^4.2.3", "color": "^4.2.3",
"cypress-plugin-tab": "^1.0.5",
"detect-browser": "^5.3.0", "detect-browser": "^5.3.0",
"events": "^3.3.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"i18next": "^23.12.2", "json-stringify-pretty-compact": "^3.0.0",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-resources-to-backend": "^1.2.1",
"json-stringify-pretty-compact": "^4.0.0",
"json-to-ast": "^2.1.0", "json-to-ast": "^2.1.0",
"jsonlint": "github:josdejong/jsonlint#85a19d7", "jsonlint": "github:josdejong/jsonlint#85a19d7",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@@ -53,30 +42,30 @@
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"maplibre-gl": "^4.1.2", "mapbox-gl-inspect": "^1.3.1",
"maplibre-gl": "^2.4.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",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.2.0", "react": "^16.0.0",
"react-accessible-accordion": "^5.0.0", "react-accessible-accordion": "^4.0.0",
"react-aria-menubutton": "^7.0.3", "react-aria-menubutton": "^7.0.3",
"react-aria-modal": "^5.0.2", "react-aria-modal": "^4.0.1",
"react-autobind": "^1.0.6", "react-autobind": "^1.0.6",
"react-autocomplete": "^1.8.1", "react-autocomplete": "^1.8.1",
"react-collapse": "^5.1.1", "react-collapse": "^5.1.1",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-dom": "^18.2.0", "react-dom": "^16.0.0",
"react-file-reader-input": "^2.0.0", "react-file-reader-input": "^2.0.0",
"react-i18next": "^15.0.1",
"react-icon-base": "^2.1.2", "react-icon-base": "^2.1.2",
"react-icons": "^5.0.1", "react-icons": "^4.3.1",
"react-sortable-hoc": "^2.0.0", "react-sortable-hoc": "^2.0.0",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",
"sass": "^1.72.0", "sass": "^1.50.0",
"slugify": "^1.6.6", "slugify": "^1.6.5",
"string-hash": "^1.1.3", "string-hash": "^1.1.3",
"url": "^0.11.3" "url": "^0.11.0"
}, },
"jshintConfig": { "jshintConfig": {
"esversion": 6 "esversion": 6
@@ -96,54 +85,39 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@cypress/code-coverage": "^3.12.30",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-replace": "^5.0.5",
"@shellygo/cypress-test-utils": "^2.1.9", "@storybook/addon-a11y": "^7.6.5",
"@types/codemirror": "^5.60.15", "@storybook/addon-actions": "^7.6.5",
"@types/color": "^3.0.6", "@storybook/addon-links": "^7.6.5",
"@storybook/addon-storysource": "^7.6.5",
"@storybook/addons": "^7.6.5",
"@storybook/builder-vite": "^7.6.5",
"@storybook/react": "^7.6.5",
"@storybook/react-vite": "^7.6.5",
"@storybook/theming": "^7.6.5",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/file-saver": "^2.0.7", "@types/react": "^16.14.52",
"@types/geojson": "^7946.0.14", "@types/react-dom": "^16.9.24",
"@types/json-to-ast": "^2.1.4", "@types/uuid": "^9.0.7",
"@types/lodash.capitalize": "^4.2.9", "@vitejs/plugin-react": "^4.2.0",
"@types/lodash.clamp": "^4.0.9",
"@types/lodash.clonedeep": "^4.5.9",
"@types/lodash.get": "^4.4.9",
"@types/lodash.isequal": "^4.5.8",
"@types/lodash.throttle": "^4.1.9",
"@types/mocha": "^10.0.6",
"@types/randomcolor": "^0.5.9",
"@types/react": "^18.2.67",
"@types/react-aria-menubutton": "^6.2.14",
"@types/react-aria-modal": "^4.0.10",
"@types/react-autocomplete": "^1.8.10",
"@types/react-collapse": "^5.0.4",
"@types/react-color": "^3.0.12",
"@types/react-dom": "^18.2.22",
"@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.8",
"@vitejs/plugin-react": "^4.2.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"cypress": "^13.13.0", "cypress": "^13.6.1",
"eslint": "^8.57.0", "eslint": "^8.53.0",
"eslint-plugin-react": "^7.34.1", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6", "eslint-plugin-react-refresh": "^0.4.4",
"i18next-parser": "^9.0.1", "express": "^4.17.3",
"istanbul": "^0.4.5", "istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.2.2", "istanbul-lib-coverage": "^3.2.0",
"mocha": "^10.3.0", "mocha": "^9.2.2",
"postcss": "^8.4.38", "postcss": "^8.4.12",
"react-hot-loader": "^4.13.1", "react-hot-loader": "^4.13.0",
"stylelint": "^16.2.1", "storybook": "^7.6.5",
"stylelint-config-recommended-scss": "^14.0.0", "stylelint": "^14.6.1",
"stylelint-scss": "^6.2.1", "stylelint-config-recommended-scss": "^6.0.0",
"typescript": "^5.4.3", "stylelint-scss": "^4.2.0",
"uuid": "^9.0.1", "typescript": "^5.3.3",
"vite": "^5.2.6", "uuid": "^8.3.2",
"vite-plugin-istanbul": "^6.0.0" "vite": "^5.0.0"
} }
} }
+115 -164
View File
@@ -1,4 +1,3 @@
// @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'
@@ -7,15 +6,14 @@ 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 MapMaplibreGl from './MapMaplibreGl' 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, { MapState } from './AppToolbar' import AppToolbar from './AppToolbar'
import AppLayout from './AppLayout' import AppLayout from './AppLayout'
import MessagePanel from './AppMessagePanel' import MessagePanel from './AppMessagePanel'
@@ -24,9 +22,11 @@ import ModalExport from './ModalExport'
import ModalSources from './ModalSources' import ModalSources from './ModalSources'
import ModalOpen from './ModalOpen' import ModalOpen from './ModalOpen'
import ModalShortcuts from './ModalShortcuts' import ModalShortcuts from './ModalShortcuts'
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,13 +37,12 @@ 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 { SortEnd } from 'react-sortable-hoc'; import {formatLayerId} from '../util/format';
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: string, mapStyle: StyleSpecification) { function setFetchAccessToken(url, mapStyle) {
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/);
@@ -64,7 +63,7 @@ function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
} }
} }
function updateRootSpec(spec: any, fieldName: string, newValues: any) { function updateRootSpec(spec, fieldName, newValues) {
return { return {
...spec, ...spec,
$root: { $root: {
@@ -77,73 +76,15 @@ function updateRootSpec(spec: any, fieldName: string, newValues: any) {
} }
} }
type OnStyleChangedOpts = { export default class App extends React.Component {
save?: boolean constructor(props) {
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
debug: boolean
}
}
export default class App extends React.Component<any, AppState> {
revisionStore: RevisionStore;
styleStore: StyleStore | ApiStyleStore;
layerWatcher: LayerWatcher;
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({
@@ -195,7 +136,7 @@ export default class App extends React.Component<any, AppState> {
{ {
key: "m", key: "m",
handler: () => { handler: () => {
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus(); document.querySelector(".maplibregl-canvas").focus();
} }
}, },
{ {
@@ -208,7 +149,7 @@ export default class App extends React.Component<any, AppState> {
document.body.addEventListener("keyup", (e) => { document.body.addEventListener("keyup", (e) => {
if(e.key === "Escape") { if(e.key === "Escape") {
(e.target as HTMLElement).blur(); e.target.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) {
@@ -218,7 +159,7 @@ export default class App extends React.Component<any, AppState> {
if(shortcut) { if(shortcut) {
this.setModal("shortcuts", false); this.setModal("shortcuts", false);
shortcut.handler(); shortcut.handler(e);
} }
} }
}) })
@@ -251,6 +192,8 @@ export default class App extends React.Component<any, AppState> {
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: [],
@@ -274,6 +217,7 @@ export default class App extends React.Component<any, AppState> {
shortcuts: false, shortcuts: false,
export: false, export: false,
// TODO: Disabled for now, this should be opened on the Nth visit to the editor // TODO: Disabled for now, this should be opened on the Nth visit to the editor
survey: false,
debug: false, debug: false,
}, },
maplibreGlDebugOptions: { maplibreGlDebugOptions: {
@@ -291,25 +235,25 @@ export default class App extends React.Component<any, AppState> {
}) })
} }
handleKeyPress = (e: KeyboardEvent) => { handleKeyPress = (e) => {
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(); this.onRedo(e);
} }
else if(e.metaKey && e.keyCode === 90) { else if(e.metaKey && e.keyCode === 90) {
e.preventDefault(); e.preventDefault();
this.onUndo(); this.onUndo(e);
} }
} }
else { else {
if(e.ctrlKey && e.keyCode === 90) { if(e.ctrlKey && e.keyCode === 90) {
e.preventDefault(); e.preventDefault();
this.onUndo(); this.onUndo(e);
} }
else if(e.ctrlKey && e.keyCode === 89) { else if(e.ctrlKey && e.keyCode === 89) {
e.preventDefault(); e.preventDefault();
this.onRedo(); this.onRedo(e);
} }
} }
} }
@@ -322,27 +266,27 @@ export default class App extends React.Component<any, AppState> {
window.removeEventListener("keydown", this.handleKeyPress); window.removeEventListener("keydown", this.handleKeyPress);
} }
saveStyle(snapshotStyle: StyleSpecification & {id: string}) { saveStyle(snapshotStyle) {
this.styleStore.save(snapshotStyle) this.styleStore.save(snapshotStyle)
} }
updateFonts(urlTemplate: string) { updateFonts(urlTemplate) {
const metadata: {[key: string]: string} = this.state.mapStyle.metadata || {} as any const metadata = this.state.mapStyle.metadata || {}
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
const glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate; let 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: string) { updateIcons(baseUrl) {
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: string, value: any) => { onChangeMetadataProperty = (property, value) => {
// 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' &&
@@ -356,14 +300,14 @@ export default class App extends React.Component<any, AppState> {
const changedStyle = { const changedStyle = {
...this.state.mapStyle, ...this.state.mapStyle,
metadata: { metadata: {
...(this.state.mapStyle as any).metadata, ...this.state.mapStyle.metadata,
[property]: value [property]: value
} }
} }
this.onStyleChanged(changedStyle) this.onStyleChanged(changedStyle)
} }
onStyleChanged = (newStyle: StyleSpecification & {id: string}, opts: OnStyleChangedOpts={}) => { onStyleChanged = (newStyle, opts={}) => {
opts = { opts = {
save: true, save: true,
addRevision: true, addRevision: true,
@@ -375,15 +319,16 @@ export default class App extends React.Component<any, AppState> {
this.getInitialStateFromUrl(newStyle); this.getInitialStateFromUrl(newStyle);
} }
const errors: ValidationError[] = validateStyleMin(newStyle) || []; const errors = validate(newStyle, latest) || [];
// The validate function doesn't give us errors for duplicate error with // The validate function doesn't give us errors for duplicate error with
// empty string for layer.id, manually deal with that here. // empty string for layer.id, manually deal with that here.
const layerErrors: (Error | ValidationError)[] = []; const layerErrors = [];
if (newStyle && newStyle.layers) { if (newStyle && newStyle.layers) {
const foundLayers = new global.Map(); const foundLayers = new 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`
); );
@@ -397,7 +342,7 @@ export default class App extends React.Component<any, AppState> {
// 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 [, index, message] = dupMatch; const [matchStr, index, message] = dupMatch;
return { return {
message: error.message, message: error.message,
parsed: { parsed: {
@@ -414,7 +359,7 @@ export default class App extends React.Component<any, AppState> {
// 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 [, index, message] = invalidSourceMatch; const [matchStr, index, message] = invalidSourceMatch;
return { return {
message: error.message, message: error.message,
parsed: { parsed: {
@@ -430,7 +375,7 @@ export default class App extends React.Component<any, AppState> {
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/); const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
if (layerMatch) { if (layerMatch) {
const [, index, group, property, message] = layerMatch; const [matchStr, 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,
@@ -451,7 +396,7 @@ export default class App extends React.Component<any, AppState> {
} }
}); });
let dirtyMapStyle: StyleSpecification | undefined = undefined; let dirtyMapStyle = undefined;
if (errors.length > 0) { if (errors.length > 0) {
dirtyMapStyle = cloneDeep(newStyle); dirtyMapStyle = cloneDeep(newStyle);
@@ -461,7 +406,7 @@ export default class App extends React.Component<any, AppState> {
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) {
@@ -472,17 +417,17 @@ export default class App extends React.Component<any, AppState> {
} }
if(newStyle.glyphs !== this.state.mapStyle.glyphs) { if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
this.updateFonts(newStyle.glyphs as string) this.updateFonts(newStyle.glyphs)
} }
if(newStyle.sprite !== this.state.mapStyle.sprite) { if(newStyle.sprite !== this.state.mapStyle.sprite) {
this.updateIcons(newStyle.sprite as string) this.updateIcons(newStyle.sprite)
} }
if (opts.addRevision) { if (opts.addRevision) {
this.revisionStore.addRevision(newStyle); this.revisionStore.addRevision(newStyle);
} }
if (opts.save) { if (opts.save) {
this.saveStyle(newStyle as StyleSpecification & {id: string}); this.saveStyle(newStyle);
} }
this.setState({ this.setState({
@@ -515,7 +460,7 @@ export default class App extends React.Component<any, AppState> {
}) })
} }
onMoveLayer = (move: SortEnd) => { onMoveLayer = (move) => {
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);
@@ -533,7 +478,7 @@ export default class App extends React.Component<any, AppState> {
this.onLayersChange(layers); this.onLayersChange(layers);
} }
onLayersChange = (changedLayers: LayerSpecification[]) => { onLayersChange = (changedLayers) => {
const changedStyle = { const changedStyle = {
...this.state.mapStyle, ...this.state.mapStyle,
layers: changedLayers layers: changedLayers
@@ -541,15 +486,15 @@ export default class App extends React.Component<any, AppState> {
this.onStyleChanged(changedStyle) this.onStyleChanged(changedStyle)
} }
onLayerDestroy = (index: number) => { onLayerDestroy = (index) => {
const layers = this.state.mapStyle.layers; let 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: number) => { onLayerCopy = (index) => {
const layers = this.state.mapStyle.layers; let 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])
@@ -558,8 +503,8 @@ export default class App extends React.Component<any, AppState> {
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
onLayerVisibilityToggle = (index: number) => { onLayerVisibilityToggle = (index) => {
const layers = this.state.mapStyle.layers; let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0) const changedLayers = layers.slice(0)
const layer = { ...changedLayers[index] } const layer = { ...changedLayers[index] }
@@ -572,7 +517,7 @@ export default class App extends React.Component<any, AppState> {
} }
onLayerIdChange = (index: number, _oldId: string, newId: string) => { onLayerIdChange = (index, oldId, newId) => {
const changedLayers = this.state.mapStyle.layers.slice(0) const changedLayers = this.state.mapStyle.layers.slice(0)
changedLayers[index] = { changedLayers[index] = {
...changedLayers[index], ...changedLayers[index],
@@ -582,26 +527,26 @@ export default class App extends React.Component<any, AppState> {
this.onLayersChange(changedLayers) this.onLayersChange(changedLayers)
} }
onLayerChanged = (index: number, layer: LayerSpecification) => { onLayerChanged = (index, layer) => {
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: MapState) => { setMapState = (newState) => {
this.setState({ this.setState({
mapState: newState mapState: newState
}, this.setStateInUrl); }, this.setStateInUrl);
} }
setDefaultValues = (styleObj: StyleSpecification & {id: string}) => { setDefaultValues = (styleObj) => {
const metadata: {[key: string]: string} = styleObj.metadata || {} as any const metadata = styleObj.metadata || {}
if(metadata['maputnik:renderer'] === undefined) { if(metadata['maputnik:renderer'] === undefined) {
const changedStyle = { const changedStyle = {
...styleObj, ...styleObj,
metadata: { metadata: {
...styleObj.metadata as any, ...styleObj.metadata,
'maputnik:renderer': 'mlgljs' 'maputnik:renderer': 'mlgljs'
} }
} }
@@ -611,19 +556,19 @@ export default class App extends React.Component<any, AppState> {
} }
} }
openStyle = (styleObj: StyleSpecification & {id: string}) => { openStyle = (styleObj) => {
styleObj = this.setDefaultValues(styleObj) styleObj = this.setDefaultValues(styleObj)
this.onStyleChanged(styleObj) this.onStyleChanged(styleObj)
} }
fetchSources() { fetchSources() {
const sourceList: {[key: string]: any} = {}; const sourceList = {};
for(const [key, val] of Object.entries(this.state.mapStyle.sources)) { for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
if( if(
!Object.prototype.hasOwnProperty.call(this.state.sources, key) && !this.state.sources.hasOwnProperty(key) &&
val.type === "vector" && val.type === "vector" &&
Object.prototype.hasOwnProperty.call(val, "url") val.hasOwnProperty("url")
) { ) {
sourceList[key] = { sourceList[key] = {
type: val.type, type: val.type,
@@ -633,38 +578,38 @@ export default class App extends React.Component<any, AppState> {
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(!Object.prototype.hasOwnProperty.call(json, "vector_layers")) { if(!json.hasOwnProperty("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];
@@ -680,17 +625,11 @@ export default class App extends React.Component<any, AppState> {
} }
_getRenderer () { _getRenderer () {
const metadata: {[key:string]: string} = this.state.mapStyle.metadata || {} as any; const metadata = this.state.mapStyle.metadata || {};
return metadata['maputnik:renderer'] || 'mlgljs'; return metadata['maputnik:renderer'] || 'mlgljs';
} }
onMapChange = (mapView: { onMapChange = (mapView) => {
zoom: number,
center: {
lng: number,
lat: number,
},
}) => {
this.setState({ this.setState({
mapView, mapView,
}); });
@@ -698,15 +637,16 @@ export default class App extends React.Component<any, AppState> {
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: StyleSpecification) => { replaceAccessTokens: (mapStyle) => {
return style.replaceAccessTokens(mapStyle, { return style.replaceAccessTokens(mapStyle, {
allowFallback: true allowFallback: true
}); });
}, },
onDataChange: (e: {map: Map}) => { onDataChange: (e) => {
this.layerWatcher.analyzeMap(e.map) this.layerWatcher.analyzeMap(e.map)
this.fetchSources(); this.fetchSources();
}, },
@@ -737,12 +677,12 @@ export default class App extends React.Component<any, AppState> {
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: {filter?: string} = {}; const elementStyle = {};
if (filterName) { if (filterName) {
elementStyle.filter = `url('#${filterName}')`; elementStyle.filter = `url('#${filterName}')`;
} }
return <div style={elementStyle} className="maputnik-map__container" data-wd-key="maplibre:container"> return <div style={elementStyle} className="maputnik-map__container">
{mapElement} {mapElement}
</div> </div>
} }
@@ -755,8 +695,8 @@ export default class App extends React.Component<any, AppState> {
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(","));
@@ -775,12 +715,12 @@ export default class App extends React.Component<any, AppState> {
history.replaceState({selectedLayerIndex}, "Maputnik", url.href); history.replaceState({selectedLayerIndex}, "Maputnik", url.href);
} }
getInitialStateFromUrl = (mapStyle: StyleSpecification) => { getInitialStateFromUrl = (mapStyle) => {
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: {[key: string]: boolean} = {}; const modalObj = {};
modals.forEach(modalName => { modals.forEach(modalName => {
modalObj[modalName] = true; modalObj[modalName] = true;
}); });
@@ -795,7 +735,7 @@ export default class App extends React.Component<any, AppState> {
const view = url.searchParams.get("view"); const view = url.searchParams.get("view");
if (view && view !== "") { if (view && view !== "") {
this.setMapState(view as MapState); this.setMapState(view);
} }
const path = url.searchParams.get("layer"); const path = url.searchParams.get("layer");
@@ -827,14 +767,18 @@ export default class App extends React.Component<any, AppState> {
} }
} }
onLayerSelect = (index: number) => { onLayerSelect = (index) => {
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: keyof AppState["isOpen"], value: boolean) { setModal(modalName, value) {
if(modalName === 'survey' && value === false) {
localStorage.setItem('survey', '');
}
this.setState({ this.setState({
isOpen: { isOpen: {
...this.state.isOpen, ...this.state.isOpen,
@@ -843,11 +787,11 @@ export default class App extends React.Component<any, AppState> {
}, this.setStateInUrl) }, this.setStateInUrl)
} }
toggleModal(modalName: keyof AppState["isOpen"]) { toggleModal(modalName) {
this.setModal(modalName, !this.state.isOpen[modalName]); this.setModal(modalName, !this.state.isOpen[modalName]);
} }
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => { onChangeOpenlayersDebug = (key, value) => {
this.setState({ this.setState({
openlayersDebugOptions: { openlayersDebugOptions: {
...this.state.openlayersDebugOptions, ...this.state.openlayersDebugOptions,
@@ -856,7 +800,7 @@ export default class App extends React.Component<any, AppState> {
}); });
} }
onChangeMaplibreGlDebug = (key: keyof AppState["maplibreGlDebugOptions"], value: any) => { onChangeMaplibreGlDebug = (key, value) => {
this.setState({ this.setState({
maplibreGlDebugOptions: { maplibreGlDebugOptions: {
...this.state.maplibreGlDebugOptions, ...this.state.maplibreGlDebugOptions,
@@ -867,7 +811,8 @@ export default class App extends React.Component<any, AppState> {
render() { render() {
const layers = this.state.mapStyle.layers || [] const layers = this.state.mapStyle.layers || []
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : undefined const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
const metadata = this.state.mapStyle.metadata || {}
const toolbar = <AppToolbar const toolbar = <AppToolbar
renderer={this._getRenderer()} renderer={this._getRenderer()}
@@ -910,7 +855,7 @@ export default class App extends React.Component<any, AppState> {
onLayerVisibilityToggle={this.onLayerVisibilityToggle} onLayerVisibilityToggle={this.onLayerVisibilityToggle}
onLayerIdChange={this.onLayerIdChange} onLayerIdChange={this.onLayerIdChange}
errors={this.state.errors} errors={this.state.errors}
/> : undefined /> : null
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}
@@ -919,7 +864,7 @@ export default class App extends React.Component<any, AppState> {
mapStyle={this.state.mapStyle} mapStyle={this.state.mapStyle}
errors={this.state.errors} errors={this.state.errors}
infos={this.state.infos} infos={this.state.infos}
/> : undefined /> : null
const modals = <div> const modals = <div>
@@ -934,6 +879,7 @@ export default class App extends React.Component<any, AppState> {
mapView={this.state.mapView} mapView={this.state.mapView}
/> />
<ModalShortcuts <ModalShortcuts
ref={(el) => this.shortcutEl = el}
isOpen={this.state.isOpen.shortcuts} isOpen={this.state.isOpen.shortcuts}
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')} onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
/> />
@@ -943,6 +889,7 @@ export default class App extends React.Component<any, AppState> {
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}
@@ -961,6 +908,10 @@ export default class App extends React.Component<any, AppState> {
isOpen={this.state.isOpen.sources} isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')} onOpenToggle={this.toggleModal.bind(this, 'sources')}
/> />
<ModalSurvey
isOpen={this.state.isOpen.survey}
onOpenToggle={this.toggleModal.bind(this, 'survey')}
/>
</div> </div>
return <AppLayout return <AppLayout
+46
View File
@@ -0,0 +1,46 @@
import React from 'react'
import PropTypes from 'prop-types'
import ScrollContainer from './ScrollContainer'
class AppLayout extends React.Component {
static propTypes = {
toolbar: PropTypes.element.isRequired,
layerList: PropTypes.element.isRequired,
layerEditor: PropTypes.element,
map: PropTypes.element.isRequired,
bottom: PropTypes.element,
modals: PropTypes.node,
}
static childContextTypes = {
reactIconBase: PropTypes.object
}
getChildContext() {
return {
reactIconBase: { size: 14 }
}
}
render() {
return <div className="maputnik-layout">
{this.props.toolbar}
<div className="maputnik-layout-list">
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
{this.props.map}
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
</div>
}
{this.props.modals}
</div>
}
}
export default AppLayout
-52
View File
@@ -1,52 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import ScrollContainer from './ScrollContainer'
import { WithTranslation, withTranslation } from 'react-i18next';
type AppLayoutInternalProps = {
toolbar: React.ReactElement
layerList: React.ReactElement
layerEditor?: React.ReactElement
map: React.ReactElement
bottom?: React.ReactElement
modals?: React.ReactNode
} & WithTranslation;
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
static childContextTypes = {
reactIconBase: PropTypes.object
}
getChildContext() {
return {
reactIconBase: { size: 14 }
}
}
render() {
document.body.dir = this.props.i18n.dir();
return <div className="maputnik-layout">
{this.props.toolbar}
<div className="maputnik-layout-main">
<div className="maputnik-layout-list">
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
{this.props.map}
</div>
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
</div>
}
{this.props.modals}
</div>
}
}
const AppLayout = withTranslation()(AppLayoutInternal);
export default AppLayout;
+62
View File
@@ -0,0 +1,62 @@
import React from 'react'
import PropTypes from 'prop-types'
import {formatLayerId} from '../util/format';
export default class AppMessagePanel extends React.Component {
static propTypes = {
errors: PropTypes.array,
infos: PropTypes.array,
mapStyle: PropTypes.object,
onLayerSelect: PropTypes.func,
currentLayer: PropTypes.object,
selectedLayerIndex: PropTypes.number,
}
static defaultProps = {
onLayerSelect: () => {},
}
render() {
const {selectedLayerIndex} = this.props;
const errors = this.props.errors.map((error, idx) => {
let content;
if (error.parsed && error.parsed.type === "layer") {
const {parsed} = error;
const {mapStyle, currentLayer} = this.props;
const layerId = mapStyle.layers[parsed.data.index].id;
content = (
<>
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
{selectedLayerIndex !== parsed.data.index &&
<>
&nbsp;&mdash;&nbsp;
<button
className="maputnik-message-panel__switch-button"
onClick={() => this.props.onLayerSelect(parsed.data.index)}
>
switch to layer
</button>
</>
}
</>
);
}
else {
content = error.message;
}
return <p key={"error-"+idx} className="maputnik-message-panel-error">
{content}
</p>
})
const infos = this.props.infos.map((m, i) => {
return <p key={"info-"+i}>{m}</p>
})
return <div className="maputnik-message-panel">
{errors}
{infos}
</div>
}
}
-66
View File
@@ -1,66 +0,0 @@
import React from 'react'
import {formatLayerId} from '../libs/format';
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
type AppMessagePanelInternalProps = {
errors?: unknown[]
infos?: string[]
mapStyle?: StyleSpecification
onLayerSelect?(...args: unknown[]): unknown
currentLayer?: LayerSpecification
selectedLayerIndex?: number
} & WithTranslation;
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
static defaultProps = {
onLayerSelect: () => {},
}
render() {
const {t, selectedLayerIndex} = this.props;
const errors = this.props.errors?.map((error: any, idx) => {
let content;
if (error.parsed && error.parsed.type === "layer") {
const {parsed} = error;
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
content = (
<>
<Trans t={t}>
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
</Trans>
{selectedLayerIndex !== parsed.data.index &&
<>
&nbsp;&mdash;&nbsp;
<button
className="maputnik-message-panel__switch-button"
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
>
{t("switch to layer")}
</button>
</>
}
</>
);
}
else {
content = error.message;
}
return <p key={"error-"+idx} className="maputnik-message-panel-error">
{content}
</p>
})
const infos = this.props.infos?.map((m, i) => {
return <p key={"info-"+i}>{m}</p>
})
return <div className="maputnik-message-panel">
{errors}
{infos}
</div>
}
}
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
export default AppMessagePanel;
@@ -1,57 +1,75 @@
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, MdLanguage} from 'react-icons/md' import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdAssignmentTurnedIn} from 'react-icons/md'
import pkgJson from '../../package.json' import pkgJson from '../../package.json'
//@ts-ignore
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
import { withTranslation, WithTranslation } from 'react-i18next';
import { supportedLanguages } from '../i18n';
// 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;
type IconTextProps = { class IconText extends React.Component {
children?: React.ReactNode static propTypes = {
}; 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>
} }
} }
type ToolbarLinkProps = { class ToolbarLink extends React.Component {
className?: string static propTypes = {
children?: React.ReactNode className: PropTypes.string,
href?: string children: PropTypes.node,
onToggleModal?(...args: unknown[]): unknown href: PropTypes.string,
}; 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>
} }
} }
type ToolbarSelectProps = { class ToolbarLinkHighlighted extends React.Component {
children?: React.ReactNode static propTypes = {
wdKey?: string className: PropTypes.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'
@@ -62,13 +80,13 @@ class ToolbarSelect extends React.Component<ToolbarSelectProps> {
} }
} }
type ToolbarActionProps = { class ToolbarAction extends React.Component {
children?: React.ReactNode static propTypes = {
onClick?(...args: unknown[]): unknown children: PropTypes.node,
wdKey?: string onClick: PropTypes.func,
}; wdKey: PropTypes.string
}
class ToolbarAction extends React.Component<ToolbarActionProps> {
render() { render() {
return <button return <button
className='maputnik-toolbar-action' className='maputnik-toolbar-action'
@@ -80,24 +98,22 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
} }
} }
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia"; export default class AppToolbar extends React.Component {
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 AppToolbarInternalProps = {
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
} & WithTranslation;
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
state = { state = {
isOpen: { isOpen: {
settings: false, settings: false,
@@ -108,60 +124,55 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
} }
} }
handleSelection(val: MapState) { handleSelection(val) {
this.props.onSetMapState(val); this.props.onSetMapState(val);
} }
handleLanguageChange(val: string) { onSkip = (target) => {
this.props.i18n.changeLanguage(val);
}
onSkip = (target: string) => {
if (target === "map") { if (target === "map") {
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus(); document.querySelector(".maplibregl-canvas").focus();
} }
else { else {
const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement; const el = document.querySelector("#skip-target-"+target);
el.focus(); el.focus();
} }
} }
render() { render() {
const t = this.props.t;
const views = [ const views = [
{ {
id: "map", id: "map",
group: "general", group: "general",
title: t("Map"), title: "Map",
}, },
{ {
id: "inspect", id: "inspect",
group: "general", group: "general",
title: t("Inspect"), title: "Inspect",
disabled: this.props.renderer === 'ol', disabled: this.props.renderer === 'ol',
}, },
{ {
id: "filter-deuteranopia", id: "filter-deuteranopia",
group: "color-accessibility", group: "color-accessibility",
title: t("Deuteranopia filter"), title: "Deuteranopia filter",
disabled: !colorAccessibilityFiltersEnabled, disabled: !colorAccessibilityFiltersEnabled,
}, },
{ {
id: "filter-protanopia", id: "filter-protanopia",
group: "color-accessibility", group: "color-accessibility",
title: t("Protanopia filter"), title: "Protanopia filter",
disabled: !colorAccessibilityFiltersEnabled, disabled: !colorAccessibilityFiltersEnabled,
}, },
{ {
id: "filter-tritanopia", id: "filter-tritanopia",
group: "color-accessibility", group: "color-accessibility",
title: t("Tritanopia filter"), title: "Tritanopia filter",
disabled: !colorAccessibilityFiltersEnabled, disabled: !colorAccessibilityFiltersEnabled,
}, },
{ {
id: "filter-achromatopsia", id: "filter-achromatopsia",
group: "color-accessibility", group: "color-accessibility",
title: t("Achromatopsia filter"), title: "Achromatopsia filter",
disabled: !colorAccessibilityFiltersEnabled, disabled: !colorAccessibilityFiltersEnabled,
}, },
]; ];
@@ -179,31 +190,31 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
<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")}
> >
{t("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")}
> >
{t("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")}
> >
{t("Map view")} Map view
</button> </button>
<a <a
className="maputnik-toolbar-logo" className="maputnik-toolbar-logo"
target="blank" target="blank"
rel="noreferrer noopener" rel="noreferrer noopener"
href="https://github.com/maplibre/maputnik" href="https://github.com/maputnik/editor"
> >
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} /> <img src="node_modules/maputnik-design/logos/logo-color.svg" />
<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>
@@ -213,38 +224,37 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar"> <div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}> <ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
<MdOpenInBrowser /> <MdOpenInBrowser />
<IconText>{t("Open")}</IconText> <IconText>Open</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}> <ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
<MdFileDownload /> <MdFileDownload />
<IconText>{t("Export")}</IconText> <IconText>Export</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}> <ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
<MdLayers /> <MdLayers />
<IconText>{t("Data Sources")}</IconText> <IconText>Data Sources</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}> <ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
<MdSettings /> <MdSettings />
<IconText>{t("Style Settings")}</IconText> <IconText>Style Settings</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarSelect wdKey="nav:inspect"> <ToolbarSelect wdKey="nav:inspect">
<MdFindInPage /> <MdFindInPage />
<label>{t("View")} <label>View
<select <select
className="maputnik-select" className="maputnik-select"
data-wd-key="maputnik-select" onChange={(e) => this.handleSelection(e.target.value)}
onChange={(e) => this.handleSelection(e.target.value as MapState)} value={currentView.id}
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} data-wd-key={item.id}> <option key={item.id} value={item.id} disabled={item.disabled}>
{item.title} {item.title}
</option> </option>
); );
})} })}
<optgroup label={t("Color accessibility")}> <optgroup label="Color accessibility">
{views.filter(v => v.group === "color-accessibility").map((item) => { {views.filter(v => v.group === "color-accessibility").map((item) => {
return ( return (
<option key={item.id} value={item.id} disabled={item.disabled}> <option key={item.id} value={item.id} disabled={item.disabled}>
@@ -257,35 +267,16 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
</label> </label>
</ToolbarSelect> </ToolbarSelect>
<ToolbarSelect wdKey="nav:language"> <ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
<MdLanguage />
<label>{t("Language")}
<select
className="maputnik-select"
data-wd-key="maputnik-lang-select"
onChange={(e) => this.handleLanguageChange(e.target.value)}
value={this.props.i18n.language}
>
{Object.entries(supportedLanguages).map(([code, name]) => {
return (
<option key={code} value={code}>
{name}
</option>
);
})}
</select>
</label>
</ToolbarSelect>
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
<MdHelpOutline /> <MdHelpOutline />
<IconText>{t("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>
} }
} }
const AppToolbar = withTranslation()(AppToolbarInternal);
export default AppToolbar;
@@ -1,43 +1,40 @@
import React, {PropsWithChildren, SyntheticEvent} from 'react' import React 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'
type BlockProps = PropsWithChildren & {
"data-wd-key"?: string
label?: string
action?: React.ReactElement
style?: object
onChange?(...args: unknown[]): unknown
fieldSpec?: object
wideMode?: boolean
error?: {message: string}
};
type BlockState = {
showDoc: boolean
};
/** Wrap a component with a label */ /** Wrap a component with a label */
export default class Block extends React.Component<BlockProps, BlockState> { export default class Block extends React.Component {
_blockEl: HTMLDivElement | null = null; static propTypes = {
"data-wd-key": PropTypes.string,
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
]),
action: PropTypes.element,
children: PropTypes.node.isRequired,
style: PropTypes.object,
onChange: PropTypes.func,
fieldSpec: PropTypes.object,
wideMode: PropTypes.bool,
error: PropTypes.array,
}
constructor (props: BlockProps) { constructor (props) {
super(props); super(props);
this.state = { this.state = {
showDoc: false, showDoc: false,
} }
} }
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) { onChange(e) {
const value = e.target.value const value = e.target.value
if (this.props.onChange) { return this.props.onChange(value === "" ? undefined : value)
return this.props.onChange(value === "" ? undefined : value)
}
} }
onToggleDoc = (val: boolean) => { onToggleDoc = (val) => {
this.setState({ this.setState({
showDoc: val showDoc: val
}); });
@@ -49,17 +46,20 @@ export default class Block extends React.Component<BlockProps, BlockState> {
* 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: SyntheticEvent<any, any>) => { onLabelClick = (event) => {
const el = event.nativeEvent.target; const el = event.nativeEvent.target;
const contains = this._blockEl?.contains(el); const nativeEvent = event.nativeEvent;
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 {reducedMotionEnabled} from '../libs/accessibility' import accessibility from '../../libs/accessibility'
type CollapseProps = { export default class Collapse extends React.Component {
isActive: boolean static propTypes = {
children: React.ReactElement isActive: PropTypes.bool.isRequired,
}; children: PropTypes.element.isRequired
}
export default class Collapse extends React.Component<CollapseProps> {
static defaultProps = { static defaultProps = {
isActive: true isActive: true
} }
render() { render() {
if (reducedMotionEnabled()) { if (accessibility.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,12 +1,13 @@
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'
type CollapserProps = { export default class Collapser extends React.Component {
isCollapsed: boolean static propTypes = {
style?: object isCollapsed: PropTypes.bool.isRequired,
}; style: PropTypes.object,
}
export default class Collapser extends React.Component<CollapserProps> {
render() { render() {
const iconStyle = { const iconStyle = {
width: 20, width: 20,
@@ -1,37 +1,28 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types'
const headers = { export default class Doc extends React.Component {
js: "JS", static propTypes = {
android: "Android", fieldSpec: PropTypes.object.isRequired,
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 style spec, so this is required // HACK: Currently we merge additional values into the style spec, so this is required
// See <https://github.com/maplibre/maputnik/blob/main/src/components/PropertyGroup.jsx#L16> // See <https://github.com/maputnik/editor/blob/main/src/components/PropertyGroup.jsx#L16>
!Array.isArray(values) !Array.isArray(values)
); );
@@ -39,7 +30,7 @@ export default class Doc extends React.Component<DocProps> {
<> <>
{doc && {doc &&
<div className="SpecDoc"> <div className="SpecDoc">
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>{doc}</div> <div className="SpecDoc__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]) => {
@@ -70,9 +61,10 @@ export default class Doc extends React.Component<DocProps> {
return ( return (
<tr key={key}> <tr key={key}>
<td>{key}</td> <td>{key}</td>
{Object.keys(headers).map((k) => { {Object.keys(headers).map(k => {
if (Object.prototype.hasOwnProperty.call(supportObj, k)) { const value = supportObj[k];
return <td key={k}>{supportObj[k as keyof typeof headers]}</td>; if (supportObj.hasOwnProperty(k)) {
return <td key={k}>{supportObj[k]}</td>;
} }
else { else {
return <td key={k}>no</td>; return <td key={k}>no</td>;
+21
View File
@@ -0,0 +1,21 @@
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>
}
}
-19
View File
@@ -1,19 +0,0 @@
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>
}
}
+20
View File
@@ -0,0 +1,20 @@
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>
}
}
-18
View File
@@ -1,18 +0,0 @@
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>
}
}
+20
View File
@@ -0,0 +1,20 @@
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>
}
}
-18
View File
@@ -1,18 +0,0 @@
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>
}
}
+20
View File
@@ -0,0 +1,20 @@
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>
}
}
-21
View File
@@ -1,21 +0,0 @@
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>
}
}
+31
View File
@@ -0,0 +1,31 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputString from './InputString'
export default class FieldComment extends React.Component {
static propTypes = {
value: PropTypes.string,
onChange: PropTypes.func.isRequired,
}
render() {
const fieldSpec = {
doc: "Comments for the current layer. This is non-standard and not in the spec."
};
return <Block
label={"Comments"}
fieldSpec={fieldSpec}
data-wd-key="layer-comment"
>
<InputString
multi={true}
value={this.props.value}
onChange={this.props.onChange}
default="Comment..."
/>
</Block>
}
}
-38
View File
@@ -1,38 +0,0 @@
import React from 'react'
import Block from './Block'
import InputString from './InputString'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldCommentInternalProps = {
value?: string
onChange(value: string | undefined): unknown
error: {message: string}
} & WithTranslation;
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
render() {
const t = this.props.t;
const fieldSpec = {
doc: t("Comments for the current layer. This is non-standard and not in the spec."),
};
return <Block
label={t("Comments")}
fieldSpec={fieldSpec}
data-wd-key="layer-comment"
error={this.props.error}
>
<InputString
multi={true}
value={this.props.value}
onChange={this.props.onChange}
default={t("Comment...")}
data-wd-key="layer-comment.input"
/>
</Block>
}
}
const FieldComment = withTranslation()(FieldCommentInternal);
export default FieldComment;
@@ -1,27 +1,26 @@
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'
type FieldDocLabelProps = { export default class FieldDocLabel extends React.Component {
label: JSX.Element | string | undefined static propTypes = {
fieldSpec?: { label: PropTypes.oneOfType([
doc?: string PropTypes.object,
PropTypes.string
]).isRequired,
fieldSpec: PropTypes.object,
onToggleDoc: PropTypes.func,
} }
onToggleDoc?(...args: unknown[]): unknown
};
type FieldDocLabelState = { constructor (props) {
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: boolean) => { onToggleDoc = (open) => {
this.setState({ this.setState({
open, open,
}, () => { }, () => {
@@ -44,7 +43,6 @@ export default class FieldDocLabel extends React.Component<FieldDocLabelProps, F
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>
+21
View File
@@ -0,0 +1,21 @@
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>
}
}
-16
View File
@@ -1,16 +0,0 @@
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>
}
}
+20
View File
@@ -0,0 +1,20 @@
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>
}
}
-20
View File
@@ -1,20 +0,0 @@
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,18 +1,19 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types'
import SpecProperty from './_SpecProperty' import SpecProperty from './_SpecProperty'
import DataProperty, { Stop } from './_DataProperty' import DataProperty 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 '../libs/spec-helper'; import {findDefaultFromSpec} from '../util/spec-helper';
function isLiteralExpression(value: any) { function isLiteralExpression (value) {
return (Array.isArray(value) && value.length === 2 && value[0] === "literal"); return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
} }
function isGetExpression(value: any) { function isGetExpression (value) {
return ( return (
Array.isArray(value) && Array.isArray(value) &&
value.length === 2 && value.length === 2 &&
@@ -20,14 +21,14 @@ function isGetExpression(value: any) {
); );
} }
function isZoomField(value: any) { function isZoomField(value) {
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: Stop) => { value.stops.every(stop => {
return ( return (
Array.isArray(stop) && Array.isArray(stop) &&
stop.length === 2 stop.length === 2
@@ -36,22 +37,22 @@ function isZoomField(value: any) {
); );
} }
function isIdentityProperty(value: any) { function isIdentityProperty (value) {
return ( return (
typeof(value) === 'object' && typeof(value) === 'object' &&
value.type === "identity" && value.type === "identity" &&
Object.prototype.hasOwnProperty.call(value, "property") value.hasOwnProperty("property")
); );
} }
function isDataStopProperty(value: any) { function isDataStopProperty (value) {
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: Stop) => { value.stops.every(stop => {
return ( return (
Array.isArray(stop) && Array.isArray(stop) &&
stop.length === 2 && stop.length === 2 &&
@@ -61,26 +62,26 @@ function isDataStopProperty(value: any) {
); );
} }
function isDataField(value: any) { function isDataField(value) {
return ( return (
isIdentityProperty(value) || isIdentityProperty(value) ||
isDataStopProperty(value) isDataStopProperty(value)
); );
} }
function isPrimative(value: any): value is string | boolean | number { function isPrimative (value) {
const valid = ["string", "boolean", "number"]; const valid = ["string", "boolean", "number"];
return valid.includes(typeof(value)); return valid.includes(typeof(value));
} }
function isArrayOfPrimatives(values: any): values is Array<string | boolean | number> { function isArrayOfPrimatives (values) {
if (Array.isArray(values)) { if (Array.isArray(values)) {
return values.every(isPrimative); return values.every(isPrimative);
} }
return false; return false;
} }
function getDataType(value: any, fieldSpec={} as any) { function getDataType (value, fieldSpec={}) {
if (value === undefined) { if (value === undefined) {
return "value"; return "value";
} }
@@ -102,33 +103,35 @@ function getDataType(value: any, fieldSpec={} as any) {
} }
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<FieldFunctionProps, FieldFunctionState> { export default class FieldFunction extends React.Component {
constructor (props: FieldFunctionProps) { static propTypes = {
super(props); onChange: PropTypes.func.isRequired,
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: Readonly<FieldFunctionProps>, state: FieldFunctionState) { static getDerivedStateFromProps(props, state) {
// 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 {};
@@ -141,7 +144,7 @@ export default class FieldFunction extends React.Component<FieldFunctionProps, F
} }
} }
getFieldFunctionType(fieldSpec: any) { getFieldFunctionType(fieldSpec) {
if (fieldSpec.expression.interpolated) { if (fieldSpec.expression.interpolated) {
return "exponential" return "exponential"
} }
@@ -180,7 +183,7 @@ export default class FieldFunction extends React.Component<FieldFunctionProps, F
}); });
} }
deleteStop = (stopIdx: number) => { deleteStop = (stopIdx) => {
const stops = this.props.value.stops.slice(0) const stops = this.props.value.stops.slice(0)
stops.splice(stopIdx, 1) stops.splice(stopIdx, 1)
@@ -204,7 +207,7 @@ export default class FieldFunction extends React.Component<FieldFunctionProps, F
if (value.stops) { if (value.stops) {
zoomFunc = { zoomFunc = {
base: value.base, base: value.base,
stops: value.stops.map((stop: Stop) => { stops: value.stops.map(stop => {
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)]; return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
}) })
} }
@@ -289,7 +292,7 @@ export default class FieldFunction extends React.Component<FieldFunctionProps, F
property: "", property: "",
type: functionType, type: functionType,
base: value.base, base: value.base,
stops: value.stops.map((stop: Stop) => { stops: value.stops.map(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)];
}) })
} }
@@ -399,7 +402,7 @@ export default class FieldFunction extends React.Component<FieldFunctionProps, F
/> />
) )
} }
return <div className={propClass} data-wd-key={"spec-field-container:"+this.props.fieldName}> return <div className={propClass} data-wd-key={"spec-field:"+this.props.fieldName}>
{specField} {specField}
</div> </div>
} }
+27
View File
@@ -0,0 +1,27 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputString from './InputString'
export default class FieldId extends React.Component {
static propTypes = {
value: PropTypes.string.isRequired,
wdKey: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
error: PropTypes.object,
}
render() {
return <Block label={"ID"} fieldSpec={latest.layer.id}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
<InputString
value={this.props.value}
onInput={this.props.onChange}
/>
</Block>
}
}
-28
View File
@@ -1,28 +0,0 @@
import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputString from './InputString'
type FieldIdProps = {
value: string
wdKey: string
onChange(value: string | undefined): unknown
error?: {message: string}
};
export default class FieldId extends React.Component<FieldIdProps> {
render() {
return <Block label="ID" fieldSpec={latest.layer.id}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
<InputString
value={this.props.value}
onInput={this.props.onChange}
data-wd-key={this.props.wdKey + ".input"}
/>
</Block>
}
}
+16
View File
@@ -0,0 +1,16 @@
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} />
}
}
-13
View File
@@ -1,13 +0,0 @@
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} />
}
}
+30
View File
@@ -0,0 +1,30 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputNumber from './InputNumber'
export default class FieldMaxZoom extends React.Component {
static propTypes = {
value: PropTypes.number,
onChange: PropTypes.func.isRequired,
error: PropTypes.object,
}
render() {
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
error={this.props.error}
data-wd-key="max-zoom"
>
<InputNumber
allowRange={true}
value={this.props.value}
onChange={this.props.onChange}
min={latest.layer.maxzoom.minimum}
max={latest.layer.maxzoom.maximum}
default={latest.layer.maxzoom.maximum}
/>
</Block>
}
}
-35
View File
@@ -1,35 +0,0 @@
import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputNumber from './InputNumber'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldMaxZoomInternalProps = {
value?: number
onChange(value: number | undefined): unknown
error?: {message: string}
} & WithTranslation;
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
render() {
const t = this.props.t;
return <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
error={this.props.error}
data-wd-key="max-zoom"
>
<InputNumber
allowRange={true}
value={this.props.value}
onChange={this.props.onChange}
min={latest.layer.maxzoom.minimum}
max={latest.layer.maxzoom.maximum}
default={latest.layer.maxzoom.maximum}
data-wd-key="max-zoom.input"
/>
</Block>
}
}
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
export default FieldMaxZoom;
+30
View File
@@ -0,0 +1,30 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputNumber from './InputNumber'
export default class FieldMinZoom extends React.Component {
static propTypes = {
value: PropTypes.number,
onChange: PropTypes.func.isRequired,
error: PropTypes.object,
}
render() {
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
error={this.props.error}
data-wd-key="min-zoom"
>
<InputNumber
allowRange={true}
value={this.props.value}
onChange={this.props.onChange}
min={latest.layer.minzoom.minimum}
max={latest.layer.minzoom.maximum}
default={latest.layer.minzoom.minimum}
/>
</Block>
}
}
-35
View File
@@ -1,35 +0,0 @@
import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputNumber from './InputNumber'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldMinZoomInternalProps = {
value?: number
onChange(...args: unknown[]): unknown
error?: {message: string}
} & WithTranslation;
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
render() {
const t = this.props.t;
return <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
error={this.props.error}
data-wd-key="min-zoom"
>
<InputNumber
allowRange={true}
value={this.props.value}
onChange={this.props.onChange}
min={latest.layer.minzoom.minimum}
max={latest.layer.minzoom.maximum}
default={latest.layer.minzoom.minimum}
data-wd-key='min-zoom.input'
/>
</Block>
}
}
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
export default FieldMinZoom;
+21
View File
@@ -0,0 +1,21 @@
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>
}
}
-18
View File
@@ -1,18 +0,0 @@
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>
}
}
+19
View File
@@ -0,0 +1,19 @@
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>
}
}
-20
View File
@@ -1,20 +0,0 @@
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>
}
}
+20
View File
@@ -0,0 +1,20 @@
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>
}
}
-22
View File
@@ -1,22 +0,0 @@
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>
}
}
+36
View File
@@ -0,0 +1,36 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
export default class FieldSource extends React.Component {
static propTypes = {
value: PropTypes.string,
wdKey: PropTypes.string,
onChange: PropTypes.func,
sourceIds: PropTypes.array,
error: PropTypes.object,
}
static defaultProps = {
onChange: () => {},
sourceIds: [],
}
render() {
return <Block
label={"Source"}
fieldSpec={latest.layer.source}
error={this.props.error}
data-wd-key={this.props.wdKey}
>
<InputAutocomplete
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceIds.map(src => [src, src])}
/>
</Block>
}
}
-40
View File
@@ -1,40 +0,0 @@
import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldSourceInternalProps = {
value?: string
wdKey?: string
onChange?(value: string| undefined): unknown
sourceIds?: unknown[]
error?: {message: string}
} & WithTranslation;
class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
static defaultProps = {
onChange: () => {},
sourceIds: [],
}
render() {
const t = this.props.t;
return <Block
label={t("Source")}
fieldSpec={latest.layer.source}
error={this.props.error}
data-wd-key={this.props.wdKey}
>
<InputAutocomplete
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceIds?.map(src => [src, src])}
/>
</Block>
}
}
const FieldSource = withTranslation()(FieldSourceInternal);
export default FieldSource;
+34
View File
@@ -0,0 +1,34 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
export default class FieldSourceLayer extends React.Component {
static propTypes = {
value: PropTypes.string,
onChange: PropTypes.func,
sourceLayerIds: PropTypes.array,
isFixed: PropTypes.bool,
}
static defaultProps = {
onChange: () => {},
sourceLayerIds: [],
isFixed: false
}
render() {
return <Block label={"Source Layer"} fieldSpec={latest.layer['source-layer']}
data-wd-key="layer-source-layer"
>
<InputAutocomplete
keepMenuWithinWindowBounds={!!this.props.isFixed}
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceLayerIds.map(l => [l, l])}
/>
</Block>
}
}

Some files were not shown because too many files have changed in this diff Show More