Compare commits

...

163 Commits

Author SHA1 Message Date
github-actions[bot]
4dd34e99fd Bump version to 2.1.0 (#925)
Automated changes by
[create-pull-request](https://github.com/peter-evans/create-pull-request)
GitHub action

---------

Co-authored-by: HarelM <3269297+HarelM@users.noreply.github.com>
Co-authored-by: Harel M <harel.mazor@gmail.com>
2024-08-29 20:22:26 +03:00
dependabot[bot]
3773acd5be Bump micromatch from 4.0.5 to 4.0.8 (#923)
Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.5
to 4.0.8.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/micromatch/micromatch/releases">micromatch's
releases</a>.</em></p>
<blockquote>
<h2>4.0.8</h2>
<p>Ultimate release that fixes both CVE-2024-4067 and CVE-2024-4068. We
consider the issues low-priority, so even if you see automated scanners
saying otherwise, don't be scared.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md">micromatch's
changelog</a>.</em></p>
<blockquote>
<h2>[4.0.8] - 2024-08-22</h2>
<ul>
<li>backported CVE-2024-4067 fix (from v4.0.6) over to 4.x branch</li>
</ul>
<h2>[4.0.7] - 2024-05-22</h2>
<ul>
<li>this is basically v4.0.5, with some README updates</li>
<li><strong>it is vulnerable to CVE-2024-4067</strong></li>
<li>Updated braces to v3.0.3 to avoid CVE-2024-4068</li>
<li>does NOT break API compatibility</li>
</ul>
<h2>[4.0.6] - 2024-05-21</h2>
<ul>
<li>Added <code>hasBraces</code> to check if a pattern contains
braces.</li>
<li>Fixes CVE-2024-4067</li>
<li><strong>BREAKS API COMPATIBILITY</strong></li>
<li>Should be labeled as a major release, but it's not.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="8bd704ec0d"><code>8bd704e</code></a>
4.0.8</li>
<li><a
href="a0e68416a4"><code>a0e6841</code></a>
run verb to generate README documentation</li>
<li><a
href="4ec288484f"><code>4ec2884</code></a>
Merge branch 'v4' into hauserkristof-feature/v4.0.8</li>
<li><a
href="03aa805217"><code>03aa805</code></a>
Merge pull request <a
href="https://redirect.github.com/micromatch/micromatch/issues/266">#266</a>
from hauserkristof/feature/v4.0.8</li>
<li><a
href="814f5f70ef"><code>814f5f7</code></a>
lint</li>
<li><a
href="67fcce6a10"><code>67fcce6</code></a>
fix: CHANGELOG about braces &amp; CVE-2024-4068, v4.0.5</li>
<li><a
href="113f2e3fa7"><code>113f2e3</code></a>
fix: CVE numbers in CHANGELOG</li>
<li><a
href="d9dbd9a266"><code>d9dbd9a</code></a>
feat: updated CHANGELOG</li>
<li><a
href="2ab13157f4"><code>2ab1315</code></a>
fix: use actions/setup-node@v4</li>
<li><a
href="1406ea38f3"><code>1406ea3</code></a>
feat: rework test to work on macos with node 10,12 and 14</li>
<li>Additional commits viewable in <a
href="https://github.com/micromatch/micromatch/compare/4.0.5...4.0.8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=micromatch&package-manager=npm_and_yarn&previous-version=4.0.5&new-version=4.0.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/maplibre/maputnik/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-29 20:13:19 +03:00
Kevin Schaul
6887d70194 Make build scripts executable (#924)
Hopefully will fix CI issue merged in #922

cc @HarelM
2024-08-29 19:52:56 +03:00
Kevin Schaul
66c5a5c953 Update desktop build to pull from this repo (#922)
Previously the desktop build lived in a separate repo and had to
download a released version of the maputnik editor source code. Now that
both live in the same repo, the desktop version can simply run the
maputnik build command and use those generated files.

This commit also removes the ci-desktop workflow, which is not needed.
The regular ci workflow already built the desktop version (this commit
also fixes that build).

Fixes #919

If this works for you all, it would be lovely to create a new tag or
release on GitHub for two reasons:
1. So the latest binaries are easier to locate, and
2. So I can update my [submission to
homebrew](6e536ff007)
to make installation easier (for os x users at least)
2024-08-29 17:07:24 +03:00
Harel M
8184ac8393 Update geocoder to 1.6.0 (#920)
This is to update geocoder to latest version which now have types and is
fully modernized.
2024-08-21 08:56:59 +03:00
Joleen Knepp
6a0d2e8ee5 Added Simplified Chinese language support (#921)
Added Simplified Chinese language support
2024-08-21 07:17:31 +03:00
Keitaroh Kobayashi
58edd262b0 Add react-i18next for multi-language support (#917)
This is a rough start on adding react-i18next. I'll be working on adding
more translatable strings and translations in the coming days. I'm going
to need to wrap class components in HOCs, so let me know if there's
something I should be fixing before doing that. I'm thinking now to keep
the exported class names exactly the same, and rename the existing
classes by prefixing an `I` (for internal). For example:

```
export default class AppToolbar ...
```

becomes

```
class IAppToolbar ...
const AppToolbar = withTranslation()(IAppToolbar);
export default AppToolbar;
```

I'll be able to contribute Japanese strings (I've talked to a couple
people on my team and they'll be happy to help as well), so that's the
language I decided to go with in this PR.

Closes #746

---------

Co-authored-by: Ko Nagase <nagase@georepublic.co.jp>
Co-authored-by: Harel M <harel.mazor@gmail.com>
2024-08-19 12:43:04 +03:00
Ian Wagner
35840409b8 Add Stadia Outdoors style (#913)
* Adds the Stadia Outdoors style (anticipating the question: the
MapLibre domain and localhost development are allowlisted)
* Minor fixes to README as the npm commands seem to have changed
slightly
* Reorder the empty style to the beginning; seems the logical place for
it to me, but open for discussion
2024-07-25 22:50:24 -04:00
Kacper Golinski
d0f6e0fadb Update feature id label to $id in feature properties popup (#912)
Current label, "Feature ID", creates a little bit of confusion. It's not
clear that this label isn't really part of the feature properties.

For example, when we want to filter by feature id, we need to use `$id`

```
[==, "$id," 123] 
```
2024-07-12 21:18:23 +03:00
Bart Louwers
0de304ca3e Add Versatiles Colorful as example style (#908)
These are great looking permissively licensed styles maintained by a
passionate open source communities. I think they should be included in
the example styles. 🙂

The thumbnails were uploaded to GitHub and should be available pretty
much forever, I chose to triple the pixel density because the other
thumbnails look bad on my high res monitor.

<img width="573" alt="Screenshot 2024-06-22 at 12 25 56"
src="https://github.com/maplibre/maputnik/assets/649392/e3138a0f-bfca-4949-915d-f9fc3e9bb346">
2024-06-24 08:09:25 +03:00
dependabot[bot]
48bf25c1b0 Bump braces from 3.0.2 to 3.0.3 (#904)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to
3.0.3.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="74b2db2938"><code>74b2db2</code></a>
3.0.3</li>
<li><a
href="88f1429a0f"><code>88f1429</code></a>
update eslint. lint, fix unit tests.</li>
<li><a
href="415d660c30"><code>415d660</code></a>
Snyk js braces 6838727 (<a
href="https://redirect.github.com/micromatch/braces/issues/40">#40</a>)</li>
<li><a
href="190510f79d"><code>190510f</code></a>
fix tests, skip 1 test in test/braces.expand</li>
<li><a
href="716eb9f12d"><code>716eb9f</code></a>
readme bump</li>
<li><a
href="a5851e57f4"><code>a5851e5</code></a>
Merge pull request <a
href="https://redirect.github.com/micromatch/braces/issues/37">#37</a>
from coderaiser/fix/vulnerability</li>
<li><a
href="2092bd1fb1"><code>2092bd1</code></a>
feature: braces: add maxSymbols (<a
href="https://github.com/micromatch/braces/issues/">https://github.com/micromatch/braces/issues/</a>...</li>
<li><a
href="9f5b4cf473"><code>9f5b4cf</code></a>
fix: vulnerability (<a
href="https://security.snyk.io/vuln/SNYK-JS-BRACES-6838727">https://security.snyk.io/vuln/SNYK-JS-BRACES-6838727</a>)</li>
<li><a
href="98414f9f1f"><code>98414f9</code></a>
remove funding file</li>
<li><a
href="665ab5d561"><code>665ab5d</code></a>
update keepEscaping doc (<a
href="https://redirect.github.com/micromatch/braces/issues/27">#27</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/micromatch/braces/compare/3.0.2...3.0.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=braces&package-manager=npm_and_yarn&previous-version=3.0.2&new-version=3.0.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/maplibre/maputnik/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-16 13:08:52 +03:00
Harel M
c82c6158e6 Update Cypress related packages (#903)
Let's see if this adds the relevant information to the CI run details.
CC: @ShellyDCMS
2024-05-29 10:47:14 +03:00
bolo
41cd7dfad1 Update OpenMapTiles styles to the latest version fixes #901 (#902)
Update version numbers of OpenMapTiles styles.

These styles are updated with the latest version (v3.15) of
OpenMapTiles.

Fixes #901
2024-05-20 14:43:00 +03:00
Kevin Schaul
7591b031ce Upgrade Cypress to fix CI errors (#897)
This upgrade should fix the error of Firefox tests failing. See this
issue for more details:
https://github.com/cypress-io/cypress/issues/29190
2024-04-16 08:26:13 +03:00
Kevin Schaul
95b5324fd3 Fix desktop ci artifacts path (#896)
The `working-directory` option does not apply to the artifacts steps, so
we have to specify the full paths
2024-04-12 16:02:53 -05:00
dependabot[bot]
f34529ef06 Bump vite from 5.2.2 to 5.2.6 (#895)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite)
from 5.2.2 to 5.2.6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>create-vite@5.2.3</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/create-vite@5.2.3/packages/create-vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->5.2.6 (2024-03-24)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: <code>fs.deny</code> with globs with directories (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16250">#16250</a>)
(<a href="https://github.com/vitejs/vite/commit/ba5269c">ba5269c</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16250">#16250</a></li>
</ul>
<h2><!-- raw HTML omitted -->5.2.5 (2024-03-24)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: avoid SSR requests in waitForRequestIdle (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16246">#16246</a>)
(<a href="https://github.com/vitejs/vite/commit/7093f77">7093f77</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16246">#16246</a></li>
<li>docs: clarify enforce vs hook.order (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16226">#16226</a>)
(<a href="https://github.com/vitejs/vite/commit/3a73e48">3a73e48</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16226">#16226</a></li>
</ul>
<h2><!-- raw HTML omitted -->5.2.4 (2024-03-23)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: dont resolve imports with malformed URI (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16244">#16244</a>)
(<a href="https://github.com/vitejs/vite/commit/fbf69d5">fbf69d5</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16244">#16244</a></li>
</ul>
<h2><!-- raw HTML omitted -->5.2.3 (2024-03-22)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: handle warmup request error correctly (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16223">#16223</a>)
(<a href="https://github.com/vitejs/vite/commit/d7c5256">d7c5256</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16223">#16223</a></li>
<li>fix: skip encode if is data uri (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16233">#16233</a>)
(<a href="https://github.com/vitejs/vite/commit/8617e76">8617e76</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16233">#16233</a></li>
<li>fix(optimizer): fix <code>optimizeDeps.include</code> glob syntax
for <code>./*</code> exports (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16230">#16230</a>)
(<a href="https://github.com/vitejs/vite/commit/f184c80">f184c80</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16230">#16230</a></li>
<li>fix(runtime): fix sourcemap with <code>prepareStackTrace</code> (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16220">#16220</a>)
(<a href="https://github.com/vitejs/vite/commit/dad7f4f">dad7f4f</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16220">#16220</a></li>
<li>chore: <code>utf8</code> replaced with <code>utf-8</code> (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16232">#16232</a>)
(<a href="https://github.com/vitejs/vite/commit/9800c73">9800c73</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/16232">#16232</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="7369016d8a"><code>7369016</code></a>
release: v5.2.6</li>
<li><a
href="ba5269cca8"><code>ba5269c</code></a>
fix: <code>fs.deny</code> with globs with directories (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16250">#16250</a>)</li>
<li><a
href="7a2791ce17"><code>7a2791c</code></a>
release: v5.2.5</li>
<li><a
href="7093f779b7"><code>7093f77</code></a>
fix: avoid SSR requests in waitForRequestIdle (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16246">#16246</a>)</li>
<li><a
href="3a73e485cd"><code>3a73e48</code></a>
docs: clarify enforce vs hook.order (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16226">#16226</a>)</li>
<li><a
href="6a07243a0e"><code>6a07243</code></a>
release: v5.2.4</li>
<li><a
href="fbf69d5f6c"><code>fbf69d5</code></a>
fix: dont resolve imports with malformed URI (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16244">#16244</a>)</li>
<li><a
href="a67f9f6945"><code>a67f9f6</code></a>
release: v5.2.3</li>
<li><a
href="8617e7638e"><code>8617e76</code></a>
fix: skip encode if is data uri (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16233">#16233</a>)</li>
<li><a
href="f184c8032b"><code>f184c80</code></a>
fix(optimizer): fix <code>optimizeDeps.include</code> glob syntax for
<code>./*</code> exports (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16">#16</a>...</li>
<li>Additional commits viewable in <a
href="https://github.com/vitejs/vite/commits/v5.2.6/packages/vite">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=5.2.2&new-version=5.2.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/maplibre/maputnik/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-03 20:26:34 +03:00
IGUCHI Kanahiro
a73b11805d localIdeographFontFamily: false to load CJK fonts (#893)
close #892 

this changes works well for CJK fonts.

<img width="736" alt="Screenshot 2024-03-25 at 22 04 36"
src="https://github.com/maplibre/maputnik/assets/20744195/adb0295e-5428-4773-b216-8f380e2dcb05">
2024-03-27 01:12:17 +02:00
Harel M
ff15b77b7f Update React to version 18 and other deps (#890)
This PR aims at updating some packages.
I'll keep this in draft until I'll be more happy with the results.
Current setup seems to work, I'll let the CI run and see how bad this
is.
Packages that needs to be updated/replaces:
- [x] ~autocompete - The only warning left in the console is related to
the autocomplete, which probably needs to be updated since it's a
package that wasn't updated in the last 6 years.~ #611
- [x] ~Codemirror is also something that will need an update, but it
looks too complicated at this point in time, so let's see how this
goes.~ #891
- [ ] react-color
- [x] ~react-aria-menubutton~ #846
- [ ] Others?

Most of the changes here are related to types, which is good.
2024-03-21 22:51:29 +02:00
Harel M
355b663e7e Another attempt to fix the inspect issues (#889)
Fixes #871

- #871 

In case of same sources, calling the render method when the map style
changes, but not right away to let the map load the sources should fix
the issue.
2024-03-14 21:02:49 +02:00
Harel M
3c043fd5e0 Replace mapbox-gl-inspect with maplibre-gl-inspect (#888)
This hopefully fixes #871 

- #871 

I still need to update maplibre-gl-inspect to allow this to be fixed.
2024-03-13 22:48:01 +02:00
Harel M
5f54dd0ccf Temp fix using patch-package for maplibre-inspect issue (#885)
This is a temporary fix for:
 - #871

The fix is using `patch-package` to fix the part of the code that is
causing the issue, I believe.
Initial tests shows that this is fixing the issue, I hope it is not
introducing other issues.
A proper fix and also using the maplibre package will be done once a
repo is migrated as can be expected after the following PR is resolved:
- https://github.com/maplibre/maplibre/issues/359
2024-03-09 23:03:23 +02:00
Harel M
3727f5da48 Improve inspect hover UI (#879)
Fixes #868
- #868

It solves all the block within blocks and all kind of controls that are
not required which creates gaps.
I use a simple table, so the width is dynamic but it is always aligned
for all the properties and the features.

![image](https://github.com/maplibre/maputnik/assets/3269297/75138b00-ec7b-4e8d-b51b-f8ff6abcd5cb)

Vary basic stuff.
There's still the layer popup, which I'm not sure I know what it is and
might need to be fixed as well.
CC: @zstadler
2024-02-21 07:17:28 +02:00
Yuri Astrakhan
079c5f67cc Merge entire history from desktop repo to /desktop
Preserve history of the maputnik/desktop repository,
but move related files to `/desktop` directory,
and adjust the ci script to use it.
2024-02-12 13:52:35 -05:00
Yuri Astrakhan
a304d4e060 Renamed ci to ci-desktop 2024-02-12 13:48:37 -05:00
Kevin Schaul
7ac1b03b5a Move into /desktop dir
For eventual merge into maplibre/maputnik repo
2024-02-12 11:06:45 -06:00
glebpinigin
b9e32894b3 Readme.md container port changed to 80 (#881)
Activating maputnik with `docker run` requires to bind port 80 from
container and not 8888, because nginx is running with default config and
listening port 80

For `npm run` port 8888 is explicitly set up [in
config](bc5ecfade6/vite.config.ts (L8)).
2024-02-08 23:35:46 +02:00
Harel M
bc5ecfade6 Removes number conversion for dirty input (#878)
Fixes #870

- #870

This regression bug was introduced as part of the refactoring of
typescript.
Here:
- #848

I've added tests to cover the scenario so that it won't happen again,
hopefully.
2024-02-07 10:32:19 +02:00
HarelM
c84c7a7b96 Fix redo issue 2024-02-04 16:37:47 +02:00
Harel M
cb77c6b4e2 Add nominatim search to maputnik (#873)
This replaces PR:
- #785

Before:

![image](https://github.com/maplibre/maputnik/assets/3269297/95297211-4108-43d8-8a43-42f87e2fbe16)
After:

![image](https://github.com/maplibre/maputnik/assets/3269297/86c3ae58-1bb7-4d2c-8ad9-6b84a21c96f7)

This is based on the geocoder example in maplibre docs:
https://maplibre.org/maplibre-gl-js/docs/examples/geocoder/
2024-02-04 11:37:23 +02:00
Harel M
ea42f434eb Update MapLibre to version 4 (#872)
This updates maplibre from a pre-release to an official release.
It also removed the pre.2 from maputnik as maputnik is now published and
there's no reason to keep it in pre-release version name.
This doesn't mean maputnik has an npm package at this point though.
2024-02-04 10:01:22 +02:00
dependabot[bot]
6f82c12861 Bump vite from 5.0.10 to 5.0.12 (#865)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite)
from 5.0.10 to 5.0.12.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v5.0.12/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->5.0.12 (2024-01-19)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: await <code>configResolved</code> hooks of worker plugins (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15597">#15597</a>)
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15605">#15605</a>)
(<a href="https://github.com/vitejs/vite/commit/ef89f80">ef89f80</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15597">#15597</a>
<a
href="https://redirect.github.com/vitejs/vite/issues/15605">#15605</a></li>
<li>fix: fs deny for case insensitive systems (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15653">#15653</a>)
(<a href="https://github.com/vitejs/vite/commit/91641c4">91641c4</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15653">#15653</a></li>
</ul>
<h2><!-- raw HTML omitted -->5.0.11 (2024-01-05)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: don't pretransform classic script links (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15361">#15361</a>)
(<a href="https://github.com/vitejs/vite/commit/19e3c9a">19e3c9a</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15361">#15361</a></li>
<li>fix: inject <code>__vite__mapDeps</code> code before sourcemap file
comment (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15483">#15483</a>)
(<a href="https://github.com/vitejs/vite/commit/d2aa096">d2aa096</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15483">#15483</a></li>
<li>fix(assets): avoid splitting <code>,</code> inside base64 value of
<code>srcset</code> attribute (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15422">#15422</a>)
(<a href="https://github.com/vitejs/vite/commit/8de7bd2">8de7bd2</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15422">#15422</a></li>
<li>fix(html): handle offset magic-string slice error (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15435">#15435</a>)
(<a href="https://github.com/vitejs/vite/commit/5ea9edb">5ea9edb</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15435">#15435</a></li>
<li>chore(deps): update dependency strip-literal to v2 (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15475">#15475</a>)
(<a href="https://github.com/vitejs/vite/commit/49d21fe">49d21fe</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15475">#15475</a></li>
<li>chore(deps): update tj-actions/changed-files action to v41 (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15476">#15476</a>)
(<a href="https://github.com/vitejs/vite/commit/2a540ee">2a540ee</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15476">#15476</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="ee81e19676"><code>ee81e19</code></a>
release: v5.0.12</li>
<li><a
href="91641c4da0"><code>91641c4</code></a>
fix: fs deny for case insensitive systems (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15653">#15653</a>)</li>
<li><a
href="ef89f8092f"><code>ef89f80</code></a>
fix: await <code>configResolved</code> hooks of worker plugins (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15597">#15597</a>)
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15605">#15605</a>)</li>
<li><a
href="b44c49302f"><code>b44c493</code></a>
release: v5.0.11</li>
<li><a
href="d2aa0969ee"><code>d2aa096</code></a>
fix: inject <code>__vite__mapDeps</code> code before sourcemap file
comment (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15483">#15483</a>)</li>
<li><a
href="2a540eee82"><code>2a540ee</code></a>
chore(deps): update tj-actions/changed-files action to v41 (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15476">#15476</a>)</li>
<li><a
href="5ea9edbc9c"><code>5ea9edb</code></a>
fix(html): handle offset magic-string slice error (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15435">#15435</a>)</li>
<li><a
href="49d21fe1fe"><code>49d21fe</code></a>
chore(deps): update dependency strip-literal to v2 (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15475">#15475</a>)</li>
<li><a
href="8de7bd2b68"><code>8de7bd2</code></a>
fix(assets): avoid splitting <code>,</code> inside base64 value of
<code>srcset</code> attribute (#...</li>
<li><a
href="19e3c9a8a1"><code>19e3c9a</code></a>
fix: don't pretransform classic script links (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15361">#15361</a>)</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v5.0.12/packages/vite">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=5.0.10&new-version=5.0.12)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/maplibre/maputnik/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-04 09:18:19 +02:00
HarelM
3b95b25777 Remove unneed console.log 2024-01-13 14:38:56 +02:00
Harel M
1da65f2116 Fix missing logo (#863)
Fixes #861
This should hopefully address the last item in the below issue of the
missing logo.
I tested it locally and it seems to be working as expected, I hope this
will be the case once deployed.
- #861
2024-01-12 18:46:10 +02:00
Harel M
a62db148cd Remove Storybook, cleanups (#860)
1. Changed references to point to this new repo
2. Fixed docker image publishing to point to ghcr.io.
3. Remove survey link - the survey is closed and there's no point in
keeping it.
4. Remove storybook - Basically a storybook is the ability to look at
components and see how they look and interact with them.
It's a powerful tool for developing component library with "live"
documentation.
But it's an overkill for this project and I would like to reduce
maintenance costs.
Currently all the "stories" are in javascript and not in typescript and
it feels like a waste of time to try and maintain it, along with
updating the storybook library itself and everything around it.
2024-01-12 10:59:57 +02:00
HarelM
6ed10a862f Update pages deploy on every commit to main 2024-01-11 22:57:07 +02:00
HarelM
123e84f19b login to ghcr.io 2024-01-11 22:29:46 +02:00
HarelM
d9b1b6f3ae Fix docker deploy, remove unneeded files 2024-01-11 22:24:47 +02:00
Harel M
e0cef99c07 Update LICENSE 2024-01-11 22:11:18 +02:00
Harel M
eb48bed32a Update MapLibre dependencies, add terrain editing (#859)
This PR aims at updating MapLibre dependencies.

The main goal of this update is to allow adding terrain specification to
the editor.
This requires version 4 of maplibre so currently it will use the
pre-release.
2024-01-11 22:05:47 +02:00
Harel M
87cf81d1c9 Prepare for on boarding (#858)
I believe this solves most of the missing checkboxes:
- https://github.com/maplibre/maplibre/issues/352

Let me know if there's anything else missing.
Other configuration can be done after the repo has been migrated I
believe.
2024-01-11 17:08:42 +02:00
ShellyDCMS
8e35ed97e6 Improve drivers (#856)
Co-authored-by: shelly_goldblit <shelly_goldblit@dell.com>
Co-authored-by: HarelM <harel.mazor@gmail.com>
2024-01-02 12:12:06 +02:00
Harel M
124ae98bf3 E2E: Improve tests, lint, and add more drivers (#855)
This PR introduces lint to cypress code, adds drivers to try and
abstract the usage of cypress as much as possible.
Nothing very interesting, mainly to try out the driver pattern for the
e2e tests.
2023-12-27 20:58:24 +02:00
Harel M
b784bf2b84 Improve tests (#854)
Recover commented out tests, 
Improve usage of helper in driver, 
Added data-wd-key for test to use instead of classes
Moved all non tsx file to a single folder (lib) instead of lib and utils

---------

Co-authored-by: Yuri Astrakhan <yuriastrakhan@gmail.com>
2023-12-27 10:19:59 +02:00
Max
09a1f3f87b Use nginx to serve built files (#808)
Resolves #842
Better suited than python + handles sigterm better

Co-authored-by: Harel M <harel.mazor@gmail.com>
2023-12-26 23:28:17 +02:00
Harel M
a22476cab2 Add lint to CI and fix errors (#853)
Adds lint to CI and fixes errors.
I'm not sure I'm fully proud of all the solutions there.
But there's no lint issues and the lint is being checked as part of the
CI.

---------

Co-authored-by: Yuri Astrakhan <yuriastrakhan@gmail.com>
2023-12-26 23:13:22 +02:00
ShellyDCMS
a324ddb654 add code coverage (#852)
Add Cypress code coverage using Istanbul nyc

![image](https://github.com/maputnik/editor/assets/60476837/e7c364b7-7b54-4bf1-85aa-ee6fb19cc40c)

---------

Co-authored-by: shelly_goldblit <shelly_goldblit@dell.com>
Co-authored-by: Harel M <harel.mazor@gmail.com>
2023-12-26 12:04:43 +02:00
Harel M
656264f2bc Migration of jsx files to tsx 3 (#851)
This is in continue to:
- #850
- #848

The last files should be converted as part of this PR, there are only a
handful left.
2023-12-25 15:48:46 +02:00
Harel M
974dd7bfd9 Migration of jsx files to tsx 2 (#850)
This is to continue the work of migrating all the jsx files into tsx
files.
The MO is basically described here: #848.

About 7 files to go...
2023-12-22 23:32:25 +02:00
Harel M
fa182e66fa Migration of jsx files to tsx 1 (#848)
In this PR I have changed some of the jsx files to tsx file.
I'm starting off with the "leafs" so that migration of the rest will be
easier, hopefully.

What I'm basically doing is taking a jsx file, copy paste it into:
https://mskelton.dev/ratchet

And after that I'm fixing the types as needed.
It's not a very long process.

Hopefully more PRs will follow and this will be over soon.
I don't plan to migrate the storybook as I generally don't understand
why is it useful, I'll open an issue to see if anyone thinks
differently.
2023-12-21 23:46:56 +02:00
Harel M
3bf0e510e6 Migrate all the non react components code to typescript (#847)
This completes the migration to typescript of all the non react
components code.
The only changes introduced besides types are the type checks using
`"something" in object` which narrows down types in typescript.
2023-12-21 00:07:53 +02:00
Harel M
e8d07fa694 Initial migration to typescript (#845)
This migrates some utilities classes from javascript to typescript.
Nothing special about the migration, mainly added types and made sure
the code adheres to the strict mode.

I have picked some small files so git doesn't think they are the same
since they are "very different".
I hope that in later PRs, when migrating lager files this won't be an
issues.
2023-12-20 22:56:33 +02:00
Harel M
4d1e2e6893 Refactor driver for E2E (#843)
This is basically the content of #841 with the code review changes and
relevant fixes to tests/driver code to pass the tests.
CC: @ShellyDCMS

After this we should lint the project and add the lint to the CI to make
sure it doesn't break.

---------

Co-authored-by: ShellyDCMS <60476837+ShellyDCMS@users.noreply.github.com>
Co-authored-by: shelly_goldblit <shelly_goldblit@dell.com>
2023-12-20 16:34:46 +02:00
Harel M
f219ff1e17 Fix favicon (#844)
This PR fixes the missing facicon.
As part of the migration to vite, this issue was introduced.
- #836
2023-12-20 07:18:13 +02:00
Yuri Astrakhan
8eabfa5519 Fix incorrect CI tag filter (#838) 2023-12-18 06:21:43 +02:00
Harel M
ad69cbdb20 Vite + TypeScript (#836)
Resolves #803

This is an initial commit to allow migrating to typescript bit by bit.
It introduces vite.
It removes all the webpack configuration (which I have no clue what all
the profiling are needed for, and couldn't find anything in the readme).
It also changes a single file from javascript to typescript:
`urlopen.js` -> `urlopen.ts`
Which was done manually, later on I'll see if I can automate most of the
migration.
For now, everything seems to work as expected.
I also upgrades storybook to use vite and renames the stories to jsx (I
honestly don't know why this complexity is needed here, but I'll keep it
for now).

cc: @damianstasik
2023-12-17 22:52:19 -05:00
Yuri Astrakhan
17eaa3f204 Rename primary branch to main (#837)
Co-authored-by: Harel M <harel.mazor@gmail.com>
2023-12-18 00:09:10 +02:00
Harel M
1df2e36dbb Replace WebDriverIO with Cypress (#831)
This does the following:
1. Moves the WDIO code from javascript to typescript
2. Moves to use files that are `cy.ts` instead of `index.js`
3. Replace e2e to use cypress
4. Introduce back some skipped tests

This is in continue to the conversation here:
https://github.com/HarelM/editor/pull/3

Before:
```
 "spec" Reporter:
------------------------------------------------------------------
[chrome 120.0.6099.71 linux #0-0] Running: chrome (v120.0.6099.71) on linux
[chrome 120.0.6099.71 linux #0-0] Session ID: ee9a87bcfce007ac7721929c6e6234d0
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0] » /test/functional/index.js
[chrome 120.0.6099.71 linux #0-0] maputnik
[chrome 120.0.6099.71 linux #0-0]     history
[chrome 120.0.6099.71 linux #0-0]        - undo/redo
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]     layers
[chrome 120.0.6099.71 linux #0-0]         ops
[chrome 120.0.6099.71 linux #0-0]            ✓ delete
[chrome 120.0.6099.71 linux #0-0]            ✓ duplicate
[chrome 120.0.6099.71 linux #0-0]            ✓ hide
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]         background
[chrome 120.0.6099.71 linux #0-0]            ✓ add
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]             modify
[chrome 120.0.6099.71 linux #0-0]                 layer
[chrome 120.0.6099.71 linux #0-0]                    - expand/collapse
[chrome 120.0.6099.71 linux #0-0]                    ✓ id
[chrome 120.0.6099.71 linux #0-0]                    ✓ min-zoom
[chrome 120.0.6099.71 linux #0-0]                    ✓ max-zoom
[chrome 120.0.6099.71 linux #0-0]                    ✓ comments
[chrome 120.0.6099.71 linux #0-0]                    - color
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]                 filter
[chrome 120.0.6099.71 linux #0-0]                    - expand/collapse
[chrome 120.0.6099.71 linux #0-0]                    - compound filter
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]                 paint
[chrome 120.0.6099.71 linux #0-0]                    - expand/collapse
[chrome 120.0.6099.71 linux #0-0]                    - color
[chrome 120.0.6099.71 linux #0-0]                    - pattern
[chrome 120.0.6099.71 linux #0-0]                    - opacity
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]                 json-editor
[chrome 120.0.6099.71 linux #0-0]                    - expand/collapse
[chrome 120.0.6099.71 linux #0-0]                    - modify
[chrome 120.0.6099.71 linux #0-0]                    - parse error
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]         fill
[chrome 120.0.6099.71 linux #0-0]            ✓ add
[chrome 120.0.6099.71 linux #0-0]            - change source
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]         line
[chrome 120.0.6099.71 linux #0-0]            ✓ add
[chrome 120.0.6099.71 linux #0-0]            - groups
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]         symbol
[chrome 120.0.6099.71 linux #0-0]            ✓ add
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]         raster
[chrome 120.0.6099.71 linux #0-0]            ✓ add
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]         circle
[chrome 120.0.6099.71 linux #0-0]            ✓ add
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]         fill extrusion
[chrome 120.0.6099.71 linux #0-0]            ✓ add
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]         groups
[chrome 120.0.6099.71 linux #0-0]            ✓ simple
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]     map
[chrome 120.0.6099.71 linux #0-0]         zoom level
[chrome 120.0.6099.71 linux #0-0]            - via url
[chrome 120.0.6099.71 linux #0-0]            - via map controls
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]     modals
[chrome 120.0.6099.71 linux #0-0]         open
[chrome 120.0.6099.71 linux #0-0]            ✓ close
[chrome 120.0.6099.71 linux #0-0]            - upload
[chrome 120.0.6099.71 linux #0-0]            ✓ load from url
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]         shortcuts
[chrome 120.0.6099.71 linux #0-0]            ✓ open/close
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]         export
[chrome 120.0.6099.71 linux #0-0]            ✓ close
[chrome 120.0.6099.71 linux #0-0]            - download
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]         sources
[chrome 120.0.6099.71 linux #0-0]            - active sources
[chrome 120.0.6099.71 linux #0-0]            - public source
[chrome 120.0.6099.71 linux #0-0]            - add new source
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]         inspect
[chrome 120.0.6099.71 linux #0-0]            ✓ toggle
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]         style settings
[chrome 120.0.6099.71 linux #0-0]            ✓ name
[chrome 120.0.6099.71 linux #0-0]            ✓ owner
[chrome 120.0.6099.71 linux #0-0]            ✓ sprite url
[chrome 120.0.6099.71 linux #0-0]            ✓ glyphs url
[chrome 120.0.6099.71 linux #0-0]            ✓ maptiler access token
[chrome 120.0.6099.71 linux #0-0]            ✓ thunderforest access token
[chrome 120.0.6099.71 linux #0-0]            ✓ style renderer
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]         sources
[chrome 120.0.6099.71 linux #0-0]            - toggle
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]     screenshots
[chrome 120.0.6099.71 linux #0-0]        ✓ front_page
[chrome 120.0.6099.71 linux #0-0]        ✓ open
[chrome 120.0.6099.71 linux #0-0]        ✓ export
[chrome 120.0.6099.71 linux #0-0]        ✓ sources
[chrome 120.0.6099.71 linux #0-0]        ✓ style settings
[chrome 120.0.6099.71 linux #0-0]        ✓ inspect
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]     accessibility
[chrome 120.0.6099.71 linux #0-0]         skip links
[chrome 120.0.6099.71 linux #0-0]            ✓ skip link to layer list
[chrome 120.0.6099.71 linux #0-0]            ✓ skip link to layer editor
[chrome 120.0.6099.71 linux #0-0]            ✓ skip link to map view
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0]     keyboard
[chrome 120.0.6099.71 linux #0-0]         shortcuts
[chrome 120.0.6099.71 linux #0-0]            ✓ ESC should unfocus
[chrome 120.0.6099.71 linux #0-0]            ✓ '?' should show shortcuts modal
[chrome 120.0.6099.71 linux #0-0]            ✓ 'o' should show open modal
[chrome 120.0.6099.71 linux #0-0]            ✓ 'e' should show export modal
[chrome 120.0.6099.71 linux #0-0]            ✓ 'd' should show sources modal
[chrome 120.0.6099.71 linux #0-0]            ✓ 's' should show settings modal
[chrome 120.0.6099.71 linux #0-0]            - 'i' should change map to inspect mode
[chrome 120.0.6099.71 linux #0-0]            ✓ 'm' should focus map
[chrome 120.0.6099.71 linux #0-0]            ✓ '!' should show debug modal
[chrome 120.0.6099.71 linux #0-0]
[chrome 120.0.6099.71 linux #0-0] 44 passing (58.8s)
[chrome 120.0.6099.71 linux #0-0] 23 skipped
```

After:
```
accessibility
    skip links
      - skip link to layer list
      - skip link to layer editor
      - skip link to map view

history
    ✓ undo/redo (4894ms)

keyboard
    shortcuts
      ✓ ESC should unfocus (1912ms)
      ✓ '?' should show shortcuts modal (458ms)
      ✓ 'o' should show open modal (710ms)
      ✓ 'e' should show export modal (692ms)
      ✓ 'd' should show sources modal (588ms)
      ✓ 's' should show settings modal (894ms)
      ✓ 'i' should change map to inspect mode (804ms)
      ✓ 'm' should focus map (837ms)
      ✓ '!' should show debug modal (607ms)

layers
    ops
      ✓ delete (4313ms)
      ✓ duplicate (1780ms)
      ✓ hide (1862ms)
    background
      ✓ add (1675ms)
      modify
        layer
          - expand/collapse
          ✓ id (3735ms)
          ✓ min-zoom (2209ms)
          ✓ max-zoom (2127ms)
          ✓ comments (2515ms)
          ✓ color (2022ms)
        filter
          - expand/collapse
          - compound filter
        paint
          - expand/collapse
          - color
          - pattern
          - opacity
        json-editor
          - expand/collapse
          - modify
          - parse error
    fill
      ✓ add (1831ms)
      - change source
    line
      ✓ add (1844ms)
      ✓ groups (687ms)
    symbol
      ✓ add (2035ms)
    raster
      ✓ add (1814ms)
    circle
      ✓ add (1867ms)
    fill extrusion
      ✓ add (1963ms)
    groups
      ✓ simple (2653ms)

map
    zoom level
      ✓ via url (2279ms)
      ✓ via map controls (733ms)

modals
    open
      ✓ close (2519ms)
      - upload
      ✓ load from url (1557ms)
    shortcuts
      ✓ open/close (1136ms)
    export
      ✓ close (755ms)
      - download
    sources
      - active sources
      - public source
      - add new source
    inspect
      ✓ toggle (1020ms)
    style settings
      ✓ name (1085ms)
      ✓ owner (1060ms)
      ✓ sprite url (1214ms)
      ✓ glyphs url (1553ms)
      ✓ maptiler access token (1111ms)
      ✓ thunderforest access token (1102ms)
      ✓ style renderer (922ms)
    sources
      - toggle



      Spec                                              Tests  Passing  Failing  Pending  Skipped  
  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ ✔  accessibility.cy.ts                       52ms        3        -        -        3        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  history.cy.ts                            00:06        1        1        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  keyboard.cy.ts                           00:10        9        9        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  layers.cy.ts                             00:39       28       17        -       11        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  map.cy.ts                                00:04        2        2        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  modals.cy.ts                             00:16       18       12        -        6        - │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘
    ✔  All specs passed!                        01:18       61       41        -       20        -  
```

---------

Co-authored-by: Yuri Astrakhan <yuriastrakhan@gmail.com>
2023-12-17 21:49:23 +02:00
Birk Skyum
0c46affda9 Add dependabot (#826)
It has a low limit on how many PRs it's allowed to open at a time, so it
should not flood the repo.
2023-12-17 01:16:47 -05:00
Harel M
18f45e932b Use driver pattern for e2e tests (#829)
I've moved all the logic relevant to WBIO into a single file in order to
be able to replace it.
I have tried to upgrade WDIO in order to use the latest version and it
got stuck on my computer.
Furthermore, I was not able to run it locally which made this whole
cycle very long.
After this will be merged I will replace WDIO with cypress.
This doesn't change anything, only moves some code in the tests to a
single file, removes unneeded files and uses the driver pattern, which
will later allow switching the underline WDIO with Cypress.

cc: @nyurik
2023-12-17 01:14:23 -05:00
Yuri Astrakhan
31d56c9fae Bump CI versions (#830) 2023-12-17 00:49:34 -05:00
Birk Skyum
b7838ad6e1 Use latest firefox (#825) 2023-10-23 08:06:02 +02:00
Birk Skyum
c92fd12854 Bump to node 18 (#824) 2023-10-22 22:11:06 +02:00
Birk Skyum
eb55796461 Merge pull request #823 from birkskyum/new-webdriver
Chrome new headless
2023-10-22 20:20:26 +02:00
Yuri Astrakhan
4b97f82980 Revert "Revert "Resolve #821 (#822)""
This reverts commit e4d559f953.
2023-10-21 18:33:48 -04:00
Yuri Astrakhan
5d0b6e3201 Revert "Revert "Added checkbox for cluster attribute (#810)""
This reverts commit 52f949e152.
2023-10-21 18:33:46 -04:00
Birk Skyum
5ab9be9fdb update node version 2023-10-22 00:27:41 +02:00
Birk Skyum
9659d41b83 chrome new headless 2023-10-22 00:20:55 +02:00
Yuri Astrakhan
52f949e152 Revert "Added checkbox for cluster attribute (#810)"
This reverts commit 393f4a38b9.
2023-10-21 18:19:24 -04:00
Yuri Astrakhan
e4d559f953 Revert "Resolve #821 (#822)"
This reverts commit 577663f706.
2023-10-21 18:19:21 -04:00
zstadler
577663f706 Resolve #821 (#822)
Please see code analysis at
https://github.com/maputnik/editor/issues/821#issuecomment-1772360990
2023-10-20 11:01:20 -04:00
Eliav-B
393f4a38b9 Added checkbox for cluster attribute (#810)
* Added additional checkbox to set the cluster attribute when adding a GeoJson source

* Fixed default source function in the 'geojson_json' case

---------

Co-authored-by: shimon <ruisuhsh100@gmail.com>
2023-10-20 10:54:40 -04:00
Kevin Schaul
3727c9ad5e 814/remove mapbox references (#816)
Fixes #814

* fix: remove outdated references to mapbox
* docs: fix references in readme
* chore: fix mapbox references in tests
* chore: fix mapbox references in stories, webpack config
* chore: remove empty array
2023-08-28 22:17:49 -04:00
Kevin Schaul
7265bf0aa4 Refactor into a go module, update dependencies (#18)
* Refactor into a go module

`go get` is no longer supported outside of a go module. This commit
converts the project into a go module, moving dependencies into a go.mod
file.

* Use go.rice instead of go-bindata-assetfs

go.rice appears to be maintained and more popular

* Edit dependencies

* Pin to specific versions

* Updating workflows

* Fix syntax, I hope

* Update go

* Increment version number

* Version number

* version number
2023-07-18 16:44:57 +02:00
pathmapper
c9504fcaed Merge pull request #16 from pathmapper/static
Add option to serve directory under /static/ (supersedes #2)
2020-07-24 18:31:30 +02:00
pathmapper
b7ef0943f4 Fix formatting 2020-07-23 20:25:24 +02:00
pathmapper
4661677387 Add docs 2020-07-23 20:19:33 +02:00
pathmapper
77ed14a340 Update desktop version number 2020-07-23 19:52:38 +02:00
pathmapper
e24d390f7c Fix build error 2020-07-23 19:33:23 +02:00
pathmapper
698fdfc958 Fix syntax error 2020-07-23 19:28:34 +02:00
pathmapper
77b3655c3c Add option to serve directory under /static/ (from #2) 2020-07-23 19:22:43 +02:00
pathmapper
c264cd1771 Merge pull request #15 from pathmapper/update_desktop_version
Update desktop version number
2020-06-03 15:08:54 +02:00
pathmapper
1495d11462 Update desktop version number
The makefile was updated in https://github.com/maputnik/desktop/pull/14
2020-06-03 15:05:47 +02:00
pathmapper
fda11e52e7 Merge pull request #14 from pathmapper/makefile_editor
Update for use in the editor ci workflow
2020-05-31 21:43:57 +02:00
pathmapper
d9e3aa6ac4 Update for use in the editor ci workflow 2020-05-31 21:41:02 +02:00
pathmapper
aeca95a27f Merge pull request #13 from pathmapper/rename_ci
Rename ci
2020-05-31 21:30:22 +02:00
pathmapper
7dfcdac202 Rename ci 2020-05-31 21:23:52 +02:00
pathmapper
4f156ee3fd Merge pull request #12 from pathmapper/gh-actions
Replace Travis with Github Actions
2020-05-31 21:14:39 +02:00
pathmapper
6d00214f55 Update README.md 2020-05-31 21:09:02 +02:00
pathmapper
1e7b6e809c Remove Travis 2020-05-31 21:05:00 +02:00
pathmapper
cdcc61e234 Create go.yml 2020-05-31 21:04:11 +02:00
pathmapper
9c63172d36 Merge pull request #11 from pathmapper/travis
Travis: Update go version and use OSM Liberty
2020-05-28 23:00:06 +02:00
pathmapper
6f21fd8dff Use OSM Liberty style 2020-05-28 22:56:08 +02:00
pathmapper
0e788c5841 Update go version 2020-05-28 22:55:39 +02:00
pathmapper
7229df704a Merge pull request #10 from pathmapper/1.7.0
Update Editor to v1.7.0
2020-05-28 22:08:56 +02:00
pathmapper
686fd27b35 Update Editor to v1.7.0 2020-05-28 22:05:48 +02:00
pathmapper
293342e4fb Update Desktop version number 2020-05-28 22:04:40 +02:00
pathmapper
03d9b946e7 Merge pull request #9 from miya0001/patch
Fixed the error `cannot use cli.StringFlag literal ...`
2020-05-28 21:54:55 +02:00
Takayuki Miyauchi
4a3825fa89 fixed the error cannot use cli.StringFlag literal ... 2020-02-21 07:24:00 +09:00
pathmapper
5371b0f9fb Merge pull request #8 from pathmapper/editor_1.6.1
Update editor to v1.6.1
2019-10-13 17:50:22 +02:00
pathmapper
538cea7f45 Update editor to v1.6.1 2019-10-13 14:28:42 +02:00
pathmapper
fdfc470ccc Merge pull request #6 from pathmapper/update_versions
Update version numbers
2019-10-10 14:30:14 +02:00
pathmapper
3153eea1da Update version numbers 2019-10-08 22:29:45 +02:00
pathmapper
8471d0af3d Merge pull request #5 from JesseCrocker/choose-listen-port
Add option to choose a listening port
2019-10-08 22:20:05 +02:00
Jesse Crocker
1ce2d59b9b Add --port option to readme 2019-09-04 09:21:56 -06:00
Jesse Crocker
d951256b1c Add option to choose a listening port 2019-04-15 12:29:15 -06:00
pathmapper
ec753869d5 Merge pull request #3 from maputnik/update-release-to-v1.5.0
Update release to v1.5.0
2019-02-09 14:07:52 +01:00
Lukas Welte
2e58be1c90 Switch around editor and desktop in version field so that it prints nicer 2019-02-09 13:47:58 +01:00
Lukas Welte
562a4f7322 Remove appveyor status from README 2019-02-09 13:45:45 +01:00
Lukas Welte
e52a63e1dd Remove the deprecation warning from the readme 2019-02-09 12:47:47 +01:00
Lukas Welte
4533fd06ed Have Desktop version (changes in Desktop code) and editor version in version field 2019-02-09 12:39:32 +01:00
pathmapper
b4fc62632c Merge pull request #4 from pathmapper/pathmapper-update-readme
Update README.md
2019-02-09 10:28:15 +01:00
pathmapper
e3a9a8a38c Update README.md 2019-02-09 10:09:05 +01:00
pathmapper
7c4e982fb3 Merge pull request #1 from fawick/master
Fix README.md
2019-02-09 09:51:13 +01:00
Lukas Welte
85dd22b09a Use recent go version 2019-02-07 17:58:11 +01:00
Lukas Welte
18e15eeb5c remove complete bin folder in make clean 2019-02-07 17:35:23 +01:00
Lukas Welte
3a45b8dd41 Move dependencies into Makefile 2019-02-07 17:33:38 +01:00
Lukas Welte
5b8412765b Linux should be enough to build go for linux, windows and mac 2019-02-07 17:29:49 +01:00
Lukas Welte
69519df82f Use folders for different maputnik executables 2019-02-07 17:25:27 +01:00
Lukas Welte
8052701021 Use go cross compile to build once with travis for all platforms 2019-02-07 17:18:48 +01:00
Lukas Welte
35c0150522 Download release instead of building editor from source 2019-02-07 16:05:02 +01:00
Lukas Welte
c55278e7da Remove editor submodule 2019-02-07 15:56:29 +01:00
Lukas Martinelli
d3ecef3de6 Update README.md 2018-04-15 16:14:22 +05:30
Fabian Wickborn
3ef0a90de4 Fix README.md 2017-10-25 20:33:45 +02:00
Lukas Martinelli
87290889fd Upgrade to v1.0.2 2017-01-25 13:57:27 +01:00
Lukas Martinelli
1997e31b6b Build latest 2017-01-18 13:06:43 +01:00
Lukas Martinelli
5b21a2fa4f Build latest 2017-01-18 13:03:50 +01:00
Lukas Martinelli
d314add6a9 Release v1.0.1 of CLI 2017-01-16 10:18:27 +01:00
Lukas Martinelli
50d61cdb0e Move to v1.0.0 2017-01-13 21:56:06 +01:00
Lukas Martinelli
4ffea21c5f Upgrade to latest Maputnik 2017-01-10 14:57:13 +01:00
Lukas Martinelli
d29a79e79f Update editor 2017-01-08 17:15:57 +01:00
Lukas Martinelli
004177a3c8 Kill watch ocmmand after 5s 2017-01-08 13:40:38 +01:00
Lukas Martinelli
b9e70a943f Update x/sys 2017-01-08 13:29:20 +01:00
Lukas Martinelli
de853eb2d7 Use Go 1.7 explicitely in Travis 2017-01-08 13:29:20 +01:00
Lukas Martinelli
f242c2c015 Update README.md 2017-01-08 10:45:01 +01:00
Lukas Martinelli
d895cf079c Execute maputnik in travis 2017-01-08 10:39:12 +01:00
Lukas Martinelli
147cbc1580 Include subdirectories in embedded assets 2017-01-01 21:12:24 +01:00
Lukas Martinelli
3e46c0d3ba Set version, badge and download instruction 2017-01-01 16:49:18 +01:00
Lukas Martinelli
d233e0a14d Create public dir 2017-01-01 16:33:29 +01:00
Lukas Martinelli
a73b2fd7e1 Use nvm for travis 2017-01-01 16:24:36 +01:00
Lukas Martinelli
69f63f2844 Use latest node in desktop build 2017-01-01 16:22:01 +01:00
Lukas Martinelli
10136c07db Publish artifacts 2017-01-01 16:16:18 +01:00
Lukas Martinelli
5e3156ab21 Fetch submodule in appveyor 2017-01-01 16:00:59 +01:00
Lukas Martinelli
6be8959951 Update to latest master 2017-01-01 15:52:10 +01:00
Lukas Martinelli
d2cd84de2b Switch filewatch implementation to fsnotify 2017-01-01 15:50:58 +01:00
Lukas Martinelli
60bea1777a Ugrade to v0.3.1 2016-12-31 16:12:07 +01:00
Lukas Martinelli
ce9216b2d5 Write and read file directly 2016-12-03 23:04:32 +01:00
Lukas Martinelli
35ed202cd0 Support CORS 2016-12-03 23:04:12 +01:00
Lukas Martinelli
0d77518a02 Support saving file 2016-12-03 16:51:47 +01:00
lukasmartinelli
11375008fa Extend README with endpoint desription 2016-11-05 17:40:19 +01:00
lukasmartinelli
009f4e105d Specify path to mingw make 2016-11-03 13:29:14 +01:00
lukasmartinelli
b1af4917e5 Add missing bindata deps 2016-11-03 13:26:14 +01:00
lukasmartinelli
5b712d74ae Use MinGW make in AppVeyor 2016-11-03 13:24:02 +01:00
lukasmartinelli
ca2df37c79 Fix go get in AppVeyor 2016-11-03 13:19:58 +01:00
lukasmartinelli
7d1890156d Update Travis go get 2016-11-03 13:18:08 +01:00
lukasmartinelli
ecab640a9a Add AppVeyor integration 2016-11-02 17:10:40 +01:00
lukasmartinelli
f8cb0619f3 Use HTTPS endpoint for submodule 2016-11-02 17:08:48 +01:00
lukasmartinelli
d874b2503b Add Travis file 2016-11-02 17:01:46 +01:00
lukasmartinelli
3d09f2a0f3 Add server usage example 2016-09-27 17:31:14 +02:00
lukasmartinelli
1f1580276d Integrate file access into API 2016-09-27 14:28:40 +02:00
lukasmartinelli
0421a7f099 Add websocket filewatcher 2016-09-27 11:45:27 +02:00
lukasmartinelli
8b722fc967 Add Makefile 2016-09-27 10:59:56 +02:00
lukasmartinelli
66e3ce8743 Add server and bindata packaging 2016-09-27 10:27:47 +02:00
Lukas Martinelli
45eb3a01e6 Initial commit 2016-09-27 10:08:08 +02:00
320 changed files with 14090 additions and 50492 deletions

View File

@@ -1,21 +0,0 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"static-fs",
"react-hot-loader/babel",
"@babel/plugin-proposal-class-properties",
"@babel/transform-runtime"
],
"env": {
"test": {
"plugins": [
["istanbul", {
"exclude": ["node_modules/**", "test/**"]
}]
]
}
}
}

View File

@@ -1,4 +0,0 @@
{
"packages": [],
"sandboxes": ["/"]
}

46
.eslintrc Normal file
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"
}
}

3
.github/FUNDING.yml vendored
View File

@@ -1 +1,2 @@
custom: "https://maputnik.github.io/donate"
github: [maplibre]
open_collective: maplibre

View File

@@ -9,7 +9,7 @@ assignees: ''
<!-- Thanks for your feedback! Please complete the following information: -->
**Maputnik version**:<!-- e.g v1.7.0, master -->
**Maputnik version**:<!-- e.g v1.7.0, main -->
**Browser**:
**OS**:<!-- (Windows, macOS, Linux) -->

11
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,11 @@
## 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.

14
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 2
versioning-strategy: increase
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 2
versioning-strategy: increase

View File

@@ -2,44 +2,24 @@ name: ci
on:
pull_request:
branches: [ master ]
branches: [ main ]
push:
branches: [ master ]
branches: [ main ]
jobs:
# post a comment linking to codesandbox with the current branch
# meta-demo-comment:
# name: meta/demo-comment
# runs-on: ubuntu-latest
# if: ${{ github.event_name == 'pull_request' }}
# steps:
# - uses: unsplash/comment-on-pr@v1.2.0
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# msg: "Demo: <https://codesandbox.io/embed/github/${{ github.repository }}/tree/${{ github.head_ref }}?view=preview>"
build-docker:
name: build/docker
runs-on: ${{ matrix.os }}
name: build docker
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v2
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:master .
- uses: actions/checkout@v4
- run: docker build -t test-docker-image-build .
# build the editor
build-node:
name: "build/node@${{ matrix.node-version }} (${{ matrix.os }})"
name: "build on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
@@ -48,146 +28,86 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [16.x]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
node-version-file: '.nvmrc'
- run: npm ci
- run: npm run build
- run: npm run lint
- run: npm run lint-css
build-artifacts:
name: "build/artifacts (${{ matrix.os }})"
runs-on: ${{ matrix.os }}
name: "build artifacts"
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
node-version: [16.x]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
node-version-file: '.nvmrc'
- run: npm ci
- run: npm run build
- run: npm run build-storybook
- name: artifacts/editor
uses: actions/upload-artifact@v1
- name: artifacts/maputnik
uses: actions/upload-artifact@v4
with:
name: editor
path: build/build
- run: npm run profiling-build
- name: artifacts/editor-profiling
uses: actions/upload-artifact@v1
with:
name: editor-profiling
path: build/profiling
- name: artifacts/storybook
uses: actions/upload-artifact@v1
with:
name: storybook
path: build/storybook
name: maputnik
path: dist
# Build and upload desktop CLI artifacts
- name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: ^1.19.x
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
with:
repository: maputnik/desktop
ref: master
path: ./src/github.com/maputnik/desktop/
- name: Make
run: cd src/github.com/maputnik/desktop/ && make
- name: Build desktop artifacts
run: npm run build-desktop
- name: Artifacts/linux
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v4
with:
name: maputnik-linux
path: ./src/github.com/maputnik/desktop/bin/linux/
path: ./desktop/bin/linux/
- name: Artifacts/darwin
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v4
with:
name: maputnik-darwin
path: ./src/github.com/maputnik/desktop/bin/darwin/
path: ./desktop/bin/darwin/
- name: Artifacts/windows
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v4
with:
name: maputnik-windows
path: ./src/github.com/maputnik/desktop/bin/windows/
# build and test the editor
test_selenium_standalone:
name: "test/standalone-${{ matrix.browser }} (${{ matrix.os }})"
runs-on: ${{ matrix.os }}
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
path: ./desktop/bin/windows/
e2e-tests:
name: "E2E tests using ${{ matrix.browser }}"
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
node-version: [16]
browser: [chrome, firefox]
container:
image: node:${{ matrix.node-version }}
options: --network-alias testhost
services:
selenium:
# geckodriver-0.31 seems to have problems as of 2022 May 1
image: selenium/standalone-${{ matrix.browser == 'firefox' && 'firefox:99.0-geckodriver-0.30-20220427' || matrix.browser }}
ports:
- 4444:4444
options: --shm-size=2gb
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm ci
- run: BROWSER=${{ matrix.browser }} TEST_NETWORK=testhost DOCKER_HOST=selenium npm run test
- if: ${{ matrix.browser == 'chrome' }}
run: ./node_modules/.bin/istanbul report --include build/coverage/coverage.json --dir build/coverage html lcov
- if: ${{ matrix.browser == 'chrome' }}
name: artifacts/coverage
uses: actions/upload-artifact@v1
with:
name: coverage
path: build/coverage
- name: artifacts/screenshots
uses: actions/upload-artifact@v1
with:
name: screenshots-${{ matrix.browser }}
path: build/screenshots
- name: Checkout
uses: actions/checkout@v4
- run: npm ci
- name: Cypress run
uses: cypress-io/github-action@v6
with:
build: npm run build
start: npm run start
browser: ${{ matrix.browser }}
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
files: ${{ github.workspace }}/.nyc_output/out.json
verbose: true

View File

@@ -0,0 +1,39 @@
name: Create bump version PR
on:
workflow_dispatch:
inputs:
version:
description: Version to change to.
required: true
type: string
jobs:
bump-version-pr:
name: Bump version PR
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Bump version
run: |
npm version --commit-hooks false --git-tag-version false ${{ inputs.version }}
./build/bump-version-changelog.js ${{ inputs.version }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
commit-message: Bump version to ${{ inputs.version }}
branch: bump-version-to-${{ inputs.version }}
title: Bump version to ${{ inputs.version }}

View File

@@ -2,27 +2,50 @@ name: deploy
on:
push:
branches: [ master ]
push:
tags:
- 'v*'
branches: [ main ]
jobs:
# publish docker to github registry
deploy-pages:
name: deploy/pages
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' }}
steps:
- uses: actions/checkout@v4
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Install
run: npm ci
- name: Build
run: npm run build
- name: Upload to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: dist
# publish docker to GitHub registry
deploy-docker:
name: deploy/docker
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v2
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u orangemug --password-stdin
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:master .
- run: docker push docker.pkg.github.com/maputnik/editor/editor:master
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v4
- run: docker build -t ghcr.io/maplibre/maputnik:main .
- run: docker push ghcr.io/maplibre/maputnik:main

104
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,104 @@
name: Release
on:
push:
branches: [main]
workflow_dispatch:
jobs:
release-check:
name: Check if version changed
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Check if version has been updated
id: check
uses: EndBug/version-check@v2
outputs:
publish: ${{ steps.check.outputs.changed }}
release:
name: Release
needs: release-check
if: ${{ needs.release-check.outputs.publish == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
registry-url: "https://registry.npmjs.org"
- name: Set up Go for desktop build
uses: actions/setup-go@v5
with:
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
id: go
- name: Get version
id: package-version
uses: martinbeentjes/npm-get-version-action@v1.3.1
- name: Install
run: npm ci
- name: Build
run: |
npm run build
npm run build-desktop
- name: Tag commit and push
id: tag_version
uses: mathieudutour/github-tag-action@v6.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
custom_tag: ${{ steps.package-version.outputs.current-version }}
- name: Create Archives
run: |
zip -r dist dist
zip -r desktop desktop/bin/
- name: Build Release Notes
id: release_notes
run: |
RELEASE_NOTES_PATH="${PWD}/release_notes.txt"
./build/release-notes.js > ${RELEASE_NOTES_PATH}
echo "release_notes=${RELEASE_NOTES_PATH}" >> $GITHUB_OUTPUT
- name: Create GitHub Release
id: create_regular_release
uses: ncipollo/release-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag: ${{ steps.tag_version.outputs.new_tag }}
name: ${{ steps.tag_version.outputs.new_tag }}
bodyFile: ${{ steps.release_notes.outputs.release_notes }}
artifacts: "dist.zip,desktop.zip"
allowUpdates: true
draft: false
prerelease: false

5
.gitignore vendored
View File

@@ -14,6 +14,7 @@ lib-cov
# Coverage directory used by tools like istanbul
coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
@@ -32,4 +33,6 @@ node_modules
public
/errorShots
/old
/build
/cypress/screenshots
/dist/
/desktop/version.go

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
legacy-peer-deps = true

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
18.19

18
.nycrc.json Normal file
View File

@@ -0,0 +1,18 @@
{
"all": true,
"extends": "@istanbuljs/nyc-config-typescript",
"check-coverage": false,
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": [
"cypress/**/*.*",
"**/*.d.ts",
"**/*.cy.tsx",
"**/*.cy.ts",
"./coverage/**",
"./cypress/**",
"./dist/**",
"node_modules"
],
"report-dir": "coverage",
"reporter": ["json", "lcov", "json-summary"]
}

View File

@@ -1,24 +0,0 @@
const rules = require('../config/webpack.rules');
module.exports = {
stories: ['../stories/**/*.stories.js'],
addons: [
'@storybook/addon-actions',
'@storybook/addon-links',
'@storybook/addon-a11y/register',
'@storybook/addon-storysource',
],
webpackFinal: async config => {
// do mutation to the config
console.log("config.module", config.module);
return {
...config,
module: {
rules: [
...rules,
]
}
};
},
};

View File

@@ -1,7 +0,0 @@
import { addons } from '@storybook/addons';
import { themes } from '@storybook/theming';
import theme from './maputnik.theme';
addons.setConfig({
theme: theme,
});

View File

@@ -1,8 +0,0 @@
import { create } from '@storybook/theming/create';
export default create({
base: 'light',
brandTitle: 'Maputnik',
brandUrl: 'https://github.com/maputnik/editor',
});

24
CHANGELOG.md Normal file
View File

@@ -0,0 +1,24 @@
## main
### ✨ Features and improvements
- _...Add new stuff here..._
### 🐞 Bug fixes
- _...Add new stuff here..._
## 2.1.0
### ✨ Features and improvements
- Add GitHub workflows for releasing new versions
- Update desktop build to pull from this repo (#922)
## 2.0.0
- Update MapLibre to version 4 (#872)
- Start continuous deployment of maputnik website
## 1.7.0
- See release notes at https://maputnik.github.io/blog/2020/04/23/release-v1.7.0

2
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,2 @@
# Contributor Covenant
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/maplibre/maplibre/blob/main/CODE_OF_CONDUCT.md)

View File

@@ -1,22 +1,16 @@
FROM node:10 as builder
FROM node:18 as builder
WORKDIR /maputnik
# Only copy package.json to prevent npm install from running on every build
COPY package.json package-lock.json ./
RUN npm install
COPY package.json package-lock.json .npmrc ./
RUN npm ci
# Build maputnik
# TODO: we should also do a npm run test here (needs more dependencies)
COPY . .
RUN npm run build
RUN npx vite build
#---------------------------------------------------------------------------
# Create a clean nginx-alpine slim image with just the build results
FROM nginx:alpine-slim
# Create a clean python-based image with just the build results
FROM python:3-slim
WORKDIR /maputnik
COPY --from=builder /maputnik/build/build .
EXPOSE 8888
CMD python -m http.server 8888
COPY --from=builder /maputnik/dist /usr/share/nginx/html/

View File

@@ -1,6 +1,7 @@
The MIT License (MIT)
Copyright (c) 2015 Lukas Martinelli
Copyright (c) 2024 MapLibre contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

118
README.md
View File

@@ -1,45 +1,44 @@
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
# Maputnik
[![GitHub CI status](https://github.com/maputnik/editor/workflows/ci/badge.svg)][github-action-ci]
[![GitHub CI status](https://github.com/maplibre/maputnik/workflows/ci/badge.svg)][github-action-ci]
[![License](https://img.shields.io/badge/license-MIT-blue.svg)][license]
[github-action-ci]: https://github.com/maputnik/editor/actions?query=workflow%3Aci
[github-action-ci]: https://github.com/maplibre/maputnik/actions?query=workflow%3Aci
[license]: https://tldrlegal.com/license/mit-license
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
targeted at developers and map designers.
## Usage
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
- :link: Design your maps online at **<https://www.maplibre.org/maputnik/>** (all in local storage)
- :link: Use the [Maputnik CLI](https://github.com/maplibre/maputnik/wiki/Maputnik-CLI) for local style development
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
```bash
docker run -it --rm -p 8888:8888 maputnik/editor
docker run -it --rm -p 8888:80 ghcr.io/maplibre/maputnik:main
```
## Donations
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer.
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
## Documentation
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/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!
- :link: **Study the [Maputnik Wiki](https://github.com/maputnik/editor/wiki)**
- :link: **Study the [Maputnik Wiki](https://github.com/maplibre/maputnik/wiki)**
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
[![Design Map from Scratch](https://j.gifs.com/g5XMgl.gif)](https://youtu.be/XoDh0gEnBQo)
## Develop
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
Maputnik is written in typescript and is using [React](https://github.com/facebook/react) and [MapLibre GL JS](https://maplibre.org/projects/maplibre-gl-js/).
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
### Getting Involved
Join the #maplibre or #maputnik slack channel at OSMUS: get an invite at https://slack.openstreetmap.us/ Read the the below guide in order to get familiar with how we do things around here.
Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
```bash
@@ -49,18 +48,14 @@ npm install
npm run start
```
If you want Maputnik to be accessible externally use the [`--host` option](https://webpack.js.org/configuration/dev-server/#devserverhost):
If you want Maputnik to be accessible externally use the [`--host` option](https://vitejs.dev/config/server-options.html#server-host):
```bash
# start externally accessible dev server
npm run start -- --host 0.0.0.0
```
The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the [webpack-dev-server docs](https://webpack.js.org/configuration/dev-server/):
> webpack uses the file system to get notified of file changes. In some cases this does not work. For example, when using Network File System (NFS). Vagrant also has a lot of problems with this. ([snippet source](https://webpack.js.org/configuration/dev-server/#devserverwatchoptions-))
To enable polling add `export WEBPACK_DEV_SERVER_POLLING=1` to your environment.
The build process will watch for changes to the filesystem, rebuild and autoreload the editor.
```
npm run build
@@ -71,14 +66,15 @@ Lint the JavaScript code.
```
# run linter
npm run lint
npm run lint-styles
npm run lint-css
npm run sort-styles
```
## Tests
For testing we use [webdriverio](https://webdriver.io) and [selenium-standalone](https://github.com/webdriverio/selenium-standalone).
For E2E testing we use [Cypress](https://www.cypress.io/)
[selenium-standalone](https://github.com/webdriverio/selenium-standalone) starts a server that will launch browsers on your local machine. You need to have Java installed on your machine as well as *chrome* or *firefox*.
[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*:
@@ -87,74 +83,34 @@ npm run test
```
or *firefox*:
```
BROWSER=firefox npm run test
npm run test -- --browser firefox
```
After some time you should see a browser launch which will be automated by the test runner.
See the following docs for more info: (Launching Browsers)[https://docs.cypress.io/guides/guides/launching-browsers]
You can also see the tests as they run or select which suites to run by executing:
## Related Projects
```
npm run cy:open
```
## Release process
1. Review [`CHANGELOG.md`](/CHANGELOG.md)
- Double-check that all changes included in the release are appropriately documented.
- To-be-released changes should be under the "main" header.
- Commit any final changes to the changelog.
2. Run [Create bump version PR](https://github.com/maplibre/maputnik/actions/workflows/create-bump-version-pr.yml) by manual workflow dispatch and set the version number in the input. This will create a PR that changes the changelog and `package.json` file to review and merge.
3. Once merged, an automatic process will kick in and creates a GitHub release and uploads release assets.
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
## Sponsors
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.
### Gold
- [Wemap](https://getwemap.com/)
- [Orbicon Informatik](https://www.orbiconinformatik.dk/)
- [Terranodo](http://terranodo.io/)
<a href="https://getwemap.com/">
<img width="33%" alt="Wemap" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/wemap.jpg" />
</a>
<a href="http://terranodo.io/">
<img width="33%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/terranodo.png" />
</a>
<a href="https://www.orbiconinformatik.dk/">
<img width="32%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/orbicon_informatik.png" />
</a>
<br/>
### Silver
- [Klokan Technologies](https://www.klokantech.com/)
- [Geofabrik](http://www.geofabrik.de/)
- [Dreipol](https://www.dreipol.ch/)
<a href="https://www.klokantech.com/">
<img width="18%" alt="Klokan Technologies" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/klokantech.png" />
</a>
<a href="http://www.geofabrik.de/">
<img width="18%" alt="Geofabrik" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/geofabrik.png" />
</a>
<a href="https://www.dreipol.ch/">
<img width="18%" alt="Dreipol" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/dreipol.png" />
</a>
<br/>
### Individuals
**Influential Stakeholder**
Alan McConchie, Odi, Mats Norén, Uli [geOps](http://geops.ch/), Helge Fahrnberger ([Toursprung](http://www.toursprung.com/)), Kirusanth Poopalasingam
**Stakeholder**
Brian Flood, Vasile Coțovanu, Andreas Kalkbrenner, Christian Mäder, Gregor Wassmann, Lee Armstrong, Rafel, Jon Burgess, Lukas Lehmann, Joachim Ungar, Alois Ackermann, Zsolt Ero, Jordan Meek
**Supporter**
Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth, Janko Mihelić, Moritz Stefaner, Sebastian Ahoi, Juerg Uhlmann, Tom Wider, Nadia Panchaud, Oliver Snowden, Stephan Heuel, Tobin Bradley, Adrian Herzog, Antti Lehto, Pascal Mages, Marc Gehling, Imre Samu, Lauri K., Visahavel Parthasarathy, Christophe Waterlot-Buisine, Max Galka, ubahnverleih, Wouter van Dam, Jakob Lobensteiner, Samuel Kurath, Brian Bancroft
You can see this file's history for previous sponsors of the original Maputnik repo.
Read more about the MapLibre Sponsorship Program at https://maplibre.org/sponsors/.
## License
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
**Disclaimer** This project is not affiliated with Mapbox or Mapbox Studio. It is an independent style editor for the
open source technology in the Mapbox GL ecosystem.
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and Maplibre contributors.
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by other map studios and make your own decisions for a good style editor.

2
SECURITY_POLICY.txt Normal file
View File

@@ -0,0 +1,2 @@
For an up-to-date policy refer to
https://github.com/maplibre/maplibre/blob/main/SECURITY_POLICY.txt

11
build/README.md Normal file
View File

@@ -0,0 +1,11 @@
# 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
build/bump-version-changelog.js Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env node
/**
* This script updates the changelog.md file with the version given in the arguments
* It replaces ## main with ## <version>
* Removes _...Add new stuff here..._
* And adds on top a ## main with add stuff here.
*
* Copied from maplibre/maplibre-gl-js
* https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
*/
import * as fs from 'fs';
const changelogPath = 'CHANGELOG.md';
let changelog = fs.readFileSync(changelogPath, 'utf8');
changelog = changelog.replace('## main', `## ${process.argv[2]}`);
changelog = changelog.replaceAll('- _...Add new stuff here..._\n', '');
changelog = `## main
### ✨ Features and improvements
- _...Add new stuff here..._
### 🐞 Bug fixes
- _...Add new stuff here..._
` + changelog;
fs.writeFileSync(changelogPath, changelog, 'utf8');

48
build/release-notes.js Executable file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/env node
// Copied from maplibre/maplibre-gl-js
// https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
import * as fs from 'fs';
const changelogPath = 'CHANGELOG.md';
const changelog = fs.readFileSync(changelogPath, 'utf8');
/*
Parse the raw changelog text and split it into individual releases.
This regular expression:
- Matches lines starting with "## x.x.x".
- Groups the version number.
- Skips the (optional) release date.
- Groups the changelog content.
- Ends when another "## x.x.x" is found.
*/
const regex = /^## (\d+\.\d+\.\d+.*?)\n(.+?)(?=\n^## \d+\.\d+\.\d+.*?\n)/gms;
let releaseNotes = [];
let match;
// eslint-disable-next-line no-cond-assign
while (match = regex.exec(changelog)) {
releaseNotes.push({
'version': match[1],
'changelog': match[2].trim(),
});
}
const latest = releaseNotes[0];
const previous = releaseNotes[1];
// Print the release notes template.
let header = 'Changes since previous version'
if (previous) {
header = `https://github.com/maplibre/maputnik
[Changes](https://github.com/maplibre/maputnik/compare/v${previous.version}...v${latest.version}) since [Maputnik v${previous.version}](https://github.com/maplibre/maputnik/releases/tag/v${previous.version})`
}
const templatedReleaseNotes = `${header}
${latest.changelog}
// eslint-disable-next-line eol-last
process.stdout.write(templatedReleaseNotes.trimEnd());

View File

@@ -1,47 +0,0 @@
var webpack = require("webpack");
var WebpackDevServer = require("webpack-dev-server");
var webpackConfig = require("./webpack.config");
var testConfig = require("../test/config/specs");
var artifacts = require("../test/artifacts");
var server;
var SCREENSHOT_PATH = artifacts.pathSync("screenshots");
exports.config = {
runner: 'local',
path: '/wd/hub',
specs: [
'./test/functional/index.js'
],
maxInstances: 10,
capabilities: [
{
maxInstances: 5,
browserName: (process.env.BROWSER || 'chrome'),
}
],
// geckodriver-0.31 seems to have problems as of 2022 May 1
services: process.env.DOCKER_HOST ? [] : [ ['selenium-standalone', { drivers: { firefox: '0.30.0', chrome: 'latest' } } ] ],
logLevel: 'info',
bail: 0,
screenshotPath: SCREENSHOT_PATH,
hostname: process.env.DOCKER_HOST || "0.0.0.0",
framework: 'mocha',
reporters: ['spec'],
mochaOpts: {
ui: 'bdd',
// Because we don't know how long the initial build will take...
timeout: 4*60*1000,
},
onPrepare: async function (config, capabilities) {
webpackConfig.devServer.host = testConfig.testNetwork;
webpackConfig.devServer.port = testConfig.port;
const compiler = webpack(webpackConfig);
server = new WebpackDevServer(webpackConfig.devServer, compiler);
await server.start();
},
onComplete: async function (exitCode, config, capabilities) {
await server.stop();
}
}

View File

@@ -1,74 +0,0 @@
"use strict";
var path = require('path');
var rules = require('./webpack.rules');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
const HOST = process.env.HOST || "127.0.0.1";
const PORT = process.env.PORT || "8888";
module.exports = {
target: 'web',
mode: 'development',
entry: [
`webpack-dev-server/client?http://${HOST}:${PORT}`,
`webpack/hot/only-dev-server`,
`./src/index.jsx` // Your appʼs entry point
],
devtool: process.env.WEBPACK_DEVTOOL || 'cheap-module-source-map',
output: {
path: path.join(__dirname, '..', 'public'),
filename: 'bundle.js'
},
resolve: {
extensions: ['.js', '.jsx']
},
module: {
noParse: [
/mapbox-gl\/dist\/mapbox-gl.js/
],
rules: rules
},
node: {
fs: "empty",
net: 'empty',
tls: 'empty'
},
devServer: {
// enable HMR
hot: true,
// serve index.html in place of 404 responses to allow HTML5 history
historyApiFallback: true,
port: PORT,
host: HOST,
watchFiles: {
options: {
// Disabled polling by default as it causes lots of CPU usage and hence drains laptop batteries. To enable polling add WEBPACK_DEV_SERVER_POLLING to your environment
// See <https://webpack.js.org/configuration/watch/#watchoptions-poll> for details
usePolling: (!!process.env.WEBPACK_DEV_SERVER_POLLING ? true : false),
watch: false
}
}
},
optimization: {
noEmitOnErrors: true,
},
plugins: [
new HtmlWebpackPlugin({
title: 'Maputnik',
template: './src/template.html'
}),
new HtmlWebpackInlineSVGPlugin({
runPreEmit: true,
}),
new CopyWebpackPlugin({
patterns: [
{
from: './src/manifest.json',
to: 'manifest.json'
}
]
})
]
};

View File

@@ -1,68 +0,0 @@
var webpack = require('webpack');
var path = require('path');
var rules = require('./webpack.rules');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
var CopyWebpackPlugin = require('copy-webpack-plugin');
var artifacts = require("../test/artifacts");
var OUTPATH = artifacts.pathSync("/build");
module.exports = {
entry: {
app: './src/index.jsx',
},
output: {
path: OUTPATH,
filename: '[name].[contenthash].js',
chunkFilename: '[contenthash].js'
},
resolve: {
extensions: ['.js', '.jsx']
},
module: {
noParse: [
/mapbox-gl\/dist\/mapbox-gl.js/
],
rules: rules
},
node: {
fs: "empty",
net: 'empty',
tls: 'empty'
},
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
new WebpackCleanupPlugin(),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new HtmlWebpackPlugin({
template: './src/template.html',
title: 'Maputnik'
}),
new HtmlWebpackInlineSVGPlugin({
runPreEmit: true,
}),
new CopyWebpackPlugin({
patterns: [
{
from: './src/manifest.json',
to: 'manifest.json'
}
]
}),
new BundleAnalyzerPlugin({
analyzerMode: 'static',
defaultSizes: 'gzip',
openAnalyzer: false,
generateStatsFile: true,
reportFilename: 'bundle-stats.html',
statsFilename: 'bundle-stats.json',
})
]
};

View File

@@ -1,20 +0,0 @@
const webpackProdConfig = require('./webpack.production.config');
const artifacts = require("../test/artifacts");
const OUTPATH = artifacts.pathSync("/profiling");
module.exports = {
...webpackProdConfig,
output: {
...webpackProdConfig.output,
path: OUTPATH,
},
resolve: {
...webpackProdConfig.resolve,
alias: {
...webpackProdConfig.resolve.alias,
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling',
}
}
};

View File

@@ -1,44 +0,0 @@
const path = require("path");
module.exports = [
{
test: /\.jsx?$/,
exclude: [
path.resolve(__dirname, '../node_modules')
],
use: 'babel-loader'
},
{
test: /\.(eot|ttf|woff|woff2)$/,
use: 'file-loader?name=fonts/[name].[ext]'
},
{
test: /\.ico$/,
use: 'file-loader?name=[name].[ext]'
},
{
test: /\.(gif|jpg|png)$/,
use: 'file-loader?name=img/[name].[ext]'
},
{
test: /\.svg$/,
use: [
'svg-inline-loader'
]
},
{
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
use: [
'style-loader',
"css-loader",
"sass-loader"
]
},
{
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
use: [
'style-loader',
'css-loader'
]
}
];

23
cypress.config.ts Normal file
View File

@@ -0,0 +1,23 @@
import { defineConfig } from "cypress";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
export default defineConfig({
env: {
codeCoverage: {
exclude: "cypress/**/*.*",
},
},
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
require("@cypress/code-coverage/task")(on, config);
return config;
},
baseUrl: "http://localhost:8888",
retries: {
runMode: 2,
openMode: 0,
},
},
});

View File

@@ -0,0 +1,40 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("accessibility", () => {
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
describe("skip links", () => {
beforeEach(() => {
when.setStyle("layer");
});
it("skip link to layer list", () => {
const selector = "root:skip:layer-list";
then(get.elementByTestId(selector)).shouldExist();
when.tab();
then(get.elementByTestId(selector)).shouldBeFocused();
when.click(selector);
then(get.skipTargetLayerList()).shouldBeFocused();
});
// This fails for some reason only in Chrome, but passes in firefox. Adding a skip here to allow merge and later on we'll decide if we want to fix this or not.
it.skip("skip link to layer editor", () => {
const selector = "root:skip:layer-editor";
then(get.elementByTestId(selector)).shouldExist();
when.tab().tab();
then(get.elementByTestId(selector)).shouldBeFocused();
when.click(selector);
then(get.skipTargetLayerEditor()).shouldBeFocused();
});
it("skip link to map view", () => {
const selector = "root:skip:map-view";
then(get.elementByTestId(selector)).shouldExist();
when.tab().tab().tab();
then(get.elementByTestId(selector)).shouldBeFocused();
when.click(selector);
then(get.canvas()).shouldBeFocused();
});
});
});

125
cypress/e2e/history.cy.ts Normal file
View File

@@ -0,0 +1,125 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("history", () => {
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
beforeAndAfter();
let undoKeyCombo: string;
let redoKeyCombo: string;
before(() => {
const isMac = get.isMac();
undoKeyCombo = isMac ? "{meta}z" : "{ctrl}z";
redoKeyCombo = isMac ? "{meta}{shift}z" : "{ctrl}y";
});
it("undo/redo", () => {
when.setStyle("geojson");
when.modal.open();
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
when.modal.fillLayers({
id: "step 1",
type: "background",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 1",
type: "background",
},
],
});
when.modal.open();
when.modal.fillLayers({
id: "step 2",
type: "background",
});
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 1",
type: "background",
},
{
id: "step 2",
type: "background",
},
],
});
when.typeKeys(undoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 1",
type: "background",
},
],
});
when.typeKeys(undoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
when.typeKeys(redoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 1",
type: "background",
},
],
});
when.typeKeys(redoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 1",
type: "background",
},
{
id: "step 2",
type: "background",
},
],
});
});
it("should not redo after undo and value change", () => {
when.setStyle("geojson");
when.modal.open();
when.modal.fillLayers({
id: "step 1",
type: "background",
});
when.modal.open();
when.modal.fillLayers({
id: "step 2",
type: "background",
});
when.typeKeys(undoKeyCombo);
when.typeKeys(undoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
when.modal.open();
when.modal.fillLayers({
id: "step 3",
type: "background",
});
when.typeKeys(redoKeyCombo);
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
layers: [
{
id: "step 3",
type: "background",
},
],
});
});
});

35
cypress/e2e/i18n.cy.ts Normal file
View File

@@ -0,0 +1,35 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("i18n", () => {
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
describe("language detector", () => {
it("English", () => {
const url = "?lng=en";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
});
it("Japanese", () => {
const url = "?lng=ja";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
});
});
describe("language switcher", () => {
beforeEach(() => {
when.setStyle("layer");
});
it("the language switcher switches to Japanese", () => {
const selector = "maputnik-lang-select";
then(get.elementByTestId(selector)).shouldExist();
when.select(selector, "ja");
then(get.elementByTestId(selector)).shouldHaveValue("ja");
then(get.elementByTestId("nav:settings")).shouldHaveText("スタイル設定");
});
});
});

View File

@@ -0,0 +1,60 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("keyboard", () => {
let { beforeAndAfter, given, when, get, then } = new MaputnikDriver();
beforeAndAfter();
describe("shortcuts", () => {
beforeEach(() => {
given.setupMockBackedResponses();
when.setStyle("");
});
it("ESC should unfocus", () => {
const targetSelector = "maputnik-select";
when.focus(targetSelector);
then(get.elementByTestId(targetSelector)).shouldBeFocused();
when.typeKeys("{esc}");
then(get.elementByTestId(targetSelector)).shouldNotBeFocused();
});
it("'?' should show shortcuts modal", () => {
when.typeKeys("?");
then(get.elementByTestId("modal:shortcuts")).shouldBeVisible();
});
it("'o' should show open modal", () => {
when.typeKeys("o");
then(get.elementByTestId("modal:open")).shouldBeVisible();
});
it("'e' should show export modal", () => {
when.typeKeys("e");
then(get.elementByTestId("modal:export")).shouldBeVisible();
});
it("'d' should show sources modal", () => {
when.typeKeys("d");
then(get.elementByTestId("modal:sources")).shouldBeVisible();
});
it("'s' should show settings modal", () => {
when.typeKeys("s");
then(get.elementByTestId("modal:settings")).shouldBeVisible();
});
it("'i' should change map to inspect mode", () => {
when.typeKeys("i");
then(get.inputValue("maputnik-select")).shouldEqual("inspect");
});
it("'m' should focus map", () => {
when.typeKeys("m");
then(get.canvas()).shouldBeFocused();
});
it("'!' should show debug modal", () => {
when.typeKeys("!");
then(get.elementByTestId("modal:debug")).shouldBeVisible();
});
});
});

497
cypress/e2e/layers.cy.ts Normal file
View File

@@ -0,0 +1,497 @@
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("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("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();
});
});
});

32
cypress/e2e/map.cy.ts Normal file
View File

@@ -0,0 +1,32 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("map", () => {
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
describe("zoom level", () => {
it("via url", () => {
let zoomLevel = 12.37;
when.setStyle("geojson", zoomLevel);
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
"Zoom: " + zoomLevel
);
});
it("via map controls", () => {
let zoomLevel = 12.37;
when.setStyle("geojson", zoomLevel);
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
when.clickZoomIn();
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
"Zoom: " + (zoomLevel + 1)
);
});
});
describe("search", () => {
it('should exist', () => {
then(get.searchControl()).shouldBeVisible();
});
});
});

View File

@@ -0,0 +1,19 @@
import { CypressHelper } from "@shellygo/cypress-test-utils";
export default class MaputnikCypressHelper {
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
public given = {
...this.helper.given,
};
public get = {
...this.helper.get,
};
public when = {
...this.helper.when,
};
public beforeAndAfter = this.helper.beforeAndAfter;
}

View File

@@ -0,0 +1,186 @@
/// <reference types="cypress-plugin-tab" />
import { CypressHelper } from "@shellygo/cypress-test-utils";
import { Assertable, then } from "@shellygo/cypress-test-utils/assertable";
import MaputnikCypressHelper from "./maputnik-cypress-helper";
import ModalDriver from "./modal-driver";
const baseUrl = "http://localhost:8888/";
const styleFromWindow = (win: Window) => {
const styleId = win.localStorage.getItem("maputnik:latest_style");
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
const obj = JSON.parse(styleItem || "");
return obj;
};
export class MaputnikAssertable<T> extends Assertable<T> {
shouldEqualToStoredStyle = () =>
then(
new CypressHelper().get.window().then((win: Window) => {
const style = styleFromWindow(win);
then(this.chainable).shouldDeepNestedInclude(style);
})
);
}
export class MaputnikDriver {
private helper = new MaputnikCypressHelper();
private modalDriver = new ModalDriver();
public beforeAndAfter = () => {
beforeEach(() => {
this.given.setupMockBackedResponses();
this.when.setStyle("both");
});
};
public then = (chainable: Cypress.Chainable<any>) =>
new MaputnikAssertable(chainable);
public given = {
...this.helper.given,
setupMockBackedResponses: () => {
this.helper.given.interceptAndMockResponse({
method: "GET",
url: baseUrl + "example-style.json",
response: {
fixture: "example-style.json",
},
alias: "example-style.json",
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: baseUrl + "example-layer-style.json",
response: {
fixture: "example-layer-style.json",
},
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: baseUrl + "geojson-style.json",
response: {
fixture: "geojson-style.json",
},
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: baseUrl + "raster-style.json",
response: {
fixture: "raster-style.json",
},
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: baseUrl + "geojson-raster-style.json",
response: {
fixture: "geojson-raster-style.json",
},
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: "*example.local/*",
response: [],
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: "*example.com/*",
response: [],
});
},
};
public when = {
...this.helper.when,
modal: this.modalDriver.when,
within: (selector: string, fn: () => void) => {
this.helper.when.within(fn, selector);
},
tab: () => this.helper.get.element("body").tab(),
waitForExampleFileResponse: () => {
this.helper.when.waitForResponse("example-style.json");
},
chooseExampleFile: () => {
this.helper.get
.bySelector("type", "file")
.selectFile("cypress/fixtures/example-style.json", { force: true });
},
setStyle: (
styleProperties: "geojson" | "raster" | "both" | "layer" | "",
zoom?: number
) => {
let url = "?debug";
switch (styleProperties) {
case "geojson":
url += `&style=${baseUrl}geojson-style.json`;
break;
case "raster":
url += `&style=${baseUrl}raster-style.json`;
break;
case "both":
url += `&style=${baseUrl}geojson-raster-style.json`;
break;
case "layer":
url += `&style=${baseUrl}/example-layer-style.json`;
break;
}
if (zoom) {
url += `#${zoom}/41.3805/2.1635`;
}
this.helper.when.visit(baseUrl + url);
if (styleProperties) {
this.helper.when.acceptConfirm();
}
// when methods should not include assertions
const toolbarLink = this.helper.get.elementByTestId("toolbar:link")
toolbarLink.scrollIntoView();
toolbarLink.should("be.visible");
},
typeKeys: (keys: string) => this.helper.get.element("body").type(keys),
clickZoomIn: () => {
this.helper.get.element(".maplibregl-ctrl-zoom-in").click();
},
selectWithin: (selector: string, value: string) => {
this.when.within(selector, () => {
this.helper.get.element("select").select(value);
});
},
select: (selector: string, value: string) => {
this.helper.get.elementByTestId(selector).select(value);
},
focus: (selector: string) => {
this.helper.when.focus(selector);
},
setValue: (selector: string, text: string) => {
this.helper.get
.elementByTestId(selector)
.clear()
.type(text, { parseSpecialCharSequences: false });
},
};
public get = {
...this.helper.get,
isMac: () => {
return Cypress.platform === "darwin";
},
styleFromLocalStorage: () =>
this.helper.get.window().then((win) => styleFromWindow(win)),
exampleFileUrl: () => {
return baseUrl + "example-style.json";
},
skipTargetLayerList: () =>
this.helper.get.elementByTestId("skip-target-layer-list"),
skipTargetLayerEditor: () =>
this.helper.get.elementByTestId("skip-target-layer-editor"),
canvas: () => this.helper.get.element("canvas"),
searchControl: () => this.helper.get.element('.maplibregl-ctrl-geocoder')
};
}

View File

@@ -0,0 +1,40 @@
import { v1 as uuid } from "uuid";
import MaputnikCypressHelper from "./maputnik-cypress-helper";
export default class ModalDriver {
private helper = new MaputnikCypressHelper();
public when = {
fillLayers: (opts: { type: string; layer?: string; id?: string }) => {
// Having logic in test code is an anti pattern.
// This should be splitted to multiple single responsibility functions
let type = opts.type;
let layer = opts.layer;
let id;
if (opts.id) {
id = opts.id;
} else {
id = `${type}:${uuid()}`;
}
this.helper.when.selectOption("add-layer.layer-type.select", type);
this.helper.when.type("add-layer.layer-id.input", id);
if (layer) {
this.helper.when.within(() => {
this.helper.get.element("input").type(layer!);
}, "add-layer.layer-source-block");
}
this.helper.when.click("add-layer");
return id;
},
open: () => {
this.helper.when.click("layer-list:add-layer");
},
close: (key: string) => {
this.helper.when.click(key + ".close-modal");
},
};
}

180
cypress/e2e/modals.cy.ts Normal file
View File

@@ -0,0 +1,180 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("modals", () => {
let { beforeAndAfter, when, get, then } = new MaputnikDriver();
beforeAndAfter();
beforeEach(() => {
when.setStyle("");
});
describe("open", () => {
beforeEach(() => {
when.click("nav:open");
});
it("close", () => {
when.modal.close("modal:open");
then(get.elementByTestId("modal:open")).shouldNotExist();
});
it.skip("upload", () => {
// HM: I was not able to make the following choose file actually to select a file and close the modal...
when.chooseExampleFile();
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
});
describe("when click open url", () => {
beforeEach(() => {
let styleFileUrl = get.exampleFileUrl();
when.setValue("modal:open.url.input", styleFileUrl);
when.click("modal:open.url.button");
when.wait(200);
});
it("load from url", () => {
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
});
});
});
describe("shortcuts", () => {
it("open/close", () => {
when.setStyle("");
when.typeKeys("?");
when.modal.close("modal:shortcuts");
then(get.elementByTestId("modal:shortcuts")).shouldNotExist();
});
});
describe("export", () => {
beforeEach(() => {
when.click("nav:export");
});
it("close", () => {
when.modal.close("modal:export");
then(get.elementByTestId("modal:export")).shouldNotExist();
});
// TODO: Work out how to download a file and check the contents
it("download");
});
describe("sources", () => {
it("active sources");
it("public source");
it("add new source");
});
describe("inspect", () => {
it("toggle", () => {
// There is no assertion in this test
when.setStyle("geojson");
when.select("maputnik-select", "inspect");
});
});
describe("style settings", () => {
beforeEach(() => {
when.click("nav:settings");
});
describe("when click name filed spec information", () => {
beforeEach(() => {
when.click("field-doc-button-Name");
});
it("should show the spec information", () => {
then(get.elementsText("spec-field-doc")).shouldInclude(
"name for the style"
);
});
});
describe("when set name and click owner", () => {
beforeEach(() => {
when.setValue("modal:settings.name", "foobar");
when.click("modal:settings.owner");
when.wait(200);
});
it("show name specifications", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
name: "foobar",
});
});
});
describe("when set owner and click name", () => {
beforeEach(() => {
when.setValue("modal:settings.owner", "foobar");
when.click("modal:settings.name");
when.wait(200);
});
it("should update owner in local storage", () => {
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
owner: "foobar",
});
});
});
it("sprite url", () => {
when.setValue("modal:settings.sprite", "http://example.com");
when.click("modal:settings.name");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sprite: "http://example.com",
});
});
it("glyphs url", () => {
let glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
when.setValue("modal:settings.glyphs", glyphsUrl);
when.click("modal:settings.name");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
glyphs: glyphsUrl,
});
});
it("maptiler access token", () => {
let apiKey = "testing123";
when.setValue(
"modal:settings.maputnik:openmaptiles_access_token",
apiKey
);
when.click("modal:settings.name");
then(
get.styleFromLocalStorage().then((style) => style.metadata)
).shouldInclude({
"maputnik:openmaptiles_access_token": apiKey,
});
});
it("thunderforest access token", () => {
let apiKey = "testing123";
when.setValue(
"modal:settings.maputnik:thunderforest_access_token",
apiKey
);
when.click("modal:settings.name");
then(
get.styleFromLocalStorage().then((style) => style.metadata)
).shouldInclude({ "maputnik:thunderforest_access_token": apiKey });
});
it("style renderer", () => {
cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
when.select("modal:settings.maputnik:renderer", "ol");
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
"ol"
);
when.click("modal:settings.name");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
metadata: { "maputnik:renderer": "ol" },
});
});
});
describe("sources", () => {
it("toggle");
});
});

View File

@@ -3,7 +3,7 @@
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mbgljs"
"maputnik:renderer": "mlgljs"
},
"sources": {},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",

View File

@@ -3,7 +3,7 @@
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mbgljs"
"maputnik:renderer": "mlgljs"
},
"sources": {},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",

View File

@@ -0,0 +1,34 @@
{
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mlgljs"
},
"sources": {
"example": {
"type": "vector",
"data": {
"type": "FeatureCollection",
"features":[{
"type": "Feature",
"properties": {
"name": "Dinagat Islands"
},
"geometry":{
"type": "Point",
"coordinates": [125.6, 10.1]
}
}]
}
},
"raster": {
"tileSize": 256,
"tiles": ["http://localhost/example/{x}/{y}/{z}"],
"type": "raster"
}
},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}

View File

@@ -0,0 +1,29 @@
{
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mlgljs"
},
"sources": {
"example": {
"type": "vector",
"data": {
"type": "FeatureCollection",
"features":[{
"type": "Feature",
"properties": {
"name": "Dinagat Islands"
},
"geometry":{
"type": "Point",
"coordinates": [125.6, 10.1]
}
}]
}
}
},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}

View File

@@ -0,0 +1,18 @@
{
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mlgljs"
},
"sources": {
"raster": {
"tileSize": 256,
"tiles": ["http://localhost/example/{x}/{y}/{z}"],
"type": "raster"
}
},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}

View File

@@ -0,0 +1,37 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }

22
cypress/support/e2e.ts Normal file
View File

@@ -0,0 +1,22 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import "@cypress/code-coverage/support";
import "cypress-plugin-tab";
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')

31
desktop/.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
# 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
desktop/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Maputnik
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

39
desktop/Makefile Normal file
View File

@@ -0,0 +1,39 @@
SOURCEDIR=.
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
BINARY=maputnik
DESKTOP_VERSION := 1.1.1
EDITOR_VERSION := $(shell node -p "require('../package.json').version")
GOPATH := $(if $(GOPATH),$(GOPATH),$(HOME)/go)
GOBIN := $(if $(GOBIN),$(GOBIN),$(HOME)/go/bin)
all: $(BINARY)
$(BINARY): $(GOBIN)/gox $(SOURCES) version.go rice-box.go
$(GOBIN)/gox -osarch "windows/amd64 linux/amd64 darwin/amd64" -output "bin/{{.OS}}/${BINARY}"
# Copy the current release into ./editor/maputnik so it can be
# embedded in the binary
editor/pull_release:
mkdir -p editor
cp -r ../dist/* editor
$(GOBIN)/gox:
go install github.com/mitchellh/gox@v1.0.1
$(GOBIN)/rice:
go install github.com/GeertJohan/go.rice/rice@v1.0.3
# Embed the current version numbers in the executable by writing version.go
.PHONY: version.go
version.go:
@echo "// DO NOT EDIT: Autogenerated by Makefile\n" > version.go
@echo "package main\n" >> version.go
@echo "const DesktopVersion = \"$(DESKTOP_VERSION)\"" >> version.go
@echo "const EditorVersion = \"$(EDITOR_VERSION)\"" >> version.go
rice-box.go: $(GOBIN)/rice editor/pull_release
$(GOBIN)/rice embed-go
.PHONY: clean
clean:
rm -rf editor && rm -f rice-box.go && rm -rf bin

72
desktop/README.md Normal file
View File

@@ -0,0 +1,72 @@
# Maputnik Desktop [![GitHub CI status](https://github.com/maplibre/maputnik/workflows/ci/badge.svg)][github-action-ci]
---
A Golang based cross platform executable for integrating Maputnik locally.
This binary packages up the JavaScript and CSS bundle produced by maputnik
and embeds it in the program for easy distribution. It also allows
exposing a local style file and work on it both in Maputnik and with your favorite
editor.
Report issues on [maplibre/maputnik](https://github.com/maplibre/maputnik).
## Install
You can download a single binary for Linux, OSX or Windows from [the latest releases of **maplibre/maputnik**](https://github.com/maplibre/maputnik/editor/releases/latest).
### Usage
Simply start up a web server and access the Maputnik editor GUI at `localhost:8000`.
```bash
maputnik
```
Expose a local style file to Maputnik allowing the web based editor
to save to the local filesystem.
```bash
maputnik --file basic-v9.json
```
Watch the local style for changes and inform the editor via web socket.
This makes it possible to edit the style with a local text editor and still
use Maputnik.
```bash
maputnik --watch --file basic-v9.json
```
Choose a local port to listen on, instead of using the default port 8000.
```bash
maputnik --port 8001
```
Specify a path to a directory which, if it exists, will be served under http://localhost:8000/static/ .
Could be used to serve sprites and glyphs.
```bash
maputnik --static ./localFolder
```
### API
`maputnik` exposes the configured styles via a HTTP API.
| Method | Description
|---------------------------------|---------------------------------------
| `GET /styles` | List the ID of all configured style files
| `GET /styles/{filename}` | Get contents of a single style file
| `PUT /styles/{filename}` | Update contents of a style file
| `WEBSOCKET /ws` | Listen to change events for the configured style files
### Build
From the root of the [maplibre/maputnik](https://github.com/maplibre/maputnik) project, install the deps and run the desktop-build command.
```
npm install
npm run build-desktop
```
You should now find the `maputnik` binary in your `bin` directory.

81
desktop/api.go Normal file
View File

@@ -0,0 +1,81 @@
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())
}
}

View File

@@ -0,0 +1,69 @@
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
desktop/go.mod Normal file
View File

@@ -0,0 +1,27 @@
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
desktop/go.sum Normal file
View File

@@ -0,0 +1,54 @@
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI=
github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4=
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/daaku/go.zipexe v1.0.2 h1:Zg55YLYTr7M9wjKn8SY/WcpuuEi+kR2u4E8RhvpyXmk=
github.com/daaku/go.zipexe v1.0.2/go.mod h1:5xWogtqlYnfBXkSB1o9xysukNP9GTvaNkqzUZbt3Bw8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/maputnik/desktop v1.0.7 h1:rdFg7emIJOT3YsZpwqSChmWtMOvu+T4h6WwVQAZP9n4=
github.com/maputnik/desktop v1.0.7/go.mod h1:wmDjHUztx9jOBz0I22589yWguAGdV/sEM57YANpN8oQ=
github.com/nkovacs/streamquote v1.0.0 h1:PmVIV08Zlx2lZK5fFZlMZ04eHcDTIFJCv/5/0twVUow=
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

80
desktop/maputnik.go Normal file
View File

@@ -0,0 +1,80 @@
package main
import (
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/GeertJohan/go.rice"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/maputnik/desktop/filewatch"
"github.com/urfave/cli"
)
func main() {
app := cli.NewApp()
app.Name = "maputnik"
app.Usage = "Server for integrating Maputnik locally"
app.Version = "Editor: " + EditorVersion + "; Desktop: " + DesktopVersion
app.Flags = []cli.Flag{
&cli.StringFlag{
Name: "file, f",
Usage: "Allow access to JSON style from web client",
},
&cli.BoolFlag{
Name: "watch",
Usage: "Notify web client about JSON style file changes",
},
&cli.IntFlag{
Name: "port",
Value: 8000,
Usage: "TCP port to listen on",
},
&cli.StringFlag{
Name: "static",
Usage: "Serve directory under /static/",
},
}
app.Action = func(c *cli.Context) error {
gui := http.FileServer(rice.MustFindBox("editor").HTTPBox())
router := mux.NewRouter().StrictSlash(true)
filename := c.String("file")
if filename != "" {
fmt.Printf("%s is accessible via Maputnik\n", filename)
// Allow access to reading and writing file on the local system
path, _ := filepath.Abs(filename)
accessor := StyleFileAccessor(path)
router.Path("/styles").Methods("GET").HandlerFunc(accessor.ListFiles)
router.Path("/styles/{styleId}").Methods("GET").HandlerFunc(accessor.ReadFile)
router.Path("/styles/{styleId}").Methods("PUT").HandlerFunc(accessor.SaveFile)
// Register websocket to notify we clients about file changes
if c.Bool("watch") {
router.Path("/ws").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
filewatch.ServeWebsocketFileWatcher(filename, w, r)
})
}
}
staticDir := c.String("static")
if staticDir != "" {
h := http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))
router.PathPrefix("/static/").Handler(h)
}
router.PathPrefix("/").Handler(http.StripPrefix("/", gui))
loggedRouter := handlers.LoggingHandler(os.Stdout, router)
corsRouter := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}), handlers.AllowedMethods([]string{"GET", "PUT"}), handlers.AllowedOrigins([]string{"*"}), handlers.AllowCredentials())(loggedRouter)
fmt.Printf("Exposing Maputnik on http://localhost:%d\n", c.Int("port"))
return http.ListenAndServe(fmt.Sprintf(":%d", c.Int("port")), corsRouter)
}
app.Run(os.Args)
}

17
i18next-parser.config.ts Normal file
View File

@@ -0,0 +1,17 @@
export default {
output: 'src/locales/$LOCALE/$NAMESPACE.json',
locales: [ 'ja', 'he','zh' ],
// Because some keys are dynamically generated, i18next-parser can't detect them.
// We add these keys manually, so we don't want to remove them.
keepRemoved: true,
// We use plain English keys, so we disable key and namespace separators.
keySeparator: false,
namespaceSeparator: false,
defaultValue: (locale, ns, key) => {
// The default value is a string that indicates that the string is not translated.
return '__STRING_NOT_TRANSLATED__';
}
}

View File

@@ -2,9 +2,10 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title><%= htmlWebpackPlugin.options.title %></title>
<title>Maputnik</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="manifest" href="manifest.json">
<link rel="manifest" href="src/manifest.json">
<link rel="icon" href="src/favicon.ico" type="image/x-icon" />
<style>
html {
background-color: rgb(28, 31, 36);
@@ -23,7 +24,7 @@
align-items: center;
}
.loading__logo svg {
.loading__logo img {
width: 200px;
height: 200px;
}
@@ -38,7 +39,6 @@
</style>
</head>
<body>
<!-- TODO: Import dynamically -->
<!-- From <https://github.com/hail2u/color-blindness-emulation> -->
<svg
aria-hidden="true"
@@ -123,10 +123,10 @@
<div id="app"></div>
<div class="loading">
<div class="loading__logo">
<!-- replaced by 'html-webpack-inline-svg-plugin' -->
<img inline src="node_modules/maputnik-design/logos/logo-loading.svg" />
</div>
<div class="loading__text">Loading&hellip;</div>
</div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

47581
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +1,49 @@
{
"name": "maputnik",
"version": "2.0.0-pre.1",
"version": "2.1.0",
"description": "A MapLibre GL visual style editor",
"type": "module",
"main": "''",
"scripts": {
"stats": "webpack --config config/webpack.production.config.js --progress=profile --json > stats.json",
"build": "webpack --config config/webpack.production.config.js --progress=profile --color",
"profiling-build": "webpack --config config/webpack.profiling.config.js --progress=profile --color",
"test": "cross-env NODE_ENV=test wdio config/wdio.conf.js",
"test-watch": "cross-env NODE_ENV=test wdio config/wdio.conf.js --watch",
"start": "webpack-dev-server --progress=profile --color --config config/webpack.config.js",
"start-prod": "webpack-dev-server --progress=profile --color --config config/webpack.production.config.js",
"start-sandbox": "webpack-dev-server --disable-host-check --host 0.0.0.0 --progress=profile --color --config config/webpack.production.config.js",
"lint-js": "eslint --ext js --ext jsx src test",
"start": "vite",
"build": "tsc && vite build --base=/maputnik/",
"build-desktop": "tsc && vite build --base=/ && cd desktop && make",
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
"lint": "eslint ./src ./cypress --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
"test": "cypress run",
"cy:open": "cypress open",
"lint-css": "stylelint \"src/styles/*.scss\"",
"lint": "npm run lint-js && npm run lint-css",
"storybook": "start-storybook -h 0.0.0.0 -p 6006",
"build-storybook": "build-storybook -o build/storybook"
"sort-styles": "jq 'sort_by(.id)' src/config/styles.json > tmp.json && mv tmp.json src/config/styles.json"
},
"repository": {
"type": "git",
"url": "https://github.com/maputnik/editor"
"url": "https://github.com/maplibre/maputnik"
},
"author": "Lukas Martinelli",
"license": "MIT",
"homepage": "https://github.com/maputnik/editor#readme",
"homepage": "https://github.com/maplibre/maputnik#readme",
"dependencies": {
"@babel/runtime": "^7.17.9",
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
"@maplibre/maplibre-gl-style-spec": "^17.0.1",
"@mdi/react": "^1.5.0",
"@maplibre/maplibre-gl-geocoder": "^1.6.0",
"@maplibre/maplibre-gl-inspect": "^1.6.3",
"@maplibre/maplibre-gl-style-spec": "^20.1.1",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"array-move": "^4.0.0",
"buffer": "^6.0.3",
"classnames": "^2.3.1",
"classnames": "^2.5.1",
"codemirror": "^5.65.2",
"color": "^4.2.3",
"cypress-plugin-tab": "^1.0.5",
"detect-browser": "^5.3.0",
"events": "^3.3.0",
"file-saver": "^2.0.5",
"json-stringify-pretty-compact": "^3.0.0",
"i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-resources-to-backend": "^1.2.1",
"json-stringify-pretty-compact": "^4.0.0",
"json-to-ast": "^2.1.0",
"jsonlint": "github:josdejong/jsonlint#85a19d7",
"lodash": "^4.17.21",
@@ -47,30 +53,30 @@
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"mapbox-gl-inspect": "^1.3.1",
"maplibre-gl": "^2.4.0",
"maplibre-gl": "^4.1.2",
"maputnik-design": "github:maputnik/design#172b06c",
"ol": "^6.14.1",
"ol-mapbox-style": "^7.1.1",
"prop-types": "^15.8.1",
"react": "^16.0.0",
"react-accessible-accordion": "^4.0.0",
"react": "^18.2.0",
"react-accessible-accordion": "^5.0.0",
"react-aria-menubutton": "^7.0.3",
"react-aria-modal": "^4.0.1",
"react-aria-modal": "^5.0.2",
"react-autobind": "^1.0.6",
"react-autocomplete": "^1.8.1",
"react-collapse": "^5.1.1",
"react-color": "^2.19.3",
"react-dom": "^16.0.0",
"react-dom": "^18.2.0",
"react-file-reader-input": "^2.0.0",
"react-i18next": "^15.0.1",
"react-icon-base": "^2.1.2",
"react-icons": "^4.3.1",
"react-icons": "^5.0.1",
"react-sortable-hoc": "^2.0.0",
"reconnecting-websocket": "^4.4.0",
"sass": "^1.50.0",
"slugify": "^1.6.5",
"sass": "^1.72.0",
"slugify": "^1.6.6",
"string-hash": "^1.1.3",
"url": "^0.11.0"
"url": "^0.11.3"
},
"jshintConfig": {
"esversion": 6
@@ -89,87 +95,55 @@
]
}
},
"eslintConfig": {
"plugins": [
"react"
],
"extends": [
"plugin:react/recommended"
],
"env": {
"browser": true,
"node": true,
"es6": true
},
"parser": "@babel/eslint-parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"impliedStrict": true,
"experimentalObjectRestSpread": true,
"jsx": true
}
},
"settings": {
"react": {
"version": "detect"
}
}
},
"devDependencies": {
"@babel/core": "^7.17.9",
"@babel/eslint-parser": "^7.19.1",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-env": "^7.16.11",
"@babel/preset-flow": "^7.16.7",
"@babel/preset-react": "^7.16.7",
"@mdi/js": "^6.6.96",
"@storybook/addon-a11y": "^6.4.20",
"@storybook/addon-actions": "^6.4.20",
"@storybook/addon-links": "^6.4.20",
"@storybook/addon-storysource": "^6.4.20",
"@storybook/addons": "^6.4.20",
"@storybook/react": "^6.4.20",
"@storybook/theming": "^6.4.20",
"@wdio/cli": "^7.19.3",
"@wdio/local-runner": "^7.19.3",
"@wdio/mocha-framework": "^7.19.3",
"@wdio/selenium-standalone-service": "^7.19.1",
"@wdio/spec-reporter": "^7.19.1",
"babel-loader": "^8.2.4",
"babel-plugin-istanbul": "^6.1.1",
"babel-plugin-static-fs": "^3.0.0",
"copy-webpack-plugin": "^6.4.1",
"@cypress/code-coverage": "^3.12.30",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@rollup/plugin-replace": "^5.0.5",
"@shellygo/cypress-test-utils": "^2.1.9",
"@types/codemirror": "^5.60.15",
"@types/color": "^3.0.6",
"@types/cors": "^2.8.17",
"@types/file-saver": "^2.0.7",
"@types/geojson": "^7946.0.14",
"@types/json-to-ast": "^2.1.4",
"@types/lodash.capitalize": "^4.2.9",
"@types/lodash.clamp": "^4.0.9",
"@types/lodash.clonedeep": "^4.5.9",
"@types/lodash.get": "^4.4.9",
"@types/lodash.isequal": "^4.5.8",
"@types/lodash.throttle": "^4.1.9",
"@types/mocha": "^10.0.6",
"@types/randomcolor": "^0.5.9",
"@types/react": "^18.2.67",
"@types/react-aria-menubutton": "^6.2.14",
"@types/react-aria-modal": "^4.0.10",
"@types/react-autocomplete": "^1.8.10",
"@types/react-collapse": "^5.0.4",
"@types/react-color": "^3.0.12",
"@types/react-dom": "^18.2.22",
"@types/react-file-reader-input": "^2.0.4",
"@types/react-icon-base": "^2.1.6",
"@types/string-hash": "^1.1.3",
"@types/uuid": "^9.0.8",
"@vitejs/plugin-react": "^4.2.1",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"css-loader": "^5.2.7",
"eslint": "^8.12.0",
"eslint-plugin-react": "^7.29.4",
"express": "^4.17.3",
"html-webpack-inline-svg-plugin": "^2.3.0",
"html-webpack-plugin": "^4.5.2",
"cypress": "^13.13.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"i18next-parser": "^9.0.1",
"istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.2.0",
"mkdirp": "^1.0.4",
"mocha": "^9.2.2",
"postcss": "^8.4.12",
"react-hot-loader": "^4.13.0",
"sass-loader": "^10.2.1",
"style-loader": "^2.0.0",
"stylelint": "^14.6.1",
"stylelint-config-recommended-scss": "^6.0.0",
"stylelint-scss": "^4.2.0",
"svg-inline-loader": "^0.8.2",
"transform-loader": "^0.2.4",
"typescript": "^4.6.3",
"uuid": "^8.3.2",
"webdriverio": "^7.19.3",
"webpack": "^4.46.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cleanup-plugin": "^0.5.1",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.8.1"
"istanbul-lib-coverage": "^3.2.2",
"mocha": "^10.3.0",
"postcss": "^8.4.38",
"react-hot-loader": "^4.13.1",
"stylelint": "^16.2.1",
"stylelint-config-recommended-scss": "^14.0.0",
"stylelint-scss": "^6.2.1",
"typescript": "^5.4.3",
"uuid": "^9.0.1",
"vite": "^5.2.6",
"vite-plugin-istanbul": "^6.0.0"
}
}

View File

@@ -1,5 +0,0 @@
{
"container": {
"startScript": "start-sandbox"
}
}

View File

@@ -1,3 +1,4 @@
// @ts-ignore - this can be easily replaced with arrow functions
import autoBind from 'react-autobind';
import React from 'react'
import cloneDeep from 'lodash.clonedeep'
@@ -6,14 +7,15 @@ import buffer from 'buffer'
import get from 'lodash.get'
import {unset} from 'lodash'
import {arrayMoveMutable} from 'array-move'
import url from 'url'
import hash from "string-hash";
import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl'
import {latest, validateStyleMin} from '@maplibre/maplibre-gl-style-spec'
import MapMapboxGl from './MapMapboxGl'
import MapMaplibreGl from './MapMaplibreGl'
import MapOpenLayers from './MapOpenLayers'
import LayerList from './LayerList'
import LayerEditor from './LayerEditor'
import AppToolbar from './AppToolbar'
import AppToolbar, { MapState } from './AppToolbar'
import AppLayout from './AppLayout'
import MessagePanel from './AppMessagePanel'
@@ -22,11 +24,9 @@ import ModalExport from './ModalExport'
import ModalSources from './ModalSources'
import ModalOpen from './ModalOpen'
import ModalShortcuts from './ModalShortcuts'
import ModalSurvey from './ModalSurvey'
import ModalDebug from './ModalDebug'
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
import {latest, validate} from '@maplibre/maplibre-gl-style-spec'
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
import style from '../libs/style'
import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
import { undoMessages, redoMessages } from '../libs/diffmessage'
@@ -37,12 +37,13 @@ import LayerWatcher from '../libs/layerwatcher'
import tokens from '../config/tokens.json'
import isEqual from 'lodash.isequal'
import Debug from '../libs/debug'
import {formatLayerId} from '../util/format';
import { SortEnd } from 'react-sortable-hoc';
import { MapOptions } from 'maplibre-gl';
// Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
window.Buffer = buffer.Buffer;
function setFetchAccessToken(url, mapStyle) {
function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
const matchesTilehosting = url.match(/\.tilehosting\.com/);
const matchesMaptiler = url.match(/\.maptiler\.com/);
const matchesThunderforest = url.match(/\.thunderforest\.com/);
@@ -63,7 +64,7 @@ function setFetchAccessToken(url, mapStyle) {
}
}
function updateRootSpec(spec, fieldName, newValues) {
function updateRootSpec(spec: any, fieldName: string, newValues: any) {
return {
...spec,
$root: {
@@ -76,15 +77,73 @@ function updateRootSpec(spec, fieldName, newValues) {
}
}
export default class App extends React.Component {
constructor(props) {
type OnStyleChangedOpts = {
save?: boolean
addRevision?: boolean
initialLoad?: boolean
}
type MappedErrors = {
message: string
parsed?: {
type: string
data: {
index: number
key: string
message: string
}
}
}
type AppState = {
errors: MappedErrors[],
infos: string[],
mapStyle: StyleSpecification & {id: string},
dirtyMapStyle?: StyleSpecification,
selectedLayerIndex: number,
selectedLayerOriginalId?: string,
sources: {[key: string]: SourceSpecification},
vectorLayers: {},
spec: any,
mapView: {
zoom: number,
center: {
lng: number,
lat: number,
},
},
maplibreGlDebugOptions: Partial<MapOptions> & {
showTileBoundaries: boolean,
showCollisionBoxes: boolean,
showOverdrawInspector: boolean,
},
openlayersDebugOptions: {
debugToolbox: boolean,
},
mapState: MapState
isOpen: {
settings: boolean
sources: boolean
open: boolean
shortcuts: boolean
export: boolean
debug: boolean
}
}
export default class App extends React.Component<any, AppState> {
revisionStore: RevisionStore;
styleStore: StyleStore | ApiStyleStore;
layerWatcher: LayerWatcher;
constructor(props: any) {
super(props)
autoBind(this);
this.revisionStore = new RevisionStore()
const params = new URLSearchParams(window.location.search.substring(1))
let port = params.get("localport")
if (port == null && (window.location.port != 80 && window.location.port != 443)) {
if (port == null && (window.location.port !== "80" && window.location.port !== "443")) {
port = window.location.port
}
this.styleStore = new ApiStyleStore({
@@ -136,7 +195,7 @@ export default class App extends React.Component {
{
key: "m",
handler: () => {
document.querySelector(".mapboxgl-canvas").focus();
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
}
},
{
@@ -149,7 +208,7 @@ export default class App extends React.Component {
document.body.addEventListener("keyup", (e) => {
if(e.key === "Escape") {
e.target.blur();
(e.target as HTMLElement).blur();
document.body.focus();
}
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
@@ -159,7 +218,7 @@ export default class App extends React.Component {
if(shortcut) {
this.setModal("shortcuts", false);
shortcut.handler(e);
shortcut.handler();
}
}
})
@@ -192,8 +251,6 @@ export default class App extends React.Component {
Debug.set("maputnik", "styleStore", this.styleStore);
}
const queryObj = url.parse(window.location.href, true).query;
this.state = {
errors: [],
infos: [],
@@ -217,10 +274,9 @@ export default class App extends React.Component {
shortcuts: false,
export: false,
// TODO: Disabled for now, this should be opened on the Nth visit to the editor
survey: false,
debug: false,
},
mapboxGlDebugOptions: {
maplibreGlDebugOptions: {
showTileBoundaries: false,
showCollisionBoxes: false,
showOverdrawInspector: false,
@@ -235,25 +291,25 @@ export default class App extends React.Component {
})
}
handleKeyPress = (e) => {
handleKeyPress = (e: KeyboardEvent) => {
if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
if(e.metaKey && e.shiftKey && e.keyCode === 90) {
e.preventDefault();
this.onRedo(e);
this.onRedo();
}
else if(e.metaKey && e.keyCode === 90) {
e.preventDefault();
this.onUndo(e);
this.onUndo();
}
}
else {
if(e.ctrlKey && e.keyCode === 90) {
e.preventDefault();
this.onUndo(e);
this.onUndo();
}
else if(e.ctrlKey && e.keyCode === 89) {
e.preventDefault();
this.onRedo(e);
this.onRedo();
}
}
}
@@ -266,31 +322,31 @@ export default class App extends React.Component {
window.removeEventListener("keydown", this.handleKeyPress);
}
saveStyle(snapshotStyle) {
saveStyle(snapshotStyle: StyleSpecification & {id: string}) {
this.styleStore.save(snapshotStyle)
}
updateFonts(urlTemplate) {
const metadata = this.state.mapStyle.metadata || {}
updateFonts(urlTemplate: string) {
const metadata: {[key: string]: string} = this.state.mapStyle.metadata || {} as any
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
const glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
downloadGlyphsMetadata(glyphUrl, fonts => {
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
})
}
updateIcons(baseUrl) {
updateIcons(baseUrl: string) {
downloadSpriteMetadata(baseUrl, icons => {
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
})
}
onChangeMetadataProperty = (property, value) => {
onChangeMetadataProperty = (property: string, value: any) => {
// If we're changing renderer reset the map state.
if (
property === 'maputnik:renderer' &&
value !== get(this.state.mapStyle, ['metadata', 'maputnik:renderer'], 'mbgljs')
value !== get(this.state.mapStyle, ['metadata', 'maputnik:renderer'], 'mlgljs')
) {
this.setState({
mapState: 'map'
@@ -300,14 +356,14 @@ export default class App extends React.Component {
const changedStyle = {
...this.state.mapStyle,
metadata: {
...this.state.mapStyle.metadata,
...(this.state.mapStyle as any).metadata,
[property]: value
}
}
this.onStyleChanged(changedStyle)
}
onStyleChanged = (newStyle, opts={}) => {
onStyleChanged = (newStyle: StyleSpecification & {id: string}, opts: OnStyleChangedOpts={}) => {
opts = {
save: true,
addRevision: true,
@@ -319,16 +375,15 @@ export default class App extends React.Component {
this.getInitialStateFromUrl(newStyle);
}
const errors = validate(newStyle, latest) || [];
const errors: ValidationError[] = validateStyleMin(newStyle) || [];
// The validate function doesn't give us errors for duplicate error with
// empty string for layer.id, manually deal with that here.
const layerErrors = [];
const layerErrors: (Error | ValidationError)[] = [];
if (newStyle && newStyle.layers) {
const foundLayers = new Map();
const foundLayers = new global.Map();
newStyle.layers.forEach((layer, index) => {
if (layer.id === "" && foundLayers.has(layer.id)) {
const message = `Duplicate layer: ${formatLayerId(layer.id)}`;
const error = new Error(
`layers[${index}]: duplicate layer id [empty_string], previously used`
);
@@ -342,7 +397,7 @@ export default class App extends React.Component {
// Special case: Duplicate layer id
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
if (dupMatch) {
const [matchStr, index, message] = dupMatch;
const [, index, message] = dupMatch;
return {
message: error.message,
parsed: {
@@ -359,7 +414,7 @@ export default class App extends React.Component {
// Special case: Invalid source
const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/);
if (invalidSourceMatch) {
const [matchStr, index, message] = invalidSourceMatch;
const [, index, message] = invalidSourceMatch;
return {
message: error.message,
parsed: {
@@ -375,7 +430,7 @@ export default class App extends React.Component {
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
if (layerMatch) {
const [matchStr, index, group, property, message] = layerMatch;
const [, index, group, property, message] = layerMatch;
const key = (group && property) ? [group, property].join(".") : property;
return {
message: error.message,
@@ -396,7 +451,7 @@ export default class App extends React.Component {
}
});
let dirtyMapStyle = undefined;
let dirtyMapStyle: StyleSpecification | undefined = undefined;
if (errors.length > 0) {
dirtyMapStyle = cloneDeep(newStyle);
@@ -406,7 +461,7 @@ export default class App extends React.Component {
try {
const objPath = message.split(":")[0];
// Errors can be deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter'
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^\[]+/)[0];
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^[]+/)![0];
unset(dirtyMapStyle, unsetPath);
}
catch (err) {
@@ -417,17 +472,17 @@ export default class App extends React.Component {
}
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
this.updateFonts(newStyle.glyphs)
this.updateFonts(newStyle.glyphs as string)
}
if(newStyle.sprite !== this.state.mapStyle.sprite) {
this.updateIcons(newStyle.sprite)
this.updateIcons(newStyle.sprite as string)
}
if (opts.addRevision) {
this.revisionStore.addRevision(newStyle);
}
if (opts.save) {
this.saveStyle(newStyle);
this.saveStyle(newStyle as StyleSpecification & {id: string});
}
this.setState({
@@ -460,7 +515,7 @@ export default class App extends React.Component {
})
}
onMoveLayer = (move) => {
onMoveLayer = (move: SortEnd) => {
let { oldIndex, newIndex } = move;
let layers = this.state.mapStyle.layers;
oldIndex = clamp(oldIndex, 0, layers.length-1);
@@ -478,7 +533,7 @@ export default class App extends React.Component {
this.onLayersChange(layers);
}
onLayersChange = (changedLayers) => {
onLayersChange = (changedLayers: LayerSpecification[]) => {
const changedStyle = {
...this.state.mapStyle,
layers: changedLayers
@@ -486,15 +541,15 @@ export default class App extends React.Component {
this.onStyleChanged(changedStyle)
}
onLayerDestroy = (index) => {
let layers = this.state.mapStyle.layers;
onLayerDestroy = (index: number) => {
const layers = this.state.mapStyle.layers;
const remainingLayers = layers.slice(0);
remainingLayers.splice(index, 1);
this.onLayersChange(remainingLayers);
}
onLayerCopy = (index) => {
let layers = this.state.mapStyle.layers;
onLayerCopy = (index: number) => {
const layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0)
const clonedLayer = cloneDeep(changedLayers[index])
@@ -503,8 +558,8 @@ export default class App extends React.Component {
this.onLayersChange(changedLayers)
}
onLayerVisibilityToggle = (index) => {
let layers = this.state.mapStyle.layers;
onLayerVisibilityToggle = (index: number) => {
const layers = this.state.mapStyle.layers;
const changedLayers = layers.slice(0)
const layer = { ...changedLayers[index] }
@@ -517,7 +572,7 @@ export default class App extends React.Component {
}
onLayerIdChange = (index, oldId, newId) => {
onLayerIdChange = (index: number, _oldId: string, newId: string) => {
const changedLayers = this.state.mapStyle.layers.slice(0)
changedLayers[index] = {
...changedLayers[index],
@@ -527,27 +582,27 @@ export default class App extends React.Component {
this.onLayersChange(changedLayers)
}
onLayerChanged = (index, layer) => {
onLayerChanged = (index: number, layer: LayerSpecification) => {
const changedLayers = this.state.mapStyle.layers.slice(0)
changedLayers[index] = layer
this.onLayersChange(changedLayers)
}
setMapState = (newState) => {
setMapState = (newState: MapState) => {
this.setState({
mapState: newState
}, this.setStateInUrl);
}
setDefaultValues = (styleObj) => {
const metadata = styleObj.metadata || {}
setDefaultValues = (styleObj: StyleSpecification & {id: string}) => {
const metadata: {[key: string]: string} = styleObj.metadata || {} as any
if(metadata['maputnik:renderer'] === undefined) {
const changedStyle = {
...styleObj,
metadata: {
...styleObj.metadata,
'maputnik:renderer': 'mbgljs'
...styleObj.metadata as any,
'maputnik:renderer': 'mlgljs'
}
}
return changedStyle
@@ -556,19 +611,19 @@ export default class App extends React.Component {
}
}
openStyle = (styleObj) => {
openStyle = (styleObj: StyleSpecification & {id: string}) => {
styleObj = this.setDefaultValues(styleObj)
this.onStyleChanged(styleObj)
}
fetchSources() {
const sourceList = {};
const sourceList: {[key: string]: any} = {};
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
for(const [key, val] of Object.entries(this.state.mapStyle.sources)) {
if(
!this.state.sources.hasOwnProperty(key) &&
!Object.prototype.hasOwnProperty.call(this.state.sources, key) &&
val.type === "vector" &&
val.hasOwnProperty("url")
Object.prototype.hasOwnProperty.call(val, "url")
) {
sourceList[key] = {
type: val.type,
@@ -578,38 +633,38 @@ export default class App extends React.Component {
let url = val.url;
try {
url = setFetchAccessToken(url, this.state.mapStyle)
url = setFetchAccessToken(url!, this.state.mapStyle)
} catch(err) {
console.warn("Failed to setFetchAccessToken: ", err);
}
fetch(url, {
fetch(url!, {
mode: 'cors',
})
.then(response => response.json())
.then(json => {
.then(response => response.json())
.then(json => {
if(!json.hasOwnProperty("vector_layers")) {
return;
}
if(!Object.prototype.hasOwnProperty.call(json, "vector_layers")) {
return;
}
// Create new objects before setState
const sources = Object.assign({}, {
[key]: this.state.sources[key],
// Create new objects before setState
const sources = Object.assign({}, {
[key]: this.state.sources[key],
});
for(const layer of json.vector_layers) {
(sources[key] as any).layers.push(layer.id)
}
console.debug("Updating source: "+key);
this.setState({
sources: sources
});
})
.catch(err => {
console.error("Failed to process sources for '%s'", url, err);
});
for(let layer of json.vector_layers) {
sources[key].layers.push(layer.id)
}
console.debug("Updating source: "+key);
this.setState({
sources: sources
});
})
.catch(err => {
console.error("Failed to process sources for '%s'", url, err);
});
}
else {
sourceList[key] = this.state.sources[key] || this.state.mapStyle.sources[key];
@@ -625,11 +680,17 @@ export default class App extends React.Component {
}
_getRenderer () {
const metadata = this.state.mapStyle.metadata || {};
return metadata['maputnik:renderer'] || 'mbgljs';
const metadata: {[key:string]: string} = this.state.mapStyle.metadata || {} as any;
return metadata['maputnik:renderer'] || 'mlgljs';
}
onMapChange = (mapView) => {
onMapChange = (mapView: {
zoom: number,
center: {
lng: number,
lat: number,
},
}) => {
this.setState({
mapView,
});
@@ -637,16 +698,15 @@ export default class App extends React.Component {
mapRenderer() {
const {mapStyle, dirtyMapStyle} = this.state;
const metadata = this.state.mapStyle.metadata || {};
const mapProps = {
mapStyle: (dirtyMapStyle || mapStyle),
replaceAccessTokens: (mapStyle) => {
replaceAccessTokens: (mapStyle: StyleSpecification) => {
return style.replaceAccessTokens(mapStyle, {
allowFallback: true
});
},
onDataChange: (e) => {
onDataChange: (e: {map: Map}) => {
this.layerWatcher.analyzeMap(e.map)
this.fetchSources();
},
@@ -665,9 +725,9 @@ export default class App extends React.Component {
onLayerSelect={this.onLayerSelect}
/>
} else {
mapElement = <MapMapboxGl {...mapProps}
mapElement = <MapMaplibreGl {...mapProps}
onChange={this.onMapChange}
options={this.state.mapboxGlDebugOptions}
options={this.state.maplibreGlDebugOptions}
inspectModeEnabled={this.state.mapState === "inspect"}
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
onLayerSelect={this.onLayerSelect} />
@@ -677,12 +737,12 @@ export default class App extends React.Component {
if(this.state.mapState.match(/^filter-/)) {
filterName = this.state.mapState.replace(/^filter-/, "");
}
const elementStyle = {};
const elementStyle: {filter?: string} = {};
if (filterName) {
elementStyle.filter = `url('#${filterName}')`;
}
return <div style={elementStyle} className="maputnik-map__container">
return <div style={elementStyle} className="maputnik-map__container" data-wd-key="maplibre:container">
{mapElement}
</div>
}
@@ -695,8 +755,8 @@ export default class App extends React.Component {
url.searchParams.set("layer", `${hashVal}~${selectedLayerIndex}`);
const openModals = Object.entries(isOpen)
.map(([key, val]) => (val === true ? key : null))
.filter(val => val !== null);
.map(([key, val]) => (val === true ? key : null))
.filter(val => val !== null);
if (openModals.length > 0) {
url.searchParams.set("modal", openModals.join(","));
@@ -715,12 +775,12 @@ export default class App extends React.Component {
history.replaceState({selectedLayerIndex}, "Maputnik", url.href);
}
getInitialStateFromUrl = (mapStyle) => {
getInitialStateFromUrl = (mapStyle: StyleSpecification) => {
const url = new URL(location.href);
const modalParam = url.searchParams.get("modal");
if (modalParam && modalParam !== "") {
const modals = modalParam.split(",");
const modalObj = {};
const modalObj: {[key: string]: boolean} = {};
modals.forEach(modalName => {
modalObj[modalName] = true;
});
@@ -735,7 +795,7 @@ export default class App extends React.Component {
const view = url.searchParams.get("view");
if (view && view !== "") {
this.setMapState(view);
this.setMapState(view as MapState);
}
const path = url.searchParams.get("layer");
@@ -767,18 +827,14 @@ export default class App extends React.Component {
}
}
onLayerSelect = (index) => {
onLayerSelect = (index: number) => {
this.setState({
selectedLayerIndex: index,
selectedLayerOriginalId: this.state.mapStyle.layers[index].id,
}, this.setStateInUrl);
}
setModal(modalName, value) {
if(modalName === 'survey' && value === false) {
localStorage.setItem('survey', '');
}
setModal(modalName: keyof AppState["isOpen"], value: boolean) {
this.setState({
isOpen: {
...this.state.isOpen,
@@ -787,11 +843,11 @@ export default class App extends React.Component {
}, this.setStateInUrl)
}
toggleModal(modalName) {
toggleModal(modalName: keyof AppState["isOpen"]) {
this.setModal(modalName, !this.state.isOpen[modalName]);
}
onChangeOpenlayersDebug = (key, value) => {
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
this.setState({
openlayersDebugOptions: {
...this.state.openlayersDebugOptions,
@@ -800,10 +856,10 @@ export default class App extends React.Component {
});
}
onChangeMaboxGlDebug = (key, value) => {
onChangeMaplibreGlDebug = (key: keyof AppState["maplibreGlDebugOptions"], value: any) => {
this.setState({
mapboxGlDebugOptions: {
...this.state.mapboxGlDebugOptions,
maplibreGlDebugOptions: {
...this.state.maplibreGlDebugOptions,
[key]: value,
}
});
@@ -811,8 +867,7 @@ export default class App extends React.Component {
render() {
const layers = this.state.mapStyle.layers || []
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
const metadata = this.state.mapStyle.metadata || {}
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : undefined
const toolbar = <AppToolbar
renderer={this._getRenderer()}
@@ -855,7 +910,7 @@ export default class App extends React.Component {
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
onLayerIdChange={this.onLayerIdChange}
errors={this.state.errors}
/> : null
/> : undefined
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
currentLayer={selectedLayer}
@@ -864,22 +919,21 @@ export default class App extends React.Component {
mapStyle={this.state.mapStyle}
errors={this.state.errors}
infos={this.state.infos}
/> : null
/> : undefined
const modals = <div>
<ModalDebug
renderer={this._getRenderer()}
mapboxGlDebugOptions={this.state.mapboxGlDebugOptions}
maplibreGlDebugOptions={this.state.maplibreGlDebugOptions}
openlayersDebugOptions={this.state.openlayersDebugOptions}
onChangeMaboxGlDebug={this.onChangeMaboxGlDebug}
onChangeMaplibreGlDebug={this.onChangeMaplibreGlDebug}
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
isOpen={this.state.isOpen.debug}
onOpenToggle={this.toggleModal.bind(this, 'debug')}
mapView={this.state.mapView}
/>
<ModalShortcuts
ref={(el) => this.shortcutEl = el}
isOpen={this.state.isOpen.shortcuts}
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
/>
@@ -889,7 +943,6 @@ export default class App extends React.Component {
onChangeMetadataProperty={this.onChangeMetadataProperty}
isOpen={this.state.isOpen.settings}
onOpenToggle={this.toggleModal.bind(this, 'settings')}
openlayersDebugOptions={this.state.openlayersDebugOptions}
/>
<ModalExport
mapStyle={this.state.mapStyle}
@@ -908,10 +961,6 @@ export default class App extends React.Component {
isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')}
/>
<ModalSurvey
isOpen={this.state.isOpen.survey}
onOpenToggle={this.toggleModal.bind(this, 'survey')}
/>
</div>
return <AppLayout

View File

@@ -1,46 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import ScrollContainer from './ScrollContainer'
class AppLayout extends React.Component {
static propTypes = {
toolbar: PropTypes.element.isRequired,
layerList: PropTypes.element.isRequired,
layerEditor: PropTypes.element,
map: PropTypes.element.isRequired,
bottom: PropTypes.element,
modals: PropTypes.node,
}
static childContextTypes = {
reactIconBase: PropTypes.object
}
getChildContext() {
return {
reactIconBase: { size: 14 }
}
}
render() {
return <div className="maputnik-layout">
{this.props.toolbar}
<div className="maputnik-layout-list">
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
{this.props.map}
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
</div>
}
{this.props.modals}
</div>
}
}
export default AppLayout

View File

@@ -0,0 +1,52 @@
import React from 'react'
import PropTypes from 'prop-types'
import ScrollContainer from './ScrollContainer'
import { WithTranslation, withTranslation } from 'react-i18next';
type AppLayoutInternalProps = {
toolbar: React.ReactElement
layerList: React.ReactElement
layerEditor?: React.ReactElement
map: React.ReactElement
bottom?: React.ReactElement
modals?: React.ReactNode
} & WithTranslation;
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
static childContextTypes = {
reactIconBase: PropTypes.object
}
getChildContext() {
return {
reactIconBase: { size: 14 }
}
}
render() {
document.body.dir = this.props.i18n.dir();
return <div className="maputnik-layout">
{this.props.toolbar}
<div className="maputnik-layout-main">
<div className="maputnik-layout-list">
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
{this.props.map}
</div>
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
</div>
}
{this.props.modals}
</div>
}
}
const AppLayout = withTranslation()(AppLayoutInternal);
export default AppLayout;

View File

@@ -1,62 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import {formatLayerId} from '../util/format';
export default class AppMessagePanel extends React.Component {
static propTypes = {
errors: PropTypes.array,
infos: PropTypes.array,
mapStyle: PropTypes.object,
onLayerSelect: PropTypes.func,
currentLayer: PropTypes.object,
selectedLayerIndex: PropTypes.number,
}
static defaultProps = {
onLayerSelect: () => {},
}
render() {
const {selectedLayerIndex} = this.props;
const errors = this.props.errors.map((error, idx) => {
let content;
if (error.parsed && error.parsed.type === "layer") {
const {parsed} = error;
const {mapStyle, currentLayer} = this.props;
const layerId = mapStyle.layers[parsed.data.index].id;
content = (
<>
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
{selectedLayerIndex !== parsed.data.index &&
<>
&nbsp;&mdash;&nbsp;
<button
className="maputnik-message-panel__switch-button"
onClick={() => this.props.onLayerSelect(parsed.data.index)}
>
switch to layer
</button>
</>
}
</>
);
}
else {
content = error.message;
}
return <p key={"error-"+idx} className="maputnik-message-panel-error">
{content}
</p>
})
const infos = this.props.infos.map((m, i) => {
return <p key={"info-"+i}>{m}</p>
})
return <div className="maputnik-message-panel">
{errors}
{infos}
</div>
}
}

View File

@@ -0,0 +1,66 @@
import React from 'react'
import {formatLayerId} from '../libs/format';
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
type AppMessagePanelInternalProps = {
errors?: unknown[]
infos?: string[]
mapStyle?: StyleSpecification
onLayerSelect?(...args: unknown[]): unknown
currentLayer?: LayerSpecification
selectedLayerIndex?: number
} & WithTranslation;
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
static defaultProps = {
onLayerSelect: () => {},
}
render() {
const {t, selectedLayerIndex} = this.props;
const errors = this.props.errors?.map((error: any, idx) => {
let content;
if (error.parsed && error.parsed.type === "layer") {
const {parsed} = error;
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
content = (
<>
<Trans t={t}>
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
</Trans>
{selectedLayerIndex !== parsed.data.index &&
<>
&nbsp;&mdash;&nbsp;
<button
className="maputnik-message-panel__switch-button"
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
>
{t("switch to layer")}
</button>
</>
}
</>
);
}
else {
content = error.message;
}
return <p key={"error-"+idx} className="maputnik-message-panel-error">
{content}
</p>
})
const infos = this.props.infos?.map((m, i) => {
return <p key={"info-"+i}>{m}</p>
})
return <div className="maputnik-message-panel">
{errors}
{infos}
</div>
}
}
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
export default AppMessagePanel;

View File

@@ -1,78 +1,57 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import {detect} from 'detect-browser';
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdAssignmentTurnedIn} from 'react-icons/md'
import logoImage from 'maputnik-design/logos/logo-color.svg'
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdLanguage} from 'react-icons/md'
import pkgJson from '../../package.json'
//@ts-ignore
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
import { withTranslation, WithTranslation } from 'react-i18next';
import { supportedLanguages } from '../i18n';
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
const browser = detect();
const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser.name) > -1;
const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser!.name) > -1;
class IconText extends React.Component {
static propTypes = {
children: PropTypes.node,
}
type IconTextProps = {
children?: React.ReactNode
};
class IconText extends React.Component<IconTextProps> {
render() {
return <span className="maputnik-icon-text">{this.props.children}</span>
}
}
class ToolbarLink extends React.Component {
static propTypes = {
className: PropTypes.string,
children: PropTypes.node,
href: PropTypes.string,
onToggleModal: PropTypes.func,
}
type ToolbarLinkProps = {
className?: string
children?: React.ReactNode
href?: string
onToggleModal?(...args: unknown[]): unknown
};
class ToolbarLink extends React.Component<ToolbarLinkProps> {
render() {
return <a
className={classnames('maputnik-toolbar-link', this.props.className)}
href={this.props.href}
rel="noopener noreferrer"
target="_blank"
data-wd-key="toolbar:link"
>
{this.props.children}
</a>
}
}
class ToolbarLinkHighlighted extends React.Component {
static propTypes = {
className: PropTypes.string,
children: PropTypes.node,
href: PropTypes.string,
onToggleModal: PropTypes.func
}
render() {
return <a
className={classnames('maputnik-toolbar-link', "maputnik-toolbar-link--highlighted", this.props.className)}
href={this.props.href}
rel="noopener noreferrer"
target="_blank"
>
<span className="maputnik-toolbar-link-wrapper">
{this.props.children}
</span>
</a>
}
}
class ToolbarSelect extends React.Component {
static propTypes = {
children: PropTypes.node,
wdKey: PropTypes.string
}
type ToolbarSelectProps = {
children?: React.ReactNode
wdKey?: string
};
class ToolbarSelect extends React.Component<ToolbarSelectProps> {
render() {
return <div
className='maputnik-toolbar-select'
@@ -83,13 +62,13 @@ class ToolbarSelect extends React.Component {
}
}
class ToolbarAction extends React.Component {
static propTypes = {
children: PropTypes.node,
onClick: PropTypes.func,
wdKey: PropTypes.string
}
type ToolbarActionProps = {
children?: React.ReactNode
onClick?(...args: unknown[]): unknown
wdKey?: string
};
class ToolbarAction extends React.Component<ToolbarActionProps> {
render() {
return <button
className='maputnik-toolbar-action'
@@ -101,22 +80,24 @@ class ToolbarAction extends React.Component {
}
}
export default class AppToolbar extends React.Component {
static propTypes = {
mapStyle: PropTypes.object.isRequired,
inspectModeEnabled: PropTypes.bool.isRequired,
onStyleChanged: PropTypes.func.isRequired,
// A new style has been uploaded
onStyleOpen: PropTypes.func.isRequired,
// A dict of source id's and the available source layers
sources: PropTypes.object.isRequired,
children: PropTypes.node,
onToggleModal: PropTypes.func,
onSetMapState: PropTypes.func,
mapState: PropTypes.string,
renderer: PropTypes.string,
}
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
type AppToolbarInternalProps = {
mapStyle: object
inspectModeEnabled: boolean
onStyleChanged(...args: unknown[]): unknown
// A new style has been uploaded
onStyleOpen(...args: unknown[]): unknown
// A dict of source id's and the available source layers
sources: object
children?: React.ReactNode
onToggleModal(...args: unknown[]): unknown
onSetMapState(mapState: MapState): unknown
mapState?: MapState
renderer?: string
} & WithTranslation;
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
state = {
isOpen: {
settings: false,
@@ -127,55 +108,60 @@ export default class AppToolbar extends React.Component {
}
}
handleSelection(val) {
handleSelection(val: MapState) {
this.props.onSetMapState(val);
}
onSkip = (target) => {
handleLanguageChange(val: string) {
this.props.i18n.changeLanguage(val);
}
onSkip = (target: string) => {
if (target === "map") {
document.querySelector(".mapboxgl-canvas").focus();
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
}
else {
const el = document.querySelector("#skip-target-"+target);
const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement;
el.focus();
}
}
render() {
const t = this.props.t;
const views = [
{
id: "map",
group: "general",
title: "Map",
title: t("Map"),
},
{
id: "inspect",
group: "general",
title: "Inspect",
disabled: this.props.renderer !== 'mbgljs',
title: t("Inspect"),
disabled: this.props.renderer === 'ol',
},
{
id: "filter-deuteranopia",
group: "color-accessibility",
title: "Deuteranopia filter",
title: t("Deuteranopia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-protanopia",
group: "color-accessibility",
title: "Protanopia filter",
title: t("Protanopia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-tritanopia",
group: "color-accessibility",
title: "Tritanopia filter",
title: t("Tritanopia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-achromatopsia",
group: "color-accessibility",
title: "Achromatopsia filter",
title: t("Achromatopsia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
];
@@ -193,31 +179,31 @@ export default class AppToolbar extends React.Component {
<button
data-wd-key="root:skip:layer-list"
className="maputnik-toolbar-skip"
onClick={e => this.onSkip("layer-list")}
onClick={_e => this.onSkip("layer-list")}
>
Layers list
{t("Layers list")}
</button>
<button
data-wd-key="root:skip:layer-editor"
className="maputnik-toolbar-skip"
onClick={e => this.onSkip("layer-editor")}
onClick={_e => this.onSkip("layer-editor")}
>
Layer editor
{t("Layer editor")}
</button>
<button
data-wd-key="root:skip:map-view"
className="maputnik-toolbar-skip"
onClick={e => this.onSkip("map")}
onClick={_e => this.onSkip("map")}
>
Map view
{t("Map view")}
</button>
<a
className="maputnik-toolbar-logo"
target="blank"
rel="noreferrer noopener"
href="https://github.com/maputnik/editor"
href="https://github.com/maplibre/maputnik"
>
<span dangerouslySetInnerHTML={{__html: logoImage}} />
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} />
<h1>
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
@@ -227,37 +213,38 @@ export default class AppToolbar extends React.Component {
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
<MdOpenInBrowser />
<IconText>Open</IconText>
<IconText>{t("Open")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
<MdFileDownload />
<IconText>Export</IconText>
<IconText>{t("Export")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
<MdLayers />
<IconText>Data Sources</IconText>
<IconText>{t("Data Sources")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
<MdSettings />
<IconText>Style Settings</IconText>
<IconText>{t("Style Settings")}</IconText>
</ToolbarAction>
<ToolbarSelect wdKey="nav:inspect">
<MdFindInPage />
<label>View
<label>{t("View")}
<select
className="maputnik-select"
onChange={(e) => this.handleSelection(e.target.value)}
value={currentView.id}
data-wd-key="maputnik-select"
onChange={(e) => this.handleSelection(e.target.value as MapState)}
value={currentView?.id}
>
{views.filter(v => v.group === "general").map((item) => {
return (
<option key={item.id} value={item.id} disabled={item.disabled}>
<option key={item.id} value={item.id} disabled={item.disabled} data-wd-key={item.id}>
{item.title}
</option>
);
})}
<optgroup label="Color accessibility">
<optgroup label={t("Color accessibility")}>
{views.filter(v => v.group === "color-accessibility").map((item) => {
return (
<option key={item.id} value={item.id} disabled={item.disabled}>
@@ -270,16 +257,35 @@ export default class AppToolbar extends React.Component {
</label>
</ToolbarSelect>
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
<ToolbarSelect wdKey="nav:language">
<MdLanguage />
<label>{t("Language")}
<select
className="maputnik-select"
data-wd-key="maputnik-lang-select"
onChange={(e) => this.handleLanguageChange(e.target.value)}
value={this.props.i18n.language}
>
{Object.entries(supportedLanguages).map(([code, name]) => {
return (
<option key={code} value={code}>
{name}
</option>
);
})}
</select>
</label>
</ToolbarSelect>
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
<MdHelpOutline />
<IconText>Help</IconText>
<IconText>{t("Help")}</IconText>
</ToolbarLink>
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
<MdAssignmentTurnedIn />
<IconText>Take the Maputnik Survey</IconText>
</ToolbarLinkHighlighted>
</div>
</div>
</nav>
}
}
const AppToolbar = withTranslation()(AppToolbarInternal);
export default AppToolbar;

View File

@@ -1,40 +1,43 @@
import React from 'react'
import PropTypes from 'prop-types'
import React, {PropsWithChildren, SyntheticEvent} from 'react'
import classnames from 'classnames'
import FieldDocLabel from './FieldDocLabel'
import Doc from './Doc'
/** Wrap a component with a label */
export default class Block extends React.Component {
static propTypes = {
"data-wd-key": PropTypes.string,
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
]),
action: PropTypes.element,
children: PropTypes.node.isRequired,
style: PropTypes.object,
onChange: PropTypes.func,
fieldSpec: PropTypes.object,
wideMode: PropTypes.bool,
error: PropTypes.array,
}
type BlockProps = PropsWithChildren & {
"data-wd-key"?: string
label?: string
action?: React.ReactElement
style?: object
onChange?(...args: unknown[]): unknown
fieldSpec?: object
wideMode?: boolean
error?: {message: string}
};
constructor (props) {
type BlockState = {
showDoc: boolean
};
/** Wrap a component with a label */
export default class Block extends React.Component<BlockProps, BlockState> {
_blockEl: HTMLDivElement | null = null;
constructor (props: BlockProps) {
super(props);
this.state = {
showDoc: false,
}
}
onChange(e) {
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) {
const value = e.target.value
return this.props.onChange(value === "" ? undefined : value)
if (this.props.onChange) {
return this.props.onChange(value === "" ? undefined : value)
}
}
onToggleDoc = (val) => {
onToggleDoc = (val: boolean) => {
this.setState({
showDoc: val
});
@@ -46,20 +49,17 @@ export default class Block extends React.Component {
* causing the picker to reopen. This causes a scenario where the picker can
* never be closed once open.
*/
onLabelClick = (event) => {
onLabelClick = (event: SyntheticEvent<any, any>) => {
const el = event.nativeEvent.target;
const nativeEvent = event.nativeEvent;
const contains = this._blockEl.contains(el);
const contains = this._blockEl?.contains(el);
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
event.stopPropagation();
}
event.preventDefault();
event.preventDefault();
}
render() {
const errors = [].concat(this.props.error || []);
return <label style={this.props.style}
data-wd-key={this.props["data-wd-key"]}
className={classnames({

View File

@@ -1,21 +1,21 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Collapse as ReactCollapse } from 'react-collapse'
import accessibility from '../../libs/accessibility'
import {reducedMotionEnabled} from '../libs/accessibility'
export default class Collapse extends React.Component {
static propTypes = {
isActive: PropTypes.bool.isRequired,
children: PropTypes.element.isRequired
}
type CollapseProps = {
isActive: boolean
children: React.ReactElement
};
export default class Collapse extends React.Component<CollapseProps> {
static defaultProps = {
isActive: true
}
render() {
if (accessibility.reducedMotionEnabled()) {
if (reducedMotionEnabled()) {
return (
<div style={{display: this.props.isActive ? "block" : "none"}}>
{this.props.children}

View File

@@ -1,13 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
import {MdArrowDropDown, MdArrowDropUp} from 'react-icons/md'
export default class Collapser extends React.Component {
static propTypes = {
isCollapsed: PropTypes.bool.isRequired,
style: PropTypes.object,
}
type CollapserProps = {
isCollapsed: boolean
style?: object
};
export default class Collapser extends React.Component<CollapserProps> {
render() {
const iconStyle = {
width: 20,

View File

@@ -1,36 +1,45 @@
import React from 'react'
import PropTypes from 'prop-types'
export default class Doc extends React.Component {
static propTypes = {
fieldSpec: PropTypes.object.isRequired,
const headers = {
js: "JS",
android: "Android",
ios: "iOS",
macos: "macOS",
};
type DocProps = {
fieldSpec: {
doc?: string
values?: {
[key: string]: {
doc?: string
}
}
'sdk-support'?: {
[key: string]: typeof headers
}
}
};
export default class Doc extends React.Component<DocProps> {
render () {
const {fieldSpec} = this.props;
const {doc, values} = fieldSpec;
const sdkSupport = fieldSpec['sdk-support'];
const headers = {
js: "JS",
android: "Android",
ios: "iOS",
macos: "macOS",
};
const renderValues = (
!!values &&
// HACK: Currently we merge additional values into the stylespec, so this is required
// See <https://github.com/maputnik/editor/blob/master/src/components/fields/PropertyGroup.jsx#L16>
// HACK: Currently we merge additional values into the style spec, so this is required
// See <https://github.com/maplibre/maputnik/blob/main/src/components/PropertyGroup.jsx#L16>
!Array.isArray(values)
);
return (
<>
{doc &&
{doc &&
<div className="SpecDoc">
<div className="SpecDoc__doc">{doc}</div>
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>{doc}</div>
{renderValues &&
<ul className="SpecDoc__values">
{Object.entries(values).map(([key, value]) => {
@@ -61,10 +70,9 @@ export default class Doc extends React.Component {
return (
<tr key={key}>
<td>{key}</td>
{Object.keys(headers).map(k => {
const value = supportObj[k];
if (supportObj.hasOwnProperty(k)) {
return <td key={k}>{supportObj[k]}</td>;
{Object.keys(headers).map((k) => {
if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
return <td key={k}>{supportObj[k as keyof typeof headers]}</td>;
}
else {
return <td key={k}>no</td>;
@@ -80,4 +88,4 @@ export default class Doc extends React.Component {
</>
);
}
}
}

View File

@@ -1,21 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputArray from './InputArray'
import Fieldset from './Fieldset'
export default class FieldArray extends React.Component {
static propTypes = {
...InputArray.propTypes,
name: PropTypes.string,
}
render() {
const {props} = this;
return <Fieldset label={props.label}>
<InputArray {...props} />
</Fieldset>
}
}

View File

@@ -0,0 +1,19 @@
import React from 'react'
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
import Fieldset from './Fieldset'
type FieldArrayProps = InputArrayProps & {
name?: string
fieldSpec?: {
doc: string
}
};
export default class FieldArray extends React.Component<FieldArrayProps> {
render() {
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputArray {...this.props} />
</Fieldset>
}
}

View File

@@ -1,20 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
export default class FieldAutocomplete extends React.Component {
static propTypes = {
...InputAutocomplete.propTypes,
}
render() {
const {props} = this;
return <Block label={props.label}>
<InputAutocomplete {...props} />
</Block>
}
}

View File

@@ -0,0 +1,18 @@
import React from 'react'
import Block from './Block'
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
type FieldAutocompleteProps = InputAutocompleteProps & {
label?: string;
};
export default class FieldAutocomplete extends React.Component<FieldAutocompleteProps> {
render() {
return <Block label={this.props.label}>
<InputAutocomplete {...this.props} />
</Block>
}
}

View File

@@ -1,20 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputCheckbox from './InputCheckbox'
export default class FieldCheckbox extends React.Component {
static propTypes = {
...InputCheckbox.propTypes,
}
render() {
const {props} = this;
return <Block label={this.props.label}>
<InputCheckbox {...props} />
</Block>
}
}

View File

@@ -0,0 +1,18 @@
import React from 'react'
import Block from './Block'
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
type FieldCheckboxProps = InputCheckboxProps & {
label?: string;
};
export default class FieldCheckbox extends React.Component<FieldCheckboxProps> {
render() {
return <Block label={this.props.label}>
<InputCheckbox {...this.props} />
</Block>
}
}

View File

@@ -1,20 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputColor from './InputColor'
export default class FieldColor extends React.Component {
static propTypes = {
...InputColor.propTypes,
}
render() {
const {props} = this;
return <Block label={props.label}>
<InputColor {...props} />
</Block>
}
}

View File

@@ -0,0 +1,21 @@
import React from 'react'
import Block from './Block'
import InputColor, {InputColorProps} from './InputColor'
type FieldColorProps = InputColorProps & {
label?: string
fieldSpec?: {
doc: string
}
};
export default class FieldColor extends React.Component<FieldColorProps> {
render() {
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputColor {...this.props} />
</Block>
}
}

View File

@@ -1,31 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputString from './InputString'
export default class FieldComment extends React.Component {
static propTypes = {
value: PropTypes.string,
onChange: PropTypes.func.isRequired,
}
render() {
const fieldSpec = {
doc: "Comments for the current layer. This is non-standard and not in the spec."
};
return <Block
label={"Comments"}
fieldSpec={fieldSpec}
data-wd-key="layer-comment"
>
<InputString
multi={true}
value={this.props.value}
onChange={this.props.onChange}
default="Comment..."
/>
</Block>
}
}

View File

@@ -0,0 +1,38 @@
import React from 'react'
import Block from './Block'
import InputString from './InputString'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldCommentInternalProps = {
value?: string
onChange(value: string | undefined): unknown
error: {message: string}
} & WithTranslation;
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
render() {
const t = this.props.t;
const fieldSpec = {
doc: t("Comments for the current layer. This is non-standard and not in the spec."),
};
return <Block
label={t("Comments")}
fieldSpec={fieldSpec}
data-wd-key="layer-comment"
error={this.props.error}
>
<InputString
multi={true}
value={this.props.value}
onChange={this.props.onChange}
default={t("Comment...")}
data-wd-key="layer-comment.input"
/>
</Block>
}
}
const FieldComment = withTranslation()(FieldCommentInternal);
export default FieldComment;

View File

@@ -1,26 +1,27 @@
import React from 'react'
import PropTypes from 'prop-types'
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
export default class FieldDocLabel extends React.Component {
static propTypes = {
label: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string
]).isRequired,
fieldSpec: PropTypes.object,
onToggleDoc: PropTypes.func,
type FieldDocLabelProps = {
label: JSX.Element | string | undefined
fieldSpec?: {
doc?: string
}
onToggleDoc?(...args: unknown[]): unknown
};
constructor (props) {
type FieldDocLabelState = {
open: boolean
};
export default class FieldDocLabel extends React.Component<FieldDocLabelProps, FieldDocLabelState> {
constructor (props: FieldDocLabelProps) {
super(props);
this.state = {
open: false,
}
}
onToggleDoc = (open) => {
onToggleDoc = (open: boolean) => {
this.setState({
open,
}, () => {
@@ -43,6 +44,7 @@ export default class FieldDocLabel extends React.Component {
aria-label={this.state.open ? "close property documentation" : "open property documentation"}
className={`maputnik-doc-button maputnik-doc-button--${this.state.open ? 'open' : 'closed'}`}
onClick={() => this.onToggleDoc(!this.state.open)}
data-wd-key={'field-doc-button-'+label}
>
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
</button>

View File

@@ -1,21 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputDynamicArray from './InputDynamicArray'
import Fieldset from './Fieldset'
export default class FieldDynamicArray extends React.Component {
static propTypes = {
...InputDynamicArray.propTypes,
name: PropTypes.string,
}
render() {
const {props} = this;
return <Fieldset label={props.label}>
<InputDynamicArray {...props} />
</Fieldset>
}
}

View File

@@ -0,0 +1,16 @@
import React from 'react'
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
import Fieldset from './Fieldset'
type FieldDynamicArrayProps = InputDynamicArrayProps & {
name?: string
};
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
render() {
return <Fieldset label={this.props.label}>
<InputDynamicArray {...this.props} />
</Fieldset>
}
}

View File

@@ -1,20 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputEnum from './InputEnum'
import Block from './Block';
import Fieldset from './Fieldset';
export default class FieldEnum extends React.Component {
static propTypes = {
...InputEnum.propTypes,
}
render() {
const {props} = this;
return <Fieldset label={props.label}>
<InputEnum {...props} />
</Fieldset>
}
}

View File

@@ -0,0 +1,20 @@
import React from 'react'
import InputEnum, {InputEnumProps} from './InputEnum'
import Fieldset from './Fieldset';
type FieldEnumProps = InputEnumProps & {
label?: string;
fieldSpec?: {
doc: string
}
};
export default class FieldEnum extends React.Component<FieldEnumProps> {
render() {
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputEnum {...this.props} />
</Fieldset>
}
}

View File

@@ -1,19 +1,18 @@
import React from 'react'
import PropTypes from 'prop-types'
import SpecProperty from './_SpecProperty'
import DataProperty from './_DataProperty'
import DataProperty, { Stop } from './_DataProperty'
import ZoomProperty from './_ZoomProperty'
import ExpressionProperty from './_ExpressionProperty'
import {function as styleFunction} from '@maplibre/maplibre-gl-style-spec';
import {findDefaultFromSpec} from '../util/spec-helper';
import {findDefaultFromSpec} from '../libs/spec-helper';
function isLiteralExpression (value) {
function isLiteralExpression(value: any) {
return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
}
function isGetExpression (value) {
function isGetExpression(value: any) {
return (
Array.isArray(value) &&
value.length === 2 &&
@@ -21,14 +20,14 @@ function isGetExpression (value) {
);
}
function isZoomField(value) {
function isZoomField(value: any) {
return (
typeof(value) === 'object' &&
value.stops &&
typeof(value.property) === 'undefined' &&
Array.isArray(value.stops) &&
value.stops.length > 1 &&
value.stops.every(stop => {
value.stops.every((stop: Stop) => {
return (
Array.isArray(stop) &&
stop.length === 2
@@ -37,22 +36,22 @@ function isZoomField(value) {
);
}
function isIdentityProperty (value) {
function isIdentityProperty(value: any) {
return (
typeof(value) === 'object' &&
value.type === "identity" &&
value.hasOwnProperty("property")
Object.prototype.hasOwnProperty.call(value, "property")
);
}
function isDataStopProperty (value) {
function isDataStopProperty(value: any) {
return (
typeof(value) === 'object' &&
value.stops &&
typeof(value.property) !== 'undefined' &&
value.stops.length > 1 &&
Array.isArray(value.stops) &&
value.stops.every(stop => {
value.stops.every((stop: Stop) => {
return (
Array.isArray(stop) &&
stop.length === 2 &&
@@ -62,26 +61,26 @@ function isDataStopProperty (value) {
);
}
function isDataField(value) {
function isDataField(value: any) {
return (
isIdentityProperty(value) ||
isDataStopProperty(value)
);
}
function isPrimative (value) {
function isPrimative(value: any): value is string | boolean | number {
const valid = ["string", "boolean", "number"];
return valid.includes(typeof(value));
}
function isArrayOfPrimatives (values) {
function isArrayOfPrimatives(values: any): values is Array<string | boolean | number> {
if (Array.isArray(values)) {
return values.every(isPrimative);
}
return false;
}
function getDataType (value, fieldSpec={}) {
function getDataType(value: any, fieldSpec={} as any) {
if (value === undefined) {
return "value";
}
@@ -103,35 +102,33 @@ function getDataType (value, fieldSpec={}) {
}
type FieldFunctionProps = {
onChange(fieldName: string, value: any): unknown
fieldName: string
fieldType: string
fieldSpec: any
errors?: {[key: string]: {message: string}}
value?: any
};
type FieldFunctionState = {
dataType: string
isEditing: boolean
}
/** Supports displaying spec field for zoom function objects
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
*/
export default class FieldFunction extends React.Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
fieldName: PropTypes.string.isRequired,
fieldType: PropTypes.string.isRequired,
fieldSpec: PropTypes.object.isRequired,
errors: PropTypes.object,
value: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string,
PropTypes.number,
PropTypes.bool,
PropTypes.array
]),
}
constructor (props) {
super();
export default class FieldFunction extends React.Component<FieldFunctionProps, FieldFunctionState> {
constructor (props: FieldFunctionProps) {
super(props);
this.state = {
dataType: getDataType(props.value, props.fieldSpec),
isEditing: false,
}
}
static getDerivedStateFromProps(props, state) {
static getDerivedStateFromProps(props: Readonly<FieldFunctionProps>, state: FieldFunctionState) {
// Because otherwise when editing values we end up accidentally changing field type.
if (state.isEditing) {
return {};
@@ -144,7 +141,7 @@ export default class FieldFunction extends React.Component {
}
}
getFieldFunctionType(fieldSpec) {
getFieldFunctionType(fieldSpec: any) {
if (fieldSpec.expression.interpolated) {
return "exponential"
}
@@ -183,7 +180,7 @@ export default class FieldFunction extends React.Component {
});
}
deleteStop = (stopIdx) => {
deleteStop = (stopIdx: number) => {
const stops = this.props.value.stops.slice(0)
stops.splice(stopIdx, 1)
@@ -207,7 +204,7 @@ export default class FieldFunction extends React.Component {
if (value.stops) {
zoomFunc = {
base: value.base,
stops: value.stops.map(stop => {
stops: value.stops.map((stop: Stop) => {
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
})
}
@@ -292,7 +289,7 @@ export default class FieldFunction extends React.Component {
property: "",
type: functionType,
base: value.base,
stops: value.stops.map(stop => {
stops: value.stops.map((stop: Stop) => {
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
})
}
@@ -402,7 +399,7 @@ export default class FieldFunction extends React.Component {
/>
)
}
return <div className={propClass} data-wd-key={"spec-field:"+this.props.fieldName}>
return <div className={propClass} data-wd-key={"spec-field-container:"+this.props.fieldName}>
{specField}
</div>
}

View File

@@ -1,27 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputString from './InputString'
export default class FieldId extends React.Component {
static propTypes = {
value: PropTypes.string.isRequired,
wdKey: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
error: PropTypes.object,
}
render() {
return <Block label={"ID"} fieldSpec={latest.layer.id}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
<InputString
value={this.props.value}
onInput={this.props.onChange}
/>
</Block>
}
}

View File

@@ -0,0 +1,28 @@
import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputString from './InputString'
type FieldIdProps = {
value: string
wdKey: string
onChange(value: string | undefined): unknown
error?: {message: string}
};
export default class FieldId extends React.Component<FieldIdProps> {
render() {
return <Block label="ID" fieldSpec={latest.layer.id}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
<InputString
value={this.props.value}
onInput={this.props.onChange}
data-wd-key={this.props.wdKey + ".input"}
/>
</Block>
}
}

View File

@@ -1,16 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputJson from './InputJson'
export default class FieldJson extends React.Component {
static propTypes = {
...InputJson.propTypes,
}
render() {
const {props} = this;
return <InputJson {...props} />
}
}

View File

@@ -0,0 +1,13 @@
import React from 'react'
import InputJson, {InputJsonProps} from './InputJson'
type FieldJsonProps = InputJsonProps & {};
export default class FieldJson extends React.Component<FieldJsonProps> {
render() {
return <InputJson {...this.props} />
}
}

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