mirror of
https://github.com/maputnik/editor.git
synced 2025-12-07 23:00:01 +00:00
Compare commits
507 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
502586e5d5 | ||
|
|
d92d599d8a | ||
|
|
3487056c7d | ||
|
|
dbcfb08c15 | ||
|
|
e96141090e | ||
|
|
5bd25fc2ed | ||
|
|
334932b298 | ||
|
|
661006d7fb | ||
|
|
c917249517 | ||
|
|
d0ca732fe7 | ||
|
|
52821cd1df | ||
|
|
328e0b8ff7 | ||
|
|
f0147cc89a | ||
|
|
78a7f152e7 | ||
|
|
e936dd16bf | ||
|
|
3d4579288c | ||
|
|
b60df8b074 | ||
|
|
c4b92fa0a9 | ||
|
|
9808d44c71 | ||
|
|
1bdd135386 | ||
|
|
740a75f2e6 | ||
|
|
b62533fa3e | ||
|
|
044349e65f | ||
|
|
e8b0bd4d0a | ||
|
|
1805aee7ba | ||
|
|
8ba2123a26 | ||
|
|
687c08527d | ||
|
|
f0744f024d | ||
|
|
9e82599464 | ||
|
|
7a60df370e | ||
|
|
aee4a041fe | ||
|
|
6fa06e5483 | ||
|
|
15962481ee | ||
|
|
6bf695cd4b | ||
|
|
7ecbc14c39 | ||
|
|
fb0e531f4a | ||
|
|
bd44e6d071 | ||
|
|
3ae37f1c46 | ||
|
|
8c7a1f7075 | ||
|
|
3e97d8a5f1 | ||
|
|
6138257a89 | ||
|
|
0bd62985b9 | ||
|
|
a346d757fd | ||
|
|
84f3970730 | ||
|
|
050e22918a | ||
|
|
f205776695 | ||
|
|
4d427bcbc3 | ||
|
|
0b4910e3c3 | ||
|
|
11a59debdf | ||
|
|
dbe2c2637e | ||
|
|
d6ce13c356 | ||
|
|
6d094a8b3e | ||
|
|
4d0456fd68 | ||
|
|
ad83f940a7 | ||
|
|
edc7e02f58 | ||
|
|
7dfc5029a3 | ||
|
|
8e02722b52 | ||
|
|
984581e01a | ||
|
|
1de7ba7e86 | ||
|
|
a3fa86f7ee | ||
|
|
a589f89c4c | ||
|
|
3b599aed4c | ||
|
|
6953db74c6 | ||
|
|
1ad473a539 | ||
|
|
fafda9ec92 | ||
|
|
11b85bf565 | ||
|
|
6ecc6670dc | ||
|
|
553f0fe23e | ||
|
|
77ddf67201 | ||
|
|
a092bc2689 | ||
|
|
38e0786463 | ||
|
|
180b17d315 | ||
|
|
8acbd784a0 | ||
|
|
07efe1e1b8 | ||
|
|
7ea53cc3a1 | ||
|
|
de21eea21b | ||
|
|
8f8ed6dff3 | ||
|
|
8915bbfeb4 | ||
|
|
df3a42acce | ||
|
|
2a7ef82d23 | ||
|
|
95168f22e3 | ||
|
|
4360753263 | ||
|
|
ad491cb465 | ||
|
|
e5bed80c96 | ||
|
|
9bf3046d4c | ||
|
|
da8dc0f7a6 | ||
|
|
b66a4afd28 | ||
|
|
a94c53534c | ||
|
|
6b22c9130f | ||
|
|
7d5927bbc8 | ||
|
|
240d02a124 | ||
|
|
92ef1c4cbb | ||
|
|
5ce57d0803 | ||
|
|
1c134d757c | ||
|
|
32d808b230 | ||
|
|
ee3def492a | ||
|
|
41bd91fcd2 | ||
|
|
02c8542848 | ||
|
|
844abd38ce | ||
|
|
d9b6f28bb5 | ||
|
|
ed85b838ec | ||
|
|
f82b138a3d | ||
|
|
89c38991b9 | ||
|
|
0e4c06cc3e | ||
|
|
7e510a2582 | ||
|
|
f3cb9c4fdd | ||
|
|
f0f6130272 | ||
|
|
0ebb299fd0 | ||
|
|
9d96525f12 | ||
|
|
fc6f9251f7 | ||
|
|
53cb317155 | ||
|
|
4215b5808f | ||
|
|
2d2f9744e2 | ||
|
|
d0b835ee52 | ||
|
|
1798305f9c | ||
|
|
4b0768d0a6 | ||
|
|
2e79a8ff4c | ||
|
|
e64ca3eb93 | ||
|
|
094c4747d3 | ||
|
|
62f0843283 | ||
|
|
8062e304b7 | ||
|
|
18e7ead78a | ||
|
|
3cab1dc49f | ||
|
|
f8dcbb8fb7 | ||
|
|
c82f38c103 | ||
|
|
fe0e7af033 | ||
|
|
ac51902435 | ||
|
|
e0ff342702 | ||
|
|
664125d820 | ||
|
|
9ae2f2c5af | ||
|
|
721f9b36b3 | ||
|
|
a33d1b819c | ||
|
|
cb4f5ea963 | ||
|
|
3c0ebfabab | ||
|
|
a822430e1d | ||
|
|
0ba11b94c8 | ||
|
|
390e90e8c2 | ||
|
|
59ef8eb4e4 | ||
|
|
2b382a9946 | ||
|
|
d52d55dd6a | ||
|
|
dc40ce7d9e | ||
|
|
383a119127 | ||
|
|
3f492e6208 | ||
|
|
0cec0cf595 | ||
|
|
bc19aea438 | ||
|
|
211850c813 | ||
|
|
c1312fb288 | ||
|
|
0c2934c489 | ||
|
|
ad34147f28 | ||
|
|
1eb6c28617 | ||
|
|
2e8a188bce | ||
|
|
ed495c3216 | ||
|
|
a773958403 | ||
|
|
6a6595d971 | ||
|
|
942b2240a7 | ||
|
|
6e86c60f89 | ||
|
|
ace6812e89 | ||
|
|
604fa6317c | ||
|
|
4479473b37 | ||
|
|
4dc8fc9696 | ||
|
|
bac59d595d | ||
|
|
ed98db8ae3 | ||
|
|
b66eb66358 | ||
|
|
934a994ac5 | ||
|
|
199a989f7d | ||
|
|
a50b09e5a2 | ||
|
|
b20c69b15a | ||
|
|
25be173487 | ||
|
|
61808d5939 | ||
|
|
de24227b1f | ||
|
|
1f5608ec77 | ||
|
|
2d87e162f1 | ||
|
|
1941fdf8a0 | ||
|
|
33fdc52667 | ||
|
|
e11a5a823a | ||
|
|
b60d101d42 | ||
|
|
5e9263b787 | ||
|
|
949bd783f5 | ||
|
|
7fe3137fd0 | ||
|
|
3c97fbe587 | ||
|
|
030d469d7c | ||
|
|
135ef8ed89 | ||
|
|
002e9c4647 | ||
|
|
a4fbe55012 | ||
|
|
63ac707415 | ||
|
|
b5dc04bb4f | ||
|
|
f3ae20f3aa | ||
|
|
1838b8aefd | ||
|
|
e9c65e1ada | ||
|
|
9ea5d213f7 | ||
|
|
7dcd6d5552 | ||
|
|
0de8f2d633 | ||
|
|
cb2f854dd5 | ||
|
|
401c920e47 | ||
|
|
40235fe473 | ||
|
|
a76e08aee7 | ||
|
|
dfe7282510 | ||
|
|
3aae2e976f | ||
|
|
f79a945fa4 | ||
|
|
8234c51412 | ||
|
|
f464f997d1 | ||
|
|
e0b7cdf9dd | ||
|
|
a819154145 | ||
|
|
616f45c586 | ||
|
|
203aaf51b7 | ||
|
|
392d1fe26d | ||
|
|
f452ea0d26 | ||
|
|
97dbb74486 | ||
|
|
1f80cfcaa6 | ||
|
|
5d0fbabb6a | ||
|
|
b5ca0fa17b | ||
|
|
41e1704d08 | ||
|
|
d4569237f5 | ||
|
|
b6ae51b5e5 | ||
|
|
3015ba605d | ||
|
|
eb589d4039 | ||
|
|
271190f434 | ||
|
|
0836790daf | ||
|
|
b3b665fcb9 | ||
|
|
c050b02b8b | ||
|
|
a791403a6a | ||
|
|
a4c6a18353 | ||
|
|
9bc603a510 | ||
|
|
af25fb926b | ||
|
|
365a0518a5 | ||
|
|
9801f49f4e | ||
|
|
bb4f3482ad | ||
|
|
e148607c7a | ||
|
|
ae370f04c1 | ||
|
|
89f6343abd | ||
|
|
ea55687171 | ||
|
|
da0b4d7911 | ||
|
|
e303283098 | ||
|
|
1119ff06c9 | ||
|
|
adc8ed26c1 | ||
|
|
06554b83dc | ||
|
|
06ea1d1697 | ||
|
|
ddb3bcde43 | ||
|
|
db2f9efb93 | ||
|
|
d32b15d425 | ||
|
|
a67f9b2edb | ||
|
|
c38547d4e7 | ||
|
|
3f350c30da | ||
|
|
d502d9b1bb | ||
|
|
06e1be716e | ||
|
|
cda855f1b7 | ||
|
|
36def799c0 | ||
|
|
2e671250b9 | ||
|
|
c881534554 | ||
|
|
e1f7336aa9 | ||
|
|
aa92e9da02 | ||
|
|
232b48ff62 | ||
|
|
a95b2932db | ||
|
|
aa288a1e11 | ||
|
|
7e6efcb9b9 | ||
|
|
817d0a7e63 | ||
|
|
fa0067ce7b | ||
|
|
9beacf7ef3 | ||
|
|
b4292028c2 | ||
|
|
d7c099bcbb | ||
|
|
36cd15f4f1 | ||
|
|
92ff1a8499 | ||
|
|
4af7a71220 | ||
|
|
611e170b5e | ||
|
|
148f64c261 | ||
|
|
2c3f47d3cb | ||
|
|
8a6e24e5e7 | ||
|
|
1d29f67065 | ||
|
|
2ffb3e73e1 | ||
|
|
bba7aa3177 | ||
|
|
c950a33031 | ||
|
|
c9ab3bdbfc | ||
|
|
e32c2e865c | ||
|
|
9e52b0b7dc | ||
|
|
d731fb2cae | ||
|
|
e057fcaea1 | ||
|
|
fff1363134 | ||
|
|
4bbfe1040e | ||
|
|
bc6e2dc81b | ||
|
|
0005698c10 | ||
|
|
53711966d2 | ||
|
|
d3b991aad4 | ||
|
|
4ef19c321d | ||
|
|
a3e3b9dfe3 | ||
|
|
abbce3e9d1 | ||
|
|
0edbfd89ff | ||
|
|
040d585d57 | ||
|
|
c74ef7b0d3 | ||
|
|
23ef937100 | ||
|
|
5157742009 | ||
|
|
96d96edc9e | ||
|
|
2a10edcc25 | ||
|
|
e4477db413 | ||
|
|
b32d926b56 | ||
|
|
6b3b5a8b6f | ||
|
|
a7df8afd6e | ||
|
|
b8205f4c38 | ||
|
|
2adb1bf917 | ||
|
|
2825dd7e04 | ||
|
|
df04064e81 | ||
|
|
0555fc48ad | ||
|
|
cd425bd26d | ||
|
|
a98444b4e7 | ||
|
|
31d05cefbe | ||
|
|
c552838fdd | ||
|
|
45942e604b | ||
|
|
9b1dd44b9d | ||
|
|
df56faa55a | ||
|
|
14cdeae3eb | ||
|
|
f97d2b0e88 | ||
|
|
a7e2154422 | ||
|
|
d8e84d67da | ||
|
|
c3174a0c72 | ||
|
|
0b05284340 | ||
|
|
ac8ae0da66 | ||
|
|
4517a8a36a | ||
|
|
8ba7eadcb9 | ||
|
|
0700e5b05b | ||
|
|
3485b7bfb0 | ||
|
|
c71c50a729 | ||
|
|
2651ab891d | ||
|
|
1e429550c6 | ||
|
|
44e4ae3740 | ||
|
|
b1552248c3 | ||
|
|
5efd2caeb8 | ||
|
|
bed012cb9c | ||
|
|
319d9024db | ||
|
|
ff7e371404 | ||
|
|
d94ee2ba98 | ||
|
|
a112c29c21 | ||
|
|
c7d6734a26 | ||
|
|
32aa8b0e1f | ||
|
|
6b22ba2707 | ||
|
|
2400c8ed00 | ||
|
|
396022e8ea | ||
|
|
0d4449b9c2 | ||
|
|
32ac92f901 | ||
|
|
f70026b702 | ||
|
|
87acc3362d | ||
|
|
732d231c78 | ||
|
|
a76ce64e1d | ||
|
|
5433a4193b | ||
|
|
56f1e58df0 | ||
|
|
d0c9db41ce | ||
|
|
f162ffd9be | ||
|
|
decc390777 | ||
|
|
ad8fa7563a | ||
|
|
68859d279d | ||
|
|
5792a531ce | ||
|
|
03af10f850 | ||
|
|
2f059874aa | ||
|
|
a53d7763ba | ||
|
|
eb526a6186 | ||
|
|
6095f871ed | ||
|
|
e3b4fe582b | ||
|
|
bbf26a3f38 | ||
|
|
fd291490d0 | ||
|
|
767d68d905 | ||
|
|
32b18e9141 | ||
|
|
5c286f8d96 | ||
|
|
404b53587f | ||
|
|
e5fbe3b74a | ||
|
|
3f262885ca | ||
|
|
c837179f71 | ||
|
|
9a947658e2 | ||
|
|
2458d4b637 | ||
|
|
e4850805fb | ||
|
|
3a15a3bb06 | ||
|
|
75ca1fa930 | ||
|
|
377840ca24 | ||
|
|
48e9589b58 | ||
|
|
11e9cef834 | ||
|
|
7e3aa09d3e | ||
|
|
e3b7e002b4 | ||
|
|
3b7fb7ae75 | ||
|
|
fab004cdfe | ||
|
|
07523c00f0 | ||
|
|
c15ac14f88 | ||
|
|
8f6006c19f | ||
|
|
16bedcf5b1 | ||
|
|
05349d8ffe | ||
|
|
a1e1895651 | ||
|
|
a111599850 | ||
|
|
121a95cee8 | ||
|
|
decd1f3ea2 | ||
|
|
c632718324 | ||
|
|
9509b59696 | ||
|
|
24dc71344e | ||
|
|
82a11e4b98 | ||
|
|
fc8665ed93 | ||
|
|
ca9424e23d | ||
|
|
99856b1bb3 | ||
|
|
fb518c2be5 | ||
|
|
1248a53029 | ||
|
|
6ce43840e5 | ||
|
|
41d9fb1c44 | ||
|
|
fd9be8f08f | ||
|
|
69a665373f | ||
|
|
8c2b110115 | ||
|
|
5e3b2dd0df | ||
|
|
d045213fa3 | ||
|
|
63bba67750 | ||
|
|
52e8fd2c29 | ||
|
|
5479b240e1 | ||
|
|
f209d8e9a5 | ||
|
|
ac40d7727e | ||
|
|
7bd9d3f5da | ||
|
|
68685dcf42 | ||
|
|
6be6db8f5e | ||
|
|
236dd79b85 | ||
|
|
7d905c5e06 | ||
|
|
6fa2542b56 | ||
|
|
7627b8fb45 | ||
|
|
5901427534 | ||
|
|
a30e57c4d8 | ||
|
|
69f2e12ea0 | ||
|
|
93c7f323fc | ||
|
|
cbe2a4c180 | ||
|
|
2e0cc4511c | ||
|
|
bcab165f97 | ||
|
|
2516fba105 | ||
|
|
9ca8760564 | ||
|
|
df94d9c842 | ||
|
|
abceb457c9 | ||
|
|
26a865bb50 | ||
|
|
d0f047d88a | ||
|
|
76d2d06e77 | ||
|
|
6c56006fbf | ||
|
|
bbe45cf8ee | ||
|
|
82da251218 | ||
|
|
196d9f0a10 | ||
|
|
cb752c0343 | ||
|
|
3917a3e323 | ||
|
|
fed1f09434 | ||
|
|
840778b64f | ||
|
|
0908856b4f | ||
|
|
b51354ae1d | ||
|
|
9ef24428fe | ||
|
|
4a75b0381b | ||
|
|
2426117233 | ||
|
|
d40c704c69 | ||
|
|
cb4fdb0f9f | ||
|
|
f0d04bdb07 | ||
|
|
df61ae8c7a | ||
|
|
2ff8ec07bb | ||
|
|
6021b51385 | ||
|
|
40111e0d8e | ||
|
|
43d9440e05 | ||
|
|
3a3e90c3dc | ||
|
|
104d6311ec | ||
|
|
f5256cf80a | ||
|
|
b470885263 | ||
|
|
7ff0ac9bb5 | ||
|
|
0fb59ca544 | ||
|
|
09b6b2dffe | ||
|
|
a8a3b7a5ad | ||
|
|
766a3e387e | ||
|
|
ec9fc8f6ad | ||
|
|
0f272e233b | ||
|
|
f806e797fa | ||
|
|
cff0a15f7e | ||
|
|
d3276829b2 | ||
|
|
a3caf8499c | ||
|
|
d739ca812c | ||
|
|
cb89ca6ef7 | ||
|
|
c3417241f1 | ||
|
|
5d70de6202 | ||
|
|
c09ffc9d41 | ||
|
|
e19a41d015 | ||
|
|
0a0400a297 | ||
|
|
153232c143 | ||
|
|
7e8813f417 | ||
|
|
b72f86a78d | ||
|
|
fed530f5f2 | ||
|
|
ba0a94f3ad | ||
|
|
d9b458d7fd | ||
|
|
ed9b806143 | ||
|
|
5bb68a38c2 | ||
|
|
cfeaf2cdce | ||
|
|
887b23ce1f | ||
|
|
f227392f9b | ||
|
|
2f7658e245 | ||
|
|
4f0c641eb0 | ||
|
|
1538f2e174 | ||
|
|
580068bf63 | ||
|
|
91604afccb | ||
|
|
c363c88f23 | ||
|
|
e9daee4470 | ||
|
|
118f0360d0 | ||
|
|
7c9dcb3083 | ||
|
|
7c3906fa40 | ||
|
|
7b24cbf39b | ||
|
|
e7b11d8bc9 | ||
|
|
08854cd88f | ||
|
|
cb46ac5421 | ||
|
|
c9fd00e2ed | ||
|
|
7c23fe3646 | ||
|
|
56aacb0149 | ||
|
|
12411ee886 | ||
|
|
85cef2945d | ||
|
|
a1dfeca6e0 | ||
|
|
3be6d14637 | ||
|
|
74b3ef9e88 | ||
|
|
019dfe9f8a | ||
|
|
e92dfd8284 | ||
|
|
fa38667125 | ||
|
|
ce39ae723c |
13
.babelrc
Normal file
13
.babelrc
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"presets": ["env", "react"],
|
||||
"plugins": ["transform-object-rest-spread", "transform-class-properties"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": [
|
||||
["istanbul", {
|
||||
exclude: ["node_modules/**", "test/**"]
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
84
.circleci/config.yml
Normal file
84
.circleci/config.yml
Normal file
@@ -0,0 +1,84 @@
|
||||
version: 2
|
||||
templates:
|
||||
# Test the build **only** no webdriver
|
||||
build-steps: &build-steps
|
||||
- checkout
|
||||
- run:
|
||||
name: "Create artifacts directory"
|
||||
command: mkdir /tmp/artifacts
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- run: npm install
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- run: mkdir -p /tmp/artifacts/logs
|
||||
- run: npm run build
|
||||
- run: npm run lint
|
||||
- run: npm run lint-styles
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts
|
||||
destination: /artifacts
|
||||
# Test in webdriver
|
||||
wdio-steps: &wdio-steps
|
||||
- checkout
|
||||
- run:
|
||||
name: "Create artifacts directory"
|
||||
command: mkdir /tmp/artifacts
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- run: npm install
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- run: mkdir -p /tmp/artifacts/logs
|
||||
- run: npm run build
|
||||
- run: npm run lint
|
||||
- run: npm run lint-styles
|
||||
- run: DOCKER_HOST=localhost npm test
|
||||
- run: ./node_modules/.bin/istanbul report --include /tmp/artifacts/coverage/coverage.json --dir /tmp/artifacts/coverage html lcov
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts
|
||||
destination: /artifacts
|
||||
jobs:
|
||||
build-linux-node-v6:
|
||||
docker:
|
||||
- image: node:6
|
||||
working_directory: ~/repo-linux-node-v6
|
||||
steps: *build-steps
|
||||
build-linux-node-v8:
|
||||
docker:
|
||||
- image: node:8
|
||||
- image: selenium/standalone-chrome:3.8.1
|
||||
working_directory: ~/repo-linux-node-v8
|
||||
steps: *wdio-steps
|
||||
build-linux-node-v9:
|
||||
docker:
|
||||
- image: node:9
|
||||
working_directory: ~/repo-linux-node-v9
|
||||
steps: *build-steps
|
||||
build-osx-node-v9:
|
||||
macos:
|
||||
xcode: "9.0"
|
||||
dependencies:
|
||||
override:
|
||||
- brew install node@9
|
||||
working_directory: ~/repo-linux-node-v9
|
||||
steps: *build-steps
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- build-linux-node-v6
|
||||
- build-linux-node-v8
|
||||
- build-linux-node-v9
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -30,3 +30,6 @@ node_modules
|
||||
|
||||
# Ignore build files
|
||||
public
|
||||
/errorShots
|
||||
/old
|
||||
/build
|
||||
|
||||
31
.travis.yml
31
.travis.yml
@@ -1,21 +1,22 @@
|
||||
language: node_js
|
||||
addons:
|
||||
firefox: latest
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
node_js:
|
||||
- "4.6"
|
||||
- "5.11"
|
||||
- "6.1"
|
||||
before_install:
|
||||
- export CHROME_BIN=chromium-browser
|
||||
- export DISPLAY=:99.0
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh -e /etc/init.d/xvfb start; fi
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
node_js: "6"
|
||||
- os: osx
|
||||
node_js: "8"
|
||||
- os: osx
|
||||
node_js: "9"
|
||||
install:
|
||||
- npm install
|
||||
script:
|
||||
- mkdir public
|
||||
- npm run build
|
||||
- node --stack_size=100000 $(which npm) run build
|
||||
- npm run lint
|
||||
- npm run test
|
||||
- npm run lint-styles
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
|
||||
156
README.md
156
README.md
@@ -1,22 +1,39 @@
|
||||
# Maputnik [](https://travis-ci.org/maputnik/editor) [](https://ci.appveyor.com/project/lukasmartinelli/editor) [](https://tldrlegal.com/license/mit-license)
|
||||
# Maputnik
|
||||
|
||||
[][travis]
|
||||
[][appveyor]
|
||||
[][dm-prod]
|
||||
[][dm-dev]
|
||||
[][license]
|
||||
|
||||
[travis]: https://travis-ci.org/maputnik/editor
|
||||
[appveyor]: https://ci.appveyor.com/project/lukasmartinelli/editor
|
||||
[dm-prod]: https://david-dm.org/maputnik/editor
|
||||
[dm-dev]: https://david-dm.org/maputnik/editor#info=devDependencies
|
||||
[license]: https://tldrlegal.com/license/mit-license
|
||||
|
||||
<img width="200" align="right" alt="Maputnik" src="src/img/maputnik.png" />
|
||||
|
||||
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
|
||||
targeted at developers and map designers. Creating your own custom map is easy with **Maputnik**.
|
||||
targeted at developers and map designers.
|
||||
|
||||
Check it out at **http://maputnik.com/editor/**
|
||||
- :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
|
||||
|
||||
*Maputnik is an early prototype and is under development.
|
||||
[Thanks to the supporters of the Kickstarter campaign who made this project possible](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)*.
|
||||
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independance is an OSS map designer.
|
||||
|
||||
## Latest Status Update Video
|
||||
## Documentation
|
||||
|
||||

|
||||
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
|
||||
|
||||
- :link: **Study the [Maputnik Wiki](https://github.com/maputnik/editor/wiki)**
|
||||
- :video_camera: Design a map from Scratch https://youtu.be/XoDh0gEnBQo
|
||||
|
||||
[](https://youtu.be/XoDh0gEnBQo)
|
||||
|
||||
## Develop
|
||||
|
||||
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react), [Immutable.js](https://facebook.github.io/immutable-js/) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
|
||||
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/).
|
||||
|
||||
We ensure building and developing Maputnik works with
|
||||
|
||||
@@ -32,7 +49,12 @@ npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
Build a production package for distribution.
|
||||
The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the webpack-dev-server docs
|
||||
|
||||
> 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 from <https://webpack.js.org/configuration/dev-server/#devserver-watchoptions->
|
||||
|
||||
To enable polling add `export WEBPACK_DEV_SERVER_POLLING=1` to your enviroment.
|
||||
|
||||
```
|
||||
npm run build
|
||||
@@ -41,103 +63,95 @@ npm run build
|
||||
Lint the JavaScript code.
|
||||
|
||||
```
|
||||
# install lint dependencies
|
||||
npm install --save-dev eslint eslint-plugin-react
|
||||
# run linter
|
||||
npm run lint
|
||||
npm run lint-styles
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
Start a container using the official Docker image.
|
||||
```
|
||||
docker run --name maputnik -p 8888:8888 -d maputnik/editor
|
||||
```
|
||||
## Tests
|
||||
For testing we use [webdriverio](http://webdriver.io) and [selenium-standalone](https://github.com/vvo/selenium-standalone)
|
||||
|
||||
Stop the container
|
||||
[selenium-standalone](https://github.com/vvo/selenium-standalone) starts a server that will launch browsers on your local machine. We use chrome so you **must** have chrome installed on your machine.
|
||||
|
||||
Now open and terminal and run the following. This will install the drivers on your local machine
|
||||
|
||||
```
|
||||
docker stop maputnik
|
||||
./node_modules/.bin/selenium-standalone install
|
||||
```
|
||||
|
||||
Now start the standalone server
|
||||
|
||||
```
|
||||
./node_modules/.bin/selenium-standalone start
|
||||
```
|
||||
|
||||
Then open another terminal and run
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
After some time you should see a browser launch which will be automated by the test runner.
|
||||
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
|
||||
|
||||
## Sponsors
|
||||
|
||||
This project would not be possible without commercial and individual 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
|
||||
|
||||
[](https://getwemap.com/)
|
||||
- [Wemap](https://getwemap.com/)
|
||||
- [Orbicon Informatik](https://www.orbiconinformatik.dk/)
|
||||
- [Terranodo](http://terranodo.io/)
|
||||
|
||||
[](http://terranodo.io/)
|
||||
<a href="https://getwemap.com/">
|
||||
<img width="33%" alt="Wemap" style="display:inline" src="media/sponsors/wemap.jpg" />
|
||||
</a>
|
||||
<a href="http://terranodo.io/">
|
||||
<img width="33%" alt="Terranodo" style="display:inline" src="media/sponsors/terranodo.png" />
|
||||
</a>
|
||||
<a href="https://www.orbiconinformatik.dk/">
|
||||
<img width="32%" alt="Terranodo" style="display:inline" src="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 alt="Klokan Technologies" style="display:inline" src="media/sponsors/klokantech.png" />
|
||||
<img width="18%" alt="Klokan Technologies" style="display:inline-block" src="media/sponsors/klokantech.png" />
|
||||
</a>
|
||||
<a href="http://www.geofabrik.de/">
|
||||
<img width="18%" alt="Geofabrik" style="display:inline-block" src="media/sponsors/geofabrik.png" />
|
||||
</a>
|
||||
<a href="https://www.dreipol.ch/">
|
||||
<img alt="Dreipol" style="display:inline" src="media/sponsors/dreipol.png" />
|
||||
<img width="18%" alt="Dreipol" style="display:inline-block" src="media/sponsors/dreipol.png" />
|
||||
</a>
|
||||
|
||||
<br/>
|
||||
|
||||
### Individuals
|
||||
|
||||
**Influential Stakeholder**
|
||||
|
||||
- Alan McConchie
|
||||
- Odi
|
||||
- Mats Norén
|
||||
- Uli [geOps](http://geops.ch/)
|
||||
- Helge Fahrnberger
|
||||
Kirusanth Poopalasingam
|
||||
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
|
||||
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
|
||||
Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth, Janko Mihelić, Moritz Stefaner, Sebastian Ahoi, Juerg Uhlmann, Tom Wider, Nadia Panchaud, Oliver Snowden, Stephan Heuel, Tobin Bradley, Adrian Herzog, Antti Lehto, Pascal Mages, Marc Gehling, Imre Samu, Lauri K., Visahavel Parthasarathy, Christophe Waterlot-Buisine, Max Galka, ubahnverleih, Wouter van Dam, Jakob Lobensteiner, Samuel Kurath, Brian Bancroft
|
||||
|
||||
## License
|
||||
|
||||
|
||||
11
appveyor.yml
11
appveyor.yml
@@ -1,14 +1,17 @@
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: "4.6"
|
||||
- nodejs_version: "5.11"
|
||||
- nodejs_version: "6.1"
|
||||
- nodejs_version: "6"
|
||||
- nodejs_version: "8"
|
||||
- nodejs_version: "9"
|
||||
platform:
|
||||
- x86
|
||||
- x64
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
- md public
|
||||
- npm install --global --production windows-build-tools
|
||||
- npm install
|
||||
build_script:
|
||||
- npm run build
|
||||
test_script:
|
||||
- npm run lint
|
||||
- npm test
|
||||
|
||||
62
config/wdio.conf.js
Normal file
62
config/wdio.conf.js
Normal file
@@ -0,0 +1,62 @@
|
||||
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 isDocker = require("is-docker");
|
||||
|
||||
|
||||
var server;
|
||||
var SCREENSHOT_PATH = artifacts.pathSync("screenshots");
|
||||
|
||||
exports.config = {
|
||||
specs: [
|
||||
'./test/functional/index.js'
|
||||
],
|
||||
exclude: [
|
||||
],
|
||||
maxInstances: 10,
|
||||
capabilities: [{
|
||||
maxInstances: 5,
|
||||
browserName: 'chrome'
|
||||
}],
|
||||
sync: true,
|
||||
logLevel: 'verbose',
|
||||
coloredLogs: true,
|
||||
bail: 0,
|
||||
screenshotPath: SCREENSHOT_PATH,
|
||||
// Note: This is here because @orangemug currently runs Maputnik inside a docker container.
|
||||
host: process.env.DOCKER_HOST || "0.0.0.0",
|
||||
baseUrl: 'http://localhost',
|
||||
waitforTimeout: 10000,
|
||||
connectionRetryTimeout: 90000,
|
||||
connectionRetryCount: 3,
|
||||
framework: 'mocha',
|
||||
reporters: ['spec'],
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
// Because we don't know how long the initial build will take...
|
||||
timeout: 4*60*1000
|
||||
},
|
||||
onPrepare: function (config, capabilities) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var compiler = webpack(webpackConfig);
|
||||
server = new WebpackDevServer(compiler, {
|
||||
stats: {
|
||||
colors: true
|
||||
}
|
||||
});
|
||||
server.listen(testConfig.port, (isDocker() ? "0.0.0.0" : "localhost"), function(err) {
|
||||
if(err) {
|
||||
reject(err);
|
||||
}
|
||||
else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
onComplete: function(exitCode) {
|
||||
server.close()
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ var webpack = require('webpack');
|
||||
var path = require('path');
|
||||
var loaders = require('./webpack.loaders');
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
|
||||
const HOST = process.env.HOST || "127.0.0.1";
|
||||
const PORT = process.env.PORT || "8888";
|
||||
@@ -16,14 +17,17 @@ module.exports = {
|
||||
],
|
||||
devtool: process.env.WEBPACK_DEVTOOL || 'cheap-module-source-map',
|
||||
output: {
|
||||
path: path.join(__dirname, 'public'),
|
||||
path: path.join(__dirname, '..', 'public'),
|
||||
filename: 'bundle.js'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['', '.js', '.jsx']
|
||||
extensions: ['.js', '.jsx']
|
||||
},
|
||||
module: {
|
||||
loaders
|
||||
noParse: [
|
||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
||||
],
|
||||
loaders: loaders
|
||||
},
|
||||
node: {
|
||||
fs: "empty",
|
||||
@@ -41,14 +45,26 @@ module.exports = {
|
||||
// serve index.html in place of 404 responses to allow HTML5 history
|
||||
historyApiFallback: true,
|
||||
port: PORT,
|
||||
host: HOST
|
||||
host: HOST,
|
||||
watchOptions: {
|
||||
// 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
|
||||
poll: (!!process.env.WEBPACK_DEV_SERVER_POLLING ? true : false),
|
||||
watch: false
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.NoErrorsPlugin(),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Maputnik',
|
||||
template: './src/template.html'
|
||||
}),
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: './src/manifest.json',
|
||||
to: 'manifest.json'
|
||||
}
|
||||
])
|
||||
]
|
||||
};
|
||||
55
config/webpack.loaders.js
Normal file
55
config/webpack.loaders.js
Normal file
@@ -0,0 +1,55 @@
|
||||
module.exports = [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /(node_modules|bower_components|public)/,
|
||||
loaders: ['react-hot-loader/webpack']
|
||||
},
|
||||
// HACK: This is a massive hack and reaches into the mapbox-gl private API.
|
||||
// We have to include this for access to `normalizeSourceURL`. We should
|
||||
// remove this ASAP, see <https://github.com/mapbox/mapbox-gl-js/issues/2416>
|
||||
{
|
||||
test: /.*node_modules[\/\\]mapbox-gl[\/\\]src[\/\\]util[\/\\].*\.js/,
|
||||
loader: 'babel-loader',
|
||||
query: {
|
||||
presets: ['env', 'react', 'flow'],
|
||||
plugins: ['transform-runtime', 'transform-decorators-legacy', 'transform-class-properties'],
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
// Note: These modules aren't ES5 therefore we much compile them.
|
||||
exclude: /(.*node_modules(?)|bower_components|public)/,
|
||||
loader: 'babel-loader',
|
||||
query: {
|
||||
presets: ['env', 'react'],
|
||||
plugins: ['transform-runtime', 'transform-decorators-legacy', 'transform-class-properties'],
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(eot|ttf|woff|woff2)$/,
|
||||
loader: 'file-loader?name=fonts/[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.ico$/,
|
||||
loader: 'file-loader?name=[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.(svg|gif|jpg|png)$/,
|
||||
loader: 'file-loader?name=img/[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader'
|
||||
},
|
||||
{
|
||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
|
||||
loaders: ["style-loader", "css-loader", "sass-loader"]
|
||||
},
|
||||
{
|
||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
|
||||
loaders: [
|
||||
'style-loader?sourceMap',
|
||||
'css-loader'
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -1,10 +1,15 @@
|
||||
|
||||
var webpack = require('webpack');
|
||||
var path = require('path');
|
||||
var loaders = require('./webpack.loaders');
|
||||
var ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
var HtmlWebpackPlugin = require('html-webpack-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 UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||
|
||||
var OUTPATH = artifacts.pathSync("/build");
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
@@ -12,12 +17,8 @@ module.exports = {
|
||||
vendor: [
|
||||
'file-saver',
|
||||
'mapbox-gl/dist/mapbox-gl.js',
|
||||
//TODO: Build failure because cannot resolve migrations file
|
||||
//"mapbox-gl-style-spec",
|
||||
"randomcolor",
|
||||
"lodash.clonedeep",
|
||||
"lodash.throttle",
|
||||
"lodash.topairs",
|
||||
'color',
|
||||
'react',
|
||||
"react-dom",
|
||||
@@ -34,14 +35,17 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'public'),
|
||||
path: OUTPATH,
|
||||
filename: '[name].[chunkhash].js',
|
||||
chunkFilename: '[chunkhash].js'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['', '.js', '.jsx']
|
||||
extensions: ['.js', '.jsx']
|
||||
},
|
||||
module: {
|
||||
noParse: [
|
||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
||||
],
|
||||
loaders
|
||||
},
|
||||
node: {
|
||||
@@ -50,23 +54,15 @@ module.exports = {
|
||||
tls: 'empty'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.NoErrorsPlugin(),
|
||||
new webpack.optimize.CommonsChunkPlugin('vendor', '[chunkhash].vendor.js'),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: '[chunkhash].vendor.js' }),
|
||||
new WebpackCleanupPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
warnings: false,
|
||||
screw_ie8: true,
|
||||
drop_console: true,
|
||||
drop_debugger: true
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.OccurenceOrderPlugin(),
|
||||
new UglifyJsPlugin(),
|
||||
new ExtractTextPlugin('[contenthash].css', {
|
||||
allChunks: true
|
||||
}),
|
||||
@@ -74,6 +70,19 @@ module.exports = {
|
||||
template: './src/template.html',
|
||||
title: 'Maputnik'
|
||||
}),
|
||||
new webpack.optimize.DedupePlugin()
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
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',
|
||||
})
|
||||
]
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
var webpackConfig = require('./webpack.config.js');
|
||||
|
||||
// Karma configuration
|
||||
module.exports = function(config) {
|
||||
var browsers = ['Chrome'];
|
||||
if (process.env.TRAVIS) {
|
||||
browsers = ['Firefox'];
|
||||
}
|
||||
|
||||
config.set({
|
||||
browsers: browsers,
|
||||
frameworks: ['mocha'],
|
||||
// ... normal karma configuration
|
||||
files: [
|
||||
// all files ending in "_test"
|
||||
{pattern: 'test/*_test.js', watched: false},
|
||||
{pattern: 'test/**/*_test.js', watched: false}
|
||||
// each file acts as entry point for the webpack configuration
|
||||
],
|
||||
|
||||
preprocessors: {
|
||||
// add webpack as preprocessor
|
||||
'test/*_test.js': ['webpack'],
|
||||
'test/**/*_test.js': ['webpack']
|
||||
},
|
||||
|
||||
webpack: webpackConfig,
|
||||
webpackMiddleware: {
|
||||
// webpack-dev-middleware configuration
|
||||
// i. e.
|
||||
stats: 'errors-only'
|
||||
}
|
||||
});
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 410 KiB |
BIN
media/sponsors/geofabrik.png
Normal file
BIN
media/sponsors/geofabrik.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
BIN
media/sponsors/orbicon_informatik.png
Normal file
BIN
media/sponsors/orbicon_informatik.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 37 KiB |
17021
package-lock.json
generated
Normal file
17021
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
177
package.json
177
package.json
@@ -1,15 +1,17 @@
|
||||
{
|
||||
"name": "maputnik",
|
||||
"version": "0.0.1",
|
||||
"version": "1.2.0",
|
||||
"description": "A MapboxGL visual style editor",
|
||||
"main": "''",
|
||||
"scripts": {
|
||||
"stats": "webpack --config webpack.production.config.js --profile --json > stats.json",
|
||||
"build": "webpack --config webpack.production.config.js --progress --profile --colors",
|
||||
"test": "karma start --single-run",
|
||||
"test-watch": "karma start",
|
||||
"start": "webpack-dev-server --progress --profile --colors --watch-poll",
|
||||
"lint": "eslint --ext js --ext jsx {src,test}"
|
||||
"stats": "webpack --config config/webpack.production.config.js --profile --json > stats.json",
|
||||
"build": "webpack --config config/webpack.production.config.js --progress --profile --colors",
|
||||
"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 --colors --config config/webpack.config.js",
|
||||
"lint": "eslint --ext js --ext jsx {src,test}",
|
||||
"lint-styles": "stylelint 'src/styles/*.scss'",
|
||||
"nsp": "nsp check --reporter summary"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -19,52 +21,57 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/maputnik/editor#readme",
|
||||
"dependencies": {
|
||||
"codemirror": "^5.18.2",
|
||||
"color": "^1.0.3",
|
||||
"file-saver": "^1.3.2",
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.1.1",
|
||||
"@mapbox/mapbox-gl-style-spec": "^11.1.1",
|
||||
"classnames": "^2.2.5",
|
||||
"codemirror": "^5.36.0",
|
||||
"color": "^3.0.0",
|
||||
"file-saver": "^1.3.8",
|
||||
"github-api": "^3.0.0",
|
||||
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
||||
"lodash.capitalize": "^4.2.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.isequal": "^4.4.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"lodash.topairs": "^4.3.0",
|
||||
"mapbox-gl": "^0.29.0",
|
||||
"mapbox-gl-style-spec": "^8.11.0",
|
||||
"mousetrap": "^1.6.0",
|
||||
"ol-mapbox-style": "0.0.11",
|
||||
"openlayers": "^3.19.1",
|
||||
"randomcolor": "^0.4.4",
|
||||
"react": "^15.4.0",
|
||||
"react-addons-pure-render-mixin": "^15.4.0",
|
||||
"react-autocomplete": "^1.4.0",
|
||||
"react-codemirror": "^0.3.0",
|
||||
"react-collapse": "^2.3.3",
|
||||
"react-color": "^2.10.0",
|
||||
"react-dom": "^15.4.0",
|
||||
"react-file-reader-input": "^1.1.0",
|
||||
"react-height": "^2.1.1",
|
||||
"react-icon-base": "^2.0.4",
|
||||
"react-icons": "^2.2.1",
|
||||
"react-motion": "^0.4.7",
|
||||
"react-sortable-hoc": "^0.4.5",
|
||||
"request": "^2.79.0"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"es2015",
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread"
|
||||
]
|
||||
"mapbox-gl": "^0.44.2",
|
||||
"mapbox-gl-inspect": "^1.3.1",
|
||||
"maputnik-design": "github:maputnik/design",
|
||||
"mousetrap": "^1.6.1",
|
||||
"ol-mapbox-style": "^2.10.1",
|
||||
"ol": "^4.6.5",
|
||||
"prop-types": "^15.6.0",
|
||||
"react": "^16.3.2",
|
||||
"react-addons-pure-render-mixin": "^15.6.2",
|
||||
"react-autocomplete": "^1.7.2",
|
||||
"react-codemirror2": "^4.2.1",
|
||||
"react-collapse": "^4.0.3",
|
||||
"react-color": "^2.14.1",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-dom": "^16.3.2",
|
||||
"react-file-reader-input": "^1.1.4",
|
||||
"react-height": "^3.0.0",
|
||||
"react-icon-base": "^2.1.1",
|
||||
"react-icons": "^2.2.7",
|
||||
"react-motion": "^0.5.2",
|
||||
"react-sortable-hoc": "^0.6.8",
|
||||
"reconnecting-websocket": "^3.2.2",
|
||||
"request": "^2.85.0",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"jshintConfig": {
|
||||
"esversion": 6
|
||||
},
|
||||
"stylelint": {
|
||||
"extends": "stylelint-config-recommended-scss",
|
||||
"rules": {
|
||||
"no-descending-specificity": null
|
||||
}
|
||||
},
|
||||
"eslintConfig": {
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"extend": [
|
||||
"extends": [
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"env": {
|
||||
@@ -84,40 +91,58 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "6.14.0",
|
||||
"babel-eslint": "^6.1.2",
|
||||
"babel-loader": "6.2.4",
|
||||
"babel-plugin-transform-class-properties": "^6.11.5",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.2.3",
|
||||
"babel-loader": "7.1.4",
|
||||
"babel-plugin-istanbul": "^4.1.6",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-flow-strip-types": "^6.21.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
||||
"babel-plugin-transform-runtime": "^6.15.0",
|
||||
"babel-preset-es2015": "6.14.0",
|
||||
"babel-preset-react": "6.11.1",
|
||||
"babel-runtime": "^6.11.6",
|
||||
"css-loader": "0.25.0",
|
||||
"eslint": "^3.5.0",
|
||||
"eslint-plugin-react": "^6.2.0",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
"file-loader": "0.9.0",
|
||||
"html-webpack-plugin": "^2.22.0",
|
||||
"json-loader": "^0.5.4",
|
||||
"karma": "^1.3.0",
|
||||
"karma-chrome-launcher": "^2.0.0",
|
||||
"karma-firefox-launcher": "^1.0.0",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"karma-webpack": "^1.8.0",
|
||||
"mocha": "^3.1.2",
|
||||
"mocha-loader": "^1.0.0",
|
||||
"node-sass": "^3.9.2",
|
||||
"react-hot-loader": "^3.0.0-beta.6",
|
||||
"sass-loader": "^4.0.1",
|
||||
"style-loader": "0.13.1",
|
||||
"transform-loader": "^0.2.3",
|
||||
"url-loader": "0.5.7",
|
||||
"webpack": "1.13.2",
|
||||
"webpack-cleanup-plugin": "^0.3.0",
|
||||
"webpack-dev-server": "1.15.1",
|
||||
"webworkify-webpack": "^1.1.3"
|
||||
"babel-plugin-transform-flow-strip-types": "^6.22.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-flow": "^6.23.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-register": "^6.26.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"base64-loader": "^1.0.0",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"cors": "^2.8.4",
|
||||
"cross-env": "^5.1.4",
|
||||
"css-loader": "^0.28.11",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-plugin-react": "^7.4.0",
|
||||
"express": "^4.16.3",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"file-loader": "^1.1.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"is-docker": "^1.1.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"istanbul-lib-coverage": "^1.2.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mocha": "^5.1.0",
|
||||
"node-sass": "^4.8.3",
|
||||
"nsp": "^3.1.0",
|
||||
"react-hot-loader": "^3.1.1",
|
||||
"sass-loader": "^7.0.1",
|
||||
"selenium-standalone": "^6.14.0",
|
||||
"style-loader": "^0.20.3",
|
||||
"stylelint": "^9.2.0",
|
||||
"stylelint-config-recommended-scss": "^3.2.0",
|
||||
"stylelint-scss": "^3.0.0",
|
||||
"transform-loader": "^0.2.4",
|
||||
"uglifyjs-webpack-plugin": "^1.2.4",
|
||||
"uuid": "^3.1.0",
|
||||
"wdio-mocha-framework": "^0.5.13",
|
||||
"wdio-phantomjs-service": "^0.2.2",
|
||||
"wdio-selenium-standalone-service": "0.0.10",
|
||||
"wdio-spec-reporter": "^0.1.2",
|
||||
"webdriverio": "^4.12.0",
|
||||
"webpack": "^3.8.1",
|
||||
"webpack-bundle-analyzer": "^2.9.0",
|
||||
"webpack-cleanup-plugin": "^0.5.1",
|
||||
"webpack-dev-server": "^2.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,18 @@
|
||||
}
|
||||
|
||||
.cm-s-maputnik.CodeMirror, .cm-s-maputnik .CodeMirror-gutters {
|
||||
background: transparent;
|
||||
color: #8e8e8e;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cm-s-maputnik.CodeMirror {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.cm-s-maputnik .CodeMirror-gutters {
|
||||
background: #212328;
|
||||
}
|
||||
|
||||
.cm-s-maputnik .CodeMirror-cursor {
|
||||
border-left: solid thin #8e8e8e !important;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react'
|
||||
import { saveAs } from 'file-saver'
|
||||
import Mousetrap from 'mousetrap'
|
||||
|
||||
import InspectionMap from './map/InspectionMap'
|
||||
import MapboxGlMap from './map/MapboxGlMap'
|
||||
import OpenLayers3Map from './map/OpenLayers3Map'
|
||||
import LayerList from './layers/LayerList'
|
||||
@@ -11,30 +9,68 @@ import Toolbar from './Toolbar'
|
||||
import AppLayout from './AppLayout'
|
||||
import MessagePanel from './MessagePanel'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import validateStyleMin from 'mapbox-gl-style-spec/lib/validate_style.min'
|
||||
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import style from '../libs/style.js'
|
||||
import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen'
|
||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
||||
import { loadDefaultStyle, StyleStore } from '../libs/stylestore'
|
||||
import { ApiStyleStore } from '../libs/apistore'
|
||||
import { RevisionStore } from '../libs/revisions'
|
||||
import LayerWatcher from '../libs/layerwatcher'
|
||||
import tokens from '../config/tokens.json'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import Debug from '../libs/debug'
|
||||
|
||||
import MapboxGl from 'mapbox-gl'
|
||||
import mapboxUtil from 'mapbox-gl/src/util/mapbox'
|
||||
|
||||
|
||||
function updateRootSpec(spec, fieldName, newValues) {
|
||||
return {
|
||||
...spec,
|
||||
$root: {
|
||||
...spec.$root,
|
||||
[fieldName]: {
|
||||
...spec.$root[fieldName],
|
||||
values: newValues
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.styleStore = new ApiStyleStore()
|
||||
this.revisionStore = new RevisionStore()
|
||||
|
||||
this.styleStore.supported(isSupported => {
|
||||
if(!isSupported) {
|
||||
console.log('Falling back to local storage for storing styles')
|
||||
this.styleStore = new StyleStore()
|
||||
}
|
||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
||||
this.styleStore = new ApiStyleStore({
|
||||
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false)
|
||||
})
|
||||
|
||||
const styleUrl = initialStyleUrl()
|
||||
if(styleUrl) {
|
||||
this.styleStore = new StyleStore()
|
||||
loadStyleUrl(styleUrl, mapStyle => this.onStyleChanged(mapStyle))
|
||||
} else {
|
||||
this.styleStore.init(err => {
|
||||
if(err) {
|
||||
console.log('Falling back to local storage for storing styles')
|
||||
this.styleStore = new StyleStore()
|
||||
}
|
||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
||||
|
||||
if(Debug.enabled()) {
|
||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if(Debug.enabled()) {
|
||||
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
errors: [],
|
||||
infos: [],
|
||||
@@ -42,45 +78,59 @@ export default class App extends React.Component {
|
||||
selectedLayerIndex: 0,
|
||||
sources: {},
|
||||
vectorLayers: {},
|
||||
inspectModeEnabled: false,
|
||||
spec: styleSpec.latest,
|
||||
}
|
||||
|
||||
this.layerWatcher = new LayerWatcher({
|
||||
onSourcesChange: v => this.setState({ sources: v }),
|
||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
Mousetrap.bind(['ctrl+z'], this.onUndo.bind(this));
|
||||
Mousetrap.bind(['ctrl+y'], this.onRedo.bind(this));
|
||||
Mousetrap.bind(['mod+z'], this.onUndo.bind(this));
|
||||
Mousetrap.bind(['mod+y', 'mod+shift+z'], this.onRedo.bind(this));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Mousetrap.unbind(['ctrl+z'], this.onUndo.bind(this));
|
||||
Mousetrap.unbind(['ctrl+y'], this.onRedo.bind(this));
|
||||
}
|
||||
|
||||
onReset() {
|
||||
this.styleStore.purge()
|
||||
loadDefaultStyle(mapStyle => this.onStyleOpen(mapStyle))
|
||||
}
|
||||
|
||||
onStyleDownload() {
|
||||
const mapStyle = this.state.mapStyle
|
||||
const blob = new Blob([JSON.stringify(mapStyle, null, 4)], {type: "application/json;charset=utf-8"});
|
||||
saveAs(blob, mapStyle.id + ".json");
|
||||
Mousetrap.unbind(['mod+z'], this.onUndo.bind(this));
|
||||
Mousetrap.unbind(['mod+y', 'mod+shift+z'], this.onRedo.bind(this));
|
||||
}
|
||||
|
||||
saveStyle(snapshotStyle) {
|
||||
snapshotStyle.modified = new Date().toJSON()
|
||||
this.styleStore.save(snapshotStyle)
|
||||
}
|
||||
|
||||
onStyleChanged(newStyle) {
|
||||
const errors = validateStyleMin(newStyle, GlSpec)
|
||||
updateFonts(urlTemplate) {
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
||||
|
||||
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
|
||||
downloadGlyphsMetadata(glyphUrl, fonts => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
||||
})
|
||||
}
|
||||
|
||||
updateIcons(baseUrl) {
|
||||
downloadSpriteMetadata(baseUrl, icons => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
|
||||
})
|
||||
}
|
||||
|
||||
onStyleChanged(newStyle, save=true) {
|
||||
|
||||
const errors = styleSpec.validate(newStyle, styleSpec.latest)
|
||||
if(errors.length === 0) {
|
||||
|
||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||
this.updateFonts(newStyle.glyphs)
|
||||
}
|
||||
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
||||
this.updateIcons(newStyle.sprite)
|
||||
}
|
||||
|
||||
this.revisionStore.addRevision(newStyle)
|
||||
this.saveStyle(newStyle)
|
||||
if(save) this.saveStyle(newStyle)
|
||||
this.setState({
|
||||
mapStyle: newStyle,
|
||||
errors: [],
|
||||
@@ -90,6 +140,8 @@ export default class App extends React.Component {
|
||||
errors: errors.map(err => err.message)
|
||||
})
|
||||
}
|
||||
|
||||
this.fetchSources();
|
||||
}
|
||||
|
||||
onUndo() {
|
||||
@@ -140,32 +192,88 @@ export default class App extends React.Component {
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
mapRenderer() {
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const mapProps = {
|
||||
mapStyle: this.state.mapStyle,
|
||||
accessToken: metadata['maputnik:access_token'],
|
||||
onDataChange: (e) => {
|
||||
this.layerWatcher.analyzeMap(e.map)
|
||||
},
|
||||
//TODO: This would actually belong to the layout component
|
||||
style:{
|
||||
top: 40,
|
||||
//left: 500,
|
||||
}
|
||||
changeInspectMode() {
|
||||
this.setState({
|
||||
inspectModeEnabled: !this.state.inspectModeEnabled
|
||||
})
|
||||
}
|
||||
|
||||
fetchSources() {
|
||||
const sourceList = {...this.state.sources};
|
||||
|
||||
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
||||
if(sourceList.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sourceList[key] = {
|
||||
type: val.type,
|
||||
layers: []
|
||||
};
|
||||
|
||||
if(!this.state.sources.hasOwnProperty(key) && val.type === "vector" && val.hasOwnProperty("url")) {
|
||||
let url = val.url;
|
||||
try {
|
||||
url = mapboxUtil.normalizeSourceURL(url, MapboxGl.accessToken);
|
||||
} catch(err) {
|
||||
console.warn("Failed to normalizeSourceURL: ", err);
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((json) => {
|
||||
if(!json.hasOwnProperty("vector_layers")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new objects before setState
|
||||
const sources = Object.assign({}, this.state.sources);
|
||||
|
||||
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);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if(!isEqual(this.state.sources, sourceList)) {
|
||||
console.debug("Setting sources");
|
||||
this.setState({
|
||||
sources: sourceList
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
mapRenderer() {
|
||||
const mapProps = {
|
||||
mapStyle: style.replaceAccessToken(this.state.mapStyle, {allowFallback: true}),
|
||||
onDataChange: (e) => {
|
||||
this.layerWatcher.analyzeMap(e.map)
|
||||
this.fetchSources();
|
||||
},
|
||||
}
|
||||
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const renderer = metadata['maputnik:renderer'] || 'mbgljs'
|
||||
|
||||
// Check if OL3 code has been loaded?
|
||||
if(renderer === 'ol3') {
|
||||
return <OpenLayers3Map {...mapProps} />
|
||||
} else if(renderer === 'inspection') {
|
||||
return <InspectionMap {...mapProps}
|
||||
sources={this.state.sources}
|
||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]} />
|
||||
} else {
|
||||
return <MapboxGlMap {...mapProps} />
|
||||
return <MapboxGlMap {...mapProps}
|
||||
inspectModeEnabled={this.state.inspectModeEnabled}
|
||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
||||
onLayerSelect={this.onLayerSelect.bind(this)} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,10 +289,11 @@ export default class App extends React.Component {
|
||||
|
||||
const toolbar = <Toolbar
|
||||
mapStyle={this.state.mapStyle}
|
||||
inspectModeEnabled={this.state.inspectModeEnabled}
|
||||
sources={this.state.sources}
|
||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
||||
onStyleOpen={this.onStyleChanged.bind(this)}
|
||||
onStyleDownload={this.onStyleDownload.bind(this)}
|
||||
onInspectModeToggle={this.changeInspectMode.bind(this)}
|
||||
/>
|
||||
|
||||
const layerList = <LayerList
|
||||
@@ -192,12 +301,14 @@ export default class App extends React.Component {
|
||||
onLayerSelect={this.onLayerSelect.bind(this)}
|
||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
||||
layers={layers}
|
||||
sources={this.state.sources}
|
||||
/>
|
||||
|
||||
const layerEditor = selectedLayer ? <LayerEditor
|
||||
layer={selectedLayer}
|
||||
sources={this.state.sources}
|
||||
vectorLayers={this.state.vectorLayers}
|
||||
spec={this.state.spec}
|
||||
onLayerChanged={this.onLayerChanged.bind(this)}
|
||||
onLayerIdChange={this.onLayerIdChange.bind(this)}
|
||||
/> : null
|
||||
@@ -216,4 +327,3 @@ export default class App extends React.Component {
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,75 +1,41 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ScrollContainer from './ScrollContainer'
|
||||
|
||||
import theme from '../config/theme'
|
||||
import colors from '../config/colors'
|
||||
import { fontSizes } from '../config/scales'
|
||||
|
||||
class AppLayout extends React.Component {
|
||||
static propTypes = {
|
||||
toolbar: React.PropTypes.element.isRequired,
|
||||
layerList: React.PropTypes.element.isRequired,
|
||||
layerEditor: React.PropTypes.element,
|
||||
map: React.PropTypes.element.isRequired,
|
||||
bottom: React.PropTypes.element,
|
||||
toolbar: PropTypes.element.isRequired,
|
||||
layerList: PropTypes.element.isRequired,
|
||||
layerEditor: PropTypes.element,
|
||||
map: PropTypes.element.isRequired,
|
||||
bottom: PropTypes.element,
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
reactIconBase: React.PropTypes.object
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
reactIconBase: { size: fontSizes[3] }
|
||||
reactIconBase: { size: 14 }
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
fontFamily: theme.fontFamily,
|
||||
color: theme.color,
|
||||
fontWeight: 300
|
||||
}}>
|
||||
return <div className="maputnik-layout">
|
||||
{this.props.toolbar}
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
top: 40,
|
||||
left: 0,
|
||||
zIndex: 1,
|
||||
width: 200,
|
||||
overflow: "hidden",
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
<div className="maputnik-layout-list">
|
||||
<ScrollContainer>
|
||||
{this.props.layerList}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
top: 40,
|
||||
left: 200,
|
||||
zIndex: 1,
|
||||
width: 350,
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
<div className="maputnik-layout-drawer">
|
||||
<ScrollContainer>
|
||||
{this.props.layerEditor}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
{this.props.map}
|
||||
{this.props.bottom && <div style={{
|
||||
position: 'fixed',
|
||||
height: 50,
|
||||
bottom: 0,
|
||||
left: 550,
|
||||
zIndex: 1,
|
||||
width: '100%',
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||
{this.props.bottom}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
import React from 'react'
|
||||
import colors from '../config/colors'
|
||||
import { margins, fontSizes } from '../config/scales'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
class Button extends React.Component {
|
||||
static propTypes = {
|
||||
onClick: React.PropTypes.func,
|
||||
style: React.PropTypes.object,
|
||||
"data-wd-key": PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
onClick={this.props.onClick}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
backgroundColor: colors.midgray,
|
||||
color: colors.lowgray,
|
||||
fontSize: fontSizes[5],
|
||||
padding: margins[1],
|
||||
userSelect: 'none',
|
||||
borderRadius: 2,
|
||||
...this.props.style,
|
||||
}}>
|
||||
className={classnames("maputnik-button", this.props.className)}
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
style={this.props.style}>
|
||||
{this.props.children}
|
||||
</a>
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react'
|
||||
import { fontSizes, margins } from '../config/scales'
|
||||
|
||||
class Heading extends React.Component {
|
||||
static propTypes = {
|
||||
level: React.PropTypes.number.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const headingProps = {
|
||||
style: {
|
||||
fontWeight: 400,
|
||||
fontSize: fontSizes[this.props.level - 1],
|
||||
marginBottom: margins[1],
|
||||
...this.props.style
|
||||
}
|
||||
}
|
||||
|
||||
switch(this.props.level) {
|
||||
case 1: return <h1 {...headingProps}>{this.props.children}</h1>
|
||||
case 2: return <h2 {...headingProps}>{this.props.children}</h2>
|
||||
case 3: return <h3 {...headingProps}>{this.props.children}</h3>
|
||||
case 4: return <h4 {...headingProps}>{this.props.children}</h4>
|
||||
case 5: return <h5 {...headingProps}>{this.props.children}</h5>
|
||||
default: return <h6 {...headingProps}>{this.props.children}</h6>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default Heading
|
||||
@@ -1,39 +1,22 @@
|
||||
import React from 'react'
|
||||
import Paragraph from './Paragraph'
|
||||
import colors from '../config/colors'
|
||||
import { fontSizes, margins } from '../config/scales'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class MessagePanel extends React.Component {
|
||||
static propTypes = {
|
||||
errors: React.PropTypes.array,
|
||||
infos: React.PropTypes.array,
|
||||
errors: PropTypes.array,
|
||||
infos: PropTypes.array,
|
||||
}
|
||||
|
||||
render() {
|
||||
const paragraphStyle = {
|
||||
margin: 0,
|
||||
lineHeight: 1.2,
|
||||
}
|
||||
|
||||
const errors = this.props.errors.map((m, i) => {
|
||||
return <Paragraph key={i}
|
||||
style={{
|
||||
...paragraphStyle,
|
||||
color: colors.red,
|
||||
}}>{m}</Paragraph>
|
||||
return <p key={"error-"+i} className="maputnik-message-panel-error">{m}</p>
|
||||
})
|
||||
|
||||
const infos = this.props.infos.map((m, i) => {
|
||||
return <Paragraph key={i}
|
||||
style={{
|
||||
...paragraphStyle,
|
||||
color: colors.lowgray,
|
||||
}}>{m}</Paragraph>
|
||||
return <p key={"info-"+i}>{m}</p>
|
||||
})
|
||||
|
||||
return <div style={{
|
||||
padding: margins[1],
|
||||
}}>
|
||||
return <div className="maputnik-message-panel">
|
||||
{errors}
|
||||
{infos}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react'
|
||||
import colors from '../config/colors'
|
||||
import { margins, fontSizes } from '../config/scales'
|
||||
|
||||
const Paragraph = (props) => <p style={{
|
||||
color: colors.lowgray,
|
||||
fontSize: fontSizes[5],
|
||||
...props.style
|
||||
}}>
|
||||
{props.children}
|
||||
</p>
|
||||
|
||||
export default Paragraph
|
||||
@@ -1,17 +1,16 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const ScrollContainer = (props) => {
|
||||
return <div style={{
|
||||
overflowX: "visible",
|
||||
overflowY: "scroll",
|
||||
bottom:0,
|
||||
left:0,
|
||||
right:0,
|
||||
top:1,
|
||||
position: "absolute",
|
||||
}}>
|
||||
{props.children}
|
||||
</div>
|
||||
class ScrollContainer extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-scroll-container">
|
||||
{this.props.children}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default ScrollContainer
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import FileReaderInput from 'react-file-reader-input'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import MdFileDownload from 'react-icons/lib/md/file-download'
|
||||
import MdFileUpload from 'react-icons/lib/md/file-upload'
|
||||
@@ -14,61 +16,58 @@ import MdInsertEmoticon from 'react-icons/lib/md/insert-emoticon'
|
||||
import MdFontDownload from 'react-icons/lib/md/font-download'
|
||||
import HelpIcon from 'react-icons/lib/md/help-outline'
|
||||
import InspectionIcon from 'react-icons/lib/md/find-in-page'
|
||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||
|
||||
import logoImage from '../img/maputnik.png'
|
||||
import AddModal from './modals/AddModal'
|
||||
import logoImage from 'maputnik-design/logos/logo-color.svg'
|
||||
import SettingsModal from './modals/SettingsModal'
|
||||
import ExportModal from './modals/ExportModal'
|
||||
import SourcesModal from './modals/SourcesModal'
|
||||
import OpenModal from './modals/OpenModal'
|
||||
import pkgJson from '../../package.json'
|
||||
|
||||
import style from '../libs/style'
|
||||
import colors from '../config/colors'
|
||||
import { margins, fontSizes } from '../config/scales'
|
||||
|
||||
const IconText = props => <span style={{ paddingLeft: margins[0] }}>
|
||||
{props.children}
|
||||
</span>
|
||||
|
||||
const actionStyle = {
|
||||
display: "inline-block",
|
||||
padding: 10,
|
||||
fontSize: fontSizes[4],
|
||||
cursor: "pointer",
|
||||
color: colors.white,
|
||||
textDecoration: 'none',
|
||||
}
|
||||
|
||||
const ToolbarLink = props => <a
|
||||
href={props.href}
|
||||
target={"blank"}
|
||||
style={{
|
||||
...actionStyle,
|
||||
...props.style,
|
||||
}}>
|
||||
{props.children}
|
||||
</a>
|
||||
|
||||
class ToolbarAction extends React.Component {
|
||||
class IconText extends React.Component {
|
||||
static propTypes = {
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { hover: false }
|
||||
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,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
className={classnames('maputnik-toolbar-link', this.props.className)}
|
||||
href={this.props.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{this.props.children}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarAction extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
onClick: PropTypes.func,
|
||||
wdKey: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
className='maputnik-toolbar-action'
|
||||
data-wd-key={this.props.wdKey}
|
||||
onClick={this.props.onClick}
|
||||
onMouseOver={e => this.setState({hover: true})}
|
||||
onMouseOut={e => this.setState({hover: false})}
|
||||
style={{
|
||||
...actionStyle,
|
||||
...this.props.style,
|
||||
backgroundColor: this.state.hover ? colors.gray : null,
|
||||
}}>
|
||||
>
|
||||
{this.props.children}
|
||||
</a>
|
||||
}
|
||||
@@ -76,14 +75,15 @@ class ToolbarAction extends React.Component {
|
||||
|
||||
export default class Toolbar extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
onStyleChanged: React.PropTypes.func.isRequired,
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
inspectModeEnabled: PropTypes.bool.isRequired,
|
||||
onStyleChanged: PropTypes.func.isRequired,
|
||||
// A new style has been uploaded
|
||||
onStyleOpen: React.PropTypes.func.isRequired,
|
||||
// Current style is requested for download
|
||||
onStyleDownload: React.PropTypes.func.isRequired,
|
||||
onStyleOpen: PropTypes.func.isRequired,
|
||||
// A dict of source id's and the available source layers
|
||||
sources: React.PropTypes.object.isRequired,
|
||||
sources: PropTypes.object.isRequired,
|
||||
onInspectModeToggle: PropTypes.func.isRequired,
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -94,33 +94,11 @@ export default class Toolbar extends React.Component {
|
||||
sources: false,
|
||||
open: false,
|
||||
add: false,
|
||||
export: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
downloadButton() {
|
||||
return <ToolbarAction onClick={this.props.onStyleDownload}>
|
||||
<MdFileDownload />
|
||||
<IconText>Download</IconText>
|
||||
</ToolbarAction>
|
||||
}
|
||||
|
||||
toggleInspectionMode() {
|
||||
const metadata = this.props.mapStyle.metadata || {}
|
||||
const currentRenderer = metadata['maputnik:renderer'] || 'mbgljs'
|
||||
|
||||
const changedRenderer = currentRenderer === 'inspection' ? 'mbgljs' : 'inspection'
|
||||
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
metadata: {
|
||||
'maputnik:renderer': changedRenderer
|
||||
}
|
||||
}
|
||||
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
toggleModal(modalName) {
|
||||
this.setState({
|
||||
isOpen: {
|
||||
@@ -131,21 +109,19 @@ export default class Toolbar extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
position: "fixed",
|
||||
height: 40,
|
||||
width: '100%',
|
||||
zIndex: 100,
|
||||
left: 0,
|
||||
top: 0,
|
||||
backgroundColor: colors.black
|
||||
}}>
|
||||
return <div className='maputnik-toolbar'>
|
||||
<SettingsModal
|
||||
mapStyle={this.props.mapStyle}
|
||||
onStyleChanged={this.props.onStyleChanged}
|
||||
isOpen={this.state.isOpen.settings}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
||||
/>
|
||||
<ExportModal
|
||||
mapStyle={this.props.mapStyle}
|
||||
onStyleChanged={this.props.onStyleChanged}
|
||||
isOpen={this.state.isOpen.export}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
||||
/>
|
||||
<OpenModal
|
||||
isOpen={this.state.isOpen.open}
|
||||
onStyleOpen={this.props.onStyleOpen}
|
||||
@@ -157,50 +133,46 @@ export default class Toolbar extends React.Component {
|
||||
isOpen={this.state.isOpen.sources}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
||||
/>
|
||||
<AddModal
|
||||
mapStyle={this.props.mapStyle}
|
||||
sources={this.props.sources}
|
||||
isOpen={this.state.isOpen.add}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'add')}
|
||||
onStyleChange={this.props.onStyleChanged}
|
||||
/>
|
||||
<ToolbarLink
|
||||
href={"https://github.com/maputnik/editor"}
|
||||
style={{
|
||||
width: 180,
|
||||
textAlign: 'left',
|
||||
backgroundColor: colors.black,
|
||||
padding: 5,
|
||||
}}
|
||||
>
|
||||
<img src={logoImage} alt="Maputnik" style={{width: 30, height: 30, paddingRight: 5, verticalAlign: 'middle'}}/>
|
||||
<span style={{fontSize: 20, verticalAlign: 'middle' }}>Maputnik</span>
|
||||
</ToolbarLink>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
|
||||
<OpenIcon />
|
||||
<IconText>Open</IconText>
|
||||
</ToolbarAction>
|
||||
{this.downloadButton()}
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'add')}>
|
||||
<AddIcon />
|
||||
<IconText>Add Layer</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'sources')}>
|
||||
<SourcesIcon />
|
||||
<IconText>Sources</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'settings')}>
|
||||
<SettingsIcon />
|
||||
<IconText>Style Settings</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.toggleInspectionMode.bind(this)}>
|
||||
<InspectionIcon />
|
||||
<IconText>Inspect</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
||||
<HelpIcon />
|
||||
<IconText>Help</IconText>
|
||||
</ToolbarLink>
|
||||
<div className="maputnik-toolbar__inner">
|
||||
<ToolbarLink
|
||||
href={"https://github.com/maputnik/editor"}
|
||||
className="maputnik-toolbar-logo"
|
||||
>
|
||||
<img src={logoImage} alt="Maputnik" />
|
||||
<h1>Maputnik
|
||||
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
||||
</h1>
|
||||
</ToolbarLink>
|
||||
<div className="maputnik-toolbar__actions">
|
||||
<ToolbarAction wdKey="nav:open" onClick={this.toggleModal.bind(this, 'open')}>
|
||||
<OpenIcon />
|
||||
<IconText>Open</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:export" onClick={this.toggleModal.bind(this, 'export')}>
|
||||
<MdFileDownload />
|
||||
<IconText>Export</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:sources" onClick={this.toggleModal.bind(this, 'sources')}>
|
||||
<SourcesIcon />
|
||||
<IconText>Sources</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:settings" onClick={this.toggleModal.bind(this, 'settings')}>
|
||||
<SettingsIcon />
|
||||
<IconText>Style Settings</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:inspect" onClick={this.props.onInspectModeToggle}>
|
||||
<InspectionIcon />
|
||||
<IconText>
|
||||
{ this.props.inspectModeEnabled && <span>Map Mode</span> }
|
||||
{ !this.props.inspectModeEnabled && <span>Inspect Mode</span> }
|
||||
</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
||||
<HelpIcon />
|
||||
<IconText>Help</IconText>
|
||||
</ToolbarLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react'
|
||||
import Color from 'color'
|
||||
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
||||
|
||||
import input from '../../config/input.js'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
function formatColor(color) {
|
||||
const rgb = color.rgb
|
||||
@@ -12,12 +11,12 @@ function formatColor(color) {
|
||||
/*** Number fields with support for min, max and units and documentation*/
|
||||
class ColorField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
name: React.PropTypes.string.isRequired,
|
||||
value: React.PropTypes.string,
|
||||
doc: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
default: React.PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string,
|
||||
doc: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
default: PropTypes.string,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -31,7 +30,7 @@ class ColorField extends React.Component {
|
||||
//but I am too stupid to get it to work together with fixed position
|
||||
//and scrollbars so I have to fallback to JavaScript
|
||||
calcPickerOffset() {
|
||||
const elem = this.refs.colorInput
|
||||
const elem = this.colorInput
|
||||
if(elem) {
|
||||
const pos = elem.getBoundingClientRect()
|
||||
return {
|
||||
@@ -39,7 +38,6 @@ class ColorField extends React.Component {
|
||||
left: pos.left + 196,
|
||||
}
|
||||
} else {
|
||||
console.warn('Color field has no element to adjust position')
|
||||
return {
|
||||
top: 160,
|
||||
left: 555,
|
||||
@@ -52,23 +50,41 @@ class ColorField extends React.Component {
|
||||
}
|
||||
|
||||
get color() {
|
||||
return Color(this.props.value || '#fff')
|
||||
// Catch invalid color.
|
||||
try {
|
||||
return Color(this.props.value).rgb()
|
||||
}
|
||||
catch(err) {
|
||||
console.warn("Error parsing color: ", err);
|
||||
return Color("rgb(255,255,255)");
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const offset = this.calcPickerOffset()
|
||||
var currentColor = this.color.object()
|
||||
currentColor = {
|
||||
r: currentColor.r,
|
||||
g: currentColor.g,
|
||||
b: currentColor.b,
|
||||
// Rename alpha -> a for ChromePicker
|
||||
a: currentColor.alpha
|
||||
}
|
||||
|
||||
const picker = <div
|
||||
className="maputnik-color-picker-offset"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
zIndex: 1,
|
||||
position: 'fixed',
|
||||
zIndex: 1,
|
||||
left: offset.left,
|
||||
top: offset.top,
|
||||
}}>
|
||||
<ChromePicker
|
||||
color={this.color.object()}
|
||||
color={currentColor}
|
||||
onChange={c => this.props.onChange(formatColor(c))}
|
||||
/>
|
||||
<div
|
||||
className="maputnik-color-picker-offset"
|
||||
onClick={this.togglePicker.bind(this)}
|
||||
style={{
|
||||
zIndex: -1,
|
||||
@@ -81,18 +97,18 @@ class ColorField extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
return <div style={{
|
||||
position: 'relative',
|
||||
display: 'inline',
|
||||
}}>
|
||||
var swatchStyle = {
|
||||
backgroundColor: this.props.value
|
||||
};
|
||||
|
||||
return <div className="maputnik-color-wrapper">
|
||||
{this.state.pickerOpened && picker}
|
||||
<div className="maputnik-color-swatch" style={swatchStyle}></div>
|
||||
<input
|
||||
ref="colorInput"
|
||||
className="maputnik-color"
|
||||
ref={(input) => this.colorInput = input}
|
||||
onClick={this.togglePicker.bind(this)}
|
||||
style={{
|
||||
...input.input,
|
||||
...this.props.style
|
||||
}}
|
||||
style={this.props.style}
|
||||
name={this.props.name}
|
||||
placeholder={this.props.default}
|
||||
value={this.props.value ? this.props.value : ""}
|
||||
|
||||
@@ -1,50 +1,23 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
import colors from '../../config/colors.js'
|
||||
import { margins, fontSizes } from '../../config/scales.js'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class DocLabel extends React.Component {
|
||||
static propTypes = {
|
||||
label: React.PropTypes.string.isRequired,
|
||||
doc: React.PropTypes.string.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { showDoc: false }
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string
|
||||
]).isRequired,
|
||||
doc: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <label
|
||||
style={{
|
||||
...input.label,
|
||||
...this.props.style,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
onMouseOver={e => this.setState({showDoc: true})}
|
||||
onMouseOut={e => this.setState({showDoc: false})}
|
||||
style={{
|
||||
cursor: 'help',
|
||||
}}
|
||||
>
|
||||
{this.props.label}
|
||||
</span>
|
||||
<div style={{
|
||||
backgroundColor: colors.gray,
|
||||
padding: margins[1],
|
||||
fontSize: 10,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
left: 0,
|
||||
width: 120,
|
||||
display: this.state.showDoc ? null : 'none',
|
||||
zIndex: 3,
|
||||
}}>
|
||||
{this.props.doc}
|
||||
</div>
|
||||
return <label className="maputnik-doc-wrapper">
|
||||
<div className="maputnik-doc-target">
|
||||
<span>{this.props.label}</span>
|
||||
<div className="maputnik-doc-popup">
|
||||
{this.props.doc}
|
||||
</div>
|
||||
</div >
|
||||
</label>
|
||||
}
|
||||
}
|
||||
|
||||
139
src/components/fields/FunctionSpecField.jsx
Normal file
139
src/components/fields/FunctionSpecField.jsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import SpecProperty from './_SpecProperty'
|
||||
import DataProperty from './_DataProperty'
|
||||
import ZoomProperty from './_ZoomProperty'
|
||||
|
||||
|
||||
function isZoomField(value) {
|
||||
return typeof value === 'object' && value.stops && typeof value.property === 'undefined'
|
||||
}
|
||||
|
||||
function isDataField(value) {
|
||||
return typeof value === 'object' && value.stops && typeof value.property !== 'undefined'
|
||||
}
|
||||
|
||||
/** Supports displaying spec field for zoom function objects
|
||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||
*/
|
||||
export default class FunctionSpecProperty extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
fieldName: PropTypes.string.isRequired,
|
||||
fieldSpec: PropTypes.object.isRequired,
|
||||
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.bool,
|
||||
PropTypes.array
|
||||
]),
|
||||
}
|
||||
|
||||
addStop() {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
const lastStop = stops[stops.length - 1]
|
||||
if (typeof lastStop[0] === "object") {
|
||||
stops.push([
|
||||
{zoom: lastStop[0].zoom + 1, value: lastStop[0].value},
|
||||
lastStop[1]
|
||||
])
|
||||
}
|
||||
else {
|
||||
stops.push([lastStop[0] + 1, lastStop[1]])
|
||||
}
|
||||
|
||||
const changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
deleteStop(stopIdx) {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
stops.splice(stopIdx, 1)
|
||||
|
||||
let changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
|
||||
if(stops.length === 1) {
|
||||
changedValue = stops[0][1]
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
makeZoomFunction() {
|
||||
const zoomFunc = {
|
||||
stops: [
|
||||
[6, this.props.value],
|
||||
[10, this.props.value]
|
||||
]
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, zoomFunc)
|
||||
}
|
||||
|
||||
makeDataFunction() {
|
||||
const dataFunc = {
|
||||
property: "",
|
||||
type: "categorical",
|
||||
stops: [
|
||||
[{zoom: 6, value: 0}, this.props.value],
|
||||
[{zoom: 10, value: 0}, this.props.value]
|
||||
]
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, dataFunc)
|
||||
}
|
||||
|
||||
render() {
|
||||
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
|
||||
let specField;
|
||||
|
||||
if (isZoomField(this.props.value)) {
|
||||
specField = (
|
||||
<ZoomProperty
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onDeleteStop={this.deleteStop.bind(this)}
|
||||
onAddStop={this.addStop.bind(this)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
else if (isDataField(this.props.value)) {
|
||||
specField = (
|
||||
<DataProperty
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onDeleteStop={this.deleteStop.bind(this)}
|
||||
onAddStop={this.addStop.bind(this)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
else {
|
||||
specField = (
|
||||
<SpecProperty
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onZoomClick={this.makeZoomFunction.bind(this)}
|
||||
onDataClick={this.makeDataFunction.bind(this)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <div className={propClass} data-wd-key={"spec-field:"+this.props.fieldName}>
|
||||
{specField}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
import React from 'react'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import ZoomSpecField from './ZoomSpecField'
|
||||
import colors from '../../config/colors'
|
||||
import { margins } from '../../config/scales'
|
||||
import FunctionSpecField from './FunctionSpecField'
|
||||
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
||||
|
||||
/** Extract field spec by {@fieldName} from the {@layerType} in the
|
||||
* style specification from either the paint or layout group */
|
||||
function getFieldSpec(layerType, fieldName) {
|
||||
const groupName = getGroupName(layerType, fieldName)
|
||||
const group = GlSpec[groupName + '_' + layerType]
|
||||
return group[fieldName]
|
||||
function getFieldSpec(spec, layerType, fieldName) {
|
||||
const groupName = getGroupName(spec, layerType, fieldName)
|
||||
const group = spec[groupName + '_' + layerType]
|
||||
const fieldSpec = group[fieldName]
|
||||
if(iconProperties.indexOf(fieldName) >= 0) {
|
||||
return {
|
||||
...fieldSpec,
|
||||
values: spec.$root.sprite.values
|
||||
}
|
||||
}
|
||||
if(fieldName === 'text-font') {
|
||||
return {
|
||||
...fieldSpec,
|
||||
values: spec.$root.glyphs.values
|
||||
}
|
||||
}
|
||||
return fieldSpec
|
||||
}
|
||||
|
||||
function getGroupName(layerType, fieldName) {
|
||||
const paint = GlSpec['paint_' + layerType] || {}
|
||||
function getGroupName(spec, layerType, fieldName) {
|
||||
const paint = spec['paint_' + layerType] || {}
|
||||
if (fieldName in paint) {
|
||||
return 'paint'
|
||||
} else {
|
||||
@@ -24,34 +36,35 @@ function getGroupName(layerType, fieldName) {
|
||||
|
||||
export default class PropertyGroup extends React.Component {
|
||||
static propTypes = {
|
||||
layer: React.PropTypes.object.isRequired,
|
||||
groupFields: React.PropTypes.array.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
layer: PropTypes.object.isRequired,
|
||||
groupFields: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
spec: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
onPropertyChange(property, newValue) {
|
||||
const group = getGroupName(this.props.layer.type, property)
|
||||
const group = getGroupName(this.props.spec, this.props.layer.type, property)
|
||||
this.props.onChange(group , property, newValue)
|
||||
}
|
||||
|
||||
render() {
|
||||
const fields = this.props.groupFields.map(fieldName => {
|
||||
const fieldSpec = getFieldSpec(this.props.layer.type, fieldName)
|
||||
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName)
|
||||
|
||||
const paint = this.props.layer.paint || {}
|
||||
const layout = this.props.layer.layout || {}
|
||||
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
|
||||
|
||||
return <ZoomSpecField
|
||||
return <FunctionSpecField
|
||||
onChange={this.onPropertyChange.bind(this)}
|
||||
key={fieldName}
|
||||
fieldName={fieldName}
|
||||
value={fieldValue}
|
||||
value={fieldValue === undefined ? fieldSpec.default : fieldValue}
|
||||
fieldSpec={fieldSpec}
|
||||
/>
|
||||
})
|
||||
|
||||
return <div>
|
||||
return <div className="maputnik-property-group">
|
||||
{fields}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import color from 'color'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
import ColorField from './ColorField'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
import CheckboxInput from '../inputs/CheckboxInput'
|
||||
@@ -9,11 +9,12 @@ import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import MultiButtonInput from '../inputs/MultiButtonInput'
|
||||
import ArrayInput from '../inputs/ArrayInput'
|
||||
import DynamicArrayInput from '../inputs/DynamicArrayInput'
|
||||
import FontInput from '../inputs/FontInput'
|
||||
import IconInput from '../inputs/IconInput'
|
||||
import capitalize from 'lodash.capitalize'
|
||||
|
||||
|
||||
import input from '../../config/input.js'
|
||||
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
||||
|
||||
function labelFromFieldName(fieldName) {
|
||||
let label = fieldName.split('-').slice(1).join(' ')
|
||||
@@ -36,76 +37,100 @@ function optionsLabelLength(options) {
|
||||
* to display @{value}. */
|
||||
export default class SpecField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
fieldName: React.PropTypes.string.isRequired,
|
||||
fieldSpec: React.PropTypes.object.isRequired,
|
||||
value: React.PropTypes.oneOfType([
|
||||
React.PropTypes.string,
|
||||
React.PropTypes.number,
|
||||
React.PropTypes.array,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
fieldName: PropTypes.string.isRequired,
|
||||
fieldSpec: PropTypes.object.isRequired,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.array,
|
||||
PropTypes.bool
|
||||
]),
|
||||
/** Override the style of the field */
|
||||
style: React.PropTypes.object,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const commonProps = {
|
||||
style: this.props.style,
|
||||
default: this.props.fieldSpec.default,
|
||||
value: this.props.value,
|
||||
default: this.props.fieldSpec.default,
|
||||
name: this.props.fieldName,
|
||||
onChange: newValue => this.props.onChange(this.props.fieldName, newValue)
|
||||
}
|
||||
switch(this.props.fieldSpec.type) {
|
||||
case 'number': return (
|
||||
<NumberInput
|
||||
{...commonProps}
|
||||
min={this.props.fieldSpec.minimum}
|
||||
max={this.props.fieldSpec.maximum}
|
||||
/>
|
||||
)
|
||||
case 'enum':
|
||||
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)])
|
||||
|
||||
if(options.length <= 3 && optionsLabelLength(options) <= 20) {
|
||||
return <MultiButtonInput
|
||||
function childNodes() {
|
||||
switch(this.props.fieldSpec.type) {
|
||||
case 'number': return (
|
||||
<NumberInput
|
||||
{...commonProps}
|
||||
options={options}
|
||||
min={this.props.fieldSpec.minimum}
|
||||
max={this.props.fieldSpec.maximum}
|
||||
/>
|
||||
} else {
|
||||
return <SelectInput
|
||||
{...commonProps}
|
||||
options={options}
|
||||
/>
|
||||
}
|
||||
case 'string': return (
|
||||
<StringInput
|
||||
{...commonProps}
|
||||
/>
|
||||
)
|
||||
case 'color': return (
|
||||
<ColorField
|
||||
{...commonProps}
|
||||
/>
|
||||
)
|
||||
case 'boolean': return (
|
||||
<CheckboxInput
|
||||
{...commonProps}
|
||||
/>
|
||||
)
|
||||
case 'array':
|
||||
if(this.props.fieldName === 'text-font') {
|
||||
return <FontInput
|
||||
)
|
||||
case 'enum':
|
||||
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)])
|
||||
|
||||
if(options.length <= 3 && optionsLabelLength(options) <= 20) {
|
||||
return <MultiButtonInput
|
||||
{...commonProps}
|
||||
options={options}
|
||||
/>
|
||||
} else {
|
||||
return <SelectInput
|
||||
{...commonProps}
|
||||
options={options}
|
||||
/>
|
||||
}
|
||||
case 'string':
|
||||
if(iconProperties.indexOf(this.props.fieldName) >= 0) {
|
||||
return <IconInput
|
||||
{...commonProps}
|
||||
icons={this.props.fieldSpec.values}
|
||||
/>
|
||||
} else {
|
||||
return <StringInput
|
||||
{...commonProps}
|
||||
/>
|
||||
}
|
||||
case 'color': return (
|
||||
<ColorField
|
||||
{...commonProps}
|
||||
/>
|
||||
} else {
|
||||
return <ArrayInput
|
||||
)
|
||||
case 'boolean': return (
|
||||
<CheckboxInput
|
||||
{...commonProps}
|
||||
type={this.props.fieldSpec.value}
|
||||
length={this.props.fieldSpec.length}
|
||||
/>
|
||||
}
|
||||
default: return null
|
||||
)
|
||||
case 'array':
|
||||
if(this.props.fieldName === 'text-font') {
|
||||
return <FontInput
|
||||
{...commonProps}
|
||||
fonts={this.props.fieldSpec.values}
|
||||
/>
|
||||
} else {
|
||||
if (this.props.fieldSpec.length) {
|
||||
return <ArrayInput
|
||||
{...commonProps}
|
||||
type={this.props.fieldSpec.value}
|
||||
length={this.props.fieldSpec.length}
|
||||
/>
|
||||
} else {
|
||||
return <DynamicArrayInput
|
||||
{...commonProps}
|
||||
type={this.props.fieldSpec.value}
|
||||
/>
|
||||
}
|
||||
}
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-wd-key={"spec-field:"+this.props.fieldName}>
|
||||
{childNodes.call(this)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
import React from 'react'
|
||||
import Color from 'color'
|
||||
|
||||
import Button from '../Button'
|
||||
import SpecField from './SpecField'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
import DocLabel from './DocLabel'
|
||||
|
||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
|
||||
import capitalize from 'lodash.capitalize'
|
||||
import input from '../../config/input.js'
|
||||
import colors from '../../config/colors.js'
|
||||
import { margins, fontSizes } from '../../config/scales.js'
|
||||
|
||||
function isZoomField(value) {
|
||||
return typeof value === 'object' && value.stops
|
||||
}
|
||||
|
||||
/** Supports displaying spec field for zoom function objects
|
||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||
*/
|
||||
export default class ZoomSpecField extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
fieldName: React.PropTypes.string.isRequired,
|
||||
fieldSpec: React.PropTypes.object.isRequired,
|
||||
|
||||
value: React.PropTypes.oneOfType([
|
||||
React.PropTypes.object,
|
||||
React.PropTypes.string,
|
||||
React.PropTypes.number,
|
||||
React.PropTypes.bool,
|
||||
]),
|
||||
}
|
||||
|
||||
addStop() {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
const lastStop = stops[stops.length - 1]
|
||||
stops.push([lastStop[0] + 1, lastStop[1]])
|
||||
|
||||
const changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
deleteStop(stopIdx) {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
stops.splice(stopIdx, 1)
|
||||
|
||||
let changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
|
||||
if(stops.length === 1) {
|
||||
changedValue = stops[0][1]
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
changeStop(changeIdx, zoomLevel, value) {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
stops[changeIdx] = [zoomLevel, value]
|
||||
const changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
render() {
|
||||
let label = <DocLabel
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
doc={this.props.fieldSpec.doc}
|
||||
style={{
|
||||
width: '50%',
|
||||
}}
|
||||
/>
|
||||
|
||||
if(isZoomField(this.props.value)) {
|
||||
const zoomFields = this.props.value.stops.map((stop, idx) => {
|
||||
label = <DocLabel
|
||||
doc={this.props.fieldSpec.doc}
|
||||
style={{ width: '42.5%'}}
|
||||
label={idx > 0 ? "" : labelFromFieldName(this.props.fieldName)}
|
||||
/>
|
||||
|
||||
if(idx === 1) {
|
||||
label = <label style={{...input.label, width: '42.5%'}}>
|
||||
<Button
|
||||
style={{fontSize: fontSizes[5]}}
|
||||
onClick={this.addStop.bind(this)}
|
||||
>
|
||||
Add stop
|
||||
</Button>
|
||||
</label>
|
||||
}
|
||||
|
||||
const zoomLevel = stop[0]
|
||||
const value = stop[1]
|
||||
|
||||
return <div key={zoomLevel} style={{
|
||||
...input.property,
|
||||
marginLeft: 0,
|
||||
marginRight: 0
|
||||
}}>
|
||||
{label}
|
||||
<Button
|
||||
style={{backgroundColor: null}}
|
||||
onClick={this.deleteStop.bind(this, idx)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</Button>
|
||||
<NumberInput
|
||||
style={{
|
||||
width: '7%',
|
||||
}}
|
||||
value={zoomLevel}
|
||||
onChange={changedStop => this.changeStop(idx, changedStop, value)}
|
||||
min={0}
|
||||
max={22}
|
||||
/>
|
||||
<SpecField
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={value}
|
||||
onChange={(_, newValue) => this.changeStop(idx, zoomLevel, newValue)}
|
||||
style={{
|
||||
width: '42%',
|
||||
marginLeft: margins[0],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
})
|
||||
|
||||
return <div style={input.property}>
|
||||
{zoomFields}
|
||||
</div>
|
||||
} else {
|
||||
return <div style={input.property}>
|
||||
{label}
|
||||
<SpecField {...this.props} style={{ width: '50%' } }/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function labelFromFieldName(fieldName) {
|
||||
let label = fieldName.split('-').slice(1).join(' ')
|
||||
return capitalize(label)
|
||||
}
|
||||
182
src/components/fields/_DataProperty.jsx
Normal file
182
src/components/fields/_DataProperty.jsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../Button'
|
||||
import SpecField from './SpecField'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import DocLabel from './DocLabel'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
|
||||
import labelFromFieldName from './_labelFromFieldName'
|
||||
import DeleteStopButton from './_DeleteStopButton'
|
||||
|
||||
|
||||
export default class DataProperty extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
onDeleteStop: PropTypes.func,
|
||||
onAddStop: PropTypes.func,
|
||||
fieldName: PropTypes.string,
|
||||
fieldSpec: PropTypes.object,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.bool,
|
||||
PropTypes.array
|
||||
]),
|
||||
}
|
||||
|
||||
getFieldFunctionType(fieldSpec) {
|
||||
if (fieldSpec.function === "interpolated") {
|
||||
return "exponential"
|
||||
}
|
||||
if (fieldSpec.type === "number") {
|
||||
return "interval"
|
||||
}
|
||||
return "categorical"
|
||||
}
|
||||
|
||||
getDataFunctionTypes(functionType) {
|
||||
if (functionType === "interpolated") {
|
||||
return ["categorical", "interval", "exponential"]
|
||||
}
|
||||
else {
|
||||
return ["categorical", "interval"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
changeStop(changeIdx, stopData, value) {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
const changedStop = stopData.zoom === undefined ? stopData.value : stopData
|
||||
stops[changeIdx] = [changedStop, value]
|
||||
const changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
changeDataProperty(propName, propVal) {
|
||||
if (propVal) {
|
||||
this.props.value[propName] = propVal
|
||||
}
|
||||
else {
|
||||
delete this.props.value[propName]
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, this.props.value)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (typeof this.props.value.type === "undefined") {
|
||||
this.props.value.type = this.getFieldFunctionType(this.props.fieldSpec)
|
||||
}
|
||||
|
||||
const dataFields = this.props.value.stops.map((stop, idx) => {
|
||||
const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined;
|
||||
const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0];
|
||||
const value = stop[1]
|
||||
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
||||
|
||||
const dataProps = {
|
||||
label: "Data value",
|
||||
value: dataLevel,
|
||||
onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value)
|
||||
}
|
||||
|
||||
let dataInput;
|
||||
if(this.props.value.type === "categorical") {
|
||||
dataInput = <StringInput {...dataProps} />
|
||||
}
|
||||
else {
|
||||
dataInput = <NumberInput {...dataProps} />
|
||||
}
|
||||
|
||||
let zoomInput = null;
|
||||
if(zoomLevel !== undefined) {
|
||||
zoomInput = <div className="maputnik-data-spec-property-stop-edit">
|
||||
<NumberInput
|
||||
value={zoomLevel}
|
||||
onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
|
||||
min={0}
|
||||
max={22}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
return <InputBlock key={idx} action={deleteStopBtn} label="">
|
||||
{zoomInput}
|
||||
<div className="maputnik-data-spec-property-stop-data">
|
||||
{dataInput}
|
||||
</div>
|
||||
<div className="maputnik-data-spec-property-stop-value">
|
||||
<SpecField
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={value}
|
||||
onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)}
|
||||
/>
|
||||
</div>
|
||||
</InputBlock>
|
||||
})
|
||||
|
||||
return <div className="maputnik-data-spec-block">
|
||||
<div className="maputnik-data-spec-property">
|
||||
<InputBlock
|
||||
doc={this.props.fieldSpec.doc}
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
>
|
||||
<div className="maputnik-data-spec-property-group">
|
||||
<DocLabel
|
||||
label="Property"
|
||||
doc={"Input a data property to base styles off of."}
|
||||
/>
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
<StringInput
|
||||
value={this.props.value.property}
|
||||
onChange={propVal => this.changeDataProperty("property", propVal)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="maputnik-data-spec-property-group">
|
||||
<DocLabel
|
||||
label="Type"
|
||||
doc={"Select a type of data scale (default is 'categorical')."}
|
||||
/>
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
<SelectInput
|
||||
value={this.props.value.type}
|
||||
onChange={propVal => this.changeDataProperty("type", propVal)}
|
||||
options={this.getDataFunctionTypes(this.props.fieldSpec.function)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="maputnik-data-spec-property-group">
|
||||
<DocLabel
|
||||
label="Default"
|
||||
doc={"Input a default value for data if not covered by the scales."}
|
||||
/>
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
<SpecField
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value.default}
|
||||
onChange={(_, propVal) => this.changeDataProperty("default", propVal)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InputBlock>
|
||||
</div>
|
||||
{dataFields}
|
||||
<Button
|
||||
className="maputnik-add-stop"
|
||||
onClick={this.props.onAddStop.bind(this)}
|
||||
>
|
||||
Add stop
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
25
src/components/fields/_DeleteStopButton.jsx
Normal file
25
src/components/fields/_DeleteStopButton.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import DocLabel from './DocLabel'
|
||||
import Button from '../Button'
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
|
||||
|
||||
export default class DeleteStopButton extends React.Component {
|
||||
static propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Button
|
||||
className="maputnik-delete-stop"
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
<DocLabel
|
||||
label={<DeleteIcon />}
|
||||
doc={"Remove zoom level stop."}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
}
|
||||
49
src/components/fields/_FunctionButtons.jsx
Normal file
49
src/components/fields/_FunctionButtons.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import DocLabel from './DocLabel'
|
||||
import Button from '../Button'
|
||||
import FunctionIcon from 'react-icons/lib/md/functions'
|
||||
import MdInsertChart from 'react-icons/lib/md/insert-chart'
|
||||
|
||||
|
||||
export default class FunctionButtons extends React.Component {
|
||||
static propTypes = {
|
||||
fieldSpec: PropTypes.object,
|
||||
onZoomClick: PropTypes.func,
|
||||
onDataClick: PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
let makeZoomButton, makeDataButton
|
||||
if (this.props.fieldSpec['zoom-function']) {
|
||||
makeZoomButton = <Button
|
||||
className="maputnik-make-zoom-function"
|
||||
onClick={this.props.onZoomClick}
|
||||
>
|
||||
<DocLabel
|
||||
label={<FunctionIcon />}
|
||||
cursorTargetStyle={{ cursor: 'pointer' }}
|
||||
doc={"Turn property into a zoom function to enable a map feature to change with map's zoom level."}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
if (this.props.fieldSpec['property-function'] && ['piecewise-constant', 'interpolated'].indexOf(this.props.fieldSpec['function']) !== -1) {
|
||||
makeDataButton = <Button
|
||||
className="maputnik-make-data-function"
|
||||
onClick={this.props.onDataClick}
|
||||
>
|
||||
<DocLabel
|
||||
label={<MdInsertChart />}
|
||||
cursorTargetStyle={{ cursor: 'pointer' }}
|
||||
doc={"Turn property into a data function to enable a map feature to change according to data properties and the map's zoom level."}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
return <div>{makeDataButton}{makeZoomButton}</div>
|
||||
}
|
||||
else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/components/fields/_SpecProperty.jsx
Normal file
34
src/components/fields/_SpecProperty.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import SpecField from './SpecField'
|
||||
import FunctionButtons from './_FunctionButtons'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
|
||||
import labelFromFieldName from './_labelFromFieldName'
|
||||
|
||||
|
||||
export default class SpecProperty extends React.Component {
|
||||
static propTypes = {
|
||||
onZoomClick: PropTypes.func.isRequired,
|
||||
onDataClick: PropTypes.func.isRequired,
|
||||
fieldName: PropTypes.string,
|
||||
fieldSpec: PropTypes.object
|
||||
}
|
||||
|
||||
render() {
|
||||
const functionBtn = <FunctionButtons
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
onZoomClick={this.props.onZoomClick}
|
||||
onDataClick={this.props.onDataClick}
|
||||
/>
|
||||
|
||||
return <InputBlock
|
||||
doc={this.props.fieldSpec.doc}
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
action={functionBtn}
|
||||
>
|
||||
<SpecField {...this.props} />
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
161
src/components/fields/_ZoomProperty.jsx
Normal file
161
src/components/fields/_ZoomProperty.jsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../Button'
|
||||
import SpecField from './SpecField'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
|
||||
import DeleteStopButton from './_DeleteStopButton'
|
||||
import labelFromFieldName from './_labelFromFieldName'
|
||||
|
||||
import docUid from '../../libs/document-uid'
|
||||
import sortNumerically from '../../libs/sort-numerically'
|
||||
|
||||
|
||||
export default class ZoomProperty extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
onDeleteStop: PropTypes.func,
|
||||
onAddStop: PropTypes.func,
|
||||
fieldName: PropTypes.string,
|
||||
fieldSpec: PropTypes.object,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.bool,
|
||||
PropTypes.array
|
||||
]),
|
||||
}
|
||||
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.state = {
|
||||
refs: {}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
refs: this.setStopRefs(this.props)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* We cache a reference for each stop by its index.
|
||||
*
|
||||
* When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus.
|
||||
*/
|
||||
setStopRefs(props) {
|
||||
// This is initialsed below only if required to improved performance.
|
||||
let newRefs;
|
||||
|
||||
if(props.value && props.value.stops) {
|
||||
props.value.stops.forEach((val, idx) => {
|
||||
if(!this.state.refs.hasOwnProperty(idx)) {
|
||||
if(!newRefs) {
|
||||
newRefs = {...this.state.refs};
|
||||
}
|
||||
newRefs[idx] = docUid("stop-");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return newRefs;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const newRefs = this.setStopRefs(nextProps);
|
||||
if(newRefs) {
|
||||
this.setState({
|
||||
refs: newRefs
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Order the stops altering the refs to reflect their new position.
|
||||
orderStopsByZoom(stops) {
|
||||
const mappedWithRef = stops
|
||||
.map((stop, idx) => {
|
||||
return {
|
||||
ref: this.state.refs[idx],
|
||||
data: stop
|
||||
}
|
||||
})
|
||||
// Sort by zoom
|
||||
.sort((a, b) => sortNumerically(a.data[0], b.data[0]));
|
||||
|
||||
// Fetch the new position of the stops
|
||||
const newRefs = {};
|
||||
mappedWithRef
|
||||
.forEach((stop, idx) =>{
|
||||
newRefs[idx] = stop.ref;
|
||||
})
|
||||
|
||||
this.setState({
|
||||
refs: newRefs
|
||||
});
|
||||
|
||||
return mappedWithRef.map((item) => item.data);
|
||||
}
|
||||
|
||||
changeZoomStop(changeIdx, stopData, value) {
|
||||
const stops = this.props.value.stops.slice(0);
|
||||
stops[changeIdx] = [stopData, value];
|
||||
|
||||
const orderedStops = this.orderStopsByZoom(stops);
|
||||
|
||||
const changedValue = {
|
||||
...this.props.value,
|
||||
stops: orderedStops
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
|
||||
render() {
|
||||
const zoomFields = this.props.value.stops.map((stop, idx) => {
|
||||
const zoomLevel = stop[0]
|
||||
const key = this.state.refs[idx];
|
||||
const value = stop[1]
|
||||
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
||||
|
||||
return <InputBlock
|
||||
key={key}
|
||||
doc={this.props.fieldSpec.doc}
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
action={deleteStopBtn}
|
||||
>
|
||||
<div>
|
||||
<div className="maputnik-zoom-spec-property-stop-edit">
|
||||
<NumberInput
|
||||
value={zoomLevel}
|
||||
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
|
||||
min={0}
|
||||
max={22}
|
||||
/>
|
||||
</div>
|
||||
<div className="maputnik-zoom-spec-property-stop-value">
|
||||
<SpecField
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={value}
|
||||
onChange={(_, newValue) => this.changeZoomStop(idx, zoomLevel, newValue)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InputBlock>
|
||||
});
|
||||
|
||||
return <div className="maputnik-zoom-spec-property">
|
||||
{zoomFields}
|
||||
<Button
|
||||
className="maputnik-add-stop"
|
||||
onClick={this.props.onAddStop.bind(this)}
|
||||
>
|
||||
Add stop
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
6
src/components/fields/_labelFromFieldName.js
Normal file
6
src/components/fields/_labelFromFieldName.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import capitalize from 'lodash.capitalize'
|
||||
|
||||
export default function labelFromFieldName(fieldName) {
|
||||
let label = fieldName.split('-').slice(1).join(' ')
|
||||
return capitalize(label)
|
||||
}
|
||||
@@ -1,19 +1,16 @@
|
||||
import React from 'react'
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
|
||||
import input from '../../config/input.js'
|
||||
import colors from '../../config/colors.js'
|
||||
import { margins } from '../../config/scales.js'
|
||||
import PropTypes from 'prop-types'
|
||||
import { combiningFilterOps } from '../../libs/filterops.js'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import DocLabel from '../fields/DocLabel'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
||||
import SingleFilterEditor from './SingleFilterEditor'
|
||||
import FilterEditorBlock from './FilterEditorBlock'
|
||||
import Button from '../Button'
|
||||
|
||||
const combiningFilterOps = ['all', 'any', 'none']
|
||||
const setFilterOps = ['in', '!in']
|
||||
const otherFilterOps = Object
|
||||
.keys(GlSpec.filter_operator.values)
|
||||
.filter(op => combiningFilterOps.indexOf(op) < 0)
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
import AddIcon from 'react-icons/lib/fa/plus'
|
||||
|
||||
function hasCombiningFilter(filter) {
|
||||
return combiningFilterOps.indexOf(filter[0]) >= 0
|
||||
@@ -27,131 +24,24 @@ function hasNestedCombiningFilter(filter) {
|
||||
return false
|
||||
}
|
||||
|
||||
class CombiningOperatorSelect extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const options = combiningFilterOps.map(op => {
|
||||
return <option key={op} value={op}>{op}</option>
|
||||
})
|
||||
|
||||
return <div
|
||||
style={{
|
||||
marginTop: margins[1],
|
||||
marginBottom: margins[1],
|
||||
}}
|
||||
>
|
||||
<select
|
||||
style={{
|
||||
...input.select,
|
||||
width: '20.5%',
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
<label style={{
|
||||
...input.label,
|
||||
width: '60%',
|
||||
marginLeft: margins[0],
|
||||
}}>
|
||||
of the filters matches
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
class OperatorSelect extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <SelectInput
|
||||
style={{
|
||||
width: '15%',
|
||||
margin: margins[0]
|
||||
}}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={otherFilterOps.map(op => [op, op])}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
class SingleFilterEditor extends React.Component {
|
||||
static propTypes = {
|
||||
filter: React.PropTypes.array.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
properties: React.PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
properties: {},
|
||||
}
|
||||
|
||||
onFilterPartChanged(filterOp, propertyName, filterArgs) {
|
||||
const newFilter = [filterOp, propertyName, ...filterArgs]
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
render() {
|
||||
const f = this.props.filter
|
||||
const filterOp = f[0]
|
||||
const propertyName = f[1]
|
||||
const filterArgs = f.slice(2)
|
||||
|
||||
return <div style={{
|
||||
marginTop: margins[1],
|
||||
marginBottom: margins[1],
|
||||
}}>
|
||||
<AutocompleteInput
|
||||
wrapperStyle={{
|
||||
width: '31%',
|
||||
marginRight: margins[0]
|
||||
}}
|
||||
value={propertyName}
|
||||
options={Object.keys(this.props.properties).map(propName => [propName, propName])}
|
||||
onChange={newPropertyName => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)}
|
||||
/>
|
||||
<OperatorSelect
|
||||
value={filterOp}
|
||||
onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
|
||||
/>
|
||||
<StringInput
|
||||
style={{
|
||||
width: '50%',
|
||||
marginLeft: margins[0]
|
||||
}}
|
||||
value={filterArgs.join(',')}
|
||||
onChange={ v=> this.onFilterPartChanged(filterOp, propertyName, v.split(','))}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class CombiningFilterEditor extends React.Component {
|
||||
static propTypes = {
|
||||
/** Properties of the vector layer and the available fields */
|
||||
properties: React.PropTypes.object.isRequired,
|
||||
filter: React.PropTypes.array.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
properties: PropTypes.object,
|
||||
filter: PropTypes.array,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
// Convert filter to combining filter
|
||||
combiningFilter() {
|
||||
let combiningOp = this.props.filter[0]
|
||||
let filters = this.props.filter.slice(1)
|
||||
let filter = this.props.filter || ['all']
|
||||
|
||||
let combiningOp = filter[0]
|
||||
let filters = filter.slice(1)
|
||||
|
||||
if(combiningFilterOps.indexOf(combiningOp) < 0) {
|
||||
combiningOp = 'all'
|
||||
filters = [this.props.filter.slice(0)]
|
||||
filters = [filter.slice(0)]
|
||||
}
|
||||
|
||||
return [combiningOp, ...filters]
|
||||
@@ -163,31 +53,62 @@ export default class CombiningFilterEditor extends React.Component {
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
deleteFilterItem(filterIdx) {
|
||||
const newFilter = this.combiningFilter().slice(0)
|
||||
console.log('Delete', filterIdx, newFilter)
|
||||
newFilter.splice(filterIdx + 1, 1)
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
addFilterItem() {
|
||||
const newFilterItem = this.combiningFilter().slice(0)
|
||||
newFilterItem.push(['==', 'name', ''])
|
||||
this.props.onChange(newFilterItem)
|
||||
}
|
||||
|
||||
render() {
|
||||
const filter = this.combiningFilter()
|
||||
let combiningOp = filter[0]
|
||||
let filters = filter.slice(1)
|
||||
|
||||
const filterEditors = filters.map((f, idx) => {
|
||||
return <SingleFilterEditor
|
||||
key={idx}
|
||||
properties={this.props.properties}
|
||||
filter={f}
|
||||
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
||||
/>
|
||||
const editorBlocks = filters.map((f, idx) => {
|
||||
return <FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
|
||||
<SingleFilterEditor
|
||||
properties={this.props.properties}
|
||||
filter={f}
|
||||
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
||||
/>
|
||||
</FilterEditorBlock>
|
||||
})
|
||||
|
||||
//TODO: Implement support for nested filter
|
||||
if(hasNestedCombiningFilter(filter)) {
|
||||
return null
|
||||
return <div className="maputnik-filter-editor-unsupported">
|
||||
Nested filters are not supported.
|
||||
</div>
|
||||
}
|
||||
|
||||
return <div>
|
||||
<CombiningOperatorSelect
|
||||
value={combiningOp}
|
||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
||||
/>
|
||||
{filterEditors}
|
||||
return <div className="maputnik-filter-editor">
|
||||
<div className="maputnik-filter-editor-compound-select" data-wd-key="layer-filter">
|
||||
<DocLabel
|
||||
label={"Compound Filter"}
|
||||
doc={styleSpec.latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."}
|
||||
/>
|
||||
<SelectInput
|
||||
value={combiningOp}
|
||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
||||
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
||||
/>
|
||||
</div>
|
||||
{editorBlocks}
|
||||
<div className="maputnik-filter-editor-add-wrapper">
|
||||
<Button
|
||||
data-wd-key="layer-filter-button"
|
||||
className="maputnik-add-filter"
|
||||
onClick={this.addFilterItem.bind(this)}>
|
||||
Add filter
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
29
src/components/filter/FilterEditorBlock.jsx
Normal file
29
src/components/filter/FilterEditorBlock.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Button from '../Button'
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
|
||||
class FilterEditorBlock extends React.Component {
|
||||
static propTypes = {
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-filter-editor-block">
|
||||
<div className="maputnik-filter-editor-block-action">
|
||||
<Button
|
||||
className="maputnik-delete-filter"
|
||||
onClick={this.props.onDelete}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="maputnik-filter-editor-block-content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default FilterEditorBlock
|
||||
93
src/components/filter/SingleFilterEditor.jsx
Normal file
93
src/components/filter/SingleFilterEditor.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { otherFilterOps } from '../../libs/filterops.js'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
|
||||
function tryParseInt(v) {
|
||||
if (v === '') return v
|
||||
if (isNaN(v)) return v
|
||||
return parseFloat(v)
|
||||
}
|
||||
|
||||
function tryParseBool(v) {
|
||||
const isString = (typeof(v) === "string");
|
||||
if(!isString) {
|
||||
return v;
|
||||
}
|
||||
|
||||
if(v.match(/^\s*true\s*$/)) {
|
||||
return true;
|
||||
}
|
||||
else if(v.match(/^\s*false\s*$/)) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
function parseFilter(v) {
|
||||
v = tryParseInt(v);
|
||||
v = tryParseBool(v);
|
||||
return v;
|
||||
}
|
||||
|
||||
class SingleFilterEditor extends React.Component {
|
||||
static propTypes = {
|
||||
filter: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
properties: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
properties: {},
|
||||
}
|
||||
|
||||
onFilterPartChanged(filterOp, propertyName, filterArgs) {
|
||||
let newFilter = [filterOp, propertyName, ...filterArgs.map(parseFilter)]
|
||||
if(filterOp === 'has' || filterOp === '!has') {
|
||||
newFilter = [filterOp, propertyName]
|
||||
} else if(filterArgs.length === 0) {
|
||||
newFilter = [filterOp, propertyName, '']
|
||||
}
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
render() {
|
||||
const f = this.props.filter
|
||||
const filterOp = f[0]
|
||||
const propertyName = f[1]
|
||||
const filterArgs = f.slice(2)
|
||||
|
||||
return <div className="maputnik-filter-editor-single">
|
||||
<div className="maputnik-filter-editor-property">
|
||||
<AutocompleteInput
|
||||
value={propertyName}
|
||||
options={Object.keys(this.props.properties).map(propName => [propName, propName])}
|
||||
onChange={newPropertyName => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)}
|
||||
/>
|
||||
</div>
|
||||
<div className="maputnik-filter-editor-operator">
|
||||
<SelectInput
|
||||
value={filterOp}
|
||||
onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
|
||||
options={otherFilterOps}
|
||||
/>
|
||||
</div>
|
||||
{filterArgs.length > 0 &&
|
||||
<div className="maputnik-filter-editor-args">
|
||||
<StringInput
|
||||
value={filterArgs.join(',')}
|
||||
onChange={ v=> this.onFilterPartChanged(filterOp, propertyName, v.split(','))}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default SingleFilterEditor
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import LineIcon from './LineIcon.jsx'
|
||||
import FillIcon from './FillIcon.jsx'
|
||||
@@ -8,20 +9,22 @@ import CircleIcon from './CircleIcon.jsx'
|
||||
|
||||
class LayerIcon extends React.Component {
|
||||
static propTypes = {
|
||||
type: React.PropTypes.string.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
type: PropTypes.string.isRequired,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const iconProps = { style: this.props.style }
|
||||
switch(this.props.type) {
|
||||
case 'fill-extrusion': return <BackgroundIcon {...iconProps} />
|
||||
case 'raster': return <FillIcon {...iconProps} />
|
||||
case 'hillshade': return <FillIcon {...iconProps} />
|
||||
case 'heatmap': return <FillIcon {...iconProps} />
|
||||
case 'fill': return <FillIcon {...iconProps} />
|
||||
case 'background': return <BackgroundIcon {...iconProps} />
|
||||
case 'line': return <LineIcon {...iconProps} />
|
||||
case 'symbol': return <SymbolIcon {...iconProps} />
|
||||
case 'circle': return <CircleIcon {...iconProps} />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export default class FillIcon extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<path id="path8" d="M 12.34,1.29 C 12.5114,1.1076 12.7497,1.0029 13,1 13.5523,1 14,1.4477 14,2 14.0047,2.2478 13.907,2.4866 13.73,2.66 9.785626,6.5516986 6.6148407,9.7551593 2.65,13.72 2.4793,13.8963 2.2453,13.9971 2,14 1.4477,14 1,13.5523 1,13 0.9953,12.7522 1.093,12.5134 1.27,12.34 4.9761967,8.7018093 9.0356422,4.5930579 12.34,1.29 Z" transform="translate(2,2)" />
|
||||
<path d="M 12.34,1.29 C 12.5114,1.1076 12.7497,1.0029 13,1 13.5523,1 14,1.4477 14,2 14.0047,2.2478 13.907,2.4866 13.73,2.66 9.785626,6.5516986 6.6148407,9.7551593 2.65,13.72 2.4793,13.8963 2.2453,13.9971 2,14 1.4477,14 1,13.5523 1,13 0.9953,12.7522 1.093,12.5134 1.27,12.34 4.9761967,8.7018093 9.0356422,4.5930579 12.34,1.29 Z" transform="translate(2,2)" />
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ export default class SymbolIcon extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<g id="svg_1" transform="matrix(1.2718518,0,0,1.2601269,16.559526,-7.4065264)">
|
||||
<path id="svg_2" d="m -9.7959773,11.060163 c -0.3734787,-0.724437 -0.3580577,-1.2147051 -0.00547,-1.8767873 l 8.6034029,-0.019416 c 0.39670292,0.6865644 0.38365934,1.4750693 -0.011097,1.8864953 l -3.1359613,-0.0033 -0.013695,7.1305 c -0.4055357,0.397083 -1.3146432,0.397083 -1.7769191,-0.02274 l 0.030226,-7.104422 z" />
|
||||
<g transform="matrix(1.2718518,0,0,1.2601269,16.559526,-7.4065264)">
|
||||
<path d="m -9.7959773,11.060163 c -0.3734787,-0.724437 -0.3580577,-1.2147051 -0.00547,-1.8767873 l 8.6034029,-0.019416 c 0.39670292,0.6865644 0.38365934,1.4750693 -0.011097,1.8864953 l -3.1359613,-0.0033 -0.013695,7.1305 c -0.4055357,0.397083 -1.3146432,0.397083 -1.7769191,-0.02274 l 0.030226,-7.104422 z" />
|
||||
</g>
|
||||
</IconBase>
|
||||
)
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
import PropTypes from 'prop-types'
|
||||
import StringInput from './StringInput'
|
||||
import NumberInput from './NumberInput'
|
||||
|
||||
import { margins } from '../../config/scales.js'
|
||||
|
||||
class ArrayInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.array,
|
||||
type: React.PropTypes.string,
|
||||
length: React.PropTypes.number,
|
||||
default: React.PropTypes.array,
|
||||
style: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func,
|
||||
value: PropTypes.array,
|
||||
type: PropTypes.string,
|
||||
length: PropTypes.number,
|
||||
default: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
changeValue(idx, newValue) {
|
||||
@@ -27,29 +24,23 @@ class ArrayInput extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const commonStyle = {
|
||||
width: '49%',
|
||||
marginRight: '1%',
|
||||
}
|
||||
const inputs = this.values.map((v, i) => {
|
||||
if(this.props.type === 'number') {
|
||||
return <NumberInput
|
||||
key={i}
|
||||
value={v}
|
||||
style={commonStyle}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
/>
|
||||
} else {
|
||||
return <StringInput
|
||||
key={i}
|
||||
value={v}
|
||||
style={commonStyle}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
/>
|
||||
}
|
||||
})
|
||||
|
||||
return <div style={{display: 'inline-block', width: '50%'}}>
|
||||
return <div className="maputnik-array">
|
||||
{inputs}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import Autocomplete from 'react-autocomplete'
|
||||
import input from '../../config/input'
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
|
||||
|
||||
const MAX_HEIGHT = 140;
|
||||
|
||||
class AutocompleteInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
options: React.PropTypes.array,
|
||||
wrapperStyle: React.PropTypes.object,
|
||||
inputStyle: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
options: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
keepMenuWithinWindowBounds: PropTypes.bool
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
@@ -18,50 +19,73 @@ class AutocompleteInput extends React.Component {
|
||||
options: [],
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
maxHeight: MAX_HEIGHT
|
||||
};
|
||||
}
|
||||
|
||||
calcMaxHeight() {
|
||||
if(this.props.keepMenuWithinWindowBounds) {
|
||||
const maxHeight = window.innerHeight - this.autocompleteMenuEl.getBoundingClientRect().top;
|
||||
const limitedMaxHeight = Math.min(maxHeight, MAX_HEIGHT);
|
||||
|
||||
if(limitedMaxHeight != this.state.maxHeight) {
|
||||
this.setState({
|
||||
maxHeight: limitedMaxHeight
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
this.calcMaxHeight();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.calcMaxHeight();
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Autocomplete
|
||||
menuStyle={{
|
||||
border: 'none',
|
||||
padding: '2px 0',
|
||||
position: 'fixed',
|
||||
overflow: 'auto',
|
||||
maxHeight: '50%',
|
||||
background: colors.gray,
|
||||
zIndex: 3,
|
||||
return <div
|
||||
ref={(el) => {
|
||||
this.autocompleteMenuEl = el;
|
||||
}}
|
||||
wrapperStyle={{
|
||||
display: 'inline-block',
|
||||
...this.props.wrapperStyle
|
||||
}}
|
||||
inputProps={{
|
||||
style: {
|
||||
...input.input,
|
||||
width: '100%',
|
||||
...this.props.inputStyle,
|
||||
}
|
||||
}}
|
||||
value={this.props.value}
|
||||
items={this.props.options}
|
||||
getItemValue={(item) => item[0]}
|
||||
onSelect={v => this.props.onChange(v)}
|
||||
onChange={(e, v) => this.props.onChange(v)}
|
||||
renderItem={(item, isHighlighted) => (
|
||||
<div
|
||||
key={item[0]}
|
||||
style={{
|
||||
userSelect: 'none',
|
||||
color: colors.lowgray,
|
||||
cursor: 'default',
|
||||
background: isHighlighted ? colors.midgray : colors.gray,
|
||||
padding: margins[0],
|
||||
fontSize: fontSizes[5],
|
||||
zIndex: 3,
|
||||
}}
|
||||
>
|
||||
{item[1]}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
>
|
||||
<Autocomplete
|
||||
menuStyle={{
|
||||
position: "fixed",
|
||||
overflow: "auto",
|
||||
maxHeight: this.state.maxHeight
|
||||
}}
|
||||
wrapperProps={{
|
||||
className: "maputnik-autocomplete",
|
||||
style: null
|
||||
}}
|
||||
inputProps={{
|
||||
className: "maputnik-string"
|
||||
}}
|
||||
value={this.props.value}
|
||||
items={this.props.options}
|
||||
getItemValue={(item) => item[0]}
|
||||
onSelect={v => this.props.onChange(v)}
|
||||
onChange={(e, v) => this.props.onChange(v)}
|
||||
shouldItemRender={(item, value) => {
|
||||
return item[0].toLowerCase().indexOf(value.toLowerCase()) > -1
|
||||
}}
|
||||
renderItem={(item, isHighlighted) => (
|
||||
<div
|
||||
key={item[0]}
|
||||
className={classnames({
|
||||
"maputnik-autocomplete-menu-item": true,
|
||||
"maputnik-autocomplete-menu-item-selected": isHighlighted,
|
||||
})}
|
||||
>
|
||||
{item[1]}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,70 +1,30 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
import colors from '../../config/colors'
|
||||
import { margins } from '../../config/scales'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class CheckboxInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func,
|
||||
value: PropTypes.bool.isRequired,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
const styles = {
|
||||
root: {
|
||||
...input.base,
|
||||
padding: 0,
|
||||
position: 'relative',
|
||||
textAlign: 'center ',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
input: {
|
||||
position: 'absolute',
|
||||
zIndex: -1,
|
||||
opacity: 0
|
||||
},
|
||||
box: {
|
||||
display: 'inline-block',
|
||||
textAlign: 'center ',
|
||||
height: 15,
|
||||
width: 15,
|
||||
marginRight: margins[1],
|
||||
marginBottom: null,
|
||||
backgroundColor: colors.gray,
|
||||
borderRadius: 2,
|
||||
borderStyle: 'solid',
|
||||
borderWidth: 2,
|
||||
borderColor: colors.gray,
|
||||
transition: 'background-color .1s ease-out'
|
||||
},
|
||||
icon: {
|
||||
display: this.props.value ? null : 'none',
|
||||
width: '75%',
|
||||
height: '75%',
|
||||
marginTop: 1,
|
||||
fill: colors.lowgray
|
||||
}
|
||||
}
|
||||
|
||||
return <label style={styles.root}>
|
||||
return <label className="maputnik-checkbox-wrapper">
|
||||
<input
|
||||
type="checkbox"
|
||||
style={{
|
||||
...styles.input,
|
||||
...this.props.style,
|
||||
}}
|
||||
onChange={e => {this.props.onChange(!this.props.value)}}
|
||||
checked={this.props.value}
|
||||
/>
|
||||
<div style={styles.box}>
|
||||
<svg
|
||||
viewBox='0 0 32 32'
|
||||
style={styles.icon}>
|
||||
className="maputnik-checkbox"
|
||||
type="checkbox"
|
||||
style={this.props.style}
|
||||
onChange={e => this.props.onChange(!this.props.value)}
|
||||
checked={this.props.value}
|
||||
/>
|
||||
<div className="maputnik-checkbox-box">
|
||||
<svg style={{
|
||||
display: this.props.value ? 'inline' : 'none'
|
||||
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
|
||||
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
|
||||
</svg>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
106
src/components/inputs/DynamicArrayInput.jsx
Normal file
106
src/components/inputs/DynamicArrayInput.jsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import StringInput from './StringInput'
|
||||
import NumberInput from './NumberInput'
|
||||
import Button from '../Button'
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
import DocLabel from '../fields/DocLabel'
|
||||
|
||||
|
||||
class DynamicArrayInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.array,
|
||||
type: PropTypes.string,
|
||||
default: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
changeValue(idx, newValue) {
|
||||
console.log(idx, newValue)
|
||||
const values = this.values.slice(0)
|
||||
values[idx] = newValue
|
||||
this.props.onChange(values)
|
||||
}
|
||||
|
||||
get values() {
|
||||
return this.props.value || this.props.default || []
|
||||
}
|
||||
|
||||
addValue() {
|
||||
const values = this.values.slice(0)
|
||||
if (this.props.type === 'number') {
|
||||
values.push(0)
|
||||
} else {
|
||||
values.push("")
|
||||
}
|
||||
|
||||
|
||||
this.props.onChange(values)
|
||||
}
|
||||
|
||||
deleteValue(valueIdx) {
|
||||
const values = this.values.slice(0)
|
||||
values.splice(valueIdx, 1)
|
||||
|
||||
this.props.onChange(values)
|
||||
}
|
||||
|
||||
render() {
|
||||
const inputs = this.values.map((v, i) => {
|
||||
const deleteValueBtn= <DeleteValueButton onClick={this.deleteValue.bind(this, i)} />
|
||||
const input = this.props.type === 'number'
|
||||
? <NumberInput
|
||||
value={v}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
/>
|
||||
: <StringInput
|
||||
value={v}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
/>
|
||||
|
||||
return <div
|
||||
style={this.props.style}
|
||||
key={i}
|
||||
className="maputnik-array-block"
|
||||
>
|
||||
<div className="maputnik-array-block-action">
|
||||
{deleteValueBtn}
|
||||
</div>
|
||||
<div className="maputnik-array-block-content">
|
||||
{input}
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
|
||||
return <div className="maputnik-array">
|
||||
{inputs}
|
||||
<Button
|
||||
className="maputnik-array-add-value"
|
||||
onClick={this.addValue.bind(this)}
|
||||
>
|
||||
Add value
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteValueButton extends React.Component {
|
||||
static propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Button
|
||||
className="maputnik-delete-stop"
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
<DocLabel
|
||||
label={<DeleteIcon />}
|
||||
doc={"Remove array entry."}
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
}
|
||||
|
||||
export default DynamicArrayInput
|
||||
@@ -1,15 +1,18 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import AutocompleteInput from './AutocompleteInput'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
//TODO: Query available font stack dynamically
|
||||
import fontStacks from '../../config/fontstacks.json'
|
||||
|
||||
class FontInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.array.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
value: PropTypes.array.isRequired,
|
||||
default: PropTypes.array,
|
||||
fonts: PropTypes.array,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
fonts: []
|
||||
}
|
||||
|
||||
get values() {
|
||||
@@ -27,12 +30,12 @@ class FontInput extends React.Component {
|
||||
return <AutocompleteInput
|
||||
key={i}
|
||||
value={value}
|
||||
options={fontStacks.map(f => [f, f])}
|
||||
options={this.props.fonts.map(f => [f, f])}
|
||||
onChange={this.changeFont.bind(this, i)}
|
||||
/>
|
||||
})
|
||||
|
||||
return <div style={{display: 'inline-block'}}>
|
||||
return <div className="maputnik-font">
|
||||
{inputs}
|
||||
</div>
|
||||
}
|
||||
|
||||
28
src/components/inputs/IconInput.jsx
Normal file
28
src/components/inputs/IconInput.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import AutocompleteInput from './AutocompleteInput'
|
||||
|
||||
|
||||
class IconInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.array,
|
||||
icons: PropTypes.array,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
icons: []
|
||||
}
|
||||
|
||||
render() {
|
||||
return <AutocompleteInput
|
||||
value={this.props.value}
|
||||
options={this.props.icons.map(f => [f, f])}
|
||||
onChange={this.props.onChange}
|
||||
wrapperStyle={this.props.style}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export default IconInput
|
||||
@@ -1,13 +1,21 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input'
|
||||
import { margins } from '../../config/scales'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import DocLabel from '../fields/DocLabel'
|
||||
|
||||
/** Wrap a component with a label */
|
||||
class InputBlock extends React.Component {
|
||||
static propTypes = {
|
||||
label: React.PropTypes.string.isRequired,
|
||||
children: React.PropTypes.element.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
"data-wd-key": PropTypes.string,
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.element,
|
||||
]).isRequired,
|
||||
doc: PropTypes.string,
|
||||
action: PropTypes.element,
|
||||
children: PropTypes.node.isRequired,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
@@ -15,30 +23,35 @@ class InputBlock extends React.Component {
|
||||
return this.props.onChange(value === "" ? null: value)
|
||||
}
|
||||
|
||||
renderChildren() {
|
||||
return React.Children.map(this.props.children, child => {
|
||||
return React.cloneElement(child, {
|
||||
style: {
|
||||
...child.props.style,
|
||||
width: '50%',
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
...input.property,
|
||||
...this.props.style,
|
||||
}}>
|
||||
<label
|
||||
style={{
|
||||
...input.label,
|
||||
width: '50%',
|
||||
}}>
|
||||
return <div style={this.props.style}
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
className={classnames({
|
||||
"maputnik-input-block": true,
|
||||
"maputnik-action-block": this.props.action
|
||||
})}
|
||||
>
|
||||
{this.props.doc &&
|
||||
<div className="maputnik-input-block-label">
|
||||
<DocLabel
|
||||
label={this.props.label}
|
||||
doc={this.props.doc}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{!this.props.doc &&
|
||||
<label className="maputnik-input-block-label">
|
||||
{this.props.label}
|
||||
</label>
|
||||
{this.renderChildren()}
|
||||
}
|
||||
{this.props.action &&
|
||||
<div className="maputnik-input-block-action">
|
||||
{this.props.action}
|
||||
</div>
|
||||
}
|
||||
<div className="maputnik-input-block-content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import Button from '../Button'
|
||||
|
||||
import colors from '../../config/colors'
|
||||
import { margins } from '../../config/scales'
|
||||
import input from '../../config/input.js'
|
||||
|
||||
class MultiButtonInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
options: React.PropTypes.array.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
options: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const selectedValue = this.props.value || this.props.options[0][0]
|
||||
const buttons = this.props.options.map(([val, label])=> {
|
||||
let options = this.props.options
|
||||
if(options.length > 0 && !Array.isArray(options[0])) {
|
||||
options = options.map(v => [v, v])
|
||||
}
|
||||
|
||||
const selectedValue = this.props.value || options[0][0]
|
||||
const buttons = options.map(([val, label])=> {
|
||||
return <Button
|
||||
key={val}
|
||||
style={{
|
||||
marginRight: margins[0],
|
||||
backgroundColor: val === selectedValue ? colors.midgray : colors.gray,
|
||||
}}
|
||||
onClick={e => this.props.onChange(val)}
|
||||
className={classnames({"maputnik-button-selected": val === selectedValue})}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
})
|
||||
|
||||
return <div style={{display: 'inline-block'}}>
|
||||
return <div className="maputnik-multibutton">
|
||||
{buttons}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class NumberInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.number,
|
||||
style: React.PropTypes.object,
|
||||
default: React.PropTypes.number,
|
||||
min: React.PropTypes.number,
|
||||
max: React.PropTypes.number,
|
||||
onChange: React.PropTypes.func,
|
||||
value: PropTypes.number,
|
||||
default: PropTypes.number,
|
||||
min: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -68,10 +67,7 @@ class NumberInput extends React.Component {
|
||||
|
||||
render() {
|
||||
return <input
|
||||
style={{
|
||||
...input.input,
|
||||
...this.props.style
|
||||
}}
|
||||
className="maputnik-number"
|
||||
placeholder={this.props.default}
|
||||
value={this.state.value}
|
||||
onChange={e => this.changeValue(e.target.value)}
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class SelectInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
options: React.PropTypes.array.isRequired,
|
||||
style: React.PropTypes.object,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
"data-wd-key": PropTypes.string.isRequired,
|
||||
options: PropTypes.array.isRequired,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const options = this.props.options.map(([val, label])=> {
|
||||
return <option key={val} value={val}>{label}</option>
|
||||
})
|
||||
let options = this.props.options
|
||||
if(options.length > 0 && !Array.isArray(options[0])) {
|
||||
options = options.map(v => [v, v])
|
||||
}
|
||||
|
||||
return <select
|
||||
style={{
|
||||
...input.select,
|
||||
...this.props.style
|
||||
}}
|
||||
className="maputnik-select"
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
style={this.props.style}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
>
|
||||
{options}
|
||||
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
|
||||
</select>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,60 @@
|
||||
import React from 'react'
|
||||
import input from '../../config/input.js'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class StringInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
default: React.PropTypes.number,
|
||||
onChange: React.PropTypes.func,
|
||||
"data-wd-key": PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
default: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
multi: PropTypes.bool,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
value: props.value || ''
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setState({ value: nextProps.value || '' })
|
||||
}
|
||||
|
||||
render() {
|
||||
return <input
|
||||
style={{
|
||||
...input.input,
|
||||
...this.props.style
|
||||
}}
|
||||
value={this.props.value}
|
||||
placeholder={this.props.default}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
/>
|
||||
let tag;
|
||||
let classes;
|
||||
|
||||
if(!!this.props.multi) {
|
||||
tag = "textarea"
|
||||
classes = [
|
||||
"maputnik-string",
|
||||
"maputnik-string--multi"
|
||||
]
|
||||
}
|
||||
else {
|
||||
tag = "input"
|
||||
classes = [
|
||||
"maputnik-string"
|
||||
]
|
||||
}
|
||||
|
||||
return React.createElement(tag, {
|
||||
"data-wd-key": this.props["data-wd-key"],
|
||||
className: classes.join(" "),
|
||||
style: this.props.style,
|
||||
value: this.state.value,
|
||||
placeholder: this.props.default,
|
||||
onChange: e => {
|
||||
this.setState({
|
||||
value: e.target.value
|
||||
})
|
||||
},
|
||||
onBlur: () => {
|
||||
if(this.state.value!==this.props.value) this.props.onChange(this.state.value)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
21
src/components/layers/Collapser.jsx
Normal file
21
src/components/layers/Collapser.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import CollapseOpenIcon from 'react-icons/lib/md/arrow-drop-down'
|
||||
import CollapseCloseIcon from 'react-icons/lib/md/arrow-drop-up'
|
||||
|
||||
export default class Collapser extends React.Component {
|
||||
static propTypes = {
|
||||
isCollapsed: PropTypes.bool.isRequired,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const iconStyle = {
|
||||
width: 20,
|
||||
height: 20,
|
||||
...this.props.style,
|
||||
}
|
||||
return this.props.isCollapsed ? <CollapseCloseIcon style={iconStyle}/> : <CollapseOpenIcon style={iconStyle} />
|
||||
}
|
||||
}
|
||||
|
||||
29
src/components/layers/CommentBlock.jsx
Normal file
29
src/components/layers/CommentBlock.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
|
||||
class MetadataBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock
|
||||
label={"Comments"}
|
||||
doc={"Comments for the current layer. This is non-standard and not in the spec."}
|
||||
data-wd-key="layer-comment"
|
||||
>
|
||||
<StringInput
|
||||
multi={true}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
default="Comment..."
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default MetadataBlock
|
||||
@@ -1,22 +1,25 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import CodeMirror from 'react-codemirror'
|
||||
import {Controlled as CodeMirror} from 'react-codemirror2'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
|
||||
import colors from '../../config/colors'
|
||||
import { margins } from '../../config/scales'
|
||||
|
||||
import 'codemirror/mode/javascript/javascript'
|
||||
import 'codemirror/addon/lint/lint'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/addon/lint/lint.css'
|
||||
import '../../codemirror-maputnik.css'
|
||||
import jsonlint from 'jsonlint'
|
||||
|
||||
// This is mainly because of this issue <https://github.com/zaach/jsonlint/issues/57> also the API has changed, see comment in file
|
||||
import '../../vendor/codemirror/addon/lint/json-lint'
|
||||
|
||||
|
||||
class JSONEditor extends React.Component {
|
||||
static propTypes = {
|
||||
layer: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
layer: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -69,13 +72,15 @@ class JSONEditor extends React.Component {
|
||||
tabSize: 2,
|
||||
theme: 'maputnik',
|
||||
viewportMargin: Infinity,
|
||||
lineNumbers: false,
|
||||
lineNumbers: true,
|
||||
lint: true,
|
||||
gutters: ["CodeMirror-lint-markers"],
|
||||
scrollbarStyle: "null",
|
||||
}
|
||||
|
||||
return <CodeMirror
|
||||
value={this.state.code}
|
||||
onChange={this.onCodeUpdate.bind(this)}
|
||||
onBeforeChange={(editor, data, value) => this.onCodeUpdate(value)}
|
||||
onFocusChange={focused => focused ? true : this.resetValue()}
|
||||
options={codeMirrorOptions}
|
||||
/>
|
||||
|
||||
@@ -1,38 +1,50 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
||||
import JSONEditor from './JSONEditor'
|
||||
import FilterEditor from '../filter/FilterEditor'
|
||||
import PropertyGroup from '../fields/PropertyGroup'
|
||||
import LayerEditorGroup from './LayerEditorGroup'
|
||||
import LayerTypeBlock from './LayerTypeBlock'
|
||||
import LayerIdBlock from './LayerIdBlock'
|
||||
import MinZoomBlock from './MinZoomBlock'
|
||||
import MaxZoomBlock from './MaxZoomBlock'
|
||||
import CommentBlock from './CommentBlock'
|
||||
import LayerSourceBlock from './LayerSourceBlock'
|
||||
import LayerSourceLayerBlock from './LayerSourceLayerBlock'
|
||||
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import MultiButtonInput from '../inputs/MultiButtonInput'
|
||||
|
||||
import input from '../../config/input.js'
|
||||
import { changeType, changeProperty } from '../../libs/layer'
|
||||
import layout from '../../config/layout.json'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
import colors from '../../config/colors'
|
||||
|
||||
class UnsupportedLayer extends React.Component {
|
||||
render() {
|
||||
return <div></div>
|
||||
|
||||
function layoutGroups(layerType) {
|
||||
const layerGroup = {
|
||||
title: 'Layer',
|
||||
type: 'layer'
|
||||
}
|
||||
const filterGroup = {
|
||||
title: 'Filter',
|
||||
type: 'filter'
|
||||
}
|
||||
const editorGroup = {
|
||||
title: 'JSON Editor',
|
||||
type: 'jsoneditor'
|
||||
}
|
||||
return [layerGroup, filterGroup].concat(layout[layerType].groups).concat([editorGroup])
|
||||
}
|
||||
|
||||
/** Layer editor supporting multiple types of layers. */
|
||||
export default class LayerEditor extends React.Component {
|
||||
static propTypes = {
|
||||
layer: React.PropTypes.object.isRequired,
|
||||
sources: React.PropTypes.object,
|
||||
vectorLayers: React.PropTypes.object,
|
||||
onLayerChanged: React.PropTypes.func,
|
||||
onLayerIdChange: React.PropTypes.func,
|
||||
layer: PropTypes.object.isRequired,
|
||||
sources: PropTypes.object,
|
||||
vectorLayers: PropTypes.object,
|
||||
spec: PropTypes.object.isRequired,
|
||||
onLayerChanged: PropTypes.func,
|
||||
onLayerIdChange: PropTypes.func,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
@@ -42,7 +54,7 @@ export default class LayerEditor extends React.Component {
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
reactIconBase: React.PropTypes.object
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -50,7 +62,7 @@ export default class LayerEditor extends React.Component {
|
||||
|
||||
//TODO: Clean this up and refactor into function
|
||||
const editorGroups = {}
|
||||
layout[this.props.layer.type].groups.forEach(group => {
|
||||
layoutGroups(this.props.layer.type).forEach(group => {
|
||||
editorGroups[group.title] = true
|
||||
})
|
||||
|
||||
@@ -74,8 +86,8 @@ export default class LayerEditor extends React.Component {
|
||||
getChildContext () {
|
||||
return {
|
||||
reactIconBase: {
|
||||
size: fontSizes[4],
|
||||
color: colors.lowgray,
|
||||
size: 14,
|
||||
color: '#8e8e8e',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,57 +107,82 @@ export default class LayerEditor extends React.Component {
|
||||
}
|
||||
|
||||
renderGroupType(type, fields) {
|
||||
let comment = ""
|
||||
if(this.props.layer.metadata) {
|
||||
comment = this.props.layer.metadata['maputnik:comment']
|
||||
}
|
||||
|
||||
let sourceLayerIds;
|
||||
if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
|
||||
sourceLayerIds = this.props.sources[this.props.layer.source].layers;
|
||||
}
|
||||
|
||||
switch(type) {
|
||||
case 'settings': return <div>
|
||||
<LayerIdBlock
|
||||
value={this.props.layer.id}
|
||||
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
|
||||
/>
|
||||
<LayerTypeBlock
|
||||
value={this.props.layer.type}
|
||||
onChange={newType => this.props.onLayerChanged(changeType(this.props.layer, newType))}
|
||||
/>
|
||||
</div>
|
||||
case 'source': return <div>
|
||||
<LayerSourceBlock
|
||||
case 'layer': return <div>
|
||||
<LayerIdBlock
|
||||
value={this.props.layer.id}
|
||||
wdKey="layer-editor.layer-id"
|
||||
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
|
||||
/>
|
||||
<LayerTypeBlock
|
||||
value={this.props.layer.type}
|
||||
onChange={newType => this.props.onLayerChanged(changeType(this.props.layer, newType))}
|
||||
/>
|
||||
{this.props.layer.type !== 'background' && <LayerSourceBlock
|
||||
sourceIds={Object.keys(this.props.sources)}
|
||||
value={this.props.layer.source}
|
||||
onChange={v => this.changeProperty(null, 'source', v)}
|
||||
/>
|
||||
}
|
||||
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
|
||||
<LayerSourceLayerBlock
|
||||
sourceLayerIds={this.props.sources[this.props.layer.source]}
|
||||
sourceLayerIds={sourceLayerIds}
|
||||
value={this.props.layer['source-layer']}
|
||||
onChange={v => this.changeProperty(null, 'source-layer', v)}
|
||||
/>
|
||||
{this.props.layer.filter &&
|
||||
<div style={input.property}>
|
||||
}
|
||||
<MinZoomBlock
|
||||
value={this.props.layer.minzoom}
|
||||
onChange={v => this.changeProperty(null, 'minzoom', v)}
|
||||
/>
|
||||
<MaxZoomBlock
|
||||
value={this.props.layer.maxzoom}
|
||||
onChange={v => this.changeProperty(null, 'maxzoom', v)}
|
||||
/>
|
||||
<CommentBlock
|
||||
value={comment}
|
||||
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
|
||||
/>
|
||||
</div>
|
||||
case 'filter': return <div>
|
||||
<div className="maputnik-filter-editor-wrapper">
|
||||
<FilterEditor
|
||||
filter={this.props.layer.filter}
|
||||
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
|
||||
onChange={f => this.changeProperty(null, 'filter', f)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
case 'properties': return <PropertyGroup
|
||||
layer={this.props.layer}
|
||||
groupFields={fields}
|
||||
spec={this.props.spec}
|
||||
onChange={this.changeProperty.bind(this)}
|
||||
/>
|
||||
case 'jsoneditor': return <JSONEditor
|
||||
layer={this.props.layer}
|
||||
onChange={this.props.onLayerChanged}
|
||||
/>
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const layerType = this.props.layer.type
|
||||
const layoutGroups = layout[layerType].groups.filter(group => {
|
||||
return !(this.props.layer.type === 'background' && group.type === 'source')
|
||||
const groups = layoutGroups(layerType).filter(group => {
|
||||
return !(layerType === 'background' && group.type === 'source')
|
||||
}).map(group => {
|
||||
return <LayerEditorGroup
|
||||
data-wd-key={group.title}
|
||||
key={group.title}
|
||||
title={group.title}
|
||||
isActive={this.state.editorGroups[group.title]}
|
||||
@@ -155,8 +192,9 @@ export default class LayerEditor extends React.Component {
|
||||
</LayerEditorGroup>
|
||||
})
|
||||
|
||||
return <div>
|
||||
{layoutGroups}
|
||||
return <div className="maputnik-layer-editor"
|
||||
>
|
||||
{groups}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,21 @@
|
||||
import React from 'react'
|
||||
import Color from 'color'
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
|
||||
import PropTypes from 'prop-types'
|
||||
import Collapse from 'react-collapse'
|
||||
import CollapseOpenIcon from 'react-icons/lib/md/arrow-drop-down'
|
||||
import CollapseCloseIcon from 'react-icons/lib/md/arrow-drop-up'
|
||||
|
||||
class Collapser extends React.Component {
|
||||
static propTypes = {
|
||||
isCollapsed: React.PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const iconStyle = {
|
||||
width: 20,
|
||||
height: 20,
|
||||
}
|
||||
return this.props.isCollapsed ? <CollapseCloseIcon style={iconStyle}/> : <CollapseOpenIcon style={iconStyle} />
|
||||
}
|
||||
}
|
||||
import Collapser from './Collapser'
|
||||
|
||||
export default class LayerEditorGroup extends React.Component {
|
||||
static propTypes = {
|
||||
title: React.PropTypes.string.isRequired,
|
||||
isActive: React.PropTypes.bool.isRequired,
|
||||
children: React.PropTypes.element.isRequired,
|
||||
onActiveToggle: React.PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { hover: false }
|
||||
"data-wd-key": PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
onActiveToggle: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<div style={{
|
||||
fontSize: fontSizes[4],
|
||||
backgroundColor: this.state.hover ? Color(colors.black).lighten(0.30).string() : Color(colors.black).lighten(0.15).string(),
|
||||
color: colors.lowgray,
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
padding: margins[1],
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
onMouseOver={e => this.setState({hover: true})}
|
||||
onMouseOut={e => this.setState({hover: false})}
|
||||
<div className="maputnik-layer-editor-group"
|
||||
data-wd-key={"layer-editor-group:"+this.props["data-wd-key"]}
|
||||
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
||||
>
|
||||
<span>{this.props.title}</span>
|
||||
@@ -56,7 +23,9 @@ export default class LayerEditorGroup extends React.Component {
|
||||
<Collapser isCollapsed={this.props.isActive} />
|
||||
</div>
|
||||
<Collapse isOpened={this.props.isActive}>
|
||||
{this.props.children}
|
||||
<div className="react-collapse-container">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
|
||||
class LayerIdBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
wdKey: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Layer ID"}>
|
||||
return <InputBlock label={"ID"} doc={styleSpec.latest.layer.id.doc}
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
<StringInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
|
||||
@@ -1,18 +1,41 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
|
||||
import Button from '../Button'
|
||||
import LayerListGroup from './LayerListGroup'
|
||||
import LayerListItem from './LayerListItem'
|
||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||
import AddModal from '../modals/AddModal'
|
||||
|
||||
import style from '../../libs/style.js'
|
||||
import { margins } from '../../config/scales.js'
|
||||
|
||||
import {SortableContainer, SortableHandle, arrayMove} from 'react-sortable-hoc';
|
||||
|
||||
const layerListPropTypes = {
|
||||
layers: React.PropTypes.array.isRequired,
|
||||
selectedLayerIndex: React.PropTypes.number.isRequired,
|
||||
onLayersChange: React.PropTypes.func.isRequired,
|
||||
onLayerSelect: React.PropTypes.func,
|
||||
layers: PropTypes.array.isRequired,
|
||||
selectedLayerIndex: PropTypes.number.isRequired,
|
||||
onLayersChange: PropTypes.func.isRequired,
|
||||
onLayerSelect: PropTypes.func,
|
||||
sources: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
function layerPrefix(name) {
|
||||
return name.replace(' ', '-').replace('_', '-').split('-')[0]
|
||||
}
|
||||
|
||||
function findClosestCommonPrefix(layers, idx) {
|
||||
const currentLayerPrefix = layerPrefix(layers[idx].id)
|
||||
let closestIdx = idx
|
||||
for (let i = idx; i > 0; i--) {
|
||||
const previousLayerPrefix = layerPrefix(layers[i-1].id)
|
||||
if(previousLayerPrefix === currentLayerPrefix) {
|
||||
closestIdx = i - 1
|
||||
} else {
|
||||
return closestIdx
|
||||
}
|
||||
}
|
||||
return closestIdx
|
||||
}
|
||||
|
||||
// List of collapsible layer editors
|
||||
@@ -23,6 +46,17 @@ class LayerListContainer extends React.Component {
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
collapsedGroups: {},
|
||||
areAllGroupsExpanded: false,
|
||||
isOpen: {
|
||||
add: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onLayerDestroy(layerId) {
|
||||
const remainingLayers = this.props.layers.slice(0)
|
||||
const idx = style.indexOfLayer(remainingLayers, layerId)
|
||||
@@ -53,28 +87,149 @@ class LayerListContainer extends React.Component {
|
||||
this.props.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
render() {
|
||||
const layerPanels = this.props.layers.map((layer, index) => {
|
||||
const layerId = layer.id
|
||||
return <LayerListItem
|
||||
index={index}
|
||||
key={layerId}
|
||||
layerId={layerId}
|
||||
layerType={layer.type}
|
||||
visibility={(layer.layout || {}).visibility}
|
||||
isSelected={index === this.props.selectedLayerIndex}
|
||||
onLayerSelect={this.props.onLayerSelect}
|
||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
||||
/>
|
||||
toggleModal(modalName) {
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
[modalName]: !this.state.isOpen[modalName]
|
||||
}
|
||||
})
|
||||
return <ul style={{
|
||||
padding: 0,
|
||||
margin: 0
|
||||
}}>
|
||||
{layerPanels}
|
||||
</ul>
|
||||
}
|
||||
|
||||
toggleLayers() {
|
||||
let idx=0
|
||||
|
||||
let newGroups=[]
|
||||
|
||||
this.groupedLayers().forEach(layers => {
|
||||
const groupPrefix = layerPrefix(layers[0].id)
|
||||
const lookupKey = [groupPrefix, idx].join('-')
|
||||
|
||||
|
||||
if (layers.length > 1) {
|
||||
newGroups[lookupKey] = this.state.areAllGroupsExpanded
|
||||
}
|
||||
|
||||
layers.forEach((layer) => {
|
||||
idx += 1
|
||||
})
|
||||
});
|
||||
|
||||
this.setState({
|
||||
collapsedGroups: newGroups,
|
||||
areAllGroupsExpanded: !this.state.areAllGroupsExpanded
|
||||
})
|
||||
}
|
||||
|
||||
groupedLayers() {
|
||||
const groups = []
|
||||
for (let i = 0; i < this.props.layers.length; i++) {
|
||||
const previousLayer = this.props.layers[i-1]
|
||||
const layer = this.props.layers[i]
|
||||
if(previousLayer && layerPrefix(previousLayer.id) == layerPrefix(layer.id)) {
|
||||
const lastGroup = groups[groups.length - 1]
|
||||
lastGroup.push(layer)
|
||||
} else {
|
||||
groups.push([layer])
|
||||
}
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
toggleLayerGroup(groupPrefix, idx) {
|
||||
const lookupKey = [groupPrefix, idx].join('-')
|
||||
const newGroups = { ...this.state.collapsedGroups }
|
||||
if(lookupKey in this.state.collapsedGroups) {
|
||||
newGroups[lookupKey] = !this.state.collapsedGroups[lookupKey]
|
||||
} else {
|
||||
newGroups[lookupKey] = false
|
||||
}
|
||||
this.setState({
|
||||
collapsedGroups: newGroups
|
||||
})
|
||||
}
|
||||
|
||||
isCollapsed(groupPrefix, idx) {
|
||||
const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join('-')]
|
||||
return collapsed === undefined ? true : collapsed
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const listItems = []
|
||||
let idx = 0
|
||||
this.groupedLayers().forEach(layers => {
|
||||
const groupPrefix = layerPrefix(layers[0].id)
|
||||
if(layers.length > 1) {
|
||||
const grp = <LayerListGroup
|
||||
data-wd-key={[groupPrefix, idx].join('-')}
|
||||
key={[groupPrefix, idx].join('-')}
|
||||
title={groupPrefix}
|
||||
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
|
||||
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
|
||||
/>
|
||||
listItems.push(grp)
|
||||
}
|
||||
|
||||
layers.forEach((layer, idxInGroup) => {
|
||||
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
|
||||
|
||||
const listItem = <LayerListItem
|
||||
className={classnames({
|
||||
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
|
||||
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1
|
||||
})}
|
||||
index={idx}
|
||||
key={layer.id}
|
||||
layerId={layer.id}
|
||||
layerType={layer.type}
|
||||
visibility={(layer.layout || {}).visibility}
|
||||
isSelected={idx === this.props.selectedLayerIndex}
|
||||
onLayerSelect={this.props.onLayerSelect}
|
||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
||||
/>
|
||||
listItems.push(listItem)
|
||||
idx += 1
|
||||
})
|
||||
})
|
||||
|
||||
return <div className="maputnik-layer-list">
|
||||
<AddModal
|
||||
layers={this.props.layers}
|
||||
sources={this.props.sources}
|
||||
isOpen={this.state.isOpen.add}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'add')}
|
||||
onLayersChange={this.props.onLayersChange}
|
||||
/>
|
||||
<header className="maputnik-layer-list-header">
|
||||
<span className="maputnik-layer-list-header-title">Layers</span>
|
||||
<span className="maputnik-space" />
|
||||
<div className="maputnik-default-property">
|
||||
<div className="maputnik-multibutton">
|
||||
<a
|
||||
onClick={this.toggleLayers.bind(this)}
|
||||
className="maputnik-button">
|
||||
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="maputnik-default-property">
|
||||
<div className="maputnik-multibutton">
|
||||
<a
|
||||
onClick={this.toggleModal.bind(this, 'add')}
|
||||
data-wd-key="layer-list:add-layer"
|
||||
className="maputnik-button maputnik-button-selected">
|
||||
Add Layer
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<ul className="maputnik-layer-list-container">
|
||||
{listItems}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
28
src/components/layers/LayerListGroup.jsx
Normal file
28
src/components/layers/LayerListGroup.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Collapser from './Collapser'
|
||||
|
||||
export default class LayerListGroup extends React.Component {
|
||||
static propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
"data-wd-key": PropTypes.string,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
onActiveToggle: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
return <li className="maputnik-layer-list-group">
|
||||
<div className="maputnik-layer-list-group-header"
|
||||
data-wd-key={"layer-list-group:"+this.props["data-wd-key"]}
|
||||
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
||||
>
|
||||
<span className="maputnik-layer-list-group-title">{this.props.title}</span>
|
||||
<span className="maputnik-space" />
|
||||
<Collapser
|
||||
style={{ height: 14, width: 14 }}
|
||||
isCollapsed={this.props.isActive}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Color from 'color'
|
||||
import classnames from 'classnames'
|
||||
|
||||
import CopyIcon from 'react-icons/lib/md/content-copy'
|
||||
import VisibilityIcon from 'react-icons/lib/md/visibility'
|
||||
@@ -10,10 +12,6 @@ import LayerIcon from '../icons/LayerIcon'
|
||||
import LayerEditor from './LayerEditor'
|
||||
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
||||
|
||||
import colors from '../../config/colors.js'
|
||||
import { fontSizes, margins } from '../../config/scales.js'
|
||||
|
||||
|
||||
@SortableHandle
|
||||
class LayerTypeDragHandle extends React.Component {
|
||||
static propTypes = LayerIcon.propTypes
|
||||
@@ -23,9 +21,9 @@ class LayerTypeDragHandle extends React.Component {
|
||||
{...this.props}
|
||||
style={{
|
||||
cursor: 'move',
|
||||
width: fontSizes[4],
|
||||
height: fontSizes[4],
|
||||
paddingRight: margins[0],
|
||||
width: 14,
|
||||
height: 14,
|
||||
paddingRight: 3,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@@ -33,47 +31,25 @@ class LayerTypeDragHandle extends React.Component {
|
||||
|
||||
class IconAction extends React.Component {
|
||||
static propTypes = {
|
||||
action: React.PropTypes.string.isRequired,
|
||||
active: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { hover: false }
|
||||
action: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
wdKey: PropTypes.string
|
||||
}
|
||||
|
||||
renderIcon() {
|
||||
const iconStyle = {
|
||||
fill: colors.black
|
||||
}
|
||||
|
||||
if(this.props.active) {
|
||||
iconStyle.fill = colors.midgray
|
||||
}
|
||||
if(this.state.hover) {
|
||||
iconStyle.fill = colors.lowgray
|
||||
}
|
||||
|
||||
switch(this.props.action) {
|
||||
case 'copy': return <CopyIcon style={iconStyle} />
|
||||
case 'show': return <VisibilityIcon style={iconStyle} />
|
||||
case 'hide': return <VisibilityOffIcon style={iconStyle} />
|
||||
case 'delete': return <DeleteIcon style={iconStyle} />
|
||||
default: return null
|
||||
case 'copy': return <CopyIcon />
|
||||
case 'show': return <VisibilityIcon />
|
||||
case 'hide': return <VisibilityOffIcon />
|
||||
case 'delete': return <DeleteIcon />
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
style={{
|
||||
display: "inline",
|
||||
marginLeft: margins[0],
|
||||
...this.props.style
|
||||
}}
|
||||
className="maputnik-layer-list-icon-action"
|
||||
data-wd-key={this.props.wdKey}
|
||||
onClick={this.props.onClick}
|
||||
onMouseOver={e => this.setState({hover: true})}
|
||||
onMouseOut={e => this.setState({hover: false})}
|
||||
>
|
||||
{this.renderIcon()}
|
||||
</a>
|
||||
@@ -83,15 +59,16 @@ class IconAction extends React.Component {
|
||||
@SortableElement
|
||||
class LayerListItem extends React.Component {
|
||||
static propTypes = {
|
||||
layerId: React.PropTypes.string.isRequired,
|
||||
layerType: React.PropTypes.string.isRequired,
|
||||
isSelected: React.PropTypes.bool,
|
||||
visibility: React.PropTypes.string,
|
||||
layerId: PropTypes.string.isRequired,
|
||||
layerType: PropTypes.string.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
visibility: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
|
||||
onLayerSelect: React.PropTypes.func.isRequired,
|
||||
onLayerCopy: React.PropTypes.func,
|
||||
onLayerDestroy: React.PropTypes.func,
|
||||
onLayerVisibilityToggle: React.PropTypes.func,
|
||||
onLayerSelect: PropTypes.func.isRequired,
|
||||
onLayerCopy: PropTypes.func,
|
||||
onLayerDestroy: PropTypes.func,
|
||||
onLayerVisibilityToggle: PropTypes.func,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
@@ -103,87 +80,43 @@ class LayerListItem extends React.Component {
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
reactIconBase: React.PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
hover: false
|
||||
}
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
reactIconBase: { size: fontSizes[4] }
|
||||
reactIconBase: { size: 14 }
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const itemStyle = {
|
||||
fontWeight: 400,
|
||||
color: colors.lowgray,
|
||||
fontSize: fontSizes[5],
|
||||
borderLeft: 0,
|
||||
borderTop: 0,
|
||||
borderBottom: 1,
|
||||
borderRight: 0,
|
||||
borderStyle: "solid",
|
||||
userSelect: 'none',
|
||||
listStyle: 'none',
|
||||
zIndex: 2000,
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
padding: margins[1],
|
||||
borderColor: Color(colors.black).lighten(0.10).string(),
|
||||
backgroundColor: colors.black,
|
||||
lineHeight: 1.3,
|
||||
}
|
||||
|
||||
if(this.state.hover) {
|
||||
itemStyle.backgroundColor = Color(colors.black).lighten(0.10).string()
|
||||
}
|
||||
|
||||
if(this.props.isSelected) {
|
||||
itemStyle.backgroundColor = Color(colors.black).lighten(0.15).string()
|
||||
}
|
||||
|
||||
const iconProps = {
|
||||
active: this.state.hover || this.props.isSelected
|
||||
}
|
||||
|
||||
return <li
|
||||
key={this.props.layerId}
|
||||
onClick={e => this.props.onLayerSelect(this.props.layerId)}
|
||||
onMouseOver={e => this.setState({hover: true})}
|
||||
onMouseOut={e => this.setState({hover: false})}
|
||||
style={itemStyle}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
}}>
|
||||
data-wd-key={"layer-list-item:"+this.props.layerId}
|
||||
className={classnames({
|
||||
"maputnik-layer-list-item": true,
|
||||
"maputnik-layer-list-item-selected": this.props.isSelected,
|
||||
[this.props.className]: true,
|
||||
})}>
|
||||
<LayerTypeDragHandle type={this.props.layerType} />
|
||||
<span style={{
|
||||
width: 115,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}>{this.props.layerId}</span>
|
||||
<span className="maputnik-layer-list-item-id">{this.props.layerId}</span>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<IconAction {...iconProps}
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
|
||||
action={'delete'}
|
||||
onClick={e => this.props.onLayerDestroy(this.props.layerId)}
|
||||
/>
|
||||
<IconAction {...iconProps}
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
|
||||
action={'copy'}
|
||||
onClick={e => this.props.onLayerCopy(this.props.layerId)}
|
||||
/>
|
||||
<IconAction {...iconProps}
|
||||
active={this.state.hover || this.props.isSelected || this.props.visibility === 'none'}
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
|
||||
action={this.props.visibility === 'visible' ? 'hide' : 'show'}
|
||||
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
||||
|
||||
class LayerSourceBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
sourceIds: React.PropTypes.array,
|
||||
value: PropTypes.string,
|
||||
wdKey: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
sourceIds: PropTypes.array,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
@@ -18,12 +20,13 @@ class LayerSourceBlock extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Source"}>
|
||||
return <InputBlock label={"Source"} doc={styleSpec.latest.layer.source.doc}
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
<AutocompleteInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceIds.map(src => [src, src])}
|
||||
wrapperStyle={{ width: '50%' }}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
||||
|
||||
class LayerSourceLayer extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
sourceLayerIds: React.PropTypes.array,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
sourceLayerIds: PropTypes.array,
|
||||
isFixed: PropTypes.bool,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceLayerIds: [],
|
||||
isFixed: false
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Source Layer"}>
|
||||
return <InputBlock label={"Source Layer"} doc={styleSpec.latest.layer['source-layer'].doc}
|
||||
data-wd-key="layer-source-layer"
|
||||
>
|
||||
<AutocompleteInput
|
||||
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceLayerIds.map(l => [l, l])}
|
||||
wrapperStyle={{ width: '50%' }}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
|
||||
class LayerTypeBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
wdKey: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Layer Type"}>
|
||||
return <InputBlock label={"Type"} doc={styleSpec.latest.layer.type.doc}
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
<SelectInput
|
||||
options={[
|
||||
['background', 'Background'],
|
||||
@@ -20,6 +25,8 @@ class LayerTypeBlock extends React.Component {
|
||||
['raster', 'Raster'],
|
||||
['circle', 'Circle'],
|
||||
['fill-extrusion', 'Fill Extrusion'],
|
||||
['hillshade', 'Hillshade'],
|
||||
['heatmap', 'Heatmap'],
|
||||
]}
|
||||
onChange={this.props.onChange}
|
||||
value={this.props.value}
|
||||
|
||||
29
src/components/layers/MaxZoomBlock.jsx
Normal file
29
src/components/layers/MaxZoomBlock.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
|
||||
class MaxZoomBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Max Zoom"} doc={styleSpec.latest.layer.maxzoom.doc}
|
||||
data-wd-key="max-zoom"
|
||||
>
|
||||
<NumberInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
min={styleSpec.latest.layer.maxzoom.minimum}
|
||||
max={styleSpec.latest.layer.maxzoom.maximum}
|
||||
default={styleSpec.latest.layer.maxzoom.maximum}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default MaxZoomBlock
|
||||
29
src/components/layers/MinZoomBlock.jsx
Normal file
29
src/components/layers/MinZoomBlock.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
|
||||
class MinZoomBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Min Zoom"} doc={styleSpec.latest.layer.minzoom.doc}
|
||||
data-wd-key="min-zoom"
|
||||
>
|
||||
<NumberInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
min={styleSpec.latest.layer.minzoom.minimum}
|
||||
max={styleSpec.latest.layer.minzoom.maximum}
|
||||
default={styleSpec.latest.layer.minzoom.minimum}
|
||||
/>
|
||||
</InputBlock>
|
||||
}
|
||||
}
|
||||
|
||||
export default MinZoomBlock
|
||||
70
src/components/map/FeatureLayerPopup.jsx
Normal file
70
src/components/map/FeatureLayerPopup.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import LayerIcon from '../icons/LayerIcon'
|
||||
|
||||
function groupFeaturesBySourceLayer(features) {
|
||||
const sources = {}
|
||||
|
||||
let returnedFeatures = {};
|
||||
|
||||
features.forEach(feature => {
|
||||
if(returnedFeatures.hasOwnProperty(feature.layer.id)) {
|
||||
returnedFeatures[feature.layer.id]++
|
||||
|
||||
const featureObject = sources[feature.layer['source-layer']].find(f => f.layer.id === feature.layer.id)
|
||||
|
||||
featureObject.counter = returnedFeatures[feature.layer.id]
|
||||
} else {
|
||||
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
|
||||
sources[feature.layer['source-layer']].push(feature)
|
||||
|
||||
returnedFeatures[feature.layer.id] = 1
|
||||
}
|
||||
})
|
||||
|
||||
return sources
|
||||
}
|
||||
|
||||
class FeatureLayerPopup extends React.Component {
|
||||
static propTypes = {
|
||||
onLayerSelect: PropTypes.func.isRequired,
|
||||
features: PropTypes.array
|
||||
}
|
||||
|
||||
render() {
|
||||
const sources = groupFeaturesBySourceLayer(this.props.features)
|
||||
|
||||
const items = Object.keys(sources).map(vectorLayerId => {
|
||||
const layers = sources[vectorLayerId].map((feature, idx) => {
|
||||
return <label
|
||||
key={idx}
|
||||
className="maputnik-popup-layer"
|
||||
onClick={() => {
|
||||
this.props.onLayerSelect(feature.layer.id)
|
||||
}}
|
||||
>
|
||||
<LayerIcon type={feature.layer.type} style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
paddingRight: 3
|
||||
}}/>
|
||||
{feature.layer.id}
|
||||
{feature.counter && <span> × {feature.counter}</span>}
|
||||
</label>
|
||||
})
|
||||
return <div key={vectorLayerId}>
|
||||
<div className="maputnik-popup-layer-id">{vectorLayerId}</div>
|
||||
{layers}
|
||||
</div>
|
||||
})
|
||||
|
||||
return <div className="maputnik-feature-layer-popup">
|
||||
{items}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default FeatureLayerPopup
|
||||
@@ -1,66 +0,0 @@
|
||||
import React from 'react'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import LayerIcon from '../icons/LayerIcon'
|
||||
import input from '../../config/input'
|
||||
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
|
||||
const Panel = (props) => {
|
||||
return <div style={{
|
||||
backgroundColor: colors.gray,
|
||||
padding: margins[0],
|
||||
fontSize: fontSizes[5],
|
||||
lineHeight: 1.2,
|
||||
}}>{props.children}</div>
|
||||
}
|
||||
|
||||
function renderFeature(feature) {
|
||||
return <div>
|
||||
<Panel>{feature.layer['source-layer']}</Panel>
|
||||
</div>
|
||||
}
|
||||
|
||||
function groupFeaturesBySourceLayer(features) {
|
||||
const sources = {}
|
||||
features.forEach(feature => {
|
||||
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
|
||||
sources[feature.layer['source-layer']].push(feature)
|
||||
})
|
||||
return sources
|
||||
}
|
||||
|
||||
class FeatureLayerTable extends React.Component {
|
||||
render() {
|
||||
const sources = groupFeaturesBySourceLayer(this.props.features)
|
||||
|
||||
const items = Object.keys(sources).map(vectorLayerId => {
|
||||
const layers = sources[vectorLayerId].map(feature => {
|
||||
return <label style={{
|
||||
...input.label,
|
||||
display: 'block',
|
||||
width: 'auto',
|
||||
}}>
|
||||
<LayerIcon type={feature.layer.type} style={{
|
||||
width: fontSizes[4],
|
||||
height: fontSizes[4],
|
||||
paddingRight: margins[0],
|
||||
}}/>
|
||||
{feature.layer.id}
|
||||
</label>
|
||||
})
|
||||
return <div>
|
||||
<Panel>{vectorLayerId}</Panel>
|
||||
{layers}
|
||||
</div>
|
||||
})
|
||||
|
||||
return <div>
|
||||
{items}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default FeatureLayerTable
|
||||
@@ -1,41 +1,67 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
function displayValue(value) {
|
||||
if (typeof value === 'undefined' || value === null) return value;
|
||||
if (value instanceof Date) return value.toLocaleString();
|
||||
if (typeof value === 'object' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'string') return value.toString();
|
||||
return value;
|
||||
}
|
||||
|
||||
function renderProperties(feature) {
|
||||
return Object.keys(feature.properties).map(propertyName => {
|
||||
const property = feature.properties[propertyName]
|
||||
return <InputBlock label={propertyName} style={{marginTop: 0, marginBottom: 0}}>
|
||||
<StringInput value={property} style={{backgroundColor: 'transparent'}}/>
|
||||
return <InputBlock key={propertyName} label={propertyName}>
|
||||
<StringInput value={displayValue(property)} style={{backgroundColor: 'transparent'}}/>
|
||||
</InputBlock>
|
||||
})
|
||||
}
|
||||
|
||||
const Panel = (props) => {
|
||||
return <div style={{
|
||||
backgroundColor: colors.gray,
|
||||
padding: margins[0],
|
||||
fontSize: fontSizes[5],
|
||||
lineHeight: 1.2,
|
||||
}}>{props.children}</div>
|
||||
}
|
||||
|
||||
function renderFeature(feature) {
|
||||
console.log(feature)
|
||||
return <div>
|
||||
<Panel>{feature.layer['source-layer']}</Panel>
|
||||
return <div key={feature.id}>
|
||||
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
|
||||
<InputBlock key={"property-type"} label={"$type"}>
|
||||
<StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
|
||||
</InputBlock>
|
||||
{renderProperties(feature)}
|
||||
</div>
|
||||
}
|
||||
|
||||
function removeDuplicatedFeatures(features) {
|
||||
let uniqueFeatures = [];
|
||||
|
||||
features.forEach(feature => {
|
||||
const featureIndex = uniqueFeatures.findIndex(feature2 => {
|
||||
return feature.layer['source-layer'] === feature2.layer['source-layer']
|
||||
&& JSON.stringify(feature.properties) === JSON.stringify(feature2.properties)
|
||||
})
|
||||
|
||||
if(featureIndex === -1) {
|
||||
uniqueFeatures.push(feature)
|
||||
} else {
|
||||
if(uniqueFeatures[featureIndex].hasOwnProperty('counter')) {
|
||||
uniqueFeatures[featureIndex].inspectModeCounter++
|
||||
} else {
|
||||
uniqueFeatures[featureIndex].inspectModeCounter = 2
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return uniqueFeatures
|
||||
}
|
||||
|
||||
class FeaturePropertyPopup extends React.Component {
|
||||
static propTypes = {
|
||||
features: PropTypes.array
|
||||
}
|
||||
|
||||
render() {
|
||||
const features = this.props.features
|
||||
return <div>
|
||||
const features = removeDuplicatedFeatures(this.props.features)
|
||||
return <div className="maputnik-feature-property-popup">
|
||||
{features.map(renderFeature)}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
|
||||
import validateColor from 'mapbox-gl-style-spec/lib/validate/validate_color'
|
||||
import colors from '../../config/colors'
|
||||
import style from '../../libs/style'
|
||||
import FeaturePropertyPopup from './FeaturePropertyPopup'
|
||||
import { colorHighlightedLayer, generateColoredLayers } from '../../libs/stylegen'
|
||||
import 'mapbox-gl/dist/mapbox-gl.css'
|
||||
import '../../mapboxgl.css'
|
||||
|
||||
function convertInspectStyle(mapStyle, sources, highlightedLayer) {
|
||||
const coloredLayers = generateColoredLayers(sources)
|
||||
|
||||
const layer = colorHighlightedLayer(highlightedLayer)
|
||||
if(layer) {
|
||||
coloredLayers.push(layer)
|
||||
}
|
||||
|
||||
const newStyle = {
|
||||
...mapStyle,
|
||||
layers: [
|
||||
{
|
||||
"id": "background",
|
||||
"type": "background",
|
||||
"paint": {
|
||||
"background-color": colors.black,
|
||||
}
|
||||
},
|
||||
...coloredLayers,
|
||||
]
|
||||
}
|
||||
return newStyle
|
||||
}
|
||||
|
||||
function renderPopup(features) {
|
||||
var mountNode = document.createElement('div');
|
||||
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode)
|
||||
return mountNode.innerHTML;
|
||||
}
|
||||
|
||||
export default class InspectionMap extends React.Component {
|
||||
static propTypes = {
|
||||
onDataChange: React.PropTypes.func,
|
||||
sources: React.PropTypes.object,
|
||||
originalStyle: React.PropTypes.object,
|
||||
highlightedLayer: React.PropTypes.object,
|
||||
style: React.PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onTileLoaded: () => {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { map: null }
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if(!this.state.map) return
|
||||
|
||||
this.state.map.setStyle(convertInspectStyle(nextProps.mapStyle, this.props.sources, nextProps.highlightedLayer), { diff: true})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
MapboxGl.accessToken = this.props.accessToken
|
||||
|
||||
const map = new MapboxGl.Map({
|
||||
container: this.container,
|
||||
style: convertInspectStyle(this.props.mapStyle, this.props.sources, this.props.highlightedLayer),
|
||||
hash: true,
|
||||
})
|
||||
|
||||
const nav = new MapboxGl.NavigationControl();
|
||||
map.addControl(nav, 'top-right');
|
||||
|
||||
map.on("style.load", () => {
|
||||
this.setState({ map });
|
||||
})
|
||||
|
||||
map.on("data", e => {
|
||||
if(e.dataType !== 'tile') return
|
||||
this.props.onDataChange({
|
||||
map: this.state.map
|
||||
})
|
||||
})
|
||||
|
||||
map.on('click', this.displayPopup.bind(this))
|
||||
map.on('mousemove', function(e) {
|
||||
var features = map.queryRenderedFeatures(e.point, { layers: this.layers })
|
||||
map.getCanvas().style.cursor = (features.length) ? 'pointer' : ''
|
||||
})
|
||||
}
|
||||
|
||||
displayPopup(e) {
|
||||
const features = this.state.map.queryRenderedFeatures(e.point, {
|
||||
layers: this.layers
|
||||
});
|
||||
|
||||
if (!features.length) {
|
||||
return
|
||||
}
|
||||
|
||||
// Populate the popup and set its coordinates
|
||||
// based on the feature found.
|
||||
const popup = new MapboxGl.Popup()
|
||||
.setLngLat(e.lngLat)
|
||||
.setHTML(renderPopup(features))
|
||||
.addTo(this.state.map)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
ref={x => this.container = x}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
...this.props.style,
|
||||
}}></div>
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,78 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ReactDOM from 'react-dom'
|
||||
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
|
||||
import FeatureLayerTable from './FeatureLayerTable'
|
||||
import validateColor from 'mapbox-gl-style-spec/lib/validate/validate_color'
|
||||
import MapboxGl from 'mapbox-gl'
|
||||
import MapboxInspect from 'mapbox-gl-inspect'
|
||||
import FeatureLayerPopup from './FeatureLayerPopup'
|
||||
import FeaturePropertyPopup from './FeaturePropertyPopup'
|
||||
import style from '../../libs/style.js'
|
||||
import tokens from '../../config/tokens.json'
|
||||
import colors from 'mapbox-gl-inspect/lib/colors'
|
||||
import Color from 'color'
|
||||
import ZoomControl from '../../libs/zoomcontrol'
|
||||
import { colorHighlightedLayer } from '../../libs/highlight'
|
||||
import 'mapbox-gl/dist/mapbox-gl.css'
|
||||
import '../../mapboxgl.css'
|
||||
import '../../libs/mapbox-rtl'
|
||||
|
||||
function renderPopup(features) {
|
||||
function renderPropertyPopup(features) {
|
||||
var mountNode = document.createElement('div');
|
||||
ReactDOM.render(<FeatureLayerTable features={features} />, mountNode)
|
||||
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode)
|
||||
return mountNode.innerHTML;
|
||||
}
|
||||
|
||||
function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
|
||||
const backgroundLayer = {
|
||||
"id": "background",
|
||||
"type": "background",
|
||||
"paint": {
|
||||
"background-color": '#1c1f24',
|
||||
}
|
||||
}
|
||||
|
||||
const layer = colorHighlightedLayer(highlightedLayer)
|
||||
if(layer) {
|
||||
coloredLayers.push(layer)
|
||||
}
|
||||
|
||||
const sources = {}
|
||||
Object.keys(originalMapStyle.sources).forEach(sourceId => {
|
||||
const source = originalMapStyle.sources[sourceId]
|
||||
if(source.type !== 'raster' && source.type !== 'raster-dem') {
|
||||
sources[sourceId] = source
|
||||
}
|
||||
})
|
||||
|
||||
const inspectStyle = {
|
||||
...originalMapStyle,
|
||||
sources: sources,
|
||||
layers: [backgroundLayer].concat(coloredLayers)
|
||||
}
|
||||
return inspectStyle
|
||||
}
|
||||
|
||||
export default class MapboxGlMap extends React.Component {
|
||||
static propTypes = {
|
||||
onDataChange: React.PropTypes.func,
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
accessToken: React.PropTypes.string,
|
||||
style: React.PropTypes.object,
|
||||
onDataChange: PropTypes.func,
|
||||
onLayerSelect: PropTypes.func.isRequired,
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
inspectModeEnabled: PropTypes.bool.isRequired,
|
||||
highlightedLayer: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
onLayerSelect: () => {},
|
||||
mapboxAccessToken: tokens.mapbox,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
MapboxGl.accessToken = tokens.mapbox
|
||||
this.state = {
|
||||
map: null,
|
||||
inspect: null,
|
||||
isPopupOpen: false,
|
||||
popupX: 0,
|
||||
popupY: 0,
|
||||
@@ -38,26 +81,65 @@ export default class MapboxGlMap extends React.Component {
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if(!this.state.map) return
|
||||
const metadata = nextProps.mapStyle.metadata || {}
|
||||
MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox
|
||||
|
||||
//Mapbox GL now does diffing natively so we don't need to calculate
|
||||
//the necessary operations ourselves!
|
||||
this.state.map.setStyle(nextProps.mapStyle, { diff: true})
|
||||
if(!nextProps.inspectModeEnabled) {
|
||||
//Mapbox GL now does diffing natively so we don't need to calculate
|
||||
//the necessary operations ourselves!
|
||||
this.state.map.setStyle(nextProps.mapStyle, { diff: true})
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) {
|
||||
this.state.inspect.toggleInspector()
|
||||
}
|
||||
if(this.props.inspectModeEnabled) {
|
||||
this.state.inspect.render()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
MapboxGl.accessToken = this.props.accessToken
|
||||
|
||||
const map = new MapboxGl.Map({
|
||||
container: this.container,
|
||||
style: this.props.mapStyle,
|
||||
hash: true,
|
||||
})
|
||||
|
||||
const nav = new MapboxGl.NavigationControl();
|
||||
map.addControl(nav, 'top-right');
|
||||
const zoom = new ZoomControl;
|
||||
map.addControl(zoom, 'top-right');
|
||||
|
||||
const nav = new MapboxGl.NavigationControl();
|
||||
map.addControl(nav, 'top-right');
|
||||
|
||||
const inspect = new MapboxInspect({
|
||||
popup: new MapboxGl.Popup({
|
||||
closeOnClick: false
|
||||
}),
|
||||
showMapPopup: true,
|
||||
showMapPopupOnHover: false,
|
||||
showInspectMapPopupOnHover: true,
|
||||
showInspectButton: false,
|
||||
blockHoverPopupOnClick: true,
|
||||
assignLayerColor: (layerId, alpha) => {
|
||||
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string()
|
||||
},
|
||||
buildInspectStyle: (originalMapStyle, coloredLayers) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
|
||||
renderPopup: features => {
|
||||
if(this.props.inspectModeEnabled) {
|
||||
return renderPropertyPopup(features)
|
||||
} else {
|
||||
var mountNode = document.createElement('div');
|
||||
ReactDOM.render(<FeatureLayerPopup features={features} onLayerSelect={this.props.onLayerSelect} />, mountNode)
|
||||
return mountNode
|
||||
}
|
||||
}
|
||||
})
|
||||
map.addControl(inspect)
|
||||
|
||||
map.on("style.load", () => {
|
||||
this.setState({ map });
|
||||
this.setState({ map, inspect });
|
||||
})
|
||||
|
||||
map.on("data", e => {
|
||||
@@ -66,37 +148,12 @@ export default class MapboxGlMap extends React.Component {
|
||||
map: this.state.map
|
||||
})
|
||||
})
|
||||
|
||||
map.on('click', this.displayPopup.bind(this));
|
||||
map.on('mousemove', function(e) {
|
||||
var features = map.queryRenderedFeatures(e.point, { layers: this.layers })
|
||||
map.getCanvas().style.cursor = (features.length) ? 'pointer' : ''
|
||||
})
|
||||
}
|
||||
|
||||
displayPopup(e) {
|
||||
const features = this.state.map.queryRenderedFeatures(e.point, {
|
||||
layers: this.layers
|
||||
});
|
||||
|
||||
if(features.length < 1) return
|
||||
const popup = new MapboxGl.Popup()
|
||||
.setLngLat(e.lngLat)
|
||||
.setHTML(renderPopup(features))
|
||||
.addTo(this.state.map)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
className="maputnik-map"
|
||||
ref={x => this.container = x}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
...this.props.style,
|
||||
}}>
|
||||
</div>
|
||||
></div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import style from '../../libs/style.js'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import { loadJSON } from '../../libs/urlopen'
|
||||
import 'ol/ol.css'
|
||||
|
||||
|
||||
class OpenLayers3Map extends React.Component {
|
||||
static propTypes = {
|
||||
onDataChange: React.PropTypes.func,
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
accessToken: React.PropTypes.string,
|
||||
onDataChange: PropTypes.func,
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
accessToken: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
@@ -13,73 +19,61 @@ class OpenLayers3Map extends React.Component {
|
||||
onDataChange: () => {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.map = null
|
||||
}
|
||||
|
||||
updateStyle(newMapStyle) {
|
||||
const olms = require('ol-mapbox-style');
|
||||
const styleFunc = olms.apply(this.map, newMapStyle)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
require.ensure(["openlayers", "ol-mapbox-style"], ()=> {
|
||||
const ol = require('openlayers')
|
||||
const olms = require('ol-mapbox-style')
|
||||
const jsonStyle = nextProps.mapStyle
|
||||
const styleFunc = olms.getStyleFunction(jsonStyle, 'openmaptiles', this.resolutions)
|
||||
console.log('New style babee')
|
||||
|
||||
const layer = this.layer
|
||||
layer.setStyle(styleFunc)
|
||||
//NOTE: We need to mark the source as changed in order
|
||||
//to trigger a rerender
|
||||
layer.getSource().changed()
|
||||
|
||||
this.state.map.render()
|
||||
require.ensure(["ol", "ol-mapbox-style"], () => {
|
||||
if(!this.map) return
|
||||
this.updateStyle(nextProps.mapStyle)
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
//Load OpenLayers dynamically once we need it
|
||||
//TODO: Make this more convenient
|
||||
require.ensure(["openlayers", "ol-mapbox-style"], ()=> {
|
||||
require.ensure(["ol", "ol/map", "ol/view", "ol/control/zoom", "ol-mapbox-style"], ()=> {
|
||||
console.log('Loaded OpenLayers3 renderer')
|
||||
|
||||
const ol = require('openlayers')
|
||||
const olms = require('ol-mapbox-style')
|
||||
const olMap = require('ol/map').default
|
||||
const olView = require('ol/view').default
|
||||
const olZoom = require('ol/control/zoom').default
|
||||
|
||||
const tilegrid = ol.tilegrid.createXYZ({tileSize: 512, maxZoom: 22})
|
||||
this.resolutions = tilegrid.getResolutions()
|
||||
this.layer = new ol.layer.VectorTile({
|
||||
source: new ol.source.VectorTile({
|
||||
attributions: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>',
|
||||
format: new ol.format.MVT(),
|
||||
tileGrid: tilegrid,
|
||||
tilePixelRatio: 8,
|
||||
url: 'https://free-0.tilehosting.com/data/v3/{z}/{x}/{y}.pbf?key=tXiQqN3lIgskyDErJCeY'
|
||||
})
|
||||
})
|
||||
|
||||
const jsonStyle = this.props.mapStyle
|
||||
const styleFunc = olms.getStyleFunction(jsonStyle, 'openmaptiles', this.resolutions)
|
||||
this.layer.setStyle(styleFunc)
|
||||
|
||||
const map = new ol.Map({
|
||||
const map = new olMap({
|
||||
target: this.container,
|
||||
layers: [this.layer],
|
||||
view: new ol.View({
|
||||
center: jsonStyle.center,
|
||||
layers: [],
|
||||
view: new olView({
|
||||
zoom: 2,
|
||||
//zoom: jsonStyle.zoom,
|
||||
center: [52.5, -78.4]
|
||||
})
|
||||
})
|
||||
map.addControl(new ol.control.Zoom());
|
||||
this.setState({ map });
|
||||
map.addControl(new olZoom())
|
||||
this.map = map
|
||||
this.updateStyle(this.props.mapStyle)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div
|
||||
ref={x => this.container = x}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}></div>
|
||||
ref={x => this.container = x}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 40,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: 'calc(100% - 40px)',
|
||||
width: "75%",
|
||||
backgroundColor: '#fff',
|
||||
...this.props.style,
|
||||
}}>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../Button'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import Modal from './Modal'
|
||||
import colors from '../../config/colors'
|
||||
|
||||
import LayerTypeBlock from '../layers/LayerTypeBlock'
|
||||
import LayerIdBlock from '../layers/LayerIdBlock'
|
||||
@@ -14,17 +13,17 @@ import LayerSourceLayerBlock from '../layers/LayerSourceLayerBlock'
|
||||
|
||||
class AddModal extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
onStyleChange: React.PropTypes.func.isRequired,
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
onOpenToggle: React.PropTypes.func.isRequired,
|
||||
layers: PropTypes.array.isRequired,
|
||||
onLayersChange: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
|
||||
// A dict of source id's and the available source layers
|
||||
sources: React.PropTypes.object.isRequired,
|
||||
sources: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
addLayer() {
|
||||
const changedLayers = this.props.mapStyle.layers.slice(0)
|
||||
const changedLayers = this.props.layers.slice(0)
|
||||
const layer = {
|
||||
id: this.state.id,
|
||||
type: this.state.type,
|
||||
@@ -32,17 +31,14 @@ class AddModal extends React.Component {
|
||||
|
||||
if(this.state.type !== 'background') {
|
||||
layer.source = this.state.source
|
||||
layer['source-layer'] = this.state['source-layer']
|
||||
if(this.state.type !== 'raster' && this.state['source-layer']) {
|
||||
layer['source-layer'] = this.state['source-layer']
|
||||
}
|
||||
}
|
||||
|
||||
changedLayers.push(layer)
|
||||
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
layers: changedLayers,
|
||||
}
|
||||
|
||||
this.props.onStyleChange(changedStyle)
|
||||
this.props.onLayersChange(changedLayers)
|
||||
this.props.onOpenToggle(false)
|
||||
}
|
||||
|
||||
@@ -59,48 +55,108 @@ class AddModal extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const sourceIds = Object.keys(nextProps.sources)
|
||||
if(!this.state.source && sourceIds.length > 0) {
|
||||
componentWillUpdate(nextProps, nextState) {
|
||||
// Check if source is valid for new type
|
||||
const oldType = this.state.type;
|
||||
const newType = nextState.type;
|
||||
|
||||
const availableSourcesOld = this.getSources(oldType);
|
||||
const availableSourcesNew = this.getSources(newType);
|
||||
|
||||
if(
|
||||
// Type has changed
|
||||
oldType !== newType
|
||||
&& this.state.source !== ""
|
||||
// Was a valid source previously
|
||||
&& availableSourcesOld.indexOf(this.state.source) > -1
|
||||
// And is not a valid source now
|
||||
&& availableSourcesNew.indexOf(nextState.source) < 0
|
||||
) {
|
||||
// Clear the source
|
||||
this.setState({
|
||||
source: sourceIds[0],
|
||||
'source-layer': this.state['source-layer'] || nextProps.sources[sourceIds[0]][0]
|
||||
})
|
||||
source: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getLayersForSource(source) {
|
||||
const sourceObj = this.props.sources[source] || {};
|
||||
return sourceObj.layers || [];
|
||||
}
|
||||
|
||||
getSources(type) {
|
||||
const sources = [];
|
||||
|
||||
const types = {
|
||||
vector: [
|
||||
"fill",
|
||||
"line",
|
||||
"symbol",
|
||||
"circle",
|
||||
"fill-extrusion"
|
||||
],
|
||||
raster: [
|
||||
"raster"
|
||||
]
|
||||
}
|
||||
|
||||
for(let [key, val] of Object.entries(this.props.sources)) {
|
||||
if(types[val.type] && types[val.type].indexOf(type) > -1) {
|
||||
sources.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
return sources;
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const sources = this.getSources(this.state.type);
|
||||
const layers = this.getLayersForSource(this.state.source);
|
||||
|
||||
return <Modal
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Add Layer'}
|
||||
data-wd-key="modal:add-layer"
|
||||
>
|
||||
<div className="maputnik-add-layer">
|
||||
<LayerIdBlock
|
||||
value={this.state.id}
|
||||
onChange={v => this.setState({ id: v })}
|
||||
wdKey="add-layer.layer-id"
|
||||
onChange={v => {
|
||||
this.setState({ id: v })
|
||||
}}
|
||||
/>
|
||||
<LayerTypeBlock
|
||||
value={this.state.type}
|
||||
wdKey="add-layer.layer-type"
|
||||
onChange={v => this.setState({ type: v })}
|
||||
/>
|
||||
{this.state.type !== 'background' &&
|
||||
<LayerSourceBlock
|
||||
sourceIds={Object.keys(this.props.sources)}
|
||||
sourceIds={sources}
|
||||
wdKey="add-layer.layer-source-block"
|
||||
value={this.state.source}
|
||||
onChange={v => this.setState({ source: v })}
|
||||
/>
|
||||
}
|
||||
{this.state.type !== 'background' &&
|
||||
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
|
||||
<LayerSourceLayerBlock
|
||||
sourceLayerIds={this.props.sources[this.state.source] || []}
|
||||
isFixed={true}
|
||||
sourceLayerIds={layers}
|
||||
value={this.state['source-layer']}
|
||||
onChange={v => this.setState({ 'source-layer': v })}
|
||||
/>
|
||||
}
|
||||
<Button onClick={this.addLayer.bind(this)}>
|
||||
<Button
|
||||
className="maputnik-add-layer-button"
|
||||
onClick={this.addLayer.bind(this)}
|
||||
data-wd-key="add-layer"
|
||||
>
|
||||
Add Layer
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
269
src/components/modals/ExportModal.jsx
Normal file
269
src/components/modals/ExportModal.jsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import CheckboxInput from '../inputs/CheckboxInput'
|
||||
import Button from '../Button'
|
||||
import Modal from './Modal'
|
||||
import MdFileDownload from 'react-icons/lib/md/file-download'
|
||||
import TiClipboard from 'react-icons/lib/ti/clipboard'
|
||||
import style from '../../libs/style.js'
|
||||
import GitHub from 'github-api'
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
||||
|
||||
|
||||
class Gist extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
onStyleChanged: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
preview: false,
|
||||
public: false,
|
||||
saving: false,
|
||||
latestGist: null,
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
preview: !!(nextProps.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']
|
||||
})
|
||||
}
|
||||
|
||||
onSave() {
|
||||
this.setState({
|
||||
...this.state,
|
||||
saving: true
|
||||
});
|
||||
|
||||
const preview = this.state.preview;
|
||||
|
||||
const mapboxToken = (this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token'];
|
||||
|
||||
const mapStyleStr = preview ?
|
||||
styleSpec.format(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle))) :
|
||||
styleSpec.format(stripAccessTokens(this.props.mapStyle));
|
||||
const styleTitle = this.props.mapStyle.name || 'Style';
|
||||
const htmlStr = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>`+styleTitle+` Preview</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v0.44.0/mapbox-gl.css" />
|
||||
<script src="https://api.mapbox.com/mapbox-gl-js/v0.44.0/mapbox-gl.js"></script>
|
||||
<style>
|
||||
body { margin:0; padding:0; }
|
||||
#map { position:absolute; top:0; bottom:0; width:100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id='map'></div>
|
||||
<script>
|
||||
mapboxgl.accessToken = '${mapboxToken}';
|
||||
var map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
style: 'style.json',
|
||||
attributionControl: true,
|
||||
hash: true
|
||||
});
|
||||
map.addControl(new mapboxgl.NavigationControl());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
const files = {
|
||||
"style.json": {
|
||||
content: mapStyleStr
|
||||
}
|
||||
}
|
||||
if(preview) {
|
||||
files["index.html"] = {
|
||||
content: htmlStr
|
||||
}
|
||||
}
|
||||
const gh = new GitHub();
|
||||
let gist = gh.getGist(); // not a gist yet
|
||||
gist.create({
|
||||
public: this.state.public,
|
||||
description: styleTitle,
|
||||
files: files
|
||||
}).then(function({data}) {
|
||||
return gist.read();
|
||||
}).then(function({data}) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
latestGist: data,
|
||||
saving: false,
|
||||
});
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
onPreviewChange(value) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
preview: value
|
||||
})
|
||||
}
|
||||
|
||||
onPublicChange(value) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
public: value
|
||||
})
|
||||
}
|
||||
|
||||
changeMetadataProperty(property, value) {
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
metadata: {
|
||||
...this.props.mapStyle.metadata,
|
||||
[property]: value
|
||||
}
|
||||
}
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
renderPreviewLink() {
|
||||
const gist = this.state.latestGist;
|
||||
const user = gist.user || 'anonymous';
|
||||
const preview = !!gist.files['index.html'];
|
||||
if(preview) {
|
||||
return <span><a target="_blank" rel="noopener noreferrer" href={"https://bl.ocks.org/"+user+"/"+gist.id}>Preview</a>,{' '}</span>
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderLatestGist() {
|
||||
const gist = this.state.latestGist;
|
||||
const saving = this.state.saving;
|
||||
if(saving) {
|
||||
return <p>Saving...</p>
|
||||
} else if(gist) {
|
||||
const user = gist.user || 'anonymous';
|
||||
const rawGistLink = "https://gist.githubusercontent.com/" + user + "/" + gist.id + "/raw/" + gist.history[0].version + "/style.json"
|
||||
const maputnikStyleLink = "https://maputnik.github.io/editor/?style=" + rawGistLink
|
||||
return <div className="maputnik-render-gist">
|
||||
<p>
|
||||
Latest saved gist:{' '}
|
||||
{this.renderPreviewLink(this)}
|
||||
<a target="_blank" rel="noopener noreferrer" href={"https://gist.github.com/" + user + "/" + gist.id}>Source</a>
|
||||
</p>
|
||||
<p>
|
||||
<CopyToClipboard text={maputnikStyleLink}>
|
||||
<span>Share this style: <Button><TiClipboard size={18} /></Button></span>
|
||||
</CopyToClipboard>
|
||||
<StringInput value={maputnikStyleLink} />
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-export-gist">
|
||||
<Button onClick={this.onSave.bind(this)}>
|
||||
<MdFileDownload />
|
||||
Save to Gist (anonymous)
|
||||
</Button>
|
||||
<div className="maputnik-modal-sub-section">
|
||||
<CheckboxInput
|
||||
value={this.state.public}
|
||||
name='gist-style-public'
|
||||
onChange={this.onPublicChange.bind(this)}
|
||||
/>
|
||||
<span> Public gist</span>
|
||||
</div>
|
||||
<div className="maputnik-modal-sub-section">
|
||||
<CheckboxInput
|
||||
value={this.state.preview}
|
||||
name='gist-style-preview'
|
||||
onChange={this.onPreviewChange.bind(this)}
|
||||
/>
|
||||
<span> Include preview</span>
|
||||
</div>
|
||||
{this.state.preview ?
|
||||
<div>
|
||||
<InputBlock
|
||||
label={"OpenMapTiles Access Token: "}>
|
||||
<StringInput
|
||||
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}/>
|
||||
</InputBlock>
|
||||
<InputBlock
|
||||
label={"Mapbox Access Token: "}>
|
||||
<StringInput
|
||||
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}/>
|
||||
</InputBlock>
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://openmaptiles.com/hosting/">Get your free access token</a>
|
||||
</div>
|
||||
: null}
|
||||
{this.renderLatestGist()}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
function stripAccessTokens(mapStyle) {
|
||||
const changedMetadata = { ...mapStyle.metadata }
|
||||
delete changedMetadata['maputnik:mapbox_access_token']
|
||||
delete changedMetadata['maputnik:openmaptiles_access_token']
|
||||
return {
|
||||
...mapStyle,
|
||||
metadata: changedMetadata
|
||||
}
|
||||
}
|
||||
|
||||
class ExportModal extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
onStyleChanged: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
downloadStyle() {
|
||||
const blob = new Blob([styleSpec.format(stripAccessTokens(this.props.mapStyle))], {type: "application/json;charset=utf-8"});
|
||||
saveAs(blob, this.props.mapStyle.id + ".json");
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Modal
|
||||
data-wd-key="export-modal"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Export Style'}
|
||||
>
|
||||
|
||||
<div className="maputnik-modal-section">
|
||||
<h4>Download Style</h4>
|
||||
<p>
|
||||
Download a JSON style to your computer.
|
||||
</p>
|
||||
<Button onClick={this.downloadStyle.bind(this)}>
|
||||
<MdFileDownload />
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="maputnik-modal-section hide">
|
||||
<h4>Save style</h4>
|
||||
<Gist mapStyle={this.props.mapStyle} onStyleChanged={this.props.onStyleChanged}/>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
export default ExportModal
|
||||
@@ -1,45 +1,34 @@
|
||||
import React from 'react'
|
||||
|
||||
import PropTypes from 'prop-types'
|
||||
import CloseIcon from 'react-icons/lib/md/close'
|
||||
|
||||
import Overlay from './Overlay'
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
|
||||
class Modal extends React.Component {
|
||||
static propTypes = {
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
title: React.PropTypes.string.isRequired,
|
||||
onOpenToggle: React.PropTypes.func.isRequired,
|
||||
"data-wd-key": PropTypes.string,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Overlay isOpen={this.props.isOpen}>
|
||||
<div style={{
|
||||
minWidth: 350,
|
||||
maxWidth: 600,
|
||||
backgroundColor: colors.black,
|
||||
boxShadow: '0px 0px 5px 0px rgba(0,0,0,0.3)',
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: colors.gray,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
padding: margins[2],
|
||||
fontSize: fontSizes[4],
|
||||
}}>
|
||||
{this.props.title}
|
||||
<span style={{flexGrow: 1}} />
|
||||
<a
|
||||
<div className="maputnik-modal"
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
>
|
||||
<header className="maputnik-modal-header">
|
||||
<h1 className="maputnik-modal-header-title">{this.props.title}</h1>
|
||||
<span className="maputnik-modal-header-space"></span>
|
||||
<a className="maputnik-modal-header-toggle"
|
||||
onClick={() => this.props.onOpenToggle(false)}
|
||||
style={{ cursor: 'pointer' }} >
|
||||
data-wd-key={this.props["data-wd-key"]+".close-modal"}
|
||||
>
|
||||
<CloseIcon />
|
||||
</a>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: margins[2],
|
||||
}}>
|
||||
{this.props.children}
|
||||
</header>
|
||||
<div className="maputnik-modal-scroller">
|
||||
<div className="maputnik-modal-content">{this.props.children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Modal from './Modal'
|
||||
import Heading from '../Heading'
|
||||
import Button from '../Button'
|
||||
import Paragraph from '../Paragraph'
|
||||
import FileReaderInput from 'react-file-reader-input'
|
||||
import request from 'request'
|
||||
|
||||
@@ -10,51 +9,29 @@ import FileUploadIcon from 'react-icons/lib/md/file-upload'
|
||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||
|
||||
import style from '../../libs/style.js'
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
import publicStyles from '../../config/styles.json'
|
||||
|
||||
class PublicStyle extends React.Component {
|
||||
static propTypes = {
|
||||
url: React.PropTypes.string.isRequired,
|
||||
thumbnailUrl: React.PropTypes.string.isRequired,
|
||||
title: React.PropTypes.string.isRequired,
|
||||
onSelect: React.PropTypes.func.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
thumbnailUrl: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
verticalAlign: 'top',
|
||||
marginTop: margins[2],
|
||||
marginRight: margins[2],
|
||||
backgroundColor: colors.gray,
|
||||
display: 'inline-block',
|
||||
width: 180,
|
||||
fontSize: fontSizes[4],
|
||||
color: colors.lowgray,
|
||||
}}>
|
||||
return <div className="maputnik-public-style">
|
||||
<Button
|
||||
className="maputnik-public-style-button"
|
||||
onClick={() => this.props.onSelect(this.props.url)}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
padding: margins[2],
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
<span style={{fontWeight: 700}}>{this.props.title}</span>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<header className="maputnik-public-style-header">
|
||||
<h4>{this.props.title}</h4>
|
||||
<span className="maputnik-space" />
|
||||
<AddIcon />
|
||||
</div>
|
||||
</header>
|
||||
<img
|
||||
style={{
|
||||
display: 'block',
|
||||
marginTop: margins[1],
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
className="maputnik-public-style-thumbnail"
|
||||
src={this.props.thumbnailUrl}
|
||||
alt={this.props.title}
|
||||
/>
|
||||
@@ -65,12 +42,25 @@ class PublicStyle extends React.Component {
|
||||
|
||||
class OpenModal extends React.Component {
|
||||
static propTypes = {
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
onOpenToggle: React.PropTypes.func.isRequired,
|
||||
onStyleOpen: React.PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
onStyleOpen: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
clearError() {
|
||||
this.setState({
|
||||
error: null
|
||||
})
|
||||
}
|
||||
|
||||
onStyleSelect(styleUrl) {
|
||||
this.clearError();
|
||||
|
||||
request({
|
||||
url: styleUrl,
|
||||
withCredentials: false,
|
||||
@@ -79,24 +69,48 @@ class OpenModal extends React.Component {
|
||||
const mapStyle = style.ensureStyleValidity(JSON.parse(body))
|
||||
console.log('Loaded style ', mapStyle.id)
|
||||
this.props.onStyleOpen(mapStyle)
|
||||
this.onOpenToggle()
|
||||
} else {
|
||||
console.warn('Could not open the style URL', styleUrl)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onOpenUrl() {
|
||||
const url = this.styleUrlElement.value;
|
||||
this.onStyleSelect(url);
|
||||
}
|
||||
|
||||
onUpload(_, files) {
|
||||
const [e, file] = files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
this.clearError();
|
||||
|
||||
reader.readAsText(file, "UTF-8");
|
||||
reader.onload = e => {
|
||||
let mapStyle = JSON.parse(e.target.result)
|
||||
let mapStyle;
|
||||
try {
|
||||
mapStyle = JSON.parse(e.target.result)
|
||||
}
|
||||
catch(err) {
|
||||
this.setState({
|
||||
error: err.toString()
|
||||
});
|
||||
return;
|
||||
}
|
||||
mapStyle = style.ensureStyleValidity(mapStyle)
|
||||
this.props.onStyleOpen(mapStyle);
|
||||
this.onOpenToggle();
|
||||
}
|
||||
reader.onerror = e => console.log(e.target);
|
||||
}
|
||||
|
||||
onOpenToggle() {
|
||||
this.clearError();
|
||||
this.props.onOpenToggle();
|
||||
}
|
||||
|
||||
render() {
|
||||
const styleOptions = publicStyles.map(style => {
|
||||
return <PublicStyle
|
||||
@@ -108,27 +122,51 @@ class OpenModal extends React.Component {
|
||||
/>
|
||||
})
|
||||
|
||||
let errorElement;
|
||||
if(this.state.error) {
|
||||
errorElement = (
|
||||
<div className="maputnik-modal-error">
|
||||
{this.state.error}
|
||||
<a href="#" onClick={() => this.clearError()} className="maputnik-modal-error-close">×</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <Modal
|
||||
data-wd-key="open-modal"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
onOpenToggle={() => this.onOpenToggle()}
|
||||
title={'Open Style'}
|
||||
>
|
||||
<Heading level={4}>Upload Style</Heading>
|
||||
<Paragraph>
|
||||
Upload a JSON style from your computer.
|
||||
</Paragraph>
|
||||
<FileReaderInput onChange={this.onUpload.bind(this)}>
|
||||
<Button>
|
||||
<FileUploadIcon />
|
||||
Upload
|
||||
</Button>
|
||||
</FileReaderInput>
|
||||
{errorElement}
|
||||
<section className="maputnik-modal-section">
|
||||
<h2>Upload Style</h2>
|
||||
<p>Upload a JSON style from your computer.</p>
|
||||
<FileReaderInput onChange={this.onUpload.bind(this)}>
|
||||
<Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button>
|
||||
</FileReaderInput>
|
||||
</section>
|
||||
|
||||
<Heading level={4}>Gallery Styles</Heading>
|
||||
<Paragraph>
|
||||
Open one of the publicly available styles to start from.
|
||||
</Paragraph>
|
||||
{styleOptions}
|
||||
<section className="maputnik-modal-section">
|
||||
<h2>Load from URL</h2>
|
||||
<p>
|
||||
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
|
||||
</p>
|
||||
<input data-wd-key="open-modal.url.input" type="text" ref={(input) => this.styleUrlElement = input} className="maputnik-input" placeholder="Enter URL..."/>
|
||||
<div>
|
||||
<Button data-wd-key="open-modal.url.button" className="maputnik-big-button" onClick={this.onOpenUrl.bind(this)}>Open URL</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="maputnik-modal-section maputnik-modal-section--shrink">
|
||||
<h2>Gallery Styles</h2>
|
||||
<p>
|
||||
Open one of the publicly available styles to start from.
|
||||
</p>
|
||||
<div className="maputnik-style-gallery-container">
|
||||
{styleOptions}
|
||||
</div>
|
||||
</section>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,22 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class ViewportOverlay extends React.Component {
|
||||
static propTypes = {
|
||||
style: React.PropTypes.object
|
||||
}
|
||||
|
||||
render() {
|
||||
const overlayStyle = {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
zIndex: 2,
|
||||
opacity: 0.875,
|
||||
backgroundColor: 'rgb(28, 31, 36)',
|
||||
...this.props.style
|
||||
}
|
||||
|
||||
return <div style={overlayStyle} />
|
||||
}
|
||||
}
|
||||
|
||||
class Overlay extends React.Component {
|
||||
static propTypes = {
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
children: React.PropTypes.element.isRequired
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
children: PropTypes.element.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
position: 'fixed',
|
||||
display: this.props.isOpen ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<ViewportOverlay />
|
||||
<div style={{
|
||||
zIndex: 3,
|
||||
}}>
|
||||
let overlayStyle = {}
|
||||
if(!this.props.isOpen) {
|
||||
overlayStyle['display'] = 'none';
|
||||
}
|
||||
|
||||
return <div className={"maputnik-overlay"} style={overlayStyle}>
|
||||
<div className={"maputnik-overlay-viewport"} />
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import Modal from './Modal'
|
||||
import colors from '../../config/colors'
|
||||
|
||||
class SettingsModal extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
onStyleChanged: React.PropTypes.func.isRequired,
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
onOpenToggle: React.PropTypes.func.isRequired,
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
onStyleChanged: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
@@ -41,54 +42,70 @@ class SettingsModal extends React.Component {
|
||||
const metadata = this.props.mapStyle.metadata || {}
|
||||
const inputProps = { }
|
||||
return <Modal
|
||||
data-wd-key="modal-settings"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'StyleSettings'}
|
||||
title={'Style Settings'}
|
||||
>
|
||||
<InputBlock label={"Name"}>
|
||||
<div style={{minWidth: 350}}>
|
||||
<InputBlock label={"Name"} doc={styleSpec.latest.$root.name.doc}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.name"
|
||||
value={this.props.mapStyle.name}
|
||||
onChange={this.changeStyleProperty.bind(this, "name")}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Owner"}>
|
||||
<InputBlock label={"Owner"} doc={"Owner ID of the style. Used by Mapbox or future style APIs."}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.owner"
|
||||
value={this.props.mapStyle.owner}
|
||||
onChange={this.changeStyleProperty.bind(this, "owner")}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Sprite URL"}>
|
||||
<InputBlock label={"Sprite URL"} doc={styleSpec.latest.$root.sprite.doc}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.sprite"
|
||||
value={this.props.mapStyle.sprite}
|
||||
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<InputBlock label={"Glyphs URL"}>
|
||||
<InputBlock label={"Glyphs URL"} doc={styleSpec.latest.$root.glyphs.doc}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.glyphs"
|
||||
value={this.props.mapStyle.glyphs}
|
||||
onChange={this.changeStyleProperty.bind(this, "glyphs")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<InputBlock label={"Access Token"}>
|
||||
<InputBlock label={"Mapbox Access Token"} doc={"Public access token for Mapbox services."}>
|
||||
<StringInput {...inputProps}
|
||||
value={metadata['maputnik:access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:access_token")}
|
||||
data-wd-key="modal-settings.maputnik:mapbox_access_token"
|
||||
value={metadata['maputnik:mapbox_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<InputBlock label={"Style Renderer"}>
|
||||
<InputBlock label={"OpenMapTiles Access Token"} doc={"Public access token for the OpenMapTiles CDN."}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.maputnik:openmaptiles_access_token"
|
||||
value={metadata['maputnik:openmaptiles_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
||||
/>
|
||||
</InputBlock>
|
||||
|
||||
<InputBlock label={"Style Renderer"} doc={"Choose the default Maputnik renderer for this style."}>
|
||||
<SelectInput {...inputProps}
|
||||
data-wd-key="modal-settings.maputnik:renderer"
|
||||
options={[
|
||||
['mbgljs', 'MapboxGL JS'],
|
||||
['ol3', 'Open Layers 3'],
|
||||
['inspection', 'Inspection Mode'],
|
||||
]}
|
||||
value={metadata['maputnik:renderer'] || 'mbgljs'}
|
||||
onChange={this.changeMetadataProperty.bind(this, 'maputnik:renderer')}
|
||||
/>
|
||||
</InputBlock>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,106 +1,87 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import Modal from './Modal'
|
||||
import Heading from '../Heading'
|
||||
import Button from '../Button'
|
||||
import Paragraph from '../Paragraph'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import SourceTypeEditor from '../sources/SourceTypeEditor'
|
||||
|
||||
import style from '../../libs/style'
|
||||
import { deleteSource, addSource, changeSource } from '../../libs/source'
|
||||
import publicSources from '../../config/tilesets.json'
|
||||
import colors from '../../config/colors'
|
||||
import { margins, fontSizes } from '../../config/scales'
|
||||
|
||||
import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||
|
||||
class PublicSource extends React.Component {
|
||||
static propTypes = {
|
||||
id: React.PropTypes.string.isRequired,
|
||||
type: React.PropTypes.string.isRequired,
|
||||
title: React.PropTypes.string.isRequired,
|
||||
onSelect: React.PropTypes.func.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{
|
||||
verticalAlign: 'top',
|
||||
marginTop: margins[2],
|
||||
marginRight: margins[2],
|
||||
backgroundColor: colors.gray,
|
||||
display: 'inline-block',
|
||||
width: 240,
|
||||
fontSize: fontSizes[4],
|
||||
color: colors.lowgray,
|
||||
}}>
|
||||
<Button
|
||||
onClick={() => this.props.onSelect(this.props.id)}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
padding: margins[2],
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span style={{fontWeight: 700}}>{this.props.title}</span>
|
||||
<br/>
|
||||
<span style={{fontSize: fontSizes[5]}}>#{this.props.id}</span>
|
||||
</div>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<AddIcon />
|
||||
</Button>
|
||||
return <div className="maputnik-public-source">
|
||||
<Button
|
||||
className="maputnik-public-source-select"
|
||||
onClick={() => this.props.onSelect(this.props.id)}
|
||||
>
|
||||
<div className="maputnik-public-source-info">
|
||||
<p className="maputnik-public-source-name">{this.props.title}</p>
|
||||
<p className="maputnik-public-source-id">#{this.props.id}</p>
|
||||
</div>
|
||||
<span className="maputnik-space" />
|
||||
<AddIcon />
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
function editorMode(source) {
|
||||
if(source.type === 'geojson') return ' geojson'
|
||||
if(source.type === 'vector' && source.tiles) {
|
||||
return 'tilexyz'
|
||||
if(source.type === 'raster') {
|
||||
if(source.tiles) return 'tilexyz_raster'
|
||||
return 'tilejson_raster'
|
||||
}
|
||||
return 'tilejson'
|
||||
if(source.type === 'raster-dem') {
|
||||
if(source.tiles) return 'tilexyz_raster-dem'
|
||||
return 'tilejson_raster-dem'
|
||||
}
|
||||
if(source.type === 'vector') {
|
||||
if(source.tiles) return 'tilexyz_vector'
|
||||
return 'tilejson_vector'
|
||||
}
|
||||
if(source.type === 'geojson') return 'geojson'
|
||||
return null
|
||||
}
|
||||
|
||||
class ActiveSourceTypeEditor extends React.Component {
|
||||
static propTypes = {
|
||||
sourceId: React.PropTypes.string.isRequired,
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onSourceDelete: React.PropTypes.func.isRequired,
|
||||
onSourceChange: React.PropTypes.func.isRequired,
|
||||
sourceId: PropTypes.string.isRequired,
|
||||
source: PropTypes.object.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const inputProps = { }
|
||||
return <div style={{
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: colors.gray,
|
||||
color: colors.lowgray,
|
||||
padding: margins[1],
|
||||
display: 'flex',
|
||||
fontSize: fontSizes[4],
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
<span style={{fontWeight: 700, fontSize: fontSizes[4], lineHeight: 2}}>#{this.props.sourceId}</span>
|
||||
<span style={{flexGrow: 1}} />
|
||||
return <div className="maputnik-active-source-type-editor">
|
||||
<div className="maputnik-active-source-type-editor-header">
|
||||
<span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
|
||||
<span className="maputnik-space" />
|
||||
<Button
|
||||
onClick={()=> this.props.onSourceDelete(this.props.sourceId)}
|
||||
className="maputnik-active-source-type-editor-header-delete"
|
||||
onClick={()=> this.props.onDelete(this.props.sourceId)}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{
|
||||
borderColor: colors.gray,
|
||||
borderWidth: 2,
|
||||
borderStyle: 'solid',
|
||||
padding: margins[1],
|
||||
}}>
|
||||
<div className="maputnik-active-source-type-editor-content">
|
||||
<SourceTypeEditor
|
||||
onChange={this.props.onSourceChange}
|
||||
onChange={this.props.onChange}
|
||||
mode={editorMode(this.props.source)}
|
||||
source={this.props.source}
|
||||
/>
|
||||
@@ -111,15 +92,15 @@ class ActiveSourceTypeEditor extends React.Component {
|
||||
|
||||
class AddSource extends React.Component {
|
||||
static propTypes = {
|
||||
onSourceAdd: React.PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
mode: 'tilejson',
|
||||
mode: 'tilejson_vector',
|
||||
sourceId: style.generateId(),
|
||||
source: this.defaultSource('tilejson'),
|
||||
source: this.defaultSource('tilejson_vector'),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,51 +111,71 @@ class AddSource extends React.Component {
|
||||
type: 'geojson',
|
||||
data: source.data || 'http://localhost:3000/geojson.json'
|
||||
}
|
||||
case 'tilejson': return {
|
||||
case 'tilejson_vector': return {
|
||||
type: 'vector',
|
||||
url: source.url || 'http://localhost:3000/tilejson.json'
|
||||
}
|
||||
case 'tilexyz': return {
|
||||
case 'tilexyz_vector': return {
|
||||
type: 'vector',
|
||||
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
|
||||
minZoom: source.minZoom || 0,
|
||||
maxZoom: source.maxZoom || 14
|
||||
minZoom: source.minzoom || 0,
|
||||
maxZoom: source.maxzoom || 14
|
||||
}
|
||||
case 'tilejson_raster': return {
|
||||
type: 'raster',
|
||||
url: source.url || 'http://localhost:3000/tilejson.json'
|
||||
}
|
||||
case 'tilexyz_raster': return {
|
||||
type: 'raster',
|
||||
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
|
||||
minzoom: source.minzoom || 0,
|
||||
maxzoom: source.maxzoom || 14
|
||||
}
|
||||
case 'tilejson_raster-dem': return {
|
||||
type: 'raster-dem',
|
||||
url: source.url || 'http://localhost:3000/tilejson.json'
|
||||
}
|
||||
case 'tilexyz_raster-dem': return {
|
||||
type: 'raster-dem',
|
||||
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
|
||||
minzoom: source.minzoom || 0,
|
||||
maxzoom: source.maxzoom || 14
|
||||
}
|
||||
default: return {}
|
||||
}
|
||||
}
|
||||
|
||||
onSourceChange(source) {
|
||||
this.setState({
|
||||
source: source
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<InputBlock label={"Source ID"}>
|
||||
return <div className="maputnik-add-source">
|
||||
<InputBlock label={"Source ID"} doc={"Unique ID that identifies the source and is used in the layer to reference the source."}>
|
||||
<StringInput
|
||||
value={this.state.sourceId}
|
||||
onChange={v => this.setState({ sourceId: v})}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Source Type"}>
|
||||
<InputBlock label={"Source Type"} doc={styleSpec.latest.source_vector.type.doc}>
|
||||
<SelectInput
|
||||
options={[
|
||||
['geojson', 'GeoJSON'],
|
||||
['tilejson', 'Vector (TileJSON URL)'],
|
||||
['tilexyz', 'Vector (XYZ URLs)'],
|
||||
['tilejson_vector', 'Vector (TileJSON URL)'],
|
||||
['tilexyz_vector', 'Vector (XYZ URLs)'],
|
||||
['tilejson_raster', 'Raster (TileJSON URL)'],
|
||||
['tilexyz_raster', 'Raster (XYZ URL)'],
|
||||
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
|
||||
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
|
||||
]}
|
||||
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
|
||||
value={this.state.mode}
|
||||
/>
|
||||
</InputBlock>
|
||||
<SourceTypeEditor
|
||||
onChange={this.onSourceChange.bind(this)}
|
||||
onChange={src => this.setState({ source: src })}
|
||||
mode={this.state.mode}
|
||||
source={this.state.source}
|
||||
/>
|
||||
<Button onClick={() => this.props.onSourceAdd(this.state.sourceId, this.state.source)}>
|
||||
<Button
|
||||
className="maputnik-add-source-button"
|
||||
onClick={() => this.props.onAdd(this.state.sourceId, this.state.source)}>
|
||||
Add Source
|
||||
</Button>
|
||||
</div>
|
||||
@@ -183,35 +184,10 @@ class AddSource extends React.Component {
|
||||
|
||||
class SourcesModal extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: React.PropTypes.object.isRequired,
|
||||
isOpen: React.PropTypes.bool.isRequired,
|
||||
onOpenToggle: React.PropTypes.func.isRequired,
|
||||
onStyleChanged: React.PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
onSourceAdd(sourceId, source) {
|
||||
const changedSources = {
|
||||
...this.props.mapStyle.sources,
|
||||
[sourceId]: source
|
||||
}
|
||||
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
sources: changedSources
|
||||
}
|
||||
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
deleteSource(sourceId) {
|
||||
const remainingSources = { ...this.props.mapStyle.sources}
|
||||
delete remainingSources[sourceId]
|
||||
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
sources: remainingSources
|
||||
}
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
onStyleChanged: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
stripTitle(source) {
|
||||
@@ -221,24 +197,26 @@ class SourcesModal extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const activeSources = Object.keys(this.props.mapStyle.sources).map(sourceId => {
|
||||
const source = this.props.mapStyle.sources[sourceId]
|
||||
const mapStyle = this.props.mapStyle
|
||||
const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
|
||||
const source = mapStyle.sources[sourceId]
|
||||
return <ActiveSourceTypeEditor
|
||||
key={sourceId}
|
||||
sourceId={sourceId}
|
||||
source={source}
|
||||
onSourceDelete={this.deleteSource.bind(this)}
|
||||
onChange={src => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
|
||||
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
|
||||
/>
|
||||
})
|
||||
|
||||
const tilesetOptions = Object.keys(publicSources).filter(sourceId => !(sourceId in this.props.mapStyle.sources)).map(sourceId => {
|
||||
const tilesetOptions = Object.keys(publicSources).filter(sourceId => !(sourceId in mapStyle.sources)).map(sourceId => {
|
||||
const source = publicSources[sourceId]
|
||||
return <PublicSource
|
||||
key={sourceId}
|
||||
id={sourceId}
|
||||
type={source.type}
|
||||
title={source.title}
|
||||
onSelect={() => this.onSourceAdd(sourceId, this.stripTitle(source))}
|
||||
onSelect={() => this.props.onStyleChanged(addSource(mapStyle, sourceId, this.stripTitle(source)))}
|
||||
/>
|
||||
})
|
||||
|
||||
@@ -248,21 +226,27 @@ class SourcesModal extends React.Component {
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Sources'}
|
||||
>
|
||||
<Heading level={4}>Active Sources</Heading>
|
||||
{activeSources}
|
||||
|
||||
<Heading level={4}>Add New Source</Heading>
|
||||
<div style={{maxWidth: 300}}>
|
||||
<p style={{color: colors.lowgray, fontSize: fontSizes[5]}}>Add a new source to your style. You can only choose the source type and id at creation time!</p>
|
||||
<AddSource onSourceAdd={this.onSourceAdd.bind(this)} />
|
||||
<div className="maputnik-modal-section">
|
||||
<h4>Active Sources</h4>
|
||||
{activeSources}
|
||||
</div>
|
||||
|
||||
<Heading level={4}>Choose Public Source</Heading>
|
||||
<Paragraph>
|
||||
Add one of the publicly availble sources to your style.
|
||||
</Paragraph>
|
||||
<div style={{maxwidth: 500}}>
|
||||
{tilesetOptions}
|
||||
<div className="maputnik-modal-section">
|
||||
<h4>Choose Public Source</h4>
|
||||
<p>
|
||||
Add one of the publicly available sources to your style.
|
||||
</p>
|
||||
<div style={{maxwidth: 500}}>
|
||||
{tilesetOptions}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="maputnik-modal-section">
|
||||
<h4>Add New Source</h4>
|
||||
<p>Add a new source to your style. You can only choose the source type and id at creation time!</p>
|
||||
<AddSource
|
||||
onAdd={(sourceId, source) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
|
||||
class TileJSONSourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
source: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"TileJSON URL"}>
|
||||
return <InputBlock label={"TileJSON URL"} doc={styleSpec.latest.source_vector.url.doc}>
|
||||
<StringInput
|
||||
value={this.props.source.url}
|
||||
onChange={url => this.props.onChange({
|
||||
@@ -24,17 +26,27 @@ class TileJSONSourceEditor extends React.Component {
|
||||
|
||||
class TileURLSourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
source: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
changeTileUrl(idx, value) {
|
||||
const tiles = this.props.source.tiles.slice(0)
|
||||
tiles[idx] = value
|
||||
this.props.onChange({
|
||||
...this.props.source,
|
||||
tiles: tiles
|
||||
})
|
||||
}
|
||||
|
||||
renderTileUrls() {
|
||||
const prefix = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th']
|
||||
const tiles = this.props.source.tiles || []
|
||||
return tiles.map((tileUrl, tileIndex) => {
|
||||
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"}>
|
||||
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"} doc={styleSpec.latest.source_vector.tiles.doc}>
|
||||
<StringInput
|
||||
value={tileUrl}
|
||||
onChange={this.changeTileUrl.bind(this, tileIndex)}
|
||||
/>
|
||||
</InputBlock>
|
||||
})
|
||||
@@ -43,21 +55,21 @@ class TileURLSourceEditor extends React.Component {
|
||||
render() {
|
||||
return <div>
|
||||
{this.renderTileUrls()}
|
||||
<InputBlock label={"Min Zoom"}>
|
||||
<InputBlock label={"Min Zoom"} doc={styleSpec.latest.source_vector.minzoom.doc}>
|
||||
<NumberInput
|
||||
value={this.props.source.minZoom}
|
||||
onChange={minZoom => this.props.onChange({
|
||||
value={this.props.source.minzoom || 0}
|
||||
onChange={minzoom => this.props.onChange({
|
||||
...this.props.source,
|
||||
minZoom: minZoom
|
||||
minzoom: minzoom
|
||||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Max Zoom"}>
|
||||
<InputBlock label={"Max Zoom"} doc={styleSpec.latest.source_vector.maxzoom.doc}>
|
||||
<NumberInput
|
||||
value={this.props.source.maxZoom}
|
||||
onChange={maxZoom => this.props.onChange({
|
||||
value={this.props.source.maxzoom || 22}
|
||||
onChange={maxzoom => this.props.onChange({
|
||||
...this.props.source,
|
||||
maxZoom: maxZoom
|
||||
maxzoom: maxzoom
|
||||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
@@ -68,12 +80,12 @@ class TileURLSourceEditor extends React.Component {
|
||||
|
||||
class GeoJSONSourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func,
|
||||
source: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"GeoJSON Data"}>
|
||||
return <InputBlock label={"GeoJSON Data"} doc={styleSpec.latest.source_geojson.data.doc}>
|
||||
<StringInput
|
||||
value={this.props.source.data}
|
||||
onChange={data => this.props.onChange({
|
||||
@@ -87,9 +99,9 @@ class GeoJSONSourceEditor extends React.Component {
|
||||
|
||||
class SourceTypeEditor extends React.Component {
|
||||
static propTypes = {
|
||||
mode: React.PropTypes.string.isRequired,
|
||||
source: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
source: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -99,8 +111,12 @@ class SourceTypeEditor extends React.Component {
|
||||
}
|
||||
switch(this.props.mode) {
|
||||
case 'geojson': return <GeoJSONSourceEditor {...commonProps} />
|
||||
case 'tilejson': return <TileJSONSourceEditor {...commonProps} />
|
||||
case 'tilexyz': return <TileURLSourceEditor {...commonProps} />
|
||||
case 'tilejson_vector': return <TileJSONSourceEditor {...commonProps} />
|
||||
case 'tilexyz_vector': return <TileURLSourceEditor {...commonProps} />
|
||||
case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} />
|
||||
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
|
||||
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
|
||||
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps} />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
const baseColors = {
|
||||
black: '#1c1f24',
|
||||
gray: '#26282e',
|
||||
midgray: '#36383e',
|
||||
lowgray: '#8e8e8e',
|
||||
|
||||
white: '#fff',
|
||||
blue: '#00d9f7',
|
||||
green: '#B4C7AD',
|
||||
orange: '#fb3',
|
||||
red: '#cf4a4a',
|
||||
}
|
||||
|
||||
const themeColors = {
|
||||
primary: baseColors.gray,
|
||||
secondary: baseColors.midgray,
|
||||
default: baseColors.gray,
|
||||
info: baseColors.blue,
|
||||
success: baseColors.green,
|
||||
warning: baseColors.orange,
|
||||
error: baseColors.red
|
||||
}
|
||||
|
||||
const colors = {
|
||||
...baseColors,
|
||||
...themeColors
|
||||
}
|
||||
|
||||
export default colors
|
||||
14
src/config/empty-style.json
Normal file
14
src/config/empty-style.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": 8,
|
||||
"name": "Empty Style",
|
||||
"metadata": {
|
||||
"mapbox:autocomposite": false,
|
||||
"mapbox:type": "template",
|
||||
"maputnik:renderer": "mbgljs",
|
||||
"openmaptiles:version": "3.x"
|
||||
},
|
||||
"sources": { },
|
||||
"glyphs": "https://demo.tileserver.org/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://demo.tileserver.org/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
[
|
||||
"Metropolis Black Italic",
|
||||
"Metropolis Black",
|
||||
"Metropolis Bold Italic",
|
||||
"Metropolis Bold",
|
||||
"Metropolis Extra Bold Italic",
|
||||
"Metropolis Extra Bold",
|
||||
"Metropolis Extra Light Italic",
|
||||
"Metropolis Extra Light",
|
||||
"Metropolis Light Italic",
|
||||
"Metropolis Light",
|
||||
"Metropolis Medium Italic",
|
||||
"Metropolis Medium",
|
||||
"Metropolis Regular Italic",
|
||||
"Metropolis Regular",
|
||||
"Metropolis Semi Bold Italic",
|
||||
"Metropolis Semi Bold",
|
||||
"Metropolis Thin Italic",
|
||||
"Metropolis Thin",
|
||||
"Open Sans Bold Italic",
|
||||
"Open Sans Bold",
|
||||
"Open Sans Extra Bold Italic",
|
||||
"Open Sans Extra Bold",
|
||||
"Open Sans Italic",
|
||||
"Open Sans Light Italic",
|
||||
"Open Sans Light",
|
||||
"Open Sans Regular",
|
||||
"Open Sans Semibold Italic",
|
||||
"Open Sans Semibold",
|
||||
"Klokantech Noto Sans Bold",
|
||||
"Klokantech Noto Sans CJK Bold",
|
||||
"Klokantech Noto Sans CJK Regular",
|
||||
"Klokantech Noto Sans Italic",
|
||||
"Klokantech Noto Sans Regular"
|
||||
]
|
||||
@@ -1,51 +0,0 @@
|
||||
import colors from './colors'
|
||||
import { margins, fontSizes } from './scales'
|
||||
|
||||
const base = {
|
||||
display: 'inline-block',
|
||||
boxSizing: 'border-box',
|
||||
fontSize: fontSizes[5],
|
||||
lineHeight: 2,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
}
|
||||
|
||||
const label = {
|
||||
...base,
|
||||
padding: null,
|
||||
color: colors.lowgray,
|
||||
userSelect: 'none',
|
||||
}
|
||||
|
||||
const property = {
|
||||
display: 'block',
|
||||
margin: margins[2],
|
||||
}
|
||||
|
||||
const input = {
|
||||
...base,
|
||||
border: 'none',
|
||||
backgroundColor: colors.gray,
|
||||
color: colors.lowgray,
|
||||
}
|
||||
|
||||
const checkbox = {
|
||||
...base,
|
||||
border: '1px solid rgb(36, 36, 36)',
|
||||
backgroundColor: colors.gray,
|
||||
color: colors.lowgray,
|
||||
}
|
||||
|
||||
const select = {
|
||||
...input,
|
||||
height: '2.15em',
|
||||
}
|
||||
|
||||
export default {
|
||||
base,
|
||||
label,
|
||||
select,
|
||||
input,
|
||||
property,
|
||||
checkbox,
|
||||
}
|
||||
@@ -2,15 +2,7 @@
|
||||
"line": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Paint",
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"line-opacity",
|
||||
@@ -18,66 +10,42 @@
|
||||
"line-width",
|
||||
"line-offset",
|
||||
"line-blur",
|
||||
"line-pattern"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Secondary",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"line-dasharray",
|
||||
"line-pattern",
|
||||
"line-translate",
|
||||
"line-translate-anchor",
|
||||
"line-cap",
|
||||
"line-join",
|
||||
"line-miter-limit",
|
||||
"line-round-limit",
|
||||
"line-dasharray",
|
||||
"line-gap-width"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
"title": "Layout properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"line-cap",
|
||||
"line-join",
|
||||
"line-miter-limit",
|
||||
"line-round-limit"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"background": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Basic",
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"background-color",
|
||||
"background-pattern",
|
||||
"background-opacity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fill": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Basic",
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"fill-opacity",
|
||||
@@ -88,25 +56,13 @@
|
||||
"fill-translate",
|
||||
"fill-translate-anchor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fill-extrusion": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Basic",
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"fill-extrusion-opacity",
|
||||
@@ -117,99 +73,52 @@
|
||||
"fill-extrusion-height",
|
||||
"fill-extrusion-base"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
}
|
||||
]
|
||||
},
|
||||
"circle": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Basic",
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"circle-color",
|
||||
"circle-opacity",
|
||||
"circle-stroke-color",
|
||||
"circle-stroke-opacity",
|
||||
"circle-blur"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Scale",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"circle-blur",
|
||||
"circle-radius",
|
||||
"circle-stroke-width",
|
||||
"circle-pitch-scale"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Position",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"circle-pitch-scale",
|
||||
"circle-translate",
|
||||
"circle-translate-anchor"
|
||||
"circle-translate-anchor",
|
||||
"circle-pitch-alignment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
}
|
||||
]
|
||||
},
|
||||
"symbol": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Settings",
|
||||
"type": "settings"
|
||||
},
|
||||
{
|
||||
"title": "Source",
|
||||
"type": "source"
|
||||
},
|
||||
{
|
||||
"title": "Basic",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"text-field",
|
||||
"text-font",
|
||||
"text-color",
|
||||
"text-size",
|
||||
"text-line-height",
|
||||
"text-halo-color",
|
||||
"text-halo-width",
|
||||
"text-halo-blur"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Placement",
|
||||
"title": "General layout properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"symbol-placement",
|
||||
"symbol-spacing",
|
||||
"text-padding",
|
||||
"symbol-avoid-edges",
|
||||
"text-allow-overlap",
|
||||
"text-ignore-placement",
|
||||
"text-translate",
|
||||
"text-translate-anchor"
|
||||
"symbol-avoid-edges"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Text",
|
||||
"title": "Text layout properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"text-field",
|
||||
"text-font",
|
||||
"text-size",
|
||||
"text-line-height",
|
||||
"text-padding",
|
||||
"text-allow-overlap",
|
||||
"text-ignore-placement",
|
||||
"text-pitch-alignment",
|
||||
"text-rotation-alignment",
|
||||
"text-max-width",
|
||||
@@ -225,7 +134,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Icon",
|
||||
"title": "Icon layout properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"icon-allow-overlap",
|
||||
@@ -239,12 +148,83 @@
|
||||
"icon-rotate",
|
||||
"icon-padding",
|
||||
"icon-keep-upright",
|
||||
"icon-offset"
|
||||
"icon-offset",
|
||||
"icon-anchor",
|
||||
"icon-pitch-alignment"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "JSON",
|
||||
"type": "jsoneditor"
|
||||
"title": "Text paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"text-color",
|
||||
"text-opacity",
|
||||
"text-halo-color",
|
||||
"text-halo-width",
|
||||
"text-halo-blur",
|
||||
"text-translate",
|
||||
"text-translate-anchor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Icon paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"icon-color",
|
||||
"icon-opacity",
|
||||
"icon-halo-color",
|
||||
"icon-halo-width",
|
||||
"icon-halo-blur",
|
||||
"icon-translate",
|
||||
"icon-translate-anchor"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"raster": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"raster-opacity",
|
||||
"raster-hue-rotate",
|
||||
"raster-brightness-min",
|
||||
"raster-brightness-max",
|
||||
"raster-saturation",
|
||||
"raster-contrast",
|
||||
"raster-fade-duration"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"hillshade": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"hillshade-illumination-direction",
|
||||
"hillshade-illumination-anchor",
|
||||
"hillshade-exaggeration",
|
||||
"hillshade-shadow-color",
|
||||
"hillshade-highlight-color",
|
||||
"hillshade-accent-color"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"heatmap": {
|
||||
"groups": [
|
||||
{
|
||||
"title": "Paint properties",
|
||||
"type": "properties",
|
||||
"fields": [
|
||||
"heatmap-radius",
|
||||
"heatmap-weight",
|
||||
"heatmap-intensity",
|
||||
"heatmap-opacity"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export const margins = [3, 5, 10, 30, 40]
|
||||
export const fontSizes = [24, 20, 18, 16, 14, 12]
|
||||
@@ -2,31 +2,55 @@
|
||||
{
|
||||
"id": "klokantech-basic",
|
||||
"title": "Klokantech Basic",
|
||||
"url": "https://rawgit.com/openmaptiles/klokantech-basic-gl-style/gh-pages/style-cdn.json",
|
||||
"thumbnail": "https://camo.githubusercontent.com/5cf548fdb9fc606f4a452d14fd2a7a959155fd40/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6d6f7267656e6b61666665652f63697578757465726630316135326971716f366b6f6c776b312f7374617469632f382e3534303538372c34372e3337303535352c31342e30382c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f69625739795a3256756132466d5a6d566c4969776959534936496a497a636d4e304e6c6b6966512e304c52544e6743632d656e76743964354d7a52373577"
|
||||
"url": "https://rawgit.com/openmaptiles/klokantech-basic-gl-style/master/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png"
|
||||
},
|
||||
{
|
||||
"id": "dark-matter",
|
||||
"title": "Dark Matter",
|
||||
"url": "https://rawgit.com/openmaptiles/dark-matter-gl-style/gh-pages/style-cdn.json",
|
||||
"thumbnail": "https://camo.githubusercontent.com/b73c515d633d2be7368e8e29e3c23e14117fd21b/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6d6f7267656e6b61666665652f6369757878356e37683031396c326870626e396c6970726d6e2f7374617469632f382e3631393138342c34372e3333363230332c392e30372c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f69625739795a3256756132466d5a6d566c4969776959534936496a497a636d4e304e6c6b6966512e304c52544e6743632d656e76743964354d7a52373577"
|
||||
"url": "https://rawgit.com/openmaptiles/dark-matter-gl-style/master/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
|
||||
},
|
||||
{
|
||||
"id": "positron",
|
||||
"title": "Positron",
|
||||
"url": "https://rawgit.com/openmaptiles/positron-gl-style/gh-pages/style-cdn.json",
|
||||
"thumbnail": "https://camo.githubusercontent.com/0dd866e3fa7b21ada87da69082eac6801e16ec99/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6d6f7267656e6b61666665652f63697578756e37736530313976326a6c387162326a743374662f7374617469632f382e3631393138342c34372e3333363230332c392e30372c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f69625739795a3256756132466d5a6d566c4969776959534936496a497a636d4e304e6c6b6966512e304c52544e6743632d656e76743964354d7a52373577"
|
||||
"url": "https://rawgit.com/openmaptiles/positron-gl-style/master/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png"
|
||||
},
|
||||
{
|
||||
"id": "osm-bright",
|
||||
"title": "OSM Bright",
|
||||
"url": "https://rawgit.com/openmaptiles/osm-bright-gl-style/gh-pages/style-cdn.json",
|
||||
"thumbnail": "https://camo.githubusercontent.com/a15e23ab59202c56502e57cde963cb7772ed3bb1/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6f70656e6d617074696c65732f63697736637a7a326e30303234326b6d673668773230626f782f7374617469632f382e3534303538372c34372e3337303535352c31342e30382c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f696233426c626d3168634852706247567a4969776959534936496d4e70646e593365544a785a7a41774d474d796233427064574a6d616a63784e7a636966512e685031427863786c644968616b4d6350534a4c513151"
|
||||
"url": "https://rawgit.com/openmaptiles/osm-bright-gl-style/master/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png"
|
||||
},
|
||||
{
|
||||
"id": "fiord-color",
|
||||
"title": "Fiord Color",
|
||||
"url": "https://rawgit.com/openmaptiles/fiord-color-gl-style/gh-pages/style-cdn.json",
|
||||
"thumbnail": "https://camo.githubusercontent.com/605f2edc30e413b37d16a6ca1d500f265725d76d/68747470733a2f2f6170692e6d6170626f782e636f6d2f7374796c65732f76312f6f70656e6d617074696c65732f6369776775693378353030317732706e7668633063327767302f7374617469632f31302e3938373235382c34362e3435333135302c332e30322c302e30302c302e30302f363030783430303f6163636573735f746f6b656e3d706b2e65794a31496a6f696233426c626d3168634852706247567a4969776959534936496d4e70646e593365544a785a7a41774d474d796233427064574a6d616a63784e7a636966512e685031427863786c644968616b4d6350534a4c513151"
|
||||
"id": "osm-liberty",
|
||||
"title": "OSM Liberty",
|
||||
"url": "https://rawgit.com/lukasmartinelli/osm-liberty/gh-pages/style.json",
|
||||
"thumbnail": "https://cdn.rawgit.com/lukasmartinelli/osm-liberty/gh-pages/thumbnail.png"
|
||||
},
|
||||
{
|
||||
"id": "empty-style",
|
||||
"title": "Empty Style",
|
||||
"url": "https://rawgit.com/maputnik/editor/master/src/config/empty-style.json",
|
||||
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII="
|
||||
},
|
||||
{
|
||||
"id": "mapbox-satellite",
|
||||
"title": "Mapbox Satellite",
|
||||
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/satellite-v9.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/mapbox-satellite.png"
|
||||
},
|
||||
{
|
||||
"id": "mapbox-bright",
|
||||
"title": "Mapbox Bright",
|
||||
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/bright-v9.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/mapbox-bright.png"
|
||||
},
|
||||
{
|
||||
"id": "mapbox-basic",
|
||||
"title": "Mapbox Basic",
|
||||
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/basic-v9.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/mapbox-basic.png"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import colors from './colors'
|
||||
import { margins, fontSizes } from './scales'
|
||||
|
||||
const dark = {
|
||||
color: colors.white,
|
||||
fontFamily: 'Roboto, sans-serif',
|
||||
}
|
||||
|
||||
export default dark
|
||||
@@ -4,23 +4,9 @@
|
||||
"url": "mapbox://mapbox.mapbox-streets-v7",
|
||||
"title": "Mapbox Streets"
|
||||
},
|
||||
"tilezen": {
|
||||
"type": "vector",
|
||||
"tiles": [
|
||||
"http://tile.mapzen.com/mapzen/vector/v1/{layers}/{z}/{x}/{y}.pbf?api_key=mapzen-RVcyVL7"
|
||||
],
|
||||
"minZoom": 0,
|
||||
"maxZoom": 15,
|
||||
"title": "Mapzen Vector Tile Service"
|
||||
},
|
||||
"openmaptiles": {
|
||||
"type": "vector",
|
||||
"url": "https://free.tilehosting.com/data/v3.json?key=25ItXg7aI5wurYDtttD",
|
||||
"title": "OpenMapTiles CDN"
|
||||
},
|
||||
"naturalearth-airports": {
|
||||
"type": "geojson",
|
||||
"data": "https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson",
|
||||
"title": "NaturalEarth Airports GeoJSON"
|
||||
"url": "https://free.tilehosting.com/data/v3.json?key={key}",
|
||||
"title": "OpenMapTiles"
|
||||
}
|
||||
}
|
||||
|
||||
4
src/config/tokens.json
Normal file
4
src/config/tokens.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"mapbox": "pk.eyJ1IjoibW9yZ2Vua2FmZmVlIiwiYSI6ImNpeHJmNXNmZTAwNHIycXBid2NqdTJibjMifQ.Dv1-GDpTWi0NP6xW9Fct1w",
|
||||
"openmaptiles": "Og58UhhtiiTaLVlPtPgs"
|
||||
}
|
||||
BIN
src/fonts/Roboto-Medium.ttf
Normal file
BIN
src/fonts/Roboto-Medium.ttf
Normal file
Binary file not shown.
@@ -1,34 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('./fonts/Roboto-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: rgb(28, 31, 36);
|
||||
}
|
||||
|
||||
.chrome-picker {
|
||||
background-color: #1c1f24 !important;
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
.chrome-picker input {
|
||||
background-color: rgb(38, 40, 46) !important;
|
||||
color: rgb(142, 142, 142) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
background-color: #26282e;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 6px;
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
|
||||
background-color: #40444e;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import './favicon.ico'
|
||||
import './index.css'
|
||||
import './styles/index.scss'
|
||||
import App from './components/App';
|
||||
|
||||
ReactDOM.render(<App/>, document.querySelector("#app"));
|
||||
|
||||
@@ -1,40 +1,66 @@
|
||||
import request from 'request'
|
||||
import style from './style.js'
|
||||
import ReconnectingWebSocket from 'reconnecting-websocket'
|
||||
|
||||
const host = 'localhost'
|
||||
const port = '8000'
|
||||
const localUrl = `http://${host}:${port}`
|
||||
const websocketUrl = `ws://${host}:${port}/ws`
|
||||
|
||||
|
||||
export class ApiStyleStore {
|
||||
supported(cb) {
|
||||
request('http://localhost:8000/styles', (error, response, body) => {
|
||||
cb(error === undefined)
|
||||
constructor(opts) {
|
||||
this.onLocalStyleChange = opts.onLocalStyleChange || (() => {})
|
||||
}
|
||||
|
||||
init(cb) {
|
||||
request(localUrl + '/styles', (error, response, body) => {
|
||||
if (!error && body && response.statusCode == 200) {
|
||||
const styleIds = JSON.parse(body)
|
||||
this.latestStyleId = styleIds[0]
|
||||
this.notifyLocalChanges()
|
||||
cb(null)
|
||||
} else {
|
||||
cb(new Error('Can not connect to style API'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
notifyLocalChanges() {
|
||||
const connection = new ReconnectingWebSocket(websocketUrl)
|
||||
connection.onmessage = e => {
|
||||
if(!e.data) return
|
||||
console.log('Received style update from API')
|
||||
let parsedStyle = style.emptyStyle
|
||||
try {
|
||||
parsedStyle = JSON.parse(e.data)
|
||||
} catch(err) {
|
||||
console.error(err)
|
||||
}
|
||||
const updatedStyle = style.ensureStyleValidity(parsedStyle)
|
||||
this.onLocalStyleChange(updatedStyle)
|
||||
}
|
||||
}
|
||||
|
||||
latestStyle(cb) {
|
||||
if(this.latestStyleId) {
|
||||
request('http://localhost:8000/styles/' + this.latestStyleId, (error, response, body) => {
|
||||
cb(JSON.parse(body))
|
||||
request(localUrl + '/styles/' + this.latestStyleId, (error, response, body) => {
|
||||
cb(style.ensureStyleValidity(JSON.parse(body)))
|
||||
})
|
||||
} else {
|
||||
request('http://localhost:8000/styles', (error, response, body) => {
|
||||
if (!error && response.statusCode == 200) {
|
||||
const styleIds = JSON.parse(body);
|
||||
this.latestStyleId = styleIds[0];
|
||||
request('http://localhost:8000/styles/' + this.latestStyleId, (error, response, body) => {
|
||||
cb(style.fromJSON(JSON.parse(body)))
|
||||
})
|
||||
}
|
||||
})
|
||||
throw new Error('No latest style available. You need to init the api backend first.')
|
||||
}
|
||||
}
|
||||
|
||||
// Save current style replacing previous version
|
||||
save(mapStyle) {
|
||||
const id = mapStyle.get('id')
|
||||
const id = mapStyle.id
|
||||
request.put({
|
||||
url: 'http://localhost:8000/styles/' + id,
|
||||
url: localUrl + '/styles/' + id,
|
||||
json: true,
|
||||
body: style.toJSON(mapStyle)
|
||||
body: mapStyle
|
||||
}, (error, response, body) => {
|
||||
console.log('Saved style');
|
||||
if(error) console.error(error)
|
||||
})
|
||||
return mapStyle
|
||||
}
|
||||
|
||||
44
src/libs/debug.js
Normal file
44
src/libs/debug.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import querystring from 'querystring'
|
||||
|
||||
|
||||
const debugStore = {};
|
||||
|
||||
function enabled() {
|
||||
const qs = querystring.parse(window.location.search.slice(1));
|
||||
if(qs.hasOwnProperty("debug")) {
|
||||
return !!qs.debug.match(/^(|1|true)$/);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function genErr() {
|
||||
return new Error("Debug not enabled, enable by appending '?debug' to your query string");
|
||||
}
|
||||
|
||||
function set(namespace, key, value) {
|
||||
if(!enabled()) {
|
||||
throw genErr();
|
||||
}
|
||||
debugStore[namespace] = debugStore[namespace] || {};
|
||||
debugStore[namespace][key] = value;
|
||||
}
|
||||
|
||||
function get(namespace, key) {
|
||||
if(!enabled()) {
|
||||
throw genErr();
|
||||
}
|
||||
if(debugStore.hasOwnProperty(namespace)) {
|
||||
return debugStore[namespace][key];
|
||||
}
|
||||
}
|
||||
|
||||
const mod = {
|
||||
enabled,
|
||||
get,
|
||||
set
|
||||
}
|
||||
|
||||
window.debug = mod;
|
||||
export default mod;
|
||||
@@ -1,7 +1,7 @@
|
||||
import diffStyles from 'mapbox-gl-style-spec/lib/diff'
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
|
||||
export function diffMessages(beforeStyle, afterStyle) {
|
||||
const changes = diffStyles(beforeStyle, afterStyle)
|
||||
const changes = styleSpec.diff(beforeStyle, afterStyle)
|
||||
return changes.map(cmd => cmd.command + ' ' + cmd.args.join(' '))
|
||||
}
|
||||
|
||||
|
||||
9
src/libs/document-uid.js
Normal file
9
src/libs/document-uid.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* A unique id for the current document.
|
||||
*/
|
||||
let REF = 0;
|
||||
|
||||
export default function(prefix="") {
|
||||
REF++;
|
||||
return prefix+REF;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user