mirror of
https://github.com/maputnik/editor.git
synced 2025-12-25 07:30:00 +00:00
Compare commits
129 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
|
||||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +0,0 @@
|
|||||||
github: [maplibre]
|
|
||||||
open_collective: maplibre
|
|
||||||
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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. -->
|
|
||||||
10
.github/ISSUE_TEMPLATE/other-issue.md
vendored
10
.github/ISSUE_TEMPLATE/other-issue.md
vendored
@@ -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) -->
|
|
||||||
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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.
|
|
||||||
36
.github/dependabot.yml
vendored
36
.github/dependabot.yml
vendored
@@ -1,36 +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*"
|
|
||||||
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:
|
|
||||||
- "*"
|
|
||||||
26
.github/workflows/auto-merge-dependabot.yml
vendored
26
.github/workflows/auto-merge-dependabot.yml
vendored
@@ -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@08eff52bf64351f401fb50d4972fa95b9f2c2d1b # v2.4.0
|
|
||||||
with:
|
|
||||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
- name: Approve Dependabot PRs
|
|
||||||
run: gh pr review --approve "$PR_URL"
|
|
||||||
env:
|
|
||||||
PR_URL: ${{github.event.pull_request.html_url}}
|
|
||||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
- name: Enable auto-merge for Dependabot PRs
|
|
||||||
run: gh pr merge --auto --squash "$PR_URL"
|
|
||||||
env:
|
|
||||||
PR_URL: ${{github.event.pull_request.html_url}}
|
|
||||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
145
.github/workflows/ci.yml
vendored
145
.github/workflows/ci.yml
vendored
@@ -1,145 +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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
|
||||||
with: { persist-credentials: false }
|
|
||||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
|
||||||
with: { persist-credentials: false }
|
|
||||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run build
|
|
||||||
- name: artifacts/maputnik
|
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
|
||||||
with:
|
|
||||||
name: maputnik
|
|
||||||
path: dist
|
|
||||||
|
|
||||||
# Build and upload desktop CLI artifacts
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
|
||||||
with:
|
|
||||||
name: maputnik-linux
|
|
||||||
path: ./desktop/bin/linux/
|
|
||||||
|
|
||||||
- name: Artifacts/darwin
|
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
|
||||||
with:
|
|
||||||
name: maputnik-darwin
|
|
||||||
path: ./desktop/bin/darwin/
|
|
||||||
|
|
||||||
- name: Artifacts/windows
|
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
|
||||||
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
|
||||||
with: { persist-credentials: false }
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run test-unit-ci
|
|
||||||
- name: Upload coverage reports to Codecov
|
|
||||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
|
||||||
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
|
||||||
with: { persist-credentials: false }
|
|
||||||
- run: npm ci
|
|
||||||
- name: Cypress run
|
|
||||||
uses: cypress-io/github-action@7ef72e250a9e564efb4ed4c2433971ada4cc38b4 # v6.10.4
|
|
||||||
with:
|
|
||||||
build: npm run build
|
|
||||||
start: npm run start
|
|
||||||
browser: chrome
|
|
||||||
- name: Upload coverage reports to Codecov
|
|
||||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
|
||||||
with:
|
|
||||||
files: ${{ github.workspace }}/.nyc_output/out.json
|
|
||||||
verbose: true
|
|
||||||
|
|
||||||
e2e-tests-docker:
|
|
||||||
name: "E2E tests using chrome and docker"
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
|
||||||
with: { persist-credentials: false }
|
|
||||||
- run: npm ci
|
|
||||||
- name: Cypress run
|
|
||||||
uses: cypress-io/github-action@7ef72e250a9e564efb4ed4c2433971ada4cc38b4 # v6.10.4
|
|
||||||
with:
|
|
||||||
build: docker build -t maputnik .
|
|
||||||
start: docker run --rm --network host maputnik --port=8888
|
|
||||||
browser: chrome
|
|
||||||
- name: Upload coverage reports to Codecov
|
|
||||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
|
||||||
with:
|
|
||||||
files: ${{ github.workspace }}/.nyc_output/out.json
|
|
||||||
verbose: true
|
|
||||||
70
.github/workflows/codeql-analysis.yml
vendored
70
.github/workflows/codeql-analysis.yml
vendored
@@ -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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
|
||||||
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@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
|
||||||
|
|
||||||
# ℹ️ 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@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
|
||||||
39
.github/workflows/create-bump-version-pr.yml
vendored
39
.github/workflows/create-bump-version-pr.yml
vendored
@@ -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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: main
|
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.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@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
|
||||||
with:
|
|
||||||
commit-message: Bump version to ${{ inputs.version }}
|
|
||||||
branch: bump-version-to-${{ inputs.version }}
|
|
||||||
title: Bump version to ${{ inputs.version }}
|
|
||||||
55
.github/workflows/deploy.yml
vendored
55
.github/workflows/deploy.yml
vendored
@@ -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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
|
||||||
with: { persist-credentials: false }
|
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.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@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.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@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
|
||||||
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
|
||||||
- run: docker push ghcr.io/maplibre/maputnik:main
|
|
||||||
104
.github/workflows/release.yml
vendored
104
.github/workflows/release.yml
vendored
@@ -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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: main
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
|
||||||
with:
|
|
||||||
node-version-file: ".nvmrc"
|
|
||||||
|
|
||||||
- name: Check if version has been updated
|
|
||||||
id: check
|
|
||||||
uses: EndBug/version-check@d17247dd94ca7b39d0b0691399be8d7c510622c9 # 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: main
|
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
|
||||||
with:
|
|
||||||
node-version-file: ".nvmrc"
|
|
||||||
registry-url: "https://registry.npmjs.org"
|
|
||||||
|
|
||||||
- name: Set up Go for desktop build
|
|
||||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.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@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.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
|
|
||||||
46
.gitignore
vendored
46
.gitignore
vendored
@@ -1,46 +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
|
|
||||||
/cypress/screenshots
|
|
||||||
/dist/
|
|
||||||
/desktop/version.go
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# Window metadata files
|
|
||||||
/desktop/winres/winres.json
|
|
||||||
/desktop/*.syso
|
|
||||||
18
.nycrc.json
18
.nycrc.json
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"all": true,
|
|
||||||
"extends": "@istanbuljs/nyc-config-typescript",
|
|
||||||
"check-coverage": false,
|
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
|
||||||
"exclude": [
|
|
||||||
"cypress/**/*.*",
|
|
||||||
"**/*.d.ts",
|
|
||||||
"**/*.cy.tsx",
|
|
||||||
"**/*.cy.ts",
|
|
||||||
"./coverage/**",
|
|
||||||
"./cypress/**",
|
|
||||||
"./dist/**",
|
|
||||||
"node_modules"
|
|
||||||
],
|
|
||||||
"report-dir": "coverage",
|
|
||||||
"reporter": ["json", "lcov", "json-summary"]
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
15
.topissuesrc
15
.topissuesrc
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
47
AGENTS.md
47
AGENTS.md
@@ -1,47 +0,0 @@
|
|||||||
Maputnik is a MapLibre style editor written using React and TypeScript.
|
|
||||||
|
|
||||||
To get started, install all npm packages:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify code correctness by running ESLint:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run lint
|
|
||||||
```
|
|
||||||
|
|
||||||
Or try fixing lint issues with:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run lint -- --fix
|
|
||||||
```
|
|
||||||
|
|
||||||
The project type checked and built with:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
To run the tests make sure that xvfb is installed:
|
|
||||||
|
|
||||||
```
|
|
||||||
apt install xvfb
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the development server in the background with Vite:
|
|
||||||
|
|
||||||
```
|
|
||||||
nohup npm run start &
|
|
||||||
```
|
|
||||||
|
|
||||||
Then start the Cypress tests with:
|
|
||||||
|
|
||||||
```
|
|
||||||
xvfb-run -a npm run test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pull Requests
|
|
||||||
|
|
||||||
- Pull requests should update `CHANGELOG.md` with a short description of the change.
|
|
||||||
76
CHANGELOG.md
76
CHANGELOG.md
@@ -1,76 +0,0 @@
|
|||||||
## main
|
|
||||||
|
|
||||||
### ✨ 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
|
|
||||||
- Allow root-relative urls in the stylefile
|
|
||||||
- _...Add new stuff here..._
|
|
||||||
|
|
||||||
### 🐞 Bug fixes
|
|
||||||
|
|
||||||
- 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 possition
|
|
||||||
- Fixed an issue with the generation of tranlations
|
|
||||||
- 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
|
|
||||||
- _...Add new stuff here..._
|
|
||||||
|
|
||||||
## 3.0.0
|
|
||||||
|
|
||||||
### ✨ Features and improvements
|
|
||||||
- Fix radio/delete filter buttons styling regression
|
|
||||||
- Add german translation
|
|
||||||
- Use same version number for web and desktop versions
|
|
||||||
- Add scheme type options for vector/raster tile
|
|
||||||
- Add `tileSize` field for raster and raster-dem tile sources
|
|
||||||
- Update Protomaps Light gallery style to v4
|
|
||||||
- Add support to edit local files on the file system if supported by the browser
|
|
||||||
- Upgrade to MapLibre LG JS v5
|
|
||||||
- Upgrade Vite 6 and Cypress 14 ([#970](https://github.com/maplibre/maputnik/pull/970))
|
|
||||||
- Upgrade OpenLayers from v6 to v10
|
|
||||||
- When loading a style into localStorage that causes a QuotaExceededError, purge localStorage and retry
|
|
||||||
- Remove react-autobind dependency
|
|
||||||
- Remove usage of legacy `childContextTypes` API
|
|
||||||
- Refactor Field components to use arrow function syntax
|
|
||||||
- Replace react-autocomplete with Downshift in the autocomplete component
|
|
||||||
- Add LocationIQ as supported map provider with access token field and gallery style
|
|
||||||
- Use maputnik go binary for the docker image to allow file watching
|
|
||||||
- Revmove support for `debug` and `localport` url parameters
|
|
||||||
- Replace react-sortable-hoc with dnd-kit to avoid react console warnings and also use a maintained library
|
|
||||||
|
|
||||||
### 🐞 Bug fixes
|
|
||||||
|
|
||||||
- Fix incorrect handing of network error response (#944)
|
|
||||||
- Show an error when adding a layer with a duplicate ID
|
|
||||||
- Replace deprecated `ReactDOM.render` usage with `createRoot` and drop the
|
|
||||||
`DOMNodeRemoved` cleanup hack
|
|
||||||
|
|
||||||
## 2.1.1
|
|
||||||
|
|
||||||
### ✨ Features and improvements
|
|
||||||
|
|
||||||
- Add GitHub workflows for releasing new versions
|
|
||||||
- Update desktop build to pull from this repo (#922)
|
|
||||||
|
|
||||||
## 2.0.0
|
|
||||||
|
|
||||||
- Update MapLibre to version 4 (#872)
|
|
||||||
- Start continuous deployment of maputnik website
|
|
||||||
|
|
||||||
## 1.7.0
|
|
||||||
|
|
||||||
- See release notes at https://maputnik.github.io/blog/2020/04/23/release-v1.7.0
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Contributor Covenant
|
|
||||||
[](https://github.com/maplibre/maplibre/blob/main/CODE_OF_CONDUCT.md)
|
|
||||||
14
Dockerfile
14
Dockerfile
@@ -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"]
|
|
||||||
22
LICENSE
22
LICENSE
@@ -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.
|
|
||||||
123
README.md
123
README.md
@@ -1,123 +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
|
|
||||||
For E2E testing we use [Cypress](https://www.cypress.io/)
|
|
||||||
|
|
||||||
[Cypress](https://www.cypress.io/) doesn't start a server so you'll need to start one manually by running `npm run start`.
|
|
||||||
|
|
||||||
Now open a terminal and run the following using *chrome*:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run test
|
|
||||||
```
|
|
||||||
or *firefox*:
|
|
||||||
```
|
|
||||||
npm run test -- --browser firefox
|
|
||||||
```
|
|
||||||
|
|
||||||
See the following docs for more info: (Launching Browsers)[https://docs.cypress.io/guides/guides/launching-browsers]
|
|
||||||
|
|
||||||
You can also see the tests as they run or select which suites to run by executing:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run cy:open
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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
|
|
||||||
0
src/fonts/Roboto-Regular.ttf → assets/Roboto-Regular-B-HLW1rL.ttf
Executable file → Normal file
0
src/fonts/Roboto-Regular.ttf → assets/Roboto-Regular-B-HLW1rL.ttf
Executable file → Normal file
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
1
assets/index-CuVViU0P.css
Normal file
1
assets/index-CuVViU0P.css
Normal file
File diff suppressed because one or more lines are too long
962
assets/index-S_bu68PO.js
Normal file
962
assets/index-S_bu68PO.js
Normal file
File diff suppressed because one or more lines are too long
1
assets/index-S_bu68PO.js.map
Normal file
1
assets/index-S_bu68PO.js.map
Normal file
File diff suppressed because one or more lines are too long
2
assets/translation-BhJ-ufwk.js
Normal file
2
assets/translation-BhJ-ufwk.js
Normal file
File diff suppressed because one or more lines are too long
1
assets/translation-BhJ-ufwk.js.map
Normal file
1
assets/translation-BhJ-ufwk.js.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"translation-BhJ-ufwk.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||||
2
assets/translation-BrzYPxJn.js
Normal file
2
assets/translation-BrzYPxJn.js
Normal file
File diff suppressed because one or more lines are too long
1
assets/translation-BrzYPxJn.js.map
Normal file
1
assets/translation-BrzYPxJn.js.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"translation-BrzYPxJn.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||||
2
assets/translation-CQD4fuPu.js
Normal file
2
assets/translation-CQD4fuPu.js
Normal file
File diff suppressed because one or more lines are too long
1
assets/translation-CQD4fuPu.js.map
Normal file
1
assets/translation-CQD4fuPu.js.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"translation-CQD4fuPu.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||||
2
assets/translation-CZ64AJ8H.js
Normal file
2
assets/translation-CZ64AJ8H.js
Normal file
File diff suppressed because one or more lines are too long
1
assets/translation-CZ64AJ8H.js.map
Normal file
1
assets/translation-CZ64AJ8H.js.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"translation-CZ64AJ8H.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||||
2
assets/translation-DvW-3CJ8.js
Normal file
2
assets/translation-DvW-3CJ8.js
Normal file
File diff suppressed because one or more lines are too long
1
assets/translation-DvW-3CJ8.js.map
Normal file
1
assets/translation-DvW-3CJ8.js.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"translation-DvW-3CJ8.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||||
2
assets/translation-XoriI0W-.js
Normal file
2
assets/translation-XoriI0W-.js
Normal file
File diff suppressed because one or more lines are too long
1
assets/translation-XoriI0W-.js.map
Normal file
1
assets/translation-XoriI0W-.js.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"translation-XoriI0W-.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
||||||
2
assets/translation-aD1CAGoy.js
Normal file
2
assets/translation-aD1CAGoy.js
Normal file
File diff suppressed because one or more lines are too long
1
assets/translation-aD1CAGoy.js.map
Normal file
1
assets/translation-aD1CAGoy.js.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"translation-aD1CAGoy.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,32 +0,0 @@
|
|||||||
import { defineConfig } from "cypress";
|
|
||||||
import { createRequire } from "module";
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
env: {
|
|
||||||
codeCoverage: {
|
|
||||||
exclude: "cypress/**/*.*",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
e2e: {
|
|
||||||
setupNodeEvents(on, config) {
|
|
||||||
// implement node event listeners here
|
|
||||||
require("@cypress/code-coverage/task")(on, config);
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
baseUrl: "http://localhost:8888",
|
|
||||||
scrollBehavior: "center",
|
|
||||||
retries: {
|
|
||||||
runMode: 2,
|
|
||||||
openMode: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
component: {
|
|
||||||
devServer: {
|
|
||||||
framework: "react",
|
|
||||||
bundler: "vite",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
|
||||||
|
|
||||||
describe("accessibility", () => {
|
|
||||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
|
|
||||||
describe("skip links", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("layer");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("skip link to layer list", () => {
|
|
||||||
const selector = "root:skip:layer-list";
|
|
||||||
then(get.elementByTestId(selector)).shouldExist();
|
|
||||||
when.tab();
|
|
||||||
then(get.elementByTestId(selector)).shouldBeFocused();
|
|
||||||
when.click(selector);
|
|
||||||
then(get.skipTargetLayerList()).shouldBeFocused();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("skip link to layer editor", () => {
|
|
||||||
const selector = "root:skip:layer-editor";
|
|
||||||
then(get.elementByTestId(selector)).shouldExist();
|
|
||||||
then(get.elementByTestId("skip-target-layer-editor")).shouldExist();
|
|
||||||
when.tab().tab();
|
|
||||||
then(get.elementByTestId(selector)).shouldBeFocused();
|
|
||||||
when.click(selector);
|
|
||||||
then(get.skipTargetLayerEditor()).shouldBeFocused();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("skip link to map view", () => {
|
|
||||||
const selector = "root:skip:map-view";
|
|
||||||
then(get.elementByTestId(selector)).shouldExist();
|
|
||||||
when.tab().tab().tab();
|
|
||||||
then(get.elementByTestId(selector)).shouldBeFocused();
|
|
||||||
when.click(selector);
|
|
||||||
then(get.canvas()).shouldBeFocused();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
|
||||||
|
|
||||||
describe("code editor", () => {
|
|
||||||
const { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
|
|
||||||
it("open code editor", () => {
|
|
||||||
when.click("nav:code-editor");
|
|
||||||
then(get.element(".maputnik-code-editor")).shouldExist();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("closes code editor", () => {
|
|
||||||
when.click("nav:code-editor");
|
|
||||||
then(get.element(".maputnik-code-editor")).shouldExist();
|
|
||||||
when.click("nav:code-editor");
|
|
||||||
then(get.element(".maputnik-code-editor")).shouldNotExist();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
|
||||||
|
|
||||||
describe("history", () => {
|
|
||||||
const { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
|
|
||||||
let undoKeyCombo: string;
|
|
||||||
let redoKeyCombo: string;
|
|
||||||
|
|
||||||
before(() => {
|
|
||||||
const isMac = get.isMac();
|
|
||||||
undoKeyCombo = isMac ? "{meta}z" : "{ctrl}z";
|
|
||||||
redoKeyCombo = isMac ? "{meta}{shift}z" : "{ctrl}y";
|
|
||||||
});
|
|
||||||
|
|
||||||
it("undo/redo", () => {
|
|
||||||
when.setStyle("geojson");
|
|
||||||
when.modal.open();
|
|
||||||
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "step 1",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "step 1",
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "step 2",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "step 1",
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "step 2",
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
when.typeKeys(undoKeyCombo);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "step 1",
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
when.typeKeys(undoKeyCombo);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
|
||||||
|
|
||||||
when.typeKeys(redoKeyCombo);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "step 1",
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
when.typeKeys(redoKeyCombo);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "step 1",
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "step 2",
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not redo after undo and value change", () => {
|
|
||||||
when.setStyle("geojson");
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "step 1",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "step 2",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.typeKeys(undoKeyCombo);
|
|
||||||
when.typeKeys(undoKeyCombo);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "step 3",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.typeKeys(redoKeyCombo);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "step 3",
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
|
||||||
|
|
||||||
describe("i18n", () => {
|
|
||||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
|
|
||||||
describe("language detector", () => {
|
|
||||||
it("English", () => {
|
|
||||||
const url = "?lng=en";
|
|
||||||
when.visit(url);
|
|
||||||
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Japanese", () => {
|
|
||||||
const url = "?lng=ja";
|
|
||||||
when.visit(url);
|
|
||||||
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("language switcher", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("layer");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("the language switcher switches to Japanese", () => {
|
|
||||||
const selector = "maputnik-lang-select";
|
|
||||||
then(get.elementByTestId(selector)).shouldExist();
|
|
||||||
when.select(selector, "ja");
|
|
||||||
then(get.elementByTestId(selector)).shouldHaveValue("ja");
|
|
||||||
|
|
||||||
then(get.elementByTestId("nav:settings")).shouldHaveText("スタイル設定");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
|
||||||
|
|
||||||
describe("keyboard", () => {
|
|
||||||
const { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
describe("shortcuts", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
given.setupMockBackedResponses();
|
|
||||||
when.setStyle("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ESC should unfocus", () => {
|
|
||||||
const targetSelector = "maputnik-select";
|
|
||||||
when.focus(targetSelector);
|
|
||||||
then(get.elementByTestId(targetSelector)).shouldBeFocused();
|
|
||||||
when.typeKeys("{esc}");
|
|
||||||
then(get.elementByTestId(targetSelector)).shouldNotBeFocused();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("'?' should show shortcuts modal", () => {
|
|
||||||
when.typeKeys("?");
|
|
||||||
then(get.elementByTestId("modal:shortcuts")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("'o' should show open modal", () => {
|
|
||||||
when.typeKeys("o");
|
|
||||||
then(get.elementByTestId("modal:open")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("'e' should show export modal", () => {
|
|
||||||
when.typeKeys("e");
|
|
||||||
then(get.elementByTestId("modal:export")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("'d' should show sources modal", () => {
|
|
||||||
when.typeKeys("d");
|
|
||||||
then(get.elementByTestId("modal:sources")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("'s' should show settings modal", () => {
|
|
||||||
when.typeKeys("s");
|
|
||||||
then(get.elementByTestId("modal:settings")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("'i' should change map to inspect mode", () => {
|
|
||||||
when.typeKeys("i");
|
|
||||||
then(get.inputValue("maputnik-select")).shouldEqual("inspect");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("'m' should focus map", () => {
|
|
||||||
when.typeKeys("m");
|
|
||||||
then(get.canvas()).shouldBeFocused();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("'!' should show debug modal", () => {
|
|
||||||
when.typeKeys("!");
|
|
||||||
then(get.elementByTestId("modal:debug")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
|
||||||
import { v1 as uuid } from "uuid";
|
|
||||||
|
|
||||||
describe("layer editor", () => {
|
|
||||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("both");
|
|
||||||
when.modal.open();
|
|
||||||
});
|
|
||||||
|
|
||||||
function createBackground() {
|
|
||||||
const id = uuid();
|
|
||||||
|
|
||||||
when.selectWithin("add-layer.layer-type", "background");
|
|
||||||
when.setValue("add-layer.layer-id.input", "background:" + id);
|
|
||||||
|
|
||||||
when.click("add-layer");
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "background:" + id,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
it("expand/collapse");
|
|
||||||
it("id", () => {
|
|
||||||
const bgId = createBackground();
|
|
||||||
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
|
||||||
|
|
||||||
const id = uuid();
|
|
||||||
when.setValue("layer-editor.layer-id.input", "foobar:" + id);
|
|
||||||
when.click("min-zoom");
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "foobar:" + id,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("source", () => {
|
|
||||||
it("should show error when the source is invalid", () => {
|
|
||||||
when.modal.fillLayers({
|
|
||||||
type: "circle",
|
|
||||||
layer: "invalid",
|
|
||||||
});
|
|
||||||
then(get.element(".maputnik-input-block--error .maputnik-input-block-label")).shouldHaveCss("color", "rgb(207, 74, 74)");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("min-zoom", () => {
|
|
||||||
let bgId: string;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
bgId = createBackground();
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
|
||||||
when.setValue("min-zoom.input-text", "1");
|
|
||||||
when.click("layer-editor.layer-id");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update min-zoom in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "background:" + bgId,
|
|
||||||
type: "background",
|
|
||||||
minzoom: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("when clicking next layer should update style on local storage", () => {
|
|
||||||
when.type("min-zoom.input-text", "{backspace}");
|
|
||||||
when.click("max-zoom.input-text");
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "background:" + bgId,
|
|
||||||
type: "background",
|
|
||||||
minzoom: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("max-zoom", () => {
|
|
||||||
let bgId: string;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
bgId = createBackground();
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
|
||||||
when.setValue("max-zoom.input-text", "1");
|
|
||||||
when.click("layer-editor.layer-id");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update style in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "background:" + bgId,
|
|
||||||
type: "background",
|
|
||||||
maxzoom: 1,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("comments", () => {
|
|
||||||
let bgId: string;
|
|
||||||
const comment = "42";
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
bgId = createBackground();
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
|
||||||
when.setValue("layer-comment.input", comment);
|
|
||||||
when.click("layer-editor.layer-id");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update style in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "background:" + bgId,
|
|
||||||
type: "background",
|
|
||||||
metadata: {
|
|
||||||
"maputnik:comment": comment,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when unsetting", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.clear("layer-comment.input");
|
|
||||||
when.click("min-zoom.input-text");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update style in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "background:" + bgId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("color", () => {
|
|
||||||
let bgId: string;
|
|
||||||
beforeEach(() => {
|
|
||||||
bgId = createBackground();
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
|
||||||
when.click("spec-field:background-color");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update style in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "background:" + bgId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("opacity", () => {
|
|
||||||
let bgId: string;
|
|
||||||
beforeEach(() => {
|
|
||||||
bgId = createBackground();
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
|
||||||
when.type("spec-field-input:background-opacity", "0.");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should keep '.' in the input field", () => {
|
|
||||||
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0.");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should revert to a valid value when focus out", () => {
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
|
||||||
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
describe("filter", () => {
|
|
||||||
it("expand/collapse");
|
|
||||||
it("compound filter");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("layout", () => {
|
|
||||||
it("text-font", () => {
|
|
||||||
when.setStyle("font");
|
|
||||||
when.collapseGroupInLayerEditor();
|
|
||||||
when.collapseGroupInLayerEditor(1);
|
|
||||||
when.collapseGroupInLayerEditor(2);
|
|
||||||
when.doWithin("spec-field:text-font", () => {
|
|
||||||
get.element(".maputnik-autocomplete input").first().click();
|
|
||||||
});
|
|
||||||
then(get.element(".maputnik-autocomplete-menu-item")).shouldBeVisible();
|
|
||||||
then(get.element(".maputnik-autocomplete-menu-item")).shouldHaveLength(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("paint", () => {
|
|
||||||
it("expand/collapse");
|
|
||||||
it("color");
|
|
||||||
it("pattern");
|
|
||||||
it("opacity");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("json-editor", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "circle",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "circle",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const sourceText = get.elementByText('"source"');
|
|
||||||
|
|
||||||
sourceText.click();
|
|
||||||
sourceText.type("\"");
|
|
||||||
|
|
||||||
then(get.element(".cm-lint-marker-error")).shouldExist();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it("expand/collapse");
|
|
||||||
it("modify");
|
|
||||||
|
|
||||||
it("parse error", () => {
|
|
||||||
const bgId = createBackground();
|
|
||||||
|
|
||||||
when.click("layer-list-item:background:" + bgId);
|
|
||||||
when.collapseGroupInLayerEditor();
|
|
||||||
when.collapseGroupInLayerEditor(1);
|
|
||||||
then(get.element(".cm-lint-marker-error")).shouldNotExist();
|
|
||||||
|
|
||||||
when.appendTextInJsonEditor(
|
|
||||||
"\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"
|
|
||||||
);
|
|
||||||
then(get.element(".cm-lint-marker-error")).shouldExist();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("sticky header", () => {
|
|
||||||
it("should keep layer header visible when scrolling properties", () => {
|
|
||||||
// Setup: Create a layer with many properties (e.g., symbol layer)
|
|
||||||
when.modal.fillLayers({
|
|
||||||
type: "symbol",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.wait(500);
|
|
||||||
const header = get.elementByTestId("layer-editor.header");
|
|
||||||
then(header).shouldBeVisible();
|
|
||||||
|
|
||||||
get.element(".maputnik-scroll-container").scrollTo("bottom", { ensureScrollable: false });
|
|
||||||
when.wait(200);
|
|
||||||
|
|
||||||
then(header).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("skip-target-layer-editor")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,541 +0,0 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
|
||||||
|
|
||||||
describe("layers list", () => {
|
|
||||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("both");
|
|
||||||
when.modal.open();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("ops", () => {
|
|
||||||
let id: string;
|
|
||||||
beforeEach(() => {
|
|
||||||
id = when.modal.fillLayers({
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update layers in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when clicking delete", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("layer-list-item:" + id + ":delete");
|
|
||||||
});
|
|
||||||
it("should empty layers in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when clicking duplicate", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("layer-list-item:" + id + ":copy");
|
|
||||||
});
|
|
||||||
it("should add copy layer in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id + "-copy",
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when clicking hide", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("layer-list-item:" + id + ":toggle-visibility");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update visibility to none in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "background",
|
|
||||||
layout: {
|
|
||||||
visibility: "none",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when clicking show", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("layer-list-item:" + id + ":toggle-visibility");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update visibility to visible in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "background",
|
|
||||||
layout: {
|
|
||||||
visibility: "visible",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when selecting a layer", () => {
|
|
||||||
let secondId: string;
|
|
||||||
beforeEach(() => {
|
|
||||||
when.modal.open();
|
|
||||||
secondId = when.modal.fillLayers({
|
|
||||||
id: "second-layer",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it("should show the selected layer in the editor", () => {
|
|
||||||
when.realClick("layer-list-item:" + secondId);
|
|
||||||
then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(secondId);
|
|
||||||
when.realClick("layer-list-item:" + id);
|
|
||||||
then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("background", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("modify", () => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("fill", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "fill",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "fill",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Change source
|
|
||||||
it("change source");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("line", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "line",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "line",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("groups", () => {
|
|
||||||
when.modal.open();
|
|
||||||
const id1 = when.modal.fillLayers({
|
|
||||||
id: "aa",
|
|
||||||
type: "line",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
const id2 = when.modal.fillLayers({
|
|
||||||
id: "aa-2",
|
|
||||||
type: "line",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
const id3 = when.modal.fillLayers({
|
|
||||||
id: "b",
|
|
||||||
type: "line",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id2)).shouldNotBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
|
|
||||||
when.click("layer-list-group:aa-0");
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id2)).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
|
|
||||||
when.click("layer-list-item:" + id2);
|
|
||||||
when.click("skip-target-layer-editor");
|
|
||||||
when.click("menu-move-layer-down");
|
|
||||||
then(get.elementByTestId("layer-list-group:aa-0")).shouldNotExist();
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "aa",
|
|
||||||
type: "line",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "b",
|
|
||||||
type: "line",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "aa-2",
|
|
||||||
type: "line",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("symbol", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "symbol",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "symbol",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show spec info when hovering and clicking single line property", () => {
|
|
||||||
when.modal.fillLayers({
|
|
||||||
type: "symbol",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.hover("spec-field-container:text-rotate");
|
|
||||||
then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
|
|
||||||
when.click("field-doc-button-Rotate", 0);
|
|
||||||
then(get.elementByTestId("spec-field-doc")).shouldContainText("Rotates the ");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show spec info when hovering and clicking multi line property", () => {
|
|
||||||
when.modal.fillLayers({
|
|
||||||
type: "symbol",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.hover("spec-field-container:text-offset");
|
|
||||||
then(get.elementByTestId("field-doc-button-Offset")).shouldBeVisible();
|
|
||||||
when.click("field-doc-button-Offset", 0);
|
|
||||||
then(get.elementByTestId("spec-field-doc")).shouldContainText("Offset distance");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should hide spec info when clicking a second time", () => {
|
|
||||||
when.modal.fillLayers({
|
|
||||||
type: "symbol",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.hover("spec-field-container:text-rotate");
|
|
||||||
then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
|
|
||||||
when.click("field-doc-button-Rotate", 0);
|
|
||||||
when.wait(200);
|
|
||||||
when.click("field-doc-button-Rotate", 0);
|
|
||||||
then(get.elementByTestId("spec-field-doc")).shouldNotBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("raster", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "raster",
|
|
||||||
layer: "raster",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "raster",
|
|
||||||
source: "raster",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("circle", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "circle",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "circle",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("fill extrusion", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "fill-extrusion",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "fill-extrusion",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("hillshade", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "hillshade",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "hillshade",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("set hillshade illumination direction array", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "hillshade",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
when.collapseGroupInLayerEditor();
|
|
||||||
when.collapseGroupInLayerEditor(1);
|
|
||||||
when.setValueToPropertyArray("spec-field:hillshade-illumination-direction", "1");
|
|
||||||
when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "2");
|
|
||||||
when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "3");
|
|
||||||
when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "4");
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "hillshade",
|
|
||||||
source: "example",
|
|
||||||
paint: {
|
|
||||||
"hillshade-illumination-direction": [ 1, 2, 3, 4 ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("set hillshade highlight color array", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "hillshade",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
when.collapseGroupInLayerEditor();
|
|
||||||
when.setValueToPropertyArray("spec-field:hillshade-highlight-color", "blue");
|
|
||||||
when.addValueToPropertyArray("spec-field:hillshade-highlight-color", "#00ff00");
|
|
||||||
when.addValueToPropertyArray("spec-field:hillshade-highlight-color", "rgba(255, 255, 0, 1)");
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "hillshade",
|
|
||||||
source: "example",
|
|
||||||
paint: {
|
|
||||||
"hillshade-highlight-color": [ "blue", "#00ff00", "rgba(255, 255, 0, 1)" ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("color-relief", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "color-relief",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "color-relief",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds elevation expression when clicking the elevation button", () => {
|
|
||||||
when.modal.fillLayers({
|
|
||||||
type: "color-relief",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
when.collapseGroupInLayerEditor();
|
|
||||||
when.click("make-elevation-function");
|
|
||||||
then(get.element("[data-wd-key='spec-field-container:color-relief-color'] .cm-line")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("groups", () => {
|
|
||||||
it("simple", () => {
|
|
||||||
when.setStyle("geojson");
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "foo",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "foo_bar",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "foo_bar_baz",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:foo_bar")).shouldNotBeVisible();
|
|
||||||
then(
|
|
||||||
get.elementByTestId("layer-list-item:foo_bar_baz")
|
|
||||||
).shouldNotBeVisible();
|
|
||||||
when.click("layer-list-group:foo-0");
|
|
||||||
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:foo_bar")).shouldBeVisible();
|
|
||||||
then(
|
|
||||||
get.elementByTestId("layer-list-item:foo_bar_baz")
|
|
||||||
).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("drag and drop", () => {
|
|
||||||
it("move layer should update local storage", () => {
|
|
||||||
when.modal.open();
|
|
||||||
const firstId = when.modal.fillLayers({
|
|
||||||
id: "a",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
when.modal.open();
|
|
||||||
const secondId = when.modal.fillLayers({
|
|
||||||
id: "b",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
when.modal.open();
|
|
||||||
const thirdId = when.modal.fillLayers({
|
|
||||||
id: "c",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.dragAndDropWithWait("layer-list-item:" + firstId, "layer-list-item:" + thirdId);
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: secondId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: thirdId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: firstId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("sticky header", () => {
|
|
||||||
it("should keep header visible when scrolling layer list", () => {
|
|
||||||
// Setup: Create multiple layers to enable scrolling
|
|
||||||
for (let i = 0; i < 20; i++) {
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: `layer-${i}`,
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
when.wait(500);
|
|
||||||
const header = get.elementByTestId("layer-list.header");
|
|
||||||
then(header).shouldBeVisible();
|
|
||||||
|
|
||||||
// Scroll the layer list container (use ensureScrollable: false to avoid flakiness)
|
|
||||||
get.elementByTestId("layer-list").scrollTo("bottom", { ensureScrollable: false });
|
|
||||||
when.wait(200);
|
|
||||||
then(header).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list:add-layer")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
|
||||||
|
|
||||||
describe("map", () => {
|
|
||||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
describe("zoom level", () => {
|
|
||||||
it("via url", () => {
|
|
||||||
const zoomLevel = 12.37;
|
|
||||||
when.setStyle("geojson", zoomLevel);
|
|
||||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
|
|
||||||
"Zoom: " + zoomLevel
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("via map controls", () => {
|
|
||||||
const zoomLevel = 12.37;
|
|
||||||
when.setStyle("geojson", zoomLevel);
|
|
||||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
|
||||||
when.clickZoomIn();
|
|
||||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
|
|
||||||
"Zoom: " + (zoomLevel + 1)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("search", () => {
|
|
||||||
it("should exist", () => {
|
|
||||||
then(get.searchControl()).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("popup", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("rectangles");
|
|
||||||
});
|
|
||||||
it("should open on feature click", () => {
|
|
||||||
when.clickCenter("maplibre:map");
|
|
||||||
then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should open a second feature after closing popup", () => {
|
|
||||||
when.clickCenter("maplibre:map");
|
|
||||||
then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
|
|
||||||
when.closePopup();
|
|
||||||
then(get.elementByTestId("feature-layer-popup")).shouldNotExist();
|
|
||||||
when.clickCenter("maplibre:map");
|
|
||||||
then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/// <reference types="cypress-real-events" />
|
|
||||||
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
|
||||||
import "cypress-real-events/support";
|
|
||||||
|
|
||||||
export default class MaputnikCypressHelper {
|
|
||||||
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
|
|
||||||
|
|
||||||
public given = {
|
|
||||||
...this.helper.given,
|
|
||||||
};
|
|
||||||
|
|
||||||
public get = {
|
|
||||||
...this.helper.get,
|
|
||||||
};
|
|
||||||
|
|
||||||
public when = {
|
|
||||||
dragAndDropWithWait: (element: string, targetElement: string) => {
|
|
||||||
this.helper.get.elementByTestId(element).realMouseDown({ button: "left", position: "center" });
|
|
||||||
this.helper.get.elementByTestId(element).realMouseMove(0, 10, { position: "center" });
|
|
||||||
this.helper.get.elementByTestId(targetElement).realMouseMove(0, 0, { position: "center" });
|
|
||||||
this.helper.when.wait(1);
|
|
||||||
this.helper.get.elementByTestId(targetElement).realMouseUp();
|
|
||||||
},
|
|
||||||
clickCenter: (element: string) => {
|
|
||||||
this.helper.get.elementByTestId(element).realMouseDown({ button: "left", position: "center" });
|
|
||||||
this.helper.when.wait(200);
|
|
||||||
this.helper.get.elementByTestId(element).realMouseUp();
|
|
||||||
},
|
|
||||||
openFileByFixture: (fixture: string, buttonTestId: string, inputTestId: string) => {
|
|
||||||
cy.window().then((win) => {
|
|
||||||
const file = {
|
|
||||||
text: cy.stub().resolves(cy.fixture(fixture).then(JSON.stringify)),
|
|
||||||
};
|
|
||||||
const fileHandle = {
|
|
||||||
getFile: cy.stub().resolves(file),
|
|
||||||
};
|
|
||||||
if (!win.showOpenFilePicker) {
|
|
||||||
this.helper.get.elementByTestId(inputTestId).selectFile("cypress/fixtures/" + fixture, { force: true });
|
|
||||||
} else {
|
|
||||||
cy.stub(win, "showOpenFilePicker").resolves([fileHandle]);
|
|
||||||
this.helper.get.elementByTestId(buttonTestId).click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
...this.helper.when,
|
|
||||||
};
|
|
||||||
|
|
||||||
public beforeAndAfter = this.helper.beforeAndAfter;
|
|
||||||
}
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
/// <reference types="cypress-plugin-tab" />
|
|
||||||
|
|
||||||
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
|
||||||
import { Assertable, then } from "@shellygo/cypress-test-utils/assertable";
|
|
||||||
import MaputnikCypressHelper from "./maputnik-cypress-helper";
|
|
||||||
import ModalDriver from "./modal-driver";
|
|
||||||
const baseUrl = "http://localhost:8888/";
|
|
||||||
|
|
||||||
const styleFromWindow = (win: Window) => {
|
|
||||||
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
|
||||||
const styleItemKey = `maputnik:style:${styleId}`;
|
|
||||||
const styleItem = win.localStorage.getItem(styleItemKey);
|
|
||||||
if (!styleItem) throw new Error("Could not get styleItem from localStorage");
|
|
||||||
const obj = JSON.parse(styleItem);
|
|
||||||
return obj;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class MaputnikAssertable<T> extends Assertable<T> {
|
|
||||||
shouldEqualToStoredStyle = () =>
|
|
||||||
then(
|
|
||||||
new CypressHelper().get.window().then((win: Window) => {
|
|
||||||
const style = styleFromWindow(win);
|
|
||||||
then(this.chainable).shouldDeepNestedInclude(style);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MaputnikDriver {
|
|
||||||
private helper = new MaputnikCypressHelper();
|
|
||||||
private modalDriver = new ModalDriver();
|
|
||||||
|
|
||||||
public beforeAndAfter = () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
this.given.setupMockBackedResponses();
|
|
||||||
this.when.setStyle("both");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public then = (chainable: Cypress.Chainable<any>) =>
|
|
||||||
new MaputnikAssertable(chainable);
|
|
||||||
|
|
||||||
public given = {
|
|
||||||
...this.helper.given,
|
|
||||||
setupMockBackedResponses: () => {
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "example-style.json",
|
|
||||||
response: {
|
|
||||||
fixture: "example-style.json",
|
|
||||||
},
|
|
||||||
alias: "example-style.json",
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "example-layer-style.json",
|
|
||||||
response: {
|
|
||||||
fixture: "example-layer-style.json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "geojson-style.json",
|
|
||||||
response: {
|
|
||||||
fixture: "geojson-style.json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "raster-style.json",
|
|
||||||
response: {
|
|
||||||
fixture: "raster-style.json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "geojson-raster-style.json",
|
|
||||||
response: {
|
|
||||||
fixture: "geojson-raster-style.json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "rectangles-style.json",
|
|
||||||
response: {
|
|
||||||
fixture: "rectangles-style.json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "example-style-with-fonts.json",
|
|
||||||
response: {
|
|
||||||
fixture: "example-style-with-fonts.json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: "*example.local/*",
|
|
||||||
response: [],
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: "*example.com/*",
|
|
||||||
response: [],
|
|
||||||
});
|
|
||||||
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,
|
|
||||||
doWithin: (selector: string, fn: () => void) => {
|
|
||||||
this.helper.when.doWithin(fn, selector);
|
|
||||||
},
|
|
||||||
tab: () => this.helper.get.element("body").tab(),
|
|
||||||
waitForExampleFileResponse: () => {
|
|
||||||
this.helper.when.waitForResponse("example-style.json");
|
|
||||||
},
|
|
||||||
chooseExampleFile: () => {
|
|
||||||
this.helper.given.fixture("example-style.json", "example-style.json");
|
|
||||||
this.helper.when.openFileByFixture("example-style.json", "modal:open.file.button", "modal:open.file.input");
|
|
||||||
this.helper.when.wait(200);
|
|
||||||
},
|
|
||||||
setStyle: (
|
|
||||||
styleProperties: "geojson" | "raster" | "both" | "layer" | "rectangles" | "font" | "",
|
|
||||||
zoom?: number
|
|
||||||
) => {
|
|
||||||
const url = new URL(baseUrl);
|
|
||||||
switch (styleProperties) {
|
|
||||||
case "geojson":
|
|
||||||
url.searchParams.set("style", baseUrl + "geojson-style.json");
|
|
||||||
break;
|
|
||||||
case "raster":
|
|
||||||
url.searchParams.set("style", baseUrl + "raster-style.json");
|
|
||||||
break;
|
|
||||||
case "both":
|
|
||||||
url.searchParams.set("style", baseUrl + "geojson-raster-style.json");
|
|
||||||
break;
|
|
||||||
case "layer":
|
|
||||||
url.searchParams.set("style", baseUrl + "example-layer-style.json");
|
|
||||||
break;
|
|
||||||
case "rectangles":
|
|
||||||
url.searchParams.set("style", baseUrl + "rectangles-style.json");
|
|
||||||
break;
|
|
||||||
case "font":
|
|
||||||
url.searchParams.set("style", baseUrl + "example-style-with-fonts.json");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (zoom) {
|
|
||||||
url.hash = `${zoom}/41.3805/2.1635`;
|
|
||||||
}
|
|
||||||
this.helper.when.visit(url.toString());
|
|
||||||
if (styleProperties) {
|
|
||||||
this.helper.when.acceptConfirm();
|
|
||||||
}
|
|
||||||
// when methods should not include assertions
|
|
||||||
const toolbarLink = this.helper.get.elementByTestId("toolbar:link");
|
|
||||||
toolbarLink.scrollIntoView();
|
|
||||||
toolbarLink.should("be.visible");
|
|
||||||
},
|
|
||||||
|
|
||||||
typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
|
|
||||||
|
|
||||||
clickZoomIn: () => {
|
|
||||||
this.helper.get.element(".maplibregl-ctrl-zoom-in").click();
|
|
||||||
},
|
|
||||||
|
|
||||||
selectWithin: (selector: string, value: string) => {
|
|
||||||
this.when.doWithin(selector, () => {
|
|
||||||
this.helper.get.element("select").select(value);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
select: (selector: string, value: string) => {
|
|
||||||
this.helper.get.elementByTestId(selector).select(value);
|
|
||||||
},
|
|
||||||
|
|
||||||
focus: (selector: string) => {
|
|
||||||
this.helper.when.focus(selector);
|
|
||||||
},
|
|
||||||
|
|
||||||
setValue: (selector: string, text: string) => {
|
|
||||||
this.helper.get
|
|
||||||
.elementByTestId(selector)
|
|
||||||
.clear()
|
|
||||||
.type(text, { parseSpecialCharSequences: false });
|
|
||||||
},
|
|
||||||
|
|
||||||
setValueToPropertyArray: (selector: string, value: string) => {
|
|
||||||
this.when.doWithin(selector, () => {
|
|
||||||
this.helper.get.element(".maputnik-array-block-content input").last().type("{selectall}"+value, {force: true });
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
addValueToPropertyArray: (selector: string, value: string) => {
|
|
||||||
this.when.doWithin(selector, () => {
|
|
||||||
this.helper.get.element(".maputnik-array-add-value").click({ force: true });
|
|
||||||
this.helper.get.element(".maputnik-array-block-content input").last().type("{selectall}"+value, {force: true });
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
closePopup: () => {
|
|
||||||
this.helper.get.element(".maplibregl-popup-close-button").click();
|
|
||||||
},
|
|
||||||
|
|
||||||
collapseGroupInLayerEditor: (index = 0) => {
|
|
||||||
this.helper.get.element(".maputnik-layer-editor-group__button").eq(index).realClick();
|
|
||||||
},
|
|
||||||
|
|
||||||
appendTextInJsonEditor: (text: string) => {
|
|
||||||
this.helper.get.element(".cm-line").first().click().type(text, { parseSpecialCharSequences: false });
|
|
||||||
},
|
|
||||||
|
|
||||||
setTextInJsonEditor: (text: string) => {
|
|
||||||
this.helper.get.element(".cm-line").first().click().clear().type(text, { parseSpecialCharSequences: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public get = {
|
|
||||||
...this.helper.get,
|
|
||||||
isMac: () => {
|
|
||||||
return Cypress.platform === "darwin";
|
|
||||||
},
|
|
||||||
|
|
||||||
styleFromLocalStorage: () =>
|
|
||||||
this.helper.get.window().then((win) => styleFromWindow(win)),
|
|
||||||
|
|
||||||
exampleFileUrl: () => {
|
|
||||||
return baseUrl + "example-style.json";
|
|
||||||
},
|
|
||||||
skipTargetLayerList: () =>
|
|
||||||
this.helper.get.elementByTestId("skip-target-layer-list"),
|
|
||||||
skipTargetLayerEditor: () =>
|
|
||||||
this.helper.get.elementByTestId("skip-target-layer-editor"),
|
|
||||||
canvas: () => this.helper.get.element("canvas"),
|
|
||||||
searchControl: () => this.helper.get.element(".maplibregl-ctrl-geocoder")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { v1 as uuid } from "uuid";
|
|
||||||
import MaputnikCypressHelper from "./maputnik-cypress-helper";
|
|
||||||
|
|
||||||
export default class ModalDriver {
|
|
||||||
private helper = new MaputnikCypressHelper();
|
|
||||||
|
|
||||||
public when = {
|
|
||||||
fillLayers: (opts: { type: string; layer?: string; id?: string }) => {
|
|
||||||
// Having logic in test code is an anti pattern.
|
|
||||||
// This should be splitted to multiple single responsibility functions
|
|
||||||
const type = opts.type;
|
|
||||||
const layer = opts.layer;
|
|
||||||
let id;
|
|
||||||
if (opts.id) {
|
|
||||||
id = opts.id;
|
|
||||||
} else {
|
|
||||||
id = `${type}:${uuid()}`;
|
|
||||||
}
|
|
||||||
this.helper.when.selectOption("add-layer.layer-type.select", type);
|
|
||||||
this.helper.when.type("add-layer.layer-id.input", id);
|
|
||||||
|
|
||||||
if (layer) {
|
|
||||||
this.helper.when.doWithin(() => {
|
|
||||||
this.helper.get.element("input").clear().type(layer!);
|
|
||||||
}, "add-layer.layer-source-block");
|
|
||||||
}
|
|
||||||
this.helper.when.click("add-layer");
|
|
||||||
|
|
||||||
return id;
|
|
||||||
},
|
|
||||||
|
|
||||||
open: () => {
|
|
||||||
this.helper.when.click("layer-list:add-layer");
|
|
||||||
},
|
|
||||||
|
|
||||||
close: (key: string) => {
|
|
||||||
this.helper.when.click(key + ".close-modal");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,451 +0,0 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
|
||||||
import tokens from "../../src/config/tokens.json" with {type: "json"};
|
|
||||||
|
|
||||||
describe("modals", () => {
|
|
||||||
const { beforeAndAfter, when, get, given, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("");
|
|
||||||
});
|
|
||||||
describe("open", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("nav:open");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("close", () => {
|
|
||||||
when.modal.close("modal:open");
|
|
||||||
then(get.elementByTestId("modal:open")).shouldNotExist();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("upload", () => {
|
|
||||||
when.chooseExampleFile();
|
|
||||||
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when click open url", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
const styleFileUrl = get.exampleFileUrl();
|
|
||||||
|
|
||||||
when.setValue("modal:open.url.input", styleFileUrl);
|
|
||||||
when.click("modal:open.url.button");
|
|
||||||
when.wait(200);
|
|
||||||
});
|
|
||||||
it("load from url", () => {
|
|
||||||
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("shortcuts", () => {
|
|
||||||
it("open/close", () => {
|
|
||||||
when.setStyle("");
|
|
||||||
when.typeKeys("?");
|
|
||||||
when.modal.close("modal:shortcuts");
|
|
||||||
then(get.elementByTestId("modal:shortcuts")).shouldNotExist();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("export", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("nav:export");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("close", () => {
|
|
||||||
when.modal.close("modal:export");
|
|
||||||
then(get.elementByTestId("modal:export")).shouldNotExist();
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Work out how to download a file and check the contents
|
|
||||||
it("download");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("sources", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("layer");
|
|
||||||
when.click("nav:sources");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("active sources");
|
|
||||||
it("public source");
|
|
||||||
|
|
||||||
it("add new source", () => {
|
|
||||||
const sourceId = "n1z2v3r";
|
|
||||||
when.setValue("modal:sources.add.source_id", sourceId);
|
|
||||||
when.select("modal:sources.add.source_type", "tile_vector");
|
|
||||||
when.select("modal:sources.add.scheme_type", "tms");
|
|
||||||
when.click("modal:sources.add.add_source");
|
|
||||||
when.wait(200);
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.sources[sourceId])
|
|
||||||
).shouldInclude({
|
|
||||||
scheme: "tms",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("add new pmtiles source", () => {
|
|
||||||
const sourceId = "pmtilestest";
|
|
||||||
when.setValue("modal:sources.add.source_id", sourceId);
|
|
||||||
when.select("modal:sources.add.source_type", "pmtiles_vector");
|
|
||||||
when.setValue("modal:sources.add.source_url", "https://data.source.coop/protomaps/openstreetmap/v4.pmtiles");
|
|
||||||
when.click("modal:sources.add.add_source");
|
|
||||||
when.click("modal:sources.add.add_source");
|
|
||||||
when.wait(200);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
sources: {
|
|
||||||
pmtilestest: {
|
|
||||||
type: "vector",
|
|
||||||
url: "pmtiles://https://data.source.coop/protomaps/openstreetmap/v4.pmtiles",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("add new raster source", () => {
|
|
||||||
const sourceId = "rastertest";
|
|
||||||
when.setValue("modal:sources.add.source_id", sourceId);
|
|
||||||
when.select("modal:sources.add.source_type", "tile_raster");
|
|
||||||
when.select("modal:sources.add.scheme_type", "xyz");
|
|
||||||
when.setValue("modal:sources.add.tile_size", "128");
|
|
||||||
when.click("modal:sources.add.add_source");
|
|
||||||
when.wait(200);
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.sources[sourceId])
|
|
||||||
).shouldInclude({
|
|
||||||
tileSize: 128,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("inspect", () => {
|
|
||||||
it("toggle", () => {
|
|
||||||
// There is no assertion in this test
|
|
||||||
when.setStyle("geojson");
|
|
||||||
when.select("maputnik-select", "inspect");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("style settings", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("nav:settings");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when click name filed spec information", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("field-doc-button-Name");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should show the spec information", () => {
|
|
||||||
then(get.elementsText("spec-field-doc")).shouldInclude(
|
|
||||||
"name for the style"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when set name and click owner", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setValue("modal:settings.name", "foobar");
|
|
||||||
when.click("modal:settings.owner");
|
|
||||||
when.wait(200);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("show name specifications", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
name: "foobar",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when set owner and click name", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setValue("modal:settings.owner", "foobar");
|
|
||||||
when.click("modal:settings.name");
|
|
||||||
when.wait(200);
|
|
||||||
});
|
|
||||||
it("should update owner in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
owner: "foobar",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sprite url", () => {
|
|
||||||
when.setTextInJsonEditor("\"http://example.com\"");
|
|
||||||
when.click("modal:settings.name");
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
sprite: "http://example.com",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sprite object", () => {
|
|
||||||
when.setTextInJsonEditor(JSON.stringify([{ id: "1", url: "2" }]));
|
|
||||||
|
|
||||||
when.click("modal:settings.name");
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
sprite: [{ id: "1", url: "2" }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("glyphs url", () => {
|
|
||||||
const glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
|
|
||||||
when.setValue("modal:settings.glyphs", glyphsUrl);
|
|
||||||
when.click("modal:settings.name");
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
glyphs: glyphsUrl,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("maptiler access token", () => {
|
|
||||||
const apiKey = "testing123";
|
|
||||||
when.setValue(
|
|
||||||
"modal:settings.maputnik:openmaptiles_access_token",
|
|
||||||
apiKey
|
|
||||||
);
|
|
||||||
when.click("modal:settings.name");
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
|
||||||
).shouldInclude({
|
|
||||||
"maputnik:openmaptiles_access_token": apiKey,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("thunderforest access token", () => {
|
|
||||||
const apiKey = "testing123";
|
|
||||||
when.setValue(
|
|
||||||
"modal:settings.maputnik:thunderforest_access_token",
|
|
||||||
apiKey
|
|
||||||
);
|
|
||||||
when.click("modal:settings.name");
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
|
||||||
).shouldInclude({ "maputnik:thunderforest_access_token": apiKey });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("stadia access token", () => {
|
|
||||||
const apiKey = "testing123";
|
|
||||||
when.setValue(
|
|
||||||
"modal:settings.maputnik:stadia_access_token",
|
|
||||||
apiKey
|
|
||||||
);
|
|
||||||
when.click("modal:settings.name");
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
|
||||||
).shouldInclude({ "maputnik:stadia_access_token": apiKey });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("locationiq access token", () => {
|
|
||||||
const apiKey = "testing123";
|
|
||||||
when.setValue(
|
|
||||||
"modal:settings.maputnik:locationiq_access_token",
|
|
||||||
apiKey
|
|
||||||
);
|
|
||||||
when.click("modal:settings.name");
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
|
||||||
).shouldInclude({ "maputnik:locationiq_access_token": apiKey });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("style projection mercator", () => {
|
|
||||||
when.select("modal:settings.projection", "mercator");
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.projection)
|
|
||||||
).shouldInclude({ type: "mercator" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("style projection globe", () => {
|
|
||||||
when.select("modal:settings.projection", "globe");
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.projection)
|
|
||||||
).shouldInclude({ type: "globe" });
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it("style projection vertical-perspective", () => {
|
|
||||||
when.select("modal:settings.projection", "vertical-perspective");
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.projection)
|
|
||||||
).shouldInclude({ type: "vertical-perspective" });
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it("style renderer", () => {
|
|
||||||
cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
|
|
||||||
when.select("modal:settings.maputnik:renderer", "ol");
|
|
||||||
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
|
|
||||||
"ol"
|
|
||||||
);
|
|
||||||
|
|
||||||
when.click("modal:settings.name");
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
metadata: { "maputnik:renderer": "ol" },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
it("inlcude API key when change renderer", () => {
|
|
||||||
|
|
||||||
when.click("modal:settings.close-modal");
|
|
||||||
when.click("nav:open");
|
|
||||||
|
|
||||||
get.elementByAttribute("aria-label", "MapTiler Basic").should("exist").click();
|
|
||||||
when.wait(1000);
|
|
||||||
when.click("nav:settings");
|
|
||||||
|
|
||||||
when.select("modal:settings.maputnik:renderer", "mlgljs");
|
|
||||||
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
|
|
||||||
"mlgljs"
|
|
||||||
);
|
|
||||||
|
|
||||||
when.select("modal:settings.maputnik:renderer", "ol");
|
|
||||||
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
|
|
||||||
"ol"
|
|
||||||
);
|
|
||||||
|
|
||||||
given.intercept("https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=*", "tileRequest", "GET");
|
|
||||||
|
|
||||||
when.select("modal:settings.maputnik:renderer", "mlgljs");
|
|
||||||
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
|
|
||||||
"mlgljs"
|
|
||||||
);
|
|
||||||
|
|
||||||
when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
|
|
||||||
when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
|
|
||||||
when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("add layer", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("layer");
|
|
||||||
when.modal.open();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows duplicate id error", () => {
|
|
||||||
when.setValue("add-layer.layer-id.input", "background");
|
|
||||||
when.click("add-layer");
|
|
||||||
then(get.elementByTestId("modal:add-layer")).shouldExist();
|
|
||||||
then(get.element(".maputnik-modal-error")).shouldContainText(
|
|
||||||
"Layer ID already exists"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("sources", () => {
|
|
||||||
it("toggle");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("global state", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("nav:global-state");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("add variable", () => {
|
|
||||||
when.wait(100);
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
state: { key1: { default: "value" } },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it("add multiple variables", () => {
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
when.wait(100);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
state: { key1: { default: "value" }, key2: { default: "value" }, key3: { default: "value" } },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("remove variable", () => {
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
when.click("global-state-remove-variable", 0);
|
|
||||||
when.wait(100);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
state: { key2: { default: "value" }, key3: { default: "value" } },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("edit variable key", () => {
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
when.wait(100);
|
|
||||||
when.setValue("global-state-variable-key:0", "mykey");
|
|
||||||
when.typeKeys("{enter}");
|
|
||||||
when.wait(100);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
state: { mykey: { default: "value" } },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("edit variable value", () => {
|
|
||||||
when.click("global-state-add-variable");
|
|
||||||
when.wait(100);
|
|
||||||
when.setValue("global-state-variable-value:0", "myvalue");
|
|
||||||
when.typeKeys("{enter}");
|
|
||||||
when.wait(100);
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
state: { key1: { default: "myvalue" } },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("error panel", () => {
|
|
||||||
it("not visible when no errors", () => {
|
|
||||||
then(get.element("maputnik-message-panel-error")).shouldNotExist();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("visible on style error", () => {
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
type: "circle",
|
|
||||||
layer: "invalid",
|
|
||||||
});
|
|
||||||
then(get.element(".maputnik-message-panel-error")).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Handle localStorage QuotaExceededError", () => {
|
|
||||||
it("handles quota exceeded error when opening style from URL", () => {
|
|
||||||
// Clear localStorage to start fresh
|
|
||||||
cy.clearLocalStorage();
|
|
||||||
|
|
||||||
// fill localStorage until we get a QuotaExceededError
|
|
||||||
cy.window().then(win => {
|
|
||||||
let chunkSize = 1000;
|
|
||||||
const chunk = new Array(chunkSize).join("x");
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
// Keep adding until we hit the quota
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
const key = `maputnik:fill-${index++}`;
|
|
||||||
win.localStorage.setItem(key, chunk);
|
|
||||||
} catch (e: any) {
|
|
||||||
// Verify it's a quota error
|
|
||||||
if (e.name === "QuotaExceededError") {
|
|
||||||
if (chunkSize <= 1) return;
|
|
||||||
else {
|
|
||||||
chunkSize /= 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw e; // Unexpected error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open the style via URL input
|
|
||||||
when.click("nav:open");
|
|
||||||
when.setValue("modal:open.url.input", get.exampleFileUrl());
|
|
||||||
when.click("modal:open.url.button");
|
|
||||||
|
|
||||||
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
|
||||||
then(get.styleFromLocalStorage()).shouldExist();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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,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,37 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
// ***********************************************
|
|
||||||
// This example commands.ts shows you how to
|
|
||||||
// create various custom commands and overwrite
|
|
||||||
// existing commands.
|
|
||||||
//
|
|
||||||
// For more comprehensive examples of custom
|
|
||||||
// commands please read more here:
|
|
||||||
// https://on.cypress.io/custom-commands
|
|
||||||
// ***********************************************
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a parent command --
|
|
||||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a child command --
|
|
||||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a dual command --
|
|
||||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This will overwrite an existing command --
|
|
||||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
|
||||||
//
|
|
||||||
// declare global {
|
|
||||||
// namespace Cypress {
|
|
||||||
// interface Chainable {
|
|
||||||
// login(email: string, password: string): Chainable<void>
|
|
||||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
|
||||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
|
||||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<title>Components App</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div data-cy-root></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
// ***********************************************************
|
|
||||||
// This example support/component.ts is processed and
|
|
||||||
// loaded automatically before your test files.
|
|
||||||
//
|
|
||||||
// This is a great place to put global configuration and
|
|
||||||
// behavior that modifies Cypress.
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off
|
|
||||||
// automatically serving support files with the
|
|
||||||
// 'supportFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/configuration
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
|
||||||
import "./commands";
|
|
||||||
|
|
||||||
import { mount } from "cypress/react";
|
|
||||||
|
|
||||||
// Augment the Cypress namespace to include type definitions for
|
|
||||||
// your custom command.
|
|
||||||
// Alternatively, can be defined in cypress/support/component.d.ts
|
|
||||||
// with a <reference path="./component" /> at the top of your spec.
|
|
||||||
declare global {
|
|
||||||
/* eslint-disable @typescript-eslint/no-namespace */
|
|
||||||
namespace Cypress {
|
|
||||||
interface Chainable {
|
|
||||||
mount: typeof mount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Cypress.Commands.add("mount", mount);
|
|
||||||
|
|
||||||
// Example use:
|
|
||||||
// cy.mount(<MyComponent />)
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
// ***********************************************************
|
|
||||||
// This example support/e2e.ts is processed and
|
|
||||||
// loaded automatically before your test files.
|
|
||||||
//
|
|
||||||
// This is a great place to put global configuration and
|
|
||||||
// behavior that modifies Cypress.
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off
|
|
||||||
// automatically serving support files with the
|
|
||||||
// 'supportFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/configuration
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
|
||||||
import "@cypress/code-coverage/support";
|
|
||||||
import "cypress-plugin-tab";
|
|
||||||
import "./commands";
|
|
||||||
|
|
||||||
// Alternatively you can use CommonJS syntax:
|
|
||||||
// require('./commands')
|
|
||||||
31
desktop/.gitignore
vendored
31
desktop/.gitignore
vendored
@@ -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,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,17 +0,0 @@
|
|||||||
export default {
|
|
||||||
output: "src/locales/$LOCALE/$NAMESPACE.json",
|
|
||||||
locales: ["de", "fr", "he", "it", "ja", "ko", "zh"],
|
|
||||||
|
|
||||||
// Because some keys are dynamically generated, i18next-parser can't detect them.
|
|
||||||
// We add these keys manually, so we don't want to remove them.
|
|
||||||
keepRemoved: true,
|
|
||||||
|
|
||||||
// We use plain English keys, so we disable key and namespace separators.
|
|
||||||
keySeparator: false,
|
|
||||||
namespaceSeparator: false,
|
|
||||||
|
|
||||||
defaultValue: (_locale, _ns, _key) => {
|
|
||||||
// The default value is a string that indicates that the string is not translated.
|
|
||||||
return "__STRING_NOT_TRANSLATED__";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Maputnik</title>
|
<title>Maputnik</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="manifest" href="src/manifest.json">
|
<link rel="manifest" href="/maputnik/assets/manifest-BrZzkYP9.json">
|
||||||
<link rel="icon" href="src/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="/maputnik/assets/favicon-DBn6BKLx.ico" type="image/x-icon" />
|
||||||
<style>
|
<style>
|
||||||
html {
|
html {
|
||||||
background-color: rgb(28, 31, 36);
|
background-color: rgb(28, 31, 36);
|
||||||
@@ -37,6 +37,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
<script type="module" crossorigin src="/maputnik/assets/index-S_bu68PO.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/maputnik/assets/index-CuVViU0P.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- From <https://github.com/hail2u/color-blindness-emulation> -->
|
<!-- From <https://github.com/hail2u/color-blindness-emulation> -->
|
||||||
@@ -123,10 +125,9 @@
|
|||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<div class="loading">
|
<div class="loading">
|
||||||
<div class="loading__logo">
|
<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>
|
||||||
<div class="loading__text">Loading…</div>
|
<div class="loading__text">Loading…</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="/src/index.jsx"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
16426
package-lock.json
generated
16426
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
153
package.json
153
package.json
@@ -1,153 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "maputnik",
|
|
||||||
"version": "3.0.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:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
|
|
||||||
"lint": "eslint",
|
|
||||||
"test": "cypress run",
|
|
||||||
"test-unit": "vitest",
|
|
||||||
"test-unit-ci": "vitest run --coverage --reporter=json",
|
|
||||||
"cy:open": "cypress open",
|
|
||||||
"lint-css": "stylelint \"src/styles/*.scss\"",
|
|
||||||
"sort-styles": "jq 'sort_by(.id)' src/config/styles.json > tmp.json && mv tmp.json src/config/styles.json"
|
|
||||||
},
|
|
||||||
"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.2",
|
|
||||||
"@codemirror/state": "^6.5.2",
|
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
|
||||||
"@codemirror/view": "^6.38.8",
|
|
||||||
"@dnd-kit/core": "^6.3.1",
|
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
|
||||||
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
|
|
||||||
"@maplibre/maplibre-gl-geocoder": "^1.9.3",
|
|
||||||
"@maplibre/maplibre-gl-inspect": "^1.8.2",
|
|
||||||
"@maplibre/maplibre-gl-style-spec": "^24.3.1",
|
|
||||||
"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.0.12",
|
|
||||||
"events": "^3.3.0",
|
|
||||||
"file-saver": "^2.0.5",
|
|
||||||
"i18next": "^25.7.2",
|
|
||||||
"i18next-browser-languagedetector": "^8.2.0",
|
|
||||||
"i18next-resources-to-backend": "^1.2.1",
|
|
||||||
"json-stringify-pretty-compact": "^4.0.0",
|
|
||||||
"json-to-ast": "^2.1.0",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"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.14.0",
|
|
||||||
"maputnik-design": "github:maputnik/design#172b06c",
|
|
||||||
"ol": "^10.7.0",
|
|
||||||
"ol-mapbox-style": "^13.2.0",
|
|
||||||
"pmtiles": "^4.3.0",
|
|
||||||
"prop-types": "^15.8.1",
|
|
||||||
"react": "^19.2.0",
|
|
||||||
"react-accessible-accordion": "^5.0.1",
|
|
||||||
"react-aria-menubutton": "^7.0.3",
|
|
||||||
"react-aria-modal": "^5.0.2",
|
|
||||||
"react-collapse": "^5.1.1",
|
|
||||||
"react-color": "^2.19.3",
|
|
||||||
"react-dom": "^19.2.0",
|
|
||||||
"react-i18next": "^16.3.5",
|
|
||||||
"react-icons": "^5.5.0",
|
|
||||||
"react-markdown": "^10.1.0",
|
|
||||||
"reconnecting-websocket": "^4.4.0",
|
|
||||||
"slugify": "^1.6.6",
|
|
||||||
"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": {
|
|
||||||
"@cypress/code-coverage": "^3.14.7",
|
|
||||||
"@eslint/js": "^9.39.2",
|
|
||||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
|
||||||
"@rollup/plugin-replace": "^6.0.2",
|
|
||||||
"@shellygo/cypress-test-utils": "^6.0.6",
|
|
||||||
"@stylistic/eslint-plugin": "^5.6.1",
|
|
||||||
"@types/codemirror": "^5.60.17",
|
|
||||||
"@types/color": "^4.2.0",
|
|
||||||
"@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.7",
|
|
||||||
"@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.1.2",
|
|
||||||
"@vitest/coverage-v8": "^4.0.16",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"cypress": "^15.8.1",
|
|
||||||
"cypress-plugin-tab": "^1.0.5",
|
|
||||||
"eslint": "^9.39.2",
|
|
||||||
"eslint-plugin-react": "^7.37.5",
|
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.23",
|
|
||||||
"i18next-parser": "^9.3.0",
|
|
||||||
"istanbul": "^0.4.5",
|
|
||||||
"istanbul-lib-coverage": "^3.2.2",
|
|
||||||
"postcss": "^8.5.6",
|
|
||||||
"react-hot-loader": "^4.13.1",
|
|
||||||
"sass": "^1.97.1",
|
|
||||||
"stylelint": "^16.26.1",
|
|
||||||
"stylelint-config-recommended-scss": "^16.0.2",
|
|
||||||
"stylelint-scss": "^6.13.0",
|
|
||||||
"typescript": "^5.9.3",
|
|
||||||
"typescript-eslint": "^8.49.0",
|
|
||||||
"uuid": "^13.0.0",
|
|
||||||
"vite": "^7.3.0",
|
|
||||||
"vite-plugin-istanbul": "^7.2.1",
|
|
||||||
"vitest": "^4.0.16"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,972 +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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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 deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter'
|
|
||||||
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^[]+/)![0];
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
mapStyle: newStyle,
|
|
||||||
dirtyMapStyle: dirtyMapStyle,
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
}) => {
|
|
||||||
this.setState({
|
|
||||||
mapView,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
mapRenderer() {
|
|
||||||
const {mapStyle, dirtyMapStyle} = this.state;
|
|
||||||
|
|
||||||
const mapProps = {
|
|
||||||
mapStyle: (dirtyMapStyle || mapStyle),
|
|
||||||
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,67 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {formatLayerId} from "../libs/format";
|
|
||||||
import {type LayerSpecification, type StyleSpecification} from "maplibre-gl";
|
|
||||||
import { Trans, 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 = (
|
|
||||||
<>
|
|
||||||
<Trans t={t}>
|
|
||||||
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
|
||||||
</Trans>
|
|
||||||
{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>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import InputJson from "./InputJson";
|
|
||||||
import React from "react";
|
|
||||||
import { withTranslation, type WithTranslation } from "react-i18next";
|
|
||||||
import { type StyleSpecification } from "maplibre-gl";
|
|
||||||
import { type StyleSpecificationWithId } from "../libs/definitions";
|
|
||||||
|
|
||||||
export type CodeEditorProps = {
|
|
||||||
value: StyleSpecification;
|
|
||||||
onChange: (value: StyleSpecificationWithId) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
} & WithTranslation;
|
|
||||||
|
|
||||||
const CodeEditorInternal: React.FC<CodeEditorProps> = (props) => {
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<button className="maputnik-button" onClick={props.onClose} aria-label={props.t("Close")} style={{ position: "sticky", top: "0", zIndex: 1 }}>{props.t("Click to close the editor")}</button>
|
|
||||||
<InputJson
|
|
||||||
lintType="style"
|
|
||||||
value={props.value}
|
|
||||||
onChange={props.onChange}
|
|
||||||
className={"maputnik-code-editor"}
|
|
||||||
withScroll={true}
|
|
||||||
/>;
|
|
||||||
</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CodeEditor = withTranslation()(CodeEditorInternal);
|
|
||||||
|
|
||||||
export default CodeEditor;
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Collapse as ReactCollapse } from "react-collapse";
|
|
||||||
import {reducedMotionEnabled} from "../libs/accessibility";
|
|
||||||
|
|
||||||
|
|
||||||
type CollapseProps = {
|
|
||||||
isActive: boolean
|
|
||||||
children: React.ReactElement
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export default class Collapse extends React.Component<CollapseProps> {
|
|
||||||
static defaultProps = {
|
|
||||||
isActive: true
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (reducedMotionEnabled()) {
|
|
||||||
return (
|
|
||||||
<div style={{display: this.props.isActive ? "block" : "none"}}>
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return (
|
|
||||||
<ReactCollapse isOpened={this.props.isActive}>
|
|
||||||
{this.props.children}
|
|
||||||
</ReactCollapse>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {MdArrowDropDown, MdArrowDropUp} from "react-icons/md";
|
|
||||||
|
|
||||||
type CollapserProps = {
|
|
||||||
isCollapsed: boolean
|
|
||||||
style?: object
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class Collapser extends React.Component<CollapserProps> {
|
|
||||||
render() {
|
|
||||||
const iconStyle = {
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
...this.props.style,
|
|
||||||
};
|
|
||||||
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Markdown from "react-markdown";
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
js: "JS",
|
|
||||||
android: "Android",
|
|
||||||
ios: "iOS"
|
|
||||||
};
|
|
||||||
|
|
||||||
type DocProps = {
|
|
||||||
fieldSpec: {
|
|
||||||
doc?: string
|
|
||||||
values?: {
|
|
||||||
[key: string]: {
|
|
||||||
doc?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"sdk-support"?: {
|
|
||||||
[key: string]: typeof headers
|
|
||||||
}
|
|
||||||
docUrl?: string,
|
|
||||||
docUrlLinkText?: string
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class Doc extends React.Component<DocProps> {
|
|
||||||
render () {
|
|
||||||
const {fieldSpec} = this.props;
|
|
||||||
|
|
||||||
const {doc, values, docUrl, docUrlLinkText} = fieldSpec;
|
|
||||||
const sdkSupport = fieldSpec["sdk-support"];
|
|
||||||
|
|
||||||
const renderValues = (
|
|
||||||
!!values &&
|
|
||||||
// HACK: Currently we merge additional values into the style spec, so this is required
|
|
||||||
// See <https://github.com/maplibre/maputnik/blob/main/src/components/PropertyGroup.jsx#L16>
|
|
||||||
!Array.isArray(values)
|
|
||||||
);
|
|
||||||
|
|
||||||
const sdkSupportToJsx = (value: string) => {
|
|
||||||
const supportValue = value.toLowerCase();
|
|
||||||
if (supportValue.startsWith("https://")) {
|
|
||||||
return <a href={supportValue} target="_blank" rel="noreferrer">{"#" + supportValue.split("/").pop()}</a>;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{doc &&
|
|
||||||
<div className="SpecDoc">
|
|
||||||
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>
|
|
||||||
<Markdown components={{
|
|
||||||
a: ({node: _node, href, children, ...props}) => <a href={href} target="_blank" {...props}>{children}</a>,
|
|
||||||
}}>{doc}</Markdown>
|
|
||||||
</div>
|
|
||||||
{renderValues &&
|
|
||||||
<ul className="SpecDoc__values">
|
|
||||||
{Object.entries(values).map(([key, value]) => {
|
|
||||||
return (
|
|
||||||
<li key={key}>
|
|
||||||
<code>{JSON.stringify(key)}</code>
|
|
||||||
<div>{value.doc}</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{sdkSupport &&
|
|
||||||
<div className="SpecDoc__sdk-support">
|
|
||||||
<table className="SpecDoc__sdk-support__table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
{Object.values(headers).map(header => {
|
|
||||||
return <th key={header}>{header}</th>;
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Object.entries(sdkSupport).map(([key, supportObj]) => {
|
|
||||||
return (
|
|
||||||
<tr key={key}>
|
|
||||||
<td>{key}</td>
|
|
||||||
{Object.keys(headers).map((k) => {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
|
|
||||||
return <td key={k}>{sdkSupportToJsx(supportObj[k as keyof typeof headers])}</td>;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return <td key={k}>no</td>;
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{docUrl && docUrlLinkText &&
|
|
||||||
<div className="SpecDoc__learn-more">
|
|
||||||
<a href={docUrl} target="_blank" rel="noreferrer">{docUrlLinkText}</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import InputArray, { type InputArrayProps } from "./InputArray";
|
|
||||||
import Fieldset from "./Fieldset";
|
|
||||||
|
|
||||||
type FieldArrayProps = InputArrayProps & {
|
|
||||||
name?: string
|
|
||||||
fieldSpec?: {
|
|
||||||
doc: string
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const FieldArray: React.FC<FieldArrayProps> = (props) => {
|
|
||||||
return (
|
|
||||||
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
|
|
||||||
<InputArray {...props} />
|
|
||||||
</Fieldset>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FieldArray;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user