Compare commits

..

4 Commits

Author SHA1 Message Date
HarelM 90f50d98e7 Fix more types, migrate two more files 2023-12-24 00:42:02 +02:00
HarelM 08f7a427f6 More componenet migration 2023-12-23 15:40:27 +02:00
HarelM 90b692158a Another one bites the dust 2023-12-23 14:58:33 +02:00
HarelM 5a62f4ab21 Improve types, migrate another file 2023-12-22 23:59:51 +02:00
262 changed files with 15524 additions and 14438 deletions
+4
View File
@@ -0,0 +1,4 @@
{
"packages": [],
"sandboxes": ["/"]
}
+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]
open_collective: maplibre
custom: "https://maputnik.github.io/donate"
+1
View File
@@ -8,3 +8,4 @@ assignees: ''
---
<!-- Thanks for reaching out! If you are having general Maputnik mapping questions, please asking them at https://gis.stackexchange.com/ using the 'maputnik' tag https://gis.stackexchange.com/questions/tagged/maputnik and read https://gis.stackexchange.com/help/how-to-ask before you do so (please keep in mind that you're asking there in a general GIS forum, not a dedicated support channel) -->
-10
View File
@@ -1,10 +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.
+5 -8
View File
@@ -1,17 +1,14 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 20
open-pull-requests-limit: 2
versioning-strategy: increase
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 2
versioning-strategy: increase
@@ -1,26 +0,0 @@
name: Automerge Dependabot
on: pull_request
permissions: write-all
jobs:
dependabot:
runs-on: ubuntu-latest
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2.4.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Approve Dependabot PRs
run: gh pr review --approve "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Enable auto-merge for Dependabot PRs
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
+100 -79
View File
@@ -7,9 +7,8 @@ on:
branches: [ main ]
jobs:
build-node:
name: "build on ${{ matrix.os }}"
build-docker:
name: build/docker
runs-on: ${{ matrix.os }}
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
@@ -17,103 +16,125 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
- run: npm ci
- run: npm run build
- run: npm run lint
- run: npm run lint-css
- uses: actions/checkout@v4
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
# build the editor
build-node:
name: "build/node@${{ matrix.node-version }} (${{ matrix.os }})"
runs-on: ${{ matrix.os }}
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18.x]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v1
with:
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 run build
build-artifacts:
name: "build artifacts"
runs-on: ubuntu-latest
name: "build/artifacts (${{ matrix.os }})"
runs-on: ${{ matrix.os }}
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
node-version: [18.x]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
- run: npm ci
- run: npm run build
- name: artifacts/maputnik
uses: actions/upload-artifact@v4
with:
name: maputnik
path: dist
- uses: actions/checkout@v4
- uses: actions/setup-node@v1
with:
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 run build
- run: npm run build-storybook
- name: artifacts/editor
uses: actions/upload-artifact@v1
with:
name: editor
path: dist
- name: artifacts/storybook
uses: actions/upload-artifact@v1
with:
name: storybook
path: build/storybook
# Build and upload desktop CLI artifacts
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
id: go
# Build and upload desktop CLI artifacts
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ^1.19.x
id: go
- name: Build desktop artifacts
run: npm run build-desktop
- name: Check out code into the Go module directory
uses: actions/checkout@v4
with:
repository: maputnik/desktop
ref: master
path: ./src/github.com/maputnik/desktop/
- name: Artifacts/linux
uses: actions/upload-artifact@v4
with:
name: maputnik-linux
path: ./desktop/bin/linux/
- name: Make
run: cd src/github.com/maputnik/desktop/ && make
- name: Artifacts/darwin
uses: actions/upload-artifact@v4
with:
name: maputnik-darwin
path: ./desktop/bin/darwin/
- name: Artifacts/linux
uses: actions/upload-artifact@v1
with:
name: maputnik-linux
path: ./src/github.com/maputnik/desktop/bin/linux/
- name: Artifacts/windows
uses: actions/upload-artifact@v4
with:
name: maputnik-windows
path: ./desktop/bin/windows/
- name: Artifacts/darwin
uses: actions/upload-artifact@v1
with:
name: maputnik-darwin
path: ./src/github.com/maputnik/desktop/bin/darwin/
e2e-tests:
name: "E2E tests using chrome"
- name: Artifacts/windows
uses: actions/upload-artifact@v1
with:
name: maputnik-windows
path: ./src/github.com/maputnik/desktop/bin/windows/
cypress-run:
strategy:
fail-fast: false
matrix:
browser: [chrome, firefox]
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- run: npm ci
- name: Cypress run
uses: cypress-io/github-action@v6
with:
build: npm run build
start: npm run start
browser: chrome
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
files: ${{ github.workspace }}/.nyc_output/out.json
verbose: true
e2e-tests-docker:
name: "E2E tests using chrome and docker"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- run: npm ci
- name: Cypress run
uses: cypress-io/github-action@v6
with:
build: docker build -t maputnik .
start: docker run --rm --network host maputnik --port=8888
browser: chrome
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
files: ${{ github.workspace }}/.nyc_output/out.json
verbose: true
browser: ${{ matrix.browser }}
-70
View File
@@ -1,70 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '17 0 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
@@ -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@v5
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v5
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@v7
with:
commit-message: Bump version to ${{ inputs.version }}
branch: bump-version-to-${{ inputs.version }}
title: Bump version to ${{ inputs.version }}
+9 -34
View File
@@ -3,49 +3,24 @@ name: deploy
on:
push:
branches: [ main ]
tags:
- 'v*'
jobs:
deploy-pages:
name: deploy/pages
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' }}
steps:
- uses: actions/checkout@v5
- name: Use Node.js from nvmrc
uses: actions/setup-node@v5
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@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: dist
# publish docker to GitHub registry
deploy-docker:
name: deploy/docker
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
if: ${{ github.event_name == 'push' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
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@v5
- run: docker build -t ghcr.io/maplibre/maputnik:main .
- run: docker push ghcr.io/maplibre/maputnik:main
- uses: actions/checkout@v4
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u orangemug --password-stdin
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
- run: docker push docker.pkg.github.com/maputnik/editor/editor:main
-101
View File
@@ -1,101 +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@v5
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v5
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@v5
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v5
with:
node-version-file: ".nvmrc"
registry-url: "https://registry.npmjs.org"
- name: Set up Go for desktop build
uses: actions/setup-go@v6
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-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 "desktop-${{ steps.package-version.outputs.current-version }}" 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: "desktop-${{ steps.package-version.outputs.current-version }}.zip"
allowUpdates: true
draft: false
prerelease: false
+1 -10
View File
@@ -14,7 +14,6 @@ lib-cov
# Coverage directory used by tools like istanbul
coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
@@ -33,14 +32,6 @@ node_modules
public
/errorShots
/old
/build
/cypress/screenshots
/dist/
/desktop/version.go
# IDE
.vscode/
.idea/
# Window metadata files
/desktop/winres/winres.json
/desktop/*.syso
-1
View File
@@ -1 +0,0 @@
legacy-peer-deps = true
-1
View File
@@ -1 +0,0 @@
22.13
-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"]
}
-24
View File
@@ -1,24 +0,0 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
ci:
autoupdate_schedule: monthly
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: check-executables-have-shebangs
- id: check-json
exclude: 'tsconfig(\.node)?\.json'
- id: check-shebang-scripts-are-executable
- id: check-symlinks
- id: check-toml
- id: check-yaml
args: [ --allow-multiple-documents ]
- id: destroyed-symlinks
- id: end-of-file-fixer
- id: mixed-line-ending
args: [ --fix=lf ]
- id: trailing-whitespace
+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',
});
-47
View File
@@ -1,47 +0,0 @@
Maputnik is a MapLibre style editor written using React and TypeScript.
To get started, install all npm packages:
```
npm install
```
Verify code correctness by running ESLint:
```
npm run lint
```
Or try fixing lint issues with:
```
npm run lint -- --fix
```
The project type checked and built with:
```
npm run build
```
To run the tests make sure that xvfb is installed:
```
apt install xvfb
```
Run the development server in the background with Vite:
```
nohup npm run start &
```
Then start the Cypress tests with:
```
xvfb-run -a npm run test
```
## Pull Requests
- Pull requests should update `CHANGELOG.md` with a short description of the change.
-54
View File
@@ -1,54 +0,0 @@
## main
### ✨ Features and improvements
- _...Add new stuff here..._
### 🐞 Bug fixes
- _...Add new stuff here..._
## 3.0.0
### ✨ Features and improvements
- Fix radio/delete filter buttons styling regression
- Add german translation
- Use same version number for web and desktop versions
- Add scheme type options for vector/raster tile
- Add `tileSize` field for raster and raster-dem tile sources
- Update Protomaps Light gallery style to v4
- Add support to edit local files on the file system if supported by the browser
- Upgrade to MapLibre LG JS v5
- Upgrade Vite 6 and Cypress 14 ([#970](https://github.com/maplibre/maputnik/pull/970))
- Upgrade OpenLayers from v6 to v10
- When loading a style into localStorage that causes a QuotaExceededError, purge localStorage and retry
- Remove react-autobind dependency
- Remove usage of legacy `childContextTypes` API
- Refactor Field components to use arrow function syntax
- Replace react-autocomplete with Downshift in the autocomplete component
- Add LocationIQ as supported map provider with access token field and gallery style
- Use maputnik go binary for the docker image to allow file watching
- Revmove support for `debug` and `localport` url parameters
- Replace react-sortable-hoc with dnd-kit to avoid react console warnings and also use a maintained library
### 🐞 Bug fixes
- Fix incorrect handing of network error response (#944)
- Show an error when adding a layer with a duplicate ID
- Replace deprecated `ReactDOM.render` usage with `createRoot` and drop the
`DOMNodeRemoved` cleanup hack
## 2.1.1
### ✨ 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)
+16 -8
View File
@@ -1,14 +1,22 @@
FROM golang:1.23-alpine AS builder
FROM node:18 as builder
WORKDIR /maputnik
RUN apk add --no-cache nodejs npm make git gcc g++ libc-dev
# Only copy package.json to prevent npm install from running on every build
COPY package.json package-lock.json ./
RUN npm install
# Build maputnik
# TODO: we should also do a npm run test here (needs more dependencies)
COPY . .
RUN npm ci
RUN CGO_ENABLED=1 GOOS=linux npm run build-linux
RUN npm run build
FROM alpine:latest
WORKDIR /app
COPY --from=builder /maputnik/desktop/bin/linux ./
ENTRYPOINT ["/app/maputnik"]
#---------------------------------------------------------------------------
# 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 -1
View File
@@ -1,7 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015 Lukas Martinelli
Copyright (c) 2024 MapLibre contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -20,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+70 -32
View File
@@ -1,10 +1,10 @@
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
# 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]
[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
A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
@@ -13,40 +13,33 @@ targeted at developers and map designers.
## Usage
- :link: Design your maps online at **<https://www.maplibre.org/maputnik/>** (all in local storage)
- :link: Use the [Maputnik CLI](https://github.com/maplibre/maputnik/wiki/Maputnik-CLI) for local style development
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
- :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.
```bash
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main
docker run -it --rm -p 8888:8888 maputnik/editor
```
To see the CLI options (for example file watching or style serving) run:
```bash
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main --help
```
You might need to mount a volume (`-v`) to be able to use these options.
## 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
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
[![Design Map from Scratch](https://j.gifs.com/g5XMgl.gif)](https://youtu.be/XoDh0gEnBQo)
## 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).
Check out our [Internationalization guide](./src/locales/README.md) for UI text related changes.
### 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/`.
```bash
@@ -74,14 +67,14 @@ Lint the JavaScript code.
```
# run linter
npm run lint
npm run lint-css
npm run sort-styles
npm run lint-styles
```
## Tests
For E2E testing we use [Cypress](https://www.cypress.io/)
[Cypress](https://www.cypress.io/) doesn't start a server so you'll need to start one manually by running `npm run start`.
[Cypress](https://www.cypress.io/) doesn't starts a server so you'll need to start one manually by running `npm run start`.
Now open a terminal and run the following using *chrome*:
@@ -101,23 +94,68 @@ You can also see the tests as they run or select which suites to run by executin
npm run cy:open
```
## Release process
1. Review [`CHANGELOG.md`](/CHANGELOG.md)
- 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.
## Related Projects
- [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
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
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and Maplibre contributors.
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by other map studios and make your own decisions for a good style editor.
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
**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');
-47
View File
@@ -1,47 +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;
const 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}`;
process.stdout.write(templatedReleaseNotes.trimEnd());
-18
View File
@@ -1,31 +1,13 @@
import { defineConfig } from "cypress";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
export default defineConfig({
env: {
codeCoverage: {
exclude: "cypress/**/*.*",
},
},
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
require("@cypress/code-coverage/task")(on, config);
return config;
},
baseUrl: "http://localhost:8888",
retries: {
runMode: 2,
openMode: 0,
},
},
component: {
devServer: {
framework: "react",
bundler: "vite",
},
},
});
+11 -11
View File
@@ -1,7 +1,7 @@
import { MaputnikDriver } from "./maputnik-driver";
import MaputnikDriver from "./driver";
describe("accessibility", () => {
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
beforeAndAfter();
describe("skip links", () => {
@@ -11,30 +11,30 @@ describe("accessibility", () => {
it("skip link to layer list", () => {
const selector = "root:skip:layer-list";
then(get.elementByTestId(selector)).shouldExist();
should.exist(selector);
when.tab();
then(get.elementByTestId(selector)).shouldBeFocused();
should.beFocused(selector);
when.click(selector);
then(get.skipTargetLayerList()).shouldBeFocused();
should.beFocused("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("skip link to layer editor", () => {
const selector = "root:skip:layer-editor";
then(get.elementByTestId(selector)).shouldExist();
should.exist(selector);
when.tab().tab();
then(get.elementByTestId(selector)).shouldBeFocused();
should.beFocused(selector);
when.click(selector);
then(get.skipTargetLayerEditor()).shouldBeFocused();
should.beFocused("skip-target-layer-editor");
});
it("skip link to map view", () => {
const selector = "root:skip:map-view";
then(get.elementByTestId(selector)).shouldExist();
should.exist(selector);
when.tab().tab().tab();
then(get.elementByTestId(selector)).shouldBeFocused();
should.beFocused(selector);
when.click(selector);
then(get.canvas()).shouldBeFocused();
should.canvasBeFocused();
});
});
});
+210
View File
@@ -0,0 +1,210 @@
import { CypressHelper } from "@shellygo/cypress-test-utils";
import { v1 as uuid } from "uuid";
export default class MaputnikDriver {
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
public beforeAndAfter = () => {
beforeEach(() => {
this.given.setupInterception();
this.when.setStyle("both");
});
};
public given = {
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/*" }, []);
},
};
public when = {
within: (selector: string, fn: () => void) => {
this.helper.when.within(fn, selector);
},
tab: () => cy.get("body").tab(),
waitForExampleFileRequset: () => {
this.helper.when.waitForResponse("example-style.json");
},
chooseExampleFile: () => {
cy.get("input[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=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");
},
fillLayersModal: (opts: {type: string, layer?: string, id?: string}) => {
var type = opts.type;
var layer = opts.layer;
var id;
if (opts.id) {
id = opts.id;
} else {
id = `${type}:${uuid()}`;
}
cy.get(
this.get.dataAttribute("add-layer.layer-type", "select")
).select(type);
cy.get(this.get.dataAttribute("add-layer.layer-id", "input")).type(id);
if (layer) {
cy.get(
this.get.dataAttribute("add-layer.layer-source-block", "input")
).type(layer);
}
this.when.click("add-layer");
return id;
},
typeKeys: (keys: string) => {
cy.get("body").type(keys);
},
click: (selector: string) => {
this.helper.when.click(selector);
},
clickZoomin: () => {
cy.get(".maplibregl-ctrl-zoom-in").click();
},
selectWithin: (selector: string, value: string) => {
this.when.within(selector, () => {
cy.get("select").select(value);
});
},
select: (selector: string, value: string) => {
this.helper.get.element(selector).select(value);
},
focus: (selector: string) => {
this.helper.when.focus(selector);
},
setValue: (selector: string, text: string) => {
cy.get(selector).clear().type(text, { parseSpecialCharSequences: false });
},
closeModal: (key: string) => {
this.helper.when.waitUntil(() => this.helper.get.element(key));
this.when.click(key + ".close-modal");
},
openLayersModal: () => {
this.helper.when.click("layer-list:add-layer");
cy.get(this.get.dataAttribute("modal:add-layer")).should("exist");
cy.get(this.get.dataAttribute("modal:add-layer")).should("be.visible");
},
};
public get = {
isMac: () => {
return Cypress.platform === "darwin";
},
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;
},
exampleFileUrl: () => {
return "http://localhost:8888/example-style.json";
},
dataAttribute: (key: string, selector?: string): string => {
return `*[data-wd-key='${key}'] ${selector || ""}`;
},
};
public should = {
canvasBeFocused: () => {
this.when.within("maplibre:map", () => {
cy.get("canvas").should("be.focused");
});
},
notExist: (selector: string) => {
cy.get(selector).should("not.exist");
},
beFocused: (selector: string) => {
this.helper.get.element(selector).should("have.focus");
},
notBeFocused: (selector: string) => {
this.helper.get.element(selector).should("not.have.focus");
},
beVisible: (selector: string) => {
this.helper.get.element(selector).should("be.visible");
},
notBeVisible: (selector: string) => {
this.helper.get.element(selector).should("not.be.visible");
},
equalStyleStore: (getter: (obj: any) => any, styleObj: any) => {
cy.window().then((win: any) => {
const obj = this.get.styleFromWindow(win);
assert.deepEqual(getter(obj), styleObj);
});
},
styleStoreEqualToExampleFileData: () => {
cy.window().then((win: any) => {
const obj = this.get.styleFromWindow(win);
cy.fixture("example-style.json").should("deep.equal", obj);
});
},
exist: (selector: string) => {
this.helper.get.element(selector).should("exist");
},
beSelected: (selector: string, value: string) => {
this.helper.get.element(selector).find(`option[value="${value}"]`).should("be.selected");
},
containText: (selector: string, text: string) => {
this.helper.get.element(selector).should("contain.text", text);
}
};
}
+35 -62
View File
@@ -1,7 +1,7 @@
import { MaputnikDriver } from "./maputnik-driver";
import MaputnikDriver from "./driver";
describe("history", () => {
const { beforeAndAfter, when, get, then } = new MaputnikDriver();
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
beforeAndAfter();
let undoKeyCombo: string;
@@ -15,29 +15,34 @@ describe("history", () => {
it("undo/redo", () => {
when.setStyle("geojson");
when.modal.open();
when.openLayersModal();
when.modal.fillLayers({
should.equalStyleStore((a: any) => a.layers, []);
when.fillLayersModal({
id: "step 1",
type: "background",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: "step 1",
type: "background",
},
],
});
]
);
when.modal.open();
when.modal.fillLayers({
when.openLayersModal();
when.fillLayersModal({
id: "step 2",
type: "background",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: "step 1",
type: "background",
@@ -46,35 +51,38 @@ describe("history", () => {
id: "step 2",
type: "background",
},
],
});
]
);
when.typeKeys(undoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: "step 1",
type: "background",
},
],
});
]
);
when.typeKeys(undoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
should.equalStyleStore((a: any) => a.layers, []);
when.typeKeys(redoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: "step 1",
type: "background",
},
],
});
]
);
when.typeKeys(redoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: "step 1",
type: "background",
@@ -83,42 +91,7 @@ describe("history", () => {
id: "step 2",
type: "background",
},
],
});
});
it("should not redo after undo and value change", () => {
when.setStyle("geojson");
when.modal.open();
when.modal.fillLayers({
id: "step 1",
type: "background",
});
when.modal.open();
when.modal.fillLayers({
id: "step 2",
type: "background",
});
when.typeKeys(undoKeyCombo);
when.typeKeys(undoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
when.modal.open();
when.modal.fillLayers({
id: "step 3",
type: "background",
});
when.typeKeys(redoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 3",
type: "background",
},
],
});
]
);
});
});
-35
View File
@@ -1,35 +0,0 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("i18n", () => {
const { 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("スタイル設定");
});
});
});
+14 -13
View File
@@ -1,60 +1,61 @@
import { MaputnikDriver } from "./maputnik-driver";
import MaputnikDriver from "./driver";
describe("keyboard", () => {
const { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
beforeAndAfter();
describe("shortcuts", () => {
beforeEach(() => {
given.setupMockBackedResponses();
given.setupInterception();
when.setStyle("");
});
it("ESC should unfocus", () => {
const targetSelector = "maputnik-select";
when.focus(targetSelector);
then(get.elementByTestId(targetSelector)).shouldBeFocused();
should.beFocused(targetSelector);
when.typeKeys("{esc}");
then(get.elementByTestId(targetSelector)).shouldNotBeFocused();
expect(should.notBeFocused(targetSelector));
});
it("'?' should show shortcuts modal", () => {
when.typeKeys("?");
then(get.elementByTestId("modal:shortcuts")).shouldBeVisible();
should.beVisible("modal:shortcuts");
});
it("'o' should show open modal", () => {
when.typeKeys("o");
then(get.elementByTestId("modal:open")).shouldBeVisible();
should.beVisible("modal:open");
});
it("'e' should show export modal", () => {
when.typeKeys("e");
then(get.elementByTestId("modal:export")).shouldBeVisible();
should.beVisible("modal:export");
});
it("'d' should show sources modal", () => {
when.typeKeys("d");
then(get.elementByTestId("modal:sources")).shouldBeVisible();
should.beVisible("modal:sources");
});
it("'s' should show settings modal", () => {
when.typeKeys("s");
then(get.elementByTestId("modal:settings")).shouldBeVisible();
should.beVisible("modal:settings");
});
it("'i' should change map to inspect mode", () => {
when.typeKeys("i");
then(get.inputValue("maputnik-select")).shouldEqual("inspect");
should.beSelected("nav:inspect", "inspect");
});
it("'m' should focus map", () => {
when.typeKeys("m");
then(get.canvas()).shouldBeFocused();
should.canvasBeFocused();
});
it("'!' should show debug modal", () => {
when.typeKeys("!");
then(get.elementByTestId("modal:debug")).shouldBeVisible();
should.beVisible("modal:debug");
});
});
});
+271 -391
View File
@@ -1,137 +1,155 @@
import { v1 as uuid } from "uuid";
import { MaputnikDriver } from "./maputnik-driver";
import MaputnikDriver from "./driver";
describe("layers", () => {
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
beforeAndAfter();
beforeEach(() => {
when.setStyle("both");
when.modal.open();
when.openLayersModal();
});
describe("ops", () => {
let id: string;
beforeEach(() => {
id = when.modal.fillLayers({
it("delete", () => {
var id = when.fillLayersModal({
type: "background",
});
});
it("should update layers in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "background",
},
],
});
]
);
when.click("layer-list-item:" + id + ":delete");
should.equalStyleStore((a: any) => a.layers, []);
});
describe("when clicking delete", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":delete");
});
it("should empty layers in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [],
});
it("duplicate", () => {
var styleObj;
var id = when.fillLayersModal({
type: "background",
});
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "background",
},
]
);
when.click("layer-list-item:" + id + ":copy");
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id + "-copy",
type: "background",
},
{
id: id,
type: "background",
},
]
);
});
describe("when clicking duplicate", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":copy");
it("hide", () => {
var styleObj;
var id = when.fillLayersModal({
type: "background",
});
it("should add copy layer in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id + "-copy",
type: "background",
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "background",
},
]
);
when.click("layer-list-item:" + id + ":toggle-visibility");
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "background",
layout: {
visibility: "none",
},
{
id: id,
type: "background",
},
]
);
when.click("layer-list-item:" + id + ":toggle-visibility");
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "background",
layout: {
visibility: "visible",
},
],
});
});
});
describe("when clicking hide", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":toggle-visibility");
});
it("should update visibility to none in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "background",
layout: {
visibility: "none",
},
},
],
});
});
describe("when clicking show", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":toggle-visibility");
});
it("should update visibility to visible in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "background",
layout: {
visibility: "visible",
},
},
],
});
});
});
},
]
);
});
});
describe("background", () => {
it("add", () => {
const id = when.modal.fillLayers({
var id = when.fillLayersModal({
type: "background",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "background",
},
],
});
]
);
});
describe("modify", () => {
function createBackground() {
// Setup
const id = uuid();
var id = uuid();
when.selectWithin("add-layer.layer-type", "background");
when.setValue("add-layer.layer-id.input", "background:" + id);
when.setValue(
get.dataAttribute("add-layer.layer-id", "input"),
"background:" + id
);
when.click("add-layer");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: "background:" + id,
type: "background",
},
],
});
]
);
return id;
}
@@ -139,166 +157,137 @@ describe("layers", () => {
describe("layer", () => {
it("expand/collapse");
it("id", () => {
const bgId = createBackground();
var bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
const id = uuid();
when.setValue("layer-editor.layer-id.input", "foobar:" + id);
var id = uuid();
when.setValue(
get.dataAttribute("layer-editor.layer-id", "input"),
"foobar:" + id
);
when.click("min-zoom");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: "foobar:" + id,
type: "background",
},
],
});
]
);
});
describe("min-zoom", () => {
let bgId: string;
it("min-zoom", () => {
var bgId = createBackground();
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.setValue("min-zoom.input-text", "1");
when.click("layer-editor.layer-id");
});
when.click("layer-list-item:background:" + bgId);
when.setValue(
get.dataAttribute("min-zoom", 'input[type="text"]'),
"1"
);
it("should update min-zoom in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
minzoom: 1,
when.click("layer-editor.layer-id");
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: "background:" + bgId,
type: "background",
minzoom: 1,
},
]
);
// AND RESET!
// driver.setValue(driver.get.dataAttribute("min-zoom", "input"), "")
// driver.click(driver.get.dataAttribute("max-zoom", "input"));
// driver.isStyleStoreEqual((a: any) => a.layers, [
// {
// "id": 'background:'+bgId,
// "type": 'background'
// }
// ]);
});
it("max-zoom", () => {
var bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.setValue(
get.dataAttribute("max-zoom", 'input[type="text"]'),
"1"
);
when.click("layer-editor.layer-id");
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: "background:" + bgId,
type: "background",
maxzoom: 1,
},
]
);
});
it("comments", () => {
var bgId = createBackground();
var id = uuid();
when.click("layer-list-item:background:" + bgId);
when.setValue(get.dataAttribute("layer-comment", "textarea"), id);
when.click("layer-editor.layer-id");
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: "background:" + bgId,
type: "background",
metadata: {
"maputnik:comment": id,
},
],
});
});
},
]
);
it("when clicking next layer should update style on local storage", () => {
when.type("min-zoom.input-text", "{backspace}");
when.click("max-zoom.input-text");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
minzoom: 1,
},
],
});
});
// Unset it again.
// TODO: This fails
// driver.setValue(driver.getDataAttribute("layer-comment", "textarea"), "");
// driver.click(driver.getDataAttribute("min-zoom", "input"));
// driver.isStyleStoreEqual((a: any) => a.layers, [
// {
// "id": 'background:'+bgId,
// "type": 'background'
// }
// ]);
});
describe("max-zoom", () => {
let bgId: string;
it("color", () => {
var bgId = createBackground();
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.setValue("max-zoom.input-text", "1");
when.click("layer-editor.layer-id");
});
when.click("layer-list-item:background:" + bgId);
it("should update style in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
maxzoom: 1,
},
],
});
});
when.click("spec-field:background-color");
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: "background:" + bgId,
type: "background",
},
]
);
});
describe("comments", () => {
let bgId: string;
const comment = "42";
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.setValue("layer-comment.input", comment);
when.click("layer-editor.layer-id");
});
it("should update style in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
metadata: {
"maputnik:comment": comment,
},
},
],
});
});
describe("when unsetting", () => {
beforeEach(() => {
when.clear("layer-comment.input");
when.click("min-zoom.input-text");
});
it("should update style in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
},
],
});
});
});
});
describe("color", () => {
let bgId: string;
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.click("spec-field:background-color");
});
it("should update style in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
},
],
});
});
});
describe("opacity", () => {
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", () => {
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0.");
});
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", () => {
@@ -320,18 +309,18 @@ describe("layers", () => {
// TODO
it.skip("parse error", () => {
const bgId = createBackground();
var bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
const errorSelector = ".CodeMirror-lint-marker-error";
then(get.elementByTestId(errorSelector)).shouldNotExist();
var errorSelector = ".CodeMirror-lint-marker-error";
should.notExist(errorSelector);
when.click(".CodeMirror");
when.typeKeys(
"\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"
);
then(get.elementByTestId(errorSelector)).shouldExist();
should.exist(errorSelector);
});
});
});
@@ -339,20 +328,21 @@ describe("layers", () => {
describe("fill", () => {
it("add", () => {
const id = when.modal.fillLayers({
var id = when.fillLayersModal({
type: "fill",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "fill",
source: "example",
},
],
});
]
);
});
// TODO: Change source
@@ -361,150 +351,106 @@ describe("layers", () => {
describe("line", () => {
it("add", () => {
const id = when.modal.fillLayers({
var id = when.fillLayersModal({
type: "line",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "line",
source: "example",
},
],
});
]
);
});
it("groups", () => {
when.modal.open();
const id1 = when.modal.fillLayers({
id: "aa",
type: "line",
layer: "example",
});
when.modal.open();
const id2 = when.modal.fillLayers({
id: "aa-2",
type: "line",
layer: "example",
});
when.modal.open();
const id3 = when.modal.fillLayers({
id: "b",
type: "line",
layer: "example",
});
then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
then(get.elementByTestId("layer-list-item:" + id2)).shouldNotBeVisible();
then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
when.click("layer-list-group:aa-0");
then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
then(get.elementByTestId("layer-list-item:" + id2)).shouldBeVisible();
then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
when.click("layer-list-item:" + id2);
when.click("skip-target-layer-editor");
when.click("menu-move-layer-down");
then(get.elementByTestId("layer-list-group:aa-0")).shouldNotExist();
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "aa",
type: "line",
source: "example",
},
{
id: "b",
type: "line",
source: "example",
},
{
id: "aa-2",
type: "line",
source: "example",
},
],
});
// TODO
// Click each of the layer groups.
});
});
describe("symbol", () => {
it("add", () => {
const id = when.modal.fillLayers({
var id = when.fillLayersModal({
type: "symbol",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "symbol",
source: "example",
},
],
});
]
);
});
});
describe("raster", () => {
it("add", () => {
const id = when.modal.fillLayers({
var id = when.fillLayersModal({
type: "raster",
layer: "raster",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "raster",
source: "raster",
},
],
});
]
);
});
});
describe("circle", () => {
it("add", () => {
const id = when.modal.fillLayers({
var id = when.fillLayersModal({
type: "circle",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "circle",
source: "example",
},
],
});
]
);
});
});
describe("fill extrusion", () => {
it("add", () => {
const id = when.modal.fillLayers({
var id = when.fillLayersModal({
type: "fill-extrusion",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
should.equalStyleStore(
(a: any) => a.layers,
[
{
id: id,
type: "fill-extrusion",
source: "example",
},
],
});
]
);
});
});
@@ -512,100 +458,34 @@ describe("layers", () => {
it("simple", () => {
when.setStyle("geojson");
when.modal.open();
when.modal.fillLayers({
when.openLayersModal();
when.fillLayersModal({
id: "foo",
type: "background",
});
when.modal.open();
when.modal.fillLayers({
when.openLayersModal();
when.fillLayersModal({
id: "foo_bar",
type: "background",
});
when.modal.open();
when.modal.fillLayers({
when.openLayersModal();
when.fillLayersModal({
id: "foo_bar_baz",
type: "background",
});
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
then(get.elementByTestId("layer-list-item:foo_bar")).shouldNotBeVisible();
then(
get.elementByTestId("layer-list-item:foo_bar_baz")
).shouldNotBeVisible();
should.beVisible("layer-list-item:foo");
should.notBeVisible("layer-list-item:foo_bar");
should.notBeVisible("layer-list-item:foo_bar_baz");
when.click("layer-list-group:foo-0");
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
then(get.elementByTestId("layer-list-item:foo_bar")).shouldBeVisible();
then(
get.elementByTestId("layer-list-item:foo_bar_baz")
).shouldBeVisible();
});
});
describe("layereditor jsonlint should error", ()=>{
it("add", () => {
const id = when.modal.fillLayers({
type: "circle",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "circle",
source: "example",
},
],
});
const sourceText = get.elementByText('"source"');
sourceText.click();
sourceText.type("\"");
const error = get.element('.CodeMirror-lint-marker-error');
error.should('exist');
});
});
describe("drag and drop", () => {
it("move layer should update local storage", () => {
when.modal.open();
const firstId = when.modal.fillLayers({
id: "a",
type: "background",
});
when.modal.open();
const secondId = when.modal.fillLayers({
id: "b",
type: "background",
});
when.modal.open();
const thirdId = when.modal.fillLayers({
id: "c",
type: "background",
});
when.dragAndDrop(get.elementByTestId("layer-list-item:" + firstId), get.elementByTestId("layer-list-item:" + thirdId));
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: secondId,
type: "background",
},
{
id: thirdId,
type: "background",
},
{
id: firstId,
type: "background",
},
],
});
should.beVisible("layer-list-item:foo");
should.beVisible("layer-list-item:foo_bar");
should.beVisible("layer-list-item:foo_bar_baz");
});
});
});
+9 -18
View File
@@ -1,32 +1,23 @@
import { MaputnikDriver } from "./maputnik-driver";
import MaputnikDriver from "./driver";
describe("map", () => {
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
beforeAndAfter();
describe("zoom level", () => {
it("via url", () => {
const zoomLevel = 12.37;
var zoomLevel = 12.37;
when.setStyle("geojson", zoomLevel);
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
"Zoom: " + zoomLevel
);
should.beVisible("maplibre:ctrl-zoom");
should.containText("maplibre:ctrl-zoom", "Zoom: " + zoomLevel);
});
it("via map controls", () => {
const zoomLevel = 12.37;
var zoomLevel = 12.37;
when.setStyle("geojson", zoomLevel);
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
when.clickZoomIn();
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
"Zoom: " + (zoomLevel + 1)
);
});
});
describe("search", () => {
it('should exist', () => {
then(get.searchControl()).shouldBeVisible();
should.beVisible("maplibre:ctrl-zoom");
when.clickZoomin();
should.containText("maplibre:ctrl-zoom", "Zoom: "+(zoomLevel + 1));
});
});
});
-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;
}
-188
View File
@@ -1,188 +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 styleItemKey = `maputnik:style:${styleId}`;
const styleItem = win.localStorage.getItem(styleItemKey);
if (!styleItem) throw new Error("Could not get styleItem from localStorage");
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,
doWithin: (selector: string, fn: () => void) => {
this.helper.when.doWithin(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
) => {
const url = new URL(baseUrl);
switch (styleProperties) {
case "geojson":
url.searchParams.set("style", baseUrl + "geojson-style.json");
break;
case "raster":
url.searchParams.set("style", baseUrl + "raster-style.json");
break;
case "both":
url.searchParams.set("style", baseUrl + "geojson-raster-style.json");
break;
case "layer":
url.searchParams.set("style", baseUrl + "example-layer-style.json");
break;
}
if (zoom) {
url.hash = `${zoom}/41.3805/2.1635`;
}
this.helper.when.visit(url.toString());
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.doWithin(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
const type = opts.type;
const 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.doWithin(() => {
this.helper.get.element("input").clear().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");
},
};
}
+61 -246
View File
@@ -1,10 +1,8 @@
import { MaputnikDriver } from "./maputnik-driver";
import tokens from "../../src/config/tokens.json" with {type: "json"};
import MaputnikDriver from "./driver";
describe("modals", () => {
const { beforeAndAfter, when, get, given, then } = new MaputnikDriver();
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
beforeAndAfter();
beforeEach(() => {
when.setStyle("");
});
@@ -14,27 +12,25 @@ describe("modals", () => {
});
it("close", () => {
when.modal.close("modal:open");
then(get.elementByTestId("modal:open")).shouldNotExist();
when.closeModal("modal:open");
should.notExist("modal:open");
});
it.skip("upload", () => {
// HM: I was not able to make the following choose file actually to select a file and close the modal...
when.chooseExampleFile();
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
should.styleStoreEqualToExampleFileData();
});
describe("when click open url", () => {
beforeEach(() => {
const styleFileUrl = get.exampleFileUrl();
it("load from url", () => {
var styleFileUrl = get.exampleFileUrl();
when.setValue("modal:open.url.input", styleFileUrl);
when.click("modal:open.url.button");
when.wait(200);
});
it("load from url", () => {
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
});
when.setValue(get.dataAttribute("modal:open.url.input"), styleFileUrl);
when.click("modal:open.url.button");
when.waitForExampleFileRequset();
should.styleStoreEqualToExampleFileData();
});
});
@@ -42,8 +38,8 @@ describe("modals", () => {
it("open/close", () => {
when.setStyle("");
when.typeKeys("?");
when.modal.close("modal:shortcuts");
then(get.elementByTestId("modal:shortcuts")).shouldNotExist();
when.closeModal("modal:shortcuts");
should.notExist("modal:shortcuts");
});
});
@@ -53,8 +49,8 @@ describe("modals", () => {
});
it("close", () => {
when.modal.close("modal:export");
then(get.elementByTestId("modal:export")).shouldNotExist();
when.closeModal("modal:export");
should.notExist("modal:export");
});
// TODO: Work out how to download a file and check the contents
@@ -62,67 +58,16 @@ describe("modals", () => {
});
describe("sources", () => {
beforeEach(() => {
when.setStyle("layer");
when.click("nav:sources");
});
it("active sources");
it("public source");
it("add new source", () => {
const sourceId = "n1z2v3r";
when.setValue("modal:sources.add.source_id", sourceId);
when.select("modal:sources.add.source_type", "tile_vector");
when.select("modal:sources.add.scheme_type", "tms");
when.click("modal:sources.add.add_source");
when.wait(200);
then(
get.styleFromLocalStorage().then((style) => style.sources[sourceId])
).shouldInclude({
scheme: "tms",
});
});
it("add new pmtiles source", () => {
const sourceId = "pmtilestest";
when.setValue("modal:sources.add.source_id", sourceId);
when.select("modal:sources.add.source_type", "pmtiles_vector");
when.setValue("modal:sources.add.source_url", "https://data.source.coop/protomaps/openstreetmap/v4.pmtiles");
when.click("modal:sources.add.add_source");
when.click("modal:sources.add.add_source");
when.wait(200);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sources: {
pmtilestest: {
type: "vector",
url: "pmtiles://https://data.source.coop/protomaps/openstreetmap/v4.pmtiles",
},
},
});
});
it("add new raster source", () => {
const sourceId = "rastertest";
when.setValue("modal:sources.add.source_id", sourceId);
when.select("modal:sources.add.source_type", "tile_raster");
when.select("modal:sources.add.scheme_type", "xyz");
when.setValue("modal:sources.add.tile_size", "128");
when.click("modal:sources.add.add_source");
when.wait(200);
then(
get.styleFromLocalStorage().then((style) => style.sources[sourceId])
).shouldInclude({
tileSize: 128,
});
});
it("add new source");
});
describe("inspect", () => {
it("toggle", () => {
// There is no assertion in this test
when.setStyle("geojson");
when.select("maputnik-select", "inspect");
when.selectWithin("nav:inspect", "inspect");
});
});
@@ -131,216 +76,86 @@ describe("modals", () => {
when.click("nav:settings");
});
describe("when click name filed spec information", () => {
beforeEach(() => {
when.click("field-doc-button-Name");
});
it("name", () => {
when.click("field-doc-button-Name");
it("should show the spec information", () => {
then(get.elementsText("spec-field-doc")).shouldInclude(
"name for the style"
);
});
should.containText("spec-field-doc", "name for the style");
});
describe("when set name and click owner", () => {
beforeEach(() => {
when.setValue("modal:settings.name", "foobar");
when.click("modal:settings.owner");
when.wait(200);
});
it("show name specifications", () => {
when.setValue(get.dataAttribute("modal:settings.name"), "foobar");
when.click("modal:settings.owner");
it("show name specifications", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
name: "foobar",
});
});
should.equalStyleStore((obj) => obj.name, "foobar");
});
describe("when set owner and click name", () => {
beforeEach(() => {
when.setValue("modal:settings.owner", "foobar");
when.click("modal:settings.name");
when.wait(200);
});
it("should update owner in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
owner: "foobar",
});
});
});
it("sprite url", () => {
when.setValue("modal:settings.sprite", "http://example.com");
it("owner", () => {
when.setValue(get.dataAttribute("modal:settings.owner"), "foobar");
when.click("modal:settings.name");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sprite: "http://example.com",
});
should.equalStyleStore((obj) => obj.owner, "foobar");
});
it("sprite url", () => {
when.setValue(
get.dataAttribute("modal:settings.sprite"),
"http://example.com"
);
when.click("modal:settings.name");
should.equalStyleStore((obj) => obj.sprite, "http://example.com");
});
it("glyphs url", () => {
const glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
when.setValue("modal:settings.glyphs", glyphsUrl);
var glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
when.setValue(get.dataAttribute("modal:settings.glyphs"), glyphsUrl);
when.click("modal:settings.name");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
glyphs: glyphsUrl,
});
should.equalStyleStore((obj) => obj.glyphs, glyphsUrl);
});
it("maptiler access token", () => {
const apiKey = "testing123";
var apiKey = "testing123";
when.setValue(
"modal:settings.maputnik:openmaptiles_access_token",
get.dataAttribute(
"modal:settings.maputnik:openmaptiles_access_token"
),
apiKey
);
when.click("modal:settings.name");
then(
get.styleFromLocalStorage().then((style) => style.metadata)
).shouldInclude({
"maputnik:openmaptiles_access_token": apiKey,
});
should.equalStyleStore(
(obj) => obj.metadata["maputnik:openmaptiles_access_token"],
apiKey
);
});
it("thunderforest access token", () => {
const apiKey = "testing123";
var apiKey = "testing123";
when.setValue(
"modal:settings.maputnik:thunderforest_access_token",
get.dataAttribute(
"modal:settings.maputnik:thunderforest_access_token"
),
apiKey
);
when.click("modal:settings.name");
then(
get.styleFromLocalStorage().then((style) => style.metadata)
).shouldInclude({ "maputnik:thunderforest_access_token": apiKey });
});
it("stadia access token", () => {
const apiKey = "testing123";
when.setValue(
"modal:settings.maputnik:stadia_access_token",
should.equalStyleStore(
(obj) => obj.metadata["maputnik:thunderforest_access_token"],
apiKey
);
when.click("modal:settings.name");
then(
get.styleFromLocalStorage().then((style) => style.metadata)
).shouldInclude({ "maputnik:stadia_access_token": apiKey });
});
it("locationiq access token", () => {
const apiKey = "testing123";
when.setValue(
"modal:settings.maputnik:locationiq_access_token",
apiKey
);
when.click("modal:settings.name");
then(
get.styleFromLocalStorage().then((style) => style.metadata)
).shouldInclude({ "maputnik:locationiq_access_token": apiKey });
});
it("style renderer", () => {
cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
when.select("modal:settings.maputnik:renderer", "ol");
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
"ol"
);
should.beSelected("modal:settings.maputnik:renderer", "ol");
when.click("modal:settings.name");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
metadata: { "maputnik:renderer": "ol" },
});
});
it("inlcude API key when change renderer", () => {
when.click("modal:settings.close-modal")
when.click("nav:open");
get.elementByAttribute('aria-label', "MapTiler Basic").should('exist').click();
when.wait(1000);
when.click("nav:settings");
when.select("modal:settings.maputnik:renderer", "mlgljs");
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
"mlgljs"
);
when.select("modal:settings.maputnik:renderer", "ol");
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
"ol"
);
given.intercept("https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=*", "tileRequest", "GET");
when.select("modal:settings.maputnik:renderer", "mlgljs");
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
"mlgljs"
);
when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
});
});
describe("add layer", () => {
beforeEach(() => {
when.setStyle("layer");
when.modal.open();
});
it("shows duplicate id error", () => {
when.setValue("add-layer.layer-id.input", "background");
when.click("add-layer");
then(get.elementByTestId("modal:add-layer")).shouldExist();
then(get.element(".maputnik-modal-error")).shouldContainText(
"Layer ID already exists"
);
should.equalStyleStore((obj) => obj.metadata["maputnik:renderer"], "ol");
});
});
describe("sources", () => {
it("toggle");
});
describe("Handle localStorage QuotaExceededError", () => {
it("handles quota exceeded error when opening style from URL", () => {
// Clear localStorage to start fresh
cy.clearLocalStorage();
// fill localStorage until we get a QuotaExceededError
cy.window().then(win => {
let chunkSize = 1000;
const chunk = new Array(chunkSize).join("x");
let index = 0;
// Keep adding until we hit the quota
while (true) {
try {
const key = `maputnik:fill-${index++}`;
win.localStorage.setItem(key, chunk);
} catch (e: any) {
// Verify it's a quota error
if (e.name === 'QuotaExceededError') {
if (chunkSize <= 1) return;
else {
chunkSize /= 2;
continue;
}
}
throw e; // Unexpected error
}
}
});
// Open the style via URL input
when.click("nav:open");
when.setValue("modal:open.url.input", get.exampleFileUrl());
when.click("modal:open.url.button");
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
then(get.styleFromLocalStorage()).shouldExist();
});
});
});
@@ -15,3 +15,4 @@
}
]
}
+10 -112
View File
@@ -1,114 +1,12 @@
{
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mlgljs",
"data": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
92,
93,
94,
95,
96,
97,
98,
99
]
},
"sources": {},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mlgljs"
},
"sources": {},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}
+1 -1
View File
@@ -31,4 +31,4 @@
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}
}
+1 -1
View File
@@ -26,4 +26,4 @@
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}
}
+1 -1
View File
@@ -15,4 +15,4 @@
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}
}
+1 -1
View File
@@ -34,4 +34,4 @@
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
// }
-12
View File
@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>
-37
View File
@@ -1,37 +0,0 @@
// ***********************************************************
// This example support/component.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
import { mount } from 'cypress/react'
// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a <reference path="./component" /> at the top of your spec.
declare global {
/* eslint-disable @typescript-eslint/no-namespace */
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}
Cypress.Commands.add('mount', mount)
// Example use:
// cy.mount(<MyComponent />)
-1
View File
@@ -14,7 +14,6 @@
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "@cypress/code-coverage/support";
import "cypress-plugin-tab";
import "./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.
-47
View File
@@ -1,47 +0,0 @@
SOURCEDIR=.
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
BINARY=maputnik
VERSION := $(shell node -p "require('../package.json').version")
GOBIN := $(or $(shell if [ -d /go/bin ]; then echo "/go/bin"; fi),$(HOME)/go/bin)
all: $(BINARY)
$(BINARY): $(GOBIN)/gox $(GOBIN)/go-winres $(SOURCES) version.go rice-box.go winres/winres.json
$(GOBIN)/go-winres make --product-version=$(VERSION)
$(GOBIN)/gox -osarch "windows/amd64 linux/amd64 darwin/amd64" -output "bin/{{.OS}}/${BINARY}"
bin/linux/$(BINARY): $(GOBIN)/gox $(GOBIN)/go-winres $(SOURCES) version.go rice-box.go winres/winres.json
$(GOBIN)/go-winres make --product-version=$(VERSION)
$(GOBIN)/gox -osarch "linux/amd64" -output "bin/{{.OS}}/${BINARY}"
winres/winres.json: winres/winres_template.json
sed 's/{{.Version}}/$(VERSION)/g' winres/winres_template.json > $@
$(GOBIN)/go-winres:
go install github.com/tc-hib/go-winres@latest
# 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 number in the executable by writing version.go
.PHONY: version.go
version.go:
@printf "// DO NOT EDIT: Autogenerated by Makefile\n" > version.go
@printf "package main\n" >> version.go
@printf "const Version = \"$(VERSION)\"\n" >> 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
-73
View File
@@ -1,73 +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 zip file containing desktop binaries for Linux, OSX and Windows from [the latest releases of **maplibre/maputnik**](https://github.com/maplibre/maputnik/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 `desktop/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=
-79
View File
@@ -1,79 +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 = Version
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)
}
-60
View File
@@ -1,60 +0,0 @@
{
"RT_GROUP_ICON": {
"APP": {
"0000": [
"../../src/img/maputnik.png"
]
}
},
"RT_MANIFEST": {
"#1": {
"0409": {
"identity": {
"name": "Maputnik",
"version": "{{.Version}}"
},
"description": "A MapLibre GL visual style editor",
"minimum-os": "win7",
"execution-level": "as invoker",
"ui-access": false,
"auto-elevate": false,
"dpi-awareness": "system",
"disable-theming": false,
"disable-window-filtering": false,
"high-resolution-scrolling-aware": false,
"ultra-high-resolution-scrolling-aware": false,
"long-path-aware": false,
"printer-driver-isolation": false,
"gdi-scaling": false,
"segment-heap": false,
"use-common-controls-v6": false
}
}
},
"RT_VERSION": {
"#1": {
"0000": {
"fixed": {
"file_version": "{{.Version}}",
"product_version": "{{.Version}}"
},
"info": {
"0409": {
"Comments": "https://github.com/maplibre/maputnik",
"CompanyName": "Maputnik",
"FileDescription": "A MapLibre GL visual style editor",
"FileVersion": "{{.Version}}",
"InternalName": "Maputnik",
"LegalCopyright": "MIT License",
"LegalTrademarks": "",
"OriginalFilename": "Maputnik.exe",
"PrivateBuild": "",
"ProductName": "Maputnik",
"ProductVersion": "{{.Version}}",
"SpecialBuild": ""
}
}
}
}
}
}
-61
View File
@@ -1,61 +0,0 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import reactRefreshPlugin from 'eslint-plugin-react-refresh';
export default tseslint.config({
extends: [
eslint.configs.recommended,
tseslint.configs.recommended,
],
files: ['**/*.{js,jsx,ts,tsx}'],
ignores: [
"dist/**/*",
],
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
globals: {
global: 'readonly'
}
},
settings: {
react: { version: '18.2' }
},
plugins: {
'react': reactPlugin,
'react-hooks': reactHooksPlugin,
'react-refresh': reactRefreshPlugin
},
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true }
],
"@typescript-eslint/no-explicit-any": "off",
'@typescript-eslint/no-unused-vars': [
'warn',
{
varsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
argsIgnorePattern: '^_'
}
],
'no-unused-vars': 'off',
'react/prop-types': 'off',
'no-undef': 'off',
'indent': ['error', 2],
'no-var': 'error',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
},
linterOptions: {
reportUnusedDisableDirectives: true,
noInlineConfig: false
}
}
)
-17
View File
@@ -1,17 +0,0 @@
export default {
output: 'src/locales/$LOCALE/$NAMESPACE.json',
locales: [ 'de', 'fr', 'he', 'it','ja', '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__';
}
}
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

+9353 -7056
View File
File diff suppressed because it is too large Load Diff
+70 -83
View File
@@ -1,53 +1,40 @@
{
"name": "maputnik",
"version": "3.0.0",
"version": "2.0.0-pre.2",
"description": "A MapLibre GL visual style editor",
"type": "module",
"main": "''",
"scripts": {
"start": "vite",
"build": "tsc && vite build --mode=production",
"build-desktop": "tsc && vite build --mode=desktop && cd desktop && make",
"build-linux": "tsc && vite build --mode=desktop && cd desktop && make bin/linux/maputnik",
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
"lint": "eslint",
"build": "tsc && vite build",
"lint": "eslint ./src --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0 && npm run lint-css",
"test": "cypress run",
"cy:open": "cypress open",
"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": {
"type": "git",
"url": "https://github.com/maplibre/maputnik"
"url": "https://github.com/maputnik/editor"
},
"author": "Lukas Martinelli",
"license": "MIT",
"homepage": "https://github.com/maplibre/maputnik#readme",
"homepage": "https://github.com/maputnik/editor#readme",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
"@maplibre/maplibre-gl-geocoder": "^1.9.0",
"@maplibre/maplibre-gl-inspect": "^1.7.1",
"@maplibre/maplibre-gl-style-spec": "^23.3.0",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@prantlf/jsonlint": "^16.0.0",
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
"@maplibre/maplibre-gl-style-spec": "^17.0.1",
"@mdi/js": "^6.6.96",
"@mdi/react": "^1.5.0",
"array-move": "^4.0.0",
"buffer": "^6.0.3",
"classnames": "^2.5.1",
"codemirror": "^5.65.20",
"color": "^5.0.0",
"classnames": "^2.3.1",
"codemirror": "^5.65.2",
"color": "^4.2.3",
"detect-browser": "^5.3.0",
"downshift": "^9.0.10",
"events": "^3.3.0",
"file-saver": "^2.0.5",
"i18next": "^25.5.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-resources-to-backend": "^1.2.1",
"json-stringify-pretty-compact": "^4.0.0",
"json-stringify-pretty-compact": "^3.0.0",
"json-to-ast": "^2.1.0",
"jsonlint": "github:josdejong/jsonlint#85a19d7",
"lodash": "^4.17.21",
"lodash.capitalize": "^4.2.1",
"lodash.clamp": "^4.0.3",
@@ -55,27 +42,30 @@
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"maplibre-gl": "^5.7.1",
"mapbox-gl-inspect": "^1.3.1",
"maplibre-gl": "^2.4.0",
"maputnik-design": "github:maputnik/design#172b06c",
"ol": "^10.6.1",
"ol-mapbox-style": "^13.1.0",
"pmtiles": "^4.3.0",
"ol": "^6.14.1",
"ol-mapbox-style": "^7.1.1",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-accessible-accordion": "^5.0.1",
"react": "^16.0.0",
"react-accessible-accordion": "^4.0.0",
"react-aria-menubutton": "^7.0.3",
"react-aria-modal": "^5.0.2",
"react-aria-modal": "^4.0.1",
"react-autobind": "^1.0.6",
"react-autocomplete": "^1.8.1",
"react-collapse": "^5.1.1",
"react-color": "^2.19.3",
"react-dom": "^18.2.0",
"react-dom": "^16.0.0",
"react-file-reader-input": "^2.0.0",
"react-i18next": "^15.7.3",
"react-icon-base": "^2.1.2",
"react-icons": "^5.5.0",
"react-icons": "^4.3.1",
"react-sortable-hoc": "^2.0.0",
"reconnecting-websocket": "^4.4.0",
"slugify": "^1.6.6",
"sass": "^1.50.0",
"slugify": "^1.6.5",
"string-hash": "^1.1.3",
"url": "^0.11.4"
"url": "^0.11.0"
},
"jshintConfig": {
"esversion": 6
@@ -95,56 +85,53 @@
}
},
"devDependencies": {
"@cypress/code-coverage": "^3.14.6",
"@eslint/js": "^9.35.0",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@rollup/plugin-replace": "^6.0.2",
"@shellygo/cypress-test-utils": "^6.0.1",
"@types/codemirror": "^5.60.16",
"@types/color": "^4.2.0",
"@types/cors": "^2.8.19",
"@rollup/plugin-replace": "^5.0.5",
"@shellygo/cypress-test-utils": "^2.0.9",
"@storybook/addon-a11y": "^7.6.5",
"@storybook/addon-actions": "^7.6.5",
"@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/codemirror": "^5.60.15",
"@types/color": "^3.0.6",
"@types/cors": "^2.8.17",
"@types/file-saver": "^2.0.7",
"@types/geojson": "^7946.0.16",
"@types/json-to-ast": "^2.1.4",
"@types/lodash.capitalize": "^4.2.9",
"@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/randomcolor": "^0.5.9",
"@types/react": "^18.2.67",
"@types/react-aria-menubutton": "^6.2.14",
"@types/react-aria-modal": "^5.0.0",
"@types/react": "^16.14.52",
"@types/react-aria-menubutton": "^6.2.13",
"@types/react-aria-modal": "^4.0.9",
"@types/react-autocomplete": "^1.8.9",
"@types/react-collapse": "^5.0.4",
"@types/react-color": "^3.0.13",
"@types/react-dom": "^18.2.22",
"@types/react-color": "^3.0.10",
"@types/react-dom": "^16.9.24",
"@types/react-file-reader-input": "^2.0.4",
"@types/react-icon-base": "^2.1.6",
"@types/string-hash": "^1.1.3",
"@types/uuid": "^10.0.0",
"@types/wicg-file-system-access": "^2023.10.6",
"@vitejs/plugin-react": "^5.0.2",
"@types/uuid": "^9.0.7",
"@vitejs/plugin-react": "^4.2.0",
"cors": "^2.8.5",
"cypress": "^15.1.0",
"cypress-plugin-tab": "^1.0.5",
"eslint": "^9.35.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"i18next-parser": "^9.3.0",
"cypress": "^13.6.1",
"eslint": "^8.53.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",
"express": "^4.17.3",
"istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.2.2",
"postcss": "^8.5.6",
"react-hot-loader": "^4.13.1",
"sass": "^1.92.1",
"stylelint": "^16.24.0",
"stylelint-config-recommended-scss": "^16.0.0",
"stylelint-scss": "^6.12.1",
"typescript": "^5.8.3",
"typescript-eslint": "^8.43.0",
"uuid": "^13.0.0",
"vite": "^7.1.5",
"vite-plugin-istanbul": "^7.1.0"
"istanbul-lib-coverage": "^3.2.0",
"mocha": "^9.2.2",
"postcss": "^8.4.12",
"react-hot-loader": "^4.13.0",
"storybook": "^7.6.5",
"stylelint": "^14.6.1",
"stylelint-config-recommended-scss": "^6.0.0",
"stylelint-scss": "^4.2.0",
"typescript": "^5.3.3",
"uuid": "^8.3.2",
"vite": "^5.0.0"
}
}
+212 -244
View File
@@ -1,3 +1,4 @@
import autoBind from 'react-autobind';
import React from 'react'
import cloneDeep from 'lodash.clonedeep'
import clamp from 'lodash.clamp'
@@ -5,16 +6,14 @@ import buffer from 'buffer'
import get from 'lodash.get'
import {unset} from 'lodash'
import {arrayMoveMutable} from 'array-move'
import url from 'url'
import hash from "string-hash";
import { PMTiles } from "pmtiles";
import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl'
import {latest, validateStyleMin} from '@maplibre/maplibre-gl-style-spec'
import MapMaplibreGl from './MapMaplibreGl'
import MapOpenLayers from './MapOpenLayers'
import LayerList from './LayerList'
import LayerEditor from './LayerEditor'
import AppToolbar, { MapState } from './AppToolbar'
import AppToolbar from './AppToolbar'
import AppLayout from './AppLayout'
import MessagePanel from './AppMessagePanel'
@@ -23,27 +22,30 @@ import ModalExport from './ModalExport'
import ModalSources from './ModalSources'
import ModalOpen from './ModalOpen'
import ModalShortcuts from './ModalShortcuts'
import ModalSurvey from './ModalSurvey'
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 { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
import { undoMessages, redoMessages } from '../libs/diffmessage'
import { createStyleStore, type IStyleStore } from '../libs/store/style-store-factory'
import { StyleStore } from '../libs/stylestore'
import { ApiStyleStore } from '../libs/apistore'
import { RevisionStore } from '../libs/revisions'
import LayerWatcher from '../libs/layerwatcher'
import tokens from '../config/tokens.json'
import isEqual from 'lodash.isequal'
import { MapOptions } from 'maplibre-gl';
import { OnStyleChangedOpts, StyleSpecificationWithId } from '../libs/definitions'
import Debug from '../libs/debug'
import {formatLayerId} from '../util/format';
// Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
window.Buffer = buffer.Buffer;
function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
function setFetchAccessToken(url, mapStyle) {
const matchesTilehosting = url.match(/\.tilehosting\.com/);
const matchesMaptiler = url.match(/\.maptiler\.com/);
const matchesThunderforest = url.match(/\.thunderforest\.com/);
const matchesLocationIQ = url.match(/\.locationiq\.com/);
if (matchesTilehosting || matchesMaptiler) {
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true})
if (accessToken) {
@@ -56,18 +58,12 @@ function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
return url.replace('{key}', accessToken)
}
}
else if (matchesLocationIQ) {
const accessToken = style.getAccessToken("locationiq", mapStyle, {allowFallback: true})
if (accessToken) {
return url.replace('{key}', accessToken)
}
}
else {
return url;
}
}
function updateRootSpec(spec: any, fieldName: string, newValues: any) {
function updateRootSpec(spec, fieldName, newValues) {
return {
...spec,
$root: {
@@ -80,107 +76,24 @@ function updateRootSpec(spec: any, fieldName: string, newValues: any) {
}
}
type MappedErrors = {
message: string
parsed?: {
type: string
data: {
index: number
key: string
message: string
}
}
}
type AppState = {
errors: MappedErrors[],
infos: string[],
mapStyle: StyleSpecificationWithId,
dirtyMapStyle?: StyleSpecification,
selectedLayerIndex: number,
selectedLayerOriginalId?: string,
sources: {[key: string]: SourceSpecification & {layers: string[]} },
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
}
fileHandle: FileSystemFileHandle | null
}
export default class App extends React.Component<any, AppState> {
revisionStore: RevisionStore;
styleStore: IStyleStore | null = null;
layerWatcher: LayerWatcher;
constructor(props: any) {
export default class App extends React.Component {
constructor(props) {
super(props)
autoBind(this);
this.revisionStore = new RevisionStore();
this.configureKeyboardShortcuts();
this.state = {
errors: [],
infos: [],
mapStyle: style.emptyStyle,
selectedLayerIndex: 0,
sources: {},
vectorLayers: {},
mapState: "map",
spec: latest,
mapView: {
zoom: 0,
center: {
lng: 0,
lat: 0,
},
},
isOpen: {
settings: false,
sources: false,
open: false,
shortcuts: false,
export: false,
debug: false,
},
maplibreGlDebugOptions: {
showTileBoundaries: false,
showCollisionBoxes: false,
showOverdrawInspector: false,
},
openlayersDebugOptions: {
debugToolbox: false,
},
fileHandle: null,
this.revisionStore = new RevisionStore()
const params = new URLSearchParams(window.location.search.substring(1))
let port = params.get("localport")
if (port == null && (window.location.port != 80 && window.location.port != 443)) {
port = window.location.port
}
this.layerWatcher = new LayerWatcher({
onVectorLayersChange: v => this.setState({ vectorLayers: v })
this.styleStore = new ApiStyleStore({
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, {save: false}),
port: port,
host: params.get("localhost")
})
}
configureKeyboardShortcuts = () => {
const shortcuts = [
{
key: "?",
@@ -223,7 +136,7 @@ export default class App extends React.Component<any, AppState> {
{
key: "m",
handler: () => {
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
document.querySelector(".maplibregl-canvas").focus();
}
},
{
@@ -236,7 +149,7 @@ export default class App extends React.Component<any, AppState> {
document.body.addEventListener("keyup", (e) => {
if(e.key === "Escape") {
(e.target as HTMLElement).blur();
e.target.blur();
document.body.focus();
}
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
@@ -246,37 +159,106 @@ export default class App extends React.Component<any, AppState> {
if(shortcut) {
this.setModal("shortcuts", false);
shortcut.handler();
shortcut.handler(e);
}
}
})
const styleUrl = initialStyleUrl()
if(styleUrl && window.confirm("Load style from URL: " + styleUrl + " and discard current changes?")) {
this.styleStore = new StyleStore()
loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle))
removeStyleQuerystring()
} else {
if(styleUrl) {
removeStyleQuerystring()
}
this.styleStore.init(err => {
if(err) {
console.log('Falling back to local storage for storing styles')
this.styleStore = new StyleStore()
}
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle, {initialLoad: true}))
if(Debug.enabled()) {
Debug.set("maputnik", "styleStore", this.styleStore);
Debug.set("maputnik", "revisionStore", this.revisionStore);
}
})
}
if(Debug.enabled()) {
Debug.set("maputnik", "revisionStore", this.revisionStore);
Debug.set("maputnik", "styleStore", this.styleStore);
}
const queryObj = url.parse(window.location.href, true).query;
this.state = {
errors: [],
infos: [],
mapStyle: style.emptyStyle,
selectedLayerIndex: 0,
sources: {},
vectorLayers: {},
mapState: "map",
spec: latest,
mapView: {
zoom: 0,
center: {
lng: 0,
lat: 0,
},
},
isOpen: {
settings: false,
sources: false,
open: false,
shortcuts: false,
export: false,
// TODO: Disabled for now, this should be opened on the Nth visit to the editor
survey: false,
debug: false,
},
maplibreGlDebugOptions: {
showTileBoundaries: false,
showCollisionBoxes: false,
showOverdrawInspector: false,
},
openlayersDebugOptions: {
debugToolbox: false,
},
}
this.layerWatcher = new LayerWatcher({
onVectorLayersChange: v => this.setState({ vectorLayers: v })
})
}
handleKeyPress = (e: KeyboardEvent) => {
handleKeyPress = (e) => {
if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
if(e.metaKey && e.shiftKey && e.keyCode === 90) {
e.preventDefault();
this.onRedo();
this.onRedo(e);
}
else if(e.metaKey && e.keyCode === 90) {
e.preventDefault();
this.onUndo();
this.onUndo(e);
}
}
else {
if(e.ctrlKey && e.keyCode === 90) {
e.preventDefault();
this.onUndo();
this.onUndo(e);
}
else if(e.ctrlKey && e.keyCode === 89) {
e.preventDefault();
this.onRedo();
this.onRedo(e);
}
}
}
async componentDidMount() {
this.styleStore = await createStyleStore((mapStyle, opts) => this.onStyleChanged(mapStyle, opts));
componentDidMount() {
window.addEventListener("keydown", this.handleKeyPress);
}
@@ -284,27 +266,27 @@ export default class App extends React.Component<any, AppState> {
window.removeEventListener("keydown", this.handleKeyPress);
}
saveStyle(snapshotStyle: StyleSpecificationWithId) {
this.styleStore?.save(snapshotStyle)
saveStyle(snapshotStyle) {
this.styleStore.save(snapshotStyle)
}
updateFonts(urlTemplate: string) {
const metadata: {[key: string]: string} = this.state.mapStyle.metadata || {} as any
updateFonts(urlTemplate) {
const metadata = this.state.mapStyle.metadata || {}
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 => {
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
})
}
updateIcons(baseUrl: string) {
updateIcons(baseUrl) {
downloadSpriteMetadata(baseUrl, 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 (
property === 'maputnik:renderer' &&
@@ -318,15 +300,14 @@ export default class App extends React.Component<any, AppState> {
const changedStyle = {
...this.state.mapStyle,
metadata: {
...(this.state.mapStyle as any).metadata,
...this.state.mapStyle.metadata,
[property]: value
}
}
this.onStyleChanged(changedStyle)
}
onStyleChanged = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}): void => {
onStyleChanged = (newStyle, opts={}) => {
opts = {
save: true,
addRevision: true,
@@ -334,36 +315,20 @@ export default class App extends React.Component<any, AppState> {
...opts,
};
// For the style object, find the urls that has "{key}" and insert the correct API keys
// Without this, going from e.g. MapTiler to OpenLayers and back will lose the maptlier key.
if (newStyle.glyphs && typeof newStyle.glyphs === 'string') {
newStyle.glyphs = setFetchAccessToken(newStyle.glyphs, newStyle);
}
if (newStyle.sprite && typeof newStyle.sprite === 'string') {
newStyle.sprite = setFetchAccessToken(newStyle.sprite, newStyle);
}
for (const [_sourceId, source] of Object.entries(newStyle.sources)) {
if (source && 'url' in source && typeof source.url === 'string') {
source.url = setFetchAccessToken(source.url, newStyle);
}
}
if (opts.initialLoad) {
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
// empty string for layer.id, manually deal with that here.
const layerErrors: (Error | ValidationError)[] = [];
const layerErrors = [];
if (newStyle && newStyle.layers) {
const foundLayers = new global.Map();
const foundLayers = new Map();
newStyle.layers.forEach((layer, index) => {
if (layer.id === "" && foundLayers.has(layer.id)) {
const message = `Duplicate layer: ${formatLayerId(layer.id)}`;
const error = new Error(
`layers[${index}]: duplicate layer id [empty_string], previously used`
);
@@ -377,7 +342,7 @@ export default class App extends React.Component<any, AppState> {
// Special case: Duplicate layer id
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
if (dupMatch) {
const [, index, message] = dupMatch;
const [matchStr, index, message] = dupMatch;
return {
message: error.message,
parsed: {
@@ -394,7 +359,7 @@ export default class App extends React.Component<any, AppState> {
// Special case: Invalid source
const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/);
if (invalidSourceMatch) {
const [, index, message] = invalidSourceMatch;
const [matchStr, index, message] = invalidSourceMatch;
return {
message: error.message,
parsed: {
@@ -410,7 +375,7 @@ export default class App extends React.Component<any, AppState> {
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
if (layerMatch) {
const [, index, group, property, message] = layerMatch;
const [matchStr, index, group, property, message] = layerMatch;
const key = (group && property) ? [group, property].join(".") : property;
return {
message: error.message,
@@ -431,31 +396,31 @@ export default class App extends React.Component<any, AppState> {
}
});
let dirtyMapStyle: StyleSpecification | undefined = undefined;
let dirtyMapStyle = undefined;
if (errors.length > 0) {
dirtyMapStyle = cloneDeep(newStyle);
for (const error of errors) {
errors.forEach(error => {
const {message} = error;
if (message) {
try {
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'
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^[]+/)![0];
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^\[]+/)[0];
unset(dirtyMapStyle, unsetPath);
}
catch (err) {
console.warn(message + " " + err);
console.warn(err);
}
}
}
});
}
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
this.updateFonts(newStyle.glyphs as string)
this.updateFonts(newStyle.glyphs)
}
if(newStyle.sprite !== this.state.mapStyle.sprite) {
this.updateIcons(newStyle.sprite as string)
this.updateIcons(newStyle.sprite)
}
if (opts.addRevision) {
@@ -495,7 +460,7 @@ export default class App extends React.Component<any, AppState> {
})
}
onMoveLayer = (move: {oldIndex: number; newIndex: number}) => {
onMoveLayer = (move) => {
let { oldIndex, newIndex } = move;
let layers = this.state.mapStyle.layers;
oldIndex = clamp(oldIndex, 0, layers.length-1);
@@ -513,7 +478,7 @@ export default class App extends React.Component<any, AppState> {
this.onLayersChange(layers);
}
onLayersChange = (changedLayers: LayerSpecification[]) => {
onLayersChange = (changedLayers) => {
const changedStyle = {
...this.state.mapStyle,
layers: changedLayers
@@ -521,15 +486,15 @@ export default class App extends React.Component<any, AppState> {
this.onStyleChanged(changedStyle)
}
onLayerDestroy = (index: number) => {
const layers = this.state.mapStyle.layers;
onLayerDestroy = (index) => {
let layers = this.state.mapStyle.layers;
const remainingLayers = layers.slice(0);
remainingLayers.splice(index, 1);
this.onLayersChange(remainingLayers);
}
onLayerCopy = (index: number) => {
const layers = this.state.mapStyle.layers;
onLayerCopy = (index) => {
let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0)
const clonedLayer = cloneDeep(changedLayers[index])
@@ -538,8 +503,8 @@ export default class App extends React.Component<any, AppState> {
this.onLayersChange(changedLayers)
}
onLayerVisibilityToggle = (index: number) => {
const layers = this.state.mapStyle.layers;
onLayerVisibilityToggle = (index) => {
let layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0)
const layer = { ...changedLayers[index] }
@@ -552,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)
changedLayers[index] = {
...changedLayers[index],
@@ -562,26 +527,26 @@ export default class App extends React.Component<any, AppState> {
this.onLayersChange(changedLayers)
}
onLayerChanged = (index: number, layer: LayerSpecification) => {
onLayerChanged = (index, layer) => {
const changedLayers = this.state.mapStyle.layers.slice(0)
changedLayers[index] = layer
this.onLayersChange(changedLayers)
}
setMapState = (newState: MapState) => {
setMapState = (newState) => {
this.setState({
mapState: newState
}, this.setStateInUrl);
}
setDefaultValues = (styleObj: StyleSpecificationWithId) => {
const metadata: {[key: string]: string} = styleObj.metadata || {} as any
setDefaultValues = (styleObj) => {
const metadata = styleObj.metadata || {}
if(metadata['maputnik:renderer'] === undefined) {
const changedStyle = {
...styleObj,
metadata: {
...styleObj.metadata as any,
...styleObj.metadata,
'maputnik:renderer': 'mlgljs'
}
}
@@ -591,62 +556,68 @@ export default class App extends React.Component<any, AppState> {
}
}
openStyle = (styleObj: StyleSpecificationWithId, fileHandle: FileSystemFileHandle | null) => {
this.setState({fileHandle: fileHandle});
openStyle = (styleObj) => {
styleObj = this.setDefaultValues(styleObj)
this.onStyleChanged(styleObj)
}
async fetchSources() {
const sourceList: {[key: string]: SourceSpecification & {layers: string[]}} = {};
for(const key of Object.keys(this.state.mapStyle.sources)) {
const source = this.state.mapStyle.sources[key];
if(source.type !== "vector" || !('url' in source)) {
sourceList[key] = this.state.sources[key] || {...this.state.mapStyle.sources[key]};
if (sourceList[key].layers === undefined) {
sourceList[key].layers = [];
}
} else {
fetchSources() {
const sourceList = {};
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
if(
!this.state.sources.hasOwnProperty(key) &&
val.type === "vector" &&
val.hasOwnProperty("url")
) {
sourceList[key] = {
type: source.type,
type: val.type,
layers: []
};
let url = source.url;
let url = val.url;
try {
url = setFetchAccessToken(url!, this.state.mapStyle)
url = setFetchAccessToken(url, this.state.mapStyle)
} catch(err) {
console.warn("Failed to setFetchAccessToken: ", err);
}
const setVectorLayers = (json:any) => {
if(!Object.prototype.hasOwnProperty.call(json, "vector_layers")) {
fetch(url, {
mode: 'cors',
})
.then(response => response.json())
.then(json => {
if(!json.hasOwnProperty("vector_layers")) {
return;
}
for(const layer of json.vector_layers) {
sourceList[key].layers.push(layer.id)
}
};
// Create new objects before setState
const sources = Object.assign({}, {
[key]: this.state.sources[key],
});
try {
if (url!.startsWith("pmtiles://")) {
const json = await (new PMTiles(url!.substring(10))).getTileJson("");
setVectorLayers(json);
} else {
const response = await fetch(url!, { mode: 'cors' });
const json = await response.json();
setVectorLayers(json);
for(let layer of json.vector_layers) {
sources[key].layers.push(layer.id)
}
} catch(err) {
console.error(`Failed to process source for url: '${url}', ${err}`);
}
console.debug("Updating source: "+key);
this.setState({
sources: sources
});
})
.catch(err => {
console.error("Failed to process sources for '%s'", url, err);
});
}
else {
sourceList[key] = this.state.sources[key] || this.state.mapStyle.sources[key];
}
}
if(!isEqual(this.state.sources, sourceList)) {
console.debug("Setting sources", sourceList);
console.debug("Setting sources");
this.setState({
sources: sourceList
})
@@ -654,17 +625,11 @@ export default class App extends React.Component<any, AppState> {
}
_getRenderer () {
const metadata: {[key:string]: string} = this.state.mapStyle.metadata || {} as any;
const metadata = this.state.mapStyle.metadata || {};
return metadata['maputnik:renderer'] || 'mlgljs';
}
onMapChange = (mapView: {
zoom: number,
center: {
lng: number,
lat: number,
},
}) => {
onMapChange = (mapView) => {
this.setState({
mapView,
});
@@ -672,15 +637,16 @@ export default class App extends React.Component<any, AppState> {
mapRenderer() {
const {mapStyle, dirtyMapStyle} = this.state;
const metadata = this.state.mapStyle.metadata || {};
const mapProps = {
mapStyle: (dirtyMapStyle || mapStyle),
replaceAccessTokens: (mapStyle: StyleSpecification) => {
replaceAccessTokens: (mapStyle) => {
return style.replaceAccessTokens(mapStyle, {
allowFallback: true
});
},
onDataChange: (e: {map: Map}) => {
onDataChange: (e) => {
this.layerWatcher.analyzeMap(e.map)
this.fetchSources();
},
@@ -699,7 +665,6 @@ export default class App extends React.Component<any, AppState> {
onLayerSelect={this.onLayerSelect}
/>
} else {
mapElement = <MapMaplibreGl {...mapProps}
onChange={this.onMapChange}
options={this.state.maplibreGlDebugOptions}
@@ -712,7 +677,7 @@ export default class App extends React.Component<any, AppState> {
if(this.state.mapState.match(/^filter-/)) {
filterName = this.state.mapState.replace(/^filter-/, "");
}
const elementStyle: {filter?: string} = {};
const elementStyle = {};
if (filterName) {
elementStyle.filter = `url('#${filterName}')`;
}
@@ -730,8 +695,8 @@ export default class App extends React.Component<any, AppState> {
url.searchParams.set("layer", `${hashVal}~${selectedLayerIndex}`);
const openModals = Object.entries(isOpen)
.map(([key, val]) => (val === true ? key : null))
.filter(val => val !== null);
.map(([key, val]) => (val === true ? key : null))
.filter(val => val !== null);
if (openModals.length > 0) {
url.searchParams.set("modal", openModals.join(","));
@@ -750,13 +715,12 @@ export default class App extends React.Component<any, AppState> {
history.replaceState({selectedLayerIndex}, "Maputnik", url.href);
}
getInitialStateFromUrl = (mapStyle: StyleSpecification) => {
getInitialStateFromUrl = (mapStyle) => {
const url = new URL(location.href);
const modalParam = url.searchParams.get("modal");
if (modalParam && modalParam !== "") {
const modals = modalParam.split(",");
const modalObj: {[key: string]: boolean} = {};
const modalObj = {};
modals.forEach(modalName => {
modalObj[modalName] = true;
});
@@ -771,7 +735,7 @@ export default class App extends React.Component<any, AppState> {
const view = url.searchParams.get("view");
if (view && view !== "") {
this.setMapState(view as MapState);
this.setMapState(view);
}
const path = url.searchParams.get("layer");
@@ -803,14 +767,18 @@ export default class App extends React.Component<any, AppState> {
}
}
onLayerSelect = (index: number) => {
onLayerSelect = (index) => {
this.setState({
selectedLayerIndex: index,
selectedLayerOriginalId: this.state.mapStyle.layers[index].id,
}, this.setStateInUrl);
}
setModal(modalName: keyof AppState["isOpen"], value: boolean) {
setModal(modalName, value) {
if(modalName === 'survey' && value === false) {
localStorage.setItem('survey', '');
}
this.setState({
isOpen: {
...this.state.isOpen,
@@ -819,15 +787,11 @@ export default class App extends React.Component<any, AppState> {
}, this.setStateInUrl)
}
toggleModal(modalName: keyof AppState["isOpen"]) {
toggleModal(modalName) {
this.setModal(modalName, !this.state.isOpen[modalName]);
}
onSetFileHandle = (fileHandle: FileSystemFileHandle | null) => {
this.setState({ fileHandle });
}
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
onChangeOpenlayersDebug = (key, value) => {
this.setState({
openlayersDebugOptions: {
...this.state.openlayersDebugOptions,
@@ -836,7 +800,7 @@ export default class App extends React.Component<any, AppState> {
});
}
onChangeMaplibreGlDebug = (key: keyof AppState["maplibreGlDebugOptions"], value: any) => {
onChangeMaplibreGlDebug = (key, value) => {
this.setState({
maplibreGlDebugOptions: {
...this.state.maplibreGlDebugOptions,
@@ -847,7 +811,8 @@ export default class App extends React.Component<any, AppState> {
render() {
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
renderer={this._getRenderer()}
@@ -890,7 +855,7 @@ export default class App extends React.Component<any, AppState> {
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
onLayerIdChange={this.onLayerIdChange}
errors={this.state.errors}
/> : undefined
/> : null
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
currentLayer={selectedLayer}
@@ -899,7 +864,7 @@ export default class App extends React.Component<any, AppState> {
mapStyle={this.state.mapStyle}
errors={this.state.errors}
infos={this.state.infos}
/> : undefined
/> : null
const modals = <div>
@@ -914,6 +879,7 @@ export default class App extends React.Component<any, AppState> {
mapView={this.state.mapView}
/>
<ModalShortcuts
ref={(el) => this.shortcutEl = el}
isOpen={this.state.isOpen.shortcuts}
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
/>
@@ -923,20 +889,18 @@ export default class App extends React.Component<any, AppState> {
onChangeMetadataProperty={this.onChangeMetadataProperty}
isOpen={this.state.isOpen.settings}
onOpenToggle={this.toggleModal.bind(this, 'settings')}
openlayersDebugOptions={this.state.openlayersDebugOptions}
/>
<ModalExport
mapStyle={this.state.mapStyle}
onStyleChanged={this.onStyleChanged}
isOpen={this.state.isOpen.export}
onOpenToggle={this.toggleModal.bind(this, 'export')}
fileHandle={this.state.fileHandle}
onSetFileHandle={this.onSetFileHandle}
/>
<ModalOpen
isOpen={this.state.isOpen.open}
onStyleOpen={this.openStyle}
onOpenToggle={this.toggleModal.bind(this, 'open')}
fileHandle={this.state.fileHandle}
/>
<ModalSources
mapStyle={this.state.mapStyle}
@@ -944,6 +908,10 @@ export default class App extends React.Component<any, AppState> {
isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')}
/>
<ModalSurvey
isOpen={this.state.isOpen.survey}
onOpenToggle={this.toggleModal.bind(this, 'survey')}
/>
</div>
return <AppLayout
+29 -28
View File
@@ -1,45 +1,46 @@
import React from 'react'
import PropTypes from 'prop-types'
import ScrollContainer from './ScrollContainer'
import { WithTranslation, withTranslation } from 'react-i18next';
import { IconContext } from 'react-icons';
type AppLayoutInternalProps = {
type AppLayoutProps = {
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> {
class AppLayout extends React.Component<AppLayoutProps> {
static childContextTypes = {
reactIconBase: PropTypes.object
}
getChildContext() {
return {
reactIconBase: { size: 14 }
}
}
render() {
document.body.dir = this.props.i18n.dir();
return <IconContext.Provider value={{size: '14px'}}>
<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">
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>
</IconContext.Provider>
}
{this.props.modals}
</div>
}
}
const AppLayout = withTranslation()(AppLayoutInternal);
export default AppLayout;
export default AppLayout
+10 -15
View File
@@ -1,24 +1,23 @@
import React from 'react'
import {formatLayerId} from '../libs/format';
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import {formatLayerId} from '../util/format';
import { StyleSpecification } from '@maplibre/maplibre-gl-style-spec';
type AppMessagePanelInternalProps = {
type AppMessagePanelProps = {
errors?: unknown[]
infos?: string[]
infos?: unknown[]
mapStyle?: StyleSpecification
onLayerSelect?(...args: unknown[]): unknown
currentLayer?: LayerSpecification
currentLayer?: object
selectedLayerIndex?: number
} & WithTranslation;
};
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
export default class AppMessagePanel extends React.Component<AppMessagePanelProps> {
static defaultProps = {
onLayerSelect: () => {},
}
render() {
const {t, selectedLayerIndex} = this.props;
const {selectedLayerIndex} = this.props;
const errors = this.props.errors?.map((error: any, idx) => {
let content;
if (error.parsed && error.parsed.type === "layer") {
@@ -26,9 +25,7 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
content = (
<>
<Trans t={t}>
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
</Trans>
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
{selectedLayerIndex !== parsed.data.index &&
<>
&nbsp;&mdash;&nbsp;
@@ -36,7 +33,7 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
className="maputnik-message-panel__switch-button"
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
>
{t("switch to layer")}
switch to layer
</button>
</>
}
@@ -62,5 +59,3 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
}
}
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
export default AppMessagePanel;
+58 -75
View File
@@ -2,21 +2,9 @@ import React from 'react'
import classnames from 'classnames'
import {detect} from 'detect-browser';
import {
MdOpenInBrowser,
MdSettings,
MdLayers,
MdHelpOutline,
MdFindInPage,
MdLanguage,
MdSave
} from 'react-icons/md'
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdAssignmentTurnedIn} from 'react-icons/md'
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';
import type { OnStyleChangedCallback } from '../libs/definitions';
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
const browser = detect();
@@ -48,13 +36,34 @@ class ToolbarLink extends React.Component<ToolbarLinkProps> {
href={this.props.href}
rel="noopener noreferrer"
target="_blank"
data-wd-key="toolbar:link"
>
{this.props.children}
</a>
}
}
type ToolbarLinkHighlightedProps = {
className?: string
children?: React.ReactNode
href?: string
onToggleModal?(...args: unknown[]): unknown
};
class ToolbarLinkHighlighted extends React.Component<ToolbarLinkHighlightedProps> {
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>
}
}
type ToolbarSelectProps = {
children?: React.ReactNode
wdKey?: string
@@ -89,24 +98,22 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
}
}
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
type AppToolbarInternalProps = {
type AppToolbarProps = {
mapStyle: object
inspectModeEnabled: boolean
onStyleChanged: OnStyleChangedCallback
onStyleChanged(...args: unknown[]): unknown
// A new style has been uploaded
onStyleOpen: OnStyleChangedCallback
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
onSetMapState(...args: unknown[]): unknown
mapState?: string
renderer?: string
} & WithTranslation;
};
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
export default class AppToolbar extends React.Component<AppToolbarProps> {
state = {
isOpen: {
settings: false,
@@ -117,14 +124,10 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
}
}
handleSelection(val: MapState) {
handleSelection(val: string | undefined) {
this.props.onSetMapState(val);
}
handleLanguageChange(val: string) {
this.props.i18n.changeLanguage(val);
}
onSkip = (target: string) => {
if (target === "map") {
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
@@ -136,41 +139,40 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
}
render() {
const t = this.props.t;
const views = [
{
id: "map",
group: "general",
title: t("Map"),
title: "Map",
},
{
id: "inspect",
group: "general",
title: t("Inspect"),
title: "Inspect",
disabled: this.props.renderer === 'ol',
},
{
id: "filter-deuteranopia",
group: "color-accessibility",
title: t("Deuteranopia filter"),
title: "Deuteranopia filter",
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-protanopia",
group: "color-accessibility",
title: t("Protanopia filter"),
title: "Protanopia filter",
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-tritanopia",
group: "color-accessibility",
title: t("Tritanopia filter"),
title: "Tritanopia filter",
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-achromatopsia",
group: "color-accessibility",
title: t("Achromatopsia filter"),
title: "Achromatopsia filter",
disabled: !colorAccessibilityFiltersEnabled,
},
];
@@ -190,29 +192,29 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("layer-list")}
>
{t("Layers list")}
Layers list
</button>
<button
data-wd-key="root:skip:layer-editor"
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("layer-editor")}
>
{t("Layer editor")}
Layer editor
</button>
<button
data-wd-key="root:skip:map-view"
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("map")}
>
{t("Map view")}
Map view
</button>
<a
className="maputnik-toolbar-logo"
target="blank"
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>
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
@@ -222,28 +224,28 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
<MdOpenInBrowser />
<IconText>{t("Open")}</IconText>
<IconText>Open</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
<MdSave />
<IconText>{t("Save")}</IconText>
<MdFileDownload />
<IconText>Export</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
<MdLayers />
<IconText>{t("Data Sources")}</IconText>
<IconText>Data Sources</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
<MdSettings />
<IconText>{t("Style Settings")}</IconText>
<IconText>Style Settings</IconText>
</ToolbarAction>
<ToolbarSelect wdKey="nav:inspect">
<MdFindInPage />
<IconText>{t("View")}
<label>View
<select
className="maputnik-select"
data-wd-key="maputnik-select"
onChange={(e) => this.handleSelection(e.target.value as MapState)}
onChange={(e) => this.handleSelection(e.target.value)}
value={currentView?.id}
>
{views.filter(v => v.group === "general").map((item) => {
@@ -253,7 +255,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
</option>
);
})}
<optgroup label={t("Color accessibility")}>
<optgroup label="Color accessibility">
{views.filter(v => v.group === "color-accessibility").map((item) => {
return (
<option key={item.id} value={item.id} disabled={item.disabled}>
@@ -263,38 +265,19 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
})}
</optgroup>
</select>
</IconText>
</label>
</ToolbarSelect>
<ToolbarSelect wdKey="nav:language">
<MdLanguage />
<IconText>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>
</IconText>
</ToolbarSelect>
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
<MdHelpOutline />
<IconText>{t("Help")}</IconText>
<IconText>Help</IconText>
</ToolbarLink>
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
<MdAssignmentTurnedIn />
<IconText>Take the Maputnik Survey</IconText>
</ToolbarLinkHighlighted>
</div>
</div>
</nav>
}
}
const AppToolbar = withTranslation()(AppToolbarInternal);
export default AppToolbar;
+4 -5
View File
@@ -1,10 +1,10 @@
import React, {PropsWithChildren, SyntheticEvent} from 'react'
import React, {SyntheticEvent} from 'react'
import classnames from 'classnames'
import FieldDocLabel from './FieldDocLabel'
import Doc from './Doc'
type BlockProps = PropsWithChildren & {
type BlockProps = {
"data-wd-key"?: string
label?: string
action?: React.ReactElement
@@ -56,9 +56,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
event.stopPropagation();
}
if (event.nativeEvent.target.nodeName !== "A") {
event.preventDefault();
}
event.preventDefault();
}
render() {
@@ -102,3 +100,4 @@ export default class Block extends React.Component<BlockProps, BlockState> {
</label>
}
}
+1
View File
@@ -31,3 +31,4 @@ export default class Collapse extends React.Component<CollapseProps> {
}
}
}
+1
View File
@@ -16,3 +16,4 @@ export default class Collapser extends React.Component<CollapserProps> {
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />
}
}
+4 -11
View File
@@ -18,8 +18,6 @@ type DocProps = {
'sdk-support'?: {
[key: string]: typeof headers
}
docUrl?: string,
docUrlLinkText?: string
}
};
@@ -27,13 +25,13 @@ export default class Doc extends React.Component<DocProps> {
render () {
const {fieldSpec} = this.props;
const {doc, values, docUrl, docUrlLinkText} = fieldSpec;
const {doc, values} = fieldSpec;
const sdkSupport = fieldSpec['sdk-support'];
const renderValues = (
!!values &&
// 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)
);
@@ -73,7 +71,7 @@ export default class Doc extends React.Component<DocProps> {
<tr key={key}>
<td>{key}</td>
{Object.keys(headers).map((k) => {
if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
if (supportObj.hasOwnProperty(k)) {
return <td key={k}>{supportObj[k as keyof typeof headers]}</td>;
}
else {
@@ -87,12 +85,7 @@ export default class Doc extends React.Component<DocProps> {
</table>
</div>
}
{docUrl && docUrlLinkText &&
<div className="SpecDoc__learn-more">
<a href={docUrl} target="_blank" rel="noreferrer">{docUrlLinkText}</a>
</div>
}
</>
);
}
}
}
+7 -7
View File
@@ -1,3 +1,4 @@
import React from 'react'
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
import Fieldset from './Fieldset'
@@ -8,12 +9,11 @@ type FieldArrayProps = InputArrayProps & {
}
};
const FieldArray: React.FC<FieldArrayProps> = (props) => {
return (
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
<InputArray {...props} />
export default class FieldArray extends React.Component<FieldArrayProps> {
render() {
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputArray {...this.props} />
</Fieldset>
);
};
}
}
export default FieldArray;
+7 -7
View File
@@ -1,3 +1,4 @@
import React from 'react'
import Block from './Block'
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
@@ -7,12 +8,11 @@ type FieldAutocompleteProps = InputAutocompleteProps & {
};
const FieldAutocomplete: React.FC<FieldAutocompleteProps> = (props) => {
return (
<Block label={props.label}>
<InputAutocomplete {...props} />
export default class FieldAutocomplete extends React.Component<FieldAutocompleteProps> {
render() {
return <Block label={this.props.label}>
<InputAutocomplete {...this.props} />
</Block>
);
};
}
}
export default FieldAutocomplete;
+7 -7
View File
@@ -1,3 +1,4 @@
import React from 'react'
import Block from './Block'
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
@@ -7,12 +8,11 @@ type FieldCheckboxProps = InputCheckboxProps & {
};
const FieldCheckbox: React.FC<FieldCheckboxProps> = (props) => {
return (
<Block label={props.label}>
<InputCheckbox {...props} />
export default class FieldCheckbox extends React.Component<FieldCheckboxProps> {
render() {
return <Block label={this.props.label}>
<InputCheckbox {...this.props} />
</Block>
);
};
}
}
export default FieldCheckbox;
+7 -7
View File
@@ -1,3 +1,4 @@
import React from 'react'
import Block from './Block'
import InputColor, {InputColorProps} from './InputColor'
@@ -10,12 +11,11 @@ type FieldColorProps = InputColorProps & {
};
const FieldColor: React.FC<FieldColorProps> = (props) => {
return (
<Block label={props.label} fieldSpec={props.fieldSpec}>
<InputColor {...props} />
export default class FieldColor extends React.Component<FieldColorProps> {
render() {
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputColor {...this.props} />
</Block>
);
};
}
}
export default FieldColor;
+15 -23
View File
@@ -2,39 +2,31 @@ import React from 'react'
import Block from './Block'
import InputString from './InputString'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldCommentInternalProps = {
type FieldCommentProps = {
value?: string
onChange(value: string | undefined): unknown
error: {message: string}
} & WithTranslation;
};
const FieldCommentInternal: React.FC<FieldCommentInternalProps> = (props) => {
const t = props.t;
const fieldSpec = {
doc: t(
"Comments for the current layer. This is non-standard and not in the spec."
),
};
export default class FieldComment extends React.Component<FieldCommentProps> {
render() {
const fieldSpec = {
doc: "Comments for the current layer. This is non-standard and not in the spec."
};
return (
<Block
label={t("Comments")}
return <Block
label={"Comments"}
fieldSpec={fieldSpec}
data-wd-key="layer-comment"
error={props.error}
error={this.props.error}
>
<InputString
multi={true}
value={props.value}
onChange={props.onChange}
default={t("Comment...")}
data-wd-key="layer-comment.input"
value={this.props.value}
onChange={this.props.onChange}
default="Comment..."
/>
</Block>
);
};
const FieldComment = withTranslation()(FieldCommentInternal);
export default FieldComment;
}
}
+41 -29
View File
@@ -2,52 +2,64 @@ import React from 'react'
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
type FieldDocLabelProps = {
label: JSX.Element | string | undefined
label: object | string | undefined
fieldSpec?: {
doc?: string
}
onToggleDoc?(...args: unknown[]): unknown
};
type FieldDocLabelState = {
open: boolean
};
const FieldDocLabel: React.FC<FieldDocLabelProps> = (props) => {
const [open, setOpen] = React.useState(false);
const onToggleDoc = (state: boolean) => {
setOpen(state);
if (props.onToggleDoc) {
props.onToggleDoc(state);
export default class FieldDocLabel extends React.Component<FieldDocLabelProps, FieldDocLabelState> {
constructor (props: FieldDocLabelProps) {
super(props);
this.state = {
open: false,
}
};
}
const { label, fieldSpec } = props;
const { doc } = fieldSpec || {};
onToggleDoc = (open: boolean) => {
this.setState({
open,
}, () => {
if (this.props.onToggleDoc) {
this.props.onToggleDoc(this.state.open);
}
});
}
if (doc) {
return (
<label className="maputnik-doc-wrapper">
render() {
const {label, fieldSpec} = this.props;
const {doc} = fieldSpec || {};
if (doc) {
return <label className="maputnik-doc-wrapper">
<div className="maputnik-doc-target">
{label}
{'\xa0'}
<button
aria-label={open ? 'close property documentation' : 'open property documentation'}
className={`maputnik-doc-button maputnik-doc-button--${open ? 'open' : 'closed'}`}
onClick={() => onToggleDoc(!open)}
data-wd-key={'field-doc-button-' + label}
aria-label={this.state.open ? "close property documentation" : "open property documentation"}
className={`maputnik-doc-button maputnik-doc-button--${this.state.open ? 'open' : 'closed'}`}
onClick={() => this.onToggleDoc(!this.state.open)}
data-wd-key={'field-doc-button-'+label}
>
{open ? <MdHighlightOff /> : <MdInfoOutline />}
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
</button>
</div>
</label>
);
} else if (label) {
return (
<label className="maputnik-doc-wrapper">
<div className="maputnik-doc-target">{label}</div>
}
else if (label) {
return <label className="maputnik-doc-wrapper">
<div className="maputnik-doc-target">
{label}
</div>
</label>
);
}
else {
<div />
}
}
return <div />;
};
export default FieldDocLabel;
}
+7 -7
View File
@@ -1,3 +1,4 @@
import React from 'react'
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
import Fieldset from './Fieldset'
@@ -5,12 +6,11 @@ type FieldDynamicArrayProps = InputDynamicArrayProps & {
name?: string
};
const FieldDynamicArray: React.FC<FieldDynamicArrayProps> = (props) => {
return (
<Fieldset label={props.label}>
<InputDynamicArray {...props} />
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
render() {
return <Fieldset label={this.props.label}>
<InputDynamicArray {...this.props} />
</Fieldset>
);
};
}
}
export default FieldDynamicArray;
+8 -9
View File
@@ -1,8 +1,9 @@
import React from 'react'
import InputEnum, {InputEnumProps} from './InputEnum'
import Fieldset from './Fieldset';
type FieldEnumProps = InputEnumProps & {
type FieldEnumProps = InputEnumProps & {
label?: string;
fieldSpec?: {
doc: string
@@ -10,12 +11,10 @@ type FieldEnumProps = InputEnumProps & {
};
const FieldEnum: React.FC<FieldEnumProps> = (props) => {
return (
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
<InputEnum {...props} />
export default class FieldEnum extends React.Component<FieldEnumProps> {
render() {
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputEnum {...this.props} />
</Fieldset>
);
};
export default FieldEnum;
}
}
+249 -220
View File
@@ -5,7 +5,7 @@ import DataProperty, { Stop } from './_DataProperty'
import ZoomProperty from './_ZoomProperty'
import ExpressionProperty from './_ExpressionProperty'
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) {
@@ -40,7 +40,7 @@ function isIdentityProperty(value: any) {
return (
typeof(value) === 'object' &&
value.type === "identity" &&
Object.prototype.hasOwnProperty.call(value, "property")
value.hasOwnProperty("property")
);
}
@@ -111,268 +111,297 @@ type FieldFunctionProps = {
value?: any
};
type FieldFunctionState = {
dataType: string
isEditing: boolean
}
/** Supports displaying spec field for zoom function objects
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
*/
const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
const [dataType, setDataType] = React.useState(
getDataType(props.value, props.fieldSpec)
);
const [isEditing, setIsEditing] = React.useState(false);
React.useEffect(() => {
if (!isEditing) {
setDataType(getDataType(props.value, props.fieldSpec));
export default class FieldFunction extends React.Component<FieldFunctionProps, FieldFunctionState> {
constructor (props: FieldFunctionProps) {
super(props);
this.state = {
dataType: getDataType(props.value, props.fieldSpec),
isEditing: false,
}
}, [props.value, props.fieldSpec, isEditing]);
}
const getFieldFunctionType = (fieldSpec: any) => {
static getDerivedStateFromProps(props: FieldFunctionProps, state: FieldFunctionState) {
// Because otherwise when editing values we end up accidentally changing field type.
if (state.isEditing) {
return {};
}
else {
return {
isEditing: false,
dataType: getDataType(props.value, props.fieldSpec)
};
}
}
getFieldFunctionType(fieldSpec: any) {
if (fieldSpec.expression.interpolated) {
return 'exponential';
return "exponential"
}
if (fieldSpec.type === 'number') {
return 'interval';
if (fieldSpec.type === "number") {
return "interval"
}
return 'categorical';
};
return "categorical"
}
const addStop = () => {
const stops = props.value.stops.slice(0);
const lastStop = stops[stops.length - 1];
if (typeof lastStop[0] === 'object') {
addStop = () => {
const stops = this.props.value.stops.slice(0)
const lastStop = stops[stops.length - 1]
if (typeof lastStop[0] === "object") {
stops.push([
{ zoom: lastStop[0].zoom + 1, value: lastStop[0].value },
lastStop[1],
]);
} else {
stops.push([lastStop[0] + 1, lastStop[1]]);
{zoom: lastStop[0].zoom + 1, value: lastStop[0].value},
lastStop[1]
])
}
else {
stops.push([lastStop[0] + 1, lastStop[1]])
}
const changedValue = {
...props.value,
...this.props.value,
stops: stops,
};
props.onChange(props.fieldName, changedValue);
};
const deleteExpression = () => {
const { fieldSpec, fieldName } = props;
props.onChange(fieldName, fieldSpec.default);
setDataType('value');
};
const deleteStop = (stopIdx: number) => {
const stops = props.value.stops.slice(0);
stops.splice(stopIdx, 1);
let changedValue: any = {
...props.value,
stops: stops,
};
if (stops.length === 1) {
changedValue = stops[0][1];
}
props.onChange(props.fieldName, changedValue);
};
this.props.onChange(this.props.fieldName, changedValue)
}
const makeZoomFunction = () => {
const { value } = props;
deleteExpression = () => {
const {fieldSpec, fieldName} = this.props;
this.props.onChange(fieldName, fieldSpec.default);
this.setState({
dataType: "value",
});
}
let zoomFunc: any;
if (typeof value === 'object') {
deleteStop = (stopIdx: number) => {
const stops = this.props.value.stops.slice(0)
stops.splice(stopIdx, 1)
let changedValue = {
...this.props.value,
stops: stops,
}
if(stops.length === 1) {
changedValue = stops[0][1]
}
this.props.onChange(this.props.fieldName, changedValue)
}
makeZoomFunction = () => {
const {value} = this.props;
let zoomFunc;
if (typeof(value) === "object") {
if (value.stops) {
zoomFunc = {
base: value.base,
stops: value.stops.map((stop: Stop) => {
return [stop[0].zoom, stop[1] || findDefaultFromSpec(props.fieldSpec)];
}),
};
} else {
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
})
}
}
else {
zoomFunc = {
base: value.base,
stops: [
[6, findDefaultFromSpec(props.fieldSpec)],
[10, findDefaultFromSpec(props.fieldSpec)],
],
};
[6, findDefaultFromSpec(this.props.fieldSpec)],
[10, findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
} else {
}
else {
zoomFunc = {
stops: [
[6, value || findDefaultFromSpec(props.fieldSpec)],
[10, value || findDefaultFromSpec(props.fieldSpec)],
],
};
[6, value || findDefaultFromSpec(this.props.fieldSpec)],
[10, value || findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
props.onChange(props.fieldName, zoomFunc);
};
this.props.onChange(this.props.fieldName, zoomFunc)
}
const undoExpression = () => {
const { value, fieldName } = props;
undoExpression = () => {
const {value, fieldName} = this.props;
if (isGetExpression(value)) {
props.onChange(fieldName, {
type: 'identity',
property: value[1],
this.props.onChange(fieldName, {
"type": "identity",
"property": value[1]
});
this.setState({
dataType: "value",
});
setDataType('value');
} else if (isLiteralExpression(value)) {
props.onChange(fieldName, value[1]);
setDataType('value');
}
};
else if (isLiteralExpression(value)) {
this.props.onChange(fieldName, value[1]);
this.setState({
dataType: "value",
});
}
}
const canUndo = () => {
const { value, fieldSpec } = props;
canUndo = () => {
const {value, fieldSpec} = this.props;
return (
isGetExpression(value) ||
isLiteralExpression(value) ||
isPrimative(value) ||
(Array.isArray(value) && fieldSpec.type === 'array')
);
};
const makeExpression = () => {
const { value, fieldSpec } = props;
let expression;
if (typeof value === 'object' && 'stops' in value) {
expression = styleFunction.convertFunction(value, fieldSpec);
} else if (isIdentityProperty(value)) {
expression = ['get', value.property];
} else {
expression = ['literal', value || props.fieldSpec.default];
}
props.onChange(props.fieldName, expression);
};
const makeDataFunction = () => {
const functionType = getFieldFunctionType(props.fieldSpec);
const stopValue = functionType === 'categorical' ? '' : 0;
const { value } = props;
let dataFunc;
if (typeof value === 'object') {
if (value.stops) {
dataFunc = {
property: '',
type: functionType,
base: value.base,
stops: value.stops.map((stop: Stop) => {
return [{ zoom: stop[0], value: stopValue }, stop[1] || findDefaultFromSpec(props.fieldSpec)];
}),
};
} else {
dataFunc = {
property: '',
type: functionType,
base: value.base,
stops: [
[{ zoom: 6, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
[{ zoom: 10, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
],
};
}
} else {
dataFunc = {
property: '',
type: functionType,
base: value.base,
stops: [
[{ zoom: 6, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
[{ zoom: 10, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
],
};
}
props.onChange(props.fieldName, dataFunc);
};
const onMarkEditing = () => {
setIsEditing(true);
};
const onUnmarkEditing = () => {
setIsEditing(false);
};
const propClass =
props.fieldSpec.default === props.value ? 'maputnik-default-property' : 'maputnik-modified-property';
let specField;
if (dataType === 'expression') {
specField = (
<ExpressionProperty
errors={props.errors}
onChange={props.onChange.bind(null, props.fieldName)}
canUndo={canUndo}
onUndo={undoExpression}
onDelete={deleteExpression}
fieldType={props.fieldType}
fieldName={props.fieldName}
fieldSpec={props.fieldSpec}
value={props.value}
onFocus={onMarkEditing}
onBlur={onUnmarkEditing}
/>
);
} else if (dataType === 'zoom_function') {
specField = (
<ZoomProperty
errors={props.errors}
onChange={props.onChange.bind(null)}
fieldType={props.fieldType}
fieldName={props.fieldName}
fieldSpec={props.fieldSpec}
value={props.value}
onDeleteStop={deleteStop}
onAddStop={addStop}
onChangeToDataFunction={makeDataFunction}
onExpressionClick={makeExpression}
/>
);
} else if (dataType === 'data_function') {
specField = (
<DataProperty
errors={props.errors}
onChange={props.onChange.bind(null)}
fieldType={props.fieldType}
fieldName={props.fieldName}
fieldSpec={props.fieldSpec}
value={props.value}
onDeleteStop={deleteStop}
onAddStop={addStop}
onChangeToZoomFunction={makeZoomFunction}
onExpressionClick={makeExpression}
/>
);
} else {
specField = (
<SpecProperty
errors={props.errors}
onChange={props.onChange.bind(null)}
fieldType={props.fieldType}
fieldName={props.fieldName}
fieldSpec={props.fieldSpec}
value={props.value}
onZoomClick={makeZoomFunction}
onDataClick={makeDataFunction}
onExpressionClick={makeExpression}
/>
(Array.isArray(value) && fieldSpec.type === "array")
);
}
return (
<div className={propClass} data-wd-key={'spec-field-container:' + props.fieldName}>
makeExpression = () => {
const {value, fieldSpec} = this.props;
let expression;
if (typeof(value) === "object" && 'stops' in value) {
expression = styleFunction.convertFunction(value, fieldSpec);
}
else if (isIdentityProperty(value)) {
expression = ["get", value.property];
}
else {
expression = ["literal", value || this.props.fieldSpec.default];
}
this.props.onChange(this.props.fieldName, expression);
}
makeDataFunction = () => {
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
const stopValue = functionType === 'categorical' ? '' : 0;
const {value} = this.props;
let dataFunc;
if (typeof(value) === "object") {
if (value.stops) {
dataFunc = {
property: "",
type: functionType,
base: value.base,
stops: value.stops.map((stop: Stop) => {
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
})
}
}
else {
dataFunc = {
property: "",
type: functionType,
base: value.base,
stops: [
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)],
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
}
else {
dataFunc = {
property: "",
type: functionType,
base: value.base,
stops: [
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
this.props.onChange(this.props.fieldName, dataFunc)
}
onMarkEditing = () => {
this.setState({isEditing: true});
}
onUnmarkEditing = () => {
this.setState({isEditing: false});
}
render() {
const {dataType} = this.state;
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
let specField;
if (dataType === "expression") {
specField = (
<ExpressionProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this, this.props.fieldName)}
canUndo={this.canUndo}
onUndo={this.undoExpression}
onDelete={this.deleteExpression}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onFocus={this.onMarkEditing}
onBlur={this.onUnmarkEditing}
/>
);
}
else if (dataType === "zoom_function") {
specField = (
<ZoomProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this)}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onDeleteStop={this.deleteStop}
onAddStop={this.addStop}
onChangeToDataFunction={this.makeDataFunction}
onExpressionClick={this.makeExpression}
/>
)
}
else if (dataType === "data_function") {
// TODO: Rename to FieldFunction **this file** shouldn't be called that
specField = (
<DataProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this)}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onDeleteStop={this.deleteStop}
onAddStop={this.addStop}
onChangeToZoomFunction={this.makeZoomFunction}
onExpressionClick={this.makeExpression}
/>
)
}
else {
specField = (
<SpecProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this)}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onZoomClick={this.makeZoomFunction}
onDataClick={this.makeDataFunction}
onExpressionClick={this.makeExpression}
/>
)
}
return <div className={propClass} data-wd-key={"spec-field:"+this.props.fieldName}>
{specField}
</div>
);
};
}
}
export default FieldFunction;
+11 -13
View File
@@ -1,5 +1,6 @@
import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputString from './InputString'
@@ -10,19 +11,16 @@ type FieldIdProps = {
error?: {message: string}
};
const FieldId: React.FC<FieldIdProps> = (props) => {
return (
<Block label="ID" fieldSpec={latest.layer.id}
data-wd-key={props.wdKey}
error={props.error}
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={props.value}
onInput={props.onChange}
data-wd-key={props.wdKey + ".input"}
value={this.props.value}
onInput={this.props.onChange}
/>
</Block>
);
};
export default FieldId;
}
}
+6 -4
View File
@@ -1,11 +1,13 @@
import React from 'react'
import InputJson, {InputJsonProps} from './InputJson'
type FieldJsonProps = InputJsonProps & {};
const FieldJson: React.FC<FieldJsonProps> = (props) => {
return <InputJson {...props} />;
};
export default class FieldJson extends React.Component<FieldJsonProps> {
render() {
return <InputJson {...this.props} />
}
}
export default FieldJson;
+11 -17
View File
@@ -1,35 +1,29 @@
import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputNumber from './InputNumber'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldMaxZoomInternalProps = {
type FieldMaxZoomProps = {
value?: number
onChange(value: number | undefined): unknown
error?: {message: string}
} & WithTranslation;
};
const FieldMaxZoomInternal: React.FC<FieldMaxZoomInternalProps> = (props) => {
const t = props.t;
return (
<Block label={t('Max Zoom')} fieldSpec={latest.layer.maxzoom}
error={props.error}
export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
render() {
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
error={this.props.error}
data-wd-key="max-zoom"
>
<InputNumber
allowRange={true}
value={props.value}
onChange={props.onChange}
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;
}
}
+11 -17
View File
@@ -1,35 +1,29 @@
import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputNumber from './InputNumber'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldMinZoomInternalProps = {
type FieldMinZoomProps = {
value?: number
onChange(...args: unknown[]): unknown
error?: {message: string}
} & WithTranslation;
};
const FieldMinZoomInternal: React.FC<FieldMinZoomInternalProps> = (props) => {
const t = props.t;
return (
<Block label={t('Min Zoom')} fieldSpec={latest.layer.minzoom}
error={props.error}
export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
render() {
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
error={this.props.error}
data-wd-key="min-zoom"
>
<InputNumber
allowRange={true}
value={props.value}
onChange={props.onChange}
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;
}
}
+7 -7
View File
@@ -1,3 +1,4 @@
import React from 'react'
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
import Fieldset from './Fieldset'
@@ -7,12 +8,11 @@ type FieldMultiInputProps = InputMultiInputProps & {
};
const FieldMultiInput: React.FC<FieldMultiInputProps> = (props) => {
return (
<Fieldset label={props.label}>
<InputMultiInput {...props} />
export default class FieldMultiInput extends React.Component<FieldMultiInputProps> {
render() {
return <Fieldset label={this.props.label}>
<InputMultiInput {...this.props} />
</Fieldset>
);
};
}
}
export default FieldMultiInput;
+7 -8
View File
@@ -1,3 +1,4 @@
import React from 'react'
import InputNumber, {InputNumberProps} from './InputNumber'
import Block from './Block'
@@ -10,12 +11,10 @@ type FieldNumberProps = InputNumberProps & {
};
const FieldNumber: React.FC<FieldNumberProps> = (props) => {
return (
<Block label={props.label} fieldSpec={props.fieldSpec}>
<InputNumber {...props} />
export default class FieldNumber extends React.Component<FieldNumberProps> {
render() {
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputNumber {...this.props} />
</Block>
);
};
export default FieldNumber;
}
}
+8 -7
View File
@@ -1,3 +1,4 @@
import React from 'react'
import Block from './Block'
import InputSelect, {InputSelectProps} from './InputSelect'
@@ -10,12 +11,12 @@ type FieldSelectProps = InputSelectProps & {
};
const FieldSelect: React.FC<FieldSelectProps> = (props) => {
return (
<Block label={props.label} fieldSpec={props.fieldSpec}>
<InputSelect {...props} />
export default class FieldSelect extends React.Component<FieldSelectProps> {
render() {
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputSelect {...this.props}/>
</Block>
);
};
}
}
export default FieldSelect;
+22 -29
View File
@@ -1,42 +1,35 @@
import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldSourceInternalProps = {
type FieldSourceProps = {
value?: string
wdKey?: string
onChange?(value: string| undefined): unknown
sourceIds?: unknown[]
error?: {message: string}
} & WithTranslation;
const FieldSourceInternal: React.FC<FieldSourceInternalProps> = ({
onChange = () => {},
sourceIds = [],
wdKey,
value,
error,
t
}) => {
return (
<Block
label={t('Source')}
fieldSpec={latest.layer.source}
error={error}
data-wd-key={wdKey}
>
<InputAutocomplete
value={value}
onChange={onChange}
options={sourceIds?.map((src) => [src, src])}
/>
</Block>
);
};
export default class FieldSource extends React.Component<FieldSourceProps> {
static defaultProps = {
onChange: () => {},
sourceIds: [],
}
const FieldSource = withTranslation()(FieldSourceInternal);
export default FieldSource;
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>
}
}
+25 -27
View File
@@ -3,37 +3,35 @@ import React from 'react'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldSourceLayerInternalProps = {
type FieldSourceLayerProps = {
value?: string
onChange?(...args: unknown[]): unknown
sourceLayerIds?: unknown[]
isFixed?: boolean
error?: {message: string}
} & WithTranslation;
const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = ({
onChange = () => {},
sourceLayerIds = [],
value,
error,
t
}) => {
return (
<Block
label={t('Source Layer')}
fieldSpec={latest.layer['source-layer']}
data-wd-key="layer-source-layer"
error={error}
>
<InputAutocomplete
value={value}
onChange={onChange}
options={sourceLayerIds?.map((l) => [l, l])}
/>
</Block>
);
};
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
export default FieldSourceLayer;
export default class FieldSourceLayer extends React.Component<FieldSourceLayerProps> {
static defaultProps = {
onChange: () => {},
sourceLayerIds: [],
isFixed: false
}
render() {
return <Block
label={"Source Layer"}
fieldSpec={latest.layer['source-layer']}
data-wd-key="layer-source-layer"
error={this.props.error}
>
<InputAutocomplete
keepMenuWithinWindowBounds={!!this.props.isFixed}
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceLayerIds?.map(l => [l, l])}
/>
</Block>
}
}
+7 -8
View File
@@ -1,3 +1,4 @@
import React from 'react'
import Block from './Block'
import InputString, {InputStringProps} from './InputString'
@@ -9,12 +10,10 @@ type FieldStringProps = InputStringProps & {
}
};
const FieldString: React.FC<FieldStringProps> = (props) => {
return (
<Block label={props.label} fieldSpec={props.fieldSpec}>
<InputString {...props} />
export default class FieldString extends React.Component<FieldStringProps> {
render() {
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputString {...this.props} />
</Block>
);
};
export default FieldString;
}
}
+39 -35
View File
@@ -1,47 +1,51 @@
import React from 'react'
import {v8} from '@maplibre/maplibre-gl-style-spec'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputSelect from './InputSelect'
import InputString from './InputString'
import { WithTranslation, withTranslation } from 'react-i18next';
import { startCase } from 'lodash'
type FieldTypeInternalProps = {
type FieldTypeProps = {
value: string
wdKey?: string
onChange(value: string): unknown
error?: {message: string}
disabled?: boolean
} & WithTranslation;
const FieldTypeInternal: React.FC<FieldTypeInternalProps> = ({
t,
value,
wdKey,
onChange,
error,
disabled = false
}) => {
const layerstypes: [string, string][] = Object.keys(v8.layer.type.values || {}).map(v => [v, startCase(v.replace(/-/g, ' '))]);
return (
<Block label={t('Type')} fieldSpec={v8.layer.type}
data-wd-key={wdKey}
error={error}
>
{disabled && (
<InputString value={value} disabled={true} />
)}
{!disabled && (
<InputSelect
options={layerstypes}
onChange={onChange}
value={value}
data-wd-key={wdKey + '.select'}
/>
)}
</Block>
);
};
const FieldType = withTranslation()(FieldTypeInternal);
export default FieldType;
export default class FieldType extends React.Component<FieldTypeProps> {
static defaultProps = {
disabled: false,
}
render() {
return <Block label={"Type"} fieldSpec={latest.layer.type}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
{this.props.disabled &&
<InputString
value={this.props.value}
disabled={true}
/>
}
{!this.props.disabled &&
<InputSelect
options={[
['background', 'Background'],
['fill', 'Fill'],
['line', 'Line'],
['symbol', 'Symbol'],
['raster', 'Raster'],
['circle', 'Circle'],
['fill-extrusion', 'Fill Extrusion'],
['hillshade', 'Hillshade'],
['heatmap', 'Heatmap'],
]}
onChange={this.props.onChange}
value={this.props.value}
/>
}
</Block>
}
}
+10 -8
View File
@@ -1,3 +1,4 @@
import React from 'react'
import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl'
import Block from './Block'
@@ -10,12 +11,13 @@ type FieldUrlProps = InputUrlProps & {
};
const FieldUrl: React.FC<FieldUrlProps> = (props) => {
return (
<Block label={props.label} fieldSpec={props.fieldSpec}>
<InputUrl {...props} />
</Block>
);
};
export default class FieldUrl extends React.Component<FieldUrlProps> {
render () {
return (
<Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputUrl {...this.props} />
</Block>
);
}
}
export default FieldUrl;

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