Compare commits

..

2 Commits

Author SHA1 Message Date
HarelM 285dca8d2c Fix lint 2024-02-04 18:07:43 +02:00
HarelM 2a3b6aa770 Improve inspect hover 2024-02-04 18:02:37 +02:00
224 changed files with 14990 additions and 21500 deletions
+46
View File
@@ -0,0 +1,46 @@
{
"root": true,
"env": {
"browser": true,
"es2020": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
],
"ignorePatterns": [
"dist"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"settings": {
"react": { "version": "16.4" }
},
"plugins": [
"@typescript-eslint",
"react-refresh"],
"rules": {
"react-refresh/only-export-components": [
"warn",
{ "allowConstantExport": true }
],
"@typescript-eslint/no-unused-vars": [
"warn",
{ "argsIgnorePattern": "^_" }
],
"no-unused-vars": "off",
"react/prop-types": ["off"],
// Disable no-undef. It's covered by @typescript-eslint
"no-undef": "off",
"indent": ["error", 2],
"no-var": ["error"]
},
"globals": {
"global": "readonly"
}
}
+1
View File
@@ -8,3 +8,4 @@ assignees: ''
--- ---
<!-- Thanks for reaching out! If you are having general Maputnik mapping questions, please asking them at https://gis.stackexchange.com/ using the 'maputnik' tag https://gis.stackexchange.com/questions/tagged/maputnik and read https://gis.stackexchange.com/help/how-to-ask before you do so (please keep in mind that you're asking there in a general GIS forum, not a dedicated support channel) --> <!-- Thanks for reaching out! If you are having general Maputnik mapping questions, please asking them at https://gis.stackexchange.com/ using the 'maputnik' tag https://gis.stackexchange.com/questions/tagged/maputnik and read https://gis.stackexchange.com/help/how-to-ask before you do so (please keep in mind that you're asking there in a general GIS forum, not a dedicated support channel) -->
-10
View File
@@ -1,10 +0,0 @@
## Launch Checklist
<!-- Thanks for the PR! Feel free to add or remove items from the checklist. -->
- [ ] Briefly describe the changes in this PR.
- [ ] Link to related issues.
- [ ] Include before/after visuals or gifs if this PR includes visual changes.
- [ ] Write tests for all new functionality.
- [ ] Add an entry to `CHANGELOG.md` under the `## main` section.
+5 -30
View File
@@ -1,39 +1,14 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2 version: 2
updates: updates:
- package-ecosystem: "npm" # See documentation for possible values - package-ecosystem: "npm"
directory: "/" # Location of package manifests directory: "/"
schedule: schedule:
interval: "daily" interval: "daily"
open-pull-requests-limit: 20 open-pull-requests-limit: 2
versioning-strategy: increase versioning-strategy: increase
groups:
vitest:
patterns:
- "*vitest*"
react:
patterns:
- "*react*"
cooldown:
default-days: 5
semver-major-days: 5
semver-minor-days: 3
semver-patch-days: 3
include:
- "*"
exclude:
- "@maplibre/*"
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "daily"
cooldown: open-pull-requests-limit: 2
default-days: 3 versioning-strategy: increase
# no semver support for github-actions
# => no specific configuration for this
include:
- "*"
@@ -1,26 +0,0 @@
name: Automerge Dependabot
on: pull_request
permissions: write-all
jobs:
dependabot:
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }}
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Approve Dependabot PRs
run: gh pr review --approve "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Enable auto-merge for Dependabot PRs
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
+71 -106
View File
@@ -7,148 +7,113 @@ on:
branches: [ main ] branches: [ main ]
jobs: jobs:
build-docker:
name: build docker
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
steps:
- uses: actions/checkout@v4
- run: docker build -t test-docker-image-build .
# build the editor
build-node: build-node:
name: "build on ${{ matrix.os }}" name: "build on ${{ matrix.os }}"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
permissions:
contents: read
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }} if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ] os: [ubuntu-latest, windows-latest, macos-latest]
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@v4
with: { persist-credentials: false } - uses: actions/setup-node@v4
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with:
with: node-version-file: '.nvmrc'
node-version-file: '.nvmrc' - run: npm ci
- run: npm ci - run: npm run build
- run: npm run build - run: npm run lint
- run: npm run lint - run: npm run lint-css
- run: npm run lint-css
build-artifacts: build-artifacts:
name: "build artifacts" name: "build artifacts"
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }} if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@v4
with: { persist-credentials: false } - uses: actions/setup-node@v4
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with:
with: node-version-file: '.nvmrc'
node-version-file: '.nvmrc' - run: npm ci
- run: npm ci - run: npm run build
- run: npm run build - name: artifacts/maputnik
- name: artifacts/maputnik uses: actions/upload-artifact@v1
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with:
with: name: maputnik
name: maputnik path: dist
path: dist
# Build and upload desktop CLI artifacts # Build and upload desktop CLI artifacts
- name: Set up Go - name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@v3
with: with:
go-version: ^1.23.x go-version: ^1.19.x
cache-dependency-path: desktop/go.sum id: go
id: go
- name: Build desktop artifacts - name: Check out code into the Go module directory
run: npm run build-desktop uses: actions/checkout@v4
with:
repository: maputnik/desktop
ref: master
path: ./src/github.com/maputnik/desktop/
- name: Artifacts/linux - name: Make
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 run: cd src/github.com/maputnik/desktop/ && make
with:
name: maputnik-linux
path: ./desktop/bin/linux/
- name: Artifacts/darwin - name: Artifacts/linux
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@v1
with: with:
name: maputnik-darwin name: maputnik-linux
path: ./desktop/bin/darwin/ path: ./src/github.com/maputnik/desktop/bin/linux/
- name: Artifacts/windows - name: Artifacts/darwin
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@v1
with: with:
name: maputnik-windows name: maputnik-darwin
path: ./desktop/bin/windows/ path: ./src/github.com/maputnik/desktop/bin/darwin/
unit-tests:
name: "Unit tests"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: { persist-credentials: false }
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: '.nvmrc'
- run: npm ci
- run: npm run test-unit-ci
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
files: ${{ github.workspace }}/coverage/coverage-final.json
verbose: true
- name: Artifacts/windows
uses: actions/upload-artifact@v1
with:
name: maputnik-windows
path: ./src/github.com/maputnik/desktop/bin/windows/
e2e-tests: e2e-tests:
name: "E2E tests using chrome" name: "E2E tests using ${{ matrix.browser }}"
strategy:
fail-fast: false
matrix:
browser: [chrome, firefox]
runs-on: ubuntu-latest runs-on: ubuntu-22.04
permissions:
contents: read
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@v4
with: { persist-credentials: false }
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: '.nvmrc'
- run: npm ci - run: npm ci
- name: Cypress run - name: Cypress run
uses: cypress-io/github-action@fa4a118725a8f001170d49631ea89e5d66fee626 # v7.4.1 uses: cypress-io/github-action@v6
with: with:
build: npm run build build: npm run build
start: npm run start start: npm run start
browser: chrome browser: ${{ matrix.browser }}
- name: Upload coverage reports to Codecov - name: Upload coverage reports to Codecov
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 uses: codecov/codecov-action@v3
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: { persist-credentials: false }
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: '.nvmrc'
- run: npm ci
- name: Cypress run
uses: cypress-io/github-action@fa4a118725a8f001170d49631ea89e5d66fee626 # v7.4.1
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@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with: with:
files: ${{ github.workspace }}/.nyc_output/out.json files: ${{ github.workspace }}/.nyc_output/out.json
verbose: true verbose: true
-70
View File
@@ -1,70 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: '17 0 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
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@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
# ️ 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@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
@@ -1,39 +0,0 @@
name: Create bump version PR
on:
workflow_dispatch:
inputs:
version:
description: Version to change to.
required: true
type: string
jobs:
bump-version-pr:
name: Bump version PR
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
- name: Bump version
run: |
npm version --commit-hooks false --git-tag-version false ${{ inputs.version }}
./build/bump-version-changelog.js ${{ inputs.version }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
commit-message: Bump version to ${{ inputs.version }}
branch: bump-version-to-${{ inputs.version }}
title: Bump version to ${{ inputs.version }}
+16 -20
View File
@@ -8,48 +8,44 @@ jobs:
deploy-pages: deploy-pages:
name: deploy/pages name: deploy/pages
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
if: ${{ github.event_name == 'push' }} if: ${{ github.event_name == 'push' }}
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@v4
with: { persist-credentials: false }
- name: Use Node.js from nvmrc - name: Use Node.js from nvmrc
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@v4
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
- name: Install - name: Install
run: npm ci run: npm ci
- name: Build - name: Build
run: npm run build run: npm run build
- name: Upload to GitHub Pages - name: Upload to GitHub Pages
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0 uses: peaceiris/actions-gh-pages@v3
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: dist publish_dir: dist
# publish docker to GitHub registry # publish docker to GitHub registry
deploy-docker: deploy-docker:
name: deploy/docker name: deploy/docker
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' }} if: ${{ github.event_name == 'push' }}
permissions:
contents: read
packages: write
strategy: strategy:
fail-fast: false fail-fast: false
steps: steps:
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@v4
- run: docker build -t ghcr.io/maplibre/maputnik:main . - run: docker build -t ghcr.io/maplibre/maputnik:main .
- run: docker push ghcr.io/maplibre/maputnik:main - run: docker push ghcr.io/maplibre/maputnik:main
-104
View File
@@ -1,104 +0,0 @@
name: Release
on:
push:
branches: [ main ]
workflow_dispatch:
jobs:
release-check:
name: Check if version changed
runs-on: ubuntu-latest
defaults:
run:
shell: bash
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: main
persist-credentials: false
- name: Use Node.js from nvmrc
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
- name: Check if version has been updated
id: check
uses: EndBug/version-check@095362f3cd50f690c8fa0e6afeea81834bd8d320 # latest
outputs:
publish: ${{ steps.check.outputs.changed }}
release:
name: Release
needs: release-check
if: ${{ needs.release-check.outputs.publish == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ".nvmrc"
registry-url: "https://registry.npmjs.org"
- name: Set up Go for desktop build
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
id: go
- name: Get version
id: package-version
uses: martinbeentjes/npm-get-version-action@3cf273023a0dda27efcd3164bdfb51908dd46a5b # v1.3.1
- name: Install
run: npm ci
- name: Build
run: |
npm run build-desktop
- name: Tag commit and push
id: tag_version
uses: mathieudutour/github-tag-action@a22cf08638b34d5badda920f9daf6e72c477b07b # v6.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
custom_tag: ${{ steps.package-version.outputs.current-version }}
- name: Create Archives
run: |
zip -r "desktop-${{ steps.package-version.outputs.current-version }}" desktop/bin/
- name: Build Release Notes
id: release_notes
run: |
RELEASE_NOTES_PATH="${PWD}/release_notes.txt"
./build/release-notes.js > ${RELEASE_NOTES_PATH}
echo "release_notes=${RELEASE_NOTES_PATH}" >> $GITHUB_OUTPUT
- name: Create GitHub Release
id: create_regular_release
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag: ${{ steps.tag_version.outputs.new_tag }}
name: ${{ steps.tag_version.outputs.new_tag }}
bodyFile: ${{ steps.release_notes.outputs.release_notes }}
artifacts: "desktop-${{ steps.package-version.outputs.current-version }}.zip"
allowUpdates: true
draft: false
prerelease: false
+1 -9
View File
@@ -33,14 +33,6 @@ node_modules
public public
/errorShots /errorShots
/old /old
/build
/cypress/screenshots /cypress/screenshots
/dist/ /dist/
/desktop/version.go
# IDE
.vscode/
.idea/
# Window metadata files
/desktop/winres/winres.json
/desktop/*.syso
-1
View File
@@ -1 +0,0 @@
legacy-peer-deps = true
+1 -1
View File
@@ -1 +1 @@
22.13 18.19
-24
View File
@@ -1,24 +0,0 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
ci:
autoupdate_schedule: monthly
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: check-executables-have-shebangs
- id: check-json
exclude: 'tsconfig(\.node)?\.json'
- id: check-shebang-scripts-are-executable
- id: check-symlinks
- id: check-toml
- id: check-yaml
args: [ --allow-multiple-documents ]
- id: destroyed-symlinks
- id: end-of-file-fixer
- id: mixed-line-ending
args: [ --fix=lf ]
- id: trailing-whitespace
-47
View File
@@ -1,47 +0,0 @@
Maputnik is a MapLibre style editor written using React and TypeScript.
To get started, install all npm packages:
```
npm install
```
Verify code correctness by running ESLint:
```
npm run lint
```
Or try fixing lint issues with:
```
npm run lint -- --fix
```
The project type checked and built with:
```
npm run build
```
To run the tests make sure that xvfb is installed:
```
apt install xvfb
```
Run the development server in the background with Vite:
```
nohup npm run start &
```
Then start the Cypress tests with:
```
xvfb-run -a npm run test
```
## Pull Requests
- Pull requests should update `CHANGELOG.md` with a short description of the change.
-84
View File
@@ -1,84 +0,0 @@
## main
### ✨ Features and improvements
- _...Add new stuff here..._
### 🐞 Bug fixes
- _...Add new stuff here..._
## 3.1.0
### ✨ Features and improvements
- Added translation to "Links" in debug modal
- Add support for hillshade's color arrays and relief-color elevation expression
- Change layers icons to make them a bit more distinct
- Remove `@mdi` packages in favor of `react-icons`
- Add ability to control the projection of the map - either globe or mercator
- Add markdown support for doc related to the style-spec fields
- Added global state modal to allow editing the global state
- Added color highlight for problematic properties
- Upgraded codemirror from version 5 to version 6
- Add code editor to allow editing the entire style
- Add support for sprite object in setting modal
- Set the correct map view when opening a new style on an empty map
- Allow root-relative urls in the stylefile
### 🐞 Bug fixes
- Improved the local style open modal and local file upload.
- Fixed the Expression editor (for long expressions) being able to be float under other components further down
- Fixed an issue when clicking on a popup and then clicking on the map again
- Fix modal close button position
- Fixed an issue with the generation of translations
- Fix missing spec info when clicking next to a property
- Fix Firefox open file that stopped working due to react upgrade
- Fix issue with missing bottom error panel
- Fixed headers in left panes (Layers list and Layer editor) to remain visible when scrolling
- Fix error when using a source from localhost
- Fix an issue with scrolling when using the code editor
## 3.0.0
### ✨ Features and improvements
- Fix radio/delete filter buttons styling regression
- Add german translation
- Use same version number for web and desktop versions
- Add scheme type options for vector/raster tile
- Add `tileSize` field for raster and raster-dem tile sources
- Update Protomaps Light gallery style to v4
- Add support to edit local files on the file system if supported by the browser
- Upgrade to MapLibre LG JS v5
- Upgrade Vite 6 and Cypress 14 ([#970](https://github.com/maplibre/maputnik/pull/970))
- Upgrade OpenLayers from v6 to v10
- When loading a style into localStorage that causes a QuotaExceededError, purge localStorage and retry
- Remove react-autobind dependency
- Remove usage of legacy `childContextTypes` API
- Refactor Field components to use arrow function syntax
- Replace react-autocomplete with Downshift in the autocomplete component
- Add LocationIQ as supported map provider with access token field and gallery style
- Use maputnik go binary for the docker image to allow file watching
- Revmove support for `debug` and `localport` url parameters
- Replace react-sortable-hoc with dnd-kit to avoid react console warnings and also use a maintained library
### 🐞 Bug fixes
- Fix incorrect handing of network error response (#944)
- Show an error when adding a layer with a duplicate ID
- Replace deprecated `ReactDOM.render` usage with `createRoot` and drop the
`DOMNodeRemoved` cleanup hack
## 2.1.1
### ✨ Features and improvements
- Add GitHub workflows for releasing new versions
- Update desktop build to pull from this repo (#922)
## 2.0.0
- Update MapLibre to version 4 (#872)
- Start continuous deployment of maputnik website
## 1.7.0
- See release notes at https://maputnik.github.io/blog/2020/04/23/release-v1.7.0
+10 -8
View File
@@ -1,14 +1,16 @@
FROM golang:1.23-alpine AS builder FROM node:18 as builder
WORKDIR /maputnik WORKDIR /maputnik
RUN apk add --no-cache nodejs npm make git gcc g++ libc-dev # Only copy package.json to prevent npm install from running on every build
COPY package.json package-lock.json ./
RUN npm install
# Build maputnik # Build maputnik
COPY . . COPY . .
RUN npm ci RUN npx vite build
RUN CGO_ENABLED=1 GOOS=linux npm run build-linux
FROM alpine:latest #---------------------------------------------------------------------------
WORKDIR /app # Create a clean nginx-alpine slim image with just the build results
COPY --from=builder /maputnik/desktop/bin/linux ./ FROM nginx:alpine-slim
ENTRYPOINT ["/app/maputnik"]
COPY --from=builder /maputnik/dist /usr/share/nginx/html/
+1
View File
@@ -20,3 +20,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
+6 -19
View File
@@ -18,15 +18,9 @@ targeted at developers and map designers.
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server. - In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
```bash ```bash
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main docker run -it --rm -p 8888:8888 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 ## Documentation
The documentation can be found in the [Wiki](https://github.com/maplibre/maputnik/wiki). You are welcome to collaborate! The documentation can be found in the [Wiki](https://github.com/maplibre/maputnik/wiki). You are welcome to collaborate!
@@ -42,8 +36,6 @@ Maputnik is written in typescript and is using [React](https://github.com/facebo
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule). We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
Check out our [Internationalization guide](./src/locales/README.md) for UI text related changes.
### Getting Involved ### 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. 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.
@@ -74,14 +66,14 @@ Lint the JavaScript code.
``` ```
# run linter # run linter
npm run lint npm run lint
npm run lint-css npm run lint-styles
npm run sort-styles
``` ```
## Tests ## Tests
For E2E testing we use [Cypress](https://www.cypress.io/) For E2E testing we use [Cypress](https://www.cypress.io/)
[Cypress](https://www.cypress.io/) doesn't start a server so you'll need to start one manually by running `npm run start`. [Cypress](https://www.cypress.io/) doesn't starts a server so you'll need to start one manually by running `npm run start`.
Now open a terminal and run the following using *chrome*: Now open a terminal and run the following using *chrome*:
@@ -101,15 +93,10 @@ You can also see the tests as they run or select which suites to run by executin
npm run cy:open npm run cy:open
``` ```
## Release process
1. Review [`CHANGELOG.md`](/CHANGELOG.md) ## Related Projects
- Double-check that all changes included in the release are appropriately documented.
- To-be-released changes should be under the "main" header.
- Commit any final changes to the changelog.
2. Run [Create bump version PR](https://github.com/maplibre/maputnik/actions/workflows/create-bump-version-pr.yml) by manual workflow dispatch and set the version number in the input. This will create a PR that changes the changelog and `package.json` file to review and merge.
3. Once merged, an automatic process will kick in and creates a GitHub release and uploads release assets.
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
## Sponsors ## Sponsors
-11
View File
@@ -1,11 +0,0 @@
# Build Scripts
This folder holds common build scripts used by some of the Github workflows.
The scripts are borrowed from [maplibre/maplibre-gl-js](https://github.com/maplibre/maplibre-gl-js/tree/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build).
## Generate Release Notes
`bump-version-changelog.js` Used to update the changelog with the current notes, and set up a space for new notes
`release-notes.js` Used to generate release notes when releasing a new version
-29
View File
@@ -1,29 +0,0 @@
#!/usr/bin/env node
/**
* This script updates the changelog.md file with the version given in the arguments
* It replaces ## main with ## <version>
* Removes _...Add new stuff here..._
* And adds on top a ## main with add stuff here.
*
* Copied from maplibre/maplibre-gl-js
* https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
*/
import * as fs from "fs";
const changelogPath = "CHANGELOG.md";
let changelog = fs.readFileSync(changelogPath, "utf8");
changelog = changelog.replace("## main", `## ${process.argv[2]}`);
changelog = changelog.replaceAll("- _...Add new stuff here..._\n", "");
changelog = `## main
### ✨ Features and improvements
- _...Add new stuff here..._
### 🐞 Bug fixes
- _...Add new stuff here..._
` + changelog;
fs.writeFileSync(changelogPath, changelog, "utf8");
-47
View File
@@ -1,47 +0,0 @@
#!/usr/bin/env node
// Copied from maplibre/maplibre-gl-js
// https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
import * as fs from "fs";
const changelogPath = "CHANGELOG.md";
const changelog = fs.readFileSync(changelogPath, "utf8");
/*
Parse the raw changelog text and split it into individual releases.
This regular expression:
- Matches lines starting with "## x.x.x".
- Groups the version number.
- Skips the (optional) release date.
- Groups the changelog content.
- Ends when another "## x.x.x" is found.
*/
const regex = /^## (\d+\.\d+\.\d+.*?)\n(.+?)(?=\n^## \d+\.\d+\.\d+.*?\n)/gms;
const releaseNotes = [];
let match;
// eslint-disable-next-line no-cond-assign
while (match = regex.exec(changelog)) {
releaseNotes.push({
"version": match[1],
"changelog": match[2].trim(),
});
}
const latest = releaseNotes[0];
const previous = releaseNotes[1];
// Print the release notes template.
let header = "Changes since previous version";
if (previous) {
header = `https://github.com/maplibre/maputnik
[Changes](https://github.com/maplibre/maputnik/compare/v${previous.version}...v${latest.version}) since [Maputnik v${previous.version}](https://github.com/maplibre/maputnik/releases/tag/v${previous.version})`;
}
const templatedReleaseNotes = `${header}
${latest.changelog}`;
process.stdout.write(templatedReleaseNotes.trimEnd());
-18
View File
@@ -8,34 +8,16 @@ export default defineConfig({
exclude: "cypress/**/*.*", exclude: "cypress/**/*.*",
}, },
}, },
e2e: { e2e: {
setupNodeEvents(on, config) { setupNodeEvents(on, config) {
// implement node event listeners here // implement node event listeners here
require("@cypress/code-coverage/task")(on, config); require("@cypress/code-coverage/task")(on, config);
on("before:browser:launch", (browser, launchOptions) => {
if (browser.family !== "chromium") {
return;
}
launchOptions.args.push("--disable-gpu");
launchOptions.args.push("--enable-features=AllowSwiftShaderFallback,AllowSoftwareGLFallbackDueToCrashes");
launchOptions.args.push("--enable-unsafe-swiftshader");
return launchOptions;
});
return config; return config;
}, },
baseUrl: "http://localhost:8888", baseUrl: "http://localhost:8888",
scrollBehavior: "center",
retries: { retries: {
runMode: 2, runMode: 2,
openMode: 0, openMode: 0,
}, },
}, },
component: {
devServer: {
framework: "react",
bundler: "vite",
},
},
}); });
+3 -3
View File
@@ -1,7 +1,7 @@
import { MaputnikDriver } from "./maputnik-driver"; import { MaputnikDriver } from "./maputnik-driver";
describe("accessibility", () => { describe("accessibility", () => {
const { beforeAndAfter, get, when, then } = new MaputnikDriver(); let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter(); beforeAndAfter();
describe("skip links", () => { describe("skip links", () => {
@@ -18,10 +18,10 @@ describe("accessibility", () => {
then(get.skipTargetLayerList()).shouldBeFocused(); then(get.skipTargetLayerList()).shouldBeFocused();
}); });
it("skip link to layer editor", () => { // This fails for some reason only in Chrome, but passes in firefox. Adding a skip here to allow merge and later on we'll decide if we want to fix this or not.
it.skip("skip link to layer editor", () => {
const selector = "root:skip:layer-editor"; const selector = "root:skip:layer-editor";
then(get.elementByTestId(selector)).shouldExist(); then(get.elementByTestId(selector)).shouldExist();
then(get.elementByTestId("skip-target-layer-editor")).shouldExist();
when.tab().tab(); when.tab().tab();
then(get.elementByTestId(selector)).shouldBeFocused(); then(get.elementByTestId(selector)).shouldBeFocused();
when.click(selector); when.click(selector);
-18
View File
@@ -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();
});
});
+2 -1
View File
@@ -1,7 +1,7 @@
import { MaputnikDriver } from "./maputnik-driver"; import { MaputnikDriver } from "./maputnik-driver";
describe("history", () => { describe("history", () => {
const { beforeAndAfter, when, get, then } = new MaputnikDriver(); let { beforeAndAfter, when, get, then } = new MaputnikDriver();
beforeAndAfter(); beforeAndAfter();
let undoKeyCombo: string; let undoKeyCombo: string;
@@ -16,6 +16,7 @@ describe("history", () => {
it("undo/redo", () => { it("undo/redo", () => {
when.setStyle("geojson"); when.setStyle("geojson");
when.modal.open(); when.modal.open();
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
when.modal.fillLayers({ when.modal.fillLayers({
id: "step 1", id: "step 1",
-35
View File
@@ -1,35 +0,0 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("i18n", () => {
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
describe("language detector", () => {
it("English", () => {
const url = "?lng=en";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
});
it("Japanese", () => {
const url = "?lng=ja";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
});
});
describe("language switcher", () => {
beforeEach(() => {
when.setStyle("layer");
});
it("the language switcher switches to Japanese", () => {
const selector = "maputnik-lang-select";
then(get.elementByTestId(selector)).shouldExist();
when.select(selector, "ja");
then(get.elementByTestId(selector)).shouldHaveValue("ja");
then(get.elementByTestId("nav:settings")).shouldHaveText("スタイル設定");
});
});
});
+1 -1
View File
@@ -1,7 +1,7 @@
import { MaputnikDriver } from "./maputnik-driver"; import { MaputnikDriver } from "./maputnik-driver";
describe("keyboard", () => { describe("keyboard", () => {
const { beforeAndAfter, given, when, get, then } = new MaputnikDriver(); let { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
beforeAndAfter(); beforeAndAfter();
describe("shortcuts", () => { describe("shortcuts", () => {
beforeEach(() => { beforeEach(() => {
-294
View File
@@ -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();
});
});
});
-541
View File
@@ -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();
});
});
});
+478
View File
@@ -0,0 +1,478 @@
import { v1 as uuid } from "uuid";
import { MaputnikDriver } from "./maputnik-driver";
describe("layers", () => {
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
beforeEach(() => {
when.setStyle("both");
when.modal.open();
});
describe("ops", () => {
let id: string;
beforeEach(() => {
id = when.modal.fillLayers({
type: "background",
});
});
it("should update layers in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "background",
},
],
});
});
describe("when clicking delete", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":delete");
});
it("should empty layers in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [],
});
});
});
describe("when clicking duplicate", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":copy");
});
it("should add copy layer in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id + "-copy",
type: "background",
},
{
id: id,
type: "background",
},
],
});
});
});
describe("when clicking hide", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":toggle-visibility");
});
it("should update visibility to none in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "background",
layout: {
visibility: "none",
},
},
],
});
});
describe("when clicking show", () => {
beforeEach(() => {
when.click("layer-list-item:" + id + ":toggle-visibility");
});
it("should update visibility to visible in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "background",
layout: {
visibility: "visible",
},
},
],
});
});
});
});
});
describe("background", () => {
it("add", () => {
let id = when.modal.fillLayers({
type: "background",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "background",
},
],
});
});
describe("modify", () => {
function createBackground() {
// Setup
let id = uuid();
when.selectWithin("add-layer.layer-type", "background");
when.setValue("add-layer.layer-id.input", "background:" + id);
when.click("add-layer");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + id,
type: "background",
},
],
});
return id;
}
// ====> THESE SHOULD BE FROM THE SPEC
describe("layer", () => {
it("expand/collapse");
it("id", () => {
let bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
let id = uuid();
when.setValue("layer-editor.layer-id.input", "foobar:" + id);
when.click("min-zoom");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "foobar:" + id,
type: "background",
},
],
});
});
describe("min-zoom", () => {
let bgId: string;
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.setValue("min-zoom.input-text", "1");
when.click("layer-editor.layer-id");
});
it("should update min-zoom in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
minzoom: 1,
},
],
});
});
it("when clicking next layer should update style on local storage", () => {
when.type("min-zoom.input-text", "{backspace}");
when.click("max-zoom.input-text");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
minzoom: 1,
},
],
});
});
});
describe("max-zoom", () => {
let bgId: string;
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.setValue("max-zoom.input-text", "1");
when.click("layer-editor.layer-id");
});
it("should update style in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
maxzoom: 1,
},
],
});
});
});
describe("comments", () => {
let bgId: string;
let comment = "42";
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.setValue("layer-comment.input", comment);
when.click("layer-editor.layer-id");
});
it("should update style in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
metadata: {
"maputnik:comment": comment,
},
},
],
});
});
describe("when unsetting", () => {
beforeEach(() => {
when.clear("layer-comment.input");
when.click("min-zoom.input-text");
});
it("should update style in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
},
],
});
});
});
});
describe("color", () => {
let bgId: string;
beforeEach(() => {
bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
when.click("spec-field:background-color");
});
it("should update style in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "background:" + bgId,
type: "background",
},
],
});
});
});
});
describe("filter", () => {
it("expand/collapse");
it("compound filter");
});
describe("paint", () => {
it("expand/collapse");
it("color");
it("pattern");
it("opacity");
});
// <=====
describe("json-editor", () => {
it("expand/collapse");
it("modify");
// TODO
it.skip("parse error", () => {
let bgId = createBackground();
when.click("layer-list-item:background:" + bgId);
let errorSelector = ".CodeMirror-lint-marker-error";
then(get.elementByTestId(errorSelector)).shouldNotExist();
when.click(".CodeMirror");
when.typeKeys(
"\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"
);
then(get.elementByTestId(errorSelector)).shouldExist();
});
});
});
});
describe("fill", () => {
it("add", () => {
let id = when.modal.fillLayers({
type: "fill",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "fill",
source: "example",
},
],
});
});
// TODO: Change source
it("change source");
});
describe("line", () => {
it("add", () => {
let id = when.modal.fillLayers({
type: "line",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "line",
source: "example",
},
],
});
});
it("groups", () => {
// TODO
// Click each of the layer groups.
});
});
describe("symbol", () => {
it("add", () => {
let id = when.modal.fillLayers({
type: "symbol",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "symbol",
source: "example",
},
],
});
});
});
describe("raster", () => {
it("add", () => {
let id = when.modal.fillLayers({
type: "raster",
layer: "raster",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "raster",
source: "raster",
},
],
});
});
});
describe("circle", () => {
it("add", () => {
let id = when.modal.fillLayers({
type: "circle",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "circle",
source: "example",
},
],
});
});
});
describe("fill extrusion", () => {
it("add", () => {
let id = when.modal.fillLayers({
type: "fill-extrusion",
layer: "example",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: id,
type: "fill-extrusion",
source: "example",
},
],
});
});
});
describe("groups", () => {
it("simple", () => {
when.setStyle("geojson");
when.modal.open();
when.modal.fillLayers({
id: "foo",
type: "background",
});
when.modal.open();
when.modal.fillLayers({
id: "foo_bar",
type: "background",
});
when.modal.open();
when.modal.fillLayers({
id: "foo_bar_baz",
type: "background",
});
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
then(get.elementByTestId("layer-list-item:foo_bar")).shouldNotBeVisible();
then(
get.elementByTestId("layer-list-item:foo_bar_baz")
).shouldNotBeVisible();
when.click("layer-list-group:foo-0");
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
then(get.elementByTestId("layer-list-item:foo_bar")).shouldBeVisible();
then(
get.elementByTestId("layer-list-item:foo_bar_baz")
).shouldBeVisible();
});
});
});
+5 -40
View File
@@ -1,11 +1,11 @@
import { MaputnikDriver } from "./maputnik-driver"; import { MaputnikDriver } from "./maputnik-driver";
describe("map", () => { describe("map", () => {
const { beforeAndAfter, get, when, then } = new MaputnikDriver(); let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter(); beforeAndAfter();
describe("zoom level", () => { describe("zoom level", () => {
it("via url", () => { it("via url", () => {
const zoomLevel = 12.37; let zoomLevel = 12.37;
when.setStyle("geojson", zoomLevel); when.setStyle("geojson", zoomLevel);
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible(); then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText( then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
@@ -14,7 +14,7 @@ describe("map", () => {
}); });
it("via map controls", () => { it("via map controls", () => {
const zoomLevel = 12.37; let zoomLevel = 12.37;
when.setStyle("geojson", zoomLevel); when.setStyle("geojson", zoomLevel);
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible(); then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
when.clickZoomIn(); when.clickZoomIn();
@@ -22,46 +22,11 @@ describe("map", () => {
"Zoom: " + (zoomLevel + 1) "Zoom: " + (zoomLevel + 1)
); );
}); });
it("via style file definition", () => {
when.setStyle("zoom_7_center_0_51");
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
"Zoom: " + (7)
);
then(get.locationHash().should("contain", "#7/51/0"));
// opening another stylefile does not update the map view again
// as discussed in https://github.com/maplibre/maputnik/issues/1546
when.openASecondStyleWithDifferentZoomAndCenter();
then(get.locationHash().should("contain", "#7/51/0"));
});
}); });
describe("search", () => { describe("search", () => {
it("should exist", () => { it('should exist', () => {
then(get.searchControl()).shouldBeVisible(); then(get.searchControl()).shouldBeVisible();
}); });
}); });
describe("popup", () => {
beforeEach(() => {
when.setStyle("rectangles");
then(get.locationHash().should("exist"));
});
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();
});
});
}); });
-37
View File
@@ -1,6 +1,4 @@
/// <reference types="cypress-real-events" />
import { CypressHelper } from "@shellygo/cypress-test-utils"; import { CypressHelper } from "@shellygo/cypress-test-utils";
import "cypress-real-events/support";
export default class MaputnikCypressHelper { export default class MaputnikCypressHelper {
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" }); private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
@@ -10,45 +8,10 @@ export default class MaputnikCypressHelper {
}; };
public get = { public get = {
locationHash: (): Cypress.Chainable<string> => cy.location("hash"),
...this.helper.get, ...this.helper.get,
}; };
public when = { 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();
}
});
},
dropFileByFixture: (fixture: string, dropzoneTestId: string) => {
this.helper.get.elementByTestId(dropzoneTestId).selectFile("cypress/fixtures/" + fixture, {
action: "drag-drop",
force: true,
});
},
...this.helper.when, ...this.helper.when,
}; };
+27 -117
View File
@@ -1,5 +1,3 @@
/// <reference types="cypress-plugin-tab" />
import { CypressHelper } from "@shellygo/cypress-test-utils"; import { CypressHelper } from "@shellygo/cypress-test-utils";
import { Assertable, then } from "@shellygo/cypress-test-utils/assertable"; import { Assertable, then } from "@shellygo/cypress-test-utils/assertable";
import MaputnikCypressHelper from "./maputnik-cypress-helper"; import MaputnikCypressHelper from "./maputnik-cypress-helper";
@@ -8,17 +6,15 @@ const baseUrl = "http://localhost:8888/";
const styleFromWindow = (win: Window) => { const styleFromWindow = (win: Window) => {
const styleId = win.localStorage.getItem("maputnik:latest_style"); const styleId = win.localStorage.getItem("maputnik:latest_style");
const styleItemKey = `maputnik:style:${styleId}`; const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
const styleItem = win.localStorage.getItem(styleItemKey); const obj = JSON.parse(styleItem || "");
if (!styleItem) throw new Error("Could not get styleItem from localStorage");
const obj = JSON.parse(styleItem);
return obj; return obj;
}; };
export class MaputnikAssertable<T> extends Assertable<T> { export class MaputnikAssertable<T> extends Assertable<T> {
shouldEqualToStoredStyle = () => shouldEqualToStoredStyle = () =>
then( then(
new CypressHelper().get.window().then((win: Window) => { new CypressHelper().get.window().then((win) => {
const style = styleFromWindow(win); const style = styleFromWindow(win);
then(this.chainable).shouldDeepNestedInclude(style); then(this.chainable).shouldDeepNestedInclude(style);
}) })
@@ -78,34 +74,6 @@ export class MaputnikDriver {
fixture: "geojson-raster-style.json", 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: baseUrl + "example-style-with-zoom-7-and-center-0-51.json",
response: {
fixture: "example-style-with-zoom-7-and-center-0-51.json",
},
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: baseUrl + "example-style-with-zoom-5-and-center-50-50.json",
response: {
fixture: "example-style-with-zoom-5-and-center-50-50.json",
},
});
this.helper.given.interceptAndMockResponse({ this.helper.given.interceptAndMockResponse({
method: "GET", method: "GET",
url: "*example.local/*", url: "*example.local/*",
@@ -116,81 +84,52 @@ export class MaputnikDriver {
url: "*example.com/*", url: "*example.com/*",
response: [], response: [],
}); });
this.helper.given.interceptAndMockResponse({
method: "GET",
url: "https://www.glyph-server.com/*",
response: ["Font 1", "Font 2", "Font 3"],
});
}, },
}; };
public when = { public when = {
...this.helper.when, ...this.helper.when,
modal: this.modalDriver.when, modal: this.modalDriver.when,
doWithin: (selector: string, fn: () => void) => { within: (selector: string, fn: () => void) => {
this.helper.when.doWithin(fn, selector); this.helper.when.within(fn, selector);
}, },
tab: () => this.helper.get.element("body").tab(), tab: () => this.helper.get.element("body").tab(),
waitForExampleFileResponse: () => { waitForExampleFileResponse: () => {
this.helper.when.waitForResponse("example-style.json"); this.helper.when.waitForResponse("example-style.json");
}, },
openASecondStyleWithDifferentZoomAndCenter: () => {
cy.contains("button", "Open").click();
cy.get('[data-wd-key="modal:open.url.input"]')
.should("be.enabled")
.clear()
.type("http://localhost:8888/example-style-with-zoom-5-and-center-50-50.json{enter}");
},
chooseExampleFile: () => { chooseExampleFile: () => {
this.helper.given.fixture("example-style.json", "example-style.json"); this.helper.get
this.helper.when.openFileByFixture("example-style.json", "modal:open.dropzone", "modal:open.file.input"); .bySelector("type", "file")
this.helper.when.wait(200); .selectFile("cypress/fixtures/example-style.json", { force: true });
},
dropExampleFile: () => {
this.helper.given.fixture("example-style.json", "example-style.json");
this.helper.when.dropFileByFixture("example-style.json", "modal:open.dropzone");
this.helper.when.wait(200);
}, },
setStyle: ( setStyle: (
styleProperties: "geojson" | "raster" | "both" | "layer" | "rectangles" | "font" | "zoom_7_center_0_51" | "", styleProperties: "geojson" | "raster" | "both" | "layer" | "",
zoom?: number zoom?: number
) => { ) => {
const url = new URL(baseUrl); let url = "?debug";
switch (styleProperties) { switch (styleProperties) {
case "geojson": case "geojson":
url.searchParams.set("style", baseUrl + "geojson-style.json"); url += `&style=${baseUrl}geojson-style.json`;
break; break;
case "raster": case "raster":
url.searchParams.set("style", baseUrl + "raster-style.json"); url += `&style=${baseUrl}raster-style.json`;
break; break;
case "both": case "both":
url.searchParams.set("style", baseUrl + "geojson-raster-style.json"); url += `&style=${baseUrl}geojson-raster-style.json`;
break; break;
case "layer": case "layer":
url.searchParams.set("style", baseUrl + "example-layer-style.json"); url += `&style=${baseUrl}/example-layer-style.json`;
break; 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;
case "zoom_7_center_0_51":
url.searchParams.set("style", baseUrl + "example-style-with-zoom-7-and-center-0-51.json");
break;
} }
if (zoom) { if (zoom) {
url.hash = `${zoom}/41.3805/2.1635`; url += `#${zoom}/41.3805/2.1635`;
} }
this.helper.when.visit(url.toString()); this.helper.when.visit(baseUrl + url);
if (styleProperties) { if (styleProperties) {
this.helper.when.acceptConfirm(); this.helper.when.acceptConfirm();
} }
// when methods should not include assertions // when methods should not include assertions
const toolbarLink = this.helper.get.elementByTestId("toolbar:link"); this.helper.get.elementByTestId("toolbar:link").should("be.visible");
toolbarLink.scrollIntoView();
toolbarLink.should("be.visible");
}, },
typeKeys: (keys: string) => this.helper.get.element("body").type(keys), typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
@@ -200,7 +139,7 @@ export class MaputnikDriver {
}, },
selectWithin: (selector: string, value: string) => { selectWithin: (selector: string, value: string) => {
this.when.doWithin(selector, () => { this.when.within(selector, () => {
this.helper.get.element("select").select(value); this.helper.get.element("select").select(value);
}); });
}, },
@@ -219,35 +158,6 @@ export class MaputnikDriver {
.clear() .clear()
.type(text, { parseSpecialCharSequences: false }); .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 = { public get = {
@@ -267,6 +177,6 @@ export class MaputnikDriver {
skipTargetLayerEditor: () => skipTargetLayerEditor: () =>
this.helper.get.elementByTestId("skip-target-layer-editor"), this.helper.get.elementByTestId("skip-target-layer-editor"),
canvas: () => this.helper.get.element("canvas"), canvas: () => this.helper.get.element("canvas"),
searchControl: () => this.helper.get.element(".maplibregl-ctrl-geocoder") searchControl: () => this.helper.get.element('.maplibregl-ctrl-geocoder')
}; };
} }
+5 -5
View File
@@ -7,9 +7,9 @@ export default class ModalDriver {
public when = { public when = {
fillLayers: (opts: { type: string; layer?: string; id?: string }) => { fillLayers: (opts: { type: string; layer?: string; id?: string }) => {
// Having logic in test code is an anti pattern. // Having logic in test code is an anti pattern.
// This should be split to multiple single responsibility functions // This should be splitted to multiple single responsibility functions
const type = opts.type; let type = opts.type;
const layer = opts.layer; let layer = opts.layer;
let id; let id;
if (opts.id) { if (opts.id) {
id = opts.id; id = opts.id;
@@ -20,8 +20,8 @@ export default class ModalDriver {
this.helper.when.type("add-layer.layer-id.input", id); this.helper.when.type("add-layer.layer-id.input", id);
if (layer) { if (layer) {
this.helper.when.doWithin(() => { this.helper.when.within(() => {
this.helper.get.element("input").clear().type(layer!); this.helper.get.element("input").type(layer!);
}, "add-layer.layer-source-block"); }, "add-layer.layer-source-block");
} }
this.helper.when.click("add-layer"); this.helper.when.click("add-layer");
+12 -288
View File
@@ -1,8 +1,7 @@
import { MaputnikDriver } from "./maputnik-driver"; import { MaputnikDriver } from "./maputnik-driver";
import tokens from "../../src/config/tokens.json" with {type: "json"};
describe("modals", () => { describe("modals", () => {
const { beforeAndAfter, when, get, given, then } = new MaputnikDriver(); let { beforeAndAfter, when, get, then } = new MaputnikDriver();
beforeAndAfter(); beforeAndAfter();
beforeEach(() => { beforeEach(() => {
@@ -18,19 +17,15 @@ describe("modals", () => {
then(get.elementByTestId("modal:open")).shouldNotExist(); then(get.elementByTestId("modal:open")).shouldNotExist();
}); });
it("upload", () => { it.skip("upload", () => {
// HM: I was not able to make the following choose file actually to select a file and close the modal...
when.chooseExampleFile(); when.chooseExampleFile();
then(get.fixture("example-style.json")).shouldEqualToStoredStyle(); then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
});
it("upload via drag and drop", () => {
when.dropExampleFile();
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
}); });
describe("when click open url", () => { describe("when click open url", () => {
beforeEach(() => { beforeEach(() => {
const styleFileUrl = get.exampleFileUrl(); let styleFileUrl = get.exampleFileUrl();
when.setValue("modal:open.url.input", styleFileUrl); when.setValue("modal:open.url.input", styleFileUrl);
when.click("modal:open.url.button"); when.click("modal:open.url.button");
@@ -66,60 +61,9 @@ describe("modals", () => {
}); });
describe("sources", () => { describe("sources", () => {
beforeEach(() => {
when.setStyle("layer");
when.click("nav:sources");
});
it("active sources"); it("active sources");
it("public source"); it("public source");
it("add new 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", () => { describe("inspect", () => {
@@ -175,24 +119,14 @@ describe("modals", () => {
}); });
it("sprite url", () => { it("sprite url", () => {
when.setTextInJsonEditor("\"http://example.com\""); when.setValue("modal:settings.sprite", "http://example.com");
when.click("modal:settings.name"); when.click("modal:settings.name");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sprite: "http://example.com", 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", () => { it("glyphs url", () => {
const glyphsUrl = "http://example.com/{fontstack}/{range}.pbf"; let glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
when.setValue("modal:settings.glyphs", glyphsUrl); when.setValue("modal:settings.glyphs", glyphsUrl);
when.click("modal:settings.name"); when.click("modal:settings.name");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
@@ -201,78 +135,31 @@ describe("modals", () => {
}); });
it("maptiler access token", () => { it("maptiler access token", () => {
const apiKey = "testing123"; let apiKey = "testing123";
when.setValue( when.setValue(
"modal:settings.maputnik:openmaptiles_access_token", "modal:settings.maputnik:openmaptiles_access_token",
apiKey apiKey
); );
when.click("modal:settings.name"); when.click("modal:settings.name");
then( then(
get.styleFromLocalStorage().then((style) => style.metadata) get.styleFromLocalStorage().pipe((style) => style.metadata)
).shouldInclude({ ).shouldInclude({
"maputnik:openmaptiles_access_token": apiKey, "maputnik:openmaptiles_access_token": apiKey,
}); });
}); });
it("thunderforest access token", () => { it("thunderforest access token", () => {
const apiKey = "testing123"; let apiKey = "testing123";
when.setValue( when.setValue(
"modal:settings.maputnik:thunderforest_access_token", "modal:settings.maputnik:thunderforest_access_token",
apiKey apiKey
); );
when.click("modal:settings.name"); when.click("modal:settings.name");
then( then(
get.styleFromLocalStorage().then((style) => style.metadata) get.styleFromLocalStorage().pipe((style) => style.metadata)
).shouldInclude({ "maputnik:thunderforest_access_token": apiKey }); ).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", () => { it("style renderer", () => {
cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
when.select("modal:settings.maputnik:renderer", "ol"); when.select("modal:settings.maputnik:renderer", "ol");
@@ -285,172 +172,9 @@ describe("modals", () => {
metadata: { "maputnik:renderer": "ol" }, metadata: { "maputnik:renderer": "ol" },
}); });
}); });
it("include 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", () => { describe("sources", () => {
it("toggle"); 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();
});
});
}); });
@@ -15,3 +15,4 @@
} }
] ]
} }
@@ -1,38 +0,0 @@
{
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mlgljs"
},
"sources": {
"example": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features":[{
"type": "Feature",
"properties": {
"name": "Dinagat Islands"
},
"geometry":{
"type": "Point",
"coordinates": [125.6, 10.1]
}
}]
}
}
},
"glyphs": "https://www.glyph-server.com/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": [
{
"id": "label",
"type": "symbol",
"source": "example",
"layout": {
"text-font": ["Font"]
}
}
]
}
@@ -1,19 +0,0 @@
{
"id": "test-style",
"center": [50,50],
"zoom": 5,
"version": 8,
"name": "Test Style",
"sources": {
"rectangles": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": []
}
}
},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}
@@ -1,17 +0,0 @@
{
"id": "test-style",
"center": [0,51],
"zoom": 7,
"version": 8,
"name": "Test Style",
"sources": {
"rectangles": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": []
}
}
},
"layers": []
}
+10 -112
View File
@@ -1,114 +1,12 @@
{ {
"id": "test-style", "id": "test-style",
"version": 8, "version": 8,
"name": "Test Style", "name": "Test Style",
"metadata": { "metadata": {
"maputnik:renderer": "mlgljs", "maputnik:renderer": "mlgljs"
"data": [ },
0, "sources": {},
1, "glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
2, "sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
3, "layers": []
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 -1
View File
@@ -31,4 +31,4 @@
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf", "glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf", "sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": [] "layers": []
} }
+1 -1
View File
@@ -26,4 +26,4 @@
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf", "glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf", "sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": [] "layers": []
} }
+1 -1
View File
@@ -15,4 +15,4 @@
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf", "glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf", "sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": [] "layers": []
} }
-92
View File
@@ -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 -1
View File
@@ -34,4 +34,4 @@
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element> // visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// } // }
// } // }
// } // }
-12
View File
@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>
-37
View File
@@ -1,37 +0,0 @@
// ***********************************************************
// This example support/component.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "./commands";
import { mount } from "cypress/react";
// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a <reference path="./component" /> at the top of your spec.
declare global {
/* eslint-disable @typescript-eslint/no-namespace */
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}
Cypress.Commands.add("mount", mount);
// Example use:
// cy.mount(<MyComponent />)
-31
View File
@@ -1,31 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
editor
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
# Binary version of pubilic/editor
rice-box.go
# Built binary
maputnik
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2016 Maputnik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-47
View File
@@ -1,47 +0,0 @@
SOURCEDIR=.
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
BINARY=maputnik
VERSION := $(shell node -p "require('../package.json').version")
GOBIN := $(or $(shell if [ -d /go/bin ]; then echo "/go/bin"; fi),$(HOME)/go/bin)
all: $(BINARY)
$(BINARY): $(GOBIN)/gox $(GOBIN)/go-winres $(SOURCES) version.go rice-box.go winres/winres.json
$(GOBIN)/go-winres make --product-version=$(VERSION)
$(GOBIN)/gox -osarch "windows/amd64 linux/amd64 darwin/amd64" -output "bin/{{.OS}}/${BINARY}"
bin/linux/$(BINARY): $(GOBIN)/gox $(GOBIN)/go-winres $(SOURCES) version.go rice-box.go winres/winres.json
$(GOBIN)/go-winres make --product-version=$(VERSION)
$(GOBIN)/gox -osarch "linux/amd64" -output "bin/{{.OS}}/${BINARY}"
winres/winres.json: winres/winres_template.json
sed 's/{{.Version}}/$(VERSION)/g' winres/winres_template.json > $@
$(GOBIN)/go-winres:
go install github.com/tc-hib/go-winres@latest
# Copy the current release into ./editor/maputnik so it can be
# embedded in the binary
editor/pull_release:
mkdir -p editor
cp -r ../dist/* editor
$(GOBIN)/gox:
go install github.com/mitchellh/gox@v1.0.1
$(GOBIN)/rice:
go install github.com/GeertJohan/go.rice/rice@v1.0.3
# Embed the current version number in the executable by writing version.go
.PHONY: version.go
version.go:
@printf "// DO NOT EDIT: Autogenerated by Makefile\n" > version.go
@printf "package main\n" >> version.go
@printf "const Version = \"$(VERSION)\"\n" >> version.go
rice-box.go: $(GOBIN)/rice editor/pull_release
$(GOBIN)/rice embed-go
.PHONY: clean
clean:
rm -rf editor && rm -f rice-box.go && rm -rf bin
-73
View File
@@ -1,73 +0,0 @@
# Maputnik Desktop [![GitHub CI status](https://github.com/maplibre/maputnik/workflows/ci/badge.svg)][github-action-ci]
---
A Golang based cross platform executable for integrating Maputnik locally.
This binary packages up the JavaScript and CSS bundle produced by maputnik
and embeds it in the program for easy distribution. It also allows
exposing a local style file and work on it both in Maputnik and with your favorite
editor.
Report issues on [maplibre/maputnik](https://github.com/maplibre/maputnik).
## Install
You can download a zip file containing desktop binaries for Linux, OSX and Windows from [the latest releases of **maplibre/maputnik**](https://github.com/maplibre/maputnik/releases/latest).
### Usage
Simply start up a web server and access the Maputnik editor GUI at `localhost:8000`.
```bash
maputnik
```
Expose a local style file to Maputnik allowing the web based editor
to save to the local filesystem.
```bash
maputnik --file basic-v9.json
```
Watch the local style for changes and inform the editor via web socket.
This makes it possible to edit the style with a local text editor and still
use Maputnik.
```bash
maputnik --watch --file basic-v9.json
```
Choose a local port to listen on, instead of using the default port 8000.
```bash
maputnik --port 8001
```
Specify a path to a directory which, if it exists, will be served under http://localhost:8000/static/ .
Could be used to serve sprites and glyphs.
```bash
maputnik --static ./localFolder
```
### API
`maputnik` exposes the configured styles via a HTTP API.
| Method | Description |
| ------------------------ | ------------------------------------------------------ |
| `GET /styles` | List the ID of all configured style files |
| `GET /styles/{filename}` | Get contents of a single style file |
| `PUT /styles/{filename}` | Update contents of a style file |
| `WEBSOCKET /ws` | Listen to change events for the configured style files |
### Build
From the root of the [maplibre/maputnik](https://github.com/maplibre/maputnik) project, install the deps and run the desktop-build command.
```
npm install
npm run build-desktop
```
You should now find the `maputnik` binary in your `desktop/bin` directory.
-81
View File
@@ -1,81 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/gorilla/mux"
)
func StyleFileAccessor(filename string) styleFileAccessor {
return styleFileAccessor{filename, styleId(filename)}
}
func styleId(filename string) string {
raw, err := ioutil.ReadFile(filename)
if err != nil {
log.Panicln(err)
}
var spec styleSpec
err = json.Unmarshal(raw, &spec)
if err != nil {
log.Panicln(err)
}
if spec.Id == "" {
fmt.Println("No id in style")
}
return spec.Id
}
type styleSpec struct {
Id string `json:"id"`
}
// Allows access to a single style file
type styleFileAccessor struct {
filename string
id string
}
func (fa styleFileAccessor) ListFiles(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.Encode([]string{fa.id})
}
func (fa styleFileAccessor) ReadFile(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
_ = vars["styleId"]
//TODO: Choose right file
// right now we just return the single file we know of
w.Header().Set("Content-Type", "application/json")
raw, err := ioutil.ReadFile(fa.filename)
if err != nil {
log.Panicln(err)
}
w.Write(raw)
}
func (fa styleFileAccessor) SaveFile(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
_ = vars["styleId"]
//TODO: Save to right file
w.Header().Set("Content-Type", "application/json")
body, _ := ioutil.ReadAll(r.Body)
var out bytes.Buffer
json.Indent(&out, body, "", " ")
if err := ioutil.WriteFile(fa.filename, out.Bytes(), 0666); err != nil {
log.Fatalf("Can not copy from request to file: %s", err.Error())
}
}
-69
View File
@@ -1,69 +0,0 @@
package filewatch
import (
"io/ioutil"
"log"
"net/http"
"github.com/fsnotify/fsnotify"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
func writer(ws *websocket.Conn, filename string) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
done := make(chan bool)
go func() {
for {
select {
case event := <-watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("Modified file:", event.Name)
var p []byte
var err error
p, err = ioutil.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
if p != nil {
if err := ws.WriteMessage(websocket.TextMessage, p); err != nil {
return
}
}
}
case err := <-watcher.Errors:
log.Println("Watch error:", err)
}
}
}()
if err = watcher.Add(filename); err != nil {
log.Fatal(err)
}
<-done
}
func ServeWebsocketFileWatcher(filename string, w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
if _, ok := err.(websocket.HandshakeError); !ok {
log.Println(err)
}
return
}
writer(ws, filename)
defer ws.Close()
}
-27
View File
@@ -1,27 +0,0 @@
module maputnik/desktop
go 1.19
require (
github.com/GeertJohan/go.rice v1.0.3
github.com/fsnotify/fsnotify v1.6.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0
github.com/maputnik/desktop v1.0.7
github.com/urfave/cli v1.22.12
)
require (
github.com/GeertJohan/go.incremental v1.0.0 // indirect
github.com/akavel/rsrc v0.8.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/daaku/go.zipexe v1.0.2 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/jessevdk/go-flags v1.4.0 // indirect
github.com/nkovacs/streamquote v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.0.1 // indirect
golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect
)
-54
View File
@@ -1,54 +0,0 @@
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI=
github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4=
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/daaku/go.zipexe v1.0.2 h1:Zg55YLYTr7M9wjKn8SY/WcpuuEi+kR2u4E8RhvpyXmk=
github.com/daaku/go.zipexe v1.0.2/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/maputnik/desktop v1.0.7 h1:rdFg7emIJOT3YsZpwqSChmWtMOvu+T4h6WwVQAZP9n4=
github.com/maputnik/desktop v1.0.7/go.mod h1:wmDjHUztx9jOBz0I22589yWguAGdV/sEM57YANpN8oQ=
github.com/nkovacs/streamquote v1.0.0 h1:PmVIV08Zlx2lZK5fFZlMZ04eHcDTIFJCv/5/0twVUow=
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-79
View File
@@ -1,79 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/GeertJohan/go.rice"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/maputnik/desktop/filewatch"
"github.com/urfave/cli"
)
func main() {
app := cli.NewApp()
app.Name = "maputnik"
app.Usage = "Server for integrating Maputnik locally"
app.Version = Version
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "file, f",
Usage: "Allow access to JSON style from web client",
},
&cli.BoolFlag{
Name: "watch",
Usage: "Notify web client about JSON style file changes",
},
&cli.IntFlag{
Name: "port",
Value: 8000,
Usage: "TCP port to listen on",
},
&cli.StringFlag{
Name: "static",
Usage: "Serve directory under /static/",
},
}
app.Action = func(c *cli.Context) error {
gui := http.FileServer(rice.MustFindBox("editor").HTTPBox())
router := mux.NewRouter().StrictSlash(true)
filename := c.String("file")
if filename != "" {
fmt.Printf("%s is accessible via Maputnik\n", filename)
// Allow access to reading and writing file on the local system
path, _ := filepath.Abs(filename)
accessor := StyleFileAccessor(path)
router.Path("/styles").Methods("GET").HandlerFunc(accessor.ListFiles)
router.Path("/styles/{styleId}").Methods("GET").HandlerFunc(accessor.ReadFile)
router.Path("/styles/{styleId}").Methods("PUT").HandlerFunc(accessor.SaveFile)
// Register websocket to notify we clients about file changes
if c.Bool("watch") {
router.Path("/ws").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
filewatch.ServeWebsocketFileWatcher(filename, w, r)
})
}
}
staticDir := c.String("static")
if staticDir != "" {
h := http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))
router.PathPrefix("/static/").Handler(h)
}
router.PathPrefix("/").Handler(http.StripPrefix("/", gui))
loggedRouter := handlers.LoggingHandler(os.Stdout, router)
corsRouter := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}), handlers.AllowedMethods([]string{"GET", "PUT"}), handlers.AllowedOrigins([]string{"*"}), handlers.AllowCredentials())(loggedRouter)
fmt.Printf("Exposing Maputnik on http://localhost:%d\n", c.Int("port"))
return http.ListenAndServe(fmt.Sprintf(":%d", c.Int("port")), corsRouter)
}
app.Run(os.Args)
}
-60
View File
@@ -1,60 +0,0 @@
{
"RT_GROUP_ICON": {
"APP": {
"0000": [
"../../src/img/maputnik.png"
]
}
},
"RT_MANIFEST": {
"#1": {
"0409": {
"identity": {
"name": "Maputnik",
"version": "{{.Version}}"
},
"description": "A MapLibre GL visual style editor",
"minimum-os": "win7",
"execution-level": "as invoker",
"ui-access": false,
"auto-elevate": false,
"dpi-awareness": "system",
"disable-theming": false,
"disable-window-filtering": false,
"high-resolution-scrolling-aware": false,
"ultra-high-resolution-scrolling-aware": false,
"long-path-aware": false,
"printer-driver-isolation": false,
"gdi-scaling": false,
"segment-heap": false,
"use-common-controls-v6": false
}
}
},
"RT_VERSION": {
"#1": {
"0000": {
"fixed": {
"file_version": "{{.Version}}",
"product_version": "{{.Version}}"
},
"info": {
"0409": {
"Comments": "https://github.com/maplibre/maputnik",
"CompanyName": "Maputnik",
"FileDescription": "A MapLibre GL visual style editor",
"FileVersion": "{{.Version}}",
"InternalName": "Maputnik",
"LegalCopyright": "MIT License",
"LegalTrademarks": "",
"OriginalFilename": "Maputnik.exe",
"PrivateBuild": "",
"ProductName": "Maputnik",
"ProductVersion": "{{.Version}}",
"SpecialBuild": ""
}
}
}
}
}
}
-70
View File
@@ -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
}
}
);
-10
View File
@@ -1,10 +0,0 @@
import { defineConfig } from "i18next-cli";
export default defineConfig({
locales: ["de", "fr", "he", "it", "ja", "ko", "tr", "zh"],
extract: {
input: ["src/**/*.{js,jsx,ts,tsx}"],
output: "src/locales/{{language}}/{{namespace}}.json",
defaultValue: "__STRING_NOT_TRANSLATED__"
}
});
+8376 -9989
View File
File diff suppressed because it is too large Load Diff
+70 -85
View File
@@ -1,22 +1,16 @@
{ {
"name": "maputnik", "name": "maputnik",
"version": "3.1.0", "version": "2.0.0",
"description": "A MapLibre GL visual style editor", "description": "A MapLibre GL visual style editor",
"type": "module", "type": "module",
"main": "''", "main": "''",
"scripts": { "scripts": {
"start": "vite", "start": "vite",
"build": "tsc && vite build --mode=production", "build": "tsc && vite build --base=/maputnik/",
"build-desktop": "tsc && vite build --mode=desktop && cd desktop && make", "lint": "eslint ./src ./cypress --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
"build-linux": "tsc && vite build --mode=desktop && cd desktop && make bin/linux/maputnik",
"i18n:extract": "npx i18next-cli extract",
"lint": "eslint",
"test": "cypress run", "test": "cypress run",
"test-unit": "vitest",
"test-unit-ci": "vitest run --coverage --reporter=json",
"cy:open": "cypress open", "cy:open": "cypress open",
"lint-css": "stylelint \"src/styles/*.scss\"", "lint-css": "stylelint \"src/styles/*.scss\""
"sort-styles": "jq 'sort_by(.id)' src/config/styles.json > tmp.json && mv tmp.json src/config/styles.json"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -26,59 +20,54 @@
"license": "MIT", "license": "MIT",
"homepage": "https://github.com/maplibre/maputnik#readme", "homepage": "https://github.com/maplibre/maputnik#readme",
"dependencies": { "dependencies": {
"@codemirror/lang-json": "^6.0.2", "@mapbox/mapbox-gl-rtl-text": "^0.2.3",
"@codemirror/lint": "^6.9.7", "@maplibre/maplibre-gl-geocoder": "^1.5.0",
"@codemirror/state": "^6.7.0", "@maplibre/maplibre-gl-style-spec": "^20.1.0",
"@codemirror/theme-one-dark": "^6.1.3", "@mdi/js": "^6.6.96",
"@codemirror/view": "^6.43.4", "@mdi/react": "^1.5.0",
"@dnd-kit/core": "^6.3.1", "@typescript-eslint/eslint-plugin": "^6.16.0",
"@dnd-kit/sortable": "^10.0.0", "@typescript-eslint/parser": "^6.16.0",
"@dnd-kit/utilities": "^3.2.2",
"@mapbox/mapbox-gl-rtl-text": "^0.4.0",
"@maplibre/maplibre-gl-geocoder": "^1.9.4",
"@maplibre/maplibre-gl-inspect": "^1.8.2",
"@maplibre/maplibre-gl-style-spec": "^25.0.1",
"array-move": "^4.0.0", "array-move": "^4.0.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"classnames": "^2.5.1", "classnames": "^2.3.1",
"codemirror": "^6.0.2", "codemirror": "^5.65.2",
"color": "^5.0.3", "color": "^4.2.3",
"detect-browser": "^5.3.0", "detect-browser": "^5.3.0",
"downshift": "^9.4.0",
"events": "^3.3.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"i18next": "^26.3.4", "json-stringify-pretty-compact": "^3.0.0",
"i18next-browser-languagedetector": "^8.2.1",
"i18next-resources-to-backend": "^1.2.1",
"json-stringify-pretty-compact": "^4.0.0",
"json-to-ast": "^2.1.0", "json-to-ast": "^2.1.0",
"lodash": "^4.18.1", "jsonlint": "github:josdejong/jsonlint#85a19d7",
"lodash": "^4.17.21",
"lodash.capitalize": "^4.2.1", "lodash.capitalize": "^4.2.1",
"lodash.clamp": "^4.0.3", "lodash.clamp": "^4.0.3",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"maplibre-gl": "^5.24.0", "mapbox-gl-inspect": "^1.3.1",
"maplibre-gl": "^4.0.0",
"maputnik-design": "github:maputnik/design#172b06c", "maputnik-design": "github:maputnik/design#172b06c",
"ol": "^10.9.0", "ol": "^6.14.1",
"ol-mapbox-style": "^13.4.1", "ol-mapbox-style": "^7.1.1",
"pmtiles": "^4.4.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^19.2.4", "react": "^16.0.0",
"react-accessible-accordion": "^5.0.1", "react-accessible-accordion": "^4.0.0",
"react-aria-menubutton": "^8.0.0", "react-aria-menubutton": "^7.0.3",
"react-aria-modal": "^5.0.2", "react-aria-modal": "^4.0.1",
"react-autobind": "^1.0.6",
"react-autocomplete": "^1.8.1",
"react-collapse": "^5.1.1", "react-collapse": "^5.1.1",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-dom": "^19.2.4", "react-dom": "^16.0.0",
"react-i18next": "^17.0.7", "react-file-reader-input": "^2.0.0",
"react-icons": "^5.6.0", "react-icon-base": "^2.1.2",
"react-markdown": "^10.1.0", "react-icons": "^4.3.1",
"react-sortable-hoc": "^2.0.0",
"reconnecting-websocket": "^4.4.0", "reconnecting-websocket": "^4.4.0",
"slugify": "^1.6.9", "sass": "^1.50.0",
"slugify": "^1.6.5",
"string-hash": "^1.1.3", "string-hash": "^1.1.3",
"url": "^0.11.4" "url": "^0.11.0"
}, },
"jshintConfig": { "jshintConfig": {
"esversion": 6 "esversion": 6
@@ -98,17 +87,15 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@cypress/code-coverage": "^4.0.3", "@cypress/code-coverage": "^3.12.15",
"@eslint/js": "^10.0.1",
"@istanbuljs/nyc-config-typescript": "^1.0.2", "@istanbuljs/nyc-config-typescript": "^1.0.2",
"@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-replace": "^5.0.5",
"@shellygo/cypress-test-utils": "^6.0.6", "@shellygo/cypress-test-utils": "^2.0.17",
"@stylistic/eslint-plugin": "^5.10.0", "@types/codemirror": "^5.60.15",
"@types/codemirror": "^5.60.17", "@types/color": "^3.0.6",
"@types/color": "^4.2.1", "@types/cors": "^2.8.17",
"@types/cors": "^2.8.19",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/geojson": "^7946.0.16", "@types/geojson": "^7946.0.13",
"@types/json-to-ast": "^2.1.4", "@types/json-to-ast": "^2.1.4",
"@types/lodash.capitalize": "^4.2.9", "@types/lodash.capitalize": "^4.2.9",
"@types/lodash.clamp": "^4.0.9", "@types/lodash.clamp": "^4.0.9",
@@ -116,38 +103,36 @@
"@types/lodash.get": "^4.4.9", "@types/lodash.get": "^4.4.9",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/lodash.throttle": "^4.1.9", "@types/lodash.throttle": "^4.1.9",
"@types/randomcolor": "^0.5.9", "@types/react": "^16.14.52",
"@types/react": "^19.2.14", "@types/react-aria-menubutton": "^6.2.13",
"@types/react-aria-menubutton": "^6.2.14", "@types/react-aria-modal": "^4.0.9",
"@types/react-aria-modal": "^5.0.0", "@types/react-autocomplete": "^1.8.9",
"@types/react-collapse": "^5.0.4", "@types/react-collapse": "^5.0.4",
"@types/react-color": "^3.0.13", "@types/react-color": "^3.0.10",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^16.9.24",
"@types/react-file-reader-input": "^2.0.4",
"@types/react-icon-base": "^2.1.6",
"@types/string-hash": "^1.1.3", "@types/string-hash": "^1.1.3",
"@types/wicg-file-system-access": "^2023.10.7", "@types/uuid": "^9.0.7",
"@vitejs/plugin-react": "^5.1.4", "@vitejs/plugin-react": "^4.2.0",
"@vitest/coverage-v8": "^4.1.9", "cors": "^2.8.5",
"cors": "^2.8.6", "cypress": "^13.6.1",
"cypress": "^15.18.0", "eslint": "^8.56.0",
"cypress-plugin-tab": "^2.0.0", "eslint-plugin-react": "^7.33.2",
"eslint": "^10.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react-refresh": "^0.4.5",
"eslint-plugin-react-hooks": "^7.1.1", "express": "^4.17.3",
"eslint-plugin-react-refresh": "^0.5.2",
"i18next-cli": "^1.53.2",
"istanbul": "^0.4.5", "istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.2.2", "istanbul-lib-coverage": "^3.2.0",
"postcss": "^8.5.16", "mocha": "^9.2.2",
"react-hot-loader": "^4.13.1", "postcss": "^8.4.12",
"sass": "^1.101.0", "react-hot-loader": "^4.13.0",
"stylelint": "^17.14.0", "stylelint": "^14.6.1",
"stylelint-config-recommended-scss": "^17.0.1", "stylelint-config-recommended-scss": "^6.0.0",
"stylelint-scss": "^7.2.0", "stylelint-scss": "^4.2.0",
"typescript": "^6.0.3", "typescript": "^5.3.3",
"typescript-eslint": "^8.62.1", "uuid": "^8.3.2",
"uuid": "^14.0.1", "vite": "^5.0.12",
"vite": "^7.3.2", "vite-plugin-istanbul": "^5.0.0"
"vite-plugin-istanbul": "^9.0.1",
"vitest": "^4.1.9"
} }
} }
+319 -324
View File
File diff suppressed because it is too large Load Diff
+32 -40
View File
@@ -1,54 +1,46 @@
import React from "react"; import React from 'react'
import ScrollContainer from "./ScrollContainer"; import PropTypes from 'prop-types'
import { type WithTranslation, withTranslation } from "react-i18next"; import ScrollContainer from './ScrollContainer'
import { IconContext } from "react-icons";
type AppLayoutInternalProps = { type AppLayoutProps = {
toolbar: React.ReactElement toolbar: React.ReactElement
layerList: React.ReactElement layerList: React.ReactElement
layerEditor?: React.ReactElement layerEditor?: React.ReactElement
codeEditor?: React.ReactElement
map: React.ReactElement map: React.ReactElement
bottom?: React.ReactElement bottom?: React.ReactElement
modals?: React.ReactNode modals?: React.ReactNode
} & WithTranslation; };
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> { class AppLayout extends React.Component<AppLayoutProps> {
static childContextTypes = {
reactIconBase: PropTypes.object
}
getChildContext() {
return {
reactIconBase: { size: 14 }
}
}
render() { render() {
document.body.dir = this.props.i18n.dir(); return <div className="maputnik-layout">
{this.props.toolbar}
return <IconContext.Provider value={{size: "14px"}}> <div className="maputnik-layout-list">
<div className="maputnik-layout"> {this.props.layerList}
{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> </div>
</IconContext.Provider>; <div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
{this.props.map}
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
</div>
}
{this.props.modals}
</div>
} }
} }
const AppLayout = withTranslation()(AppLayoutInternal); export default AppLayout
export default AppLayout;
+22 -26
View File
@@ -1,33 +1,31 @@
import React from "react"; import React from 'react'
import { formatLayerId } from "../libs/format"; import {formatLayerId} from '../libs/format';
import { type LayerSpecification, type StyleSpecification } from "maplibre-gl"; import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
import { type WithTranslation, withTranslation } from "react-i18next";
import { type MappedError } from "../libs/definitions";
type AppMessagePanelInternalProps = { type AppMessagePanelProps = {
errors?: MappedError[] errors?: unknown[]
infos?: string[] infos?: unknown[]
mapStyle?: StyleSpecification mapStyle?: StyleSpecification
onLayerSelect?(index: number): void; onLayerSelect?(...args: unknown[]): unknown
currentLayer?: LayerSpecification currentLayer?: LayerSpecification
selectedLayerIndex?: number selectedLayerIndex?: number
} & WithTranslation; };
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> { export default class AppMessagePanel extends React.Component<AppMessagePanelProps> {
static defaultProps = { static defaultProps = {
onLayerSelect: () => { }, onLayerSelect: () => {},
}; }
render() { render() {
const { t, selectedLayerIndex } = this.props; const {selectedLayerIndex} = this.props;
const errors = this.props.errors?.map((error, idx) => { const errors = this.props.errors?.map((error: any, idx) => {
let content; let content;
if (error.parsed && error.parsed.type === "layer") { if (error.parsed && error.parsed.type === "layer") {
const { parsed } = error; const {parsed} = error;
const layerId = this.props.mapStyle?.layers[parsed.data.index].id; const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
content = ( content = (
<> <>
{t("Layer")} <span>{formatLayerId(layerId)}</span>: {parsed.data.message} Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
{selectedLayerIndex !== parsed.data.index && {selectedLayerIndex !== parsed.data.index &&
<> <>
&nbsp;&mdash;&nbsp; &nbsp;&mdash;&nbsp;
@@ -35,7 +33,7 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
className="maputnik-message-panel__switch-button" className="maputnik-message-panel__switch-button"
onClick={() => this.props.onLayerSelect!(parsed.data.index)} onClick={() => this.props.onLayerSelect!(parsed.data.index)}
> >
{t("switch to layer")} switch to layer
</button> </button>
</> </>
} }
@@ -45,21 +43,19 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
else { else {
content = error.message; content = error.message;
} }
return <p key={"error-" + idx} className="maputnik-message-panel-error"> return <p key={"error-"+idx} className="maputnik-message-panel-error">
{content} {content}
</p>; </p>
}); })
const infos = this.props.infos?.map((m, i) => { const infos = this.props.infos?.map((m, i) => {
return <p key={"info-" + i}>{m}</p>; return <p key={"info-"+i}>{m}</p>
}); })
return <div className="maputnik-message-panel"> return <div className="maputnik-message-panel">
{errors} {errors}
{infos} {infos}
</div>; </div>
} }
} }
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
export default AppMessagePanel;
+46 -95
View File
@@ -1,30 +1,16 @@
import React from "react"; import React from 'react'
import classnames from "classnames"; import classnames from 'classnames'
import {detect} from "detect-browser"; import {detect} from 'detect-browser';
import { import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage} from 'react-icons/md'
MdOpenInBrowser, import pkgJson from '../../package.json'
MdSettings,
MdLayers,
MdHelpOutline,
MdFindInPage,
MdLanguage,
MdSave,
MdPublic,
MdCode
} from "react-icons/md";
import pkgJson from "../../package.json";
//@ts-ignore //@ts-ignore
import maputnikLogo from "maputnik-design/logos/logo-color.svg?inline"; 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. // This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
const browser = detect(); const browser = detect();
const colorAccessibilityFiltersEnabled = ["chrome", "firefox"].indexOf(browser!.name) > -1; const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser!.name) > -1;
export type ModalTypes = "settings" | "sources" | "open" | "shortcuts" | "export" | "debug" | "globalState" | "codeEditor";
type IconTextProps = { type IconTextProps = {
children?: React.ReactNode children?: React.ReactNode
@@ -33,7 +19,7 @@ type IconTextProps = {
class IconText extends React.Component<IconTextProps> { class IconText extends React.Component<IconTextProps> {
render() { render() {
return <span className="maputnik-icon-text">{this.props.children}</span>; return <span className="maputnik-icon-text">{this.props.children}</span>
} }
} }
@@ -41,19 +27,20 @@ type ToolbarLinkProps = {
className?: string className?: string
children?: React.ReactNode children?: React.ReactNode
href?: string href?: string
onToggleModal?(...args: unknown[]): unknown
}; };
class ToolbarLink extends React.Component<ToolbarLinkProps> { class ToolbarLink extends React.Component<ToolbarLinkProps> {
render() { render() {
return <a return <a
className={classnames("maputnik-toolbar-link", this.props.className)} className={classnames('maputnik-toolbar-link', this.props.className)}
href={this.props.href} href={this.props.href}
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
data-wd-key="toolbar:link" data-wd-key="toolbar:link"
> >
{this.props.children} {this.props.children}
</a>; </a>
} }
} }
@@ -69,7 +56,7 @@ class ToolbarSelect extends React.Component<ToolbarSelectProps> {
data-wd-key={this.props.wdKey} data-wd-key={this.props.wdKey}
> >
{this.props.children} {this.props.children}
</div>; </div>
} }
} }
@@ -87,28 +74,28 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
onClick={this.props.onClick} onClick={this.props.onClick}
> >
{this.props.children} {this.props.children}
</button>; </button>
} }
} }
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia"; export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
type AppToolbarInternalProps = { type AppToolbarProps = {
mapStyle: object mapStyle: object
inspectModeEnabled: boolean inspectModeEnabled: boolean
onStyleChanged: OnStyleChangedCallback onStyleChanged(...args: unknown[]): unknown
// A new style has been uploaded // A new style has been uploaded
onStyleOpen: OnStyleChangedCallback onStyleOpen(...args: unknown[]): unknown
// A dict of source id's and the available source layers // A dict of source id's and the available source layers
sources: object sources: object
children?: React.ReactNode children?: React.ReactNode
onToggleModal(modal: ModalTypes): void onToggleModal(...args: unknown[]): unknown
onSetMapState(mapState: MapState): unknown onSetMapState(mapState: MapState): unknown
mapState?: MapState mapState?: MapState
renderer?: string renderer?: string
} & WithTranslation; };
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> { export default class AppToolbar extends React.Component<AppToolbarProps> {
state = { state = {
isOpen: { isOpen: {
settings: false, settings: false,
@@ -117,16 +104,12 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
add: false, add: false,
export: false, export: false,
} }
}; }
handleSelection(val: MapState) { handleSelection(val: MapState) {
this.props.onSetMapState(val); this.props.onSetMapState(val);
} }
handleLanguageChange(val: string) {
this.props.i18n.changeLanguage(val);
}
onSkip = (target: string) => { onSkip = (target: string) => {
if (target === "map") { if (target === "map") {
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus(); (document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
@@ -135,44 +118,43 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement; const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement;
el.focus(); el.focus();
} }
}; }
render() { render() {
const t = this.props.t;
const views = [ const views = [
{ {
id: "map", id: "map",
group: "general", group: "general",
title: t("Map"), title: "Map",
}, },
{ {
id: "inspect", id: "inspect",
group: "general", group: "general",
title: t("Inspect"), title: "Inspect",
disabled: this.props.renderer === "ol", disabled: this.props.renderer === 'ol',
}, },
{ {
id: "filter-deuteranopia", id: "filter-deuteranopia",
group: "color-accessibility", group: "color-accessibility",
title: t("Deuteranopia filter"), title: "Deuteranopia filter",
disabled: !colorAccessibilityFiltersEnabled, disabled: !colorAccessibilityFiltersEnabled,
}, },
{ {
id: "filter-protanopia", id: "filter-protanopia",
group: "color-accessibility", group: "color-accessibility",
title: t("Protanopia filter"), title: "Protanopia filter",
disabled: !colorAccessibilityFiltersEnabled, disabled: !colorAccessibilityFiltersEnabled,
}, },
{ {
id: "filter-tritanopia", id: "filter-tritanopia",
group: "color-accessibility", group: "color-accessibility",
title: t("Tritanopia filter"), title: "Tritanopia filter",
disabled: !colorAccessibilityFiltersEnabled, disabled: !colorAccessibilityFiltersEnabled,
}, },
{ {
id: "filter-achromatopsia", id: "filter-achromatopsia",
group: "color-accessibility", group: "color-accessibility",
title: t("Achromatopsia filter"), title: "Achromatopsia filter",
disabled: !colorAccessibilityFiltersEnabled, disabled: !colorAccessibilityFiltersEnabled,
}, },
]; ];
@@ -192,21 +174,21 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
className="maputnik-toolbar-skip" className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("layer-list")} onClick={_e => this.onSkip("layer-list")}
> >
{t("Layers list")} Layers list
</button> </button>
<button <button
data-wd-key="root:skip:layer-editor" data-wd-key="root:skip:layer-editor"
className="maputnik-toolbar-skip" className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("layer-editor")} onClick={_e => this.onSkip("layer-editor")}
> >
{t("Layer editor")} Layer editor
</button> </button>
<button <button
data-wd-key="root:skip:map-view" data-wd-key="root:skip:map-view"
className="maputnik-toolbar-skip" className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("map")} onClick={_e => this.onSkip("map")}
> >
{t("Map view")} Map view
</button> </button>
<a <a
className="maputnik-toolbar-logo" className="maputnik-toolbar-logo"
@@ -214,7 +196,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
rel="noreferrer noopener" rel="noreferrer noopener"
href="https://github.com/maplibre/maputnik" href="https://github.com/maplibre/maputnik"
> >
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} /> <img src={maputnikLogo} alt="Maputnik on GitHub" />
<h1> <h1>
<span className="maputnik-toolbar-name">{pkgJson.name}</span> <span className="maputnik-toolbar-name">{pkgJson.name}</span>
<span className="maputnik-toolbar-version">v{pkgJson.version}</span> <span className="maputnik-toolbar-version">v{pkgJson.version}</span>
@@ -222,34 +204,26 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
</a> </a>
</div> </div>
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar"> <div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
<ToolbarAction wdKey="nav:open" onClick={() => this.props.onToggleModal("open")}> <ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
<MdOpenInBrowser /> <MdOpenInBrowser />
<IconText>{t("Open")}</IconText> <IconText>Open</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction wdKey="nav:export" onClick={() => this.props.onToggleModal("export")}> <ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
<MdSave /> <MdFileDownload />
<IconText>{t("Save")}</IconText> <IconText>Export</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction wdKey="nav:code-editor" onClick={() => this.props.onToggleModal("codeEditor")}> <ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
<MdCode />
<IconText>{t("Code Editor")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:sources" onClick={() => this.props.onToggleModal("sources")}>
<MdLayers /> <MdLayers />
<IconText>{t("Data Sources")}</IconText> <IconText>Data Sources</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarAction wdKey="nav:settings" onClick={() => this.props.onToggleModal("settings")}> <ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
<MdSettings /> <MdSettings />
<IconText>{t("Style Settings")}</IconText> <IconText>Style Settings</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:global-state" onClick={() => this.props.onToggleModal("globalState")}>
<MdPublic />
<IconText>{t("Global State")}</IconText>
</ToolbarAction> </ToolbarAction>
<ToolbarSelect wdKey="nav:inspect"> <ToolbarSelect wdKey="nav:inspect">
<MdFindInPage /> <MdFindInPage />
<IconText>{t("View")} <label>View
<select <select
className="maputnik-select" className="maputnik-select"
data-wd-key="maputnik-select" data-wd-key="maputnik-select"
@@ -263,7 +237,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
</option> </option>
); );
})} })}
<optgroup label={t("Color accessibility")}> <optgroup label="Color accessibility">
{views.filter(v => v.group === "color-accessibility").map((item) => { {views.filter(v => v.group === "color-accessibility").map((item) => {
return ( return (
<option key={item.id} value={item.id} disabled={item.disabled}> <option key={item.id} value={item.id} disabled={item.disabled}>
@@ -273,38 +247,15 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
})} })}
</optgroup> </optgroup>
</select> </select>
</IconText> </label>
</ToolbarSelect>
<ToolbarSelect wdKey="nav:language">
<MdLanguage />
<IconText>Language
<select
className="maputnik-select"
data-wd-key="maputnik-lang-select"
onChange={(e) => this.handleLanguageChange(e.target.value)}
value={this.props.i18n.language}
>
{Object.entries(supportedLanguages).map(([code, name]) => {
return (
<option key={code} value={code}>
{name}
</option>
);
})}
</select>
</IconText>
</ToolbarSelect> </ToolbarSelect>
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}> <ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
<MdHelpOutline /> <MdHelpOutline />
<IconText>{t("Help")}</IconText> <IconText>Help</IconText>
</ToolbarLink> </ToolbarLink>
</div> </div>
</div> </div>
</nav>; </nav>
} }
} }
const AppToolbar = withTranslation()(AppToolbarInternal);
export default AppToolbar;
+18 -19
View File
@@ -1,13 +1,14 @@
import React, {type CSSProperties, type PropsWithChildren, type SyntheticEvent} from "react"; import React, {SyntheticEvent} from 'react'
import classnames from "classnames"; import classnames from 'classnames'
import FieldDocLabel from "./FieldDocLabel"; import FieldDocLabel from './FieldDocLabel'
import Doc from "./Doc"; import Doc from './Doc'
export type BlockProps = PropsWithChildren & {
type BlockProps = {
"data-wd-key"?: string "data-wd-key"?: string
label?: string label?: string
action?: React.ReactElement action?: React.ReactElement
style?: CSSProperties style?: object
onChange?(...args: unknown[]): unknown onChange?(...args: unknown[]): unknown
fieldSpec?: object fieldSpec?: object
wideMode?: boolean wideMode?: boolean
@@ -26,13 +27,13 @@ export default class Block extends React.Component<BlockProps, BlockState> {
super(props); super(props);
this.state = { this.state = {
showDoc: false, showDoc: false,
}; }
} }
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) { onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) {
const value = e.target.value; const value = e.target.value
if (this.props.onChange) { if (this.props.onChange) {
return this.props.onChange(value === "" ? undefined : value); return this.props.onChange(value === "" ? undefined : value)
} }
} }
@@ -40,7 +41,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
this.setState({ this.setState({
showDoc: val showDoc: val
}); });
}; }
/** /**
* Some fields for example <InputColor/> bind click events inside the element * Some fields for example <InputColor/> bind click events inside the element
@@ -55,10 +56,8 @@ export default class Block extends React.Component<BlockProps, BlockState> {
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) { if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
event.stopPropagation(); event.stopPropagation();
} }
if (event.nativeEvent.target.nodeName !== "A") { event.preventDefault();
event.preventDefault(); }
}
};
render() { render() {
return <label style={this.props.style} return <label style={this.props.style}
@@ -66,8 +65,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
className={classnames({ className={classnames({
"maputnik-input-block": true, "maputnik-input-block": true,
"maputnik-input-block--wide": this.props.wideMode, "maputnik-input-block--wide": this.props.wideMode,
"maputnik-action-block": this.props.action, "maputnik-action-block": this.props.action
"maputnik-input-block--error": this.props.error
})} })}
onClick={this.onLabelClick} onClick={this.onLabelClick}
> >
@@ -88,17 +86,18 @@ export default class Block extends React.Component<BlockProps, BlockState> {
<div className="maputnik-input-block-action"> <div className="maputnik-input-block-action">
{this.props.action} {this.props.action}
</div> </div>
<div className="maputnik-input-block-content" ref={el => {this._blockEl = el;}}> <div className="maputnik-input-block-content" ref={el => this._blockEl = el}>
{this.props.children} {this.props.children}
</div> </div>
{this.props.fieldSpec && {this.props.fieldSpec &&
<div <div
className="maputnik-doc-inline" className="maputnik-doc-inline"
style={{display: this.state.showDoc ? "" : "none"}} style={{display: this.state.showDoc ? '' : 'none'}}
> >
<Doc fieldSpec={this.props.fieldSpec} /> <Doc fieldSpec={this.props.fieldSpec} />
</div> </div>
} }
</label>; </label>
} }
} }
-29
View File
@@ -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;
+7 -6
View File
@@ -1,6 +1,6 @@
import React from "react"; import React from 'react'
import { Collapse as ReactCollapse } from "react-collapse"; import { Collapse as ReactCollapse } from 'react-collapse'
import {reducedMotionEnabled} from "../libs/accessibility"; import {reducedMotionEnabled} from '../libs/accessibility'
type CollapseProps = { type CollapseProps = {
@@ -12,7 +12,7 @@ type CollapseProps = {
export default class Collapse extends React.Component<CollapseProps> { export default class Collapse extends React.Component<CollapseProps> {
static defaultProps = { static defaultProps = {
isActive: true isActive: true
}; }
render() { render() {
if (reducedMotionEnabled()) { if (reducedMotionEnabled()) {
@@ -20,14 +20,15 @@ export default class Collapse extends React.Component<CollapseProps> {
<div style={{display: this.props.isActive ? "block" : "none"}}> <div style={{display: this.props.isActive ? "block" : "none"}}>
{this.props.children} {this.props.children}
</div> </div>
); )
} }
else { else {
return ( return (
<ReactCollapse isOpened={this.props.isActive}> <ReactCollapse isOpened={this.props.isActive}>
{this.props.children} {this.props.children}
</ReactCollapse> </ReactCollapse>
); )
} }
} }
} }
+5 -4
View File
@@ -1,5 +1,5 @@
import React from "react"; import React from 'react'
import {MdArrowDropDown, MdArrowDropUp} from "react-icons/md"; import {MdArrowDropDown, MdArrowDropUp} from 'react-icons/md'
type CollapserProps = { type CollapserProps = {
isCollapsed: boolean isCollapsed: boolean
@@ -12,7 +12,8 @@ export default class Collapser extends React.Component<CollapserProps> {
width: 20, width: 20,
height: 20, height: 20,
...this.props.style, ...this.props.style,
}; }
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />; return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />
} }
} }
+9 -28
View File
@@ -1,10 +1,10 @@
import React from "react"; import React from 'react'
import Markdown from "react-markdown";
const headers = { const headers = {
js: "JS", js: "JS",
android: "Android", android: "Android",
ios: "iOS" ios: "iOS",
macos: "macOS",
}; };
type DocProps = { type DocProps = {
@@ -15,11 +15,9 @@ type DocProps = {
doc?: string doc?: string
} }
} }
"sdk-support"?: { 'sdk-support'?: {
[key: string]: typeof headers [key: string]: typeof headers
} }
docUrl?: string,
docUrlLinkText?: string
} }
}; };
@@ -27,8 +25,8 @@ export default class Doc extends React.Component<DocProps> {
render () { render () {
const {fieldSpec} = this.props; const {fieldSpec} = this.props;
const {doc, values, docUrl, docUrlLinkText} = fieldSpec; const {doc, values} = fieldSpec;
const sdkSupport = fieldSpec["sdk-support"]; const sdkSupport = fieldSpec['sdk-support'];
const renderValues = ( const renderValues = (
!!values && !!values &&
@@ -37,23 +35,11 @@ export default class Doc extends React.Component<DocProps> {
!Array.isArray(values) !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 ( return (
<> <>
{doc && {doc &&
<div className="SpecDoc"> <div className="SpecDoc">
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'> <div className="SpecDoc__doc" data-wd-key='spec-field-doc'>{doc}</div>
<Markdown components={{
a: ({node: _node, href, children, ...props}) => <a href={href} target="_blank" {...props}>{children}</a>,
}}>{doc}</Markdown>
</div>
{renderValues && {renderValues &&
<ul className="SpecDoc__values"> <ul className="SpecDoc__values">
{Object.entries(values).map(([key, value]) => { {Object.entries(values).map(([key, value]) => {
@@ -86,7 +72,7 @@ export default class Doc extends React.Component<DocProps> {
<td>{key}</td> <td>{key}</td>
{Object.keys(headers).map((k) => { {Object.keys(headers).map((k) => {
if (Object.prototype.hasOwnProperty.call(supportObj, k)) { if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
return <td key={k}>{sdkSupportToJsx(supportObj[k as keyof typeof headers])}</td>; return <td key={k}>{supportObj[k as keyof typeof headers]}</td>;
} }
else { else {
return <td key={k}>no</td>; return <td key={k}>no</td>;
@@ -99,12 +85,7 @@ export default class Doc extends React.Component<DocProps> {
</table> </table>
</div> </div>
} }
{docUrl && docUrlLinkText &&
<div className="SpecDoc__learn-more">
<a href={docUrl} target="_blank" rel="noreferrer">{docUrlLinkText}</a>
</div>
}
</> </>
); );
} }
} }
+9 -9
View File
@@ -1,5 +1,6 @@
import InputArray, { type InputArrayProps } from "./InputArray"; import React from 'react'
import Fieldset from "./Fieldset"; import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
import Fieldset from './Fieldset'
type FieldArrayProps = InputArrayProps & { type FieldArrayProps = InputArrayProps & {
name?: string name?: string
@@ -8,12 +9,11 @@ type FieldArrayProps = InputArrayProps & {
} }
}; };
const FieldArray: React.FC<FieldArrayProps> = (props) => { export default class FieldArray extends React.Component<FieldArrayProps> {
return ( render() {
<Fieldset label={props.label} fieldSpec={props.fieldSpec}> return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputArray {...props} /> <InputArray {...this.props} />
</Fieldset> </Fieldset>
); }
}; }
export default FieldArray;
+9 -9
View File
@@ -1,5 +1,6 @@
import Block from "./Block"; import React from 'react'
import InputAutocomplete, { type InputAutocompleteProps } from "./InputAutocomplete"; import Block from './Block'
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
type FieldAutocompleteProps = InputAutocompleteProps & { type FieldAutocompleteProps = InputAutocompleteProps & {
@@ -7,12 +8,11 @@ type FieldAutocompleteProps = InputAutocompleteProps & {
}; };
const FieldAutocomplete: React.FC<FieldAutocompleteProps> = (props) => { export default class FieldAutocomplete extends React.Component<FieldAutocompleteProps> {
return ( render() {
<Block label={props.label}> return <Block label={this.props.label}>
<InputAutocomplete {...props} /> <InputAutocomplete {...this.props} />
</Block> </Block>
); }
}; }
export default FieldAutocomplete;
+9 -9
View File
@@ -1,5 +1,6 @@
import Block from "./Block"; import React from 'react'
import InputCheckbox, {type InputCheckboxProps} from "./InputCheckbox"; import Block from './Block'
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
type FieldCheckboxProps = InputCheckboxProps & { type FieldCheckboxProps = InputCheckboxProps & {
@@ -7,12 +8,11 @@ type FieldCheckboxProps = InputCheckboxProps & {
}; };
const FieldCheckbox: React.FC<FieldCheckboxProps> = (props) => { export default class FieldCheckbox extends React.Component<FieldCheckboxProps> {
return ( render() {
<Block label={props.label}> return <Block label={this.props.label}>
<InputCheckbox {...props} /> <InputCheckbox {...this.props} />
</Block> </Block>
); }
}; }
export default FieldCheckbox;
+9 -9
View File
@@ -1,5 +1,6 @@
import Block from "./Block"; import React from 'react'
import InputColor, {type InputColorProps} from "./InputColor"; import Block from './Block'
import InputColor, {InputColorProps} from './InputColor'
type FieldColorProps = InputColorProps & { type FieldColorProps = InputColorProps & {
@@ -10,12 +11,11 @@ type FieldColorProps = InputColorProps & {
}; };
const FieldColor: React.FC<FieldColorProps> = (props) => { export default class FieldColor extends React.Component<FieldColorProps> {
return ( render() {
<Block label={props.label} fieldSpec={props.fieldSpec}> return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputColor {...props} /> <InputColor {...this.props} />
</Block> </Block>
); }
}; }
export default FieldColor;
+18 -25
View File
@@ -1,40 +1,33 @@
import React from "react"; import React from 'react'
import Block from "./Block"; import Block from './Block'
import InputString from "./InputString"; import InputString from './InputString'
import { type WithTranslation, withTranslation } from "react-i18next";
type FieldCommentInternalProps = { type FieldCommentProps = {
value?: string value?: string
onChange(value: string | undefined): unknown onChange(value: string | undefined): unknown
error: {message: string} error: {message: string}
} & WithTranslation; };
const FieldCommentInternal: React.FC<FieldCommentInternalProps> = (props) => { export default class FieldComment extends React.Component<FieldCommentProps> {
const t = props.t; render() {
const fieldSpec = { const fieldSpec = {
doc: t( doc: "Comments for the current layer. This is non-standard and not in the spec."
"Comments for the current layer. This is non-standard and not in the spec." };
),
};
return ( return <Block
<Block label={"Comments"}
label={t("Comments")}
fieldSpec={fieldSpec} fieldSpec={fieldSpec}
data-wd-key="layer-comment" data-wd-key="layer-comment"
error={props.error} error={this.props.error}
> >
<InputString <InputString
multi={true} multi={true}
value={props.value} value={this.props.value}
onChange={props.onChange} onChange={this.props.onChange}
default={t("Comment...")} default="Comment..."
data-wd-key="layer-comment.input" data-wd-key="layer-comment.input"
/> />
</Block> </Block>
); }
}; }
const FieldComment = withTranslation()(FieldCommentInternal);
export default FieldComment;
+44 -32
View File
@@ -1,53 +1,65 @@
import React, { type JSX } from "react"; import React from 'react'
import {MdInfoOutline, MdHighlightOff} from "react-icons/md"; import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
type FieldDocLabelProps = { type FieldDocLabelProps = {
label: JSX.Element | string | undefined label: object | string | undefined
fieldSpec?: { fieldSpec?: {
doc?: string doc?: string
} }
onToggleDoc?(...args: unknown[]): unknown onToggleDoc?(...args: unknown[]): unknown
}; };
type FieldDocLabelState = {
open: boolean
};
const FieldDocLabel: React.FC<FieldDocLabelProps> = (props) => { export default class FieldDocLabel extends React.Component<FieldDocLabelProps, FieldDocLabelState> {
const [open, setOpen] = React.useState(false); constructor (props: FieldDocLabelProps) {
super(props);
const onToggleDoc = (state: boolean) => { this.state = {
setOpen(state); open: false,
if (props.onToggleDoc) {
props.onToggleDoc(state);
} }
}; }
const { label, fieldSpec } = props; onToggleDoc = (open: boolean) => {
const { doc } = fieldSpec || {}; this.setState({
open,
}, () => {
if (this.props.onToggleDoc) {
this.props.onToggleDoc(this.state.open);
}
});
}
if (doc) { render() {
return ( const {label, fieldSpec} = this.props;
<label className="maputnik-doc-wrapper"> const {doc} = fieldSpec || {};
if (doc) {
return <label className="maputnik-doc-wrapper">
<div className="maputnik-doc-target"> <div className="maputnik-doc-target">
{label} {label}
{"\xa0"} {'\xa0'}
<button <button
aria-label={open ? "close property documentation" : "open property documentation"} aria-label={this.state.open ? "close property documentation" : "open property documentation"}
className={`maputnik-doc-button maputnik-doc-button--${open ? "open" : "closed"}`} className={`maputnik-doc-button maputnik-doc-button--${this.state.open ? 'open' : 'closed'}`}
onClick={() => onToggleDoc(!open)} onClick={() => this.onToggleDoc(!this.state.open)}
data-wd-key={"field-doc-button-" + label} data-wd-key={'field-doc-button-'+label}
> >
{open ? <MdHighlightOff /> : <MdInfoOutline />} {this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
</button> </button>
</div> </div>
</label> </label>
); }
} else if (label) { else if (label) {
return ( return <label className="maputnik-doc-wrapper">
<label className="maputnik-doc-wrapper"> <div className="maputnik-doc-target">
<div className="maputnik-doc-target">{label}</div> {label}
</div>
</label> </label>
); }
else {
<div />
}
} }
return <div />; }
};
export default FieldDocLabel;
+9 -9
View File
@@ -1,16 +1,16 @@
import InputDynamicArray, {type InputDynamicArrayProps} from "./InputDynamicArray"; import React from 'react'
import Fieldset from "./Fieldset"; import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
import Fieldset from './Fieldset'
type FieldDynamicArrayProps = InputDynamicArrayProps & { type FieldDynamicArrayProps = InputDynamicArrayProps & {
name?: string name?: string
}; };
const FieldDynamicArray: React.FC<FieldDynamicArrayProps> = (props) => { export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
return ( render() {
<Fieldset label={props.label}> return <Fieldset label={this.props.label}>
<InputDynamicArray {...props} /> <InputDynamicArray {...this.props} />
</Fieldset> </Fieldset>
); }
}; }
export default FieldDynamicArray;
+10 -11
View File
@@ -1,8 +1,9 @@
import InputEnum, {type InputEnumProps} from "./InputEnum"; import React from 'react'
import Fieldset from "./Fieldset"; import InputEnum, {InputEnumProps} from './InputEnum'
import Fieldset from './Fieldset';
type FieldEnumProps = InputEnumProps & { type FieldEnumProps = InputEnumProps & {
label?: string; label?: string;
fieldSpec?: { fieldSpec?: {
doc: string doc: string
@@ -10,12 +11,10 @@ type FieldEnumProps = InputEnumProps & {
}; };
const FieldEnum: React.FC<FieldEnumProps> = (props) => { export default class FieldEnum extends React.Component<FieldEnumProps> {
return ( render() {
<Fieldset label={props.label} fieldSpec={props.fieldSpec}> return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputEnum {...props} /> <InputEnum {...this.props} />
</Fieldset> </Fieldset>
); }
}; }
export default FieldEnum;
+231 -230
View File
@@ -1,12 +1,11 @@
import React from "react"; import React from 'react'
import SpecProperty from "./_SpecProperty"; import SpecProperty from './_SpecProperty'
import DataProperty, { type Stop } from "./_DataProperty"; import DataProperty, { Stop } from './_DataProperty'
import ZoomProperty from "./_ZoomProperty"; import ZoomProperty from './_ZoomProperty'
import ExpressionProperty from "./_ExpressionProperty"; import ExpressionProperty from './_ExpressionProperty'
import {function as styleFunction} from "@maplibre/maplibre-gl-style-spec"; import {function as styleFunction} from '@maplibre/maplibre-gl-style-spec';
import {findDefaultFromSpec} from "../libs/spec-helper"; import {findDefaultFromSpec} from '../libs/spec-helper';
import { type MappedLayerErrors } from "../libs/definitions";
function isLiteralExpression(value: any) { function isLiteralExpression(value: any) {
@@ -23,9 +22,9 @@ function isGetExpression(value: any) {
function isZoomField(value: any) { function isZoomField(value: any) {
return ( return (
typeof(value) === "object" && typeof(value) === 'object' &&
value.stops && value.stops &&
typeof(value.property) === "undefined" && typeof(value.property) === 'undefined' &&
Array.isArray(value.stops) && Array.isArray(value.stops) &&
value.stops.length > 1 && value.stops.length > 1 &&
value.stops.every((stop: Stop) => { value.stops.every((stop: Stop) => {
@@ -39,7 +38,7 @@ function isZoomField(value: any) {
function isIdentityProperty(value: any) { function isIdentityProperty(value: any) {
return ( return (
typeof(value) === "object" && typeof(value) === 'object' &&
value.type === "identity" && value.type === "identity" &&
Object.prototype.hasOwnProperty.call(value, "property") Object.prototype.hasOwnProperty.call(value, "property")
); );
@@ -47,16 +46,16 @@ function isIdentityProperty(value: any) {
function isDataStopProperty(value: any) { function isDataStopProperty(value: any) {
return ( return (
typeof(value) === "object" && typeof(value) === 'object' &&
value.stops && value.stops &&
typeof(value.property) !== "undefined" && typeof(value.property) !== 'undefined' &&
value.stops.length > 1 && value.stops.length > 1 &&
Array.isArray(value.stops) && Array.isArray(value.stops) &&
value.stops.every((stop: Stop) => { value.stops.every((stop: Stop) => {
return ( return (
Array.isArray(stop) && Array.isArray(stop) &&
stop.length === 2 && stop.length === 2 &&
typeof(stop[0]) === "object" typeof(stop[0]) === 'object'
); );
}) })
); );
@@ -91,18 +90,6 @@ function getDataType(value: any, fieldSpec={} as any) {
else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) { else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) {
return "value"; return "value";
} }
else if (fieldSpec.type === "numberArray" && isArrayOfPrimatives(value)) {
return "value";
}
else if (fieldSpec.type === "colorArray") {
return "value";
}
else if (fieldSpec.type === "padding") {
return "value";
}
else if (fieldSpec.type === "variableAnchorOffsetCollection") {
return "value";
}
else if (isZoomField(value)) { else if (isZoomField(value)) {
return "zoom_function"; return "zoom_function";
} }
@@ -120,287 +107,301 @@ type FieldFunctionProps = {
fieldName: string fieldName: string
fieldType: string fieldType: string
fieldSpec: any fieldSpec: any
errors?: MappedLayerErrors errors?: {[key: string]: {message: string}}
value?: any value?: any
}; };
type FieldFunctionState = {
dataType: string
isEditing: boolean
}
/** Supports displaying spec field for zoom function objects /** Supports displaying spec field for zoom function objects
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property * https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
*/ */
const FieldFunction: React.FC<FieldFunctionProps> = (props) => { export default class FieldFunction extends React.Component<FieldFunctionProps, FieldFunctionState> {
const [dataType, setDataType] = React.useState( constructor (props: FieldFunctionProps) {
getDataType(props.value, props.fieldSpec) super(props);
); this.state = {
const [isEditing, setIsEditing] = React.useState(false); dataType: getDataType(props.value, props.fieldSpec),
isEditing: false,
React.useEffect(() => {
if (!isEditing) {
setDataType(getDataType(props.value, props.fieldSpec));
} }
}, [props.value, props.fieldSpec, isEditing]); }
const getFieldFunctionType = (fieldSpec: any) => { static getDerivedStateFromProps(props: FieldFunctionProps, state: FieldFunctionState) {
// Because otherwise when editing values we end up accidentally changing field type.
if (state.isEditing) {
return {};
}
else {
return {
isEditing: false,
dataType: getDataType(props.value, props.fieldSpec)
};
}
}
getFieldFunctionType(fieldSpec: any) {
if (fieldSpec.expression.interpolated) { if (fieldSpec.expression.interpolated) {
return "exponential"; return "exponential"
} }
if (fieldSpec.type === "number") { if (fieldSpec.type === "number") {
return "interval"; return "interval"
} }
return "categorical"; return "categorical"
}; }
const addStop = () => { addStop = () => {
const stops = props.value.stops.slice(0); const stops = this.props.value.stops.slice(0)
const lastStop = stops[stops.length - 1]; const lastStop = stops[stops.length - 1]
if (typeof lastStop[0] === "object") { if (typeof lastStop[0] === "object") {
stops.push([ stops.push([
{ zoom: lastStop[0].zoom + 1, value: lastStop[0].value }, {zoom: lastStop[0].zoom + 1, value: lastStop[0].value},
lastStop[1], lastStop[1]
]); ])
} else { }
stops.push([lastStop[0] + 1, lastStop[1]]); else {
stops.push([lastStop[0] + 1, lastStop[1]])
} }
const changedValue = { const changedValue = {
...props.value, ...this.props.value,
stops: stops, stops: stops,
};
props.onChange(props.fieldName, changedValue);
};
const deleteExpression = () => {
const { fieldSpec, fieldName } = props;
props.onChange(fieldName, fieldSpec.default);
setDataType("value");
};
const deleteStop = (stopIdx: number) => {
const stops = props.value.stops.slice(0);
stops.splice(stopIdx, 1);
let changedValue: any = {
...props.value,
stops: stops,
};
if (stops.length === 1) {
changedValue = stops[0][1];
} }
props.onChange(props.fieldName, changedValue); this.props.onChange(this.props.fieldName, changedValue)
}; }
const makeZoomFunction = () => { deleteExpression = () => {
const { value } = props; const {fieldSpec, fieldName} = this.props;
this.props.onChange(fieldName, fieldSpec.default);
this.setState({
dataType: "value",
});
}
let zoomFunc: any; deleteStop = (stopIdx: number) => {
if (typeof value === "object") { const stops = this.props.value.stops.slice(0)
stops.splice(stopIdx, 1)
let changedValue = {
...this.props.value,
stops: stops,
}
if(stops.length === 1) {
changedValue = stops[0][1]
}
this.props.onChange(this.props.fieldName, changedValue)
}
makeZoomFunction = () => {
const {value} = this.props;
let zoomFunc;
if (typeof(value) === "object") {
if (value.stops) { if (value.stops) {
zoomFunc = { zoomFunc = {
base: value.base, base: value.base,
stops: value.stops.map((stop: Stop) => { stops: value.stops.map((stop: Stop) => {
return [stop[0].zoom, stop[1] || findDefaultFromSpec(props.fieldSpec)]; return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
}), })
}; }
} else { }
else {
zoomFunc = { zoomFunc = {
base: value.base, base: value.base,
stops: [ stops: [
[6, findDefaultFromSpec(props.fieldSpec)], [6, findDefaultFromSpec(this.props.fieldSpec)],
[10, findDefaultFromSpec(props.fieldSpec)], [10, findDefaultFromSpec(this.props.fieldSpec)]
], ]
}; }
} }
} else { }
else {
zoomFunc = { zoomFunc = {
stops: [ stops: [
[6, value || findDefaultFromSpec(props.fieldSpec)], [6, value || findDefaultFromSpec(this.props.fieldSpec)],
[10, value || findDefaultFromSpec(props.fieldSpec)], [10, value || findDefaultFromSpec(this.props.fieldSpec)]
], ]
}; }
} }
props.onChange(props.fieldName, zoomFunc); this.props.onChange(this.props.fieldName, zoomFunc)
}; }
const undoExpression = () => { undoExpression = () => {
const { value, fieldName } = props; const {value, fieldName} = this.props;
if (isGetExpression(value)) { if (isGetExpression(value)) {
props.onChange(fieldName, { this.props.onChange(fieldName, {
type: "identity", "type": "identity",
property: value[1], "property": value[1]
});
this.setState({
dataType: "value",
}); });
setDataType("value");
} else if (isLiteralExpression(value)) {
props.onChange(fieldName, value[1]);
setDataType("value");
} }
}; else if (isLiteralExpression(value)) {
this.props.onChange(fieldName, value[1]);
this.setState({
dataType: "value",
});
}
}
const canUndo = () => { canUndo = () => {
const { value, fieldSpec } = props; const {value, fieldSpec} = this.props;
return ( return (
isGetExpression(value) || isGetExpression(value) ||
isLiteralExpression(value) || isLiteralExpression(value) ||
isPrimative(value) || isPrimative(value) ||
(Array.isArray(value) && fieldSpec.type === "array") (Array.isArray(value) && fieldSpec.type === "array")
); );
}; }
const makeExpression = () => { makeExpression = () => {
const { value, fieldSpec } = props; const {value, fieldSpec} = this.props;
let expression; let expression;
if (typeof value === "object" && "stops" in value) { if (typeof(value) === "object" && 'stops' in value) {
expression = styleFunction.convertFunction(value, fieldSpec); expression = styleFunction.convertFunction(value, fieldSpec);
} else if (isIdentityProperty(value)) {
expression = ["get", value.property];
} else {
expression = ["literal", value || props.fieldSpec.default];
} }
props.onChange(props.fieldName, expression); else if (isIdentityProperty(value)) {
}; expression = ["get", value.property];
}
else {
expression = ["literal", value || this.props.fieldSpec.default];
}
this.props.onChange(this.props.fieldName, expression);
}
const makeDataFunction = () => { makeDataFunction = () => {
const functionType = getFieldFunctionType(props.fieldSpec); const functionType = this.getFieldFunctionType(this.props.fieldSpec);
const stopValue = functionType === "categorical" ? "" : 0; const stopValue = functionType === 'categorical' ? '' : 0;
const { value } = props; const {value} = this.props;
let dataFunc; let dataFunc;
if (typeof value === "object") { if (typeof(value) === "object") {
if (value.stops) { if (value.stops) {
dataFunc = { dataFunc = {
property: "", property: "",
type: functionType, type: functionType,
base: value.base, base: value.base,
stops: value.stops.map((stop: Stop) => { stops: value.stops.map((stop: Stop) => {
return [{ zoom: stop[0], value: stopValue }, stop[1] || findDefaultFromSpec(props.fieldSpec)]; return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
}), })
}; }
} else { }
else {
dataFunc = { dataFunc = {
property: "", property: "",
type: functionType, type: functionType,
base: value.base, base: value.base,
stops: [ stops: [
[{ zoom: 6, value: stopValue }, findDefaultFromSpec(props.fieldSpec)], [{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)],
[{ zoom: 10, value: stopValue }, findDefaultFromSpec(props.fieldSpec)], [{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)]
], ]
}; }
} }
} else { }
else {
dataFunc = { dataFunc = {
property: "", property: "",
type: functionType, type: functionType,
base: value.base, base: value.base,
stops: [ stops: [
[{ zoom: 6, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)], [{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
[{ zoom: 10, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)], [{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
], ]
}; }
} }
props.onChange(props.fieldName, dataFunc); this.props.onChange(this.props.fieldName, dataFunc)
};
const makeElevationFunction = () => {
const expression = [
"interpolate",
["linear"],
["elevation"],
0,
"black",
2000,
"white"
];
props.onChange(props.fieldName, expression);
};
const onMarkEditing = () => {
setIsEditing(true);
};
const onUnmarkEditing = () => {
setIsEditing(false);
};
const propClass =
props.fieldSpec.default === props.value ? "maputnik-default-property" : "maputnik-modified-property";
let specField;
if (dataType === "expression") {
specField = (
<ExpressionProperty
errors={props.errors}
onChange={props.onChange.bind(null, props.fieldName)}
canUndo={canUndo}
onUndo={undoExpression}
onDelete={deleteExpression}
fieldType={props.fieldType}
fieldName={props.fieldName}
fieldSpec={props.fieldSpec}
value={props.value}
onFocus={onMarkEditing}
onBlur={onUnmarkEditing}
/>
);
} else if (dataType === "zoom_function") {
specField = (
<ZoomProperty
errors={props.errors}
onChange={props.onChange.bind(null)}
fieldType={props.fieldType}
fieldName={props.fieldName}
fieldSpec={props.fieldSpec}
value={props.value}
onDeleteStop={deleteStop}
onAddStop={addStop}
onChangeToDataFunction={makeDataFunction}
onExpressionClick={makeExpression}
/>
);
} else if (dataType === "data_function") {
specField = (
<DataProperty
errors={props.errors}
onChange={props.onChange.bind(null)}
fieldType={props.fieldType}
fieldName={props.fieldName}
fieldSpec={props.fieldSpec}
value={props.value}
onDeleteStop={deleteStop}
onAddStop={addStop}
onChangeToZoomFunction={makeZoomFunction}
onExpressionClick={makeExpression}
/>
);
} else {
specField = (
<SpecProperty
errors={props.errors}
onChange={props.onChange.bind(null)}
fieldType={props.fieldType}
fieldName={props.fieldName}
fieldSpec={props.fieldSpec}
value={props.value}
onZoomClick={makeZoomFunction}
onDataClick={makeDataFunction}
onExpressionClick={makeExpression}
onElevationClick={makeElevationFunction}
/>
);
} }
return ( onMarkEditing = () => {
<div className={propClass} data-wd-key={"spec-field-container:" + props.fieldName}> this.setState({isEditing: true});
}
onUnmarkEditing = () => {
this.setState({isEditing: false});
}
render() {
const {dataType} = this.state;
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
let specField;
if (dataType === "expression") {
specField = (
<ExpressionProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this, this.props.fieldName)}
canUndo={this.canUndo}
onUndo={this.undoExpression}
onDelete={this.deleteExpression}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onFocus={this.onMarkEditing}
onBlur={this.onUnmarkEditing}
/>
);
}
else if (dataType === "zoom_function") {
specField = (
<ZoomProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this)}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onDeleteStop={this.deleteStop}
onAddStop={this.addStop}
onChangeToDataFunction={this.makeDataFunction}
onExpressionClick={this.makeExpression}
/>
)
}
else if (dataType === "data_function") {
// TODO: Rename to FieldFunction **this file** shouldn't be called that
specField = (
<DataProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this)}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onDeleteStop={this.deleteStop}
onAddStop={this.addStop}
onChangeToZoomFunction={this.makeZoomFunction}
onExpressionClick={this.makeExpression}
/>
)
}
else {
specField = (
<SpecProperty
errors={this.props.errors}
onChange={this.props.onChange.bind(this)}
fieldType={this.props.fieldType}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onZoomClick={this.makeZoomFunction}
onDataClick={this.makeDataFunction}
onExpressionClick={this.makeExpression}
/>
)
}
return <div className={propClass} data-wd-key={"spec-field-container:"+this.props.fieldName}>
{specField} {specField}
</div> </div>
); }
}; }
export default FieldFunction;
+14 -15
View File
@@ -1,7 +1,8 @@
import React from 'react'
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json"; import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from "./Block"; import Block from './Block'
import InputString from "./InputString"; import InputString from './InputString'
type FieldIdProps = { type FieldIdProps = {
value: string value: string
@@ -10,19 +11,17 @@ type FieldIdProps = {
error?: {message: string} error?: {message: string}
}; };
const FieldId: React.FC<FieldIdProps> = (props) => { export default class FieldId extends React.Component<FieldIdProps> {
return ( render() {
<Block label="ID" fieldSpec={latest.layer.id} return <Block label={"ID"} fieldSpec={latest.layer.id}
data-wd-key={props.wdKey} data-wd-key={this.props.wdKey}
error={props.error} error={this.props.error}
> >
<InputString <InputString
value={props.value} value={this.props.value}
onInput={props.onChange} onInput={this.props.onChange}
data-wd-key={props.wdKey + ".input"} data-wd-key={this.props.wdKey + ".input"}
/> />
</Block> </Block>
); }
}; }
export default FieldId;
+7 -5
View File
@@ -1,11 +1,13 @@
import InputJson, {type InputJsonProps} from "./InputJson"; import React from 'react'
import InputJson, {InputJsonProps} from './InputJson'
type FieldJsonProps = InputJsonProps & {}; type FieldJsonProps = InputJsonProps & {};
const FieldJson: React.FC<FieldJsonProps> = (props) => { export default class FieldJson extends React.Component<FieldJsonProps> {
return <InputJson {...props} />; render() {
}; return <InputJson {...this.props} />
}
}
export default FieldJson;
+14 -19
View File
@@ -1,35 +1,30 @@
import React from "react"; import React from 'react'
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json"; import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from "./Block"; import Block from './Block'
import InputNumber from "./InputNumber"; import InputNumber from './InputNumber'
import { type WithTranslation, withTranslation } from "react-i18next";
type FieldMaxZoomInternalProps = { type FieldMaxZoomProps = {
value?: number value?: number
onChange(value: number | undefined): unknown onChange(value: number | undefined): unknown
error?: {message: string} error?: {message: string}
} & WithTranslation; };
const FieldMaxZoomInternal: React.FC<FieldMaxZoomInternalProps> = (props) => { export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
const t = props.t; render() {
return ( return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
<Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom} error={this.props.error}
error={props.error}
data-wd-key="max-zoom" data-wd-key="max-zoom"
> >
<InputNumber <InputNumber
allowRange={true} allowRange={true}
value={props.value} value={this.props.value}
onChange={props.onChange} onChange={this.props.onChange}
min={latest.layer.maxzoom.minimum} min={latest.layer.maxzoom.minimum}
max={latest.layer.maxzoom.maximum} max={latest.layer.maxzoom.maximum}
default={latest.layer.maxzoom.maximum} default={latest.layer.maxzoom.maximum}
data-wd-key="max-zoom.input" data-wd-key="max-zoom.input"
/> />
</Block> </Block>
); }
}; }
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
export default FieldMaxZoom;
+14 -19
View File
@@ -1,35 +1,30 @@
import React from "react"; import React from 'react'
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json"; import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from "./Block"; import Block from './Block'
import InputNumber from "./InputNumber"; import InputNumber from './InputNumber'
import { type WithTranslation, withTranslation } from "react-i18next";
type FieldMinZoomInternalProps = { type FieldMinZoomProps = {
value?: number value?: number
onChange(...args: unknown[]): unknown onChange(...args: unknown[]): unknown
error?: {message: string} error?: {message: string}
} & WithTranslation; };
const FieldMinZoomInternal: React.FC<FieldMinZoomInternalProps> = (props) => { export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
const t = props.t; render() {
return ( return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
<Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom} error={this.props.error}
error={props.error}
data-wd-key="min-zoom" data-wd-key="min-zoom"
> >
<InputNumber <InputNumber
allowRange={true} allowRange={true}
value={props.value} value={this.props.value}
onChange={props.onChange} onChange={this.props.onChange}
min={latest.layer.minzoom.minimum} min={latest.layer.minzoom.minimum}
max={latest.layer.minzoom.maximum} max={latest.layer.minzoom.maximum}
default={latest.layer.minzoom.minimum} default={latest.layer.minzoom.minimum}
data-wd-key='min-zoom.input' data-wd-key='min-zoom.input'
/> />
</Block> </Block>
); }
}; }
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
export default FieldMinZoom;
+9 -9
View File
@@ -1,5 +1,6 @@
import InputMultiInput, {type InputMultiInputProps} from "./InputMultiInput"; import React from 'react'
import Fieldset from "./Fieldset"; import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
import Fieldset from './Fieldset'
type FieldMultiInputProps = InputMultiInputProps & { type FieldMultiInputProps = InputMultiInputProps & {
@@ -7,12 +8,11 @@ type FieldMultiInputProps = InputMultiInputProps & {
}; };
const FieldMultiInput: React.FC<FieldMultiInputProps> = (props) => { export default class FieldMultiInput extends React.Component<FieldMultiInputProps> {
return ( render() {
<Fieldset label={props.label}> return <Fieldset label={this.props.label}>
<InputMultiInput {...props} /> <InputMultiInput {...this.props} />
</Fieldset> </Fieldset>
); }
}; }
export default FieldMultiInput;
+9 -10
View File
@@ -1,5 +1,6 @@
import InputNumber, {type InputNumberProps} from "./InputNumber"; import React from 'react'
import Block from "./Block"; import InputNumber, {InputNumberProps} from './InputNumber'
import Block from './Block'
type FieldNumberProps = InputNumberProps & { type FieldNumberProps = InputNumberProps & {
@@ -10,12 +11,10 @@ type FieldNumberProps = InputNumberProps & {
}; };
const FieldNumber: React.FC<FieldNumberProps> = (props) => { export default class FieldNumber extends React.Component<FieldNumberProps> {
return ( render() {
<Block label={props.label} fieldSpec={props.fieldSpec}> return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputNumber {...props} /> <InputNumber {...this.props} />
</Block> </Block>
); }
}; }
export default FieldNumber;
+10 -9
View File
@@ -1,5 +1,6 @@
import Block from "./Block"; import React from 'react'
import InputSelect, {type InputSelectProps} from "./InputSelect"; import Block from './Block'
import InputSelect, {InputSelectProps} from './InputSelect'
type FieldSelectProps = InputSelectProps & { type FieldSelectProps = InputSelectProps & {
@@ -10,12 +11,12 @@ type FieldSelectProps = InputSelectProps & {
}; };
const FieldSelect: React.FC<FieldSelectProps> = (props) => { export default class FieldSelect extends React.Component<FieldSelectProps> {
return ( render() {
<Block label={props.label} fieldSpec={props.fieldSpec}> return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputSelect {...props} /> <InputSelect {...this.props}/>
</Block> </Block>
); }
}; }
export default FieldSelect;
+25 -32
View File
@@ -1,42 +1,35 @@
import React from "react"; import React from 'react'
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json"; import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from "./Block"; import Block from './Block'
import InputAutocomplete from "./InputAutocomplete"; import InputAutocomplete from './InputAutocomplete'
import { type WithTranslation, withTranslation } from "react-i18next";
type FieldSourceInternalProps = { type FieldSourceProps = {
value?: string value?: string
wdKey?: string wdKey?: string
onChange?(value: string| undefined): unknown onChange?(value: string| undefined): unknown
sourceIds?: unknown[] sourceIds?: unknown[]
error?: {message: string} error?: {message: string}
} & WithTranslation;
const FieldSourceInternal: React.FC<FieldSourceInternalProps> = ({
onChange = () => {},
sourceIds = [],
wdKey,
value,
error,
t
}) => {
return (
<Block
label={t("Source")}
fieldSpec={latest.layer.source}
error={error}
data-wd-key={wdKey}
>
<InputAutocomplete
value={value}
onChange={onChange}
options={sourceIds?.map((src) => [src, src])}
/>
</Block>
);
}; };
export default class FieldSource extends React.Component<FieldSourceProps> {
static defaultProps = {
onChange: () => {},
sourceIds: [],
}
const FieldSource = withTranslation()(FieldSourceInternal); render() {
export default FieldSource; return <Block
label={"Source"}
fieldSpec={latest.layer.source}
error={this.props.error}
data-wd-key={this.props.wdKey}
>
<InputAutocomplete
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceIds?.map(src => [src, src])}
/>
</Block>
}
}
+29 -31
View File
@@ -1,39 +1,37 @@
import React from "react"; import React from 'react'
import {latest} from "@maplibre/maplibre-gl-style-spec"; import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from "./Block"; import Block from './Block'
import InputAutocomplete from "./InputAutocomplete"; import InputAutocomplete from './InputAutocomplete'
import { type WithTranslation, withTranslation } from "react-i18next";
type FieldSourceLayerInternalProps = { type FieldSourceLayerProps = {
value?: string value?: string
onChange?(...args: unknown[]): unknown onChange?(...args: unknown[]): unknown
sourceLayerIds?: unknown[] sourceLayerIds?: unknown[]
isFixed?: boolean
error?: {message: string} error?: {message: string}
} & WithTranslation;
const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = ({
onChange = () => {},
sourceLayerIds = [],
value,
error,
t
}) => {
return (
<Block
label={t("Source Layer")}
fieldSpec={latest.layer["source-layer"]}
data-wd-key="layer-source-layer"
error={error}
>
<InputAutocomplete
value={value}
onChange={onChange}
options={sourceLayerIds?.map((l) => [l, l])}
/>
</Block>
);
}; };
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal); export default class FieldSourceLayer extends React.Component<FieldSourceLayerProps> {
export default FieldSourceLayer; static defaultProps = {
onChange: () => {},
sourceLayerIds: [],
isFixed: false
}
render() {
return <Block
label={"Source Layer"}
fieldSpec={latest.layer['source-layer']}
data-wd-key="layer-source-layer"
error={this.props.error}
>
<InputAutocomplete
keepMenuWithinWindowBounds={!!this.props.isFixed}
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceLayerIds?.map(l => [l, l])}
/>
</Block>
}
}
-49
View File
@@ -1,49 +0,0 @@
import Block, { type BlockProps } from "./Block";
import InputSpec, { type FieldSpecType, type InputSpecProps } from "./InputSpec";
import Fieldset, { type FieldsetProps } from "./Fieldset";
function getElementFromType(fieldSpec: { type?: FieldSpecType, values?: unknown[] }): typeof Fieldset | typeof Block {
switch(fieldSpec.type) {
case "color":
return Block;
case "enum":
return (Object.keys(fieldSpec.values!).length <= 3 ? Fieldset : Block);
case "boolean":
return Block;
case "array":
return Fieldset;
case "resolvedImage":
return Block;
case "number":
return Block;
case "string":
return Block;
case "formatted":
return Block;
case "padding":
return Block;
case "numberArray":
return Fieldset;
case "colorArray":
return Fieldset;
case "variableAnchorOffsetCollection":
return Fieldset;
default:
console.warn("No such type for: " + fieldSpec.type);
return Block;
}
}
export type FieldSpecProps = InputSpecProps & BlockProps & FieldsetProps;
const FieldSpec: React.FC<FieldSpecProps> = (props) => {
const TypeBlock = getElementFromType(props.fieldSpec!);
return (
<TypeBlock label={props.label} action={props.action} fieldSpec={props.fieldSpec} error={props.error}>
<InputSpec {...props} />
</TypeBlock>
);
};
export default FieldSpec;
+9 -10
View File
@@ -1,5 +1,6 @@
import Block from "./Block"; import React from 'react'
import InputString, {type InputStringProps} from "./InputString"; import Block from './Block'
import InputString, {InputStringProps} from './InputString'
type FieldStringProps = InputStringProps & { type FieldStringProps = InputStringProps & {
name?: string name?: string
@@ -9,12 +10,10 @@ type FieldStringProps = InputStringProps & {
} }
}; };
const FieldString: React.FC<FieldStringProps> = (props) => { export default class FieldString extends React.Component<FieldStringProps> {
return ( render() {
<Block label={props.label} fieldSpec={props.fieldSpec}> return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputString {...props} /> <InputString {...this.props} />
</Block> </Block>
); }
}; }
export default FieldString;
+44 -39
View File
@@ -1,47 +1,52 @@
import React from "react"; import React from 'react'
import {v8} from "@maplibre/maplibre-gl-style-spec";
import Block from "./Block";
import InputSelect from "./InputSelect";
import InputString from "./InputString";
import { type WithTranslation, withTranslation } from "react-i18next";
import { startCase } from "lodash";
type FieldTypeInternalProps = { import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputSelect from './InputSelect'
import InputString from './InputString'
type FieldTypeProps = {
value: string value: string
wdKey?: string wdKey?: string
onChange(value: string): unknown onChange(value: string): unknown
error?: {message: string} error?: {message: string}
disabled?: boolean disabled?: boolean
} & WithTranslation;
const FieldTypeInternal: React.FC<FieldTypeInternalProps> = ({
t,
value,
wdKey,
onChange,
error,
disabled = false
}) => {
const layerstypes: [string, string][] = Object.keys(v8.layer.type.values || {}).map(v => [v, startCase(v.replace(/-/g, " "))]);
return (
<Block label={t("Type")} fieldSpec={v8.layer.type}
data-wd-key={wdKey}
error={error}
>
{disabled && (
<InputString value={value} disabled={true} />
)}
{!disabled && (
<InputSelect
options={layerstypes}
onChange={onChange}
value={value}
data-wd-key={wdKey + ".select"}
/>
)}
</Block>
);
}; };
const FieldType = withTranslation()(FieldTypeInternal); export default class FieldType extends React.Component<FieldTypeProps> {
export default FieldType; static defaultProps = {
disabled: false,
}
render() {
return <Block label={"Type"} fieldSpec={latest.layer.type}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
{this.props.disabled &&
<InputString
value={this.props.value}
disabled={true}
/>
}
{!this.props.disabled &&
<InputSelect
options={[
['background', 'Background'],
['fill', 'Fill'],
['line', 'Line'],
['symbol', 'Symbol'],
['raster', 'Raster'],
['circle', 'Circle'],
['fill-extrusion', 'Fill Extrusion'],
['hillshade', 'Hillshade'],
['heatmap', 'Heatmap'],
]}
onChange={this.props.onChange}
value={this.props.value}
data-wd-key={this.props.wdKey + ".select"}
/>
}
</Block>
}
}
+12 -10
View File
@@ -1,5 +1,6 @@
import InputUrl, {type FieldUrlProps as InputUrlProps} from "./InputUrl"; import React from 'react'
import Block from "./Block"; import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl'
import Block from './Block'
type FieldUrlProps = InputUrlProps & { type FieldUrlProps = InputUrlProps & {
@@ -10,12 +11,13 @@ type FieldUrlProps = InputUrlProps & {
}; };
const FieldUrl: React.FC<FieldUrlProps> = (props) => { export default class FieldUrl extends React.Component<FieldUrlProps> {
return ( render () {
<Block label={props.label} fieldSpec={props.fieldSpec}> return (
<InputUrl {...props} /> <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
</Block> <InputUrl {...this.props} />
); </Block>
}; );
}
}
export default FieldUrl;
+49 -37
View File
@@ -1,53 +1,65 @@
import React, { type PropsWithChildren, type ReactElement } from "react"; import React, { ReactElement } from 'react'
import classnames from "classnames"; import FieldDocLabel from './FieldDocLabel'
import FieldDocLabel from "./FieldDocLabel"; import Doc from './Doc'
import Doc from "./Doc"; import generateUniqueId from '../libs/document-uid';
import generateUniqueId from "../libs/document-uid";
export type FieldsetProps = PropsWithChildren & { type FieldsetProps = {
label?: string, label?: string,
fieldSpec?: { doc?: string }, fieldSpec?: { doc?: string },
action?: ReactElement, action?: ReactElement,
error?: {message: string}
}; };
type FieldsetState = {
showDoc: boolean
};
const Fieldset: React.FC<FieldsetProps> = (props) => { export default class Fieldset extends React.Component<FieldsetProps, FieldsetState> {
const [showDoc, setShowDoc] = React.useState(false); _labelId: string;
const labelId = React.useRef(generateUniqueId("fieldset_label_"));
const onToggleDoc = (val: boolean) => { constructor (props: FieldsetProps) {
setShowDoc(val); super(props);
}; this._labelId = generateUniqueId(`fieldset_label_`);
this.state = {
showDoc: false,
}
}
return ( onToggleDoc = (val: boolean) => {
<div className="maputnik-input-block" role="group" aria-labelledby={labelId.current}> this.setState({
{props.fieldSpec && ( showDoc: val
});
}
render () {
return <div className="maputnik-input-block" role="group" aria-labelledby={this._labelId}>
{this.props.fieldSpec &&
<div className="maputnik-input-block-label"> <div className="maputnik-input-block-label">
<FieldDocLabel <FieldDocLabel
label={props.label} label={this.props.label}
onToggleDoc={onToggleDoc} onToggleDoc={this.onToggleDoc}
fieldSpec={props.fieldSpec} fieldSpec={this.props.fieldSpec}
/> />
</div> </div>
)} }
{!props.fieldSpec && ( {!this.props.fieldSpec &&
<div className={classnames({ <div className="maputnik-input-block-label">
"maputnik-input-block-label": true, {this.props.label}
"maputnik-input-block--error": props.error
})}>
{props.label}
</div> </div>
)} }
<div className="maputnik-input-block-action">{props.action}</div> <div className="maputnik-input-block-action">
<div className="maputnik-input-block-content">{props.children}</div> {this.props.action}
{props.fieldSpec && ( </div>
<div className="maputnik-doc-inline" style={{ display: showDoc ? "" : "none" }}> <div className="maputnik-input-block-content">
<Doc fieldSpec={props.fieldSpec} /> {this.props.children}
</div>
{this.props.fieldSpec &&
<div
className="maputnik-doc-inline"
style={{display: this.state.showDoc ? '' : 'none'}}
>
<Doc fieldSpec={this.props.fieldSpec} />
</div> </div>
)} }
</div> </div>
); }
}; }
export default Fieldset;
+70 -75
View File
@@ -1,25 +1,22 @@
import React from "react"; import React from 'react'
import { TbMathFunction } from "react-icons/tb"; import {mdiTableRowPlusAfter} from '@mdi/js';
import { PiListPlusBold } from "react-icons/pi"; import {isEqual} from 'lodash';
import {isEqual} from "lodash"; import {ExpressionSpecification, LegacyFilterSpecification, StyleSpecification} from 'maplibre-gl'
import {type ExpressionSpecification, type LegacyFilterSpecification} from "maplibre-gl"; import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec'
import {migrate, convertFilter} from "@maplibre/maplibre-gl-style-spec"; import {mdiFunctionVariant} from '@mdi/js';
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import {combiningFilterOps} from "../libs/filterops"; import {combiningFilterOps} from '../libs/filterops'
import InputSelect from "./InputSelect"; import InputSelect from './InputSelect'
import Block from "./Block"; import Block from './Block'
import SingleFilterEditor from "./SingleFilterEditor"; import SingleFilterEditor from './SingleFilterEditor'
import FilterEditorBlock from "./FilterEditorBlock"; import FilterEditorBlock from './FilterEditorBlock'
import InputButton from "./InputButton"; import InputButton from './InputButton'
import Doc from "./Doc"; import Doc from './Doc'
import ExpressionProperty from "./_ExpressionProperty"; import ExpressionProperty from './_ExpressionProperty';
import { type WithTranslation, withTranslation } from "react-i18next";
import type { MappedLayerErrors, StyleSpecificationWithId } from "../libs/definitions";
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification { function combiningFilter(props: FilterEditorProps): LegacyFilterSpecification | ExpressionSpecification {
const filter = props.filter || ["all"]; const filter = props.filter || ['all'];
if (!Array.isArray(filter)) { if (!Array.isArray(filter)) {
return filter; return filter;
@@ -29,7 +26,7 @@ function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecific
let filters = filter.slice(1); let filters = filter.slice(1);
if(combiningFilterOps.indexOf(combiningOp) < 0) { if(combiningFilterOps.indexOf(combiningOp) < 0) {
combiningOp = "all"; combiningOp = 'all';
filters = [filter.slice(0)]; filters = [filter.slice(0)];
} }
@@ -41,7 +38,7 @@ function migrateFilter(filter: LegacyFilterSpecification | ExpressionSpecificati
return (migrate(createStyleFromFilter(filter) as any).layers[0] as any).filter; return (migrate(createStyleFromFilter(filter) as any).layers[0] as any).filter;
} }
function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpecification): StyleSpecificationWithId { function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpecification): StyleSpecification & {id: string} {
return { return {
"id": "tmp", "id": "tmp",
"version": 8, "version": 8,
@@ -50,7 +47,7 @@ function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpe
"sources": { "sources": {
"tmp": { "tmp": {
"type": "geojson", "type": "geojson",
"data": "" "data": ''
} }
}, },
"sprite": "", "sprite": "",
@@ -82,23 +79,23 @@ function checkIfSimpleFilter (filter: LegacyFilterSpecification | ExpressionSpec
} }
function hasCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) { function hasCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
return combiningFilterOps.indexOf(filter[0]) >= 0; return combiningFilterOps.indexOf(filter[0]) >= 0
} }
function hasNestedCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) { function hasNestedCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
if(hasCombiningFilter(filter)) { if(hasCombiningFilter(filter)) {
return filter.slice(1).map(f => hasCombiningFilter(f as any)).filter(f => f == true).length > 0; return filter.slice(1).map(f => hasCombiningFilter(f as any)).filter(f => f == true).length > 0
} }
return false; return false
} }
type FilterEditorInternalProps = { type FilterEditorProps = {
/** Properties of the vector layer and the available fields */ /** Properties of the vector layer and the available fields */
properties?: {[key:string]: any} properties?: {[key:string]: any}
filter?: any[] filter?: any[]
errors?: MappedLayerErrors errors?: {[key:string]: any}
onChange(value: LegacyFilterSpecification | ExpressionSpecification): void onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
} & WithTranslation; };
type FilterEditorState = { type FilterEditorState = {
showDoc: boolean showDoc: boolean
@@ -106,12 +103,12 @@ type FilterEditorState = {
valueIsSimpleFilter?: boolean valueIsSimpleFilter?: boolean
}; };
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> { export default class FilterEditor extends React.Component<FilterEditorProps, FilterEditorState> {
static defaultProps = { static defaultProps = {
filter: ["all"], filter: ["all"],
}; }
constructor (props: FilterEditorInternalProps) { constructor (props: FilterEditorProps) {
super(props); super(props);
this.state = { this.state = {
showDoc: false, showDoc: false,
@@ -121,57 +118,57 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
// Convert filter to combining filter // Convert filter to combining filter
onFilterPartChanged(filterIdx: number, newPart: any[]) { onFilterPartChanged(filterIdx: number, newPart: any[]) {
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification; const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
newFilter[filterIdx] = newPart; newFilter[filterIdx] = newPart
this.props.onChange(newFilter); this.props.onChange(newFilter)
} }
deleteFilterItem(filterIdx: number) { deleteFilterItem(filterIdx: number) {
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification; const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
newFilter.splice(filterIdx + 1, 1); newFilter.splice(filterIdx + 1, 1)
this.props.onChange(newFilter); this.props.onChange(newFilter)
} }
addFilterItem = () => { addFilterItem = () => {
const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification; const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
(newFilterItem as any[]).push(["==", "name", ""]); (newFilterItem as any[]).push(['==', 'name', ''])
this.props.onChange(newFilterItem); this.props.onChange(newFilterItem)
}; }
onToggleDoc = (val: boolean) => { onToggleDoc = (val: boolean) => {
this.setState({ this.setState({
showDoc: val showDoc: val
}); });
}; }
makeFilter = () => { makeFilter = () => {
this.setState({ this.setState({
displaySimpleFilter: true, displaySimpleFilter: true,
}); })
}; }
makeExpression = () => { makeExpression = () => {
const filter = combiningFilter(this.props); const filter = combiningFilter(this.props);
this.props.onChange(migrateFilter(filter)); this.props.onChange(migrateFilter(filter));
this.setState({ this.setState({
displaySimpleFilter: false, displaySimpleFilter: false,
}); })
}; }
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) { static getDerivedStateFromProps(props: FilterEditorProps, currentState: FilterEditorState) {
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props)); const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
// Upgrade but never downgrade // Upgrade but never downgrade
if (!displaySimpleFilter && state.displaySimpleFilter === true) { if (!displaySimpleFilter && currentState.displaySimpleFilter === true) {
return { return {
displaySimpleFilter: false, displaySimpleFilter: false,
valueIsSimpleFilter: false, valueIsSimpleFilter: false,
}; };
} }
else if (displaySimpleFilter && state.displaySimpleFilter === false) { else if (displaySimpleFilter && currentState.displaySimpleFilter === false) {
return { return {
valueIsSimpleFilter: true, valueIsSimpleFilter: true,
}; }
} }
else { else {
return { return {
@@ -181,7 +178,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
} }
render() { render() {
const {errors, t} = this.props; const {errors} = this.props;
const {displaySimpleFilter} = this.state; const {displaySimpleFilter} = this.state;
const fieldSpec={ const fieldSpec={
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter." doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
@@ -193,16 +190,18 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
if (isNestedCombiningFilter) { if (isNestedCombiningFilter) {
return <div className="maputnik-filter-editor-unsupported"> return <div className="maputnik-filter-editor-unsupported">
<p> <p>
{t("Nested filters are not supported.")} Nested filters are not supported.
</p> </p>
<InputButton <InputButton
onClick={this.makeExpression} onClick={this.makeExpression}
title={t("Convert to expression")} title="Convert to expression"
> >
<TbMathFunction /> <svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
{t("Upgrade to expression")} <path fill="currentColor" d={mdiFunctionVariant} />
</svg>
Upgrade to expression
</InputButton> </InputButton>
</div>; </div>
} }
else if (displaySimpleFilter) { else if (displaySimpleFilter) {
const filter = combiningFilter(this.props); const filter = combiningFilter(this.props);
@@ -213,10 +212,12 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
<div> <div>
<InputButton <InputButton
onClick={this.makeExpression} onClick={this.makeExpression}
title={t("Convert to expression")} title="Convert to expression"
className="maputnik-make-zoom-function" className="maputnik-make-zoom-function"
> >
<TbMathFunction /> <svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg>
</InputButton> </InputButton>
</div> </div>
); );
@@ -238,7 +239,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
} }
</div> </div>
); );
}); })
return ( return (
@@ -246,17 +247,13 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
<Block <Block
key="top" key="top"
fieldSpec={fieldSpec} fieldSpec={fieldSpec}
label={t("Filter")} label={"Filter"}
action={actions} action={actions}
> >
<InputSelect <InputSelect
value={combiningOp} value={combiningOp}
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)} onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
options={[ options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
["all", t("every filter matches")],
["none", t("no filter matches")],
["any", t("any filter matches")]
]}
/> />
</Block> </Block>
{editorBlocks} {editorBlocks}
@@ -269,14 +266,15 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
className="maputnik-add-filter" className="maputnik-add-filter"
onClick={this.addFilterItem} onClick={this.addFilterItem}
> >
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} /> <svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
{t("Add filter")} <path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add filter
</InputButton> </InputButton>
</div> </div>
<div <div
key="doc" key="doc"
className="maputnik-doc-inline" className="maputnik-doc-inline"
style={{display: this.state.showDoc ? "" : "none"}} style={{display: this.state.showDoc ? '' : 'none'}}
> >
<Doc fieldSpec={fieldSpec} /> <Doc fieldSpec={fieldSpec} />
</div> </div>
@@ -294,19 +292,19 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
this.props.onChange(defaultFilter); this.props.onChange(defaultFilter);
}} }}
fieldName="filter" fieldName="filter"
fieldSpec={fieldSpec}
value={filter} value={filter}
errors={errors} errors={errors}
onChange={this.props.onChange} onChange={this.props.onChange}
/> />
{this.state.valueIsSimpleFilter && {this.state.valueIsSimpleFilter &&
<div className="maputnik-expr-infobox"> <div className="maputnik-expr-infobox">
{t("You've entered an old style filter.")} You&apos;ve entered a old style filter,{' '}
{" "}
<button <button
onClick={this.makeFilter} onClick={this.makeFilter}
className="maputnik-expr-infobox__button" className="maputnik-expr-infobox__button"
> >
{t("Switch to filter editor.")} switch to filter editor
</button> </button>
</div> </div>
} }
@@ -315,6 +313,3 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
} }
} }
} }
const FilterEditor = withTranslation()(FilterEditorInternal);
export default FilterEditor;
+12 -16
View File
@@ -1,31 +1,27 @@
import React, { type PropsWithChildren } from "react"; import React from 'react'
import InputButton from "./InputButton"; import InputButton from './InputButton'
import {MdDelete} from "react-icons/md"; import {MdDelete} from 'react-icons/md'
import { type WithTranslation, withTranslation } from "react-i18next";
type FilterEditorBlockInternalProps = PropsWithChildren & { type FilterEditorBlockProps = {
onDelete(...args: unknown[]): unknown onDelete(...args: unknown[]): unknown
} & WithTranslation; };
class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInternalProps> { export default class FilterEditorBlock extends React.Component<FilterEditorBlockProps> {
render() { render() {
const t = this.props.t;
return <div className="maputnik-filter-editor-block"> return <div className="maputnik-filter-editor-block">
<div className="maputnik-filter-editor-block-content">
{this.props.children}
</div>
<div className="maputnik-filter-editor-block-action"> <div className="maputnik-filter-editor-block-action">
<InputButton <InputButton
className="maputnik-icon-button" className="maputnik-delete-filter"
onClick={this.props.onDelete} onClick={this.props.onDelete}
title={t("Delete filter block")} title="Delete filter block"
> >
<MdDelete /> <MdDelete />
</InputButton> </InputButton>
</div> </div>
</div>; <div className="maputnik-filter-editor-block-content">
{this.props.children}
</div>
</div>
} }
} }
const FilterEditorBlock = withTranslation()(FilterEditorBlockInternal);
export default FilterEditorBlock;
+15
View File
@@ -0,0 +1,15 @@
import React from 'react'
import IconBase from 'react-icon-base'
export default class IconBackground extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<path d="m 1.821019,10.255581 7.414535,5.020197 c 0.372277,0.25206 0.958697,0.239771 1.30985,-0.02745 L 17.539255,9.926162 C 17.89041,9.658941 17.873288,9.238006 17.501015,8.985946 L 10.08648,3.9657402 C 9.714204,3.7136802 9.127782,3.7259703 8.776627,3.9931918 L 1.782775,9.315365 c -0.3511551,0.267221 -0.3340331,0.688156 0.03824,0.940216 z" />
</IconBase>
)
}
}
+15
View File
@@ -0,0 +1,15 @@
import React from 'react'
import IconBase from 'react-icon-base'
export default class IconCircle extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<path transform="translate(2 2)" d="M7.5,0C11.6422,0,15,3.3578,15,7.5S11.6422,15,7.5,15 S0,11.6422,0,7.5S3.3578,0,7.5,0z M7.5,1.6666c-3.2217,0-5.8333,2.6117-5.8333,5.8334S4.2783,13.3334,7.5,13.3334 s5.8333-2.6117,5.8333-5.8334S10.7217,1.6666,7.5,1.6666z"></path>
</IconBase>
)
}
}
+15
View File
@@ -0,0 +1,15 @@
import React from 'react'
import IconBase from 'react-icon-base'
export default class IconFill extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<path d="M 2.84978,9.763512 9.462149,4.7316391 16.47225,9.478015 9.859886,14.509879 2.84978,9.763512 m -1.028761,0.492069 7.414535,5.020197 c 0.372277,0.25206 0.958697,0.239771 1.30985,-0.02745 L 17.539255,9.926162 C 17.89041,9.658941 17.873288,9.238006 17.501015,8.985946 L 10.08648,3.9657402 C 9.714204,3.7136802 9.127782,3.7259703 8.776627,3.9931918 L 1.782775,9.315365 c -0.3511551,0.267221 -0.3340331,0.688156 0.03824,0.940216 l 0,0 z" />
</IconBase>
)
}
}
+24 -24
View File
@@ -1,33 +1,33 @@
import React from "react"; import React from 'react'
import type {CSSProperties} from "react"; import IconLine from './IconLine'
import { BsBack, BsDiamondFill, BsSunFill } from "react-icons/bs"; import IconFill from './IconFill'
import { MdBubbleChart, MdCircle, MdLocationPin, MdPhoto, MdPriorityHigh } from "react-icons/md"; import IconSymbol from './IconSymbol'
import { IoAnalyticsOutline } from "react-icons/io5"; import IconBackground from './IconBackground'
import { IoMdCube } from "react-icons/io"; import IconCircle from './IconCircle'
import { FaMountain } from "react-icons/fa"; import IconMissing from './IconMissing'
type IconLayerProps = { type IconLayerProps = {
type: string type: string
style?: CSSProperties style?: object
className?: string className?: string
}; };
const IconLayer: React.FC<IconLayerProps> = (props) => { export default class IconLayer extends React.Component<IconLayerProps> {
const iconProps = { style: props.style }; render() {
switch(props.type) { const iconProps = { style: this.props.style }
case "fill-extrusion": return <IoMdCube {...iconProps} />; switch(this.props.type) {
case "raster": return <MdPhoto {...iconProps} />; case 'fill-extrusion': return <IconBackground {...iconProps} />
case "hillshade": return <BsSunFill {...iconProps} />; case 'raster': return <IconFill {...iconProps} />
case "color-relief": return <FaMountain {...iconProps} />; case 'hillshade': return <IconFill {...iconProps} />
case "heatmap": return <MdBubbleChart {...iconProps} />; case 'heatmap': return <IconFill {...iconProps} />
case "fill": return <BsDiamondFill {...iconProps} />; case 'fill': return <IconFill {...iconProps} />
case "background": return <BsBack {...iconProps} />; case 'background': return <IconBackground {...iconProps} />
case "line": return <IoAnalyticsOutline {...iconProps} />; case 'line': return <IconLine {...iconProps} />
case "symbol": return <MdLocationPin {...iconProps} />; case 'symbol': return <IconSymbol {...iconProps} />
case "circle": return <MdCircle {...iconProps} />; case 'circle': return <IconCircle {...iconProps} />
default: return <MdPriorityHigh {...iconProps} />; default: return <IconMissing {...iconProps} />
}
} }
}; }
export default IconLayer;

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