mirror of
https://github.com/maputnik/editor.git
synced 2026-07-09 15:37:28 +00:00
Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd2497332d | |||
| 27dfe9026b | |||
| 087d35d0e0 | |||
| 4b705df0f0 | |||
| f971a00d0b | |||
| 2b5f5da7be | |||
| 75a046e3d0 | |||
| ac8a3ee050 | |||
| 8e1a419909 | |||
| 2c71412e49 | |||
| bc7f27d15a | |||
| b3812fa927 | |||
| b6f569ad96 | |||
| 9ebd6d84d9 | |||
| 6010c6810a | |||
| 7943252400 | |||
| 72b9e624a7 | |||
| 4c847faaae | |||
| 7089ca1b5c | |||
| 2cd6019cb2 | |||
| 775b5f4a39 | |||
| 93baf3d5e2 | |||
| 74f44a49af | |||
| 6310d6f11e | |||
| 8153050d38 | |||
| 917da6bc7d | |||
| 5f0ea7be24 | |||
| 8db5842e85 | |||
| 5a8effb363 | |||
| 236058e95b | |||
| 2da67c7963 | |||
| f67704c545 | |||
| 83b96e8f80 | |||
| 59cf0fc47c | |||
| bc8f9fd685 | |||
| 542e7777d8 | |||
| 12c5dd912b | |||
| 95dbed3f69 | |||
| 3acf7ccf5b | |||
| 6c4758c89c | |||
| 9bfea8f868 | |||
| 2d2a4739f1 | |||
| fd218db356 | |||
| bc2b08ed92 | |||
| 9af15e5359 | |||
| 669781ccca | |||
| 18da95d2a6 | |||
| b47d105e1f | |||
| 5f9c21cf2b | |||
| 956d24c524 | |||
| 4e02c6e12d | |||
| e9966e5a20 | |||
| 3deb491306 | |||
| 34572dc3f0 | |||
| 6bf79c2121 | |||
| a925995f89 | |||
| c674575fbc | |||
| b030a2a707 | |||
| 74aa3b48db | |||
| a399df0adc | |||
| 1282062b32 | |||
| bb243db63c | |||
| 45c1281490 | |||
| 40e452d547 | |||
| 2e62b1802a | |||
| ea4c3f4e3e | |||
| 19c538a29e | |||
| d379d462f2 | |||
| 8eb9fe062f | |||
| 9ca274805c | |||
| fc507c7e79 | |||
| 66453a46ca | |||
| 96b0c53fd2 | |||
| 663034b749 | |||
| c82696d268 | |||
| 7d987cf68b | |||
| 075437555a | |||
| 654dc9c31b | |||
| 2c8bc5aa04 | |||
| 07bee66764 | |||
| f675c7ff7b | |||
| ad85fd8f12 | |||
| 634d664e46 | |||
| 0046122c87 | |||
| c0f798a6f6 | |||
| 47941e3738 | |||
| 237457a159 | |||
| d9dad5614e | |||
| f18a594131 | |||
| fde3d8fc18 | |||
| a493d6df52 | |||
| 7e8eca6f97 | |||
| c3764b65d9 | |||
| 56e151329d | |||
| 28d6589928 | |||
| 7adf516383 | |||
| 9a1385823e | |||
| 2fc5ab4509 | |||
| 3108b88e59 | |||
| b910e4fdb6 | |||
| 907c09a927 | |||
| 16fb99d9b1 | |||
| a364176a3e | |||
| 48164f5a9d | |||
| 046b1b3bb2 | |||
| f33e09df62 | |||
| 7333eb6378 | |||
| ffdc04b3aa | |||
| 223809dda5 | |||
| 744ad0f917 | |||
| 2f324c695b | |||
| dd4cbb1b3d | |||
| e61aa393c6 | |||
| e9c24d5ac9 | |||
| a7ed7cdb45 | |||
| dea98ad7b6 | |||
| 987c3cd31e | |||
| 6d970fe73f | |||
| 9cfd0ced73 | |||
| 1464a337e3 | |||
| 1eaba084ed | |||
| 37ba7457d5 | |||
| 43a7c058fd | |||
| 87c7c7ff93 | |||
| 8c8241b13b | |||
| c187f02c27 | |||
| 617adcdc48 | |||
| e71c49e38c | |||
| 9572eefd48 | |||
| 3303a25737 | |||
| 49f91a69f1 | |||
| c853c754b1 | |||
| 1b5596052c | |||
| 1c1b5cd208 | |||
| c948814efc | |||
| 7b9d3512c6 | |||
| edf3a58ea6 | |||
| 5a4b5fb9e9 | |||
| 6f9f53add6 | |||
| e0199c9ce7 | |||
| e1ed42f16f | |||
| 104c3c0c10 | |||
| f4a1aa4729 | |||
| fda7fac260 |
@@ -1,45 +0,0 @@
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile
|
||||
|
||||
#
|
||||
#
|
||||
# COPIED FROM .gitignore , please keep it in sync
|
||||
#
|
||||
#
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
# Ignore build files
|
||||
public
|
||||
/errorShots
|
||||
/old
|
||||
/build
|
||||
@@ -1,14 +0,0 @@
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{js,jsx,html,sass}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
@@ -1,2 +0,0 @@
|
||||
github: [maplibre]
|
||||
open_collective: maplibre
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve Maputnik
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Thanks for your feedback! Please complete the following information: -->
|
||||
|
||||
**Maputnik version**:<!-- e.g v1.7.0, main -->
|
||||
**Browser**:
|
||||
**OS**:<!-- (Windows, macOS, Linux) -->
|
||||
|
||||
**Description of the bug**:
|
||||
|
||||
**Steps to reproduce the behavior**:
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
**Style file or style URL**:
|
||||
<!-- If applicable, attach a style file (zip) or provide a style URL. -->
|
||||
|
||||
**Screenshots**:
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
name: Other issue
|
||||
about: Feature request or other issue which is no bug report
|
||||
title: ''
|
||||
labels: ''
|
||||
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) -->
|
||||
@@ -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.
|
||||
@@ -1,39 +0,0 @@
|
||||
# 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
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 20
|
||||
versioning-strategy: increase
|
||||
groups:
|
||||
vitest:
|
||||
patterns:
|
||||
- "*vitest*"
|
||||
react:
|
||||
patterns:
|
||||
- "*react*"
|
||||
cooldown:
|
||||
default-days: 5
|
||||
semver-major-days: 5
|
||||
semver-minor-days: 3
|
||||
semver-patch-days: 3
|
||||
include:
|
||||
- "*"
|
||||
exclude:
|
||||
- "@maplibre/*"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
cooldown:
|
||||
default-days: 3
|
||||
# no semver support for github-actions
|
||||
# => no specific configuration for this
|
||||
include:
|
||||
- "*"
|
||||
@@ -1,26 +0,0 @@
|
||||
name: Automerge Dependabot
|
||||
|
||||
on: pull_request
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.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}}
|
||||
@@ -1,166 +0,0 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
|
||||
build-node:
|
||||
name: "build on ${{ matrix.os }}"
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: read
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with: { persist-credentials: false }
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run lint
|
||||
- run: npm run lint-css
|
||||
|
||||
|
||||
|
||||
build-artifacts:
|
||||
name: "build artifacts"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with: { persist-credentials: false }
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- name: artifacts/maputnik
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: maputnik
|
||||
path: dist
|
||||
|
||||
# Build and upload desktop CLI artifacts
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
with:
|
||||
go-version: ^1.23.x
|
||||
cache-dependency-path: desktop/go.sum
|
||||
id: go
|
||||
|
||||
- name: Build desktop artifacts
|
||||
run: npm run build-desktop
|
||||
|
||||
- name: Artifacts/linux
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: maputnik-linux
|
||||
path: ./desktop/bin/linux/
|
||||
|
||||
- name: Artifacts/darwin
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: maputnik-darwin
|
||||
path: ./desktop/bin/darwin/
|
||||
|
||||
- name: Artifacts/windows
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: maputnik-windows
|
||||
path: ./desktop/bin/windows/
|
||||
|
||||
unit-tests:
|
||||
name: "Unit tests"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with: { persist-credentials: false }
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- run: npx playwright install --with-deps chromium
|
||||
- run: npm run test-unit-ci
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
files: ${{ github.workspace }}/coverage/coverage-final.json
|
||||
verbose: true
|
||||
|
||||
e2e-tests:
|
||||
name: "E2E tests using chrome"
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with: { persist-credentials: false }
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- run: npx playwright install --with-deps chromium
|
||||
- name: Playwright run
|
||||
run: npm run test-e2e
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
files: ${{ github.workspace }}/coverage/coverage-final.json
|
||||
verbose: true
|
||||
- name: Upload Playwright report
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
e2e-tests-docker:
|
||||
name: "E2E tests using chrome and docker"
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with: { persist-credentials: false }
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- run: npx playwright install --with-deps chromium
|
||||
- name: Build docker image
|
||||
run: docker build -t maputnik .
|
||||
- name: Start maputnik container
|
||||
run: docker run -d --network host --name maputnik maputnik --port=8888
|
||||
- name: Wait for maputnik to be ready
|
||||
run: |
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sSf http://localhost:8888/ > /dev/null; then
|
||||
echo "maputnik is up"; exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo "maputnik did not start in time"; docker logs maputnik; exit 1
|
||||
- name: Playwright run
|
||||
run: npm run test-e2e
|
||||
env:
|
||||
E2E_NO_WEBSERVER: "1"
|
||||
@@ -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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
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@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
|
||||
# ℹ️ 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@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
@@ -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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
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@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
commit-message: Bump version to ${{ inputs.version }}
|
||||
branch: bump-version-to-${{ inputs.version }}
|
||||
title: Bump version to ${{ inputs.version }}
|
||||
@@ -1,55 +0,0 @@
|
||||
name: deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
deploy-pages:
|
||||
name: deploy/pages
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with: { persist-credentials: false }
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
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@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: dist
|
||||
|
||||
# publish docker to GitHub registry
|
||||
deploy-docker:
|
||||
name: deploy/docker
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
||||
- run: docker push ghcr.io/maplibre/maputnik:main
|
||||
@@ -1,104 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release-check:
|
||||
name: Check if version changed
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Check if version has been updated
|
||||
id: check
|
||||
uses: EndBug/version-check@095362f3cd50f690c8fa0e6afeea81834bd8d320 # latest
|
||||
|
||||
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Set up Go for desktop build
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
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@3cf273023a0dda27efcd3164bdfb51908dd46a5b # 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@a22cf08638b34d5badda920f9daf6e72c477b07b # 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@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
|
||||
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
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
# Ignore build files
|
||||
public
|
||||
/errorShots
|
||||
/old
|
||||
/dist/
|
||||
/desktop/version.go
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Window metadata files
|
||||
/desktop/winres/winres.json
|
||||
/desktop/*.syso
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"all": true,
|
||||
"extends": "@istanbuljs/nyc-config-typescript",
|
||||
"check-coverage": false,
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": [
|
||||
"e2e/**/*.*",
|
||||
"**/*.d.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*.test.tsx",
|
||||
"**/*.browser.test.tsx",
|
||||
"./coverage/**",
|
||||
"./e2e/**",
|
||||
"./dist/**",
|
||||
"node_modules"
|
||||
],
|
||||
"report-dir": "coverage",
|
||||
"reporter": ["json", "lcov", "json-summary"]
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: monthly
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-json
|
||||
exclude: 'tsconfig(\.node)?\.json'
|
||||
- id: check-shebang-scripts-are-executable
|
||||
- id: check-symlinks
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
args: [ --allow-multiple-documents ]
|
||||
- id: destroyed-symlinks
|
||||
- id: end-of-file-fixer
|
||||
- id: mixed-line-ending
|
||||
args: [ --fix=lf ]
|
||||
- id: trailing-whitespace
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"labels": {
|
||||
"bug": 5,
|
||||
"maintenance": 3,
|
||||
"mentioned in the 1st survey": 2
|
||||
},
|
||||
"reactions": {
|
||||
"+1": 2,
|
||||
"-1": -1,
|
||||
"laugh": 1,
|
||||
"hooray": 2,
|
||||
"confused": 1,
|
||||
"heart": 2
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
|
||||
Install the Playwright browser (first time only):
|
||||
|
||||
```
|
||||
npx playwright install --with-deps chromium
|
||||
```
|
||||
|
||||
Then run the end-to-end tests (Playwright starts the dev server automatically):
|
||||
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
|
||||
Run the unit and component tests with Vitest:
|
||||
|
||||
```
|
||||
npm run test-unit
|
||||
```
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- Pull requests should update `CHANGELOG.md` with a short description of the change.
|
||||
@@ -1,85 +0,0 @@
|
||||
## main
|
||||
|
||||
### ✨ Features and improvements
|
||||
- Replace Cypress with Playwright for end-to-end tests and Vitest browser mode (Playwright provider) for component tests; drop the `@shellygo/cypress-test-utils` helper in favour of a `MaputnikDriver` page object
|
||||
- _...Add new stuff here..._
|
||||
|
||||
### 🐞 Bug fixes
|
||||
- _...Add new stuff here..._
|
||||
|
||||
## 3.1.0
|
||||
|
||||
### ✨ Features and improvements
|
||||
- Added translation to "Links" in debug modal
|
||||
- Add support for hillshade's color arrays and relief-color elevation expression
|
||||
- Change layers icons to make them a bit more distinct
|
||||
- Remove `@mdi` packages in favor of `react-icons`
|
||||
- Add ability to control the projection of the map - either globe or mercator
|
||||
- Add markdown support for doc related to the style-spec fields
|
||||
- Added global state modal to allow editing the global state
|
||||
- Added color highlight for problematic properties
|
||||
- Upgraded codemirror from version 5 to version 6
|
||||
- Add code editor to allow editing the entire style
|
||||
- Add support for sprite object in setting modal
|
||||
- Set the correct map view when opening a new style on an empty map
|
||||
- Allow root-relative urls in the stylefile
|
||||
|
||||
### 🐞 Bug fixes
|
||||
|
||||
- Improved the local style open modal and local file upload.
|
||||
- Fixed the Expression editor (for long expressions) being able to be float under other components further down
|
||||
- Fixed an issue when clicking on a popup and then clicking on the map again
|
||||
- Fix modal close button position
|
||||
- Fixed an issue with the generation of translations
|
||||
- Fix missing spec info when clicking next to a property
|
||||
- Fix Firefox open file that stopped working due to react upgrade
|
||||
- Fix issue with missing bottom error panel
|
||||
- Fixed headers in left panes (Layers list and Layer editor) to remain visible when scrolling
|
||||
- Fix error when using a source from localhost
|
||||
- Fix an issue with scrolling when using the code editor
|
||||
|
||||
## 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
|
||||
@@ -1,2 +0,0 @@
|
||||
# Contributor Covenant
|
||||
[](https://github.com/maplibre/maplibre/blob/main/CODE_OF_CONDUCT.md)
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
FROM golang:1.23-alpine AS builder
|
||||
WORKDIR /maputnik
|
||||
|
||||
RUN apk add --no-cache nodejs npm make git gcc g++ libc-dev
|
||||
|
||||
# Build maputnik
|
||||
COPY . .
|
||||
RUN npm ci
|
||||
RUN CGO_ENABLED=1 GOOS=linux npm run build-linux
|
||||
|
||||
FROM alpine:latest
|
||||
WORKDIR /app
|
||||
COPY --from=builder /maputnik/desktop/bin/linux ./
|
||||
ENTRYPOINT ["/app/maputnik"]
|
||||
@@ -1,22 +0,0 @@
|
||||
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
|
||||
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.
|
||||
@@ -1,142 +0,0 @@
|
||||
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
|
||||
|
||||
# Maputnik
|
||||
[][github-action-ci]
|
||||
[][license]
|
||||
|
||||
[github-action-ci]: https://github.com/maplibre/maputnik/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/)
|
||||
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
|
||||
- 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
|
||||
```
|
||||
|
||||
To see the CLI options (for example file watching or style serving) run:
|
||||
```bash
|
||||
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main --help
|
||||
```
|
||||
You might need to mount a volume (`-v`) to be able to use these options.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation can be found in the [Wiki](https://github.com/maplibre/maputnik/wiki). You are welcome to collaborate!
|
||||
|
||||
- :link: **Study the [Maputnik Wiki](https://github.com/maplibre/maputnik/wiki)**
|
||||
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
|
||||
|
||||
[](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/).
|
||||
|
||||
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
|
||||
# install dependencies
|
||||
npm install
|
||||
# start dev server
|
||||
npm run start
|
||||
```
|
||||
|
||||
If you want Maputnik to be accessible externally use the [`--host` option](https://vitejs.dev/config/server-options.html#server-host):
|
||||
|
||||
```bash
|
||||
# start externally accessible dev server
|
||||
npm run start -- --host 0.0.0.0
|
||||
```
|
||||
|
||||
The build process will watch for changes to the filesystem, rebuild and autoreload the editor.
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
Lint the JavaScript code.
|
||||
|
||||
```
|
||||
# run linter
|
||||
npm run lint
|
||||
npm run lint-css
|
||||
npm run sort-styles
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
### End-to-end tests
|
||||
|
||||
For E2E testing we use [Playwright](https://playwright.dev/). The tests live in the [`e2e`](/e2e) directory and drive the app through the `MaputnikDriver` page object.
|
||||
|
||||
The first time you run the tests, install the browser:
|
||||
|
||||
```
|
||||
npx playwright install chromium
|
||||
```
|
||||
|
||||
Playwright automatically starts the dev server (`npm run start`) for you, so you can just run:
|
||||
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
|
||||
Some useful options:
|
||||
|
||||
```
|
||||
# see the tests run in a headed browser
|
||||
npm run test -- --headed
|
||||
|
||||
# run a single spec / filter by title
|
||||
npm run test -- e2e/map.spec.ts
|
||||
npm run test -- -g "zoom level"
|
||||
|
||||
# open the interactive UI mode
|
||||
npx playwright test --ui
|
||||
```
|
||||
|
||||
Running the E2E tests also produces a code-coverage report in `coverage/` (collected via istanbul instrumentation of the dev server).
|
||||
|
||||
### Unit & component tests
|
||||
|
||||
Unit tests and component tests run with [Vitest](https://vitest.dev/); component tests (`*.browser.test.tsx`) use Vitest's browser mode with the Playwright provider.
|
||||
|
||||
```
|
||||
npm run test-unit
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
## 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/.
|
||||
|
||||
## 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.
|
||||
@@ -1,2 +0,0 @@
|
||||
For an up-to-date policy refer to
|
||||
https://github.com/maplibre/maplibre/blob/main/SECURITY_POLICY.txt
|
||||
Executable → Regular
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
const e={"Drag and drop a style JSON file here or click to browse":"Drag and drop a style JSON file here or click to browse"};export{e as default};
|
||||
//# sourceMappingURL=translation-BGNHuhFm.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"translation-BGNHuhFm.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"translation-By0N8Bv5.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"translation-C9ujdbWm.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"translation-CZCLjgpE.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"translation-CcF6vdbg.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"translation-CniVGPpK.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"translation-DErdfNS6.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"translation-a6aNEnxv.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"translation-xtrlBNbA.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||
@@ -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
|
||||
@@ -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");
|
||||
@@ -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());
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -1,73 +0,0 @@
|
||||
# Maputnik Desktop [][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.
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { test, describe, beforeEach } from "./utils/fixtures";
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("accessibility", () => {
|
||||
const { given, get, when, then } = new MaputnikDriver();
|
||||
|
||||
beforeEach(async () => {
|
||||
await given.setupMockBackedResponses();
|
||||
await when.setStyle("both");
|
||||
});
|
||||
|
||||
describe("skip links", () => {
|
||||
beforeEach(async () => {
|
||||
await when.setStyle("layer");
|
||||
});
|
||||
|
||||
test("skip link to layer list", async () => {
|
||||
const selector = "root:skip:layer-list";
|
||||
await then(get.elementByTestId(selector)).shouldExist();
|
||||
await when.tab();
|
||||
await then(get.elementByTestId(selector)).shouldBeFocused();
|
||||
await when.click(selector);
|
||||
await then(get.skipTargetLayerList()).shouldBeFocused();
|
||||
});
|
||||
|
||||
test("skip link to layer editor", async () => {
|
||||
const selector = "root:skip:layer-editor";
|
||||
await then(get.elementByTestId(selector)).shouldExist();
|
||||
await then(get.elementByTestId("skip-target-layer-editor")).shouldExist();
|
||||
await when.tab();
|
||||
await when.tab();
|
||||
await then(get.elementByTestId(selector)).shouldBeFocused();
|
||||
await when.click(selector);
|
||||
await then(get.skipTargetLayerEditor()).shouldBeFocused();
|
||||
});
|
||||
|
||||
test("skip link to map view", async () => {
|
||||
const selector = "root:skip:map-view";
|
||||
await then(get.elementByTestId(selector)).shouldExist();
|
||||
await when.tab();
|
||||
await when.tab();
|
||||
await when.tab();
|
||||
await then(get.elementByTestId(selector)).shouldBeFocused();
|
||||
await when.click(selector);
|
||||
await then(get.canvas()).shouldBeFocused();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import { beforeEach, describe, test } from "./utils/fixtures";
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("code editor", () => {
|
||||
const { given, get, when, then } = new MaputnikDriver();
|
||||
|
||||
beforeEach(async () => {
|
||||
await given.setupMockBackedResponses();
|
||||
await when.setStyle("both");
|
||||
});
|
||||
|
||||
test("open code editor", async () => {
|
||||
await when.click("nav:code-editor");
|
||||
await then(get.element(".maputnik-code-editor")).shouldExist();
|
||||
});
|
||||
|
||||
test("closes code editor", async () => {
|
||||
await when.click("nav:code-editor");
|
||||
await then(get.element(".maputnik-code-editor")).shouldExist();
|
||||
await when.click("nav:code-editor");
|
||||
await then(get.element(".maputnik-code-editor")).shouldNotExist();
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"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": [
|
||||
{
|
||||
"id": "background",
|
||||
"type": "background"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {
|
||||
"example": {
|
||||
"type": "geojson",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features":[{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Dinagat Islands"
|
||||
},
|
||||
"geometry":{
|
||||
"type": "Point",
|
||||
"coordinates": [125.6, 10.1]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"glyphs": "https://www.glyph-server.com/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": [
|
||||
{
|
||||
"id": "label",
|
||||
"type": "symbol",
|
||||
"source": "example",
|
||||
"layout": {
|
||||
"text-font": ["Font"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"center": [50,50],
|
||||
"zoom": 5,
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"sources": {
|
||||
"rectangles": {
|
||||
"type": "geojson",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"center": [0,51],
|
||||
"zoom": 7,
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"sources": {
|
||||
"rectangles": {
|
||||
"type": "geojson",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"layers": []
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {
|
||||
"example": {
|
||||
"type": "vector",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features":[{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Dinagat Islands"
|
||||
},
|
||||
"geometry":{
|
||||
"type": "Point",
|
||||
"coordinates": [125.6, 10.1]
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"raster": {
|
||||
"tileSize": 256,
|
||||
"tiles": ["http://localhost/example/{x}/{y}/{z}"],
|
||||
"type": "raster"
|
||||
}
|
||||
},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {
|
||||
"example": {
|
||||
"type": "vector",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features":[{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Dinagat Islands"
|
||||
},
|
||||
"geometry":{
|
||||
"type": "Point",
|
||||
"coordinates": [125.6, 10.1]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {
|
||||
"raster": {
|
||||
"tileSize": 256,
|
||||
"tiles": ["http://localhost/example/{x}/{y}/{z}"],
|
||||
"type": "raster"
|
||||
}
|
||||
},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
{
|
||||
"version": 8,
|
||||
"sources": {
|
||||
"rectangles": {
|
||||
"type": "geojson",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[
|
||||
-130.78125,
|
||||
-33.13755119234615
|
||||
],
|
||||
[
|
||||
-130.78125,
|
||||
63.548552232036414
|
||||
],
|
||||
[
|
||||
15.468749999999998,
|
||||
63.548552232036414
|
||||
],
|
||||
[
|
||||
15.468749999999998,
|
||||
-33.13755119234615
|
||||
],
|
||||
[
|
||||
-130.78125,
|
||||
-33.13755119234615
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[
|
||||
-48.515625,
|
||||
-54.97761367069625
|
||||
],
|
||||
[
|
||||
-48.515625,
|
||||
36.5978891330702
|
||||
],
|
||||
[
|
||||
169.45312499999997,
|
||||
36.5978891330702
|
||||
],
|
||||
[
|
||||
169.45312499999997,
|
||||
-54.97761367069625
|
||||
],
|
||||
[
|
||||
-48.515625,
|
||||
-54.97761367069625
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "background",
|
||||
"type": "background",
|
||||
"paint": {
|
||||
"background-color": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rectangles",
|
||||
"type": "fill",
|
||||
"source": "rectangles",
|
||||
"paint": {
|
||||
"fill-opacity": 0.3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { beforeEach, describe, test } from "./utils/fixtures";
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("history", () => {
|
||||
const { given, get, when, then } = new MaputnikDriver();
|
||||
|
||||
const undoKeyCombo = process.platform === "darwin" ? "{meta}z" : "{ctrl}z";
|
||||
const redoKeyCombo = process.platform === "darwin" ? "{meta}{shift}z" : "{ctrl}y";
|
||||
|
||||
beforeEach(async () => {
|
||||
await given.setupMockBackedResponses();
|
||||
await when.setStyle("both");
|
||||
});
|
||||
|
||||
test("undo/redo", async () => {
|
||||
await when.setStyle("geojson");
|
||||
await when.modal.open();
|
||||
|
||||
await when.modal.fillLayers({
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
});
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id: "step 1", type: "background" }],
|
||||
});
|
||||
|
||||
await when.modal.open();
|
||||
await when.modal.fillLayers({
|
||||
id: "step 2",
|
||||
type: "background",
|
||||
});
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{ id: "step 1", type: "background" },
|
||||
{ id: "step 2", type: "background" },
|
||||
],
|
||||
});
|
||||
|
||||
await when.typeKeys(undoKeyCombo);
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id: "step 1", type: "background" }],
|
||||
});
|
||||
|
||||
await when.typeKeys(undoKeyCombo);
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
||||
|
||||
await when.typeKeys(redoKeyCombo);
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id: "step 1", type: "background" }],
|
||||
});
|
||||
|
||||
await when.typeKeys(redoKeyCombo);
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{ id: "step 1", type: "background" },
|
||||
{ id: "step 2", type: "background" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("should not redo after undo and value change", async () => {
|
||||
await when.setStyle("geojson");
|
||||
await when.modal.open();
|
||||
await when.modal.fillLayers({
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
await when.modal.open();
|
||||
await when.modal.fillLayers({
|
||||
id: "step 2",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
await when.typeKeys(undoKeyCombo);
|
||||
await when.typeKeys(undoKeyCombo);
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
||||
|
||||
await when.modal.open();
|
||||
await when.modal.fillLayers({
|
||||
id: "step 3",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
await when.typeKeys(redoKeyCombo);
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id: "step 3", type: "background" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
import { beforeEach, describe, test } from "./utils/fixtures";
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("i18n", () => {
|
||||
const { given, get, when, then } = new MaputnikDriver();
|
||||
|
||||
beforeEach(async () => {
|
||||
await given.setupMockBackedResponses();
|
||||
await when.setStyle("both");
|
||||
});
|
||||
|
||||
describe("language detector", () => {
|
||||
test("English", async () => {
|
||||
await when.visit("?lng=en");
|
||||
await then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
|
||||
});
|
||||
|
||||
test("Japanese", async () => {
|
||||
await when.visit("?lng=ja");
|
||||
await then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
|
||||
});
|
||||
});
|
||||
|
||||
describe("language switcher", () => {
|
||||
beforeEach(async () => {
|
||||
await when.setStyle("layer");
|
||||
});
|
||||
|
||||
test("the language switcher switches to Japanese", async () => {
|
||||
const selector = "maputnik-lang-select";
|
||||
await then(get.elementByTestId(selector)).shouldExist();
|
||||
await when.select(selector, "ja");
|
||||
await then(get.elementByTestId(selector)).shouldHaveValue("ja");
|
||||
|
||||
await then(get.elementByTestId("nav:settings")).shouldHaveText("スタイル設定");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,66 +0,0 @@
|
||||
import { beforeEach, describe, test } from "./utils/fixtures";
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("keyboard", () => {
|
||||
const { given, get, when, then } = new MaputnikDriver();
|
||||
|
||||
beforeEach(async () => {
|
||||
await given.setupMockBackedResponses();
|
||||
await when.setStyle("both");
|
||||
});
|
||||
|
||||
describe("shortcuts", () => {
|
||||
beforeEach(async () => {
|
||||
await given.setupMockBackedResponses();
|
||||
await when.setStyle("");
|
||||
});
|
||||
|
||||
test("ESC should unfocus", async () => {
|
||||
const targetSelector = "maputnik-select";
|
||||
await when.focus(targetSelector);
|
||||
await then(get.elementByTestId(targetSelector)).shouldBeFocused();
|
||||
await when.typeKeys("{esc}");
|
||||
await then(get.elementByTestId(targetSelector)).shouldNotBeFocused();
|
||||
});
|
||||
|
||||
test("'?' should show shortcuts modal", async () => {
|
||||
await when.typeKeys("?");
|
||||
await then(get.elementByTestId("modal:shortcuts")).shouldBeVisible();
|
||||
});
|
||||
|
||||
test("'o' should show open modal", async () => {
|
||||
await when.typeKeys("o");
|
||||
await then(get.elementByTestId("modal:open")).shouldBeVisible();
|
||||
});
|
||||
|
||||
test("'e' should show export modal", async () => {
|
||||
await when.typeKeys("e");
|
||||
await then(get.elementByTestId("modal:export")).shouldBeVisible();
|
||||
});
|
||||
|
||||
test("'d' should show sources modal", async () => {
|
||||
await when.typeKeys("d");
|
||||
await then(get.elementByTestId("modal:sources")).shouldBeVisible();
|
||||
});
|
||||
|
||||
test("'s' should show settings modal", async () => {
|
||||
await when.typeKeys("s");
|
||||
await then(get.elementByTestId("modal:settings")).shouldBeVisible();
|
||||
});
|
||||
|
||||
test("'i' should change map to inspect mode", async () => {
|
||||
await when.typeKeys("i");
|
||||
await then(get.inputValue("maputnik-select")).shouldEqual("inspect");
|
||||
});
|
||||
|
||||
test("'m' should focus map", async () => {
|
||||
await when.typeKeys("m");
|
||||
await then(get.canvas()).shouldBeFocused();
|
||||
});
|
||||
|
||||
test("'!' should show debug modal", async () => {
|
||||
await when.typeKeys("!");
|
||||
await then(get.elementByTestId("modal:debug")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,246 +0,0 @@
|
||||
import { v1 as uuid } from "uuid";
|
||||
import { beforeEach, describe, test } from "./utils/fixtures";
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("layer editor", () => {
|
||||
const { given, get, when, then } = new MaputnikDriver();
|
||||
|
||||
beforeEach(async () => {
|
||||
await given.setupMockBackedResponses();
|
||||
await when.setStyle("both");
|
||||
await when.modal.open();
|
||||
});
|
||||
|
||||
async function createBackground() {
|
||||
const id = uuid();
|
||||
|
||||
await when.selectWithin("add-layer.layer-type", "background");
|
||||
await when.setValue("add-layer.layer-id.input", "background:" + id);
|
||||
|
||||
await when.click("add-layer");
|
||||
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id: "background:" + id, type: "background" }],
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
test.skip("expand/collapse", () => {});
|
||||
|
||||
test("id", async () => {
|
||||
const bgId = await createBackground();
|
||||
|
||||
await when.click("layer-list-item:background:" + bgId);
|
||||
|
||||
const id = uuid();
|
||||
await when.setValue("layer-editor.layer-id.input", "foobar:" + id);
|
||||
await when.click("min-zoom");
|
||||
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id: "foobar:" + id, type: "background" }],
|
||||
});
|
||||
});
|
||||
|
||||
describe("source", () => {
|
||||
test("should show error when the source is invalid", async () => {
|
||||
await when.modal.fillLayers({
|
||||
type: "circle",
|
||||
layer: "invalid",
|
||||
});
|
||||
await then(
|
||||
get.element(".maputnik-input-block--error .maputnik-input-block-label")
|
||||
).shouldHaveCss("color", "rgb(207, 74, 74)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("min-zoom", () => {
|
||||
let bgId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
bgId = await createBackground();
|
||||
await when.click("layer-list-item:background:" + bgId);
|
||||
await when.setValue("min-zoom.input-text", "1");
|
||||
await when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
test("should update min-zoom in local storage", async () => {
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id: "background:" + bgId, type: "background", minzoom: 1 }],
|
||||
});
|
||||
});
|
||||
|
||||
test("when clicking next layer should update style on local storage", async () => {
|
||||
await when.type("min-zoom.input-text", "{backspace}");
|
||||
await when.click("max-zoom.input-text");
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id: "background:" + bgId, type: "background", minzoom: 1 }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("max-zoom", () => {
|
||||
let bgId: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
bgId = await createBackground();
|
||||
await when.click("layer-list-item:background:" + bgId);
|
||||
await when.setValue("max-zoom.input-text", "1");
|
||||
await when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
test("should update style in local storage", async () => {
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id: "background:" + bgId, type: "background", maxzoom: 1 }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("comments", () => {
|
||||
let bgId: string;
|
||||
const comment = "42";
|
||||
|
||||
beforeEach(async () => {
|
||||
bgId = await createBackground();
|
||||
await when.click("layer-list-item:background:" + bgId);
|
||||
await when.setValue("layer-comment.input", comment);
|
||||
await when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
test("should update style in local storage", async () => {
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
metadata: { "maputnik:comment": comment },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("when unsetting", () => {
|
||||
beforeEach(async () => {
|
||||
await when.clear("layer-comment.input");
|
||||
await when.click("min-zoom.input-text");
|
||||
});
|
||||
|
||||
test("should update style in local storage", async () => {
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id: "background:" + bgId, type: "background" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("color", () => {
|
||||
let bgId: string;
|
||||
beforeEach(async () => {
|
||||
bgId = await createBackground();
|
||||
await when.click("layer-list-item:background:" + bgId);
|
||||
await when.click("spec-field:background-color");
|
||||
});
|
||||
|
||||
test("should update style in local storage", async () => {
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id: "background:" + bgId, type: "background" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("opacity", () => {
|
||||
let bgId: string;
|
||||
beforeEach(async () => {
|
||||
bgId = await createBackground();
|
||||
await when.click("layer-list-item:background:" + bgId);
|
||||
await when.type("spec-field-input:background-opacity", "0.");
|
||||
});
|
||||
|
||||
test("should keep '.' in the input field", async () => {
|
||||
await then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0.");
|
||||
});
|
||||
|
||||
test("should revert to a valid value when focus out", async () => {
|
||||
await when.click("layer-list-item:background:" + bgId);
|
||||
await then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("filter", () => {
|
||||
test.skip("expand/collapse", () => {});
|
||||
test.skip("compound filter", () => {});
|
||||
});
|
||||
|
||||
describe("layout", () => {
|
||||
test("text-font", async () => {
|
||||
await when.setStyle("font");
|
||||
await when.collapseGroupInLayerEditor();
|
||||
await when.collapseGroupInLayerEditor(1);
|
||||
await when.collapseGroupInLayerEditor(2);
|
||||
await when.clickWithin("spec-field:text-font", ".maputnik-autocomplete input");
|
||||
await then(get.element(".maputnik-autocomplete-menu-item")).shouldBeVisible();
|
||||
await then(get.element(".maputnik-autocomplete-menu-item")).shouldHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("paint", () => {
|
||||
test.skip("expand/collapse", () => {});
|
||||
test.skip("color", () => {});
|
||||
test.skip("pattern", () => {});
|
||||
test.skip("opacity", () => {});
|
||||
});
|
||||
|
||||
describe("json-editor", () => {
|
||||
test("add", async () => {
|
||||
const id = await when.modal.fillLayers({
|
||||
type: "circle",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id, type: "circle", source: "example" }],
|
||||
});
|
||||
|
||||
await when.clickByText('"source"');
|
||||
await when.typeKeys('"');
|
||||
|
||||
await then(get.element(".cm-lint-marker-error")).shouldExist();
|
||||
});
|
||||
|
||||
test.skip("expand/collapse", () => {});
|
||||
test.skip("modify", () => {});
|
||||
|
||||
test("parse error", async () => {
|
||||
const bgId = await createBackground();
|
||||
|
||||
await when.click("layer-list-item:background:" + bgId);
|
||||
await when.collapseGroupInLayerEditor();
|
||||
await when.collapseGroupInLayerEditor(1);
|
||||
await then(get.element(".cm-lint-marker-error")).shouldNotExist();
|
||||
|
||||
// Inject an invalid token (CodeMirror auto-closes brackets/quotes, so a
|
||||
// bare word reliably breaks the JSON) and expect a lint error.
|
||||
await when.appendTextInJsonEditor("zzz");
|
||||
await then(get.element(".cm-lint-marker-error")).shouldExist();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sticky header", () => {
|
||||
test("should keep layer header visible when scrolling properties", async () => {
|
||||
// Setup: Create a layer with many properties (e.g. symbol layer)
|
||||
await when.modal.fillLayers({
|
||||
type: "symbol",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
await when.wait(500);
|
||||
const header = get.elementByTestId("layer-editor.header");
|
||||
await then(header).shouldBeVisible();
|
||||
|
||||
await when.scrollToBottom(get.element(".maputnik-scroll-container"));
|
||||
await when.wait(200);
|
||||
|
||||
await then(header).shouldBeVisible();
|
||||
await then(get.elementByTestId("skip-target-layer-editor")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,351 +0,0 @@
|
||||
import { beforeEach, describe, test } from "./utils/fixtures";
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("layers list", () => {
|
||||
const { given, get, when, then } = new MaputnikDriver();
|
||||
|
||||
beforeEach(async () => {
|
||||
await given.setupMockBackedResponses();
|
||||
await when.setStyle("both");
|
||||
await when.modal.open();
|
||||
});
|
||||
|
||||
describe("ops", () => {
|
||||
let id: string;
|
||||
beforeEach(async () => {
|
||||
id = await when.modal.fillLayers({ type: "background" });
|
||||
});
|
||||
|
||||
test("should update layers in local storage", async () => {
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id, type: "background" }],
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking delete", () => {
|
||||
beforeEach(async () => {
|
||||
await when.click("layer-list-item:" + id + ":delete");
|
||||
});
|
||||
test("should empty layers in local storage", async () => {
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking duplicate", () => {
|
||||
beforeEach(async () => {
|
||||
await when.click("layer-list-item:" + id + ":copy");
|
||||
});
|
||||
test("should add copy layer in local storage", async () => {
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{ id: id + "-copy", type: "background" },
|
||||
{ id, type: "background" },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking hide", () => {
|
||||
beforeEach(async () => {
|
||||
await when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||
});
|
||||
|
||||
test("should update visibility to none in local storage", async () => {
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id, type: "background", layout: { visibility: "none" } }],
|
||||
});
|
||||
});
|
||||
|
||||
describe("when clicking show", () => {
|
||||
beforeEach(async () => {
|
||||
await when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||
});
|
||||
|
||||
test("should update visibility to visible in local storage", async () => {
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id, type: "background", layout: { visibility: "visible" } }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when selecting a layer", () => {
|
||||
let secondId: string;
|
||||
beforeEach(async () => {
|
||||
await when.modal.open();
|
||||
secondId = await when.modal.fillLayers({
|
||||
id: "second-layer",
|
||||
type: "background",
|
||||
});
|
||||
});
|
||||
test("should show the selected layer in the editor", async () => {
|
||||
await when.realClick("layer-list-item:" + secondId);
|
||||
await then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(secondId);
|
||||
await when.realClick("layer-list-item:" + id);
|
||||
await then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("background", () => {
|
||||
test("add", async () => {
|
||||
const id = await when.modal.fillLayers({ type: "background" });
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id, type: "background" }],
|
||||
});
|
||||
});
|
||||
|
||||
test.skip("modify", () => {});
|
||||
});
|
||||
|
||||
describe("fill", () => {
|
||||
test("add", async () => {
|
||||
const id = await when.modal.fillLayers({ type: "fill", layer: "example" });
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id, type: "fill", source: "example" }],
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Change source
|
||||
test.skip("change source", () => {});
|
||||
});
|
||||
|
||||
describe("line", () => {
|
||||
test("add", async () => {
|
||||
const id = await when.modal.fillLayers({ type: "line", layer: "example" });
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id, type: "line", source: "example" }],
|
||||
});
|
||||
});
|
||||
|
||||
test("groups", async () => {
|
||||
const id1 = await when.modal.fillLayers({ id: "aa", type: "line", layer: "example" });
|
||||
|
||||
await when.modal.open();
|
||||
const id2 = await when.modal.fillLayers({ id: "aa-2", type: "line", layer: "example" });
|
||||
|
||||
await when.modal.open();
|
||||
const id3 = await when.modal.fillLayers({ id: "b", type: "line", layer: "example" });
|
||||
|
||||
await then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
|
||||
await then(get.elementByTestId("layer-list-item:" + id2)).shouldNotBeVisible();
|
||||
await then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
|
||||
await when.click("layer-list-group:aa-0");
|
||||
await then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
|
||||
await then(get.elementByTestId("layer-list-item:" + id2)).shouldBeVisible();
|
||||
await then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
|
||||
await when.click("layer-list-item:" + id2);
|
||||
await when.click("skip-target-layer-editor");
|
||||
await when.click("menu-move-layer-down");
|
||||
await then(get.elementByTestId("layer-list-group:aa-0")).shouldNotExist();
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{ id: "aa", type: "line", source: "example" },
|
||||
{ id: "b", type: "line", source: "example" },
|
||||
{ id: "aa-2", type: "line", source: "example" },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("symbol", () => {
|
||||
test("add", async () => {
|
||||
const id = await when.modal.fillLayers({ type: "symbol", layer: "example" });
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id, type: "symbol", source: "example" }],
|
||||
});
|
||||
});
|
||||
|
||||
test("should show spec info when hovering and clicking single line property", async () => {
|
||||
await when.modal.fillLayers({ type: "symbol", layer: "example" });
|
||||
|
||||
await when.hover("spec-field-container:text-rotate");
|
||||
await then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
|
||||
await when.click("field-doc-button-Rotate", 0);
|
||||
await then(get.elementByTestId("spec-field-doc")).shouldContainText("Rotates the ");
|
||||
});
|
||||
|
||||
test("should show spec info when hovering and clicking multi line property", async () => {
|
||||
await when.modal.fillLayers({ type: "symbol", layer: "example" });
|
||||
|
||||
await when.hover("spec-field-container:text-offset");
|
||||
await then(get.elementByTestId("field-doc-button-Offset")).shouldBeVisible();
|
||||
await when.click("field-doc-button-Offset", 0);
|
||||
await then(get.elementByTestId("spec-field-doc")).shouldContainText("Offset distance");
|
||||
});
|
||||
|
||||
test("should hide spec info when clicking a second time", async () => {
|
||||
await when.modal.fillLayers({ type: "symbol", layer: "example" });
|
||||
|
||||
await when.hover("spec-field-container:text-rotate");
|
||||
await then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
|
||||
await when.click("field-doc-button-Rotate", 0);
|
||||
await when.wait(200);
|
||||
await when.click("field-doc-button-Rotate", 0);
|
||||
await then(get.elementByTestId("spec-field-doc")).shouldNotBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("raster", () => {
|
||||
test("add", async () => {
|
||||
const id = await when.modal.fillLayers({ type: "raster", layer: "raster" });
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id, type: "raster", source: "raster" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("circle", () => {
|
||||
test("add", async () => {
|
||||
const id = await when.modal.fillLayers({ type: "circle", layer: "example" });
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id, type: "circle", source: "example" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fill extrusion", () => {
|
||||
test("add", async () => {
|
||||
const id = await when.modal.fillLayers({ type: "fill-extrusion", layer: "example" });
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id, type: "fill-extrusion", source: "example" }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("hillshade", () => {
|
||||
test("add", async () => {
|
||||
const id = await when.modal.fillLayers({ type: "hillshade", layer: "example" });
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id, type: "hillshade", source: "example" }],
|
||||
});
|
||||
});
|
||||
|
||||
test("set hillshade illumination direction array", async () => {
|
||||
const id = await when.modal.fillLayers({ type: "hillshade", layer: "example" });
|
||||
await when.collapseGroupInLayerEditor();
|
||||
await when.collapseGroupInLayerEditor(1);
|
||||
await when.setValueToPropertyArray("spec-field:hillshade-illumination-direction", "1");
|
||||
await when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "2");
|
||||
await when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "3");
|
||||
await when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "4");
|
||||
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id,
|
||||
type: "hillshade",
|
||||
source: "example",
|
||||
paint: { "hillshade-illumination-direction": [1, 2, 3, 4] },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("set hillshade highlight color array", async () => {
|
||||
const id = await when.modal.fillLayers({ type: "hillshade", layer: "example" });
|
||||
await when.collapseGroupInLayerEditor();
|
||||
await when.setValueToPropertyArray("spec-field:hillshade-highlight-color", "blue");
|
||||
await when.addValueToPropertyArray("spec-field:hillshade-highlight-color", "#00ff00");
|
||||
await when.addValueToPropertyArray("spec-field:hillshade-highlight-color", "rgba(255, 255, 0, 1)");
|
||||
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id,
|
||||
type: "hillshade",
|
||||
source: "example",
|
||||
paint: {
|
||||
"hillshade-highlight-color": ["blue", "#00ff00", "rgba(255, 255, 0, 1)"],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("color-relief", () => {
|
||||
test("add", async () => {
|
||||
const id = await when.modal.fillLayers({ type: "color-relief", layer: "example" });
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [{ id, type: "color-relief", source: "example" }],
|
||||
});
|
||||
});
|
||||
|
||||
test("adds elevation expression when clicking the elevation button", async () => {
|
||||
await when.modal.fillLayers({ type: "color-relief", layer: "example" });
|
||||
await when.collapseGroupInLayerEditor();
|
||||
await when.click("make-elevation-function");
|
||||
await then(
|
||||
get.element("[data-wd-key='spec-field-container:color-relief-color'] .cm-line")
|
||||
).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("groups", () => {
|
||||
test("simple", async () => {
|
||||
await when.setStyle("geojson");
|
||||
|
||||
await when.modal.open();
|
||||
await when.modal.fillLayers({ id: "foo", type: "background" });
|
||||
|
||||
await when.modal.open();
|
||||
await when.modal.fillLayers({ id: "foo_bar", type: "background" });
|
||||
|
||||
await when.modal.open();
|
||||
await when.modal.fillLayers({ id: "foo_bar_baz", type: "background" });
|
||||
|
||||
await then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
||||
await then(get.elementByTestId("layer-list-item:foo_bar")).shouldNotBeVisible();
|
||||
await then(get.elementByTestId("layer-list-item:foo_bar_baz")).shouldNotBeVisible();
|
||||
await when.click("layer-list-group:foo-0");
|
||||
await then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
||||
await then(get.elementByTestId("layer-list-item:foo_bar")).shouldBeVisible();
|
||||
await then(get.elementByTestId("layer-list-item:foo_bar_baz")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("drag and drop", () => {
|
||||
test("move layer should update local storage", async () => {
|
||||
const firstId = await when.modal.fillLayers({ id: "a", type: "background" });
|
||||
await when.modal.open();
|
||||
const secondId = await when.modal.fillLayers({ id: "b", type: "background" });
|
||||
await when.modal.open();
|
||||
const thirdId = await when.modal.fillLayers({ id: "c", type: "background" });
|
||||
|
||||
await when.dragAndDropWithWait("layer-list-item:" + firstId, "layer-list-item:" + thirdId);
|
||||
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{ id: secondId, type: "background" },
|
||||
{ id: thirdId, type: "background" },
|
||||
{ id: firstId, type: "background" },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("sticky header", () => {
|
||||
test("should keep header visible when scrolling layer list", async () => {
|
||||
// Setup: Create multiple layers to enable scrolling
|
||||
// The modal is already open (beforeEach) for the first layer.
|
||||
await when.modal.fillLayers({ id: "layer-0", type: "background" });
|
||||
for (let i = 1; i < 20; i++) {
|
||||
await when.modal.open();
|
||||
await when.modal.fillLayers({ id: `layer-${i}`, type: "background" });
|
||||
}
|
||||
|
||||
await when.wait(500);
|
||||
const header = get.elementByTestId("layer-list.header");
|
||||
await then(header).shouldBeVisible();
|
||||
|
||||
// Scroll the layer list container
|
||||
await when.scrollToBottom(get.elementByTestId("layer-list"));
|
||||
await when.wait(200);
|
||||
await then(header).shouldBeVisible();
|
||||
await then(get.elementByTestId("layer-list:add-layer")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
import { beforeEach, describe, test } from "./utils/fixtures";
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("map", () => {
|
||||
const { given, get, when, then } = new MaputnikDriver();
|
||||
|
||||
beforeEach(async () => {
|
||||
await given.setupMockBackedResponses();
|
||||
await when.setStyle("both");
|
||||
});
|
||||
|
||||
describe("zoom level", () => {
|
||||
test("via url", async () => {
|
||||
const zoomLevel = 12.37;
|
||||
await when.setStyle("geojson", zoomLevel);
|
||||
await then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
||||
await then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText("Zoom: " + zoomLevel);
|
||||
});
|
||||
|
||||
test("via map controls", async () => {
|
||||
const zoomLevel = 12.37;
|
||||
await when.setStyle("geojson", zoomLevel);
|
||||
await then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
||||
await when.clickZoomIn();
|
||||
await then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText("Zoom: " + (zoomLevel + 1));
|
||||
});
|
||||
|
||||
test("via style file definition", async () => {
|
||||
await when.setStyle("zoom_7_center_0_51");
|
||||
await then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
||||
await then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText("Zoom: " + 7);
|
||||
await then(get.locationHash()).shouldInclude("#7/51/0");
|
||||
|
||||
// opening another stylefile does not update the map view again
|
||||
// as discussed in https://github.com/maplibre/maputnik/issues/1546
|
||||
await when.openASecondStyleWithDifferentZoomAndCenter();
|
||||
await then(get.locationHash()).shouldInclude("#7/51/0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("search", () => {
|
||||
test("should exist", async () => {
|
||||
await then(get.searchControl()).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("popup", () => {
|
||||
beforeEach(async () => {
|
||||
await when.setStyle("rectangles");
|
||||
await then(get.locationHash()).shouldExist();
|
||||
});
|
||||
|
||||
test("should open on feature click", async () => {
|
||||
await when.clickCenter("maplibre:map");
|
||||
await then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
|
||||
});
|
||||
|
||||
test("should open a second feature after closing popup", async () => {
|
||||
await when.clickCenter("maplibre:map");
|
||||
await then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
|
||||
await when.closePopup();
|
||||
await then(get.elementByTestId("feature-layer-popup")).shouldNotExist();
|
||||
await when.clickCenter("maplibre:map");
|
||||
await then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,192 +0,0 @@
|
||||
import { PlaywrightHelper } from "./playwright-helper";
|
||||
import { ModalDriver } from "./modal-driver";
|
||||
|
||||
const baseUrl = "http://localhost:8888/";
|
||||
const isMac = process.platform === "darwin";
|
||||
|
||||
/**
|
||||
* The maputnik-specific driver. It builds on the generic {@link PlaywrightHelper}
|
||||
* — spreading its `given`/`when`/`get` primitives and adding domain concepts
|
||||
* (loading a style, the add-layer modal, the JSON editor, …). All Playwright
|
||||
* access goes through the helper; the driver never touches `page` directly.
|
||||
*/
|
||||
export class MaputnikDriver {
|
||||
private readonly helper = new PlaywrightHelper();
|
||||
private readonly modalDriver = new ModalDriver();
|
||||
|
||||
then = this.helper.then;
|
||||
|
||||
/** Reads the maputnik style currently persisted in localStorage. */
|
||||
private async readStoredStyle(): Promise<any> {
|
||||
const styleId = await this.helper.get.localStorageItem("maputnik:latest_style");
|
||||
const styleItem = await this.helper.get.localStorageItem(`maputnik:style:${styleId}`);
|
||||
if (!styleItem) throw new Error("Could not get styleItem from localStorage");
|
||||
return JSON.parse(styleItem);
|
||||
}
|
||||
|
||||
public given = {
|
||||
...this.helper.given,
|
||||
|
||||
setupMockBackedResponses: async () => {
|
||||
const styleFixtures = [
|
||||
"example-style.json",
|
||||
"example-layer-style.json",
|
||||
"geojson-style.json",
|
||||
"raster-style.json",
|
||||
"geojson-raster-style.json",
|
||||
"rectangles-style.json",
|
||||
"example-style-with-fonts.json",
|
||||
"example-style-with-zoom-7-and-center-0-51.json",
|
||||
"example-style-with-zoom-5-and-center-50-50.json",
|
||||
];
|
||||
for (const fixture of styleFixtures) {
|
||||
await this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: baseUrl + fixture,
|
||||
response: { fixture },
|
||||
alias: fixture === "example-style.json" ? "example-style.json" : undefined,
|
||||
});
|
||||
}
|
||||
await this.helper.given.interceptAndMockResponse({ method: "GET", url: /example\.local\//, response: [] });
|
||||
await this.helper.given.interceptAndMockResponse({ method: "GET", url: /example\.com\//, response: [] });
|
||||
await this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: "https://www.glyph-server.com/*",
|
||||
response: ["Font 1", "Font 2", "Font 3"],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
public when = {
|
||||
...this.helper.when,
|
||||
|
||||
modal: this.modalDriver.when,
|
||||
|
||||
setStyle: async (
|
||||
styleProperties:
|
||||
| "geojson"
|
||||
| "raster"
|
||||
| "both"
|
||||
| "layer"
|
||||
| "rectangles"
|
||||
| "font"
|
||||
| "zoom_7_center_0_51"
|
||||
| "",
|
||||
zoom?: number
|
||||
) => {
|
||||
const styleFileByKey: Record<string, string> = {
|
||||
geojson: "geojson-style.json",
|
||||
raster: "raster-style.json",
|
||||
both: "geojson-raster-style.json",
|
||||
layer: "example-layer-style.json",
|
||||
rectangles: "rectangles-style.json",
|
||||
font: "example-style-with-fonts.json",
|
||||
zoom_7_center_0_51: "example-style-with-zoom-7-and-center-0-51.json",
|
||||
};
|
||||
|
||||
const url = new URL(baseUrl);
|
||||
if (styleProperties && styleFileByKey[styleProperties]) {
|
||||
url.searchParams.set("style", baseUrl + styleFileByKey[styleProperties]);
|
||||
}
|
||||
if (zoom) {
|
||||
url.hash = `${zoom}/41.3805/2.1635`;
|
||||
}
|
||||
|
||||
await this.helper.when.visit(url.toString());
|
||||
|
||||
const toolbarLink = this.helper.get.elementByTestId("toolbar:link");
|
||||
await toolbarLink.scrollIntoViewIfNeeded();
|
||||
await this.then(toolbarLink).shouldBeVisible();
|
||||
},
|
||||
|
||||
openASecondStyleWithDifferentZoomAndCenter: async () => {
|
||||
await this.helper.when.clickButtonByName("Open");
|
||||
const input = this.helper.get.elementByTestId("modal:open.url.input");
|
||||
await input.fill("http://localhost:8888/example-style-with-zoom-5-and-center-50-50.json");
|
||||
await input.press("Enter");
|
||||
},
|
||||
|
||||
chooseExampleFile: async () => {
|
||||
await this.helper.when.openFileByFixture("example-style.json", "modal:open.dropzone", "modal:open.file.input");
|
||||
await this.helper.when.wait(200);
|
||||
},
|
||||
|
||||
dropExampleFile: async () => {
|
||||
await this.helper.when.dropFileByFixture("example-style.json", "modal:open.dropzone");
|
||||
await this.helper.when.wait(200);
|
||||
},
|
||||
|
||||
clickZoomIn: async () => {
|
||||
await this.helper.get.element(".maplibregl-ctrl-zoom-in").click();
|
||||
},
|
||||
|
||||
closePopup: async () => {
|
||||
await this.helper.get.element(".maplibregl-popup-close-button").click();
|
||||
},
|
||||
|
||||
collapseGroupInLayerEditor: async (index = 0) => {
|
||||
await this.helper.get.element(".maputnik-layer-editor-group__button").nth(index).click();
|
||||
},
|
||||
|
||||
appendTextInJsonEditor: async (text: string) => {
|
||||
await this.helper.get.element(".cm-line").first().click();
|
||||
// Move to the very start of the document so the inserted text breaks the
|
||||
// root JSON structure (CodeMirror auto-closes brackets otherwise).
|
||||
await this.helper.when.typeKeys("{home}");
|
||||
await this.helper.when.typeText(text);
|
||||
},
|
||||
|
||||
setTextInJsonEditor: async (text: string) => {
|
||||
await this.helper.get.element(".cm-line").first().click();
|
||||
await this.helper.when.typeKeys("{selectall}");
|
||||
await this.helper.when.typeText(text);
|
||||
},
|
||||
|
||||
setValueToPropertyArray: async (selector: string, value: string) => {
|
||||
const input = this.helper.get.elementByTestId(selector).locator(".maputnik-array-block-content input").last();
|
||||
await input.focus();
|
||||
await this.helper.when.typeKeys("{selectall}");
|
||||
await this.helper.when.typeText(value);
|
||||
},
|
||||
|
||||
addValueToPropertyArray: async (selector: string, value: string) => {
|
||||
const block = this.helper.get.elementByTestId(selector);
|
||||
await block.locator(".maputnik-array-add-value").click();
|
||||
const input = block.locator(".maputnik-array-block-content input").last();
|
||||
await input.focus();
|
||||
await this.helper.when.typeKeys("{selectall}");
|
||||
await this.helper.when.typeText(value);
|
||||
},
|
||||
|
||||
waitForExampleFileResponse: () => this.helper.when.waitForResponse("example-style.json"),
|
||||
|
||||
/** Fill localStorage until we get a QuotaExceededError. */
|
||||
fillLocalStorage: () => this.helper.when.fillLocalStorageUntilQuota("maputnik:fill-"),
|
||||
};
|
||||
|
||||
public get = {
|
||||
...this.helper.get,
|
||||
|
||||
isMac: () => isMac,
|
||||
|
||||
canvas: () => this.helper.get.element("canvas"),
|
||||
|
||||
searchControl: () => this.helper.get.element(".maplibregl-ctrl-geocoder"),
|
||||
|
||||
skipTargetLayerList: () => this.helper.get.elementByTestId("skip-target-layer-list"),
|
||||
|
||||
skipTargetLayerEditor: () => this.helper.get.elementByTestId("skip-target-layer-editor"),
|
||||
|
||||
styleFromLocalStorage: () => this.helper.query(() => this.readStoredStyle()),
|
||||
|
||||
fixture: (name: string) => this.helper.readFixture(name),
|
||||
|
||||
responseBody: (alias: string) => {
|
||||
// Our mocked style responses always return the matching fixture.
|
||||
const name = alias.endsWith(".json") ? alias : `${alias}.json`;
|
||||
return this.helper.readFixture(name);
|
||||
},
|
||||
|
||||
exampleFileUrl: () => baseUrl + "example-style.json",
|
||||
};
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { v1 as uuid } from "uuid";
|
||||
import { PlaywrightHelper } from "./playwright-helper";
|
||||
|
||||
export class ModalDriver {
|
||||
private readonly helper = new PlaywrightHelper();
|
||||
|
||||
public when = {
|
||||
fillLayers: async (opts: { type: string; layer?: string; id?: string }) => {
|
||||
const { when, get, then } = this.helper;
|
||||
const id = opts.id ?? `${opts.type}:${uuid()}`;
|
||||
|
||||
await when.select("add-layer.layer-type.select", opts.type);
|
||||
await when.type("add-layer.layer-id.input", id);
|
||||
|
||||
if (opts.layer) {
|
||||
const input = get.elementByTestId("add-layer.layer-source-block").locator("input");
|
||||
await input.click();
|
||||
await input.fill(opts.layer);
|
||||
// The source input is a controlled downshift combobox; wait for React to
|
||||
// settle on the typed value before submitting.
|
||||
await then(input).shouldHaveValue(opts.layer);
|
||||
// Close the autocomplete menu so it does not intercept the add button.
|
||||
await get.elementByTestId("add-layer.layer-id.input").click();
|
||||
}
|
||||
await when.click("add-layer");
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
open: async () => {
|
||||
await this.helper.when.click("layer-list:add-layer");
|
||||
},
|
||||
|
||||
close: async (key: string) => {
|
||||
await this.helper.when.click(key + ".close-modal");
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,405 +0,0 @@
|
||||
import { test, expect, describe, beforeEach } from "./utils/fixtures";
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
import tokens from "../src/config/tokens.json" with { type: "json" };
|
||||
|
||||
describe("modals", () => {
|
||||
const { given, get, when, then } = new MaputnikDriver();
|
||||
|
||||
beforeEach(async () => {
|
||||
await given.setupMockBackedResponses();
|
||||
await when.setStyle("both");
|
||||
await when.setStyle("");
|
||||
});
|
||||
|
||||
describe("open", () => {
|
||||
beforeEach(async () => {
|
||||
await when.click("nav:open");
|
||||
});
|
||||
|
||||
test("close", async () => {
|
||||
await when.modal.close("modal:open");
|
||||
await then(get.elementByTestId("modal:open")).shouldNotExist();
|
||||
});
|
||||
|
||||
test("upload", async () => {
|
||||
await when.chooseExampleFile();
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude(get.fixture("example-style.json"));
|
||||
});
|
||||
|
||||
test("upload via drag and drop", async () => {
|
||||
await when.dropExampleFile();
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude(get.fixture("example-style.json"));
|
||||
});
|
||||
|
||||
describe("when click open url", () => {
|
||||
beforeEach(async () => {
|
||||
const styleFileUrl = get.exampleFileUrl();
|
||||
|
||||
await when.setValue("modal:open.url.input", styleFileUrl);
|
||||
await when.click("modal:open.url.button");
|
||||
await when.wait(200);
|
||||
});
|
||||
test("load from url", async () => {
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude(get.responseBody("example-style.json"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("shortcuts", () => {
|
||||
test("open/close", async () => {
|
||||
await when.setStyle("");
|
||||
await when.typeKeys("?");
|
||||
await when.modal.close("modal:shortcuts");
|
||||
await then(get.elementByTestId("modal:shortcuts")).shouldNotExist();
|
||||
});
|
||||
});
|
||||
|
||||
describe("export", () => {
|
||||
beforeEach(async () => {
|
||||
await when.click("nav:export");
|
||||
});
|
||||
|
||||
test("close", async () => {
|
||||
await when.modal.close("modal:export");
|
||||
await then(get.elementByTestId("modal:export")).shouldNotExist();
|
||||
});
|
||||
|
||||
// TODO: Work out how to download a file and check the contents
|
||||
test.skip("download", () => {});
|
||||
});
|
||||
|
||||
describe("sources", () => {
|
||||
beforeEach(async () => {
|
||||
await when.setStyle("layer");
|
||||
await when.click("nav:sources");
|
||||
});
|
||||
|
||||
test.skip("active sources", () => {});
|
||||
test.skip("public source", () => {});
|
||||
|
||||
test("add new source", async () => {
|
||||
const sourceId = "n1z2v3r";
|
||||
await when.setValue("modal:sources.add.source_id", sourceId);
|
||||
await when.select("modal:sources.add.source_type", "tile_vector");
|
||||
await when.select("modal:sources.add.scheme_type", "tms");
|
||||
await when.click("modal:sources.add.add_source");
|
||||
await when.wait(200);
|
||||
await then(get.styleFromLocalStorage().then((style) => style.sources[sourceId])).shouldInclude({
|
||||
scheme: "tms",
|
||||
});
|
||||
});
|
||||
|
||||
test("add new pmtiles source", async () => {
|
||||
const sourceId = "pmtilestest";
|
||||
await when.setValue("modal:sources.add.source_id", sourceId);
|
||||
await when.select("modal:sources.add.source_type", "pmtiles_vector");
|
||||
await when.setValue(
|
||||
"modal:sources.add.source_url",
|
||||
"https://data.source.coop/protomaps/openstreetmap/v4.pmtiles"
|
||||
);
|
||||
await when.click("modal:sources.add.add_source");
|
||||
await when.click("modal:sources.add.add_source");
|
||||
await when.wait(200);
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
sources: {
|
||||
pmtilestest: {
|
||||
type: "vector",
|
||||
url: "pmtiles://https://data.source.coop/protomaps/openstreetmap/v4.pmtiles",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("add new raster source", async () => {
|
||||
const sourceId = "rastertest";
|
||||
await when.setValue("modal:sources.add.source_id", sourceId);
|
||||
await when.select("modal:sources.add.source_type", "tile_raster");
|
||||
await when.select("modal:sources.add.scheme_type", "xyz");
|
||||
await when.setValue("modal:sources.add.tile_size", "128");
|
||||
await when.click("modal:sources.add.add_source");
|
||||
await when.wait(200);
|
||||
await then(get.styleFromLocalStorage().then((style) => style.sources[sourceId])).shouldInclude({
|
||||
tileSize: 128,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("inspect", () => {
|
||||
test("toggle", async () => {
|
||||
// There is no assertion in this test
|
||||
await when.setStyle("geojson");
|
||||
await when.select("maputnik-select", "inspect");
|
||||
});
|
||||
});
|
||||
|
||||
describe("style settings", () => {
|
||||
beforeEach(async () => {
|
||||
await when.click("nav:settings");
|
||||
});
|
||||
|
||||
describe("when click name filed spec information", () => {
|
||||
beforeEach(async () => {
|
||||
await when.click("field-doc-button-Name");
|
||||
});
|
||||
|
||||
test("should show the spec information", async () => {
|
||||
await then(get.elementsText("spec-field-doc")).shouldInclude("name for the style");
|
||||
});
|
||||
});
|
||||
|
||||
describe("when set name and click owner", () => {
|
||||
beforeEach(async () => {
|
||||
await when.setValue("modal:settings.name", "foobar");
|
||||
await when.click("modal:settings.owner");
|
||||
await when.wait(200);
|
||||
});
|
||||
|
||||
test("show name specifications", async () => {
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
name: "foobar",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when set owner and click name", () => {
|
||||
beforeEach(async () => {
|
||||
await when.setValue("modal:settings.owner", "foobar");
|
||||
await when.click("modal:settings.name");
|
||||
await when.wait(200);
|
||||
});
|
||||
test("should update owner in local storage", async () => {
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
owner: "foobar",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("sprite url", async () => {
|
||||
await when.setTextInJsonEditor('"http://example.com"');
|
||||
await when.click("modal:settings.name");
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
sprite: "http://example.com",
|
||||
});
|
||||
});
|
||||
|
||||
test("sprite object", async () => {
|
||||
await when.setTextInJsonEditor(JSON.stringify([{ id: "1", url: "2" }]));
|
||||
|
||||
await when.click("modal:settings.name");
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
sprite: [{ id: "1", url: "2" }],
|
||||
});
|
||||
});
|
||||
|
||||
test("glyphs url", async () => {
|
||||
const glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
|
||||
await when.setValue("modal:settings.glyphs", glyphsUrl);
|
||||
await when.click("modal:settings.name");
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
glyphs: glyphsUrl,
|
||||
});
|
||||
});
|
||||
|
||||
test("maptiler access token", async () => {
|
||||
const apiKey = "testing123";
|
||||
await when.setValue("modal:settings.maputnik:openmaptiles_access_token", apiKey);
|
||||
await when.click("modal:settings.name");
|
||||
await then(get.styleFromLocalStorage().then((style) => style.metadata)).shouldInclude({
|
||||
"maputnik:openmaptiles_access_token": apiKey,
|
||||
});
|
||||
});
|
||||
|
||||
test("thunderforest access token", async () => {
|
||||
const apiKey = "testing123";
|
||||
await when.setValue("modal:settings.maputnik:thunderforest_access_token", apiKey);
|
||||
await when.click("modal:settings.name");
|
||||
await then(get.styleFromLocalStorage().then((style) => style.metadata)).shouldInclude({
|
||||
"maputnik:thunderforest_access_token": apiKey,
|
||||
});
|
||||
});
|
||||
|
||||
test("stadia access token", async () => {
|
||||
const apiKey = "testing123";
|
||||
await when.setValue("modal:settings.maputnik:stadia_access_token", apiKey);
|
||||
await when.click("modal:settings.name");
|
||||
await then(get.styleFromLocalStorage().then((style) => style.metadata)).shouldInclude({
|
||||
"maputnik:stadia_access_token": apiKey,
|
||||
});
|
||||
});
|
||||
|
||||
test("locationiq access token", async () => {
|
||||
const apiKey = "testing123";
|
||||
await when.setValue("modal:settings.maputnik:locationiq_access_token", apiKey);
|
||||
await when.click("modal:settings.name");
|
||||
await then(get.styleFromLocalStorage().then((style) => style.metadata)).shouldInclude({
|
||||
"maputnik:locationiq_access_token": apiKey,
|
||||
});
|
||||
});
|
||||
|
||||
test("style projection mercator", async () => {
|
||||
await when.select("modal:settings.projection", "mercator");
|
||||
await then(get.styleFromLocalStorage().then((style) => style.projection)).shouldInclude({
|
||||
type: "mercator",
|
||||
});
|
||||
});
|
||||
|
||||
test("style projection globe", async () => {
|
||||
await when.select("modal:settings.projection", "globe");
|
||||
await then(get.styleFromLocalStorage().then((style) => style.projection)).shouldInclude({
|
||||
type: "globe",
|
||||
});
|
||||
});
|
||||
|
||||
test("style projection vertical-perspective", async () => {
|
||||
await when.select("modal:settings.projection", "vertical-perspective");
|
||||
await then(get.styleFromLocalStorage().then((style) => style.projection)).shouldInclude({
|
||||
type: "vertical-perspective",
|
||||
});
|
||||
});
|
||||
|
||||
test("style renderer", async () => {
|
||||
await when.select("modal:settings.maputnik:renderer", "ol");
|
||||
await then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("ol");
|
||||
|
||||
await when.click("modal:settings.name");
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
metadata: { "maputnik:renderer": "ol" },
|
||||
});
|
||||
});
|
||||
|
||||
test("include API key when change renderer", async () => {
|
||||
await when.click("modal:settings.close-modal");
|
||||
await when.click("nav:open");
|
||||
|
||||
await when.clickByAttribute("aria-label", "MapTiler Basic");
|
||||
await when.wait(1000);
|
||||
await when.click("nav:settings");
|
||||
|
||||
await when.select("modal:settings.maputnik:renderer", "mlgljs");
|
||||
await then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("mlgljs");
|
||||
|
||||
await when.select("modal:settings.maputnik:renderer", "ol");
|
||||
await then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("ol");
|
||||
|
||||
await given.intercept(
|
||||
/https:\/\/api\.maptiler\.com\/tiles\/v3-openmaptiles\/tiles\.json\?key=.*/,
|
||||
"tileRequest",
|
||||
"GET"
|
||||
);
|
||||
|
||||
await when.select("modal:settings.maputnik:renderer", "mlgljs");
|
||||
await then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual("mlgljs");
|
||||
|
||||
const request = await when.waitForResponse("tileRequest");
|
||||
expect(request.url()).toContain(
|
||||
`https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("add layer", () => {
|
||||
beforeEach(async () => {
|
||||
await when.setStyle("layer");
|
||||
await when.modal.open();
|
||||
});
|
||||
|
||||
test("shows duplicate id error", async () => {
|
||||
await when.setValue("add-layer.layer-id.input", "background");
|
||||
await when.click("add-layer");
|
||||
await then(get.elementByTestId("modal:add-layer")).shouldExist();
|
||||
await then(get.element(".maputnik-modal-error")).shouldContainText("Layer ID already exists");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sources placeholder", () => {
|
||||
test.skip("toggle", () => {});
|
||||
});
|
||||
|
||||
describe("global state", () => {
|
||||
beforeEach(async () => {
|
||||
await when.click("nav:global-state");
|
||||
});
|
||||
|
||||
test("add variable", async () => {
|
||||
await when.wait(100);
|
||||
await when.click("global-state-add-variable");
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
state: { key1: { default: "value" } },
|
||||
});
|
||||
});
|
||||
|
||||
test("add multiple variables", async () => {
|
||||
await when.click("global-state-add-variable");
|
||||
await when.click("global-state-add-variable");
|
||||
await when.click("global-state-add-variable");
|
||||
await when.wait(100);
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
state: { key1: { default: "value" }, key2: { default: "value" }, key3: { default: "value" } },
|
||||
});
|
||||
});
|
||||
|
||||
test("remove variable", async () => {
|
||||
await when.click("global-state-add-variable");
|
||||
await when.click("global-state-add-variable");
|
||||
await when.click("global-state-add-variable");
|
||||
await when.click("global-state-remove-variable", 0);
|
||||
await when.wait(100);
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
state: { key2: { default: "value" }, key3: { default: "value" } },
|
||||
});
|
||||
});
|
||||
|
||||
test("edit variable key", async () => {
|
||||
await when.click("global-state-add-variable");
|
||||
await when.wait(100);
|
||||
await when.setValue("global-state-variable-key:0", "mykey");
|
||||
await when.typeKeys("{enter}");
|
||||
await when.wait(100);
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
state: { mykey: { default: "value" } },
|
||||
});
|
||||
});
|
||||
|
||||
test("edit variable value", async () => {
|
||||
await when.click("global-state-add-variable");
|
||||
await when.wait(100);
|
||||
await when.setValue("global-state-variable-value:0", "myvalue");
|
||||
await when.typeKeys("{enter}");
|
||||
await when.wait(100);
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
state: { key1: { default: "myvalue" } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("error panel", () => {
|
||||
test("not visible when no errors", async () => {
|
||||
await then(get.element("maputnik-message-panel-error")).shouldNotExist();
|
||||
});
|
||||
|
||||
test("visible on style error", async () => {
|
||||
await when.modal.open();
|
||||
await when.modal.fillLayers({
|
||||
type: "circle",
|
||||
layer: "invalid",
|
||||
});
|
||||
await then(get.element(".maputnik-message-panel-error")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Handle localStorage QuotaExceededError", () => {
|
||||
test("handles quota exceeded error when opening style from URL", async () => {
|
||||
// Clear localStorage to start fresh
|
||||
await when.clearLocalStorage();
|
||||
await when.fillLocalStorage();
|
||||
|
||||
// Open the style via URL input
|
||||
await when.click("nav:open");
|
||||
await when.setValue("modal:open.url.input", get.exampleFileUrl());
|
||||
await when.click("modal:open.url.button");
|
||||
|
||||
await then(get.styleFromLocalStorage()).shouldDeepNestedInclude(get.responseBody("example-style.json"));
|
||||
await then(get.styleFromLocalStorage()).shouldExist();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,414 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { expect, type Locator, type Page, type Request } from "@playwright/test";
|
||||
import { currentPage, recordCoverageChunk } from "./utils/fixtures";
|
||||
|
||||
const DATA_ATTRIBUTE = "data-wd-key";
|
||||
const FIXTURES_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "fixtures");
|
||||
const isMac = process.platform === "darwin";
|
||||
|
||||
function testIdSelector(testId: string): string {
|
||||
return `[${DATA_ATTRIBUTE}="${testId}"]`;
|
||||
}
|
||||
|
||||
/** Retries `assertion` until it stops throwing */
|
||||
async function retry(
|
||||
assertion: () => Promise<void> | void,
|
||||
timeout = 10000,
|
||||
interval = 100
|
||||
): Promise<void> {
|
||||
const start = Date.now();
|
||||
let lastError: unknown;
|
||||
while (true) {
|
||||
try {
|
||||
await assertion();
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (Date.now() - start > timeout) throw lastError;
|
||||
await new Promise((resolve) => setTimeout(resolve, interval));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A lazily-evaluated value (e.g. the style in localStorage). Assertions on a
|
||||
* Query re-read the value until they pass.
|
||||
*/
|
||||
class Query<T> {
|
||||
readonly __query = true as const;
|
||||
constructor(private readonly getter: () => Promise<T>) {}
|
||||
|
||||
get(): Promise<T> {
|
||||
return this.getter();
|
||||
}
|
||||
|
||||
then<U>(mapper: (value: T) => U | Promise<U>): Query<U> {
|
||||
return new Query<U>(async () => mapper(await this.getter()));
|
||||
}
|
||||
}
|
||||
|
||||
function isQuery(target: unknown): target is Query<unknown> {
|
||||
return typeof target === "object" && target !== null && (target as Query<unknown>).__query === true;
|
||||
}
|
||||
|
||||
function isLocator(target: unknown): target is Locator {
|
||||
return (
|
||||
typeof target === "object" &&
|
||||
target !== null &&
|
||||
typeof (target as Locator).count === "function" &&
|
||||
typeof (target as Locator).boundingBox === "function"
|
||||
);
|
||||
}
|
||||
|
||||
/** Asserts that every top-level key in `expected` deep-equals its counterpart in `actual`. */
|
||||
function assertDeepNestedInclude(actual: any, expected: Record<string, unknown>): void {
|
||||
for (const key of Object.keys(expected)) {
|
||||
expect(actual?.[key], `property "${key}"`).toEqual(expected[key]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fluent, auto-retrying assertions over a Playwright Locator or a lazily
|
||||
* evaluated value/Query. This is the generic base that the maputnik-specific
|
||||
* assertable extends.
|
||||
*/
|
||||
export class Assertable<T> {
|
||||
constructor(private readonly target: T) {}
|
||||
|
||||
private locator(): Locator {
|
||||
if (!isLocator(this.target)) throw new Error("Expected a Locator target for this assertion");
|
||||
return this.target;
|
||||
}
|
||||
|
||||
protected async assertValue(assertion: (value: any) => void): Promise<void> {
|
||||
const target = this.target;
|
||||
if (isQuery(target)) {
|
||||
await retry(async () => assertion(await target.get()));
|
||||
} else {
|
||||
assertion(await (target as any));
|
||||
}
|
||||
}
|
||||
|
||||
// Element assertions (auto-retrying via Playwright web-first assertions).
|
||||
shouldBeVisible = () => expect(this.locator().first()).toBeVisible();
|
||||
// Some testids resolve to many elements that are always rendered but hidden
|
||||
// (e.g. per-field documentation panels); "not visible" means none is visible.
|
||||
shouldNotBeVisible = () => expect(this.locator().filter({ visible: true })).toHaveCount(0);
|
||||
shouldExist = async () => {
|
||||
if (isLocator(this.target)) {
|
||||
await expect(this.locator().first()).toBeAttached();
|
||||
} else {
|
||||
await this.assertValue((value) => expect(value).toBeTruthy());
|
||||
}
|
||||
};
|
||||
shouldNotExist = () => expect(this.locator()).toHaveCount(0);
|
||||
shouldBeFocused = () => expect(this.locator().first()).toBeFocused();
|
||||
shouldNotBeFocused = () => expect(this.locator().first()).not.toBeFocused();
|
||||
shouldHaveValue = (value: string) => expect(this.locator().first()).toHaveValue(value);
|
||||
shouldContainText = async (text: string) => {
|
||||
const locator = this.locator();
|
||||
// Prefer the visible element when a testid resolves to several (only the
|
||||
// open documentation panel is visible; the rest are hidden in the DOM).
|
||||
const target = (await locator.count()) > 1 ? locator.filter({ visible: true }).first() : locator.first();
|
||||
await expect(target).toContainText(text);
|
||||
};
|
||||
shouldHaveText = (text: string) => expect(this.locator().first()).toHaveText(text);
|
||||
shouldHaveLength = (length: number) => expect(this.locator()).toHaveCount(length);
|
||||
shouldHaveCss = (property: string, value: string) => expect(this.locator().first()).toHaveCSS(property, value);
|
||||
|
||||
// Value assertions (auto-retrying for Query targets).
|
||||
shouldEqual = (value: any) => this.assertValue((actual) => expect(actual).toBe(value));
|
||||
|
||||
shouldInclude = (value: any) =>
|
||||
this.assertValue((actual) => {
|
||||
if (typeof value === "object" && value !== null) {
|
||||
expect(actual).toMatchObject(value);
|
||||
} else {
|
||||
expect(String(actual)).toContain(String(value));
|
||||
}
|
||||
});
|
||||
|
||||
shouldDeepNestedInclude = (value: Record<string, unknown>) =>
|
||||
this.assertValue((actual) => assertDeepNestedInclude(actual, value));
|
||||
}
|
||||
|
||||
async function typeSequence(page: Page, text: string): Promise<void> {
|
||||
const tokens = text.match(/\{[^}]+\}|[^{]+/g) ?? [];
|
||||
const modifierMap: Record<string, string> = { meta: "Meta", ctrl: "Control", shift: "Shift", alt: "Alt" };
|
||||
const namedKeys: Record<string, string> = {
|
||||
esc: "Escape",
|
||||
enter: "Enter",
|
||||
backspace: "Backspace",
|
||||
del: "Delete",
|
||||
tab: "Tab",
|
||||
home: "Home",
|
||||
};
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
if (!token.startsWith("{") || !token.endsWith("}")) {
|
||||
await page.keyboard.type(token);
|
||||
continue;
|
||||
}
|
||||
const name = token.slice(1, -1).toLowerCase();
|
||||
if (name === "selectall") {
|
||||
await page.keyboard.press(isMac ? "Meta+a" : "Control+a");
|
||||
} else if (namedKeys[name]) {
|
||||
await page.keyboard.press(namedKeys[name]);
|
||||
} else if (modifierMap[name]) {
|
||||
const modifiers = [modifierMap[name]];
|
||||
let j = i + 1;
|
||||
while (j < tokens.length && /^\{(meta|ctrl|shift|alt)\}$/i.test(tokens[j])) {
|
||||
modifiers.push(modifierMap[tokens[j].slice(1, -1).toLowerCase()]);
|
||||
j++;
|
||||
}
|
||||
const key = tokens[j] ?? "";
|
||||
await page.keyboard.press([...modifiers, key].join("+"));
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function centerOf(locator: Locator): Promise<{ x: number; y: number }> {
|
||||
const box = await locator.boundingBox();
|
||||
if (!box) throw new Error("Element has no bounding box");
|
||||
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
||||
}
|
||||
|
||||
/**
|
||||
* This is where all plywright-specific test helpers live.
|
||||
* It is used by the MaputnikDriver to implement the Maputnik-specific test helpers.
|
||||
*/
|
||||
export class PlaywrightHelper {
|
||||
private readonly recordedRequests = new Map<string, Request[]>();
|
||||
|
||||
private get page(): Page {
|
||||
return currentPage();
|
||||
}
|
||||
|
||||
private testId(testId: string): Locator {
|
||||
return this.page.locator(testIdSelector(testId));
|
||||
}
|
||||
|
||||
/** Reads and parses a JSON fixture from the fixtures directory. */
|
||||
public readFixture(name: string): any {
|
||||
return JSON.parse(fs.readFileSync(path.join(FIXTURES_DIR, name), "utf-8"));
|
||||
}
|
||||
|
||||
/** Wraps a lazily-evaluated value so assertions on it auto-retry. */
|
||||
public query<T>(getter: () => Promise<T>): Query<T> {
|
||||
return new Query<T>(getter);
|
||||
}
|
||||
|
||||
/** Entry point for fluent assertions over a Locator or a value/Query. */
|
||||
public then = <T>(target: T): Assertable<T> => new Assertable(target);
|
||||
|
||||
public given = {
|
||||
intercept: async (pattern: RegExp, alias: string, _method = "GET") => {
|
||||
this.recordedRequests.set(alias, []);
|
||||
await this.page.route(pattern, (route) => {
|
||||
this.recordedRequests.get(alias)!.push(route.request());
|
||||
route.continue();
|
||||
});
|
||||
},
|
||||
|
||||
interceptAndMockResponse: async (options: {
|
||||
method?: string;
|
||||
url: string | RegExp;
|
||||
response: unknown | { fixture: string };
|
||||
alias?: string;
|
||||
}) => {
|
||||
const { url, response, alias } = options;
|
||||
if (alias) this.recordedRequests.set(alias, []);
|
||||
await this.page.route(url, (route) => {
|
||||
if (alias) this.recordedRequests.get(alias)!.push(route.request());
|
||||
const body =
|
||||
response && typeof response === "object" && "fixture" in (response as any)
|
||||
? this.readFixture((response as { fixture: string }).fixture)
|
||||
: response;
|
||||
route.fulfill({ json: body });
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
public when = {
|
||||
visit: async (url: string) => {
|
||||
// Snapshot coverage before navigating, since a full page load resets it.
|
||||
await recordCoverageChunk(this.page);
|
||||
await this.page.goto(url);
|
||||
},
|
||||
|
||||
wait: (ms: number) => this.page.waitForTimeout(ms),
|
||||
|
||||
tab: () => this.page.keyboard.press("Tab"),
|
||||
|
||||
typeKeys: (keys: string) => typeSequence(this.page, keys),
|
||||
|
||||
/** Types raw text into the focused element (no "{key}" sequence parsing). */
|
||||
typeText: (text: string) => this.page.keyboard.type(text),
|
||||
|
||||
clickButtonByName: async (name: string) => {
|
||||
await this.page.getByRole("button", { name }).click();
|
||||
},
|
||||
|
||||
click: async (testId: string, index = 0) => {
|
||||
// Documentation buttons are wrapped in a <label>/.maputnik-doc-target that
|
||||
// Playwright treats as intercepting the click; bypass the check for them.
|
||||
const force = testId.startsWith("field-doc-button-");
|
||||
await this.testId(testId).nth(index).click({ force });
|
||||
},
|
||||
|
||||
realClick: async (testId: string) => {
|
||||
await this.testId(testId).click();
|
||||
},
|
||||
|
||||
hover: async (testId: string) => {
|
||||
await this.testId(testId).hover();
|
||||
},
|
||||
|
||||
focus: async (testId: string) => {
|
||||
await this.testId(testId).focus();
|
||||
},
|
||||
|
||||
clear: async (testId: string) => {
|
||||
await this.testId(testId).clear();
|
||||
},
|
||||
|
||||
select: async (testId: string, value: string) => {
|
||||
await this.testId(testId).selectOption(value);
|
||||
},
|
||||
|
||||
selectWithin: async (parentTestId: string, value: string) => {
|
||||
await this.testId(parentTestId).locator("select").selectOption(value);
|
||||
},
|
||||
|
||||
clickWithin: async (parentTestId: string, selector: string) => {
|
||||
await this.testId(parentTestId).locator(selector).first().click();
|
||||
},
|
||||
|
||||
clickByText: async (text: string) => {
|
||||
await this.page.getByText(text).click();
|
||||
},
|
||||
|
||||
clickByAttribute: async (attribute: string, value: string) => {
|
||||
await this.page.locator(`[${attribute}="${value}"]`).click();
|
||||
},
|
||||
|
||||
scrollToBottom: async (element: Locator) => {
|
||||
await element.evaluate((el) => el.scrollTo(0, el.scrollHeight));
|
||||
},
|
||||
|
||||
setValue: async (testId: string, text: string) => {
|
||||
const input = this.testId(testId);
|
||||
await input.fill("");
|
||||
await input.fill(text);
|
||||
},
|
||||
|
||||
type: async (testId: string, text: string) => {
|
||||
await this.testId(testId).focus();
|
||||
// Place the caret at the start of the field, so a leading "{backspace}"
|
||||
// is a no-op rather than clearing an already-committed value.
|
||||
await this.page.keyboard.press("Home");
|
||||
await typeSequence(this.page, text);
|
||||
},
|
||||
|
||||
dragAndDropWithWait: async (source: string, target: string) => {
|
||||
const from = await centerOf(this.testId(source));
|
||||
const to = await centerOf(this.testId(target));
|
||||
await this.page.mouse.move(from.x, from.y);
|
||||
await this.page.mouse.down();
|
||||
await this.page.mouse.move(from.x, from.y + 10);
|
||||
await this.page.mouse.move(to.x, to.y, { steps: 10 });
|
||||
await this.page.waitForTimeout(100);
|
||||
await this.page.mouse.up();
|
||||
},
|
||||
|
||||
clickCenter: async (testId: string) => {
|
||||
const { x, y } = await centerOf(this.testId(testId));
|
||||
await this.page.mouse.move(x, y);
|
||||
await this.page.mouse.down();
|
||||
await this.page.waitForTimeout(200);
|
||||
await this.page.mouse.up();
|
||||
},
|
||||
|
||||
openFileByFixture: async (fixture: string, buttonTestId: string, inputTestId: string) => {
|
||||
const content = JSON.stringify(this.readFixture(fixture));
|
||||
const hasPicker = await this.page.evaluate(() => "showOpenFilePicker" in window);
|
||||
if (hasPicker) {
|
||||
await this.page.evaluate((fileContent) => {
|
||||
(window as any).showOpenFilePicker = async () => [
|
||||
{ getFile: async () => ({ text: async () => fileContent }) },
|
||||
];
|
||||
}, content);
|
||||
await this.testId(buttonTestId).click();
|
||||
} else {
|
||||
await this.testId(inputTestId).setInputFiles({
|
||||
name: fixture,
|
||||
mimeType: "application/json",
|
||||
buffer: Buffer.from(content),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
dropFileByFixture: async (fixture: string, dropzoneTestId: string) => {
|
||||
const content = JSON.stringify(this.readFixture(fixture));
|
||||
const dataTransfer = await this.page.evaluateHandle((fileContent) => {
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(new File([fileContent], "example-style.json", { type: "application/json" }));
|
||||
return dt;
|
||||
}, content);
|
||||
const dropzone = this.testId(dropzoneTestId);
|
||||
await dropzone.dispatchEvent("dragenter", { dataTransfer });
|
||||
await dropzone.dispatchEvent("dragover", { dataTransfer });
|
||||
await dropzone.dispatchEvent("drop", { dataTransfer });
|
||||
},
|
||||
|
||||
waitForResponse: async (alias: string) => {
|
||||
const requests = this.recordedRequests.get(alias);
|
||||
if (!requests) throw new Error(`No intercept registered for alias "${alias}"`);
|
||||
await retry(async () => {
|
||||
if (requests.length === 0) throw new Error(`No request recorded for alias "${alias}"`);
|
||||
});
|
||||
return requests[requests.length - 1];
|
||||
},
|
||||
|
||||
clearLocalStorage: () => this.page.evaluate(() => window.localStorage.clear()),
|
||||
|
||||
/** Writes to localStorage under `keyPrefix` until a QuotaExceededError is hit. */
|
||||
fillLocalStorageUntilQuota: (keyPrefix: string) =>
|
||||
this.page.evaluate((prefix) => {
|
||||
let chunkSize = 1000;
|
||||
const chunk = new Array(chunkSize).join("x");
|
||||
let index = 0;
|
||||
while (true) {
|
||||
try {
|
||||
window.localStorage.setItem(`${prefix}${index++}`, chunk);
|
||||
} catch (e: any) {
|
||||
if (e.name === "QuotaExceededError") {
|
||||
if (chunkSize <= 1) return;
|
||||
chunkSize /= 2;
|
||||
continue;
|
||||
}
|
||||
throw e; // Unexpected error
|
||||
}
|
||||
}
|
||||
}, keyPrefix),
|
||||
};
|
||||
|
||||
public get = {
|
||||
element: (selector: string) => this.page.locator(selector),
|
||||
|
||||
localStorageItem: (key: string) =>
|
||||
this.page.evaluate((k) => window.localStorage.getItem(k), key),
|
||||
|
||||
elementByTestId: (testId: string) => this.testId(testId),
|
||||
|
||||
inputValue: (testId: string) => new Query<string>(() => this.testId(testId).first().inputValue()),
|
||||
|
||||
elementsText: (testId: string) => new Query<string>(() => this.testId(testId).first().innerText()),
|
||||
|
||||
locationHash: () => new Query<string>(async () => new URL(this.page.url()).hash),
|
||||
};
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* Clears the istanbul coverage output directory before the e2e run so stale
|
||||
* coverage from previous runs is not merged into the report.
|
||||
*/
|
||||
export default function globalSetup(): void {
|
||||
const dir = path.resolve(process.cwd(), ".nyc_output");
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
/**
|
||||
* Merges the per-test istanbul coverage chunks collected in `.nyc_output` into a
|
||||
* report (configured by `.nycrc.json`) once the whole e2e run has finished.
|
||||
*/
|
||||
export default function globalTeardown(): void {
|
||||
const dir = path.resolve(process.cwd(), ".nyc_output");
|
||||
const hasCoverage = fs.existsSync(dir) && fs.readdirSync(dir).some((f) => f.endsWith(".json"));
|
||||
if (!hasCoverage) {
|
||||
console.warn("No coverage data collected; skipping coverage report.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
execFileSync("npx", ["nyc", "report"], { stdio: "inherit" });
|
||||
} catch (error) {
|
||||
// Don't fail the whole run if the report can't be generated (e.g. when
|
||||
// running against a container whose source paths differ from the host).
|
||||
console.warn("Failed to generate coverage report:", error);
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
let activePage: Page | undefined;
|
||||
const coverageChunks: unknown[] = [];
|
||||
|
||||
/** The page for the currently running test. Throws if used outside a test. */
|
||||
export function currentPage(): Page {
|
||||
if (!activePage) {
|
||||
throw new Error("No active page: a MaputnikDriver method was called outside of a running test.");
|
||||
}
|
||||
return activePage;
|
||||
}
|
||||
|
||||
const OUTPUT_DIR = path.resolve(process.cwd(), ".nyc_output");
|
||||
|
||||
/**
|
||||
* Reads the istanbul coverage object (injected by vite-plugin-istanbul) from the
|
||||
* given page. Returns `null` when the page has not been instrumented.
|
||||
*/
|
||||
async function readCoverage(page: Page): Promise<unknown | null> {
|
||||
try {
|
||||
return await page.evaluate(() => (window as unknown as { __coverage__?: unknown }).__coverage__ ?? null);
|
||||
} catch {
|
||||
// Page might be navigating/closed.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists a coverage chunk to `.nyc_output` so that `nyc report` can merge it.
|
||||
* istanbul-lib-coverage (used by nyc) sums the hit counts across every file it
|
||||
* finds, so writing one file per chunk is enough to accumulate coverage across
|
||||
* navigations and tests.
|
||||
*/
|
||||
export function writeCoverage(coverage: unknown, id: string): void {
|
||||
if (!coverage) return;
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
fs.writeFileSync(path.join(OUTPUT_DIR, `playwright-${id}.json`), JSON.stringify(coverage));
|
||||
}
|
||||
|
||||
/** Records a coverage snapshot (called before navigations, which reset __coverage__). */
|
||||
export async function recordCoverageChunk(page: Page): Promise<void> {
|
||||
const chunk = await readCoverage(page);
|
||||
if (chunk) coverageChunks.push(chunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto fixture that binds the current test's page for the (page-lazy)
|
||||
* MaputnikDriver, auto-accepts confirm dialogs, and writes the istanbul
|
||||
* coverage collected during the test to `.nyc_output`.
|
||||
*/
|
||||
const extendedTest = test.extend<{ maputnikPage: void }>({
|
||||
maputnikPage: [
|
||||
async ({ page }, use, testInfo) => {
|
||||
activePage = page;
|
||||
coverageChunks.length = 0;
|
||||
// Accept confirm dialogs (e.g. the "replace current style" prompt). These
|
||||
// are dismissed by default, which would cancel loading a style via URL.
|
||||
page.on("dialog", (dialog) => dialog.accept().catch(() => undefined));
|
||||
|
||||
await use();
|
||||
|
||||
const finalCoverage = await readCoverage(page);
|
||||
if (finalCoverage) coverageChunks.push(finalCoverage);
|
||||
coverageChunks.forEach((chunk, index) => writeCoverage(chunk, `${testInfo.testId}-${index}`));
|
||||
coverageChunks.length = 0;
|
||||
activePage = undefined;
|
||||
},
|
||||
{ auto: true },
|
||||
],
|
||||
});
|
||||
|
||||
const describe = extendedTest.describe;
|
||||
const beforeEach = extendedTest.beforeEach;
|
||||
export {
|
||||
expect,
|
||||
describe,
|
||||
extendedTest as test,
|
||||
beforeEach,
|
||||
};
|
||||
@@ -1,70 +0,0 @@
|
||||
import eslint from "@eslint/js";
|
||||
import {defineConfig} from "eslint/config";
|
||||
import stylisticTs from "@stylistic/eslint-plugin";
|
||||
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 defineConfig({
|
||||
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,
|
||||
"@stylistic": stylisticTs
|
||||
},
|
||||
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": "off",
|
||||
"@stylistic/indent": ["error", 2],
|
||||
"semi": "off",
|
||||
"@stylistic/semi": ["error", "always"],
|
||||
"quotes": "off",
|
||||
"@stylistic/quotes": ["error", "double", { avoidEscape: true }],
|
||||
"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",
|
||||
"@typescript-eslint/consistent-type-imports": ["error", { "fixStyle": "inline-type-imports" }],
|
||||
|
||||
},
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: true,
|
||||
noInlineConfig: false
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1,10 +0,0 @@
|
||||
import { defineConfig } from "i18next-cli";
|
||||
|
||||
export default defineConfig({
|
||||
locales: ["de", "fr", "he", "it", "ja", "ko", "tr", "zh"],
|
||||
extract: {
|
||||
input: ["src/**/*.{js,jsx,ts,tsx}"],
|
||||
output: "src/locales/{{language}}/{{namespace}}.json",
|
||||
defaultValue: "__STRING_NOT_TRANSLATED__"
|
||||
}
|
||||
});
|
||||
+5
-4
@@ -4,8 +4,8 @@
|
||||
<meta charset="utf-8">
|
||||
<title>Maputnik</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="manifest" href="src/manifest.json">
|
||||
<link rel="icon" href="src/favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="/maputnik/assets/manifest-BrZzkYP9.json">
|
||||
<link rel="icon" href="/maputnik/assets/favicon-DBn6BKLx.ico" type="image/x-icon" />
|
||||
<style>
|
||||
html {
|
||||
background-color: rgb(28, 31, 36);
|
||||
@@ -37,6 +37,8 @@
|
||||
}
|
||||
|
||||
</style>
|
||||
<script type="module" crossorigin src="/maputnik/assets/index-CCCYy6bO.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/maputnik/assets/index-CeR5SWEc.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- From <https://github.com/hail2u/color-blindness-emulation> -->
|
||||
@@ -123,10 +125,9 @@
|
||||
<div id="app"></div>
|
||||
<div class="loading">
|
||||
<div class="loading__logo">
|
||||
<img inline src="node_modules/maputnik-design/logos/logo-loading.svg" />
|
||||
<img inline src="data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20xmlns:xlink='http://www.w3.org/1999/xlink'%20width='1200'%20height='1200'%20viewBox='0%200%20100%20100'%3e%3cstyle%3e@keyframes%20circle-anim{0%25,40%25{fill-opacity:0}60%25,to{fill-opacity:1}}.circle0,.circle1,.circle2,.circle3,.circle4,.circle5{stroke-opacity:0;animation-name:circle-anim;will-change:transform;animation-timing-function:east-in-out;animation-duration:800ms;animation-iteration-count:infinite;animation-direction:alternate}.circle0{animation-delay:100ms}.circle1{animation-delay:200ms}.circle2{animation-delay:300ms}.circle3{animation-delay:400ms}.circle4{animation-delay:500ms}.circle5{animation-delay:600ms}%3c/style%3e%3cg%20class='map'%20stroke='%23000'%3e%3cuse%20xlink:href='%23ref-1--map__main'%20fill='%234eba6f'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--map__line1'%20fill='none'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--map__line2'%20fill='none'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--map__line3'%20fill='none'%3e%3c/use%3e%3c/g%3e%3cg%20class='palette'%3e%3cuse%20xlink:href='%23ref-1--palette__main'%20fill='%23fff'%20stroke='%23000'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--palette__inner'%20fill='none'%20stroke='%23000'%3e%3c/use%3e%3cuse%20class='circle5'%20xlink:href='%23ref-1--palette__circle5'%20fill='%23f7c44c'%3e%3c/use%3e%3cuse%20class='circle4'%20xlink:href='%23ref-1--palette__circle4'%20fill='%234eba6f'%3e%3c/use%3e%3cuse%20class='circle3'%20xlink:href='%23ref-1--palette__circle3'%20fill='%23f7c44c'%3e%3c/use%3e%3cuse%20class='circle2'%20xlink:href='%23ref-1--palette__circle2'%20fill='%234eba6f'%3e%3c/use%3e%3cuse%20class='circle1'%20xlink:href='%23ref-1--palette__circle1'%20fill='%23f7c44c'%3e%3c/use%3e%3cuse%20class='circle0'%20xlink:href='%23ref-1--palette__circle0'%20fill='%234eba6f'%3e%3c/use%3e%3c/g%3e%3cg%20class='brush'%20stroke='%23000'%3e%3cuse%20xlink:href='%23ref-1--brush__bottom'%20fill='%23f7c44c'%3e%3c/use%3e%3cuse%20xlink:href='%23ref-1--brush__top'%20fill='%23fff'%3e%3c/use%3e%3c/g%3e%3cdefs%3e%3cpath%20id='ref-1--map__main'%20stroke-width='2.366'%20stroke-linejoin='round'%20d='M18.84%207.717l15.44%207.542%2015.75-7.762%2015.7%207.857L81.005%207.67%2096.31%2054.052%2073.598%2062.12%2050.93%2053.872l-25.1%208.066-22.668-8.066z'%3e%3c/path%3e%3cpath%20id='ref-1--map__line1'%20d='M65.556%2015.07l7.647%2046.838'%20stroke-width='1.104'%3e%3c/path%3e%3cpath%20id='ref-1--map__line2'%20d='M50.261%207.422l.717%2046.6'%20stroke-width='1.104'%3e%3c/path%3e%3cpath%20id='ref-1--map__line3'%20d='M34.011%2015.07l-8.603%2046.6'%20stroke-width='1.104'%3e%3c/path%3e%3cpath%20id='ref-1--palette__main'%20stroke-width='2.3'%20d='M47.352%2030.887c7.993.226%2016.934%209.725%2017.954%2015.25%201.02%205.527-.743%2011.125-4.298%2013.875-3.554%202.75-8.6%202.905-8.723%208.302-.097%204.237%208.457%208.5%208.088%2015.653-.406%207.857-15.508%2013.15-30.943%206.102-8.556-3.906-14.249-13.653-13.385-26.238C16.833%2052.334%2022.32%2043.658%2027.382%2039c5.977-5.503%2011.977-8.337%2019.97-8.112z'%3e%3c/path%3e%3ccircle%20id='ref-1--palette__inner'%20stroke-width='2.3'%20cx='41.873'%20cy='61.901'%20r='6.389'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle5'%20cy='44.56'%20cx='54.347'%20r='4.336'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle4'%20cx='40.443'%20cy='41.555'%20r='4.336'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle3'%20r='4.336'%20cy='51.102'%20cx='29.651'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle2'%20cx='25.293'%20cy='65.836'%20r='4.336'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle1'%20r='4.336'%20cy='79.326'%20cx='32.764'%3e%3c/circle%3e%3ccircle%20id='ref-1--palette__circle0'%20cx='46.669'%20cy='80.571'%20r='4.336'%3e%3c/circle%3e%3cpath%20id='ref-1--brush__bottom'%20d='M76.333%2089.333c-1.645-9.794-4.375-35.26-4.32-37.887.056-2.627%202.52-4.34%205.36-4.317%202.842.022%205.098%201.87%205.314%204.27.107%201.2-1.576%2028.06-2.318%2037.844-.332%204.374-3.31%204.413-4.036.09z'%20stroke-width='2.3'%20stroke-linejoin='round'%3e%3c/path%3e%3cpath%20id='ref-1--brush__top'%20stroke-linejoin='round'%20stroke-width='2.3'%20d='M77.184%2026.428s-5.621%207.02-5.621%2011.978c0%204.957%202.206%206.878%205.81%206.878%203.606%200%205.148-1.708%205.29-6.736.142-5.028-5.479-12.12-5.479-12.12z'%3e%3c/path%3e%3c/defs%3e%3c/svg%3e" />
|
||||
</div>
|
||||
<div class="loading__text">Loading…</div>
|
||||
</div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Generated
-14073
File diff suppressed because it is too large
Load Diff
-153
@@ -1,153 +0,0 @@
|
||||
{
|
||||
"name": "maputnik",
|
||||
"version": "3.1.0",
|
||||
"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:extract": "npx i18next-cli extract",
|
||||
"lint": "eslint",
|
||||
"test": "playwright test",
|
||||
"test-e2e": "playwright test",
|
||||
"test-unit": "vitest",
|
||||
"test-unit-ci": "vitest run --coverage --reporter=json",
|
||||
"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"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maplibre/maputnik"
|
||||
},
|
||||
"author": "Lukas Martinelli",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/maplibre/maputnik#readme",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lint": "^6.9.7",
|
||||
"@codemirror/state": "^6.7.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.43.5",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.4.0",
|
||||
"@maplibre/maplibre-gl-geocoder": "^1.9.4",
|
||||
"@maplibre/maplibre-gl-inspect": "^1.8.2",
|
||||
"@maplibre/maplibre-gl-style-spec": "^25.0.2",
|
||||
"array-move": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.5.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"color": "^5.0.3",
|
||||
"detect-browser": "^5.3.0",
|
||||
"downshift": "^9.4.0",
|
||||
"events": "^3.3.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^26.3.4",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"json-to-ast": "^2.1.0",
|
||||
"lodash": "^4.18.1",
|
||||
"lodash.capitalize": "^4.2.1",
|
||||
"lodash.clamp": "^4.0.3",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"maplibre-gl": "^5.24.0",
|
||||
"maputnik-design": "github:maputnik/design#172b06c",
|
||||
"ol": "^10.9.0",
|
||||
"ol-mapbox-style": "^13.4.1",
|
||||
"pmtiles": "^4.4.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.2.7",
|
||||
"react-accessible-accordion": "^5.0.1",
|
||||
"react-aria-menubutton": "^8.0.0",
|
||||
"react-aria-modal": "^5.0.2",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-i18next": "^17.0.8",
|
||||
"react-icons": "^5.7.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"slugify": "^1.6.9",
|
||||
"string-hash": "^1.1.3",
|
||||
"url": "^0.11.4"
|
||||
},
|
||||
"jshintConfig": {
|
||||
"esversion": 6
|
||||
},
|
||||
"stylelint": {
|
||||
"extends": "stylelint-config-recommended-scss",
|
||||
"rules": {
|
||||
"no-descending-specificity": null,
|
||||
"media-feature-name-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreMediaFeatureNames": [
|
||||
"prefers-reduced-motion"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@types/codemirror": "^5.60.17",
|
||||
"@types/color": "^4.2.1",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@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": "^19.2.17",
|
||||
"@types/react-aria-menubutton": "^6.2.14",
|
||||
"@types/react-aria-modal": "^5.0.0",
|
||||
"@types/react-collapse": "^5.0.4",
|
||||
"@types/react-color": "^3.0.13",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/string-hash": "^1.1.3",
|
||||
"@types/wicg-file-system-access": "^2023.10.7",
|
||||
"@vitejs/plugin-react": "5.2",
|
||||
"@vitest/browser": "^4.1.10",
|
||||
"@vitest/browser-playwright": "^4.1.10",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"cors": "^2.8.6",
|
||||
"eslint": "^10.6.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.3",
|
||||
"i18next-cli": "^1.65.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"nyc": "^18.0.0",
|
||||
"postcss": "^8.5.16",
|
||||
"react-hot-loader": "^4.13.1",
|
||||
"sass": "^1.101.0",
|
||||
"stylelint": "^17.14.0",
|
||||
"stylelint-config-recommended-scss": "^17.0.1",
|
||||
"stylelint-scss": "^7.2.0",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.62.1",
|
||||
"uuid": "^14.0.1",
|
||||
"vite": "^7.3.2",
|
||||
"vite-plugin-istanbul": "^9.0.1",
|
||||
"vitest": "^4.1.10",
|
||||
"vitest-browser-react": "^2.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const isCI = !!process.env.CI;
|
||||
// When the app is already served elsewhere (e.g. the docker e2e job) set
|
||||
// E2E_NO_WEBSERVER=1 so Playwright does not start its own dev server.
|
||||
const useExternalServer = !!process.env.E2E_NO_WEBSERVER;
|
||||
const baseURL = process.env.E2E_BASE_URL ?? "http://localhost:8888/";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./e2e",
|
||||
testMatch: "**/*.spec.ts",
|
||||
globalSetup: "./e2e/utils/e2e-setup.ts",
|
||||
globalTeardown: "./e2e/utils/e2e-teardown.ts",
|
||||
fullyParallel: true,
|
||||
forbidOnly: isCI,
|
||||
retries: isCI ? 2 : 0,
|
||||
reporter: isCI ? [["list"], ["html", { open: "never" }]] : "list",
|
||||
use: {
|
||||
baseURL,
|
||||
trace: "on-first-retry",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
launchOptions: {
|
||||
// Allow WebGL (maplibre) to fall back to software rendering in headless.
|
||||
args: [
|
||||
"--disable-gpu",
|
||||
"--enable-features=AllowSwiftShaderFallback,AllowSoftwareGLFallbackDueToCrashes",
|
||||
"--enable-unsafe-swiftshader",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webServer: useExternalServer
|
||||
? undefined
|
||||
: {
|
||||
command: "npm run start",
|
||||
url: "http://localhost:8888/maputnik/",
|
||||
reuseExistingServer: !isCI,
|
||||
timeout: 120000,
|
||||
},
|
||||
});
|
||||
@@ -1,993 +0,0 @@
|
||||
import React from "react";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import clamp from "lodash.clamp";
|
||||
import buffer from "buffer";
|
||||
import get from "lodash.get";
|
||||
import {unset} from "lodash";
|
||||
import {arrayMoveMutable} from "array-move";
|
||||
import hash from "string-hash";
|
||||
import { PMTiles } from "pmtiles";
|
||||
import {type Map, type LayerSpecification, type StyleSpecification, type ValidationError, type SourceSpecification} from "maplibre-gl";
|
||||
import {validateStyleMin} from "@maplibre/maplibre-gl-style-spec";
|
||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||
|
||||
import MapMaplibreGl from "./MapMaplibreGl";
|
||||
import MapOpenLayers from "./MapOpenLayers";
|
||||
import CodeEditor from "./CodeEditor";
|
||||
import LayerList from "./LayerList";
|
||||
import LayerEditor from "./LayerEditor";
|
||||
import AppToolbar, { type MapState } from "./AppToolbar";
|
||||
import AppLayout from "./AppLayout";
|
||||
import MessagePanel from "./AppMessagePanel";
|
||||
|
||||
import ModalSettings from "./modals/ModalSettings";
|
||||
import ModalExport from "./modals/ModalExport";
|
||||
import ModalSources from "./modals/ModalSources";
|
||||
import ModalOpen from "./modals/ModalOpen";
|
||||
import ModalShortcuts from "./modals/ModalShortcuts";
|
||||
import ModalDebug from "./modals/ModalDebug";
|
||||
import ModalGlobalState from "./modals/ModalGlobalState";
|
||||
|
||||
import {downloadGlyphsMetadata, downloadSpriteMetadata} from "../libs/metadata";
|
||||
import style from "../libs/style";
|
||||
import { undoMessages, redoMessages } from "../libs/diffmessage";
|
||||
import { createStyleStore, type IStyleStore } from "../libs/store/style-store-factory";
|
||||
import { RevisionStore } from "../libs/revisions";
|
||||
import LayerWatcher from "../libs/layerwatcher";
|
||||
import tokens from "../config/tokens.json";
|
||||
import isEqual from "lodash.isequal";
|
||||
import { type MapOptions } from "maplibre-gl";
|
||||
import { type MappedError, type OnStyleChangedOpts, type StyleSpecificationWithId } from "../libs/definitions";
|
||||
|
||||
// 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) {
|
||||
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) {
|
||||
return url.replace("{key}", accessToken);
|
||||
}
|
||||
}
|
||||
else if (matchesThunderforest) {
|
||||
const accessToken = style.getAccessToken("thunderforest", mapStyle, {allowFallback: true});
|
||||
if (accessToken) {
|
||||
return url.replace("{key}", accessToken);
|
||||
}
|
||||
}
|
||||
else if (matchesLocationIQ) {
|
||||
const accessToken = style.getAccessToken("locationiq", mapStyle, {allowFallback: true});
|
||||
if (accessToken) {
|
||||
return url.replace("{key}", accessToken);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
function updateRootSpec(spec: any, fieldName: string, newValues: any) {
|
||||
return {
|
||||
...spec,
|
||||
$root: {
|
||||
...spec.$root,
|
||||
[fieldName]: {
|
||||
...spec.$root[fieldName],
|
||||
values: newValues
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
type AppState = {
|
||||
errors: MappedError[],
|
||||
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,
|
||||
},
|
||||
_from: "map" | "app"
|
||||
},
|
||||
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
|
||||
globalState: boolean
|
||||
codeEditor: boolean
|
||||
}
|
||||
fileHandle: FileSystemFileHandle | null
|
||||
};
|
||||
|
||||
export default class App extends React.Component<any, AppState> {
|
||||
revisionStore: RevisionStore;
|
||||
styleStore: IStyleStore | null = null;
|
||||
layerWatcher: LayerWatcher;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
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,
|
||||
},
|
||||
_from: "app"
|
||||
},
|
||||
isOpen: {
|
||||
settings: false,
|
||||
sources: false,
|
||||
open: false,
|
||||
shortcuts: false,
|
||||
export: false,
|
||||
debug: false,
|
||||
globalState: false,
|
||||
codeEditor: false
|
||||
},
|
||||
maplibreGlDebugOptions: {
|
||||
showTileBoundaries: false,
|
||||
showCollisionBoxes: false,
|
||||
showOverdrawInspector: false,
|
||||
},
|
||||
openlayersDebugOptions: {
|
||||
debugToolbox: false,
|
||||
},
|
||||
fileHandle: null,
|
||||
};
|
||||
|
||||
this.layerWatcher = new LayerWatcher({
|
||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
||||
});
|
||||
}
|
||||
|
||||
configureKeyboardShortcuts = () => {
|
||||
const shortcuts = [
|
||||
{
|
||||
key: "?",
|
||||
handler: () => {
|
||||
this.toggleModal("shortcuts");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "o",
|
||||
handler: () => {
|
||||
this.toggleModal("open");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "e",
|
||||
handler: () => {
|
||||
this.toggleModal("export");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "d",
|
||||
handler: () => {
|
||||
this.toggleModal("sources");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "s",
|
||||
handler: () => {
|
||||
this.toggleModal("settings");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "g",
|
||||
handler: () => {
|
||||
this.toggleModal("globalState");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "i",
|
||||
handler: () => {
|
||||
this.setMapState(
|
||||
this.state.mapState === "map" ? "inspect" : "map"
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "m",
|
||||
handler: () => {
|
||||
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "!",
|
||||
handler: () => {
|
||||
this.toggleModal("debug");
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
document.body.addEventListener("keyup", (e) => {
|
||||
if(e.key === "Escape") {
|
||||
(e.target as HTMLElement).blur();
|
||||
document.body.focus();
|
||||
}
|
||||
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
|
||||
const shortcut = shortcuts.find((shortcut) => {
|
||||
return (shortcut.key === e.key);
|
||||
});
|
||||
|
||||
if(shortcut) {
|
||||
this.setModal("shortcuts", false);
|
||||
shortcut.handler();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleKeyPress = (e: KeyboardEvent) => {
|
||||
if(navigator.platform.toUpperCase().indexOf("MAC") >= 0) {
|
||||
if(e.metaKey && e.shiftKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onRedo();
|
||||
}
|
||||
else if(e.metaKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onUndo();
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(e.ctrlKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onUndo();
|
||||
}
|
||||
else if(e.ctrlKey && e.keyCode === 89) {
|
||||
e.preventDefault();
|
||||
this.onRedo();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
this.styleStore = await createStyleStore((mapStyle, opts) => this.onStyleChanged(mapStyle, opts));
|
||||
window.addEventListener("keydown", this.handleKeyPress);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("keydown", this.handleKeyPress);
|
||||
}
|
||||
|
||||
saveStyle(snapshotStyle: StyleSpecificationWithId) {
|
||||
this.styleStore?.save(snapshotStyle);
|
||||
}
|
||||
|
||||
updateFonts(urlTemplate: string) {
|
||||
const metadata: {[key: string]: string} = this.state.mapStyle.metadata || {} as any;
|
||||
const accessToken = metadata["maputnik:openmaptiles_access_token"] || tokens.openmaptiles;
|
||||
|
||||
const glyphUrl = (typeof urlTemplate === "string")? urlTemplate.replace("{key}", accessToken): urlTemplate;
|
||||
downloadGlyphsMetadata(glyphUrl).then(fonts => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, "glyphs", fonts)});
|
||||
});
|
||||
}
|
||||
|
||||
updateIcons(baseUrl: string) {
|
||||
downloadSpriteMetadata(baseUrl).then(icons => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, "sprite", icons)});
|
||||
});
|
||||
}
|
||||
|
||||
onChangeMetadataProperty = (property: string, value: any) => {
|
||||
// If we're changing renderer reset the map state.
|
||||
if (
|
||||
property === "maputnik:renderer" &&
|
||||
value !== get(this.state.mapStyle, ["metadata", "maputnik:renderer"], "mlgljs")
|
||||
) {
|
||||
this.setState({
|
||||
mapState: "map"
|
||||
});
|
||||
}
|
||||
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
metadata: {
|
||||
...(this.state.mapStyle as any).metadata,
|
||||
[property]: value
|
||||
}
|
||||
};
|
||||
|
||||
this.onStyleChanged(changedStyle);
|
||||
};
|
||||
|
||||
onStyleChanged = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}): void => {
|
||||
opts = {
|
||||
save: true,
|
||||
addRevision: true,
|
||||
initialLoad: false,
|
||||
...opts,
|
||||
};
|
||||
|
||||
|
||||
// Detect empty style
|
||||
const oldStyle = this.state.mapStyle;
|
||||
const isEmptySources = !oldStyle.sources || Object.keys(oldStyle.sources).length === 0;
|
||||
const isEmptyLayers = !oldStyle.layers || oldStyle.layers.length === 0;
|
||||
const isEmptyStyle = isEmptySources && isEmptyLayers;
|
||||
|
||||
// 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) || [];
|
||||
// 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)[] = [];
|
||||
if (newStyle && newStyle.layers) {
|
||||
const foundLayers = new global.Map();
|
||||
newStyle.layers.forEach((layer, index) => {
|
||||
if (layer.id === "" && foundLayers.has(layer.id)) {
|
||||
const error = new Error(
|
||||
`layers[${index}]: duplicate layer id [empty_string], previously used`
|
||||
);
|
||||
layerErrors.push(error);
|
||||
}
|
||||
foundLayers.set(layer.id, true);
|
||||
});
|
||||
}
|
||||
|
||||
const mappedErrors: MappedError[] = layerErrors.concat(errors).map(error => {
|
||||
// Special case: Duplicate layer id
|
||||
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
|
||||
if (dupMatch) {
|
||||
const [, index, message] = dupMatch;
|
||||
return {
|
||||
message: error.message,
|
||||
parsed: {
|
||||
type: "layer",
|
||||
data: {
|
||||
index: parseInt(index, 10),
|
||||
key: "id",
|
||||
message,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Special case: Invalid source
|
||||
const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/);
|
||||
if (invalidSourceMatch) {
|
||||
const [, index, message] = invalidSourceMatch;
|
||||
return {
|
||||
message: error.message,
|
||||
parsed: {
|
||||
type: "layer",
|
||||
data: {
|
||||
index: parseInt(index, 10),
|
||||
key: "source",
|
||||
message,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
|
||||
if (layerMatch) {
|
||||
const [, index, group, property, message] = layerMatch;
|
||||
const key = (group && property) ? [group, property].join(".") : property;
|
||||
return {
|
||||
message: error.message,
|
||||
parsed: {
|
||||
type: "layer",
|
||||
data: {
|
||||
index: parseInt(index, 10),
|
||||
key,
|
||||
message
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
let dirtyMapStyle: StyleSpecification | undefined = undefined;
|
||||
if (errors.length > 0) {
|
||||
dirtyMapStyle = cloneDeep(newStyle);
|
||||
|
||||
for (const error of errors) {
|
||||
const {message} = error;
|
||||
if (message) {
|
||||
try {
|
||||
const objPath = message.split(":")[0];
|
||||
// Errors can be deeply 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];
|
||||
unset(dirtyMapStyle, unsetPath);
|
||||
}
|
||||
catch (err) {
|
||||
console.warn(message + " " + err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||
this.updateFonts(newStyle.glyphs as string);
|
||||
}
|
||||
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
||||
this.updateIcons(newStyle.sprite as string);
|
||||
}
|
||||
|
||||
if (opts.addRevision) {
|
||||
this.revisionStore.addRevision(newStyle);
|
||||
}
|
||||
if (opts.save) {
|
||||
this.saveStyle(newStyle);
|
||||
}
|
||||
|
||||
const zoom = newStyle?.zoom;
|
||||
const center = newStyle?.center;
|
||||
|
||||
this.setState({
|
||||
mapStyle: newStyle,
|
||||
dirtyMapStyle: dirtyMapStyle,
|
||||
mapView: isEmptyStyle && zoom && center ? {
|
||||
zoom: zoom,
|
||||
center: {
|
||||
lng: center[0],
|
||||
lat: center[1],
|
||||
},
|
||||
_from: "app"
|
||||
} : this.state.mapView,
|
||||
errors: mappedErrors,
|
||||
}, () => {
|
||||
this.fetchSources();
|
||||
this.setStateInUrl();
|
||||
});
|
||||
};
|
||||
|
||||
onUndo = () => {
|
||||
const activeStyle = this.revisionStore.undo();
|
||||
|
||||
const messages = undoMessages(this.state.mapStyle, activeStyle);
|
||||
this.onStyleChanged(activeStyle, {addRevision: false});
|
||||
this.setState({
|
||||
infos: messages,
|
||||
});
|
||||
};
|
||||
|
||||
onRedo = () => {
|
||||
const activeStyle = this.revisionStore.redo();
|
||||
const messages = redoMessages(this.state.mapStyle, activeStyle);
|
||||
this.onStyleChanged(activeStyle, {addRevision: false});
|
||||
this.setState({
|
||||
infos: messages,
|
||||
});
|
||||
};
|
||||
|
||||
onMoveLayer = (move: {oldIndex: number; newIndex: number}) => {
|
||||
let { oldIndex, newIndex } = move;
|
||||
let layers = this.state.mapStyle.layers;
|
||||
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
||||
newIndex = clamp(newIndex, 0, layers.length-1);
|
||||
if(oldIndex === newIndex) return;
|
||||
|
||||
if (oldIndex === this.state.selectedLayerIndex) {
|
||||
this.setState({
|
||||
selectedLayerIndex: newIndex
|
||||
});
|
||||
}
|
||||
|
||||
layers = layers.slice(0);
|
||||
arrayMoveMutable(layers, oldIndex, newIndex);
|
||||
this.onLayersChange(layers);
|
||||
};
|
||||
|
||||
onLayersChange = (changedLayers: LayerSpecification[]) => {
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
layers: changedLayers
|
||||
};
|
||||
this.onStyleChanged(changedStyle);
|
||||
};
|
||||
|
||||
onLayerDestroy = (index: number) => {
|
||||
const 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;
|
||||
const changedLayers = layers.slice(0);
|
||||
|
||||
const clonedLayer = cloneDeep(changedLayers[index]);
|
||||
clonedLayer.id = clonedLayer.id + "-copy";
|
||||
changedLayers.splice(index, 0, clonedLayer);
|
||||
this.onLayersChange(changedLayers);
|
||||
};
|
||||
|
||||
onLayerVisibilityToggle = (index: number) => {
|
||||
const layers = this.state.mapStyle.layers;
|
||||
const changedLayers = layers.slice(0);
|
||||
|
||||
const layer = { ...changedLayers[index] };
|
||||
const changedLayout = "layout" in layer ? {...layer.layout} : {};
|
||||
changedLayout.visibility = changedLayout.visibility === "none" ? "visible" : "none";
|
||||
|
||||
layer.layout = changedLayout;
|
||||
changedLayers[index] = layer;
|
||||
this.onLayersChange(changedLayers);
|
||||
};
|
||||
|
||||
|
||||
onLayerIdChange = (index: number, _oldId: string, newId: string) => {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0);
|
||||
changedLayers[index] = {
|
||||
...changedLayers[index],
|
||||
id: newId
|
||||
};
|
||||
|
||||
this.onLayersChange(changedLayers);
|
||||
};
|
||||
|
||||
onLayerChanged = (index: number, layer: LayerSpecification) => {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0);
|
||||
changedLayers[index] = layer;
|
||||
|
||||
this.onLayersChange(changedLayers);
|
||||
};
|
||||
|
||||
setMapState = (newState: MapState) => {
|
||||
this.setState({
|
||||
mapState: newState
|
||||
}, this.setStateInUrl);
|
||||
};
|
||||
|
||||
setDefaultValues = (styleObj: StyleSpecificationWithId) => {
|
||||
const metadata: {[key: string]: string} = styleObj.metadata || {} as any;
|
||||
if(metadata["maputnik:renderer"] === undefined) {
|
||||
const changedStyle = {
|
||||
...styleObj,
|
||||
metadata: {
|
||||
...styleObj.metadata as any,
|
||||
"maputnik:renderer": "mlgljs"
|
||||
}
|
||||
};
|
||||
return changedStyle;
|
||||
} else {
|
||||
return styleObj;
|
||||
}
|
||||
};
|
||||
|
||||
openStyle = (styleObj: StyleSpecificationWithId, fileHandle: FileSystemFileHandle | null) => {
|
||||
this.setState({fileHandle: fileHandle});
|
||||
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 {
|
||||
sourceList[key] = {
|
||||
type: source.type,
|
||||
layers: []
|
||||
};
|
||||
|
||||
let url = source.url;
|
||||
|
||||
try {
|
||||
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")) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(const layer of json.vector_layers) {
|
||||
sourceList[key].layers.push(layer.id);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(`Failed to process source for url: '${url}', ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!isEqual(this.state.sources, sourceList)) {
|
||||
console.debug("Setting sources", sourceList);
|
||||
this.setState({
|
||||
sources: sourceList
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getRenderer () {
|
||||
const metadata: {[key:string]: string} = this.state.mapStyle.metadata || {} as any;
|
||||
return metadata["maputnik:renderer"] || "mlgljs";
|
||||
}
|
||||
|
||||
onMapChange = (mapView: {
|
||||
zoom: number,
|
||||
center: {
|
||||
lng: number,
|
||||
lat: number,
|
||||
},
|
||||
_from: "map" | "app"
|
||||
}) => {
|
||||
this.setState({
|
||||
mapView,
|
||||
});
|
||||
};
|
||||
|
||||
mapRenderer() {
|
||||
const {mapStyle, dirtyMapStyle} = this.state;
|
||||
|
||||
const mapProps = {
|
||||
mapStyle: (dirtyMapStyle || mapStyle),
|
||||
mapView: this.state.mapView,
|
||||
replaceAccessTokens: (mapStyle: StyleSpecification) => {
|
||||
return style.replaceAccessTokens(mapStyle, {
|
||||
allowFallback: true
|
||||
});
|
||||
},
|
||||
onDataChange: (e: {map: Map}) => {
|
||||
this.layerWatcher.analyzeMap(e.map);
|
||||
this.fetchSources();
|
||||
},
|
||||
};
|
||||
|
||||
const renderer = this._getRenderer();
|
||||
|
||||
let mapElement;
|
||||
|
||||
// Check if OL code has been loaded?
|
||||
if(renderer === "ol") {
|
||||
mapElement = <MapOpenLayers
|
||||
{...mapProps}
|
||||
onChange={this.onMapChange}
|
||||
debugToolbox={this.state.openlayersDebugOptions.debugToolbox}
|
||||
onLayerSelect={(layerId) => this.onLayerSelect(+layerId)}
|
||||
/>;
|
||||
} else {
|
||||
|
||||
mapElement = <MapMaplibreGl {...mapProps}
|
||||
onChange={this.onMapChange}
|
||||
options={this.state.maplibreGlDebugOptions}
|
||||
inspectModeEnabled={this.state.mapState === "inspect"}
|
||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
||||
onLayerSelect={this.onLayerSelect} />;
|
||||
}
|
||||
|
||||
let filterName;
|
||||
if(this.state.mapState.match(/^filter-/)) {
|
||||
filterName = this.state.mapState.replace(/^filter-/, "");
|
||||
}
|
||||
const elementStyle: {filter?: string} = {};
|
||||
if (filterName) {
|
||||
elementStyle.filter = `url('#${filterName}')`;
|
||||
}
|
||||
|
||||
return <div style={elementStyle} className="maputnik-map__container" data-wd-key="maplibre:container">
|
||||
{mapElement}
|
||||
</div>;
|
||||
}
|
||||
|
||||
setStateInUrl = () => {
|
||||
const {mapState, mapStyle, isOpen} = this.state;
|
||||
const {selectedLayerIndex} = this.state;
|
||||
const url = new URL(location.href);
|
||||
const hashVal = hash(JSON.stringify(mapStyle));
|
||||
url.searchParams.set("layer", `${hashVal}~${selectedLayerIndex}`);
|
||||
|
||||
const openModals = Object.entries(isOpen)
|
||||
.map(([key, val]) => (val === true ? key : null))
|
||||
.filter(val => val !== null);
|
||||
|
||||
if (openModals.length > 0) {
|
||||
url.searchParams.set("modal", openModals.join(","));
|
||||
}
|
||||
else {
|
||||
url.searchParams.delete("modal");
|
||||
}
|
||||
|
||||
if (mapState === "map") {
|
||||
url.searchParams.delete("view");
|
||||
}
|
||||
else if (mapState === "inspect") {
|
||||
url.searchParams.set("view", "inspect");
|
||||
}
|
||||
|
||||
history.replaceState({selectedLayerIndex}, "Maputnik", url.href);
|
||||
};
|
||||
|
||||
getInitialStateFromUrl = (mapStyle: StyleSpecification) => {
|
||||
const url = new URL(location.href);
|
||||
const modalParam = url.searchParams.get("modal");
|
||||
|
||||
if (modalParam && modalParam !== "") {
|
||||
const modals = modalParam.split(",");
|
||||
const modalObj: {[key: string]: boolean} = {};
|
||||
modals.forEach(modalName => {
|
||||
modalObj[modalName] = true;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
...modalObj,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const view = url.searchParams.get("view");
|
||||
if (view && view !== "") {
|
||||
this.setMapState(view as MapState);
|
||||
}
|
||||
|
||||
const path = url.searchParams.get("layer");
|
||||
if (path) {
|
||||
try {
|
||||
const parts = path.split("~");
|
||||
const [hashVal, selectedLayerIndex] = [
|
||||
parts[0],
|
||||
parseInt(parts[1], 10),
|
||||
];
|
||||
|
||||
let valid = true;
|
||||
if (hashVal !== "-") {
|
||||
const currentHashVal = hash(JSON.stringify(mapStyle));
|
||||
if (currentHashVal !== parseInt(hashVal, 10)) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
if (valid) {
|
||||
this.setState({
|
||||
selectedLayerIndex,
|
||||
selectedLayerOriginalId: mapStyle.layers[selectedLayerIndex].id,
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onLayerSelect = (index: number) => {
|
||||
this.setState({
|
||||
selectedLayerIndex: index,
|
||||
selectedLayerOriginalId: this.state.mapStyle.layers[index].id,
|
||||
}, this.setStateInUrl);
|
||||
};
|
||||
|
||||
setModal(modalName: keyof AppState["isOpen"], value: boolean) {
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
[modalName]: value
|
||||
}
|
||||
}, this.setStateInUrl);
|
||||
}
|
||||
|
||||
toggleModal(modalName: keyof AppState["isOpen"]) {
|
||||
this.setModal(modalName, !this.state.isOpen[modalName]);
|
||||
}
|
||||
|
||||
onSetFileHandle = (fileHandle: FileSystemFileHandle | null) => {
|
||||
this.setState({ fileHandle });
|
||||
};
|
||||
|
||||
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
|
||||
this.setState({
|
||||
openlayersDebugOptions: {
|
||||
...this.state.openlayersDebugOptions,
|
||||
[key]: value,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onChangeMaplibreGlDebug = (key: keyof AppState["maplibreGlDebugOptions"], value: any) => {
|
||||
this.setState({
|
||||
maplibreGlDebugOptions: {
|
||||
...this.state.maplibreGlDebugOptions,
|
||||
[key]: value,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const layers = this.state.mapStyle.layers || [];
|
||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : undefined;
|
||||
|
||||
const toolbar = <AppToolbar
|
||||
renderer={this._getRenderer()}
|
||||
mapState={this.state.mapState}
|
||||
mapStyle={this.state.mapStyle}
|
||||
inspectModeEnabled={this.state.mapState === "inspect"}
|
||||
sources={this.state.sources}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
onStyleOpen={this.onStyleChanged}
|
||||
onSetMapState={this.setMapState}
|
||||
onToggleModal={(modal: keyof AppState["isOpen"]) => this.toggleModal(modal)}
|
||||
/>;
|
||||
|
||||
const codeEditor = this.state.isOpen.codeEditor ? <CodeEditor
|
||||
value={this.state.mapStyle}
|
||||
onChange={(style) => this.onStyleChanged(style)}
|
||||
onClose={() => this.setModal("codeEditor", false)}
|
||||
/> : undefined;
|
||||
|
||||
const layerList = <LayerList
|
||||
onMoveLayer={this.onMoveLayer}
|
||||
onLayerDestroy={this.onLayerDestroy}
|
||||
onLayerCopy={this.onLayerCopy}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
||||
onLayersChange={this.onLayersChange}
|
||||
onLayerSelect={this.onLayerSelect}
|
||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
||||
layers={layers}
|
||||
sources={this.state.sources}
|
||||
errors={this.state.errors}
|
||||
/>;
|
||||
|
||||
const layerEditor = selectedLayer ? <LayerEditor
|
||||
key={this.state.selectedLayerOriginalId}
|
||||
layer={selectedLayer}
|
||||
layerIndex={this.state.selectedLayerIndex}
|
||||
isFirstLayer={this.state.selectedLayerIndex < 1}
|
||||
isLastLayer={this.state.selectedLayerIndex === this.state.mapStyle.layers.length-1}
|
||||
sources={this.state.sources}
|
||||
vectorLayers={this.state.vectorLayers}
|
||||
spec={this.state.spec}
|
||||
onMoveLayer={this.onMoveLayer}
|
||||
onLayerChanged={this.onLayerChanged}
|
||||
onLayerDestroy={this.onLayerDestroy}
|
||||
onLayerCopy={this.onLayerCopy}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
||||
onLayerIdChange={this.onLayerIdChange}
|
||||
errors={this.state.errors}
|
||||
/> : undefined;
|
||||
|
||||
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
||||
currentLayer={selectedLayer}
|
||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
||||
onLayerSelect={this.onLayerSelect}
|
||||
mapStyle={this.state.mapStyle}
|
||||
errors={this.state.errors}
|
||||
infos={this.state.infos}
|
||||
/> : undefined;
|
||||
|
||||
|
||||
const modals = <div>
|
||||
<ModalDebug
|
||||
renderer={this._getRenderer()}
|
||||
maplibreGlDebugOptions={this.state.maplibreGlDebugOptions}
|
||||
openlayersDebugOptions={this.state.openlayersDebugOptions}
|
||||
onChangeMaplibreGlDebug={this.onChangeMaplibreGlDebug}
|
||||
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
|
||||
isOpen={this.state.isOpen.debug}
|
||||
onOpenToggle={() => this.toggleModal("debug")}
|
||||
mapView={this.state.mapView}
|
||||
/>
|
||||
<ModalShortcuts
|
||||
isOpen={this.state.isOpen.shortcuts}
|
||||
onOpenToggle={() => this.toggleModal("shortcuts")}
|
||||
/>
|
||||
<ModalSettings
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
onChangeMetadataProperty={this.onChangeMetadataProperty}
|
||||
isOpen={this.state.isOpen.settings}
|
||||
onOpenToggle={() => this.toggleModal("settings")}
|
||||
/>
|
||||
<ModalExport
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
isOpen={this.state.isOpen.export}
|
||||
onOpenToggle={() => this.toggleModal("export")}
|
||||
fileHandle={this.state.fileHandle}
|
||||
onSetFileHandle={this.onSetFileHandle}
|
||||
/>
|
||||
<ModalOpen
|
||||
isOpen={this.state.isOpen.open}
|
||||
onStyleOpen={this.openStyle}
|
||||
onOpenToggle={() => this.toggleModal("open")}
|
||||
fileHandle={this.state.fileHandle}
|
||||
/>
|
||||
<ModalSources
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
isOpen={this.state.isOpen.sources}
|
||||
onOpenToggle={() => this.toggleModal("sources")}
|
||||
/>
|
||||
<ModalGlobalState
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
isOpen={this.state.isOpen.globalState}
|
||||
onOpenToggle={() => this.toggleModal("globalState")}
|
||||
/>
|
||||
</div>;
|
||||
|
||||
return <AppLayout
|
||||
toolbar={toolbar}
|
||||
layerList={layerList}
|
||||
layerEditor={layerEditor}
|
||||
codeEditor={codeEditor}
|
||||
map={this.mapRenderer()}
|
||||
bottom={bottomPanel}
|
||||
modals={modals}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import React from "react";
|
||||
import ScrollContainer from "./ScrollContainer";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import { IconContext } from "react-icons";
|
||||
|
||||
type AppLayoutInternalProps = {
|
||||
toolbar: React.ReactElement
|
||||
layerList: React.ReactElement
|
||||
layerEditor?: React.ReactElement
|
||||
codeEditor?: React.ReactElement
|
||||
map: React.ReactElement
|
||||
bottom?: React.ReactElement
|
||||
modals?: React.ReactNode
|
||||
} & WithTranslation;
|
||||
|
||||
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
||||
|
||||
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">
|
||||
{this.props.codeEditor && <div className="maputnik-layout-code-editor">
|
||||
<ScrollContainer>
|
||||
{this.props.codeEditor}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
}
|
||||
{!this.props.codeEditor && <>
|
||||
<div className="maputnik-layout-list">
|
||||
{this.props.layerList}
|
||||
</div>
|
||||
<div className="maputnik-layout-drawer">
|
||||
<ScrollContainer>
|
||||
{this.props.layerEditor}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
</>}
|
||||
{this.props.map}
|
||||
</div>
|
||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||
{this.props.bottom}
|
||||
</div>
|
||||
}
|
||||
{this.props.modals}
|
||||
</div>
|
||||
</IconContext.Provider>;
|
||||
}
|
||||
}
|
||||
|
||||
const AppLayout = withTranslation()(AppLayoutInternal);
|
||||
export default AppLayout;
|
||||
@@ -1,65 +0,0 @@
|
||||
import React from "react";
|
||||
import { formatLayerId } from "../libs/format";
|
||||
import { type LayerSpecification, type StyleSpecification } from "maplibre-gl";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import { type MappedError } from "../libs/definitions";
|
||||
|
||||
type AppMessagePanelInternalProps = {
|
||||
errors?: MappedError[]
|
||||
infos?: string[]
|
||||
mapStyle?: StyleSpecification
|
||||
onLayerSelect?(index: number): void;
|
||||
currentLayer?: LayerSpecification
|
||||
selectedLayerIndex?: number
|
||||
} & WithTranslation;
|
||||
|
||||
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
|
||||
static defaultProps = {
|
||||
onLayerSelect: () => { },
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, selectedLayerIndex } = this.props;
|
||||
const errors = this.props.errors?.map((error, idx) => {
|
||||
let content;
|
||||
if (error.parsed && error.parsed.type === "layer") {
|
||||
const { parsed } = error;
|
||||
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
|
||||
content = (
|
||||
<>
|
||||
{t("Layer")} <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||
{selectedLayerIndex !== parsed.data.index &&
|
||||
<>
|
||||
—
|
||||
<button
|
||||
className="maputnik-message-panel__switch-button"
|
||||
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
|
||||
>
|
||||
{t("switch to layer")}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
else {
|
||||
content = error.message;
|
||||
}
|
||||
return <p key={"error-" + idx} className="maputnik-message-panel-error">
|
||||
{content}
|
||||
</p>;
|
||||
});
|
||||
|
||||
const infos = this.props.infos?.map((m, i) => {
|
||||
return <p key={"info-" + i}>{m}</p>;
|
||||
});
|
||||
|
||||
return <div className="maputnik-message-panel">
|
||||
{errors}
|
||||
{infos}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
|
||||
export default AppMessagePanel;
|
||||
@@ -1,310 +0,0 @@
|
||||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
import {detect} from "detect-browser";
|
||||
|
||||
import {
|
||||
MdOpenInBrowser,
|
||||
MdSettings,
|
||||
MdLayers,
|
||||
MdHelpOutline,
|
||||
MdFindInPage,
|
||||
MdLanguage,
|
||||
MdSave,
|
||||
MdPublic,
|
||||
MdCode
|
||||
} from "react-icons/md";
|
||||
import pkgJson from "../../package.json";
|
||||
//@ts-ignore
|
||||
import maputnikLogo from "maputnik-design/logos/logo-color.svg?inline";
|
||||
import { withTranslation, type 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();
|
||||
const colorAccessibilityFiltersEnabled = ["chrome", "firefox"].indexOf(browser!.name) > -1;
|
||||
|
||||
export type ModalTypes = "settings" | "sources" | "open" | "shortcuts" | "export" | "debug" | "globalState" | "codeEditor";
|
||||
|
||||
type IconTextProps = {
|
||||
children?: React.ReactNode
|
||||
};
|
||||
|
||||
|
||||
class IconText extends React.Component<IconTextProps> {
|
||||
render() {
|
||||
return <span className="maputnik-icon-text">{this.props.children}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
type ToolbarLinkProps = {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
href?: string
|
||||
};
|
||||
|
||||
class ToolbarLink extends React.Component<ToolbarLinkProps> {
|
||||
render() {
|
||||
return <a
|
||||
className={classnames("maputnik-toolbar-link", this.props.className)}
|
||||
href={this.props.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
data-wd-key="toolbar:link"
|
||||
>
|
||||
{this.props.children}
|
||||
</a>;
|
||||
}
|
||||
}
|
||||
|
||||
type ToolbarSelectProps = {
|
||||
children?: React.ReactNode
|
||||
wdKey?: string
|
||||
};
|
||||
|
||||
class ToolbarSelect extends React.Component<ToolbarSelectProps> {
|
||||
render() {
|
||||
return <div
|
||||
className='maputnik-toolbar-select'
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
type ToolbarActionProps = {
|
||||
children?: React.ReactNode
|
||||
onClick?(...args: unknown[]): unknown
|
||||
wdKey?: string
|
||||
};
|
||||
|
||||
class ToolbarAction extends React.Component<ToolbarActionProps> {
|
||||
render() {
|
||||
return <button
|
||||
className='maputnik-toolbar-action'
|
||||
data-wd-key={this.props.wdKey}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
{this.props.children}
|
||||
</button>;
|
||||
}
|
||||
}
|
||||
|
||||
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
|
||||
|
||||
type AppToolbarInternalProps = {
|
||||
mapStyle: object
|
||||
inspectModeEnabled: boolean
|
||||
onStyleChanged: OnStyleChangedCallback
|
||||
// A new style has been uploaded
|
||||
onStyleOpen: OnStyleChangedCallback
|
||||
// A dict of source id's and the available source layers
|
||||
sources: object
|
||||
children?: React.ReactNode
|
||||
onToggleModal(modal: ModalTypes): void
|
||||
onSetMapState(mapState: MapState): unknown
|
||||
mapState?: MapState
|
||||
renderer?: string
|
||||
} & WithTranslation;
|
||||
|
||||
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
state = {
|
||||
isOpen: {
|
||||
settings: false,
|
||||
sources: false,
|
||||
open: false,
|
||||
add: false,
|
||||
export: false,
|
||||
}
|
||||
};
|
||||
|
||||
handleSelection(val: MapState) {
|
||||
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();
|
||||
}
|
||||
else {
|
||||
const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement;
|
||||
el.focus();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const views = [
|
||||
{
|
||||
id: "map",
|
||||
group: "general",
|
||||
title: t("Map"),
|
||||
},
|
||||
{
|
||||
id: "inspect",
|
||||
group: "general",
|
||||
title: t("Inspect"),
|
||||
disabled: this.props.renderer === "ol",
|
||||
},
|
||||
{
|
||||
id: "filter-deuteranopia",
|
||||
group: "color-accessibility",
|
||||
title: t("Deuteranopia filter"),
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-protanopia",
|
||||
group: "color-accessibility",
|
||||
title: t("Protanopia filter"),
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-tritanopia",
|
||||
group: "color-accessibility",
|
||||
title: t("Tritanopia filter"),
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-achromatopsia",
|
||||
group: "color-accessibility",
|
||||
title: t("Achromatopsia filter"),
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
];
|
||||
|
||||
const currentView = views.find((view) => {
|
||||
return view.id === this.props.mapState;
|
||||
});
|
||||
|
||||
return <nav className='maputnik-toolbar'>
|
||||
<div className="maputnik-toolbar__inner">
|
||||
<div
|
||||
className="maputnik-toolbar-logo-container"
|
||||
>
|
||||
{/* Keyboard accessible quick links */}
|
||||
<button
|
||||
data-wd-key="root:skip:layer-list"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("layer-list")}
|
||||
>
|
||||
{t("Layers list")}
|
||||
</button>
|
||||
<button
|
||||
data-wd-key="root:skip:layer-editor"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("layer-editor")}
|
||||
>
|
||||
{t("Layer editor")}
|
||||
</button>
|
||||
<button
|
||||
data-wd-key="root:skip:map-view"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("map")}
|
||||
>
|
||||
{t("Map view")}
|
||||
</button>
|
||||
<a
|
||||
className="maputnik-toolbar-logo"
|
||||
target="blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://github.com/maplibre/maputnik"
|
||||
>
|
||||
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} />
|
||||
<h1>
|
||||
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
|
||||
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
||||
</h1>
|
||||
</a>
|
||||
</div>
|
||||
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
|
||||
<ToolbarAction wdKey="nav:open" onClick={() => this.props.onToggleModal("open")}>
|
||||
<MdOpenInBrowser />
|
||||
<IconText>{t("Open")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:export" onClick={() => this.props.onToggleModal("export")}>
|
||||
<MdSave />
|
||||
<IconText>{t("Save")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:code-editor" onClick={() => this.props.onToggleModal("codeEditor")}>
|
||||
<MdCode />
|
||||
<IconText>{t("Code Editor")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:sources" onClick={() => this.props.onToggleModal("sources")}>
|
||||
<MdLayers />
|
||||
<IconText>{t("Data Sources")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:settings" onClick={() => this.props.onToggleModal("settings")}>
|
||||
<MdSettings />
|
||||
<IconText>{t("Style Settings")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:global-state" onClick={() => this.props.onToggleModal("globalState")}>
|
||||
<MdPublic />
|
||||
<IconText>{t("Global State")}</IconText>
|
||||
</ToolbarAction>
|
||||
|
||||
<ToolbarSelect wdKey="nav:inspect">
|
||||
<MdFindInPage />
|
||||
<IconText>{t("View")}
|
||||
<select
|
||||
className="maputnik-select"
|
||||
data-wd-key="maputnik-select"
|
||||
onChange={(e) => this.handleSelection(e.target.value as MapState)}
|
||||
value={currentView?.id}
|
||||
>
|
||||
{views.filter(v => v.group === "general").map((item) => {
|
||||
return (
|
||||
<option key={item.id} value={item.id} disabled={item.disabled} data-wd-key={item.id}>
|
||||
{item.title}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
<optgroup label={t("Color accessibility")}>
|
||||
{views.filter(v => v.group === "color-accessibility").map((item) => {
|
||||
return (
|
||||
<option key={item.id} value={item.id} disabled={item.disabled}>
|
||||
{item.title}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</optgroup>
|
||||
</select>
|
||||
</IconText>
|
||||
</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"}>
|
||||
<MdHelpOutline />
|
||||
<IconText>{t("Help")}</IconText>
|
||||
</ToolbarLink>
|
||||
</div>
|
||||
</div>
|
||||
</nav>;
|
||||
}
|
||||
}
|
||||
|
||||
const AppToolbar = withTranslation()(AppToolbarInternal);
|
||||
export default AppToolbar;
|
||||
@@ -1,104 +0,0 @@
|
||||
import React, {type CSSProperties, type PropsWithChildren, type SyntheticEvent} from "react";
|
||||
import classnames from "classnames";
|
||||
import FieldDocLabel from "./FieldDocLabel";
|
||||
import Doc from "./Doc";
|
||||
|
||||
export type BlockProps = PropsWithChildren & {
|
||||
"data-wd-key"?: string
|
||||
label?: string
|
||||
action?: React.ReactElement
|
||||
style?: CSSProperties
|
||||
onChange?(...args: unknown[]): unknown
|
||||
fieldSpec?: object
|
||||
wideMode?: boolean
|
||||
error?: {message: string}
|
||||
};
|
||||
|
||||
type BlockState = {
|
||||
showDoc: boolean
|
||||
};
|
||||
|
||||
/** Wrap a component with a label */
|
||||
export default class Block extends React.Component<BlockProps, BlockState> {
|
||||
_blockEl: HTMLDivElement | null = null;
|
||||
|
||||
constructor (props: BlockProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showDoc: false,
|
||||
};
|
||||
}
|
||||
|
||||
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) {
|
||||
const value = e.target.value;
|
||||
if (this.props.onChange) {
|
||||
return this.props.onChange(value === "" ? undefined : value);
|
||||
}
|
||||
}
|
||||
|
||||
onToggleDoc = (val: boolean) => {
|
||||
this.setState({
|
||||
showDoc: val
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Some fields for example <InputColor/> bind click events inside the element
|
||||
* to close the picker. This in turn propagates to the <label/> element
|
||||
* causing the picker to reopen. This causes a scenario where the picker can
|
||||
* never be closed once open.
|
||||
*/
|
||||
onLabelClick = (event: SyntheticEvent<any, any>) => {
|
||||
const el = event.nativeEvent.target;
|
||||
const contains = this._blockEl?.contains(el);
|
||||
|
||||
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
if (event.nativeEvent.target.nodeName !== "A") {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return <label style={this.props.style}
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
className={classnames({
|
||||
"maputnik-input-block": true,
|
||||
"maputnik-input-block--wide": this.props.wideMode,
|
||||
"maputnik-action-block": this.props.action,
|
||||
"maputnik-input-block--error": this.props.error
|
||||
})}
|
||||
onClick={this.onLabelClick}
|
||||
>
|
||||
{this.props.fieldSpec &&
|
||||
<div className="maputnik-input-block-label">
|
||||
<FieldDocLabel
|
||||
label={this.props.label}
|
||||
onToggleDoc={this.onToggleDoc}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{!this.props.fieldSpec &&
|
||||
<div className="maputnik-input-block-label">
|
||||
{this.props.label}
|
||||
</div>
|
||||
}
|
||||
<div className="maputnik-input-block-action">
|
||||
{this.props.action}
|
||||
</div>
|
||||
<div className="maputnik-input-block-content" ref={el => {this._blockEl = el;}}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
{this.props.fieldSpec &&
|
||||
<div
|
||||
className="maputnik-doc-inline"
|
||||
style={{display: this.state.showDoc ? "" : "none"}}
|
||||
>
|
||||
<Doc fieldSpec={this.props.fieldSpec} />
|
||||
</div>
|
||||
}
|
||||
</label>;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user