Compare commits

...

207 Commits

Author SHA1 Message Date
orangemug
59ef8eb4e4 v1.1.0-beta 2018-01-18 22:19:05 +00:00
Orange Mug
2b382a9946 Merge pull request #217 from orangemug/pr-214
Updated to mapbox-gl@0.43.0
2018-01-18 21:48:31 +00:00
orangemug
d52d55dd6a Updated to mapbox-gl-js v0.43.0. Fixes issue #212 2018-01-18 21:22:18 +00:00
Tobin Bradley
ed495c3216 Update export to use GL JS 0.43.0
Fixes #212 broken bl.ocks preview.
2018-01-12 11:37:05 -05:00
Orange Mug
6e86c60f89 Merge pull request #185 from gregorywolanski/130
Add expand/collapse all layer groups feature (#130)
2018-01-04 15:52:38 +00:00
Grzegorz Wolański
604fa6317c Expand/Collapse button position bug fix 2017-11-30 20:40:56 +01:00
Orange Mug
4479473b37 Merge pull request #207 from orangemug/fix/update-codemirror
Update to react-codemirror2
2017-11-30 09:14:54 +00:00
Orange Mug
4dc8fc9696 Merge pull request #205 from orangemug/fix/fetch-sources-get-called-on-each-change
Fix to stop fetchSources getting called on each change
2017-11-30 07:31:32 +00:00
Orange Mug
bac59d595d Merge pull request #204 from orangemug/fix/catch-invalid-color
Catch invalid color
2017-11-29 21:58:06 +00:00
Orange Mug
ed98db8ae3 Merge pull request #199 from orangemug/fix/issue-97-layer-list-cutoff
Fixed layer list cutoff (#97)
2017-11-29 19:30:19 +00:00
Orange Mug
b66eb66358 Merge pull request #201 from orangemug/fix/lighthouse-errors
Google lighthouse audit fixes
2017-11-29 19:29:23 +00:00
orangemug
934a994ac5 Update to react-codemirror2
react-codemirror is no longer maintained
2017-11-29 19:22:41 +00:00
orangemug
199a989f7d Fix to stop fetchSources getting called on each change. 2017-11-29 18:13:47 +00:00
orangemug
a50b09e5a2 Tidy logic. 2017-11-29 18:07:03 +00:00
orangemug
b20c69b15a Catch invalid color during parse. 2017-11-29 18:00:52 +00:00
Yuri Astrakhan
25be173487 Merge pull request #200 from orangemug/fix/issue-116-zoom-field-v3
Fix zoom field input ordering (#116)
2017-11-29 10:50:18 -05:00
orangemug
61808d5939 Fixed lint errors. 2017-11-29 15:03:37 +00:00
orangemug
de24227b1f Updated package-lock.json 2017-11-29 11:34:08 +00:00
orangemug
1f5608ec77 Added manifest.json to production webpack config. 2017-11-29 11:29:48 +00:00
orangemug
2d87e162f1 Merge remote-tracking branch 'upstream/master' into fix/lighthouse-errors 2017-11-29 11:14:54 +00:00
orangemug
1941fdf8a0 Merge remote-tracking branch 'upstream/master' into fix/issue-116-zoom-field-v3 2017-11-29 10:56:56 +00:00
orangemug
33fdc52667 Added MAX_HEIGHT constant. 2017-11-29 10:29:11 +00:00
orangemug
e11a5a823a Only limit AutoComplete to window bounds if element is fixed. 2017-11-29 10:20:07 +00:00
orangemug
b60d101d42 Fixed PropTypes typo. 2017-11-29 10:19:22 +00:00
orangemug
5e9263b787 Merge remote-tracking branch 'upstream/master' into fix/issue-97-layer-list-cutoff 2017-11-29 10:09:56 +00:00
Orange Mug
949bd783f5 Merge pull request #195 from orangemug/feature/webpack-bundle-analyzer
Webpack bundle analyzer
2017-11-28 08:58:23 +00:00
Orange Mug
7fe3137fd0 Merge pull request #196 from orangemug/fix/source-layer-autocomplete
Improved source/layer autocomplete
2017-11-28 08:57:24 +00:00
orangemug
3c97fbe587 tabs -> spaces. 2017-11-17 17:27:16 +00:00
orangemug
030d469d7c Broke <FunctionSpecField/> into smaller parts. 2017-11-17 17:17:53 +00:00
orangemug
135ef8ed89 Merge remote-tracking branch 'upstream/master' into fix/issue-116-zoom-field-v2 2017-11-17 13:23:48 +00:00
orangemug
002e9c4647 Fix for new sources definition. 2017-11-17 13:06:26 +00:00
orangemug
a4fbe55012 Added type to sources list and now filtering in modal autocomplete. 2017-11-17 11:43:56 +00:00
orangemug
63ac707415 Call fetchSources after component mount. 2017-11-17 10:53:46 +00:00
orangemug
b5dc04bb4f Merge remote-tracking branch 'upstream/master' into fix/source-layer-autocomplete 2017-11-17 10:41:23 +00:00
orangemug
f3ae20f3aa Updated package-lock. 2017-11-17 10:26:49 +00:00
orangemug
1838b8aefd Merge remote-tracking branch 'upstream/master' into feature/webpack-bundle-analyzer
Conflicts:
	config/webpack.production.config.js
2017-11-17 10:22:28 +00:00
Orange Mug
e9c65e1ada Merge pull request #193 from orangemug/fix/tooltip-pointer-events
Disable pointer-events on doc tooltips
2017-11-16 16:20:52 +00:00
orangemug
9ea5d213f7 Merge remote-tracking branch 'upstream/master' into fix/tooltip-pointer-events 2017-11-16 13:06:40 +00:00
Orange Mug
7dcd6d5552 Merge pull request #192 from orangemug/fix/update-deps
Updated dependencies
2017-11-16 13:04:56 +00:00
orangemug
0de8f2d633 Moved node.js test versions to match latest LTS
https://github.com/nodejs/Release
2017-11-15 14:45:50 +00:00
orangemug
cb2f854dd5 Sub-dependencies broke their API without a major version bump.
This fixes those errors and adds a package-lock.json
2017-11-15 14:29:56 +00:00
orangemug
401c920e47 Fix to keep autocomplete menu within window bounds. 2017-11-08 15:44:43 +00:00
orangemug
40235fe473 Initial work to reorder zoom fields. 2017-11-08 13:45:34 +00:00
orangemug
a76e08aee7 Fixed source layer autocomplete to fetch from the sources json definition 2017-11-08 11:11:36 +00:00
orangemug
dfe7282510 Fixed some errors reported in google lighthouse audit tool. 2017-11-08 10:13:02 +00:00
orangemug
3aae2e976f Merge remote-tracking branch 'upstream/master' into fix/update-deps
Conflicts:
	src/components/Toolbar.jsx
2017-11-08 09:01:35 +00:00
orangemug
f79a945fa4 Fixed more eslint errors. 2017-11-08 08:51:24 +00:00
orangemug
8234c51412 Fixed eslint errors. 2017-11-08 08:47:36 +00:00
orangemug
f464f997d1 Added 'pointer-events: none' to prevent tooltips from overlapping the UI. 2017-11-07 18:29:55 +00:00
orangemug
e0b7cdf9dd Removed nodejs v4 tests temporarily 2017-11-07 18:20:35 +00:00
orangemug
a819154145 Moved to testing from webpack.production.config also added verbose logging. 2017-11-07 18:13:06 +00:00
Orange Mug
616f45c586 Merge pull request #191 from orangemug/fix/console-errors
Fixed startup errors/warnings
2017-11-07 15:05:16 +00:00
orangemug
203aaf51b7 Removed old plugins from webpack config. 2017-11-07 11:50:00 +00:00
orangemug
392d1fe26d Added webpack-bundle-analyzer 2017-11-07 11:48:01 +00:00
orangemug
f452ea0d26 Fixed webpack production conf after updated deps.
See <https://webpack.js.org/guides/migrating/#occurrenceorderplugin-is-now-on-by-default>
2017-11-07 11:35:46 +00:00
Orange Mug
97dbb74486 Merge pull request #189 from orangemug/feature/disable-polling-by-default
Disable polling by default
2017-11-07 11:15:50 +00:00
Orange Mug
1f80cfcaa6 Merge pull request #190 from orangemug/feature/overflow-toolbar-actions
Added css overflow scroll to toolbar actions
2017-11-07 11:14:16 +00:00
orangemug
5d0fbabb6a Updated mocha dep. 2017-11-07 11:07:44 +00:00
orangemug
b5ca0fa17b Updated webpack deps. 2017-11-07 11:05:30 +00:00
orangemug
41e1704d08 Updated extract-text-webpack-plugin 2017-11-07 10:56:08 +00:00
orangemug
d4569237f5 Changed es2015 to env in babelrc. 2017-11-07 10:55:25 +00:00
orangemug
b6ae51b5e5 Updated eslint deps. 2017-11-07 10:52:02 +00:00
orangemug
3015ba605d Switched to babel-preset-env from babel-preset-es2015
See <http://babeljs.io/env>
2017-11-07 10:48:15 +00:00
orangemug
eb589d4039 Updated the webpack loader deps. 2017-11-07 10:44:52 +00:00
orangemug
271190f434 Updated more babel deps.
babel-loader fixed at 7.1.1 due to <https://github.com/babel/babel-loader/issues/505>
2017-11-07 10:41:40 +00:00
orangemug
0836790daf Upgraded babel dev deps. 2017-11-07 10:34:08 +00:00
orangemug
b3b665fcb9 Bumped react/react-dom deps. 2017-11-07 10:25:24 +00:00
orangemug
c050b02b8b Updated '@mapbox/mapbox-gl-style-spec' 2017-11-07 10:21:39 +00:00
orangemug
a791403a6a Updated deps fixed for clean install. 2017-11-07 10:11:42 +00:00
orangemug
a4c6a18353 Updated react-sortable-hoc to 0.6.8 to remove prop-types warnings. 2017-11-06 15:35:29 +00:00
orangemug
9bc603a510 Update to use prop-types module in components. 2017-11-06 15:32:04 +00:00
orangemug
af25fb926b Bumped react-codemirror to 1.0.0 2017-11-06 15:12:51 +00:00
orangemug
365a0518a5 Removed the console.warn because the logging wasn't helpful. 2017-11-06 15:05:00 +00:00
orangemug
9801f49f4e Added noParse for prebuilt openlayers and mapbox-gl modules. 2017-11-06 14:58:23 +00:00
orangemug
bb4f3482ad Removed required from minzoom/maxzoom as it can be undefined.
See <https://www.mapbox.com/mapbox-gl-js/style-spec/#layer-minzoom>
2017-11-06 14:45:03 +00:00
orangemug
e148607c7a Removed required prop and fixed component name. 2017-11-06 14:13:45 +00:00
orangemug
ae370f04c1 Added css overflow scroll to toolbar actions. 2017-11-06 10:23:51 +00:00
orangemug
89f6343abd Removed ignoring node_modules in webpack watch 2017-11-04 14:24:36 +00:00
orangemug
ea55687171 Added note to the docs regarding WEBPACK_DEV_SERVER_POLLING 2017-11-03 11:12:23 +00:00
orangemug
da0b4d7911 Disable webpack-dev-server polling by default. 2017-11-03 11:04:15 +00:00
Orange Mug
e303283098 Merge pull request #187 from orangemug/feature/more-badges
Added more badges
2017-11-02 13:01:30 +00:00
orangemug
1119ff06c9 Added more badges. 2017-11-01 16:54:17 +00:00
Orange Mug
adc8ed26c1 Merge pull request #179 from gregorywolanski/87
Display zoom level #87
2017-10-30 08:00:34 +00:00
Gregory Wolanski
06554b83dc Add expand/collapse all layer groups feature (#130) 2017-10-25 21:35:19 +02:00
Gregory Wolanski
06ea1d1697 Display zoom level #87 2017-10-19 20:41:38 +02:00
Orange Mug
ddb3bcde43 Merge pull request #178 from pjsier/fix/layer-function-type
Assigning default function type from spec
2017-10-18 17:31:13 +01:00
Orange Mug
db2f9efb93 Merge pull request #176 from orangemug/fix/noopener-noreferrer
Added rel="noopener noreferrer" to external links.
2017-10-17 16:43:48 +01:00
pjsier
d32b15d425 Assigning default function type from spec 2017-10-16 14:19:19 -05:00
Orange Mug
a67f9b2edb Merge pull request #175 from orangemug/feature/change-link-to-new-domain
Changed link from maputnik.com -> maputnik.github.io
2017-10-16 19:48:13 +01:00
orangemug
c38547d4e7 Removed {} from props. 2017-10-16 15:18:29 +01:00
orangemug
3f350c30da Added rel="noopener noreferrer" to external links. 2017-10-16 15:01:35 +01:00
Orange Mug
d502d9b1bb Merge pull request #174 from pjsier/feature/share-style-link
Add share style link, copy button
2017-10-16 14:55:12 +01:00
orangemug
06e1be716e Changed link from maputnik.com -> maputnik.github.io 2017-10-16 09:34:05 +01:00
pjsier
cda855f1b7 Add share style link, copy button
This adds a copy to clipboard button and input with the style parameter
pre-populated after exporting a style to an anonymous gist. Also
includes the URL as an input next to the button.
2017-10-15 20:04:43 -05:00
Orange Mug
36def799c0 Merge pull request #172 from pjsier/fix/insecure-assets
Fix insecure asset loading
2017-10-14 20:08:19 +01:00
pjsier
2e671250b9 Remove tilezen tileset and style 2017-10-12 15:13:57 -05:00
pjsier
c881534554 Fix insecure asset loading 2017-10-12 14:33:24 -05:00
Orange Mug
e1f7336aa9 Merge pull request #166 from orangemug/feature/load-from-url
Added 'Load from URL' option in open modal
2017-10-12 19:14:31 +01:00
orangemug
aa92e9da02 Merge remote-tracking branch 'upstream/master' into feature/load-from-url 2017-10-12 15:15:20 +01:00
Orange Mug
232b48ff62 Merge pull request #165 from gregorywolanski/master
Modal scrolling #156
2017-10-12 10:29:07 +01:00
Grzegorz Wolański
a95b2932db Modal scrolling #156: Issue fixed 2017-10-11 23:11:40 +02:00
Orange Mug
aa288a1e11 Merge pull request #161 from pjsier/feature/data-driven-styles
Add data-driven styling
2017-10-11 16:55:04 +01:00
Orange Mug
7e6efcb9b9 Merge pull request #169 from orangemug/feature/comments-field-docs
Added comments field doc and updated react-collapse to fix styling
2017-10-11 14:37:43 +01:00
Orange Mug
817d0a7e63 Merge pull request #170 from orangemug/fix/maputnik-logo-stretch
Fixes logo styling in toolbar
2017-10-11 14:34:43 +01:00
pjsier
fa0067ce7b Update mapbox deps, clarify data prop scope 2017-10-11 08:01:55 -05:00
orangemug
9beacf7ef3 Fixed image path in test 2017-10-11 13:16:04 +01:00
pjsier
b4292028c2 Fix default field bug 2017-10-11 05:58:32 -05:00
orangemug
d7c099bcbb Fixed logo stying in toolbar, also switched to the logo in github:maputnik/design. 2017-10-11 11:17:02 +01:00
Orange Mug
36cd15f4f1 Merge pull request #168 from orangemug/feature/issue-114-boolean-types
Added true/false conversion to filter field
2017-10-11 10:27:43 +01:00
orangemug
92ff1a8499 Added comments field doc and updated react-collapse to fix styling. 2017-10-10 22:34:16 +01:00
pjsier
4af7a71220 Rename ZoomSpecField to FunctionSpecField 2017-10-10 14:23:20 -05:00
orangemug
611e170b5e Added true/false conversion to filter fields. Fixes #114 2017-10-10 18:41:54 +01:00
pjsier
148f64c261 Restrict data function types, reorder buttons
Checking the Mapbox style spec properties to see whether or not
exponential should be allowed as the property type, defaulting to
categorical which appears to work for either type. Also re-orders zoom
and data function buttons, aligning zoom right if data not supplied.
2017-10-10 10:30:06 -05:00
orangemug
2c3f47d3cb Added 'Load from URL' option in open modal. Fixes #120 2017-10-10 16:27:16 +01:00
Gregory Wolanski
8a6e24e5e7 Modal scrolling #156 2017-10-08 21:42:04 +02:00
pjsier
1d29f67065 Check for property-function support on data styles 2017-10-05 06:08:55 -05:00
pjsier
2ffb3e73e1 Re-add default field after style update 2017-10-05 05:50:47 -05:00
pjsier
bba7aa3177 Merge branch 'master' into feature/data-driven-styles 2017-10-05 05:42:17 -05:00
Orange Mug
c950a33031 Merge pull request #124 from orangemug/fix/issue-110-update-mapbox-style-spec
Update style spec
2017-10-05 06:16:04 +01:00
Orange Mug
c9ab3bdbfc Merge pull request #163 from chriswhong/patch-1
Add related projects section
2017-10-05 06:00:36 +01:00
Chris Whong
e32c2e865c Add 2017-10-04 22:42:04 -04:00
pjsier
9e52b0b7dc Remove default from data properties
It looks like default is not supported in this version of the style
spec, so pending the PR to update it I'm removing it as an input.
2017-10-01 21:07:55 -05:00
pjsier
d731fb2cae Fix scss linter errors 2017-10-01 19:17:43 -05:00
pjsier
e057fcaea1 Add data-driven styling to editor
Builds off of the ZoomSpecField component with separate options for
handling data-driven properties. Reuses most of the zoom field
functionality with tweaks that I tried to keep as small as possible, and
the layout is based off of comments on the existing issue.
2017-10-01 18:20:57 -05:00
Orange Mug
fff1363134 Merge pull request #152 from orangemug/feature/circle-ci-artifact-builds
Per branch / commit builds
2017-07-22 13:43:47 +01:00
orangemug
4bbfe1040e Fixed regexp for windows. 2017-07-11 06:30:37 +01:00
Orange Mug
bc6e2dc81b Merge pull request #149 from bartvde/proptype-warnings
Make sure propTypes accept what gets passed in
2017-07-07 09:07:25 +01:00
orangemug
0005698c10 Added build to post test step. 2017-07-06 19:59:12 +01:00
orangemug
53711966d2 Added circleci build artifacts. 2017-07-06 19:53:39 +01:00
Orange Mug
d3b991aad4 Merge pull request #146 from bartvde/babel-preset
Add transform-class-properties plugin to babel config
2017-07-06 19:34:26 +01:00
Orange Mug
4ef19c321d Merge pull request #151 from bartvde/key-warning
Put key on the right element
2017-07-05 19:32:28 +01:00
bartvde
a3e3b9dfe3 Put key on the right element 2017-06-29 10:20:24 -05:00
bartvde
abbce3e9d1 Make sure propTypes accept what gets passed in 2017-06-26 16:54:46 -05:00
bartvde
0edbfd89ff Add transform-class-properties plugin to babel config 2017-06-26 16:24:07 -05:00
Orange Mug
040d585d57 Merge pull request #133 from tschaub/loader
Animated loading indicator
2017-06-23 17:46:05 +01:00
Orange Mug
c74ef7b0d3 Merge pull request #134 from tschaub/close-open-dialog
Close the "open" dialog after choosing a new style
2017-06-23 17:38:07 +01:00
Orange Mug
23ef937100 Merge pull request #139 from tbarsballe/dynamic-arrays
Add DynamicArrayInput for handling variable-length array fields
2017-06-23 17:35:42 +01:00
Orange Mug
5157742009 Merge pull request #141 from tbarsballe/autocomplete-style
Improve autocomplete styling
2017-06-23 17:22:02 +01:00
Orange Mug
96d96edc9e Merge pull request #145 from bartvde/style-warning
Prevent warning for ColorField
2017-06-23 17:03:53 +01:00
Orange Mug
2a10edcc25 Merge pull request #144 from bartvde/babelrc
Move babel config out of package.json
2017-06-23 16:47:47 +01:00
bartvde
e4477db413 Prevent warning for ColorField 2017-06-23 16:00:43 +02:00
bartvde
b32d926b56 Move babel config out of package.json 2017-06-23 15:47:15 +02:00
Lukas Martinelli
6b3b5a8b6f Merge pull request #142 from tbarsballe/ol
Improved OpenLayers support
2017-06-09 13:19:36 -04:00
Tim Schaub
a7df8afd6e Animated loading indicator 2017-06-02 15:12:21 -06:00
Tim Schaub
b8205f4c38 Close the "open" dialog after choosing a new style 2017-06-02 15:11:29 -06:00
Torben Barsballe
2adb1bf917 Add DynamicArrayInput for handling variable-length array fields 2017-05-29 14:32:05 -07:00
Torben Barsballe
2825dd7e04 Improve autocomplete styling
Use "position: absolute" so that autocomplete follows the field when you scroll.
2017-05-29 13:22:50 -07:00
Torben Barsballe
df04064e81 Improved OpenLayers support
Added support for gejson sources
Fix toVectorLayer map callback (this was undefined)
Improved ol css; show map controls
2017-05-29 13:18:09 -07:00
Lukas Martinelli
0555fc48ad Merge pull request #140 from tbarsballe/ci-fix
Update react-collapse to ^4.0.2
2017-05-26 14:10:53 -04:00
Torben Barsballe
cd425bd26d Update react-collapse to ^4.0.2 2017-05-26 09:30:27 -07:00
Orange Mug
a98444b4e7 Merge pull request #135 from orangemug/fix/react-height-peer-dep
Update react-height peer dependency version
2017-04-29 13:59:40 +01:00
orangemug
31d05cefbe Update react-height peer dependency version. 2017-04-28 18:39:56 +01:00
orangemug
c552838fdd Merge remote-tracking branch 'upstream/master' into fix/issue-110-update-mapbox-style-spec
Conflicts:
	package.json
2017-04-13 08:26:25 +01:00
orangemug
45942e604b Updated @mapbox/mapbox-gl-style-spec to v9 2017-04-13 08:24:49 +01:00
Lukas Martinelli
9b1dd44b9d Merge pull request #128 from orangemug/feature/rtl-plugin-issue-126
Added @mapbox/mapbox-gl-rtl-text plugin
2017-04-11 16:42:17 -04:00
orangemug
df56faa55a Added missing file-loader dep. 2017-04-11 20:54:12 +01:00
orangemug
14cdeae3eb Switch to using object urls so we don't need ajax for mapbox-gl-rtl-text plugin (issue #126) 2017-04-11 17:29:28 +01:00
orangemug
f97d2b0e88 Added @mapbox/mapbox-gl-rtl-text plugin (fixes #126) 2017-04-11 08:18:29 +01:00
Lukas Martinelli
a7e2154422 Merge pull request #127 from orangemug/fix/build-path
Fixed build path since move of webpack.config.* to ./config
2017-04-10 09:04:49 -05:00
orangemug
d8e84d67da Revert to mapbox-gl 0.34 because of mapbox-gl-inspect. 2017-04-10 13:58:08 +01:00
orangemug
c3174a0c72 Moved to using @mapbox/mapbox-gl-style-spec 2017-04-10 13:29:57 +01:00
orangemug
0b05284340 Fixed build path since move of webpack.config.* to ./config 2017-04-10 09:39:36 +01:00
orangemug
ac8ae0da66 Merge branch 'master' into fix/issue-110-update-mapbox-style-spec 2017-04-08 22:41:26 +01:00
Lukas Martinelli
4517a8a36a Merge pull request #125 from orangemug/feature/ui-tests
Initial webdriver test
2017-04-05 14:11:06 -05:00
orangemug
8ba7eadcb9 Removed tests for node v5 2017-04-05 18:01:26 +01:00
orangemug
0700e5b05b Yet another attempt at the build matrix. 2017-04-05 14:13:16 +01:00
orangemug
3485b7bfb0 Fixed matrix typo. 2017-04-04 21:42:08 +01:00
orangemug
c71c50a729 Also need os in yaml. 2017-04-04 20:21:14 +01:00
orangemug
2651ab891d Moved linux env into matrix. 2017-04-04 20:18:50 +01:00
orangemug
1e429550c6 Moved to mapbox-gl-inspect^1.2.3 2017-04-04 19:18:41 +01:00
orangemug
44e4ae3740 Added node 7 to appveyor and also installed windows-build-tools for odd nodejs versions (issue #125) 2017-04-04 18:58:03 +01:00
orangemug
b1552248c3 Added C compiler for odd versions of node.js
See <https://github.com/laverdet/node-fibers/issues/333#issuecomment-282312238>
2017-04-04 18:29:15 +01:00
orangemug
5efd2caeb8 Added nodejs 7 to travis and updated to latest patch versions 2017-04-04 18:09:43 +01:00
orangemug
bed012cb9c Updated web driver config. 2017-04-04 16:03:02 +01:00
orangemug
319d9024db Removed .runner-opts from gitignore 2017-04-04 15:53:37 +01:00
orangemug
ff7e371404 Initial webdriver test. 2017-04-04 15:51:30 +01:00
orangemug
d94ee2ba98 Merge remote-tracking branch 'upstream/master' into fix/issue-110-update-mapbox-style-spec 2017-04-04 08:57:33 +01:00
orangemug
a112c29c21 Moved mapbox-gl-inspect to point at lukasmartinelli/mapbox-gl-inspect repo. 2017-04-04 08:55:45 +01:00
Lukas Martinelli
c7d6734a26 Merge pull request #122 from orangemug/fix/issue-54
Fix to add error notice when uploading invalid JSON
2017-04-02 16:20:23 -04:00
orangemug
32aa8b0e1f Fix lint errors. 2017-04-02 18:42:27 +01:00
orangemug
6b22ba2707 Removed duplicate css rules. 2017-04-02 16:11:14 +01:00
orangemug
2400c8ed00 Clear error on modal toggle. 2017-04-02 16:02:57 +01:00
orangemug
396022e8ea Styling fixes for firefox. 2017-04-02 16:01:56 +01:00
orangemug
0d4449b9c2 Added css flexbox to modal to allow for a max-height. 2017-04-02 15:13:07 +01:00
Lukas Martinelli
32ac92f901 Merge pull request #105 from orangemug/fix/issue-89
Remove overflow hidden to allow modal to show in safari
2017-03-28 23:30:37 -04:00
Lukas Martinelli
f70026b702 Merge pull request #119 from nyurik/nyurik-patch-1
Increase popup Z-index
2017-03-28 23:30:23 -04:00
Yuri Astrakhan
87acc3362d Increase popup Z-index
Per https://github.com/maputnik/editor/issues/118
2017-03-28 23:03:13 -04:00
Lukas Martinelli
732d231c78 Do not barf if glyphs url not set #113 2017-03-28 11:00:58 -04:00
orangemug
a76ce64e1d Temp updated to fix peer dependency issue 2017-03-22 10:54:11 +00:00
orangemug
5433a4193b Move from mapbox-gl-style-spec -> mapbox-gl. 2017-03-22 10:36:20 +00:00
Lukas Martinelli
56f1e58df0 Upgrade to Mapbox GL 0.33 #108 2017-03-19 16:15:38 -04:00
Helge Fahrnberger
d0c9db41ce Merge pull request #104 from orangemug/feature/issue-47
Added JSON linting (#47)
2017-03-15 15:22:34 +01:00
Helge Fahrnberger
f162ffd9be Merge pull request #107 from orangemug/feature/layer-comments
Added layer comments via 'metadata.maputnik:comment' (issue #28)
2017-03-15 15:19:45 +01:00
orangemug
decc390777 Fix to add error notice when uploading invalid JSON (issue #54) 2017-03-09 21:29:25 +00:00
orangemug
ad8fa7563a Added JSON linting (fixes #47) 2017-03-08 21:35:19 +00:00
Lukas Martinelli
68859d279d Merge pull request #101 from orangemug/fix/issue-44
Alpha value slider fix (#44)
2017-03-07 10:12:17 -05:00
Lukas Martinelli
5792a531ce Merge pull request #102 from orangemug/feature/color-swatch
Added color swatch to color field
2017-03-07 10:07:02 -05:00
orangemug
03af10f850 Fixed maputnik-color-swatch size. 2017-03-07 13:02:09 +00:00
orangemug
2f059874aa Fixed css lint errors. 2017-03-07 12:59:08 +00:00
orangemug
a53d7763ba Added layer comments via 'metadata.maputnik:comment' (issue #28) 2017-03-07 12:25:23 +00:00
orangemug
eb526a6186 Remove overflow hidden to allow modal to show in safari (issue #89) 2017-03-07 10:47:46 +00:00
orangemug
6095f871ed Added color swatch to color field. 2017-03-07 10:17:45 +00:00
orangemug
e3b4fe582b Fixed alpha value slider (fixes #44) 2017-03-07 09:59:11 +00:00
jirik
bbf26a3f38 Update fontstacks URL according to https://github.com/klokantech/tileserver-gl/pull/104#issuecomment-274444087 2017-02-02 13:19:52 +01:00
Lukas Martinelli
fd291490d0 Merge pull request #93 from albertov/toolbar-offset
Configurable toolbar top offset.
2017-01-26 23:18:17 +01:00
Lukas Martinelli
767d68d905 Replace 0px with 0 2017-01-26 21:12:05 +01:00
Alberto Valverde
32b18e9141 Configurable toolbar top offset.
For facilitate embeding in other apps.
2017-01-26 20:43:06 +01:00
104 changed files with 15504 additions and 776 deletions

4
.babelrc Normal file
View File

@@ -0,0 +1,4 @@
{
"presets": ["env", "react"],
"plugins": ["transform-object-rest-spread", "transform-class-properties"]
}

View File

@@ -1,13 +1,26 @@
language: node_js
addons:
firefox: latest
os:
- linux
- osx
node_js:
- "4.6"
- "5.11"
- "6.1"
matrix:
include:
- os: linux
node_js: "6"
- os: linux
env: CXX=g++-4.8
node_js: "7"
- os: linux
node_js: "8"
- os: linux
env: CXX=g++-4.8
node_js: "9"
- os: osx
node_js: "6"
- os: osx
node_js: "7"
- os: osx
node_js: "8"
- os: osx
node_js: "9"
before_install:
- export CHROME_BIN=chromium-browser
- export DISPLAY=:99.0
@@ -20,3 +33,9 @@ script:
- npm run lint
- npm run lint-styles
- npm run test
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8

View File

@@ -1,11 +1,23 @@
# Maputnik [![Build Status](https://travis-ci.org/maputnik/editor.svg?branch=master)](https://travis-ci.org/maputnik/editor) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/anelbgv6jdb3qnh9/branch/master?svg=true)](https://ci.appveyor.com/project/lukasmartinelli/editor) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://tldrlegal.com/license/mit-license)
# Maputnik
[![Build Status](https://travis-ci.org/maputnik/editor.svg?branch=master)][travis]
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/anelbgv6jdb3qnh9/branch/master?svg=true)][appveyor]
[![Dependency Status](https://david-dm.org/maputnik/editor.svg)][dm-prod]
[![Dev Dependency Status](https://david-dm.org/maputnik/editor/dev-status.svg)][dm-dev]
[![License](https://img.shields.io/badge/license-MIT-blue.svg)][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.
- :link: Design your maps online at **http://maputnik.com/editor/** (all in local storage)
- :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
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.
@@ -37,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
@@ -51,6 +68,10 @@ npm run lint
npm run lint-styles
```
## 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
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.

View File

@@ -1,11 +1,16 @@
environment:
matrix:
- nodejs_version: "4.6"
- nodejs_version: "5.11"
- nodejs_version: "6.1"
- nodejs_version: "6"
- nodejs_version: "7"
- 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

6
circle.yml Normal file
View File

@@ -0,0 +1,6 @@
machine:
node:
version: 6
test:
post:
- npm run build

48
config/wdio.conf.js Normal file
View File

@@ -0,0 +1,48 @@
var webpack = require("webpack");
var WebpackDevServer = require("webpack-dev-server");
var webpackConfig = require("./webpack.production.config");
var testConfig = require("../test/config/specs");
var server;
exports.config = {
specs: [
'./test/specs/**/*.js'
],
exclude: [
],
maxInstances: 10,
capabilities: [{
maxInstances: 5,
browserName: 'firefox'
}],
sync: true,
logLevel: 'verbose',
coloredLogs: true,
bail: 0,
screenshotPath: './errorShots/',
baseUrl: 'http://localhost',
waitforTimeout: 10000,
connectionRetryTimeout: 90000,
connectionRetryCount: 3,
services: ['phantomjs'],
framework: 'mocha',
reporters: ['spec'],
phantomjsOpts: {
webdriverLogfile: 'phantomjs.log'
},
mochaOpts: {
ui: 'bdd',
// Because we don't know how long the initial build will take...
timeout: 2*60*1000
},
onPrepare: function (config, capabilities) {
var compiler = webpack(webpackConfig);
server = new WebpackDevServer(compiler, {});
server.listen(testConfig.port);
},
onComplete: function(exitCode) {
server.close();
}
}

View File

@@ -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,18 @@ 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/,
/openlayers\/dist\/ol.js/
],
loaders: loaders
},
node: {
fs: "empty",
@@ -41,14 +46,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'
}
])
]
};

View File

@@ -6,24 +6,24 @@ module.exports = [
},
{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components|public)/,
loader: 'babel',
exclude: /(.*node_modules(?![\/\\]@mapbox[\/\\]mapbox-gl-style-spec)|bower_components|public)/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react'],
presets: ['env', 'react'],
plugins: ['transform-runtime', 'transform-decorators-legacy', 'transform-class-properties'],
}
},
{
test: /\.(eot|ttf|woff|woff2)$/,
loader: 'file?name=fonts/[name].[ext]'
loader: 'file-loader?name=fonts/[name].[ext]'
},
{
test: /\.ico$/,
loader: 'file?name=[name].[ext]'
loader: 'file-loader?name=[name].[ext]'
},
{
test: /\.(svg|gif|jpg|png)$/,
loader: 'file?name=img/[name].[ext]'
loader: 'file-loader?name=img/[name].[ext]'
},
{
test: /\.json$/,
@@ -36,8 +36,8 @@ module.exports = [
{
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
loaders: [
'style?sourceMap',
'css'
'style-loader?sourceMap',
'css-loader'
]
}
];

View File

@@ -5,6 +5,16 @@ 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 OUTPATH;
if(process.env.CIRCLE_ARTIFACTS) {
OUTPATH = path.join(process.env.CIRCLE_ARTIFACTS, "build");
}
else {
OUTPATH = path.join(__dirname, '..', 'public');
}
module.exports = {
entry: {
@@ -12,8 +22,6 @@ module.exports = {
vendor: [
'file-saver',
'mapbox-gl/dist/mapbox-gl.js',
//TODO: Build failure because cannot resolve migrations file
//"mapbox-gl-style-spec",
"lodash.clonedeep",
"lodash.throttle",
'color',
@@ -32,14 +40,18 @@ 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/,
/openlayers\/dist\/ol.js/
],
loaders
},
node: {
@@ -48,8 +60,8 @@ 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': {
@@ -62,7 +74,6 @@ module.exports = {
screw_ie8: true,
}
}),
new webpack.optimize.OccurenceOrderPlugin(),
new ExtractTextPlugin('[contenthash].css', {
allChunks: true
}),
@@ -70,6 +81,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',
})
]
};

View File

@@ -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'
}
});
};

13208
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,14 @@
{
"name": "maputnik",
"version": "1.0.1",
"version": "1.1.0-beta",
"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",
"stats": "webpack --config config/webpack.production.config.js --profile --json > stats.json",
"build": "webpack --config config/webpack.production.config.js --progress --profile --colors",
"test": "wdio config/wdio.conf.js",
"test-watch": "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'"
},
@@ -20,47 +20,43 @@
"license": "MIT",
"homepage": "https://github.com/maputnik/editor#readme",
"dependencies": {
"@mapbox/mapbox-gl-rtl-text": "^0.1.1",
"@mapbox/mapbox-gl-style-spec": "^10.0.1",
"classnames": "^2.2.5",
"codemirror": "^5.18.2",
"color": "^1.0.3",
"file-saver": "^1.3.2",
"codemirror": "^5.32.0",
"color": "^2.0.0",
"file-saver": "^1.3.3",
"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",
"mapbox-gl": "^0.31.0",
"mapbox-gl-inspect": "^1.2.1",
"mapbox-gl-style-spec": "^8.11.0",
"mousetrap": "^1.6.0",
"ol-mapbox-style": "1.0.1",
"openlayers": "^3.19.1",
"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",
"reconnecting-websocket": "^3.0.3",
"request": "^2.79.0",
"mapbox-gl": "^0.43.0",
"mapbox-gl-inspect": "^1.2.4",
"maputnik-design": "github:maputnik/design",
"mousetrap": "^1.6.1",
"ol-mapbox-style": "^1.0.1",
"openlayers": "^4.4.2",
"prop-types": "^15.6.0",
"react": "16.0.0",
"react-addons-pure-render-mixin": "^15.6.2",
"react-autocomplete": "^1.7.2",
"react-codemirror2": "^3.0.7",
"react-collapse": "^4.0.3",
"react-color": "^2.13.8",
"react-copy-to-clipboard": "^5.0.1",
"react-dom": "16.0.0",
"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.83.0",
"url": "^0.11.0"
},
"babel": {
"presets": [
"es2015",
"react"
],
"plugins": [
"transform-object-rest-spread"
]
},
"jshintConfig": {
"esversion": 6
},
@@ -71,7 +67,7 @@
"plugins": [
"react"
],
"extend": [
"extends": [
"plugin:react/recommended"
],
"env": {
@@ -91,41 +87,47 @@
}
},
"devDependencies": {
"babel-core": "6.21.0",
"babel-eslint": "^7.1.1",
"babel-loader": "6.2.10",
"babel-plugin-transform-class-properties": "^6.11.5",
"babel-core": "^6.26.0",
"babel-eslint": "^8.0.2",
"babel-loader": "7.1.1",
"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.18.0",
"babel-preset-react": "6.16.0",
"babel-runtime": "^6.11.6",
"css-loader": "0.26.1",
"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",
"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-react": "^6.24.1",
"babel-runtime": "^6.26.0",
"base64-loader": "^1.0.0",
"copy-webpack-plugin": "^4.2.0",
"css-loader": "^0.28.7",
"eslint": "^4.10.0",
"eslint-plugin-react": "^7.4.0",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.5",
"html-webpack-plugin": "^2.30.1",
"json-loader": "^0.5.7",
"karma": "^1.7.1",
"karma-chrome-launcher": "^2.2.0",
"karma-firefox-launcher": "^1.0.1",
"karma-mocha": "^1.3.0",
"karma-webpack": "^2.0.1",
"mocha": "^3.1.2",
"mocha-loader": "^1.0.0",
"node-sass": "^4.2.0",
"react-hot-loader": "^3.0.0-beta.6",
"sass-loader": "^4.0.1",
"style-loader": "0.13.1",
"stylelint": "^7.7.1",
"karma-webpack": "^2.0.5",
"mocha": "^4.0.1",
"mocha-loader": "^1.1.1",
"node-sass": "^4.6.0",
"react-hot-loader": "^3.1.1",
"sass-loader": "^6.0.6",
"style-loader": "^0.19.0",
"stylelint": "^7.13.0",
"stylelint-config-standard": "^15.0.1",
"transform-loader": "^0.2.3",
"url-loader": "0.5.7",
"webpack": "1.14.0",
"webpack-cleanup-plugin": "^0.4.1",
"webpack-dev-server": "1.16.2"
"transform-loader": "^0.2.4",
"wdio-mocha-framework": "^0.5.11",
"wdio-phantomjs-service": "^0.2.2",
"wdio-spec-reporter": "^0.1.2",
"webdriverio": "^4.8.0",
"webpack": "^3.8.1",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-cleanup-plugin": "^0.5.1",
"webpack-dev-server": "^2.9.4"
}
}

View File

@@ -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;
}

View File

@@ -10,9 +10,7 @@ import AppLayout from './AppLayout'
import MessagePanel from './MessagePanel'
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import validateStyleMin from 'mapbox-gl-style-spec/lib/validate_style.min'
import formatStyle from 'mapbox-gl-style-spec/lib/format'
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'
@@ -21,6 +19,7 @@ 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'
function updateRootSpec(spec, fieldName, newValues) {
return {
@@ -65,16 +64,16 @@ export default class App extends React.Component {
sources: {},
vectorLayers: {},
inspectModeEnabled: false,
spec: GlSpec,
spec: styleSpec.latest,
}
this.layerWatcher = new LayerWatcher({
onSourcesChange: v => this.setState({ sources: v }),
onVectorLayersChange: v => this.setState({ vectorLayers: v })
})
}
componentDidMount() {
this.fetchSources();
Mousetrap.bind(['ctrl+z'], this.onUndo.bind(this));
Mousetrap.bind(['ctrl+y'], this.onRedo.bind(this));
}
@@ -115,7 +114,7 @@ export default class App extends React.Component {
this.updateIcons(newStyle.sprite)
}
const errors = validateStyleMin(newStyle, GlSpec)
const errors = styleSpec.validate(newStyle, styleSpec.latest)
if(errors.length === 0) {
this.revisionStore.addRevision(newStyle)
if(save) this.saveStyle(newStyle)
@@ -128,6 +127,8 @@ export default class App extends React.Component {
errors: errors.map(err => err.message)
})
}
this.fetchSources();
}
onUndo() {
@@ -184,11 +185,58 @@ export default class App extends React.Component {
})
}
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") {
const url = val.url;
fetch(url)
.then((response) => {
return response.json();
})
.then((json) => {
// 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),
onDataChange: (e) => {
this.layerWatcher.analyzeMap(e.map)
this.fetchSources();
},
}

View File

@@ -1,17 +1,18 @@
import React from 'react'
import PropTypes from 'prop-types'
import ScrollContainer from './ScrollContainer'
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() {

View File

@@ -1,11 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
class Button extends React.Component {
static propTypes = {
onClick: React.PropTypes.func,
style: React.PropTypes.object,
className: React.PropTypes.string,
onClick: PropTypes.func,
style: PropTypes.object,
className: PropTypes.string,
children: PropTypes.node
}
render() {

View File

@@ -1,18 +1,19 @@
import React from 'react'
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 errors = this.props.errors.map((m, i) => {
return <p className="maputnik-message-panel-error">{m}</p>
return <p key={"error-"+i} className="maputnik-message-panel-error">{m}</p>
})
const infos = this.props.infos.map((m, i) => {
return <p key={i}>{m}</p>
return <p key={"info-"+i}>{m}</p>
})
return <div className="maputnik-message-panel">

View File

@@ -1,9 +1,16 @@
import React from 'react'
import PropTypes from 'prop-types'
const ScrollContainer = (props) => {
class ScrollContainer extends React.Component {
static propTypes = {
children: PropTypes.node
}
render() {
return <div className="maputnik-scroll-container">
{props.children}
{this.props.children}
</div>
}
}
export default ScrollContainer

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import FileReaderInput from 'react-file-reader-input'
import classnames from 'classnames'
@@ -16,7 +17,7 @@ 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 logoImage from '../img/maputnik.png'
import logoImage from 'maputnik-design/logos/logo-color.svg'
import SettingsModal from './modals/SettingsModal'
import ExportModal from './modals/ExportModal'
import SourcesModal from './modals/SourcesModal'
@@ -24,39 +25,62 @@ import OpenModal from './modals/OpenModal'
import style from '../libs/style'
function IconText(props) {
return <span className="maputnik-icon-text">{props.children}</span>
class IconText extends React.Component {
static propTypes = {
children: PropTypes.node,
}
render() {
return <span className="maputnik-icon-text">{this.props.children}</span>
}
}
function ToolbarLink(props) {
class ToolbarLink extends React.Component {
static propTypes = {
className: PropTypes.string,
children: PropTypes.node,
href: PropTypes.string,
}
render() {
return <a
className={classnames('maputnik-toolbar-link', props.className)}
href={props.href}
target={"blank"}
className={classnames('maputnik-toolbar-link', this.props.className)}
href={this.props.href}
rel="noopener noreferrer"
target="_blank"
>
{props.children}
{this.props.children}
</a>
}
}
function ToolbarAction(props) {
class ToolbarAction extends React.Component {
static propTypes = {
children: PropTypes.node,
onClick: PropTypes.func
}
render() {
return <a
className='maputnik-toolbar-action'
onClick={props.onClick}
onClick={this.props.onClick}
>
{props.children}
{this.props.children}
</a>
}
}
export default class Toolbar extends React.Component {
static propTypes = {
mapStyle: React.PropTypes.object.isRequired,
inspectModeEnabled: React.PropTypes.bool.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,
onStyleOpen: PropTypes.func.isRequired,
// A dict of source id's and the available source layers
sources: React.PropTypes.object.isRequired,
onInspectModeToggle: React.PropTypes.func.isRequired
sources: PropTypes.object.isRequired,
onInspectModeToggle: PropTypes.func.isRequired,
children: PropTypes.node
}
constructor(props) {
@@ -106,6 +130,7 @@ export default class Toolbar extends React.Component {
isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')}
/>
<div className="maputnik-toolbar__inner">
<ToolbarLink
href={"https://github.com/maputnik/editor"}
className="maputnik-toolbar-logo"
@@ -113,6 +138,7 @@ export default class Toolbar extends React.Component {
<img src={logoImage} alt="Maputnik" />
<h1>Maputnik</h1>
</ToolbarLink>
<div className="maputnik-toolbar__actions">
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
<OpenIcon />
<IconText>Open</IconText>
@@ -141,5 +167,7 @@ export default class Toolbar extends React.Component {
<IconText>Help</IconText>
</ToolbarLink>
</div>
</div>
</div>
}
}

View File

@@ -1,6 +1,7 @@
import React from 'react'
import Color from 'color'
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
import PropTypes from 'prop-types'
function formatColor(color) {
const rgb = color.rgb
@@ -10,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) {
@@ -29,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 {
@@ -37,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,
@@ -50,11 +50,27 @@ 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={{
@@ -64,7 +80,7 @@ class ColorField extends React.Component {
top: offset.top,
}}>
<ChromePicker
color={this.color.object()}
color={currentColor}
onChange={c => this.props.onChange(formatColor(c))}
/>
<div
@@ -81,11 +97,16 @@ class ColorField extends React.Component {
/>
</div>
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
className="maputnik-color"
ref="colorInput"
ref={(input) => this.colorInput = input}
onClick={this.togglePicker.bind(this)}
style={this.props.style}
name={this.props.name}

View File

@@ -1,12 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
export default class DocLabel extends React.Component {
static propTypes = {
label: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.string
label: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string
]).isRequired,
doc: React.PropTypes.string.isRequired,
doc: PropTypes.string.isRequired,
}
render() {

View File

@@ -0,0 +1,140 @@
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}>
{specField}
</div>
}
}

View File

@@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import ZoomSpecField from './ZoomSpecField'
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
@@ -35,10 +36,10 @@ function getGroupName(spec, 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,
spec: React.PropTypes.object.isRequired,
layer: PropTypes.object.isRequired,
groupFields: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
spec: PropTypes.object.isRequired,
}
onPropertyChange(property, newValue) {
@@ -54,7 +55,7 @@ export default class PropertyGroup extends React.Component {
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}

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import color from 'color'
import ColorField from './ColorField'
@@ -8,6 +9,7 @@ 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'
@@ -35,16 +37,17 @@ 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() {
@@ -105,11 +108,18 @@ export default class SpecField extends React.Component {
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
}

View File

@@ -1,180 +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 InputBlock from '../inputs/InputBlock'
import AddIcon from 'react-icons/lib/md/add-circle-outline'
import DeleteIcon from 'react-icons/lib/md/delete'
import FunctionIcon from 'react-icons/lib/md/functions'
import capitalize from 'lodash.capitalize'
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 ZoomSpecProperty 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)
}
makeZoomFunction() {
const zoomFunc = {
stops: [
[6, this.props.value],
[10, this.props.value]
]
}
this.props.onChange(this.props.fieldName, zoomFunc)
}
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)
}
renderZoomProperty() {
const zoomFields = this.props.value.stops.map((stop, idx) => {
const zoomLevel = stop[0]
const value = stop[1]
const deleteStopBtn= <DeleteStopButton onClick={this.deleteStop.bind(this, idx)} />
return <InputBlock
key={zoomLevel}
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.changeStop(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.changeStop(idx, zoomLevel, newValue)}
/>
</div>
</div>
</InputBlock>
})
return <div className="maputnik-zoom-spec-property">
{zoomFields}
<Button
className="maputnik-add-stop"
onClick={this.addStop.bind(this)}
>
Add stop
</Button>
</div>
}
renderProperty() {
let zoomBtn = null
if(this.props.fieldSpec['zoom-function']) {
zoomBtn = <MakeZoomFunctionButton onClick={this.makeZoomFunction.bind(this)} />
}
return <InputBlock
doc={this.props.fieldSpec.doc}
label={labelFromFieldName(this.props.fieldName)}
action={zoomBtn}
>
<SpecField {...this.props} />
</InputBlock>
}
render() {
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
return <div className={propClass}>
{isZoomField(this.props.value) ? this.renderZoomProperty() : this.renderProperty()}
</div>
}
}
function MakeZoomFunctionButton(props) {
return <Button
className="maputnik-make-zoom-function"
onClick={props.onClick}
>
<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>
}
function DeleteStopButton(props) {
return <Button
className="maputnik-delete-stop"
onClick={props.onClick}
>
<DocLabel
label={<DeleteIcon />}
doc={"Remove zoom level stop."}
/>
</Button>
}
function labelFromFieldName(fieldName) {
let label = fieldName.split('-').slice(1).join(' ')
return capitalize(label)
}

View File

@@ -0,0 +1,176 @@
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)
stops[changeIdx] = [stopData, 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 = stop[0].zoom
const dataLevel = stop[0].value
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} />
}
return <InputBlock key={idx} action={deleteStopBtn} label="">
<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>
<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>
}
}

View 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>
}
}

View 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
}
}
}

View 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>
}
}

View 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>
}
}

View 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)
}

View File

@@ -1,7 +1,8 @@
import React from 'react'
import PropTypes from 'prop-types'
import { combiningFilterOps } from '../../libs/filterops.js'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import DocLabel from '../fields/DocLabel'
import SelectInput from '../inputs/SelectInput'
import SingleFilterEditor from './SingleFilterEditor'
@@ -26,9 +27,9 @@ function hasNestedCombiningFilter(filter) {
export default class CombiningFilterEditor extends React.Component {
static propTypes = {
/** Properties of the vector layer and the available fields */
properties: React.PropTypes.object,
filter: React.PropTypes.array,
onChange: React.PropTypes.func.isRequired,
properties: PropTypes.object,
filter: PropTypes.array,
onChange: PropTypes.func.isRequired,
}
// Convert filter to combining filter
@@ -91,7 +92,7 @@ export default class CombiningFilterEditor extends React.Component {
<div className="maputnik-filter-editor-compound-select">
<DocLabel
label={"Compound Filter"}
doc={GlSpec.layer.filter.doc + " Combine multiple filters together by using a compound filter."}
doc={styleSpec.latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."}
/>
<SelectInput
value={combiningOp}

View File

@@ -1,11 +1,12 @@
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: React.PropTypes.func.isRequired,
children: React.PropTypes.element.isRequired,
onDelete: PropTypes.func.isRequired,
children: PropTypes.element.isRequired,
}
render() {

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import { otherFilterOps } from '../../libs/filterops.js'
import StringInput from '../inputs/StringInput'
@@ -11,11 +12,34 @@ function tryParseInt(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: React.PropTypes.array.isRequired,
onChange: React.PropTypes.func.isRequired,
properties: React.PropTypes.object,
filter: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
properties: PropTypes.object,
}
static defaultProps = {
@@ -23,7 +47,7 @@ class SingleFilterEditor extends React.Component {
}
onFilterPartChanged(filterOp, propertyName, filterArgs) {
let newFilter = [filterOp, propertyName, ...filterArgs.map(tryParseInt)]
let newFilter = [filterOp, propertyName, ...filterArgs.map(parseFilter)]
if(filterOp === 'has' || filterOp === '!has') {
newFilter = [filterOp, propertyName]
} else if(filterArgs.length === 0) {

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import LineIcon from './LineIcon.jsx'
import FillIcon from './FillIcon.jsx'
@@ -8,8 +9,8 @@ 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() {

View File

@@ -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>
)
}

View File

@@ -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>
)

View File

@@ -1,14 +1,15 @@
import React from 'react'
import PropTypes from 'prop-types'
import StringInput from './StringInput'
import NumberInput from './NumberInput'
class ArrayInput extends React.Component {
static propTypes = {
value: React.PropTypes.array,
type: React.PropTypes.string,
length: React.PropTypes.number,
default: React.PropTypes.array,
onChange: React.PropTypes.func,
value: PropTypes.array,
type: PropTypes.string,
length: PropTypes.number,
default: PropTypes.array,
onChange: PropTypes.func,
}
changeValue(idx, newValue) {

View File

@@ -1,13 +1,75 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import Autocomplete from 'react-autocomplete'
const MAX_HEIGHT = 140;
class AutocompleteMenu extends React.Component {
static propTypes = {
keepMenuWithinWindowBounds: PropTypes.bool,
style: PropTypes.object,
children: PropTypes.node
}
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();
}
constructor(props) {
super(props);
this.state = {
maxHeight: MAX_HEIGHT
};
}
static defaultProps = {
style: {}
}
render() {
const maxHeight = this.state.maxHeight - this.props.style.marginBottom || 0;
const style = {
maxHeight: maxHeight+"px"
}
return (
<div
ref={(el) => {
this.autocompleteMenuEl = el;
}}
className={"maputnik-autocomplete-menu"}
style={style}
>
{this.props.children}
</div>
)
}
}
class AutocompleteInput extends React.Component {
static propTypes = {
value: React.PropTypes.string,
options: React.PropTypes.array,
onChange: React.PropTypes.func,
value: PropTypes.string,
options: PropTypes.array,
onChange: PropTypes.func,
keepMenuWithinWindowBounds: PropTypes.bool
}
static defaultProps = {
@@ -16,14 +78,16 @@ class AutocompleteInput extends React.Component {
}
render() {
const AutocompleteMenu = (items, value, style) => <div className={"maputnik-autocomplete-menu"} children={items} />
return <Autocomplete
wrapperProps={{
className: "maputnik-autocomplete",
style: null
}}
renderMenu={AutocompleteMenu}
renderMenu={(items) => {
return <AutocompleteMenu keepMenuWithinWindowBounds={this.props.keepMenuWithinWindowBounds} style={{marginBottom: 4}}>
{items}
</AutocompleteMenu>
}}
inputProps={{
className: "maputnik-string"
}}

View File

@@ -1,10 +1,11 @@
import React from 'react'
import PropTypes from 'prop-types'
class CheckboxInput extends React.Component {
static propTypes = {
value: React.PropTypes.bool.isRequired,
style: React.PropTypes.object,
onChange: React.PropTypes.func,
value: PropTypes.bool.isRequired,
style: PropTypes.object,
onChange: PropTypes.func,
}
render() {

View 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

View File

@@ -1,12 +1,14 @@
import React from 'react'
import PropTypes from 'prop-types'
import AutocompleteInput from './AutocompleteInput'
class FontInput extends React.Component {
static propTypes = {
value: React.PropTypes.array.isRequired,
fonts: React.PropTypes.array,
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 = {

View File

@@ -1,13 +1,14 @@
import React from 'react'
import PropTypes from 'prop-types'
import AutocompleteInput from './AutocompleteInput'
class IconInput extends React.Component {
static propTypes = {
value: React.PropTypes.array,
icons: React.PropTypes.array,
style: React.PropTypes.object,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.array,
icons: PropTypes.array,
style: PropTypes.object,
onChange: PropTypes.func.isRequired,
}
static defaultProps = {

View File

@@ -1,18 +1,20 @@
import React from 'react'
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.oneOfType([
React.PropTypes.string,
React.PropTypes.element,
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
]).isRequired,
doc: React.PropTypes.string,
action: React.PropTypes.element,
children: React.PropTypes.element.isRequired,
style: React.PropTypes.object,
doc: PropTypes.string,
action: PropTypes.element,
children: PropTypes.node.isRequired,
style: PropTypes.object,
onChange: PropTypes.func,
}
onChange(e) {

View File

@@ -1,12 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import Button from '../Button'
class MultiButtonInput extends React.Component {
static propTypes = {
value: React.PropTypes.string.isRequired,
options: React.PropTypes.array.isRequired,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
options: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {

View File

@@ -1,12 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
class NumberInput extends React.Component {
static propTypes = {
value: React.PropTypes.number,
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) {

View File

@@ -1,11 +1,12 @@
import React from 'react'
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,
options: PropTypes.array.isRequired,
style: PropTypes.object,
onChange: PropTypes.func.isRequired,
}

View File

@@ -1,11 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
class StringInput extends React.Component {
static propTypes = {
value: React.PropTypes.string,
style: React.PropTypes.object,
default: React.PropTypes.string,
onChange: React.PropTypes.func,
value: PropTypes.string,
style: PropTypes.object,
default: PropTypes.string,
onChange: PropTypes.func,
multi: PropTypes.bool,
}
constructor(props) {
@@ -20,16 +22,33 @@ class StringInput extends React.Component {
}
render() {
return <input
className="maputnik-string"
style={this.props.style}
value={this.state.value}
placeholder={this.props.default}
onChange={e => this.setState({ value: e.target.value })}
onBlur={() => {
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, {
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)
}}
/>
}
});
}
}

View File

@@ -1,11 +1,12 @@
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: React.PropTypes.bool.isRequired,
style: React.PropTypes.object,
isCollapsed: PropTypes.bool.isRequired,
style: PropTypes.object,
}
render() {

View File

@@ -0,0 +1,25 @@
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."}>
<StringInput
multi={true}
value={this.props.value}
onChange={this.props.onChange}
default="Comment..."
/>
</InputBlock>
}
}
export default MetadataBlock

View File

@@ -1,19 +1,26 @@
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 '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) {
@@ -66,13 +73,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}
/>

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import JSONEditor from './JSONEditor'
import FilterEditor from '../filter/FilterEditor'
@@ -8,6 +9,7 @@ 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'
@@ -42,12 +44,12 @@ function layoutGroups(layerType) {
/** 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,
spec: React.PropTypes.object.isRequired,
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 = {
@@ -57,7 +59,7 @@ export default class LayerEditor extends React.Component {
}
static childContextTypes = {
reactIconBase: React.PropTypes.object
reactIconBase: PropTypes.object
}
constructor(props) {
@@ -110,6 +112,11 @@ export default class LayerEditor extends React.Component {
}
renderGroupType(type, fields) {
let comment = ""
if(this.props.layer.metadata) {
comment = this.props.layer.metadata['maputnik:comment']
}
switch(type) {
case 'layer': return <div>
<LayerIdBlock
@@ -127,7 +134,7 @@ export default class LayerEditor extends React.Component {
/>
}
{this.props.layer.type !== 'raster' && this.props.layer.type !== 'background' && <LayerSourceLayerBlock
sourceLayerIds={this.props.sources[this.props.layer.source]}
sourceLayerIds={this.props.sources[this.props.layer.source].layers}
value={this.props.layer['source-layer']}
onChange={v => this.changeProperty(null, 'source-layer', v)}
/>
@@ -140,6 +147,10 @@ export default class LayerEditor extends React.Component {
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">

View File

@@ -1,13 +1,14 @@
import React from 'react'
import PropTypes from 'prop-types'
import Collapse from 'react-collapse'
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
title: PropTypes.string.isRequired,
isActive: PropTypes.bool.isRequired,
children: PropTypes.element.isRequired,
onActiveToggle: PropTypes.func.isRequired
}
render() {

View File

@@ -1,17 +1,18 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
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,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"ID"} doc={GlSpec.layer.id.doc}>
return <InputBlock label={"ID"} doc={styleSpec.latest.layer.id.doc}>
<StringInput
value={this.props.value}
onChange={this.props.onChange}

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import cloneDeep from 'lodash.clonedeep'
@@ -12,11 +13,11 @@ import style from '../../libs/style.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,
sources: React.PropTypes.object.isRequired,
layers: PropTypes.array.isRequired,
selectedLayerIndex: PropTypes.number.isRequired,
onLayersChange: PropTypes.func.isRequired,
onLayerSelect: PropTypes.func,
sources: PropTypes.object.isRequired,
}
function layerPrefix(name) {
@@ -49,6 +50,7 @@ class LayerListContainer extends React.Component {
super(props)
this.state = {
collapsedGroups: {},
areAllGroupsExpanded: false,
isOpen: {
add: false,
}
@@ -94,6 +96,31 @@ class LayerListContainer extends React.Component {
})
}
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++) {
@@ -177,11 +204,24 @@ class LayerListContainer extends React.Component {
<header className="maputnik-layer-list-header">
<span className="maputnik-layer-list-header-title">Layers</span>
<span className="maputnik-space" />
<Button
<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')}
className="maputnik-add-layer">
className="maputnik-button maputnik-button-selected">
Add Layer
</Button>
</a>
</div>
</div>
</header>
<ul className="maputnik-layer-list-container">
{listItems}

View File

@@ -1,16 +1,16 @@
import React from 'react'
import PropTypes from 'prop-types'
import Collapser from './Collapser'
export default class LayerEditorGroup extends React.Component {
export default class LayerListGroup extends React.Component {
static propTypes = {
title: React.PropTypes.string.isRequired,
children: React.PropTypes.element.isRequired,
isActive: React.PropTypes.bool.isRequired,
onActiveToggle: React.PropTypes.func.isRequired
title: PropTypes.string.isRequired,
isActive: PropTypes.bool.isRequired,
onActiveToggle: PropTypes.func.isRequired
}
render() {
return <div className="maputnik-layer-list-group">
return <li className="maputnik-layer-list-group">
<div className="maputnik-layer-list-group-header"
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
>
@@ -21,6 +21,6 @@ export default class LayerEditorGroup extends React.Component {
isCollapsed={this.props.isActive}
/>
</div>
</div>
</li>
}
}

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import Color from 'color'
import classnames from 'classnames'
@@ -30,8 +31,8 @@ class LayerTypeDragHandle extends React.Component {
class IconAction extends React.Component {
static propTypes = {
action: React.PropTypes.string.isRequired,
onClick: React.PropTypes.func.isRequired,
action: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
}
renderIcon() {
@@ -57,16 +58,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,
className: 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 = {
@@ -78,7 +79,7 @@ class LayerListItem extends React.Component {
}
static childContextTypes = {
reactIconBase: React.PropTypes.object
reactIconBase: PropTypes.object
}
getChildContext() {

View File

@@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
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'
@@ -8,9 +9,9 @@ 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,
onChange: PropTypes.func,
sourceIds: PropTypes.array,
}
static defaultProps = {
@@ -19,7 +20,7 @@ class LayerSourceBlock extends React.Component {
}
render() {
return <InputBlock label={"Source"} doc={GlSpec.layer.source.doc}>
return <InputBlock label={"Source"} doc={styleSpec.latest.layer.source.doc}>
<AutocompleteInput
value={this.props.value}
onChange={this.props.onChange}

View File

@@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
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'
@@ -8,19 +9,22 @@ 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"} doc={GlSpec.layer['source-layer'].doc}>
return <InputBlock label={"Source Layer"} doc={styleSpec.latest.layer['source-layer'].doc}>
<AutocompleteInput
keepMenuWithinWindowBounds={!!this.props.isFixed}
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceLayerIds.map(l => [l, l])}

View File

@@ -1,17 +1,18 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
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,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"Type"} doc={GlSpec.layer.type.doc}>
return <InputBlock label={"Type"} doc={styleSpec.latest.layer.type.doc}>
<SelectInput
options={[
['background', 'Background'],

View File

@@ -1,23 +1,24 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
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: React.PropTypes.number.isRequired,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.number,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"Max Zoom"} doc={GlSpec.layer.maxzoom.doc}>
return <InputBlock label={"Max Zoom"} doc={styleSpec.latest.layer.maxzoom.doc}>
<NumberInput
value={this.props.value}
onChange={this.props.onChange}
min={GlSpec.layer.maxzoom.minimum}
max={GlSpec.layer.maxzoom.maximum}
default={GlSpec.layer.maxzoom.maximum}
min={styleSpec.latest.layer.maxzoom.minimum}
max={styleSpec.latest.layer.maxzoom.maximum}
default={styleSpec.latest.layer.maxzoom.maximum}
/>
</InputBlock>
}

View File

@@ -1,23 +1,24 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
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: React.PropTypes.number.isRequired,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.number,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"Min Zoom"} doc={GlSpec.layer.minzoom.doc}>
return <InputBlock label={"Min Zoom"} doc={styleSpec.latest.layer.minzoom.doc}>
<NumberInput
value={this.props.value}
onChange={this.props.onChange}
min={GlSpec.layer.minzoom.minimum}
max={GlSpec.layer.minzoom.maximum}
default={GlSpec.layer.minzoom.minimum}
min={styleSpec.latest.layer.minzoom.minimum}
max={styleSpec.latest.layer.minzoom.maximum}
default={styleSpec.latest.layer.minzoom.minimum}
/>
</InputBlock>
}

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import LayerIcon from '../icons/LayerIcon'
@@ -14,6 +15,10 @@ function groupFeaturesBySourceLayer(features) {
}
class FeatureLayerPopup extends React.Component {
static propTypes = {
features: PropTypes.array
}
render() {
const sources = groupFeaturesBySourceLayer(this.props.features)

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
@@ -31,6 +32,9 @@ function renderFeature(feature) {
}
class FeaturePropertyPopup extends React.Component {
static propTypes = {
features: PropTypes.array
}
render() {
const features = this.props.features

View File

@@ -1,17 +1,19 @@
import React from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
import MapboxGl from 'mapbox-gl'
import MapboxInspect from 'mapbox-gl-inspect'
import FeatureLayerPopup from './FeatureLayerPopup'
import FeaturePropertyPopup from './FeaturePropertyPopup'
import validateColor from 'mapbox-gl-style-spec/lib/validate/validate_color'
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 renderLayerPopup(features) {
var mountNode = document.createElement('div');
@@ -57,10 +59,10 @@ function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
export default class MapboxGlMap extends React.Component {
static propTypes = {
onDataChange: React.PropTypes.func,
mapStyle: React.PropTypes.object.isRequired,
inspectModeEnabled: React.PropTypes.bool.isRequired,
highlightedLayer: React.PropTypes.object,
onDataChange: PropTypes.func,
mapStyle: PropTypes.object.isRequired,
inspectModeEnabled: PropTypes.bool.isRequired,
highlightedLayer: PropTypes.object,
}
static defaultProps = {
@@ -109,6 +111,9 @@ export default class MapboxGlMap extends React.Component {
hash: true,
})
const zoom = new ZoomControl;
map.addControl(zoom, 'top-right');
const nav = new MapboxGl.NavigationControl();
map.addControl(nav, 'top-right');

View File

@@ -1,7 +1,9 @@
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 'openlayers/dist/ol.css'
function suitableVectorSource(mapStyle) {
const sources = Object.keys(mapStyle.sources)
@@ -11,7 +13,7 @@ function suitableVectorSource(mapStyle) {
source: mapStyle.sources[sourceId]
}
})
.filter(({source}) => source.type === 'vector')
.filter(({source}) => (source.type === 'vector' || source.type === 'geojson'))
return sources[0]
}
@@ -28,6 +30,16 @@ function toVectorLayer(source, tilegrid, cb) {
})
}
function newGeoJSONLayer(sourceUrl) {
const ol = require('openlayers')
return new ol.layer.Vector({
source: new ol.source.Vector({
format: new ol.format.GeoJSON(),
url: sourceUrl
})
})
}
if (source.type === 'vector') {
if(!source.tiles) {
sourceFromTileJSON(source.url, tileSource => {
cb(newMVTLayer(tileSource.tiles[0]))
@@ -35,6 +47,9 @@ function toVectorLayer(source, tilegrid, cb) {
} else {
cb(newMVTLayer(source.tiles[0]))
}
} else if (source.type === 'geojson') {
cb(newGeoJSONLayer(source.data))
}
}
function sourceFromTileJSON(url, cb) {
@@ -51,9 +66,10 @@ function sourceFromTileJSON(url, cb) {
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 = {
@@ -91,10 +107,11 @@ class OpenLayers3Map extends React.Component {
}
if(!this.layer) {
var self = this
toVectorLayer(newSource.source, this.tilegrid, vectorLayer => {
this.layer = vectorLayer
this.map.addLayer(this.layer)
setStyleFunc(this.map, this.layer)
self.layer = vectorLayer
self.map.addLayer(self.layer)
setStyleFunc(self.map, self.layer)
})
} else {
setStyleFunc(this.map, this.layer)
@@ -140,10 +157,10 @@ class OpenLayers3Map extends React.Component {
ref={x => this.container = x}
style={{
position: "fixed",
top: 0,
top: 40,
right: 0,
bottom: 0,
height: "100%",
height: 'calc(100% - 40px)',
width: "75%",
backgroundColor: '#fff',
...this.props.style,

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import Button from '../Button'
import InputBlock from '../inputs/InputBlock'
@@ -13,13 +14,13 @@ import LayerSourceLayerBlock from '../layers/LayerSourceLayerBlock'
class AddModal extends React.Component {
static propTypes = {
layers: React.PropTypes.array.isRequired,
onLayersChange: 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() {
@@ -55,18 +56,54 @@ 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 availableSources = this.getSources(nextState.type);
if(
this.state.source !== ""
&& availableSources.indexOf(this.state.source) < 0
) {
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].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}
@@ -83,14 +120,15 @@ class AddModal extends React.Component {
/>
{this.state.type !== 'background' &&
<LayerSourceBlock
sourceIds={Object.keys(this.props.sources)}
sourceIds={sources}
value={this.state.source}
onChange={v => this.setState({ source: v })}
/>
}
{this.state.type !== 'background' && this.state.type !== 'raster' &&
<LayerSourceLayerBlock
sourceLayerIds={this.props.sources[this.state.source] || []}
isFixed={true}
sourceLayerIds={layers}
value={this.state['source-layer']}
onChange={v => this.setState({ 'source-layer': v })}
/>

View File

@@ -1,7 +1,8 @@
import React from 'react'
import PropTypes from 'prop-types'
import { saveAs } from 'file-saver'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
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'
@@ -9,15 +10,16 @@ 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 formatStyle from 'mapbox-gl-style-spec/lib/format'
import GitHub from 'github-api'
import { CopyToClipboard } from 'react-copy-to-clipboard'
class Gist extends React.Component {
static propTypes = {
mapStyle: React.PropTypes.object.isRequired,
onStyleChanged: React.PropTypes.func.isRequired,
mapStyle: PropTypes.object.isRequired,
onStyleChanged: PropTypes.func.isRequired,
}
constructor(props) {
@@ -44,8 +46,8 @@ class Gist extends React.Component {
const preview = this.state.preview && (this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token'];
const mapStyleStr = preview ?
formatStyle(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle))) :
formatStyle(stripAccessTokens(this.props.mapStyle));
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>
@@ -54,8 +56,8 @@ class Gist extends React.Component {
<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.28.0/mapbox-gl.css" />
<script src="https://api.mapbox.com/mapbox-gl-js/v0.28.0/mapbox-gl.js"></script>
<link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v0.43.0/mapbox-gl.css" />
<script src="https://api.mapbox.com/mapbox-gl-js/v0.43.0/mapbox-gl.js"></script>
<style>
body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; }
@@ -125,7 +127,7 @@ class Gist extends React.Component {
const user = gist.user || 'anonymous';
const preview = !!gist.files['index.html'];
if(preview) {
return <span><a target="_blank" href={"https://bl.ocks.org/"+user+"/"+gist.id}>Preview</a>,{' '}</span>
return <span><a target="_blank" rel="noopener noreferrer" href={"https://bl.ocks.org/"+user+"/"+gist.id}>Preview</a>,{' '}</span>
}
return null;
}
@@ -137,11 +139,21 @@ class Gist extends React.Component {
return <p>Saving...</p>
} else if(gist) {
const user = gist.user || 'anonymous';
return <p>
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" href={"https://gist.github.com/"+user+"/"+gist.id}>Source</a>
<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>
}
}
@@ -166,7 +178,7 @@ class Gist extends React.Component {
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}/>
</InputBlock>
<a target="_blank" href="https://openmaptiles.com/hosting/">Get your free access token</a>
<a target="_blank" rel="noopener noreferrer" href="https://openmaptiles.com/hosting/">Get your free access token</a>
</div>
: null}
{this.renderLatestGist()}
@@ -186,10 +198,10 @@ function stripAccessTokens(mapStyle) {
class ExportModal 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) {
@@ -197,7 +209,7 @@ class ExportModal extends React.Component {
}
downloadStyle() {
const blob = new Blob([formatStyle(stripAccessTokens(this.props.mapStyle))], {type: "application/json;charset=utf-8"});
const blob = new Blob([styleSpec.format(stripAccessTokens(this.props.mapStyle))], {type: "application/json;charset=utf-8"});
saveAs(blob, this.props.mapStyle.id + ".json");
}

View File

@@ -1,12 +1,14 @@
import React from 'react'
import PropTypes from 'prop-types'
import CloseIcon from 'react-icons/lib/md/close'
import Overlay from './Overlay'
class Modal extends React.Component {
static propTypes = {
isOpen: React.PropTypes.bool.isRequired,
title: React.PropTypes.string.isRequired,
onOpenToggle: React.PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
onOpenToggle: PropTypes.func.isRequired,
children: PropTypes.node,
}
render() {
@@ -21,8 +23,10 @@ class Modal extends React.Component {
<CloseIcon />
</a>
</header>
<div className="maputnik-modal-scroller">
<div className="maputnik-modal-content">{this.props.children}</div>
</div>
</div>
</Overlay>
}
}

View File

@@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import Modal from './Modal'
import Button from '../Button'
import FileReaderInput from 'react-file-reader-input'
@@ -12,10 +13,10 @@ 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() {
@@ -41,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,
@@ -55,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
@@ -84,11 +122,22 @@ 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
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
onOpenToggle={() => this.onOpenToggle()}
title={'Open Style'}
>
{errorElement}
<section className="maputnik-modal-section">
<h2>Upload Style</h2>
<p>Upload a JSON style from your computer.</p>
@@ -96,7 +145,19 @@ class OpenModal extends React.Component {
<Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button>
</FileReaderInput>
</section>
<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 type="text" ref={(input) => this.styleUrlElement = input} className="maputnik-input" placeholder="Enter URL..."/>
<div>
<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.

View File

@@ -1,10 +1,11 @@
import React from 'react'
import PropTypes from 'prop-types'
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() {

View File

@@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
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'
@@ -8,10 +9,10 @@ import Modal from './Modal'
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) {
@@ -46,7 +47,7 @@ class SettingsModal extends React.Component {
title={'Style Settings'}
>
<div style={{minWidth: 350}}>
<InputBlock label={"Name"} doc={GlSpec.$root.name.doc}>
<InputBlock label={"Name"} doc={styleSpec.latest.$root.name.doc}>
<StringInput {...inputProps}
value={this.props.mapStyle.name}
onChange={this.changeStyleProperty.bind(this, "name")}
@@ -58,14 +59,14 @@ class SettingsModal extends React.Component {
onChange={this.changeStyleProperty.bind(this, "owner")}
/>
</InputBlock>
<InputBlock label={"Sprite URL"} doc={GlSpec.$root.sprite.doc}>
<InputBlock label={"Sprite URL"} doc={styleSpec.latest.$root.sprite.doc}>
<StringInput {...inputProps}
value={this.props.mapStyle.sprite}
onChange={this.changeStyleProperty.bind(this, "sprite")}
/>
</InputBlock>
<InputBlock label={"Glyphs URL"} doc={GlSpec.$root.glyphs.doc}>
<InputBlock label={"Glyphs URL"} doc={styleSpec.latest.$root.glyphs.doc}>
<StringInput {...inputProps}
value={this.props.mapStyle.glyphs}
onChange={this.changeStyleProperty.bind(this, "glyphs")}

View File

@@ -1,5 +1,6 @@
import React from 'react'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import PropTypes from 'prop-types'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import Modal from './Modal'
import Button from '../Button'
import InputBlock from '../inputs/InputBlock'
@@ -16,10 +17,10 @@ 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() {
@@ -54,10 +55,10 @@ function editorMode(source) {
class ActiveSourceTypeEditor extends React.Component {
static propTypes = {
sourceId: React.PropTypes.string.isRequired,
source: React.PropTypes.object.isRequired,
onDelete: React.PropTypes.func.isRequired,
onChange: React.PropTypes.func.isRequired,
sourceId: PropTypes.string.isRequired,
source: PropTypes.object.isRequired,
onDelete: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
@@ -87,7 +88,7 @@ class ActiveSourceTypeEditor extends React.Component {
class AddSource extends React.Component {
static propTypes = {
onAdd: React.PropTypes.func.isRequired,
onAdd: PropTypes.func.isRequired,
}
constructor(props) {
@@ -138,7 +139,7 @@ class AddSource extends React.Component {
onChange={v => this.setState({ sourceId: v})}
/>
</InputBlock>
<InputBlock label={"Source Type"} doc={GlSpec.source_tile.type.doc}>
<InputBlock label={"Source Type"} doc={styleSpec.latest.source_vector.type.doc}>
<SelectInput
options={[
['geojson', 'GeoJSON'],
@@ -167,10 +168,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,
mapStyle: PropTypes.object.isRequired,
isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired,
onStyleChanged: PropTypes.func.isRequired,
}
stripTitle(source) {

View File

@@ -1,17 +1,18 @@
import React from 'react'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
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.isRequired,
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"TileJSON URL"} doc={GlSpec.source_tile.url.doc}>
return <InputBlock label={"TileJSON URL"} doc={styleSpec.latest.source_vector.url.doc}>
<StringInput
value={this.props.source.url}
onChange={url => this.props.onChange({
@@ -25,8 +26,8 @@ class TileJSONSourceEditor extends React.Component {
class TileURLSourceEditor extends React.Component {
static propTypes = {
source: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired,
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
changeTileUrl(idx, value) {
@@ -42,7 +43,7 @@ class TileURLSourceEditor extends React.Component {
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"} doc={GlSpec.source_tile.tiles.doc}>
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)}
@@ -54,7 +55,7 @@ class TileURLSourceEditor extends React.Component {
render() {
return <div>
{this.renderTileUrls()}
<InputBlock label={"Min Zoom"} doc={GlSpec.source_tile.minzoom.doc}>
<InputBlock label={"Min Zoom"} doc={styleSpec.latest.source_vector.minzoom.doc}>
<NumberInput
value={this.props.source.minzoom || 0}
onChange={minzoom => this.props.onChange({
@@ -63,7 +64,7 @@ class TileURLSourceEditor extends React.Component {
})}
/>
</InputBlock>
<InputBlock label={"Max Zoom"} doc={GlSpec.source_tile.maxzoom.doc}>
<InputBlock label={"Max Zoom"} doc={styleSpec.latest.source_vector.maxzoom.doc}>
<NumberInput
value={this.props.source.maxzoom || 22}
onChange={maxzoom => this.props.onChange({
@@ -79,12 +80,12 @@ class TileURLSourceEditor extends React.Component {
class GeoJSONSourceEditor extends React.Component {
static propTypes = {
source: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired,
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"GeoJSON Data"} doc={GlSpec.source_geojson.data.doc}>
return <InputBlock label={"GeoJSON Data"} doc={styleSpec.latest.source_geojson.data.doc}>
<StringInput
value={this.props.source.data}
onChange={data => this.props.onChange({
@@ -98,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() {

View File

@@ -3,25 +3,25 @@
"id": "klokantech-basic",
"title": "Klokantech Basic",
"url": "https://rawgit.com/openmaptiles/klokantech-basic-gl-style/master/style.json",
"thumbnail": "http://maputnik.com/thumbnails/klokantech-basic.png"
"thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png"
},
{
"id": "dark-matter",
"title": "Dark Matter",
"url": "https://rawgit.com/openmaptiles/dark-matter-gl-style/master/style.json",
"thumbnail": "http://maputnik.com/thumbnails/dark-matter.png"
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
},
{
"id": "positron",
"title": "Positron",
"url": "https://rawgit.com/openmaptiles/positron-gl-style/master/style.json",
"thumbnail": "http://maputnik.com/thumbnails/positron.png"
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png"
},
{
"id": "osm-bright",
"title": "OSM Bright",
"url": "https://rawgit.com/openmaptiles/osm-bright-gl-style/master/style.json",
"thumbnail": "http://maputnik.com/thumbnails/osm-bright.png"
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png"
},
{
"id": "osm-liberty",
@@ -39,24 +39,18 @@
"id": "mapbox-satellite",
"title": "Mapbox Satellite",
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/satellite-v9.json",
"thumbnail": "http://maputnik.com/thumbnails/mapbox-satellite.png"
"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": "http://maputnik.com/thumbnails/mapbox-bright.png"
"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": "http://maputnik.com/thumbnails/mapbox-basic.png"
},
{
"id": "tilezen",
"title": "Tilezen",
"url": "https://rawgit.com/lukasmartinelli/tilezen-gl-style/master/style.json",
"thumbnail": "http://maputnik.com/thumbnails/tilezen.png"
"thumbnail": "https://maputnik.github.io/thumbnails/mapbox-basic.png"
}
]

View File

@@ -8,14 +8,5 @@
"type": "vector",
"url": "https://free.tilehosting.com/data/v3.json?key={key}",
"title": "OpenMapTiles"
},
"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"
}
}

View File

@@ -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
View File

@@ -0,0 +1,9 @@
/**
* A unique id for the current document.
*/
let REF = 0;
export default function(prefix="") {
REF++;
return prefix+REF;
}

View File

@@ -1,6 +1,6 @@
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
export const combiningFilterOps = ['all', 'any', 'none']
export const setFilterOps = ['in', '!in']
export const otherFilterOps = Object
.keys(GlSpec.filter_operator.values)
.keys(styleSpec.latest.filter_operator.values)
.filter(op => combiningFilterOps.indexOf(op) < 0)

View File

@@ -1,16 +1,16 @@
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
export function changeType(layer, newType) {
const changedPaintProps = { ...layer.paint }
Object.keys(changedPaintProps).forEach(propertyName => {
if(!(propertyName in GlSpec['paint_' + newType])) {
if(!(propertyName in styleSpec.latest['paint_' + newType])) {
delete changedPaintProps[propertyName]
}
})
const changedLayoutProps = { ...layer.layout }
Object.keys(changedLayoutProps).forEach(propertyName => {
if(!(propertyName in GlSpec['layout_' + newType])) {
if(!(propertyName in styleSpec.latest['layout_' + newType])) {
delete changedLayoutProps[propertyName]
}
})
@@ -27,6 +27,28 @@ export function changeType(layer, newType) {
* to a {@newValue}.
*/
export function changeProperty(layer, group, property, newValue) {
// Remove the property if undefined
if(newValue === undefined) {
if(group) {
const newLayer = {
...layer
};
delete newLayer[group][property];
// Remove the group if it is now empty
if(Object.keys(newLayer[group]).length < 1) {
delete newLayer[group];
}
return newLayer;
} else {
const newLayer = {
...layer
};
delete newLayer[property];
return newLayer;
}
}
else {
if(group) {
return {
...layer,
@@ -41,4 +63,5 @@ export function changeProperty(layer, group, property, newValue) {
[property]: newValue
}
}
}
}

11
src/libs/mapbox-rtl.js Normal file
View File

@@ -0,0 +1,11 @@
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
// Load mapbox-gl-rtl-text using object urls without needing http://localhost for AJAX.
const data = require("base64-loader?mimetype=text/javascript!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.js");
const blob = new window.Blob([window.atob(data)]);
const objectUrl = window.URL.createObjectURL(blob, {
type: "text/javascript"
});
MapboxGl.setRTLTextPlugin(objectUrl);

View File

@@ -1,4 +1,5 @@
import request from 'request'
import npmurl from 'url'
function loadJSON(url, defaultValue, cb) {
request({
@@ -23,9 +24,15 @@ export function downloadGlyphsMetadata(urlTemplate, cb) {
if(!urlTemplate) return cb([])
// Special handling because Tileserver GL serves the fontstacks metadata differently
// https://github.com/klokantech/tileserver-gl/pull/104
let url = urlTemplate.replace('/fonts/{fontstack}/{range}.pbf', '/fontstacks.json')
url = url.replace('{fontstack}/{range}.pbf', 'fontstacks.json')
// https://github.com/klokantech/tileserver-gl/pull/104#issuecomment-274444087
let urlObj = npmurl.parse(urlTemplate);
const normPathPart = '/%7Bfontstack%7D/%7Brange%7D.pbf';
if(urlObj.pathname === normPathPart) {
urlObj.pathname = '/fontstacks.json';
} else {
urlObj.pathname = urlObj.pathname.replace(normPathPart, '.json');
}
let url = npmurl.format(urlObj);
loadJSON(url, [], cb)
}

View File

@@ -0,0 +1,14 @@
export default function(a, b) {
a = parseFloat(a, 10);
b = parseFloat(b, 10);
if(a < b) {
return -1
}
else if(a > b) {
return 1
}
else {
return 0;
}
}

View File

@@ -1,6 +1,5 @@
import React from 'react';
import spec from 'mapbox-gl-style-spec/reference/latest.min.js'
import derefLayers from 'mapbox-gl-style-spec/lib/deref'
import deref from '@mapbox/mapbox-gl-style-spec/deref'
import tokens from '../config/tokens.json'
// Empty style is always used if no style could be restored or fetched
@@ -37,7 +36,7 @@ function ensureHasNoInteractive(style) {
function ensureHasNoRefs(style) {
const derefedStyle = {
...style,
layers: derefLayers(style.layers)
layers: deref(style.layers)
}
return derefedStyle
}
@@ -70,7 +69,7 @@ function replaceAccessToken(mapStyle) {
}
const changedStyle = {
...mapStyle,
glyphs: mapStyle.glyphs.replace('{key}', accessToken),
glyphs: mapStyle.glyphs ? mapStyle.glyphs.replace('{key}', accessToken) : mapStyle.glyphs,
sources: changedSources
}

26
src/libs/zoomcontrol.js Normal file
View File

@@ -0,0 +1,26 @@
export default class ZoomControl {
onAdd(map) {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group mapboxgl-ctrl-zoom';
this.addEventListeners();
return this._container;
}
updateZoomLevel() {
this._container.innerHTML = `Zoom level: ${this._map.getZoom().toFixed(2)}`;
}
addEventListeners (){
this._map.on('render', this.updateZoomLevel.bind(this) );
this._map.on('zoomIn', this.updateZoomLevel.bind(this) );
this._map.on('zoomOut', this.updateZoomLevel.bind(this) );
}
onRemove() {
this._container.parentNode.removeChild(this._container);
this._map = undefined;
}
}

9
src/manifest.json Normal file
View File

@@ -0,0 +1,9 @@
{
"name": "Maputnik",
"short_name": "Maputnik",
"description": "Visual Map Designer",
"start_url": ".",
"display": "browser",
"background_color": "#1c1f24",
"theme_color": "#1c1f24"
}

View File

@@ -25,6 +25,13 @@
color: white;
}
.mapboxgl-ctrl-zoom {
color: rgb(138, 138, 138);
font-weight: bold;
padding: 4px 8px;
user-select: none;
}
.mapboxgl-ctrl-group {
background: rgb(28, 31, 36);
}

View File

@@ -1,10 +1,10 @@
// MAP
.maputnik-map {
position: fixed !important;
top: 40px;
top: $toolbar-height + $toolbar-offset;
right: 0;
bottom: 0;
height: calc(100% - $toolbar-height);
height: calc(100% - #{$toolbar-height + $toolbar-offset});
width: 75%;
}
@@ -34,12 +34,14 @@
top: 20px;
left: 0;
width: 120px;
z-index: 3;
z-index: 10;
pointer-events: none;
}
}
.maputnik-doc-target:hover .maputnik-doc-popup {
display: block;
text-align: left;
}
// BUTTON
@@ -104,13 +106,17 @@
.maputnik-action-block {
.maputnik-input-block-label {
display: inline-block;
width: 43%;
width: 35%;
}
.maputnik-input-block-action {
vertical-align: top;
display: inline-block;
width: 7%;
width: 15%;
}
.maputnik-input-block-action > div {
text-align: right;
}
}

View File

@@ -3,3 +3,15 @@
display: inline-block;
}
}
.maputnik-render-gist {
p {
margin: 10px 0;
}
input.maputnik-string {
margin-left: 5px;
width: 60%;
display: inline-block;
}
}

View File

@@ -15,6 +15,11 @@
.maputnik-string {
@extend .maputnik-input;
&--multi {
resize: vertical;
height: 78px;
}
}
.maputnik-number {
@@ -30,7 +35,15 @@
.maputnik-color-wrapper {
position: relative;
display: inline;
@include flex-row;
}
.maputnik-color-swatch {
height: 26px;
width: 3px;
flex-shrink: 0;
flex-grow: 0;
}
// ARRAY
@@ -39,6 +52,25 @@
> * {
margin-bottom: $margin-3;
}
.maputnik-array-block {
.maputnik-array-block-action {
vertical-align: top;
display: inline-block;
width: 14%;
}
.maputnik-array-block-content {
vertical-align: top;
display: inline-block;
width: 86%;
}
}
.maputnik-array-add-value {
display: inline-block;
float: right;
}
}
// SELECT
@@ -109,7 +141,8 @@
&-menu {
border: none;
padding: 2px 0;
position: fixed;
margin-right: 10px;
position: absolute;
overflow: auto;
max-height: 50%;
background: $color-gray;

View File

@@ -1,7 +1,7 @@
// LAYER LIST
.maputnik-layer-list {
&-header {
padding: $margin-2;
padding: $margin-2 $margin-2 $margin-3;
@include flex-row;

View File

@@ -17,20 +17,19 @@
&-list {
position: fixed;
bottom: 0;
height: calc(100% - $toolbar-height);
top: 40px;
height: calc(100% - #{$toolbar-height + $toolbar-offset});
top: $toolbar-height + $toolbar-offset;
left: 0;
z-index: 3;
width: 200px;
overflow: hidden;
background-color: $color-black;
}
&-drawer {
position: fixed;
bottom: 0;
height: calc(100% - $toolbar-height);
top: 40px;
height: calc(100% - #{$toolbar-height + $toolbar-offset});
top: $toolbar-height + $toolbar-offset;
left: 200px;
z-index: 1;
width: 350px;

22
src/styles/_map.scss Normal file
View File

@@ -0,0 +1,22 @@
//OPENLAYERS
.maputnik-layout {
.ol-zoom {
top: 10px;
right: 10px;
left: auto;
}
.ol-attribution.ol-logo-only {
height: 20px;
}
.ol-control {
button {
background-color: rgb(28, 31, 36);
}
button:hover {
background-color: rgb(86, 83, 83);
}
}
}

View File

@@ -12,3 +12,10 @@
@include vendor-prefix(flex-direction, row);
}
@mixin flex-column {
display: flex;
display: -ms-flexbox;
@include vendor-prefix(flex-direction, column);
}

View File

@@ -2,6 +2,7 @@
.maputnik-modal {
min-width: 350px;
max-width: 600px;
overflow: hidden;
background-color: $color-black;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.3);
z-index: 3;
@@ -10,6 +11,17 @@
.maputnik-modal-section {
padding-top: $margin-3;
padding-bottom: $margin-3;
/* Bug fix: <http://stackoverflow.com/questions/28636832/firefox-overflow-y-not-working-with-nested-flexbox> */
min-height: 0;
@include flex-column;
flex-shrink: 0;
}
.maputnik-modal-section--shrink {
flex-shrink: 1;
}
.maputnik-modal-header {
@@ -28,8 +40,15 @@
cursor: pointer;
}
.maputnik-modal-scroller {
max-height: calc(100vh - 35px);
overflow-y: auto;
}
.maputnik-modal-content {
padding: $margin-3;
@include flex-column;
}
.maputnik-modal-header-space {
@@ -66,8 +85,7 @@
}
.maputnik-style-gallery-container {
max-height: 400px;
overflow-y: scroll;
flex-shrink: 1;
}
.maputnik-public-style {
@@ -199,3 +217,19 @@
color: $color-lowgray;
}
}
.maputnik-modal-error {
border: solid 2px #ef5350;
color: #ef5350;
padding: 8px;
padding-right: 32px;
position: relative;
}
.maputnik-modal-error-close {
position: absolute;
right: 8px;
top: 8px;
text-decoration: none;
color: #ef5350;
}

View File

@@ -1,12 +1,15 @@
::-webkit-scrollbar {
// HACK: ::webkit-scrollbar selector covers to much of the UI. Bigger changes to come so for now just use :not() to ignore the toolbar
div:not(.maputnik-toolbar__actions) {
&::-webkit-scrollbar {
background-color: #26282e;
width: 5px;
}
}
::-webkit-scrollbar-thumb {
&::-webkit-scrollbar-thumb {
border-radius: 6px;
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
background-color: #666;
padding-left: 2px;
padding-right: 2px;
}
}

View File

@@ -5,7 +5,7 @@
width: 100%;
z-index: 100;
left: 0;
top: 0;
top: $toolbar-offset;
background-color: $color-black;
}
@@ -22,9 +22,8 @@
img {
width: 30px;
height: 30px;
padding-right: $margin-2;
vertical-align: middle;
vertical-align: top;
}
}
@@ -55,3 +54,17 @@
display: inline;
margin-left: $margin-1;
}
.maputnik-toolbar-logo {
flex: 0 0 140px;
}
.maputnik-toolbar__inner {
display: flex;
}
.maputnik-toolbar__actions {
white-space: nowrap;
flex: 1;
overflow-y: auto;
}

View File

@@ -67,3 +67,68 @@
.maputnik-zoom-spec-property .maputnik-input-block:not(:first-child) .maputnik-input-block-label {
visibility: hidden;
}
// DATA FUNC
.maputnik-make-data-function {
background-color: transparent;
display: inline-block;
padding-bottom: 0;
padding-top: 0;
vertical-align: middle;
@extend .maputnik-icon-button;
}
// DATA PROPERTY
.maputnik-data-spec-block {
overflow: auto;
}
.maputnik-data-spec-property {
.maputnik-input-block-label {
width: 30%;
}
.maputnik-input-block-content {
width: 70%;
}
.maputnik-data-spec-property-group {
margin-bottom: 3%;
.maputnik-doc-wrapper {
width: 25%;
color: $color-lowgray;
}
.maputnik-doc-wrapper:hover {
color: inherit;
}
.maputnik-data-spec-property-input {
width: 75%;
display: inline-block;
.maputnik-string {
margin-bottom: 3%;
}
}
}
}
.maputnik-data-spec-block {
.maputnik-data-spec-property-stop-edit,
.maputnik-data-spec-property-stop-data {
display: inline-block;
margin-bottom: 3%;
}
.maputnik-data-spec-property-stop-edit {
width: 18%;
margin-right: 3%;
}
.maputnik-data-spec-property-stop-data {
width: 78%;
}
}

View File

@@ -18,6 +18,7 @@ $font-size-6: 12px;
$font-family: Roboto, sans-serif;
$toolbar-height: 40px;
$toolbar-offset: 0;
@import 'mixins';
@import 'reset';
@@ -34,3 +35,4 @@ $toolbar-height: 40px;
@import 'filtereditor';
@import 'zoomproperty';
@import 'popup';
@import 'map';

View File

@@ -4,8 +4,73 @@
<meta charset="utf-8">
<title><%= htmlWebpackPlugin.options.title %></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="manifest" href="manifest.json">
<style>
html {
background-color: rgb(28, 31, 36);
}
#loader,
#loader:before,
#loader:after {
border-radius: 50%;
width: 2.5em;
height: 2.5em;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation: pulseload 1.8s infinite ease-in-out;
animation: pulseload 1.8s infinite ease-in-out;
}
#loader {
color: #ffffff;
font-size: 10px;
margin: 80px auto;
position: relative;
text-indent: -9999em;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
#loader:before,
#loader:after {
content: '';
position: absolute;
top: 0;
}
#loader:before {
left: -3.5em;
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
#loader:after {
left: 3.5em;
}
@-webkit-keyframes pulseload {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
@keyframes pulseload {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
</style>
</head>
<body>
<div id="app">Loading...</div>
<div id="app">
<div id="loader">Loading...</div>
</div>
</body>
</html>

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