mirror of
https://github.com/a-nyx/maputnik-with-pmtiles.git
synced 2025-01-01 01:03:16 +01:00
Merge branch 'master' into master
This commit is contained in:
commit
ad83f940a7
98 changed files with 16151 additions and 858 deletions
4
.babelrc
Normal file
4
.babelrc
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"presets": ["env", "react"],
|
||||||
|
"plugins": ["transform-object-rest-spread", "transform-class-properties"]
|
||||||
|
}
|
18
.travis.yml
18
.travis.yml
|
@ -3,24 +3,24 @@ addons:
|
||||||
firefox: latest
|
firefox: latest
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: linux
|
|
||||||
node_js: "4"
|
|
||||||
- os: linux
|
|
||||||
env: CXX=g++-4.8
|
|
||||||
node_js: "5"
|
|
||||||
- os: linux
|
- os: linux
|
||||||
node_js: "6"
|
node_js: "6"
|
||||||
- os: linux
|
- os: linux
|
||||||
env: CXX=g++-4.8
|
env: CXX=g++-4.8
|
||||||
node_js: "7"
|
node_js: "7"
|
||||||
- os: osx
|
- os: linux
|
||||||
node_js: "4"
|
node_js: "8"
|
||||||
- os: osx
|
- os: linux
|
||||||
node_js: "5"
|
env: CXX=g++-4.8
|
||||||
|
node_js: "9"
|
||||||
- os: osx
|
- os: osx
|
||||||
node_js: "6"
|
node_js: "6"
|
||||||
- os: osx
|
- os: osx
|
||||||
node_js: "7"
|
node_js: "7"
|
||||||
|
- os: osx
|
||||||
|
node_js: "8"
|
||||||
|
- os: osx
|
||||||
|
node_js: "9"
|
||||||
before_install:
|
before_install:
|
||||||
- export CHROME_BIN=chromium-browser
|
- export CHROME_BIN=chromium-browser
|
||||||
- export DISPLAY=:99.0
|
- export DISPLAY=:99.0
|
||||||
|
|
27
README.md
27
README.md
|
@ -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" />
|
<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/)
|
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.
|
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
|
- :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.
|
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
|
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
|
npm run build
|
||||||
|
@ -51,6 +68,10 @@ npm run lint
|
||||||
npm run lint-styles
|
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
|
## 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.
|
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.
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
environment:
|
environment:
|
||||||
matrix:
|
matrix:
|
||||||
- nodejs_version: "4"
|
|
||||||
- nodejs_version: "6"
|
- nodejs_version: "6"
|
||||||
- nodejs_version: "7"
|
- nodejs_version: "7"
|
||||||
|
- nodejs_version: "8"
|
||||||
|
- nodejs_version: "9"
|
||||||
platform:
|
platform:
|
||||||
- x86
|
- x86
|
||||||
- x64
|
- x64
|
||||||
|
|
6
circle.yml
Normal file
6
circle.yml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
machine:
|
||||||
|
node:
|
||||||
|
version: 6
|
||||||
|
test:
|
||||||
|
post:
|
||||||
|
- npm run build
|
|
@ -1,6 +1,6 @@
|
||||||
var webpack = require("webpack");
|
var webpack = require("webpack");
|
||||||
var WebpackDevServer = require("webpack-dev-server");
|
var WebpackDevServer = require("webpack-dev-server");
|
||||||
var webpackConfig = require("./webpack.config");
|
var webpackConfig = require("./webpack.production.config");
|
||||||
var testConfig = require("../test/config/specs");
|
var testConfig = require("../test/config/specs");
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ exports.config = {
|
||||||
browserName: 'firefox'
|
browserName: 'firefox'
|
||||||
}],
|
}],
|
||||||
sync: true,
|
sync: true,
|
||||||
logLevel: 'silent',
|
logLevel: 'verbose',
|
||||||
coloredLogs: true,
|
coloredLogs: true,
|
||||||
bail: 0,
|
bail: 0,
|
||||||
screenshotPath: './errorShots/',
|
screenshotPath: './errorShots/',
|
||||||
|
@ -29,16 +29,17 @@ exports.config = {
|
||||||
services: ['phantomjs'],
|
services: ['phantomjs'],
|
||||||
framework: 'mocha',
|
framework: 'mocha',
|
||||||
reporters: ['spec'],
|
reporters: ['spec'],
|
||||||
|
phantomjsOpts: {
|
||||||
|
webdriverLogfile: 'phantomjs.log'
|
||||||
|
},
|
||||||
mochaOpts: {
|
mochaOpts: {
|
||||||
ui: 'bdd',
|
ui: 'bdd',
|
||||||
// Because we don't know how long the initial build will take...
|
// Because we don't know how long the initial build will take...
|
||||||
timeout: 2*60*1000
|
timeout: 4*60*1000
|
||||||
},
|
},
|
||||||
onPrepare: function (config, capabilities) {
|
onPrepare: function (config, capabilities) {
|
||||||
var compiler = webpack(webpackConfig);
|
var compiler = webpack(webpackConfig);
|
||||||
server = new WebpackDevServer(compiler, {
|
server = new WebpackDevServer(compiler, {});
|
||||||
stats: "minimal"
|
|
||||||
});
|
|
||||||
server.listen(testConfig.port);
|
server.listen(testConfig.port);
|
||||||
},
|
},
|
||||||
onComplete: function(exitCode) {
|
onComplete: function(exitCode) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ var webpack = require('webpack');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var loaders = require('./webpack.loaders');
|
var loaders = require('./webpack.loaders');
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
|
|
||||||
const HOST = process.env.HOST || "127.0.0.1";
|
const HOST = process.env.HOST || "127.0.0.1";
|
||||||
const PORT = process.env.PORT || "8888";
|
const PORT = process.env.PORT || "8888";
|
||||||
|
@ -20,10 +21,13 @@ module.exports = {
|
||||||
filename: 'bundle.js'
|
filename: 'bundle.js'
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['', '.js', '.jsx']
|
extensions: ['.js', '.jsx']
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
loaders
|
noParse: [
|
||||||
|
/mapbox-gl\/dist\/mapbox-gl.js/
|
||||||
|
],
|
||||||
|
loaders: loaders
|
||||||
},
|
},
|
||||||
node: {
|
node: {
|
||||||
fs: "empty",
|
fs: "empty",
|
||||||
|
@ -41,14 +45,26 @@ module.exports = {
|
||||||
// serve index.html in place of 404 responses to allow HTML5 history
|
// serve index.html in place of 404 responses to allow HTML5 history
|
||||||
historyApiFallback: true,
|
historyApiFallback: true,
|
||||||
port: PORT,
|
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: [
|
plugins: [
|
||||||
new webpack.NoErrorsPlugin(),
|
new webpack.NoEmitOnErrorsPlugin(),
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
title: 'Maputnik',
|
title: 'Maputnik',
|
||||||
template: './src/template.html'
|
template: './src/template.html'
|
||||||
})
|
}),
|
||||||
|
new CopyWebpackPlugin([
|
||||||
|
{
|
||||||
|
from: './src/manifest.json',
|
||||||
|
to: 'manifest.json'
|
||||||
|
}
|
||||||
|
])
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,26 +4,38 @@ module.exports = [
|
||||||
exclude: /(node_modules|bower_components|public)/,
|
exclude: /(node_modules|bower_components|public)/,
|
||||||
loaders: ['react-hot-loader/webpack']
|
loaders: ['react-hot-loader/webpack']
|
||||||
},
|
},
|
||||||
|
// HACK: This is a massive hack and reaches into the mapbox-gl private API.
|
||||||
|
// We have to include this for access to `normalizeSourceURL`. We should
|
||||||
|
// remove this ASAP, see <https://github.com/mapbox/mapbox-gl-js/issues/2416>
|
||||||
|
{
|
||||||
|
test: /.*node_modules[\/\\]mapbox-gl[\/\\]src[\/\\]util[\/\\].*\.js/,
|
||||||
|
loader: 'babel-loader',
|
||||||
|
query: {
|
||||||
|
presets: ['env', 'react', 'flow'],
|
||||||
|
plugins: ['transform-runtime', 'transform-decorators-legacy', 'transform-class-properties'],
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.jsx?$/,
|
test: /\.jsx?$/,
|
||||||
exclude: /(node_modules|bower_components|public)/,
|
// Note: These modules aren't ES5 therefore we much compile them.
|
||||||
loader: 'babel',
|
exclude: /(.*node_modules(?![\/\\](@mapbox[\/\\]mapbox-gl-style-spec|ol|mapbox-to-ol-style))|bower_components|public)/,
|
||||||
|
loader: 'babel-loader',
|
||||||
query: {
|
query: {
|
||||||
presets: ['es2015', 'react'],
|
presets: ['env', 'react'],
|
||||||
plugins: ['transform-runtime', 'transform-decorators-legacy', 'transform-class-properties'],
|
plugins: ['transform-runtime', 'transform-decorators-legacy', 'transform-class-properties'],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(eot|ttf|woff|woff2)$/,
|
test: /\.(eot|ttf|woff|woff2)$/,
|
||||||
loader: 'file?name=fonts/[name].[ext]'
|
loader: 'file-loader?name=fonts/[name].[ext]'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.ico$/,
|
test: /\.ico$/,
|
||||||
loader: 'file?name=[name].[ext]'
|
loader: 'file-loader?name=[name].[ext]'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(svg|gif|jpg|png)$/,
|
test: /\.(svg|gif|jpg|png)$/,
|
||||||
loader: 'file?name=img/[name].[ext]'
|
loader: 'file-loader?name=img/[name].[ext]'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.json$/,
|
test: /\.json$/,
|
||||||
|
@ -36,8 +48,8 @@ module.exports = [
|
||||||
{
|
{
|
||||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
|
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
|
||||||
loaders: [
|
loaders: [
|
||||||
'style?sourceMap',
|
'style-loader?sourceMap',
|
||||||
'css'
|
'css-loader'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -5,6 +5,17 @@ var loaders = require('./webpack.loaders');
|
||||||
var ExtractTextPlugin = require('extract-text-webpack-plugin');
|
var ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
||||||
|
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
|
var UglifyJsPlugin = require('uglifyjs-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 = {
|
module.exports = {
|
||||||
entry: {
|
entry: {
|
||||||
|
@ -12,8 +23,6 @@ module.exports = {
|
||||||
vendor: [
|
vendor: [
|
||||||
'file-saver',
|
'file-saver',
|
||||||
'mapbox-gl/dist/mapbox-gl.js',
|
'mapbox-gl/dist/mapbox-gl.js',
|
||||||
//TODO: Build failure because cannot resolve migrations file
|
|
||||||
//"mapbox-gl-style-spec",
|
|
||||||
"lodash.clonedeep",
|
"lodash.clonedeep",
|
||||||
"lodash.throttle",
|
"lodash.throttle",
|
||||||
'color',
|
'color',
|
||||||
|
@ -32,14 +41,17 @@ module.exports = {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.join(__dirname, '..', 'public'),
|
path: OUTPATH,
|
||||||
filename: '[name].[chunkhash].js',
|
filename: '[name].[chunkhash].js',
|
||||||
chunkFilename: '[chunkhash].js'
|
chunkFilename: '[chunkhash].js'
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['', '.js', '.jsx']
|
extensions: ['.js', '.jsx']
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
|
noParse: [
|
||||||
|
/mapbox-gl\/dist\/mapbox-gl.js/
|
||||||
|
],
|
||||||
loaders
|
loaders
|
||||||
},
|
},
|
||||||
node: {
|
node: {
|
||||||
|
@ -48,21 +60,15 @@ module.exports = {
|
||||||
tls: 'empty'
|
tls: 'empty'
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.NoErrorsPlugin(),
|
new webpack.NoEmitOnErrorsPlugin(),
|
||||||
new webpack.optimize.CommonsChunkPlugin('vendor', '[chunkhash].vendor.js'),
|
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: '[chunkhash].vendor.js' }),
|
||||||
new WebpackCleanupPlugin(),
|
new WebpackCleanupPlugin(),
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': {
|
'process.env': {
|
||||||
NODE_ENV: '"production"'
|
NODE_ENV: '"production"'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new webpack.optimize.UglifyJsPlugin({
|
new UglifyJsPlugin(),
|
||||||
compress: {
|
|
||||||
warnings: false,
|
|
||||||
screw_ie8: true,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
new webpack.optimize.OccurenceOrderPlugin(),
|
|
||||||
new ExtractTextPlugin('[contenthash].css', {
|
new ExtractTextPlugin('[contenthash].css', {
|
||||||
allChunks: true
|
allChunks: true
|
||||||
}),
|
}),
|
||||||
|
@ -70,6 +76,19 @@ module.exports = {
|
||||||
template: './src/template.html',
|
template: './src/template.html',
|
||||||
title: 'Maputnik'
|
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',
|
||||||
|
})
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
13986
package-lock.json
generated
Normal file
13986
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
149
package.json
149
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "maputnik",
|
"name": "maputnik",
|
||||||
"version": "1.0.1",
|
"version": "1.1.0",
|
||||||
"description": "A MapboxGL visual style editor",
|
"description": "A MapboxGL visual style editor",
|
||||||
"main": "''",
|
"main": "''",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -8,9 +8,10 @@
|
||||||
"build": "webpack --config config/webpack.production.config.js --progress --profile --colors",
|
"build": "webpack --config config/webpack.production.config.js --progress --profile --colors",
|
||||||
"test": "wdio config/wdio.conf.js",
|
"test": "wdio config/wdio.conf.js",
|
||||||
"test-watch": "wdio config/wdio.conf.js --watch",
|
"test-watch": "wdio config/wdio.conf.js --watch",
|
||||||
"start": "webpack-dev-server --progress --profile --colors --watch-poll --config config/webpack.config.js",
|
"start": "webpack-dev-server --progress --profile --colors --config config/webpack.config.js",
|
||||||
"lint": "eslint --ext js --ext jsx {src,test}",
|
"lint": "eslint --ext js --ext jsx {src,test}",
|
||||||
"lint-styles": "stylelint 'src/styles/*.scss'"
|
"lint-styles": "stylelint 'src/styles/*.scss'",
|
||||||
|
"nsp": "nsp check --reporter summary"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -20,49 +21,43 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/maputnik/editor#readme",
|
"homepage": "https://github.com/maputnik/editor#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/mapbox-gl-rtl-text": "^0.1.0",
|
"@mapbox/mapbox-gl-rtl-text": "^0.1.1",
|
||||||
|
"@mapbox/mapbox-gl-style-spec": "^11.1.1",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"codemirror": "^5.18.2",
|
"codemirror": "^5.32.0",
|
||||||
"color": "^1.0.3",
|
"color": "^2.0.0",
|
||||||
"file-saver": "^1.3.2",
|
"file-saver": "^1.3.3",
|
||||||
"github-api": "^3.0.0",
|
"github-api": "^3.0.0",
|
||||||
"jsonlint": "josdejong/jsonlint#85a19d7",
|
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
||||||
"lodash.capitalize": "^4.2.1",
|
"lodash.capitalize": "^4.2.1",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"lodash.isequal": "^4.4.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"mapbox-gl": "^0.33.0",
|
"mapbox-gl": "^0.44.1",
|
||||||
"mapbox-gl-inspect": "^1.2.2",
|
"mapbox-gl-inspect": "^1.3.0",
|
||||||
"mapbox-gl-style-spec": "^8.11.0",
|
"maputnik-design": "github:maputnik/design",
|
||||||
"mousetrap": "^1.6.0",
|
"mousetrap": "^1.6.1",
|
||||||
"ol-mapbox-style": "1.0.1",
|
"ol-mapbox-style": "^2.10.1",
|
||||||
"openlayers": "^3.19.1",
|
"ol": "^4.6.4",
|
||||||
"react": "^15.4.0",
|
"prop-types": "^15.6.0",
|
||||||
"react-addons-pure-render-mixin": "^15.4.0",
|
"react": "16.0.0",
|
||||||
"react-autocomplete": "^1.4.0",
|
"react-addons-pure-render-mixin": "^15.6.2",
|
||||||
"react-codemirror": "^0.3.0",
|
"react-autocomplete": "^1.7.2",
|
||||||
"react-collapse": "^4.0.2",
|
"react-codemirror2": "^3.0.7",
|
||||||
"react-color": "^2.10.0",
|
"react-collapse": "^4.0.3",
|
||||||
"react-dom": "^15.4.0",
|
"react-color": "^2.13.8",
|
||||||
"react-file-reader-input": "^1.1.0",
|
"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-height": "^3.0.0",
|
||||||
"react-icon-base": "^2.0.4",
|
"react-icon-base": "^2.1.1",
|
||||||
"react-icons": "^2.2.1",
|
"react-icons": "^2.2.7",
|
||||||
"react-motion": "^0.4.7",
|
"react-motion": "^0.5.2",
|
||||||
"react-sortable-hoc": "^0.4.5",
|
"react-sortable-hoc": "^0.6.8",
|
||||||
"reconnecting-websocket": "^3.0.3",
|
"reconnecting-websocket": "^3.2.2",
|
||||||
"request": "^2.79.0",
|
"request": "^2.83.0",
|
||||||
"url": "^0.11.0"
|
"url": "^0.11.0"
|
||||||
},
|
},
|
||||||
"babel": {
|
|
||||||
"presets": [
|
|
||||||
"es2015",
|
|
||||||
"react"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"transform-object-rest-spread"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"jshintConfig": {
|
"jshintConfig": {
|
||||||
"esversion": 6
|
"esversion": 6
|
||||||
},
|
},
|
||||||
|
@ -73,7 +68,7 @@
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"react"
|
"react"
|
||||||
],
|
],
|
||||||
"extend": [
|
"extends": [
|
||||||
"plugin:react/recommended"
|
"plugin:react/recommended"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
|
@ -93,45 +88,51 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "6.21.0",
|
"babel-core": "^6.26.0",
|
||||||
"babel-eslint": "^7.1.1",
|
"babel-eslint": "^8.0.2",
|
||||||
"babel-loader": "6.2.10",
|
"babel-loader": "7.1.1",
|
||||||
"babel-plugin-transform-class-properties": "^6.11.5",
|
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||||
"babel-plugin-transform-flow-strip-types": "^6.21.0",
|
"babel-plugin-transform-flow-strip-types": "^6.22.0",
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.8.0",
|
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||||
"babel-plugin-transform-runtime": "^6.15.0",
|
"babel-plugin-transform-runtime": "^6.23.0",
|
||||||
"babel-preset-es2015": "6.18.0",
|
"babel-preset-env": "^1.6.1",
|
||||||
"babel-preset-react": "6.16.0",
|
"babel-preset-es2015": "^6.24.1",
|
||||||
"babel-runtime": "^6.11.6",
|
"babel-preset-flow": "^6.23.0",
|
||||||
|
"babel-preset-react": "^6.24.1",
|
||||||
|
"babel-runtime": "^6.26.0",
|
||||||
"base64-loader": "^1.0.0",
|
"base64-loader": "^1.0.0",
|
||||||
"css-loader": "0.26.1",
|
"copy-webpack-plugin": "^4.2.0",
|
||||||
"eslint": "^3.5.0",
|
"css-loader": "^0.28.7",
|
||||||
"eslint-plugin-react": "^6.2.0",
|
"eslint": "^4.10.0",
|
||||||
"extract-text-webpack-plugin": "^1.0.1",
|
"eslint-plugin-react": "^7.4.0",
|
||||||
"file-loader": "^0.11.1",
|
"extract-text-webpack-plugin": "^3.0.2",
|
||||||
"html-webpack-plugin": "^2.22.0",
|
"file-loader": "^1.1.5",
|
||||||
"json-loader": "^0.5.4",
|
"html-webpack-plugin": "^2.30.1",
|
||||||
"karma": "^1.3.0",
|
"json-loader": "^0.5.7",
|
||||||
"karma-chrome-launcher": "^2.0.0",
|
"karma": "^1.7.1",
|
||||||
"karma-firefox-launcher": "^1.0.0",
|
"karma-chrome-launcher": "^2.2.0",
|
||||||
|
"karma-firefox-launcher": "^1.0.1",
|
||||||
"karma-mocha": "^1.3.0",
|
"karma-mocha": "^1.3.0",
|
||||||
"karma-webpack": "^2.0.1",
|
"karma-webpack": "^2.0.5",
|
||||||
"mocha": "^3.1.2",
|
"mocha": "^4.0.1",
|
||||||
"mocha-loader": "^1.0.0",
|
"mocha-loader": "^1.1.1",
|
||||||
"node-sass": "^4.2.0",
|
"node-sass": "^4.6.0",
|
||||||
"react-hot-loader": "^3.0.0-beta.6",
|
"nsp": "^3.1.0",
|
||||||
"sass-loader": "^4.0.1",
|
"react-hot-loader": "^3.1.1",
|
||||||
"style-loader": "0.13.1",
|
"sass-loader": "^6.0.6",
|
||||||
"stylelint": "^7.7.1",
|
"style-loader": "^0.19.0",
|
||||||
|
"stylelint": "^7.13.0",
|
||||||
"stylelint-config-standard": "^15.0.1",
|
"stylelint-config-standard": "^15.0.1",
|
||||||
"transform-loader": "^0.2.3",
|
"transform-loader": "^0.2.4",
|
||||||
"wdio-mocha-framework": "^0.5.9",
|
"uglifyjs-webpack-plugin": "^1.1.8",
|
||||||
|
"wdio-mocha-framework": "^0.5.11",
|
||||||
"wdio-phantomjs-service": "^0.2.2",
|
"wdio-phantomjs-service": "^0.2.2",
|
||||||
"wdio-spec-reporter": "^0.1.0",
|
"wdio-spec-reporter": "^0.1.2",
|
||||||
"webdriverio": "^4.6.2",
|
"webdriverio": "^4.8.0",
|
||||||
"webpack": "1.14.0",
|
"webpack": "^3.8.1",
|
||||||
"webpack-cleanup-plugin": "^0.4.1",
|
"webpack-bundle-analyzer": "^2.9.0",
|
||||||
"webpack-dev-server": "1.16.2"
|
"webpack-cleanup-plugin": "^0.5.1",
|
||||||
|
"webpack-dev-server": "^2.9.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,7 @@ import AppLayout from './AppLayout'
|
||||||
import MessagePanel from './MessagePanel'
|
import MessagePanel from './MessagePanel'
|
||||||
|
|
||||||
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
|
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
|
||||||
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
|
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||||
import validateStyleMin from 'mapbox-gl-style-spec/lib/validate_style.min'
|
|
||||||
import formatStyle from 'mapbox-gl-style-spec/lib/format'
|
|
||||||
import style from '../libs/style.js'
|
import style from '../libs/style.js'
|
||||||
import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen'
|
import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen'
|
||||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
||||||
|
@ -21,6 +19,11 @@ import { ApiStyleStore } from '../libs/apistore'
|
||||||
import { RevisionStore } from '../libs/revisions'
|
import { RevisionStore } from '../libs/revisions'
|
||||||
import LayerWatcher from '../libs/layerwatcher'
|
import LayerWatcher from '../libs/layerwatcher'
|
||||||
import tokens from '../config/tokens.json'
|
import tokens from '../config/tokens.json'
|
||||||
|
import isEqual from 'lodash.isequal'
|
||||||
|
|
||||||
|
import MapboxGl from 'mapbox-gl'
|
||||||
|
import mapboxUtil from 'mapbox-gl/src/util/mapbox'
|
||||||
|
|
||||||
|
|
||||||
function updateRootSpec(spec, fieldName, newValues) {
|
function updateRootSpec(spec, fieldName, newValues) {
|
||||||
return {
|
return {
|
||||||
|
@ -65,11 +68,10 @@ export default class App extends React.Component {
|
||||||
sources: {},
|
sources: {},
|
||||||
vectorLayers: {},
|
vectorLayers: {},
|
||||||
inspectModeEnabled: false,
|
inspectModeEnabled: false,
|
||||||
spec: GlSpec,
|
spec: styleSpec.latest,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.layerWatcher = new LayerWatcher({
|
this.layerWatcher = new LayerWatcher({
|
||||||
onSourcesChange: v => this.setState({ sources: v }),
|
|
||||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -96,7 +98,9 @@ export default class App extends React.Component {
|
||||||
updateFonts(urlTemplate) {
|
updateFonts(urlTemplate) {
|
||||||
const metadata = this.state.mapStyle.metadata || {}
|
const metadata = this.state.mapStyle.metadata || {}
|
||||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
||||||
downloadGlyphsMetadata(urlTemplate.replace('{key}', accessToken), fonts => {
|
|
||||||
|
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
|
||||||
|
downloadGlyphsMetadata(glyphUrl, fonts => {
|
||||||
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -108,6 +112,10 @@ export default class App extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onStyleChanged(newStyle, save=true) {
|
onStyleChanged(newStyle, save=true) {
|
||||||
|
|
||||||
|
const errors = styleSpec.validate(newStyle, styleSpec.latest)
|
||||||
|
if(errors.length === 0) {
|
||||||
|
|
||||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||||
this.updateFonts(newStyle.glyphs)
|
this.updateFonts(newStyle.glyphs)
|
||||||
}
|
}
|
||||||
|
@ -115,8 +123,6 @@ export default class App extends React.Component {
|
||||||
this.updateIcons(newStyle.sprite)
|
this.updateIcons(newStyle.sprite)
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors = validateStyleMin(newStyle, GlSpec)
|
|
||||||
if(errors.length === 0) {
|
|
||||||
this.revisionStore.addRevision(newStyle)
|
this.revisionStore.addRevision(newStyle)
|
||||||
if(save) this.saveStyle(newStyle)
|
if(save) this.saveStyle(newStyle)
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -128,6 +134,8 @@ export default class App extends React.Component {
|
||||||
errors: errors.map(err => err.message)
|
errors: errors.map(err => err.message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.fetchSources();
|
||||||
}
|
}
|
||||||
|
|
||||||
onUndo() {
|
onUndo() {
|
||||||
|
@ -184,11 +192,68 @@ 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" && val.hasOwnProperty("url")) {
|
||||||
|
let url = val.url;
|
||||||
|
try {
|
||||||
|
url = mapboxUtil.normalizeSourceURL(url, MapboxGl.accessToken);
|
||||||
|
} catch(err) {
|
||||||
|
console.warn("Failed to normalizeSourceURL: ", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then((response) => {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((json) => {
|
||||||
|
if(!json.hasOwnProperty("vector_layers")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new objects before setState
|
||||||
|
const sources = Object.assign({}, this.state.sources);
|
||||||
|
|
||||||
|
for(let layer of json.vector_layers) {
|
||||||
|
sources[key].layers.push(layer.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug("Updating source: "+key);
|
||||||
|
this.setState({
|
||||||
|
sources: sources
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to process sources for '%s'", url, err);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isEqual(this.state.sources, sourceList)) {
|
||||||
|
console.debug("Setting sources");
|
||||||
|
this.setState({
|
||||||
|
sources: sourceList
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mapRenderer() {
|
mapRenderer() {
|
||||||
const mapProps = {
|
const mapProps = {
|
||||||
mapStyle: style.replaceAccessToken(this.state.mapStyle),
|
mapStyle: style.replaceAccessToken(this.state.mapStyle, {allowFallback: true}),
|
||||||
onDataChange: (e) => {
|
onDataChange: (e) => {
|
||||||
this.layerWatcher.analyzeMap(e.map)
|
this.layerWatcher.analyzeMap(e.map)
|
||||||
|
this.fetchSources();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,7 +266,8 @@ export default class App extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
return <MapboxGlMap {...mapProps}
|
return <MapboxGlMap {...mapProps}
|
||||||
inspectModeEnabled={this.state.inspectModeEnabled}
|
inspectModeEnabled={this.state.inspectModeEnabled}
|
||||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]} />
|
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
||||||
|
onLayerSelect={this.onLayerSelect.bind(this)} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import ScrollContainer from './ScrollContainer'
|
import ScrollContainer from './ScrollContainer'
|
||||||
|
|
||||||
class AppLayout extends React.Component {
|
class AppLayout extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
toolbar: React.PropTypes.element.isRequired,
|
toolbar: PropTypes.element.isRequired,
|
||||||
layerList: React.PropTypes.element.isRequired,
|
layerList: PropTypes.element.isRequired,
|
||||||
layerEditor: React.PropTypes.element,
|
layerEditor: PropTypes.element,
|
||||||
map: React.PropTypes.element.isRequired,
|
map: PropTypes.element.isRequired,
|
||||||
bottom: React.PropTypes.element,
|
bottom: PropTypes.element,
|
||||||
}
|
}
|
||||||
|
|
||||||
static childContextTypes = {
|
static childContextTypes = {
|
||||||
reactIconBase: React.PropTypes.object
|
reactIconBase: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
getChildContext() {
|
getChildContext() {
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
class Button extends React.Component {
|
class Button extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onClick: React.PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
style: React.PropTypes.object,
|
style: PropTypes.object,
|
||||||
className: React.PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
children: PropTypes.node
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
class MessagePanel extends React.Component {
|
class MessagePanel extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
errors: React.PropTypes.array,
|
errors: PropTypes.array,
|
||||||
infos: React.PropTypes.array,
|
infos: PropTypes.array,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const errors = this.props.errors.map((m, i) => {
|
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) => {
|
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">
|
return <div className="maputnik-message-panel">
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
import React from 'react'
|
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">
|
return <div className="maputnik-scroll-container">
|
||||||
{props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default ScrollContainer
|
export default ScrollContainer
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import FileReaderInput from 'react-file-reader-input'
|
import FileReaderInput from 'react-file-reader-input'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
|
@ -16,47 +17,71 @@ import MdFontDownload from 'react-icons/lib/md/font-download'
|
||||||
import HelpIcon from 'react-icons/lib/md/help-outline'
|
import HelpIcon from 'react-icons/lib/md/help-outline'
|
||||||
import InspectionIcon from 'react-icons/lib/md/find-in-page'
|
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 SettingsModal from './modals/SettingsModal'
|
||||||
import ExportModal from './modals/ExportModal'
|
import ExportModal from './modals/ExportModal'
|
||||||
import SourcesModal from './modals/SourcesModal'
|
import SourcesModal from './modals/SourcesModal'
|
||||||
import OpenModal from './modals/OpenModal'
|
import OpenModal from './modals/OpenModal'
|
||||||
|
import pkgJson from '../../package.json'
|
||||||
|
|
||||||
import style from '../libs/style'
|
import style from '../libs/style'
|
||||||
|
|
||||||
function IconText(props) {
|
class IconText extends React.Component {
|
||||||
return <span className="maputnik-icon-text">{props.children}</span>
|
static propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarLink(props) {
|
render() {
|
||||||
|
return <span className="maputnik-icon-text">{this.props.children}</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolbarLink extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
children: PropTypes.node,
|
||||||
|
href: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
return <a
|
return <a
|
||||||
className={classnames('maputnik-toolbar-link', props.className)}
|
className={classnames('maputnik-toolbar-link', this.props.className)}
|
||||||
href={props.href}
|
href={this.props.href}
|
||||||
target={"blank"}
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
{props.children}
|
{this.props.children}
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ToolbarAction(props) {
|
class ToolbarAction extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
onClick: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
return <a
|
return <a
|
||||||
className='maputnik-toolbar-action'
|
className='maputnik-toolbar-action'
|
||||||
onClick={props.onClick}
|
onClick={this.props.onClick}
|
||||||
>
|
>
|
||||||
{props.children}
|
{this.props.children}
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class Toolbar extends React.Component {
|
export default class Toolbar extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
mapStyle: React.PropTypes.object.isRequired,
|
mapStyle: PropTypes.object.isRequired,
|
||||||
inspectModeEnabled: React.PropTypes.bool.isRequired,
|
inspectModeEnabled: PropTypes.bool.isRequired,
|
||||||
onStyleChanged: React.PropTypes.func.isRequired,
|
onStyleChanged: PropTypes.func.isRequired,
|
||||||
// A new style has been uploaded
|
// 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
|
// A dict of source id's and the available source layers
|
||||||
sources: React.PropTypes.object.isRequired,
|
sources: PropTypes.object.isRequired,
|
||||||
onInspectModeToggle: React.PropTypes.func.isRequired
|
onInspectModeToggle: PropTypes.func.isRequired,
|
||||||
|
children: PropTypes.node
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -106,13 +131,17 @@ export default class Toolbar extends React.Component {
|
||||||
isOpen={this.state.isOpen.sources}
|
isOpen={this.state.isOpen.sources}
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
||||||
/>
|
/>
|
||||||
|
<div className="maputnik-toolbar__inner">
|
||||||
<ToolbarLink
|
<ToolbarLink
|
||||||
href={"https://github.com/maputnik/editor"}
|
href={"https://github.com/maputnik/editor"}
|
||||||
className="maputnik-toolbar-logo"
|
className="maputnik-toolbar-logo"
|
||||||
>
|
>
|
||||||
<img src={logoImage} alt="Maputnik" />
|
<img src={logoImage} alt="Maputnik" />
|
||||||
<h1>Maputnik</h1>
|
<h1>Maputnik
|
||||||
|
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
||||||
|
</h1>
|
||||||
</ToolbarLink>
|
</ToolbarLink>
|
||||||
|
<div className="maputnik-toolbar__actions">
|
||||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
|
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
|
||||||
<OpenIcon />
|
<OpenIcon />
|
||||||
<IconText>Open</IconText>
|
<IconText>Open</IconText>
|
||||||
|
@ -141,5 +170,7 @@ export default class Toolbar extends React.Component {
|
||||||
<IconText>Help</IconText>
|
<IconText>Help</IconText>
|
||||||
</ToolbarLink>
|
</ToolbarLink>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Color from 'color'
|
import Color from 'color'
|
||||||
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
function formatColor(color) {
|
function formatColor(color) {
|
||||||
const rgb = color.rgb
|
const rgb = color.rgb
|
||||||
|
@ -10,12 +11,12 @@ function formatColor(color) {
|
||||||
/*** Number fields with support for min, max and units and documentation*/
|
/*** Number fields with support for min, max and units and documentation*/
|
||||||
class ColorField extends React.Component {
|
class ColorField extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
name: React.PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: React.PropTypes.string,
|
value: PropTypes.string,
|
||||||
doc: React.PropTypes.string,
|
doc: PropTypes.string,
|
||||||
style: React.PropTypes.object,
|
style: PropTypes.object,
|
||||||
default: React.PropTypes.string,
|
default: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
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
|
//but I am too stupid to get it to work together with fixed position
|
||||||
//and scrollbars so I have to fallback to JavaScript
|
//and scrollbars so I have to fallback to JavaScript
|
||||||
calcPickerOffset() {
|
calcPickerOffset() {
|
||||||
const elem = this.refs.colorInput
|
const elem = this.colorInput
|
||||||
if(elem) {
|
if(elem) {
|
||||||
const pos = elem.getBoundingClientRect()
|
const pos = elem.getBoundingClientRect()
|
||||||
return {
|
return {
|
||||||
|
@ -37,7 +38,6 @@ class ColorField extends React.Component {
|
||||||
left: pos.left + 196,
|
left: pos.left + 196,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn('Color field has no element to adjust position')
|
|
||||||
return {
|
return {
|
||||||
top: 160,
|
top: 160,
|
||||||
left: 555,
|
left: 555,
|
||||||
|
@ -50,7 +50,14 @@ class ColorField extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
get color() {
|
get color() {
|
||||||
return Color(this.props.value || '#fff').rgb()
|
// 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() {
|
render() {
|
||||||
|
@ -91,7 +98,7 @@ class ColorField extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
var swatchStyle = {
|
var swatchStyle = {
|
||||||
"background-color": this.props.value
|
backgroundColor: this.props.value
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className="maputnik-color-wrapper">
|
return <div className="maputnik-color-wrapper">
|
||||||
|
@ -99,7 +106,7 @@ class ColorField extends React.Component {
|
||||||
<div className="maputnik-color-swatch" style={swatchStyle}></div>
|
<div className="maputnik-color-swatch" style={swatchStyle}></div>
|
||||||
<input
|
<input
|
||||||
className="maputnik-color"
|
className="maputnik-color"
|
||||||
ref="colorInput"
|
ref={(input) => this.colorInput = input}
|
||||||
onClick={this.togglePicker.bind(this)}
|
onClick={this.togglePicker.bind(this)}
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
name={this.props.name}
|
name={this.props.name}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
export default class DocLabel extends React.Component {
|
export default class DocLabel extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
label: React.PropTypes.oneOfType([
|
label: PropTypes.oneOfType([
|
||||||
React.PropTypes.object,
|
PropTypes.object,
|
||||||
React.PropTypes.string
|
PropTypes.string
|
||||||
]).isRequired,
|
]).isRequired,
|
||||||
doc: React.PropTypes.string.isRequired,
|
doc: PropTypes.string.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
140
src/components/fields/FunctionSpecField.jsx
Normal file
140
src/components/fields/FunctionSpecField.jsx
Normal 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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react'
|
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']
|
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
||||||
|
|
||||||
/** Extract field spec by {@fieldName} from the {@layerType} in the
|
/** 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 {
|
export default class PropertyGroup extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
layer: React.PropTypes.object.isRequired,
|
layer: PropTypes.object.isRequired,
|
||||||
groupFields: React.PropTypes.array.isRequired,
|
groupFields: PropTypes.array.isRequired,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
spec: React.PropTypes.object.isRequired,
|
spec: PropTypes.object.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
onPropertyChange(property, newValue) {
|
onPropertyChange(property, newValue) {
|
||||||
|
@ -54,7 +55,7 @@ export default class PropertyGroup extends React.Component {
|
||||||
const layout = this.props.layer.layout || {}
|
const layout = this.props.layer.layout || {}
|
||||||
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
|
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
|
||||||
|
|
||||||
return <ZoomSpecField
|
return <FunctionSpecField
|
||||||
onChange={this.onPropertyChange.bind(this)}
|
onChange={this.onPropertyChange.bind(this)}
|
||||||
key={fieldName}
|
key={fieldName}
|
||||||
fieldName={fieldName}
|
fieldName={fieldName}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import color from 'color'
|
import color from 'color'
|
||||||
|
|
||||||
import ColorField from './ColorField'
|
import ColorField from './ColorField'
|
||||||
|
@ -8,6 +9,7 @@ import StringInput from '../inputs/StringInput'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
import MultiButtonInput from '../inputs/MultiButtonInput'
|
import MultiButtonInput from '../inputs/MultiButtonInput'
|
||||||
import ArrayInput from '../inputs/ArrayInput'
|
import ArrayInput from '../inputs/ArrayInput'
|
||||||
|
import DynamicArrayInput from '../inputs/DynamicArrayInput'
|
||||||
import FontInput from '../inputs/FontInput'
|
import FontInput from '../inputs/FontInput'
|
||||||
import IconInput from '../inputs/IconInput'
|
import IconInput from '../inputs/IconInput'
|
||||||
import capitalize from 'lodash.capitalize'
|
import capitalize from 'lodash.capitalize'
|
||||||
|
@ -35,16 +37,17 @@ function optionsLabelLength(options) {
|
||||||
* to display @{value}. */
|
* to display @{value}. */
|
||||||
export default class SpecField extends React.Component {
|
export default class SpecField extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
fieldName: React.PropTypes.string.isRequired,
|
fieldName: PropTypes.string.isRequired,
|
||||||
fieldSpec: React.PropTypes.object.isRequired,
|
fieldSpec: PropTypes.object.isRequired,
|
||||||
value: React.PropTypes.oneOfType([
|
value: PropTypes.oneOfType([
|
||||||
React.PropTypes.string,
|
PropTypes.string,
|
||||||
React.PropTypes.number,
|
PropTypes.number,
|
||||||
React.PropTypes.array,
|
PropTypes.array,
|
||||||
|
PropTypes.bool
|
||||||
]),
|
]),
|
||||||
/** Override the style of the field */
|
/** Override the style of the field */
|
||||||
style: React.PropTypes.object,
|
style: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -105,11 +108,18 @@ export default class SpecField extends React.Component {
|
||||||
fonts={this.props.fieldSpec.values}
|
fonts={this.props.fieldSpec.values}
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
|
if (this.props.fieldSpec.length) {
|
||||||
return <ArrayInput
|
return <ArrayInput
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
type={this.props.fieldSpec.value}
|
type={this.props.fieldSpec.value}
|
||||||
length={this.props.fieldSpec.length}
|
length={this.props.fieldSpec.length}
|
||||||
/>
|
/>
|
||||||
|
} else {
|
||||||
|
return <DynamicArrayInput
|
||||||
|
{...commonProps}
|
||||||
|
type={this.props.fieldSpec.value}
|
||||||
|
/>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
default: return null
|
default: return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
176
src/components/fields/_DataProperty.jsx
Normal file
176
src/components/fields/_DataProperty.jsx
Normal 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>
|
||||||
|
}
|
||||||
|
}
|
25
src/components/fields/_DeleteStopButton.jsx
Normal file
25
src/components/fields/_DeleteStopButton.jsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import DocLabel from './DocLabel'
|
||||||
|
import Button from '../Button'
|
||||||
|
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||||
|
|
||||||
|
|
||||||
|
export default class DeleteStopButton extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Button
|
||||||
|
className="maputnik-delete-stop"
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
>
|
||||||
|
<DocLabel
|
||||||
|
label={<DeleteIcon />}
|
||||||
|
doc={"Remove zoom level stop."}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
}
|
49
src/components/fields/_FunctionButtons.jsx
Normal file
49
src/components/fields/_FunctionButtons.jsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import DocLabel from './DocLabel'
|
||||||
|
import Button from '../Button'
|
||||||
|
import FunctionIcon from 'react-icons/lib/md/functions'
|
||||||
|
import MdInsertChart from 'react-icons/lib/md/insert-chart'
|
||||||
|
|
||||||
|
|
||||||
|
export default class FunctionButtons extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
fieldSpec: PropTypes.object,
|
||||||
|
onZoomClick: PropTypes.func,
|
||||||
|
onDataClick: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let makeZoomButton, makeDataButton
|
||||||
|
if (this.props.fieldSpec['zoom-function']) {
|
||||||
|
makeZoomButton = <Button
|
||||||
|
className="maputnik-make-zoom-function"
|
||||||
|
onClick={this.props.onZoomClick}
|
||||||
|
>
|
||||||
|
<DocLabel
|
||||||
|
label={<FunctionIcon />}
|
||||||
|
cursorTargetStyle={{ cursor: 'pointer' }}
|
||||||
|
doc={"Turn property into a zoom function to enable a map feature to change with map's zoom level."}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
if (this.props.fieldSpec['property-function'] && ['piecewise-constant', 'interpolated'].indexOf(this.props.fieldSpec['function']) !== -1) {
|
||||||
|
makeDataButton = <Button
|
||||||
|
className="maputnik-make-data-function"
|
||||||
|
onClick={this.props.onDataClick}
|
||||||
|
>
|
||||||
|
<DocLabel
|
||||||
|
label={<MdInsertChart />}
|
||||||
|
cursorTargetStyle={{ cursor: 'pointer' }}
|
||||||
|
doc={"Turn property into a data function to enable a map feature to change according to data properties and the map's zoom level."}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
return <div>{makeDataButton}{makeZoomButton}</div>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
src/components/fields/_SpecProperty.jsx
Normal file
34
src/components/fields/_SpecProperty.jsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import SpecField from './SpecField'
|
||||||
|
import FunctionButtons from './_FunctionButtons'
|
||||||
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
|
||||||
|
import labelFromFieldName from './_labelFromFieldName'
|
||||||
|
|
||||||
|
|
||||||
|
export default class SpecProperty extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onZoomClick: PropTypes.func.isRequired,
|
||||||
|
onDataClick: PropTypes.func.isRequired,
|
||||||
|
fieldName: PropTypes.string,
|
||||||
|
fieldSpec: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const functionBtn = <FunctionButtons
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
onZoomClick={this.props.onZoomClick}
|
||||||
|
onDataClick={this.props.onDataClick}
|
||||||
|
/>
|
||||||
|
|
||||||
|
return <InputBlock
|
||||||
|
doc={this.props.fieldSpec.doc}
|
||||||
|
label={labelFromFieldName(this.props.fieldName)}
|
||||||
|
action={functionBtn}
|
||||||
|
>
|
||||||
|
<SpecField {...this.props} />
|
||||||
|
</InputBlock>
|
||||||
|
}
|
||||||
|
}
|
161
src/components/fields/_ZoomProperty.jsx
Normal file
161
src/components/fields/_ZoomProperty.jsx
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import Button from '../Button'
|
||||||
|
import SpecField from './SpecField'
|
||||||
|
import NumberInput from '../inputs/NumberInput'
|
||||||
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
|
||||||
|
import DeleteStopButton from './_DeleteStopButton'
|
||||||
|
import labelFromFieldName from './_labelFromFieldName'
|
||||||
|
|
||||||
|
import docUid from '../../libs/document-uid'
|
||||||
|
import sortNumerically from '../../libs/sort-numerically'
|
||||||
|
|
||||||
|
|
||||||
|
export default class ZoomProperty extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
onDeleteStop: PropTypes.func,
|
||||||
|
onAddStop: PropTypes.func,
|
||||||
|
fieldName: PropTypes.string,
|
||||||
|
fieldSpec: PropTypes.object,
|
||||||
|
value: PropTypes.oneOfType([
|
||||||
|
PropTypes.object,
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.number,
|
||||||
|
PropTypes.bool,
|
||||||
|
PropTypes.array
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
this.state = {
|
||||||
|
refs: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.setState({
|
||||||
|
refs: this.setStopRefs(this.props)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We cache a reference for each stop by its index.
|
||||||
|
*
|
||||||
|
* When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus.
|
||||||
|
*/
|
||||||
|
setStopRefs(props) {
|
||||||
|
// This is initialsed below only if required to improved performance.
|
||||||
|
let newRefs;
|
||||||
|
|
||||||
|
if(props.value && props.value.stops) {
|
||||||
|
props.value.stops.forEach((val, idx) => {
|
||||||
|
if(!this.state.refs.hasOwnProperty(idx)) {
|
||||||
|
if(!newRefs) {
|
||||||
|
newRefs = {...this.state.refs};
|
||||||
|
}
|
||||||
|
newRefs[idx] = docUid("stop-");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
const newRefs = this.setStopRefs(nextProps);
|
||||||
|
if(newRefs) {
|
||||||
|
this.setState({
|
||||||
|
refs: newRefs
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order the stops altering the refs to reflect their new position.
|
||||||
|
orderStopsByZoom(stops) {
|
||||||
|
const mappedWithRef = stops
|
||||||
|
.map((stop, idx) => {
|
||||||
|
return {
|
||||||
|
ref: this.state.refs[idx],
|
||||||
|
data: stop
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Sort by zoom
|
||||||
|
.sort((a, b) => sortNumerically(a.data[0], b.data[0]));
|
||||||
|
|
||||||
|
// Fetch the new position of the stops
|
||||||
|
const newRefs = {};
|
||||||
|
mappedWithRef
|
||||||
|
.forEach((stop, idx) =>{
|
||||||
|
newRefs[idx] = stop.ref;
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
refs: newRefs
|
||||||
|
});
|
||||||
|
|
||||||
|
return mappedWithRef.map((item) => item.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeZoomStop(changeIdx, stopData, value) {
|
||||||
|
const stops = this.props.value.stops.slice(0);
|
||||||
|
stops[changeIdx] = [stopData, value];
|
||||||
|
|
||||||
|
const orderedStops = this.orderStopsByZoom(stops);
|
||||||
|
|
||||||
|
const changedValue = {
|
||||||
|
...this.props.value,
|
||||||
|
stops: orderedStops
|
||||||
|
}
|
||||||
|
this.props.onChange(this.props.fieldName, changedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const zoomFields = this.props.value.stops.map((stop, idx) => {
|
||||||
|
const zoomLevel = stop[0]
|
||||||
|
const key = this.state.refs[idx];
|
||||||
|
const value = stop[1]
|
||||||
|
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
||||||
|
|
||||||
|
return <InputBlock
|
||||||
|
key={key}
|
||||||
|
doc={this.props.fieldSpec.doc}
|
||||||
|
label={labelFromFieldName(this.props.fieldName)}
|
||||||
|
action={deleteStopBtn}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="maputnik-zoom-spec-property-stop-edit">
|
||||||
|
<NumberInput
|
||||||
|
value={zoomLevel}
|
||||||
|
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
|
||||||
|
min={0}
|
||||||
|
max={22}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="maputnik-zoom-spec-property-stop-value">
|
||||||
|
<SpecField
|
||||||
|
fieldName={this.props.fieldName}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
value={value}
|
||||||
|
onChange={(_, newValue) => this.changeZoomStop(idx, zoomLevel, newValue)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</InputBlock>
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className="maputnik-zoom-spec-property">
|
||||||
|
{zoomFields}
|
||||||
|
<Button
|
||||||
|
className="maputnik-add-stop"
|
||||||
|
onClick={this.props.onAddStop.bind(this)}
|
||||||
|
>
|
||||||
|
Add stop
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
6
src/components/fields/_labelFromFieldName.js
Normal file
6
src/components/fields/_labelFromFieldName.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import capitalize from 'lodash.capitalize'
|
||||||
|
|
||||||
|
export default function labelFromFieldName(fieldName) {
|
||||||
|
let label = fieldName.split('-').slice(1).join(' ')
|
||||||
|
return capitalize(label)
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import { combiningFilterOps } from '../../libs/filterops.js'
|
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 DocLabel from '../fields/DocLabel'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
import SingleFilterEditor from './SingleFilterEditor'
|
import SingleFilterEditor from './SingleFilterEditor'
|
||||||
|
@ -26,9 +27,9 @@ function hasNestedCombiningFilter(filter) {
|
||||||
export default class CombiningFilterEditor extends React.Component {
|
export default class CombiningFilterEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
/** Properties of the vector layer and the available fields */
|
/** Properties of the vector layer and the available fields */
|
||||||
properties: React.PropTypes.object,
|
properties: PropTypes.object,
|
||||||
filter: React.PropTypes.array,
|
filter: PropTypes.array,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert filter to combining filter
|
// Convert filter to combining filter
|
||||||
|
@ -91,7 +92,7 @@ export default class CombiningFilterEditor extends React.Component {
|
||||||
<div className="maputnik-filter-editor-compound-select">
|
<div className="maputnik-filter-editor-compound-select">
|
||||||
<DocLabel
|
<DocLabel
|
||||||
label={"Compound Filter"}
|
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
|
<SelectInput
|
||||||
value={combiningOp}
|
value={combiningOp}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
import DeleteIcon from 'react-icons/lib/md/delete'
|
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||||
|
|
||||||
class FilterEditorBlock extends React.Component {
|
class FilterEditorBlock extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onDelete: React.PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
children: React.PropTypes.element.isRequired,
|
children: PropTypes.element.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import { otherFilterOps } from '../../libs/filterops.js'
|
import { otherFilterOps } from '../../libs/filterops.js'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
|
@ -11,11 +12,34 @@ function tryParseInt(v) {
|
||||||
return parseFloat(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 {
|
class SingleFilterEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
filter: React.PropTypes.array.isRequired,
|
filter: PropTypes.array.isRequired,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
properties: React.PropTypes.object,
|
properties: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -23,7 +47,7 @@ class SingleFilterEditor extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onFilterPartChanged(filterOp, propertyName, filterArgs) {
|
onFilterPartChanged(filterOp, propertyName, filterArgs) {
|
||||||
let newFilter = [filterOp, propertyName, ...filterArgs.map(tryParseInt)]
|
let newFilter = [filterOp, propertyName, ...filterArgs.map(parseFilter)]
|
||||||
if(filterOp === 'has' || filterOp === '!has') {
|
if(filterOp === 'has' || filterOp === '!has') {
|
||||||
newFilter = [filterOp, propertyName]
|
newFilter = [filterOp, propertyName]
|
||||||
} else if(filterArgs.length === 0) {
|
} else if(filterArgs.length === 0) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import LineIcon from './LineIcon.jsx'
|
import LineIcon from './LineIcon.jsx'
|
||||||
import FillIcon from './FillIcon.jsx'
|
import FillIcon from './FillIcon.jsx'
|
||||||
|
@ -8,8 +9,8 @@ import CircleIcon from './CircleIcon.jsx'
|
||||||
|
|
||||||
class LayerIcon extends React.Component {
|
class LayerIcon extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
type: React.PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
style: React.PropTypes.object,
|
style: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -17,6 +18,8 @@ class LayerIcon extends React.Component {
|
||||||
switch(this.props.type) {
|
switch(this.props.type) {
|
||||||
case 'fill-extrusion': return <BackgroundIcon {...iconProps} />
|
case 'fill-extrusion': return <BackgroundIcon {...iconProps} />
|
||||||
case 'raster': return <FillIcon {...iconProps} />
|
case 'raster': return <FillIcon {...iconProps} />
|
||||||
|
case 'hillshade': return <FillIcon {...iconProps} />
|
||||||
|
case 'heatmap': return <FillIcon {...iconProps} />
|
||||||
case 'fill': return <FillIcon {...iconProps} />
|
case 'fill': return <FillIcon {...iconProps} />
|
||||||
case 'background': return <BackgroundIcon {...iconProps} />
|
case 'background': return <BackgroundIcon {...iconProps} />
|
||||||
case 'line': return <LineIcon {...iconProps} />
|
case 'line': return <LineIcon {...iconProps} />
|
||||||
|
|
|
@ -6,7 +6,7 @@ export default class FillIcon extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
<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>
|
</IconBase>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@ export default class SymbolIcon extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||||
<g id="svg_1" transform="matrix(1.2718518,0,0,1.2601269,16.559526,-7.4065264)">
|
<g 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" />
|
<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>
|
</g>
|
||||||
</IconBase>
|
</IconBase>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import StringInput from './StringInput'
|
import StringInput from './StringInput'
|
||||||
import NumberInput from './NumberInput'
|
import NumberInput from './NumberInput'
|
||||||
|
|
||||||
class ArrayInput extends React.Component {
|
class ArrayInput extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.array,
|
value: PropTypes.array,
|
||||||
type: React.PropTypes.string,
|
type: PropTypes.string,
|
||||||
length: React.PropTypes.number,
|
length: PropTypes.number,
|
||||||
default: React.PropTypes.array,
|
default: PropTypes.array,
|
||||||
onChange: React.PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
changeValue(idx, newValue) {
|
changeValue(idx, newValue) {
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import Autocomplete from 'react-autocomplete'
|
import Autocomplete from 'react-autocomplete'
|
||||||
|
|
||||||
|
|
||||||
|
const MAX_HEIGHT = 140;
|
||||||
|
|
||||||
class AutocompleteInput extends React.Component {
|
class AutocompleteInput extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.string,
|
value: PropTypes.string,
|
||||||
options: React.PropTypes.array,
|
options: PropTypes.array,
|
||||||
onChange: React.PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
keepMenuWithinWindowBounds: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -15,15 +19,49 @@ class AutocompleteInput extends React.Component {
|
||||||
options: [],
|
options: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
constructor(props) {
|
||||||
const AutocompleteMenu = (items, value, style) => <div className={"maputnik-autocomplete-menu"} children={items} />
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
maxHeight: MAX_HEIGHT
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return <Autocomplete
|
calcMaxHeight() {
|
||||||
|
if(this.props.keepMenuWithinWindowBounds) {
|
||||||
|
const maxHeight = window.innerHeight - this.autocompleteMenuEl.getBoundingClientRect().top;
|
||||||
|
const limitedMaxHeight = Math.min(maxHeight, MAX_HEIGHT);
|
||||||
|
|
||||||
|
if(limitedMaxHeight != this.state.maxHeight) {
|
||||||
|
this.setState({
|
||||||
|
maxHeight: limitedMaxHeight
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
componentDidMount() {
|
||||||
|
this.calcMaxHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
this.calcMaxHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div
|
||||||
|
ref={(el) => {
|
||||||
|
this.autocompleteMenuEl = el;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Autocomplete
|
||||||
|
menuStyle={{
|
||||||
|
position: "absolute",
|
||||||
|
overflow: "auto",
|
||||||
|
maxHeight: this.state.maxHeight
|
||||||
|
}}
|
||||||
wrapperProps={{
|
wrapperProps={{
|
||||||
className: "maputnik-autocomplete",
|
className: "maputnik-autocomplete",
|
||||||
style: null
|
style: null
|
||||||
}}
|
}}
|
||||||
renderMenu={AutocompleteMenu}
|
|
||||||
inputProps={{
|
inputProps={{
|
||||||
className: "maputnik-string"
|
className: "maputnik-string"
|
||||||
}}
|
}}
|
||||||
|
@ -32,6 +70,9 @@ class AutocompleteInput extends React.Component {
|
||||||
getItemValue={(item) => item[0]}
|
getItemValue={(item) => item[0]}
|
||||||
onSelect={v => this.props.onChange(v)}
|
onSelect={v => this.props.onChange(v)}
|
||||||
onChange={(e, v) => this.props.onChange(v)}
|
onChange={(e, v) => this.props.onChange(v)}
|
||||||
|
shouldItemRender={(item, value) => {
|
||||||
|
return item[0].toLowerCase().indexOf(value.toLowerCase()) > -1
|
||||||
|
}}
|
||||||
renderItem={(item, isHighlighted) => (
|
renderItem={(item, isHighlighted) => (
|
||||||
<div
|
<div
|
||||||
key={item[0]}
|
key={item[0]}
|
||||||
|
@ -44,6 +85,7 @@ class AutocompleteInput extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
class CheckboxInput extends React.Component {
|
class CheckboxInput extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.bool.isRequired,
|
value: PropTypes.bool.isRequired,
|
||||||
style: React.PropTypes.object,
|
style: PropTypes.object,
|
||||||
onChange: React.PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
106
src/components/inputs/DynamicArrayInput.jsx
Normal file
106
src/components/inputs/DynamicArrayInput.jsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import StringInput from './StringInput'
|
||||||
|
import NumberInput from './NumberInput'
|
||||||
|
import Button from '../Button'
|
||||||
|
import DeleteIcon from 'react-icons/lib/md/delete'
|
||||||
|
import DocLabel from '../fields/DocLabel'
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicArrayInput extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.array,
|
||||||
|
type: PropTypes.string,
|
||||||
|
default: PropTypes.array,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
style: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
changeValue(idx, newValue) {
|
||||||
|
console.log(idx, newValue)
|
||||||
|
const values = this.values.slice(0)
|
||||||
|
values[idx] = newValue
|
||||||
|
this.props.onChange(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
get values() {
|
||||||
|
return this.props.value || this.props.default || []
|
||||||
|
}
|
||||||
|
|
||||||
|
addValue() {
|
||||||
|
const values = this.values.slice(0)
|
||||||
|
if (this.props.type === 'number') {
|
||||||
|
values.push(0)
|
||||||
|
} else {
|
||||||
|
values.push("")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.props.onChange(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteValue(valueIdx) {
|
||||||
|
const values = this.values.slice(0)
|
||||||
|
values.splice(valueIdx, 1)
|
||||||
|
|
||||||
|
this.props.onChange(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const inputs = this.values.map((v, i) => {
|
||||||
|
const deleteValueBtn= <DeleteValueButton onClick={this.deleteValue.bind(this, i)} />
|
||||||
|
const input = this.props.type === 'number'
|
||||||
|
? <NumberInput
|
||||||
|
value={v}
|
||||||
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
/>
|
||||||
|
: <StringInput
|
||||||
|
value={v}
|
||||||
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
return <div
|
||||||
|
style={this.props.style}
|
||||||
|
key={i}
|
||||||
|
className="maputnik-array-block"
|
||||||
|
>
|
||||||
|
<div className="maputnik-array-block-action">
|
||||||
|
{deleteValueBtn}
|
||||||
|
</div>
|
||||||
|
<div className="maputnik-array-block-content">
|
||||||
|
{input}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
return <div className="maputnik-array">
|
||||||
|
{inputs}
|
||||||
|
<Button
|
||||||
|
className="maputnik-array-add-value"
|
||||||
|
onClick={this.addValue.bind(this)}
|
||||||
|
>
|
||||||
|
Add value
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeleteValueButton extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Button
|
||||||
|
className="maputnik-delete-stop"
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
>
|
||||||
|
<DocLabel
|
||||||
|
label={<DeleteIcon />}
|
||||||
|
doc={"Remove array entry."}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DynamicArrayInput
|
|
@ -1,12 +1,14 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import AutocompleteInput from './AutocompleteInput'
|
import AutocompleteInput from './AutocompleteInput'
|
||||||
|
|
||||||
class FontInput extends React.Component {
|
class FontInput extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.array.isRequired,
|
value: PropTypes.array.isRequired,
|
||||||
fonts: React.PropTypes.array,
|
default: PropTypes.array,
|
||||||
style: React.PropTypes.object,
|
fonts: PropTypes.array,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
style: PropTypes.object,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import AutocompleteInput from './AutocompleteInput'
|
import AutocompleteInput from './AutocompleteInput'
|
||||||
|
|
||||||
|
|
||||||
class IconInput extends React.Component {
|
class IconInput extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.array,
|
value: PropTypes.array,
|
||||||
icons: React.PropTypes.array,
|
icons: PropTypes.array,
|
||||||
style: React.PropTypes.object,
|
style: PropTypes.object,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import DocLabel from '../fields/DocLabel'
|
import DocLabel from '../fields/DocLabel'
|
||||||
|
|
||||||
/** Wrap a component with a label */
|
/** Wrap a component with a label */
|
||||||
class InputBlock extends React.Component {
|
class InputBlock extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
label: React.PropTypes.oneOfType([
|
label: PropTypes.oneOfType([
|
||||||
React.PropTypes.string,
|
PropTypes.string,
|
||||||
React.PropTypes.element,
|
PropTypes.element,
|
||||||
]).isRequired,
|
]).isRequired,
|
||||||
doc: React.PropTypes.string,
|
doc: PropTypes.string,
|
||||||
action: React.PropTypes.element,
|
action: PropTypes.element,
|
||||||
children: React.PropTypes.element.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
style: React.PropTypes.object,
|
style: PropTypes.object,
|
||||||
|
onChange: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(e) {
|
onChange(e) {
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
|
|
||||||
class MultiButtonInput extends React.Component {
|
class MultiButtonInput extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
options: React.PropTypes.array.isRequired,
|
options: PropTypes.array.isRequired,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
class NumberInput extends React.Component {
|
class NumberInput extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.number,
|
value: PropTypes.number,
|
||||||
default: React.PropTypes.number,
|
default: PropTypes.number,
|
||||||
min: React.PropTypes.number,
|
min: PropTypes.number,
|
||||||
max: React.PropTypes.number,
|
max: PropTypes.number,
|
||||||
onChange: React.PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
class SelectInput extends React.Component {
|
class SelectInput extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
options: React.PropTypes.array.isRequired,
|
options: PropTypes.array.isRequired,
|
||||||
style: React.PropTypes.object,
|
style: PropTypes.object,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
class StringInput extends React.Component {
|
class StringInput extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.string,
|
value: PropTypes.string,
|
||||||
style: React.PropTypes.object,
|
style: PropTypes.object,
|
||||||
default: React.PropTypes.string,
|
default: PropTypes.string,
|
||||||
onChange: React.PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
multi: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import CollapseOpenIcon from 'react-icons/lib/md/arrow-drop-down'
|
import CollapseOpenIcon from 'react-icons/lib/md/arrow-drop-down'
|
||||||
import CollapseCloseIcon from 'react-icons/lib/md/arrow-drop-up'
|
import CollapseCloseIcon from 'react-icons/lib/md/arrow-drop-up'
|
||||||
|
|
||||||
export default class Collapser extends React.Component {
|
export default class Collapser extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
isCollapsed: React.PropTypes.bool.isRequired,
|
isCollapsed: PropTypes.bool.isRequired,
|
||||||
style: React.PropTypes.object,
|
style: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
import InputBlock from '../inputs/InputBlock'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
|
|
||||||
class MetadataBlock extends React.Component {
|
class MetadataBlock extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.string,
|
value: PropTypes.string,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"Comments"}>
|
return <InputBlock label={"Comments"} doc={"Comments for the current layer. This is non-standard and not in the spec."}>
|
||||||
<StringInput
|
<StringInput
|
||||||
multi={true}
|
multi={true}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react'
|
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 InputBlock from '../inputs/InputBlock'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
@ -18,8 +19,8 @@ import '../../vendor/codemirror/addon/lint/json-lint'
|
||||||
|
|
||||||
class JSONEditor extends React.Component {
|
class JSONEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
layer: React.PropTypes.object.isRequired,
|
layer: PropTypes.object.isRequired,
|
||||||
onChange: React.PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -80,7 +81,7 @@ class JSONEditor extends React.Component {
|
||||||
|
|
||||||
return <CodeMirror
|
return <CodeMirror
|
||||||
value={this.state.code}
|
value={this.state.code}
|
||||||
onChange={this.onCodeUpdate.bind(this)}
|
onBeforeChange={(editor, data, value) => this.onCodeUpdate(value)}
|
||||||
onFocusChange={focused => focused ? true : this.resetValue()}
|
onFocusChange={focused => focused ? true : this.resetValue()}
|
||||||
options={codeMirrorOptions}
|
options={codeMirrorOptions}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import JSONEditor from './JSONEditor'
|
import JSONEditor from './JSONEditor'
|
||||||
import FilterEditor from '../filter/FilterEditor'
|
import FilterEditor from '../filter/FilterEditor'
|
||||||
|
@ -43,12 +44,12 @@ function layoutGroups(layerType) {
|
||||||
/** Layer editor supporting multiple types of layers. */
|
/** Layer editor supporting multiple types of layers. */
|
||||||
export default class LayerEditor extends React.Component {
|
export default class LayerEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
layer: React.PropTypes.object.isRequired,
|
layer: PropTypes.object.isRequired,
|
||||||
sources: React.PropTypes.object,
|
sources: PropTypes.object,
|
||||||
vectorLayers: React.PropTypes.object,
|
vectorLayers: PropTypes.object,
|
||||||
spec: React.PropTypes.object.isRequired,
|
spec: PropTypes.object.isRequired,
|
||||||
onLayerChanged: React.PropTypes.func,
|
onLayerChanged: PropTypes.func,
|
||||||
onLayerIdChange: React.PropTypes.func,
|
onLayerIdChange: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -58,7 +59,7 @@ export default class LayerEditor extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
static childContextTypes = {
|
static childContextTypes = {
|
||||||
reactIconBase: React.PropTypes.object
|
reactIconBase: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -116,6 +117,11 @@ export default class LayerEditor extends React.Component {
|
||||||
comment = this.props.layer.metadata['maputnik:comment']
|
comment = this.props.layer.metadata['maputnik:comment']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sourceLayerIds;
|
||||||
|
if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
|
||||||
|
sourceLayerIds = this.props.sources[this.props.layer.source].layers;
|
||||||
|
}
|
||||||
|
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case 'layer': return <div>
|
case 'layer': return <div>
|
||||||
<LayerIdBlock
|
<LayerIdBlock
|
||||||
|
@ -132,8 +138,9 @@ export default class LayerEditor extends React.Component {
|
||||||
onChange={v => this.changeProperty(null, 'source', v)}
|
onChange={v => this.changeProperty(null, 'source', v)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{this.props.layer.type !== 'raster' && this.props.layer.type !== 'background' && <LayerSourceLayerBlock
|
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
|
||||||
sourceLayerIds={this.props.sources[this.props.layer.source]}
|
<LayerSourceLayerBlock
|
||||||
|
sourceLayerIds={sourceLayerIds}
|
||||||
value={this.props.layer['source-layer']}
|
value={this.props.layer['source-layer']}
|
||||||
onChange={v => this.changeProperty(null, 'source-layer', v)}
|
onChange={v => this.changeProperty(null, 'source-layer', v)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import Collapse from 'react-collapse'
|
import Collapse from 'react-collapse'
|
||||||
import Collapser from './Collapser'
|
import Collapser from './Collapser'
|
||||||
|
|
||||||
export default class LayerEditorGroup extends React.Component {
|
export default class LayerEditorGroup extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
title: React.PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
isActive: React.PropTypes.bool.isRequired,
|
isActive: PropTypes.bool.isRequired,
|
||||||
children: React.PropTypes.element.isRequired,
|
children: PropTypes.element.isRequired,
|
||||||
onActiveToggle: React.PropTypes.func.isRequired
|
onActiveToggle: PropTypes.func.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import React from 'react'
|
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 InputBlock from '../inputs/InputBlock'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
|
|
||||||
class LayerIdBlock extends React.Component {
|
class LayerIdBlock extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"ID"} doc={GlSpec.layer.id.doc}>
|
return <InputBlock label={"ID"} doc={styleSpec.latest.layer.id.doc}>
|
||||||
<StringInput
|
<StringInput
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import cloneDeep from 'lodash.clonedeep'
|
import cloneDeep from 'lodash.clonedeep'
|
||||||
|
|
||||||
|
@ -12,11 +13,11 @@ import style from '../../libs/style.js'
|
||||||
import {SortableContainer, SortableHandle, arrayMove} from 'react-sortable-hoc';
|
import {SortableContainer, SortableHandle, arrayMove} from 'react-sortable-hoc';
|
||||||
|
|
||||||
const layerListPropTypes = {
|
const layerListPropTypes = {
|
||||||
layers: React.PropTypes.array.isRequired,
|
layers: PropTypes.array.isRequired,
|
||||||
selectedLayerIndex: React.PropTypes.number.isRequired,
|
selectedLayerIndex: PropTypes.number.isRequired,
|
||||||
onLayersChange: React.PropTypes.func.isRequired,
|
onLayersChange: PropTypes.func.isRequired,
|
||||||
onLayerSelect: React.PropTypes.func,
|
onLayerSelect: PropTypes.func,
|
||||||
sources: React.PropTypes.object.isRequired,
|
sources: PropTypes.object.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
function layerPrefix(name) {
|
function layerPrefix(name) {
|
||||||
|
@ -49,6 +50,7 @@ class LayerListContainer extends React.Component {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
collapsedGroups: {},
|
collapsedGroups: {},
|
||||||
|
areAllGroupsExpanded: false,
|
||||||
isOpen: {
|
isOpen: {
|
||||||
add: false,
|
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() {
|
groupedLayers() {
|
||||||
const groups = []
|
const groups = []
|
||||||
for (let i = 0; i < this.props.layers.length; i++) {
|
for (let i = 0; i < this.props.layers.length; i++) {
|
||||||
|
@ -137,7 +164,7 @@ class LayerListContainer extends React.Component {
|
||||||
const grp = <LayerListGroup
|
const grp = <LayerListGroup
|
||||||
key={[groupPrefix, idx].join('-')}
|
key={[groupPrefix, idx].join('-')}
|
||||||
title={groupPrefix}
|
title={groupPrefix}
|
||||||
isActive={!this.isCollapsed(groupPrefix, idx)}
|
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
|
||||||
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
|
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
|
||||||
/>
|
/>
|
||||||
listItems.push(grp)
|
listItems.push(grp)
|
||||||
|
@ -145,9 +172,10 @@ class LayerListContainer extends React.Component {
|
||||||
|
|
||||||
layers.forEach((layer, idxInGroup) => {
|
layers.forEach((layer, idxInGroup) => {
|
||||||
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
|
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
|
||||||
|
|
||||||
const listItem = <LayerListItem
|
const listItem = <LayerListItem
|
||||||
className={classnames({
|
className={classnames({
|
||||||
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx),
|
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
|
||||||
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1
|
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1
|
||||||
})}
|
})}
|
||||||
index={idx}
|
index={idx}
|
||||||
|
@ -177,11 +205,24 @@ class LayerListContainer extends React.Component {
|
||||||
<header className="maputnik-layer-list-header">
|
<header className="maputnik-layer-list-header">
|
||||||
<span className="maputnik-layer-list-header-title">Layers</span>
|
<span className="maputnik-layer-list-header-title">Layers</span>
|
||||||
<span className="maputnik-space" />
|
<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')}
|
onClick={this.toggleModal.bind(this, 'add')}
|
||||||
className="maputnik-add-layer">
|
className="maputnik-button maputnik-button-selected">
|
||||||
Add Layer
|
Add Layer
|
||||||
</Button>
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<ul className="maputnik-layer-list-container">
|
<ul className="maputnik-layer-list-container">
|
||||||
{listItems}
|
{listItems}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import Collapser from './Collapser'
|
import Collapser from './Collapser'
|
||||||
|
|
||||||
export default class LayerEditorGroup extends React.Component {
|
export default class LayerListGroup extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
title: React.PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
children: React.PropTypes.element.isRequired,
|
isActive: PropTypes.bool.isRequired,
|
||||||
isActive: React.PropTypes.bool.isRequired,
|
onActiveToggle: PropTypes.func.isRequired
|
||||||
onActiveToggle: React.PropTypes.func.isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div className="maputnik-layer-list-group">
|
return <li className="maputnik-layer-list-group">
|
||||||
<div className="maputnik-layer-list-group-header"
|
<div className="maputnik-layer-list-group-header"
|
||||||
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
||||||
>
|
>
|
||||||
|
@ -21,6 +21,6 @@ export default class LayerEditorGroup extends React.Component {
|
||||||
isCollapsed={this.props.isActive}
|
isCollapsed={this.props.isActive}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import Color from 'color'
|
import Color from 'color'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
|
@ -30,8 +31,8 @@ class LayerTypeDragHandle extends React.Component {
|
||||||
|
|
||||||
class IconAction extends React.Component {
|
class IconAction extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
action: React.PropTypes.string.isRequired,
|
action: PropTypes.string.isRequired,
|
||||||
onClick: React.PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
renderIcon() {
|
renderIcon() {
|
||||||
|
@ -57,16 +58,16 @@ class IconAction extends React.Component {
|
||||||
@SortableElement
|
@SortableElement
|
||||||
class LayerListItem extends React.Component {
|
class LayerListItem extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
layerId: React.PropTypes.string.isRequired,
|
layerId: PropTypes.string.isRequired,
|
||||||
layerType: React.PropTypes.string.isRequired,
|
layerType: PropTypes.string.isRequired,
|
||||||
isSelected: React.PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
visibility: React.PropTypes.string,
|
visibility: PropTypes.string,
|
||||||
className: React.PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
|
||||||
onLayerSelect: React.PropTypes.func.isRequired,
|
onLayerSelect: PropTypes.func.isRequired,
|
||||||
onLayerCopy: React.PropTypes.func,
|
onLayerCopy: PropTypes.func,
|
||||||
onLayerDestroy: React.PropTypes.func,
|
onLayerDestroy: PropTypes.func,
|
||||||
onLayerVisibilityToggle: React.PropTypes.func,
|
onLayerVisibilityToggle: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -78,7 +79,7 @@ class LayerListItem extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
static childContextTypes = {
|
static childContextTypes = {
|
||||||
reactIconBase: React.PropTypes.object
|
reactIconBase: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
getChildContext() {
|
getChildContext() {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react'
|
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 InputBlock from '../inputs/InputBlock'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
@ -8,9 +9,9 @@ import AutocompleteInput from '../inputs/AutocompleteInput'
|
||||||
|
|
||||||
class LayerSourceBlock extends React.Component {
|
class LayerSourceBlock extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.string,
|
value: PropTypes.string,
|
||||||
onChange: React.PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
sourceIds: React.PropTypes.array,
|
sourceIds: PropTypes.array,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -19,7 +20,7 @@ class LayerSourceBlock extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"Source"} doc={GlSpec.layer.source.doc}>
|
return <InputBlock label={"Source"} doc={styleSpec.latest.layer.source.doc}>
|
||||||
<AutocompleteInput
|
<AutocompleteInput
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react'
|
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 InputBlock from '../inputs/InputBlock'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
@ -8,19 +9,22 @@ import AutocompleteInput from '../inputs/AutocompleteInput'
|
||||||
|
|
||||||
class LayerSourceLayer extends React.Component {
|
class LayerSourceLayer extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.string,
|
value: PropTypes.string,
|
||||||
onChange: React.PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
sourceLayerIds: React.PropTypes.array,
|
sourceLayerIds: PropTypes.array,
|
||||||
|
isFixed: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
sourceLayerIds: [],
|
sourceLayerIds: [],
|
||||||
|
isFixed: false
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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
|
<AutocompleteInput
|
||||||
|
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
options={this.props.sourceLayerIds.map(l => [l, l])}
|
options={this.props.sourceLayerIds.map(l => [l, l])}
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import React from 'react'
|
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 InputBlock from '../inputs/InputBlock'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
|
||||||
class LayerTypeBlock extends React.Component {
|
class LayerTypeBlock extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"Type"} doc={GlSpec.layer.type.doc}>
|
return <InputBlock label={"Type"} doc={styleSpec.latest.layer.type.doc}>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={[
|
options={[
|
||||||
['background', 'Background'],
|
['background', 'Background'],
|
||||||
|
@ -21,6 +22,8 @@ class LayerTypeBlock extends React.Component {
|
||||||
['raster', 'Raster'],
|
['raster', 'Raster'],
|
||||||
['circle', 'Circle'],
|
['circle', 'Circle'],
|
||||||
['fill-extrusion', 'Fill Extrusion'],
|
['fill-extrusion', 'Fill Extrusion'],
|
||||||
|
['hillshade', 'Hillshade'],
|
||||||
|
['heatmap', 'Heatmap'],
|
||||||
]}
|
]}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
import React from 'react'
|
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 InputBlock from '../inputs/InputBlock'
|
||||||
import NumberInput from '../inputs/NumberInput'
|
import NumberInput from '../inputs/NumberInput'
|
||||||
|
|
||||||
class MaxZoomBlock extends React.Component {
|
class MaxZoomBlock extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.number.isRequired,
|
value: PropTypes.number,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"Max Zoom"} doc={GlSpec.layer.maxzoom.doc}>
|
return <InputBlock label={"Max Zoom"} doc={styleSpec.latest.layer.maxzoom.doc}>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
min={GlSpec.layer.maxzoom.minimum}
|
min={styleSpec.latest.layer.maxzoom.minimum}
|
||||||
max={GlSpec.layer.maxzoom.maximum}
|
max={styleSpec.latest.layer.maxzoom.maximum}
|
||||||
default={GlSpec.layer.maxzoom.maximum}
|
default={styleSpec.latest.layer.maxzoom.maximum}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
import React from 'react'
|
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 InputBlock from '../inputs/InputBlock'
|
||||||
import NumberInput from '../inputs/NumberInput'
|
import NumberInput from '../inputs/NumberInput'
|
||||||
|
|
||||||
class MinZoomBlock extends React.Component {
|
class MinZoomBlock extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: React.PropTypes.number.isRequired,
|
value: PropTypes.number,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"Min Zoom"} doc={GlSpec.layer.minzoom.doc}>
|
return <InputBlock label={"Min Zoom"} doc={styleSpec.latest.layer.minzoom.doc}>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
min={GlSpec.layer.minzoom.minimum}
|
min={styleSpec.latest.layer.minzoom.minimum}
|
||||||
max={GlSpec.layer.minzoom.maximum}
|
max={styleSpec.latest.layer.minzoom.maximum}
|
||||||
default={GlSpec.layer.minzoom.minimum}
|
default={styleSpec.latest.layer.minzoom.minimum}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,38 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import InputBlock from '../inputs/InputBlock'
|
import InputBlock from '../inputs/InputBlock'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
import LayerIcon from '../icons/LayerIcon'
|
import LayerIcon from '../icons/LayerIcon'
|
||||||
|
|
||||||
|
|
||||||
function groupFeaturesBySourceLayer(features) {
|
function groupFeaturesBySourceLayer(features) {
|
||||||
const sources = {}
|
const sources = {}
|
||||||
|
|
||||||
|
let returnedFeatures = {};
|
||||||
|
|
||||||
features.forEach(feature => {
|
features.forEach(feature => {
|
||||||
|
if(returnedFeatures.hasOwnProperty(feature.layer.id)) {
|
||||||
|
returnedFeatures[feature.layer.id]++
|
||||||
|
|
||||||
|
const featureObject = sources[feature.layer['source-layer']].find(f => f.layer.id === feature.layer.id)
|
||||||
|
|
||||||
|
featureObject.counter = returnedFeatures[feature.layer.id]
|
||||||
|
} else {
|
||||||
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
|
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
|
||||||
sources[feature.layer['source-layer']].push(feature)
|
sources[feature.layer['source-layer']].push(feature)
|
||||||
|
|
||||||
|
returnedFeatures[feature.layer.id] = 1
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return sources
|
return sources
|
||||||
}
|
}
|
||||||
|
|
||||||
class FeatureLayerPopup extends React.Component {
|
class FeatureLayerPopup extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onLayerSelect: PropTypes.func.isRequired,
|
||||||
|
features: PropTypes.array
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const sources = groupFeaturesBySourceLayer(this.props.features)
|
const sources = groupFeaturesBySourceLayer(this.props.features)
|
||||||
|
|
||||||
|
@ -22,6 +41,9 @@ class FeatureLayerPopup extends React.Component {
|
||||||
return <label
|
return <label
|
||||||
key={idx}
|
key={idx}
|
||||||
className="maputnik-popup-layer"
|
className="maputnik-popup-layer"
|
||||||
|
onClick={() => {
|
||||||
|
this.props.onLayerSelect(feature.layer.id)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<LayerIcon type={feature.layer.type} style={{
|
<LayerIcon type={feature.layer.type} style={{
|
||||||
width: 14,
|
width: 14,
|
||||||
|
@ -29,6 +51,7 @@ class FeatureLayerPopup extends React.Component {
|
||||||
paddingRight: 3
|
paddingRight: 3
|
||||||
}}/>
|
}}/>
|
||||||
{feature.layer.id}
|
{feature.layer.id}
|
||||||
|
{feature.counter && <span> × {feature.counter}</span>}
|
||||||
</label>
|
</label>
|
||||||
})
|
})
|
||||||
return <div key={vectorLayerId}>
|
return <div key={vectorLayerId}>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import InputBlock from '../inputs/InputBlock'
|
import InputBlock from '../inputs/InputBlock'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ function renderProperties(feature) {
|
||||||
|
|
||||||
function renderFeature(feature) {
|
function renderFeature(feature) {
|
||||||
return <div key={feature.id}>
|
return <div key={feature.id}>
|
||||||
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}</div>
|
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
|
||||||
<InputBlock key={"property-type"} label={"$type"}>
|
<InputBlock key={"property-type"} label={"$type"}>
|
||||||
<StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
|
<StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
|
@ -30,10 +31,36 @@ function renderFeature(feature) {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeDuplicatedFeatures(features) {
|
||||||
|
let uniqueFeatures = [];
|
||||||
|
|
||||||
|
features.forEach(feature => {
|
||||||
|
const featureIndex = uniqueFeatures.findIndex(feature2 => {
|
||||||
|
return feature.layer['source-layer'] === feature2.layer['source-layer']
|
||||||
|
&& JSON.stringify(feature.properties) === JSON.stringify(feature2.properties)
|
||||||
|
})
|
||||||
|
|
||||||
|
if(featureIndex === -1) {
|
||||||
|
uniqueFeatures.push(feature)
|
||||||
|
} else {
|
||||||
|
if(uniqueFeatures[featureIndex].hasOwnProperty('counter')) {
|
||||||
|
uniqueFeatures[featureIndex].inspectModeCounter++
|
||||||
|
} else {
|
||||||
|
uniqueFeatures[featureIndex].inspectModeCounter = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return uniqueFeatures
|
||||||
|
}
|
||||||
|
|
||||||
class FeaturePropertyPopup extends React.Component {
|
class FeaturePropertyPopup extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
features: PropTypes.array
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const features = this.props.features
|
const features = removeDuplicatedFeatures(this.props.features)
|
||||||
return <div className="maputnik-feature-property-popup">
|
return <div className="maputnik-feature-property-popup">
|
||||||
{features.map(renderFeature)}
|
{features.map(renderFeature)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,25 +1,20 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import ReactDOM from 'react-dom'
|
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 MapboxInspect from 'mapbox-gl-inspect'
|
||||||
import FeatureLayerPopup from './FeatureLayerPopup'
|
import FeatureLayerPopup from './FeatureLayerPopup'
|
||||||
import FeaturePropertyPopup from './FeaturePropertyPopup'
|
import FeaturePropertyPopup from './FeaturePropertyPopup'
|
||||||
import validateColor from 'mapbox-gl-style-spec/lib/validate/validate_color'
|
|
||||||
import style from '../../libs/style.js'
|
import style from '../../libs/style.js'
|
||||||
import tokens from '../../config/tokens.json'
|
import tokens from '../../config/tokens.json'
|
||||||
import colors from 'mapbox-gl-inspect/lib/colors'
|
import colors from 'mapbox-gl-inspect/lib/colors'
|
||||||
import Color from 'color'
|
import Color from 'color'
|
||||||
|
import ZoomControl from '../../libs/zoomcontrol'
|
||||||
import { colorHighlightedLayer } from '../../libs/highlight'
|
import { colorHighlightedLayer } from '../../libs/highlight'
|
||||||
import 'mapbox-gl/dist/mapbox-gl.css'
|
import 'mapbox-gl/dist/mapbox-gl.css'
|
||||||
import '../../mapboxgl.css'
|
import '../../mapboxgl.css'
|
||||||
import '../../libs/mapbox-rtl'
|
import '../../libs/mapbox-rtl'
|
||||||
|
|
||||||
function renderLayerPopup(features) {
|
|
||||||
var mountNode = document.createElement('div');
|
|
||||||
ReactDOM.render(<FeatureLayerPopup features={features} />, mountNode)
|
|
||||||
return mountNode.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPropertyPopup(features) {
|
function renderPropertyPopup(features) {
|
||||||
var mountNode = document.createElement('div');
|
var mountNode = document.createElement('div');
|
||||||
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode)
|
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode)
|
||||||
|
@ -43,7 +38,7 @@ function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
|
||||||
const sources = {}
|
const sources = {}
|
||||||
Object.keys(originalMapStyle.sources).forEach(sourceId => {
|
Object.keys(originalMapStyle.sources).forEach(sourceId => {
|
||||||
const source = originalMapStyle.sources[sourceId]
|
const source = originalMapStyle.sources[sourceId]
|
||||||
if(source.type !== 'raster') {
|
if(source.type !== 'raster' && source.type !== 'raster-dem') {
|
||||||
sources[sourceId] = source
|
sources[sourceId] = source
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -58,15 +53,17 @@ function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
|
||||||
|
|
||||||
export default class MapboxGlMap extends React.Component {
|
export default class MapboxGlMap extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onDataChange: React.PropTypes.func,
|
onDataChange: PropTypes.func,
|
||||||
mapStyle: React.PropTypes.object.isRequired,
|
onLayerSelect: PropTypes.func.isRequired,
|
||||||
inspectModeEnabled: React.PropTypes.bool.isRequired,
|
mapStyle: PropTypes.object.isRequired,
|
||||||
highlightedLayer: React.PropTypes.object,
|
inspectModeEnabled: PropTypes.bool.isRequired,
|
||||||
|
highlightedLayer: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onMapLoaded: () => {},
|
onMapLoaded: () => {},
|
||||||
onDataChange: () => {},
|
onDataChange: () => {},
|
||||||
|
onLayerSelect: () => {},
|
||||||
mapboxAccessToken: tokens.mapbox,
|
mapboxAccessToken: tokens.mapbox,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,6 +107,9 @@ export default class MapboxGlMap extends React.Component {
|
||||||
hash: true,
|
hash: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const zoom = new ZoomControl;
|
||||||
|
map.addControl(zoom, 'top-right');
|
||||||
|
|
||||||
const nav = new MapboxGl.NavigationControl();
|
const nav = new MapboxGl.NavigationControl();
|
||||||
map.addControl(nav, 'top-right');
|
map.addControl(nav, 'top-right');
|
||||||
|
|
||||||
|
@ -129,7 +129,9 @@ export default class MapboxGlMap extends React.Component {
|
||||||
if(this.props.inspectModeEnabled) {
|
if(this.props.inspectModeEnabled) {
|
||||||
return renderPropertyPopup(features)
|
return renderPropertyPopup(features)
|
||||||
} else {
|
} else {
|
||||||
return renderLayerPopup(features)
|
var mountNode = document.createElement('div');
|
||||||
|
ReactDOM.render(<FeatureLayerPopup features={features} onLayerSelect={this.props.onLayerSelect} />, mountNode)
|
||||||
|
return mountNode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,59 +1,17 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import style from '../../libs/style.js'
|
import style from '../../libs/style.js'
|
||||||
import isEqual from 'lodash.isequal'
|
import isEqual from 'lodash.isequal'
|
||||||
import { loadJSON } from '../../libs/urlopen'
|
import { loadJSON } from '../../libs/urlopen'
|
||||||
|
import 'ol/ol.css'
|
||||||
|
|
||||||
function suitableVectorSource(mapStyle) {
|
|
||||||
const sources = Object.keys(mapStyle.sources)
|
|
||||||
.map(sourceId => {
|
|
||||||
return {
|
|
||||||
id: sourceId,
|
|
||||||
source: mapStyle.sources[sourceId]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(({source}) => source.type === 'vector')
|
|
||||||
return sources[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
function toVectorLayer(source, tilegrid, cb) {
|
|
||||||
function newMVTLayer(tileUrl) {
|
|
||||||
const ol = require('openlayers')
|
|
||||||
return new ol.layer.VectorTile({
|
|
||||||
source: new ol.source.VectorTile({
|
|
||||||
format: new ol.format.MVT(),
|
|
||||||
tileGrid: tilegrid,
|
|
||||||
tilePixelRatio: 8,
|
|
||||||
url: tileUrl
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!source.tiles) {
|
|
||||||
sourceFromTileJSON(source.url, tileSource => {
|
|
||||||
cb(newMVTLayer(tileSource.tiles[0]))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
cb(newMVTLayer(source.tiles[0]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sourceFromTileJSON(url, cb) {
|
|
||||||
loadJSON(url, null, tilejson => {
|
|
||||||
if(!tilejson) return
|
|
||||||
cb({
|
|
||||||
type: 'vector',
|
|
||||||
tiles: tilejson.tiles,
|
|
||||||
minzoom: tilejson.minzoom,
|
|
||||||
maxzoom: tilejson.maxzoom,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
class OpenLayers3Map extends React.Component {
|
class OpenLayers3Map extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onDataChange: React.PropTypes.func,
|
onDataChange: PropTypes.func,
|
||||||
mapStyle: React.PropTypes.object.isRequired,
|
mapStyle: PropTypes.object.isRequired,
|
||||||
accessToken: React.PropTypes.string,
|
accessToken: PropTypes.string,
|
||||||
|
style: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -63,48 +21,17 @@ class OpenLayers3Map extends React.Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.tilegrid = null
|
|
||||||
this.resolutions = null
|
|
||||||
this.layer = null
|
|
||||||
this.map = null
|
this.map = null
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStyle(newMapStyle) {
|
updateStyle(newMapStyle) {
|
||||||
const oldSource = suitableVectorSource(this.props.mapStyle)
|
const olms = require('ol-mapbox-style');
|
||||||
const newSource = suitableVectorSource(newMapStyle)
|
const styleFunc = olms.apply(this.map, newMapStyle)
|
||||||
const resolutions = this.resolutions
|
|
||||||
|
|
||||||
function setStyleFunc(map, layer) {
|
|
||||||
const olms = require('ol-mapbox-style')
|
|
||||||
const styleFunc = olms.getStyleFunction(newMapStyle, newSource.id, resolutions)
|
|
||||||
layer.setStyle(styleFunc)
|
|
||||||
//NOTE: We need to mark the source as changed in order
|
|
||||||
//to trigger a rerender
|
|
||||||
layer.getSource().changed()
|
|
||||||
map.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
if(newSource) {
|
|
||||||
if(this.layer && !isEqual(oldSource, newSource)) {
|
|
||||||
this.map.removeLayer(this.layer)
|
|
||||||
this.layer = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!this.layer) {
|
|
||||||
toVectorLayer(newSource.source, this.tilegrid, vectorLayer => {
|
|
||||||
this.layer = vectorLayer
|
|
||||||
this.map.addLayer(this.layer)
|
|
||||||
setStyleFunc(this.map, this.layer)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setStyleFunc(this.map, this.layer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
require.ensure(["openlayers", "ol-mapbox-style"], () => {
|
require.ensure(["ol", "ol-mapbox-style"], () => {
|
||||||
if(!this.map || !this.resolutions) return
|
if(!this.map) return
|
||||||
this.updateStyle(nextProps.mapStyle)
|
this.updateStyle(nextProps.mapStyle)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -112,24 +39,22 @@ class OpenLayers3Map extends React.Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
//Load OpenLayers dynamically once we need it
|
//Load OpenLayers dynamically once we need it
|
||||||
//TODO: Make this more convenient
|
//TODO: Make this more convenient
|
||||||
require.ensure(["openlayers", "ol-mapbox-style"], ()=> {
|
require.ensure(["ol", "ol/map", "ol/view", "ol/control/zoom", "ol-mapbox-style"], ()=> {
|
||||||
console.log('Loaded OpenLayers3 renderer')
|
console.log('Loaded OpenLayers3 renderer')
|
||||||
|
|
||||||
const ol = require('openlayers')
|
const olMap = require('ol/map').default
|
||||||
const olms = require('ol-mapbox-style')
|
const olView = require('ol/view').default
|
||||||
|
const olZoom = require('ol/control/zoom').default
|
||||||
|
|
||||||
this.tilegrid = ol.tilegrid.createXYZ({tileSize: 512, maxZoom: 22})
|
const map = new olMap({
|
||||||
this.resolutions = this.tilegrid.getResolutions()
|
|
||||||
|
|
||||||
const map = new ol.Map({
|
|
||||||
target: this.container,
|
target: this.container,
|
||||||
layers: [],
|
layers: [],
|
||||||
view: new ol.View({
|
view: new olView({
|
||||||
zoom: 2,
|
zoom: 2,
|
||||||
center: [52.5, -78.4]
|
center: [52.5, -78.4]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
map.addControl(new ol.control.Zoom())
|
map.addControl(new olZoom())
|
||||||
this.map = map
|
this.map = map
|
||||||
this.updateStyle(this.props.mapStyle)
|
this.updateStyle(this.props.mapStyle)
|
||||||
})
|
})
|
||||||
|
@ -140,10 +65,10 @@ class OpenLayers3Map extends React.Component {
|
||||||
ref={x => this.container = x}
|
ref={x => this.container = x}
|
||||||
style={{
|
style={{
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
top: 0,
|
top: 40,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
height: "100%",
|
height: 'calc(100% - 40px)',
|
||||||
width: "75%",
|
width: "75%",
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
...this.props.style,
|
...this.props.style,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
import InputBlock from '../inputs/InputBlock'
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
@ -13,13 +14,13 @@ import LayerSourceLayerBlock from '../layers/LayerSourceLayerBlock'
|
||||||
|
|
||||||
class AddModal extends React.Component {
|
class AddModal extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
layers: React.PropTypes.array.isRequired,
|
layers: PropTypes.array.isRequired,
|
||||||
onLayersChange: React.PropTypes.func.isRequired,
|
onLayersChange: PropTypes.func.isRequired,
|
||||||
isOpen: React.PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
onOpenToggle: React.PropTypes.func.isRequired,
|
onOpenToggle: PropTypes.func.isRequired,
|
||||||
|
|
||||||
// A dict of source id's and the available source layers
|
// A dict of source id's and the available source layers
|
||||||
sources: React.PropTypes.object.isRequired,
|
sources: PropTypes.object.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
addLayer() {
|
addLayer() {
|
||||||
|
@ -55,18 +56,65 @@ class AddModal extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillUpdate(nextProps, nextState) {
|
||||||
const sourceIds = Object.keys(nextProps.sources)
|
// Check if source is valid for new type
|
||||||
if(!this.state.source && sourceIds.length > 0) {
|
const oldType = this.state.type;
|
||||||
|
const newType = nextState.type;
|
||||||
|
|
||||||
|
const availableSourcesOld = this.getSources(oldType);
|
||||||
|
const availableSourcesNew = this.getSources(newType);
|
||||||
|
|
||||||
|
if(
|
||||||
|
// Type has changed
|
||||||
|
oldType !== newType
|
||||||
|
&& this.state.source !== ""
|
||||||
|
// Was a valid source previously
|
||||||
|
&& availableSourcesOld.indexOf(this.state.source) > -1
|
||||||
|
// And is not a valid source now
|
||||||
|
&& availableSourcesNew.indexOf(nextState.source) < 0
|
||||||
|
) {
|
||||||
|
// Clear the source
|
||||||
this.setState({
|
this.setState({
|
||||||
source: sourceIds[0],
|
source: ""
|
||||||
'source-layer': this.state['source-layer'] || (nextProps.sources[sourceIds[0]] || [])[0]
|
});
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLayersForSource(source) {
|
||||||
|
const sourceObj = this.props.sources[source] || {};
|
||||||
|
return sourceObj.layers || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getSources(type) {
|
||||||
|
const sources = [];
|
||||||
|
|
||||||
|
const types = {
|
||||||
|
vector: [
|
||||||
|
"fill",
|
||||||
|
"line",
|
||||||
|
"symbol",
|
||||||
|
"circle",
|
||||||
|
"fill-extrusion"
|
||||||
|
],
|
||||||
|
raster: [
|
||||||
|
"raster"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let [key, val] of Object.entries(this.props.sources)) {
|
||||||
|
if(types[val.type] && types[val.type].indexOf(type) > -1) {
|
||||||
|
sources.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const sources = this.getSources(this.state.type);
|
||||||
|
const layers = this.getLayersForSource(this.state.source);
|
||||||
|
|
||||||
return <Modal
|
return <Modal
|
||||||
isOpen={this.props.isOpen}
|
isOpen={this.props.isOpen}
|
||||||
onOpenToggle={this.props.onOpenToggle}
|
onOpenToggle={this.props.onOpenToggle}
|
||||||
|
@ -83,14 +131,15 @@ class AddModal extends React.Component {
|
||||||
/>
|
/>
|
||||||
{this.state.type !== 'background' &&
|
{this.state.type !== 'background' &&
|
||||||
<LayerSourceBlock
|
<LayerSourceBlock
|
||||||
sourceIds={Object.keys(this.props.sources)}
|
sourceIds={sources}
|
||||||
value={this.state.source}
|
value={this.state.source}
|
||||||
onChange={v => this.setState({ source: v })}
|
onChange={v => this.setState({ source: v })}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{this.state.type !== 'background' && this.state.type !== 'raster' &&
|
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
|
||||||
<LayerSourceLayerBlock
|
<LayerSourceLayerBlock
|
||||||
sourceLayerIds={this.props.sources[this.state.source] || []}
|
isFixed={true}
|
||||||
|
sourceLayerIds={layers}
|
||||||
value={this.state['source-layer']}
|
value={this.state['source-layer']}
|
||||||
onChange={v => this.setState({ 'source-layer': v })}
|
onChange={v => this.setState({ 'source-layer': v })}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import { saveAs } from 'file-saver'
|
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 InputBlock from '../inputs/InputBlock'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
@ -9,21 +10,23 @@ import CheckboxInput from '../inputs/CheckboxInput'
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
import MdFileDownload from 'react-icons/lib/md/file-download'
|
import MdFileDownload from 'react-icons/lib/md/file-download'
|
||||||
|
import TiClipboard from 'react-icons/lib/ti/clipboard'
|
||||||
import style from '../../libs/style.js'
|
import style from '../../libs/style.js'
|
||||||
import formatStyle from 'mapbox-gl-style-spec/lib/format'
|
|
||||||
import GitHub from 'github-api'
|
import GitHub from 'github-api'
|
||||||
|
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
||||||
|
|
||||||
|
|
||||||
class Gist extends React.Component {
|
class Gist extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
mapStyle: React.PropTypes.object.isRequired,
|
mapStyle: PropTypes.object.isRequired,
|
||||||
onStyleChanged: React.PropTypes.func.isRequired,
|
onStyleChanged: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
preview: false,
|
preview: false,
|
||||||
|
public: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
latestGist: null,
|
latestGist: null,
|
||||||
}
|
}
|
||||||
|
@ -41,11 +44,14 @@ class Gist extends React.Component {
|
||||||
...this.state,
|
...this.state,
|
||||||
saving: true
|
saving: true
|
||||||
});
|
});
|
||||||
const preview = this.state.preview && (this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token'];
|
|
||||||
|
const preview = this.state.preview;
|
||||||
|
|
||||||
|
const mapboxToken = (this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token'];
|
||||||
|
|
||||||
const mapStyleStr = preview ?
|
const mapStyleStr = preview ?
|
||||||
formatStyle(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle))) :
|
styleSpec.format(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle))) :
|
||||||
formatStyle(stripAccessTokens(this.props.mapStyle));
|
styleSpec.format(stripAccessTokens(this.props.mapStyle));
|
||||||
const styleTitle = this.props.mapStyle.name || 'Style';
|
const styleTitle = this.props.mapStyle.name || 'Style';
|
||||||
const htmlStr = `
|
const htmlStr = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
@ -54,8 +60,8 @@ class Gist extends React.Component {
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>`+styleTitle+` Preview</title>
|
<title>`+styleTitle+` Preview</title>
|
||||||
<link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v0.28.0/mapbox-gl.css" />
|
<link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v0.44.0/mapbox-gl.css" />
|
||||||
<script src="https://api.mapbox.com/mapbox-gl-js/v0.28.0/mapbox-gl.js"></script>
|
<script src="https://api.mapbox.com/mapbox-gl-js/v0.44.0/mapbox-gl.js"></script>
|
||||||
<style>
|
<style>
|
||||||
body { margin:0; padding:0; }
|
body { margin:0; padding:0; }
|
||||||
#map { position:absolute; top:0; bottom:0; width:100%; }
|
#map { position:absolute; top:0; bottom:0; width:100%; }
|
||||||
|
@ -64,6 +70,7 @@ class Gist extends React.Component {
|
||||||
<body>
|
<body>
|
||||||
<div id='map'></div>
|
<div id='map'></div>
|
||||||
<script>
|
<script>
|
||||||
|
mapboxgl.accessToken = '${mapboxToken}';
|
||||||
var map = new mapboxgl.Map({
|
var map = new mapboxgl.Map({
|
||||||
container: 'map',
|
container: 'map',
|
||||||
style: 'style.json',
|
style: 'style.json',
|
||||||
|
@ -88,7 +95,7 @@ class Gist extends React.Component {
|
||||||
const gh = new GitHub();
|
const gh = new GitHub();
|
||||||
let gist = gh.getGist(); // not a gist yet
|
let gist = gh.getGist(); // not a gist yet
|
||||||
gist.create({
|
gist.create({
|
||||||
public: true,
|
public: this.state.public,
|
||||||
description: styleTitle,
|
description: styleTitle,
|
||||||
files: files
|
files: files
|
||||||
}).then(function({data}) {
|
}).then(function({data}) {
|
||||||
|
@ -109,6 +116,13 @@ class Gist extends React.Component {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPublicChange(value) {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
public: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
changeMetadataProperty(property, value) {
|
changeMetadataProperty(property, value) {
|
||||||
const changedStyle = {
|
const changedStyle = {
|
||||||
...this.props.mapStyle,
|
...this.props.mapStyle,
|
||||||
|
@ -125,7 +139,7 @@ class Gist extends React.Component {
|
||||||
const user = gist.user || 'anonymous';
|
const user = gist.user || 'anonymous';
|
||||||
const preview = !!gist.files['index.html'];
|
const preview = !!gist.files['index.html'];
|
||||||
if(preview) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -137,11 +151,21 @@ class Gist extends React.Component {
|
||||||
return <p>Saving...</p>
|
return <p>Saving...</p>
|
||||||
} else if(gist) {
|
} else if(gist) {
|
||||||
const user = gist.user || 'anonymous';
|
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:{' '}
|
Latest saved gist:{' '}
|
||||||
{this.renderPreviewLink(this)}
|
{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>
|
||||||
|
<p>
|
||||||
|
<CopyToClipboard text={maputnikStyleLink}>
|
||||||
|
<span>Share this style: <Button><TiClipboard size={18} /></Button></span>
|
||||||
|
</CopyToClipboard>
|
||||||
|
<StringInput value={maputnikStyleLink} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,13 +175,22 @@ class Gist extends React.Component {
|
||||||
<MdFileDownload />
|
<MdFileDownload />
|
||||||
Save to Gist (anonymous)
|
Save to Gist (anonymous)
|
||||||
</Button>
|
</Button>
|
||||||
{' '}
|
<div className="maputnik-modal-sub-section">
|
||||||
|
<CheckboxInput
|
||||||
|
value={this.state.public}
|
||||||
|
name='gist-style-public'
|
||||||
|
onChange={this.onPublicChange.bind(this)}
|
||||||
|
/>
|
||||||
|
<span> Public gist</span>
|
||||||
|
</div>
|
||||||
|
<div className="maputnik-modal-sub-section">
|
||||||
<CheckboxInput
|
<CheckboxInput
|
||||||
value={this.state.preview}
|
value={this.state.preview}
|
||||||
name='gist-style-preview'
|
name='gist-style-preview'
|
||||||
onChange={this.onPreviewChange.bind(this)}
|
onChange={this.onPreviewChange.bind(this)}
|
||||||
/>
|
/>
|
||||||
<span> Include preview</span>
|
<span> Include preview</span>
|
||||||
|
</div>
|
||||||
{this.state.preview ?
|
{this.state.preview ?
|
||||||
<div>
|
<div>
|
||||||
<InputBlock
|
<InputBlock
|
||||||
|
@ -166,7 +199,13 @@ class Gist extends React.Component {
|
||||||
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
|
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
|
||||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}/>
|
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
<a target="_blank" href="https://openmaptiles.com/hosting/">Get your free access token</a>
|
<InputBlock
|
||||||
|
label={"Mapbox Access Token: "}>
|
||||||
|
<StringInput
|
||||||
|
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
|
||||||
|
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}/>
|
||||||
|
</InputBlock>
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href="https://openmaptiles.com/hosting/">Get your free access token</a>
|
||||||
</div>
|
</div>
|
||||||
: null}
|
: null}
|
||||||
{this.renderLatestGist()}
|
{this.renderLatestGist()}
|
||||||
|
@ -186,10 +225,10 @@ function stripAccessTokens(mapStyle) {
|
||||||
|
|
||||||
class ExportModal extends React.Component {
|
class ExportModal extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
mapStyle: React.PropTypes.object.isRequired,
|
mapStyle: PropTypes.object.isRequired,
|
||||||
onStyleChanged: React.PropTypes.func.isRequired,
|
onStyleChanged: PropTypes.func.isRequired,
|
||||||
isOpen: React.PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
onOpenToggle: React.PropTypes.func.isRequired,
|
onOpenToggle: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -197,7 +236,7 @@ class ExportModal extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadStyle() {
|
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");
|
saveAs(blob, this.props.mapStyle.id + ".json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import CloseIcon from 'react-icons/lib/md/close'
|
import CloseIcon from 'react-icons/lib/md/close'
|
||||||
import Overlay from './Overlay'
|
import Overlay from './Overlay'
|
||||||
|
|
||||||
class Modal extends React.Component {
|
class Modal extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
isOpen: React.PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
title: React.PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
onOpenToggle: React.PropTypes.func.isRequired,
|
onOpenToggle: PropTypes.func.isRequired,
|
||||||
|
children: PropTypes.node,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -21,8 +23,10 @@ class Modal extends React.Component {
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</a>
|
</a>
|
||||||
</header>
|
</header>
|
||||||
|
<div className="maputnik-modal-scroller">
|
||||||
<div className="maputnik-modal-content">{this.props.children}</div>
|
<div className="maputnik-modal-content">{this.props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
import FileReaderInput from 'react-file-reader-input'
|
import FileReaderInput from 'react-file-reader-input'
|
||||||
|
@ -12,10 +13,10 @@ import publicStyles from '../../config/styles.json'
|
||||||
|
|
||||||
class PublicStyle extends React.Component {
|
class PublicStyle extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
url: React.PropTypes.string.isRequired,
|
url: PropTypes.string.isRequired,
|
||||||
thumbnailUrl: React.PropTypes.string.isRequired,
|
thumbnailUrl: PropTypes.string.isRequired,
|
||||||
title: React.PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
onSelect: React.PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -41,9 +42,9 @@ class PublicStyle extends React.Component {
|
||||||
|
|
||||||
class OpenModal extends React.Component {
|
class OpenModal extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
isOpen: React.PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
onOpenToggle: React.PropTypes.func.isRequired,
|
onOpenToggle: PropTypes.func.isRequired,
|
||||||
onStyleOpen: React.PropTypes.func.isRequired,
|
onStyleOpen: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -68,12 +69,18 @@ class OpenModal extends React.Component {
|
||||||
const mapStyle = style.ensureStyleValidity(JSON.parse(body))
|
const mapStyle = style.ensureStyleValidity(JSON.parse(body))
|
||||||
console.log('Loaded style ', mapStyle.id)
|
console.log('Loaded style ', mapStyle.id)
|
||||||
this.props.onStyleOpen(mapStyle)
|
this.props.onStyleOpen(mapStyle)
|
||||||
|
this.onOpenToggle()
|
||||||
} else {
|
} else {
|
||||||
console.warn('Could not open the style URL', styleUrl)
|
console.warn('Could not open the style URL', styleUrl)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onOpenUrl() {
|
||||||
|
const url = this.styleUrlElement.value;
|
||||||
|
this.onStyleSelect(url);
|
||||||
|
}
|
||||||
|
|
||||||
onUpload(_, files) {
|
onUpload(_, files) {
|
||||||
const [e, file] = files[0];
|
const [e, file] = files[0];
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
@ -94,6 +101,7 @@ class OpenModal extends React.Component {
|
||||||
}
|
}
|
||||||
mapStyle = style.ensureStyleValidity(mapStyle)
|
mapStyle = style.ensureStyleValidity(mapStyle)
|
||||||
this.props.onStyleOpen(mapStyle);
|
this.props.onStyleOpen(mapStyle);
|
||||||
|
this.onOpenToggle();
|
||||||
}
|
}
|
||||||
reader.onerror = e => console.log(e.target);
|
reader.onerror = e => console.log(e.target);
|
||||||
}
|
}
|
||||||
|
@ -137,6 +145,18 @@ class OpenModal extends React.Component {
|
||||||
<Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button>
|
<Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button>
|
||||||
</FileReaderInput>
|
</FileReaderInput>
|
||||||
</section>
|
</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">
|
<section className="maputnik-modal-section maputnik-modal-section--shrink">
|
||||||
<h2>Gallery Styles</h2>
|
<h2>Gallery Styles</h2>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
|
||||||
class Overlay extends React.Component {
|
class Overlay extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
isOpen: React.PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
children: React.PropTypes.element.isRequired
|
children: PropTypes.element.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react'
|
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 InputBlock from '../inputs/InputBlock'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
@ -8,10 +9,10 @@ import Modal from './Modal'
|
||||||
|
|
||||||
class SettingsModal extends React.Component {
|
class SettingsModal extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
mapStyle: React.PropTypes.object.isRequired,
|
mapStyle: PropTypes.object.isRequired,
|
||||||
onStyleChanged: React.PropTypes.func.isRequired,
|
onStyleChanged: PropTypes.func.isRequired,
|
||||||
isOpen: React.PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
onOpenToggle: React.PropTypes.func.isRequired,
|
onOpenToggle: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -46,7 +47,7 @@ class SettingsModal extends React.Component {
|
||||||
title={'Style Settings'}
|
title={'Style Settings'}
|
||||||
>
|
>
|
||||||
<div style={{minWidth: 350}}>
|
<div style={{minWidth: 350}}>
|
||||||
<InputBlock label={"Name"} doc={GlSpec.$root.name.doc}>
|
<InputBlock label={"Name"} doc={styleSpec.latest.$root.name.doc}>
|
||||||
<StringInput {...inputProps}
|
<StringInput {...inputProps}
|
||||||
value={this.props.mapStyle.name}
|
value={this.props.mapStyle.name}
|
||||||
onChange={this.changeStyleProperty.bind(this, "name")}
|
onChange={this.changeStyleProperty.bind(this, "name")}
|
||||||
|
@ -58,14 +59,14 @@ class SettingsModal extends React.Component {
|
||||||
onChange={this.changeStyleProperty.bind(this, "owner")}
|
onChange={this.changeStyleProperty.bind(this, "owner")}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
<InputBlock label={"Sprite URL"} doc={GlSpec.$root.sprite.doc}>
|
<InputBlock label={"Sprite URL"} doc={styleSpec.latest.$root.sprite.doc}>
|
||||||
<StringInput {...inputProps}
|
<StringInput {...inputProps}
|
||||||
value={this.props.mapStyle.sprite}
|
value={this.props.mapStyle.sprite}
|
||||||
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
|
|
||||||
<InputBlock label={"Glyphs URL"} doc={GlSpec.$root.glyphs.doc}>
|
<InputBlock label={"Glyphs URL"} doc={styleSpec.latest.$root.glyphs.doc}>
|
||||||
<StringInput {...inputProps}
|
<StringInput {...inputProps}
|
||||||
value={this.props.mapStyle.glyphs}
|
value={this.props.mapStyle.glyphs}
|
||||||
onChange={this.changeStyleProperty.bind(this, "glyphs")}
|
onChange={this.changeStyleProperty.bind(this, "glyphs")}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react'
|
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 Modal from './Modal'
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
import InputBlock from '../inputs/InputBlock'
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
@ -16,10 +17,10 @@ import DeleteIcon from 'react-icons/lib/md/delete'
|
||||||
|
|
||||||
class PublicSource extends React.Component {
|
class PublicSource extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
id: React.PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
type: React.PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
title: React.PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
onSelect: React.PropTypes.func.isRequired,
|
onSelect: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -44,6 +45,10 @@ function editorMode(source) {
|
||||||
if(source.tiles) return 'tilexyz_raster'
|
if(source.tiles) return 'tilexyz_raster'
|
||||||
return 'tilejson_raster'
|
return 'tilejson_raster'
|
||||||
}
|
}
|
||||||
|
if(source.type === 'raster-dem') {
|
||||||
|
if(source.tiles) return 'tilexyz_raster-dem'
|
||||||
|
return 'tilejson_raster-dem'
|
||||||
|
}
|
||||||
if(source.type === 'vector') {
|
if(source.type === 'vector') {
|
||||||
if(source.tiles) return 'tilexyz_vector'
|
if(source.tiles) return 'tilexyz_vector'
|
||||||
return 'tilejson_vector'
|
return 'tilejson_vector'
|
||||||
|
@ -54,10 +59,10 @@ function editorMode(source) {
|
||||||
|
|
||||||
class ActiveSourceTypeEditor extends React.Component {
|
class ActiveSourceTypeEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
sourceId: React.PropTypes.string.isRequired,
|
sourceId: PropTypes.string.isRequired,
|
||||||
source: React.PropTypes.object.isRequired,
|
source: PropTypes.object.isRequired,
|
||||||
onDelete: React.PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -87,7 +92,7 @@ class ActiveSourceTypeEditor extends React.Component {
|
||||||
|
|
||||||
class AddSource extends React.Component {
|
class AddSource extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onAdd: React.PropTypes.func.isRequired,
|
onAdd: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -126,6 +131,16 @@ class AddSource extends React.Component {
|
||||||
minzoom: source.minzoom || 0,
|
minzoom: source.minzoom || 0,
|
||||||
maxzoom: source.maxzoom || 14
|
maxzoom: source.maxzoom || 14
|
||||||
}
|
}
|
||||||
|
case 'tilejson_raster-dem': return {
|
||||||
|
type: 'raster-dem',
|
||||||
|
url: source.url || 'http://localhost:3000/tilejson.json'
|
||||||
|
}
|
||||||
|
case 'tilexyz_raster-dem': return {
|
||||||
|
type: 'raster-dem',
|
||||||
|
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
|
||||||
|
minzoom: source.minzoom || 0,
|
||||||
|
maxzoom: source.maxzoom || 14
|
||||||
|
}
|
||||||
default: return {}
|
default: return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,7 +153,7 @@ class AddSource extends React.Component {
|
||||||
onChange={v => this.setState({ sourceId: v})}
|
onChange={v => this.setState({ sourceId: v})}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
<InputBlock label={"Source Type"} doc={GlSpec.source_tile.type.doc}>
|
<InputBlock label={"Source Type"} doc={styleSpec.latest.source_vector.type.doc}>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={[
|
options={[
|
||||||
['geojson', 'GeoJSON'],
|
['geojson', 'GeoJSON'],
|
||||||
|
@ -146,6 +161,8 @@ class AddSource extends React.Component {
|
||||||
['tilexyz_vector', 'Vector (XYZ URLs)'],
|
['tilexyz_vector', 'Vector (XYZ URLs)'],
|
||||||
['tilejson_raster', 'Raster (TileJSON URL)'],
|
['tilejson_raster', 'Raster (TileJSON URL)'],
|
||||||
['tilexyz_raster', 'Raster (XYZ URL)'],
|
['tilexyz_raster', 'Raster (XYZ URL)'],
|
||||||
|
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
|
||||||
|
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
|
||||||
]}
|
]}
|
||||||
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
|
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
|
||||||
value={this.state.mode}
|
value={this.state.mode}
|
||||||
|
@ -167,10 +184,10 @@ class AddSource extends React.Component {
|
||||||
|
|
||||||
class SourcesModal extends React.Component {
|
class SourcesModal extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
mapStyle: React.PropTypes.object.isRequired,
|
mapStyle: PropTypes.object.isRequired,
|
||||||
isOpen: React.PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
onOpenToggle: React.PropTypes.func.isRequired,
|
onOpenToggle: PropTypes.func.isRequired,
|
||||||
onStyleChanged: React.PropTypes.func.isRequired,
|
onStyleChanged: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
stripTitle(source) {
|
stripTitle(source) {
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import React from 'react'
|
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 InputBlock from '../inputs/InputBlock'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
import NumberInput from '../inputs/NumberInput'
|
import NumberInput from '../inputs/NumberInput'
|
||||||
|
|
||||||
class TileJSONSourceEditor extends React.Component {
|
class TileJSONSourceEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
source: React.PropTypes.object.isRequired,
|
source: PropTypes.object.isRequired,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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
|
<StringInput
|
||||||
value={this.props.source.url}
|
value={this.props.source.url}
|
||||||
onChange={url => this.props.onChange({
|
onChange={url => this.props.onChange({
|
||||||
|
@ -25,8 +26,8 @@ class TileJSONSourceEditor extends React.Component {
|
||||||
|
|
||||||
class TileURLSourceEditor extends React.Component {
|
class TileURLSourceEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
source: React.PropTypes.object.isRequired,
|
source: PropTypes.object.isRequired,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
changeTileUrl(idx, value) {
|
changeTileUrl(idx, value) {
|
||||||
|
@ -42,7 +43,7 @@ class TileURLSourceEditor extends React.Component {
|
||||||
const prefix = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th']
|
const prefix = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th']
|
||||||
const tiles = this.props.source.tiles || []
|
const tiles = this.props.source.tiles || []
|
||||||
return tiles.map((tileUrl, tileIndex) => {
|
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
|
<StringInput
|
||||||
value={tileUrl}
|
value={tileUrl}
|
||||||
onChange={this.changeTileUrl.bind(this, tileIndex)}
|
onChange={this.changeTileUrl.bind(this, tileIndex)}
|
||||||
|
@ -54,7 +55,7 @@ class TileURLSourceEditor extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return <div>
|
return <div>
|
||||||
{this.renderTileUrls()}
|
{this.renderTileUrls()}
|
||||||
<InputBlock label={"Min Zoom"} doc={GlSpec.source_tile.minzoom.doc}>
|
<InputBlock label={"Min Zoom"} doc={styleSpec.latest.source_vector.minzoom.doc}>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={this.props.source.minzoom || 0}
|
value={this.props.source.minzoom || 0}
|
||||||
onChange={minzoom => this.props.onChange({
|
onChange={minzoom => this.props.onChange({
|
||||||
|
@ -63,7 +64,7 @@ class TileURLSourceEditor extends React.Component {
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
<InputBlock label={"Max Zoom"} doc={GlSpec.source_tile.maxzoom.doc}>
|
<InputBlock label={"Max Zoom"} doc={styleSpec.latest.source_vector.maxzoom.doc}>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={this.props.source.maxzoom || 22}
|
value={this.props.source.maxzoom || 22}
|
||||||
onChange={maxzoom => this.props.onChange({
|
onChange={maxzoom => this.props.onChange({
|
||||||
|
@ -79,12 +80,12 @@ class TileURLSourceEditor extends React.Component {
|
||||||
|
|
||||||
class GeoJSONSourceEditor extends React.Component {
|
class GeoJSONSourceEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
source: React.PropTypes.object.isRequired,
|
source: PropTypes.object.isRequired,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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
|
<StringInput
|
||||||
value={this.props.source.data}
|
value={this.props.source.data}
|
||||||
onChange={data => this.props.onChange({
|
onChange={data => this.props.onChange({
|
||||||
|
@ -98,9 +99,9 @@ class GeoJSONSourceEditor extends React.Component {
|
||||||
|
|
||||||
class SourceTypeEditor extends React.Component {
|
class SourceTypeEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
mode: React.PropTypes.string.isRequired,
|
mode: PropTypes.string.isRequired,
|
||||||
source: React.PropTypes.object.isRequired,
|
source: PropTypes.object.isRequired,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -114,6 +115,8 @@ class SourceTypeEditor extends React.Component {
|
||||||
case 'tilexyz_vector': return <TileURLSourceEditor {...commonProps} />
|
case 'tilexyz_vector': return <TileURLSourceEditor {...commonProps} />
|
||||||
case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} />
|
case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} />
|
||||||
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
|
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
|
||||||
|
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
|
||||||
|
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps} />
|
||||||
default: return null
|
default: return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,7 +91,8 @@
|
||||||
"circle-stroke-width",
|
"circle-stroke-width",
|
||||||
"circle-pitch-scale",
|
"circle-pitch-scale",
|
||||||
"circle-translate",
|
"circle-translate",
|
||||||
"circle-translate-anchor"
|
"circle-translate-anchor",
|
||||||
|
"circle-pitch-alignment"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -147,7 +148,9 @@
|
||||||
"icon-rotate",
|
"icon-rotate",
|
||||||
"icon-padding",
|
"icon-padding",
|
||||||
"icon-keep-upright",
|
"icon-keep-upright",
|
||||||
"icon-offset"
|
"icon-offset",
|
||||||
|
"icon-anchor",
|
||||||
|
"icon-pitch-alignment"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -194,5 +197,35 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"hillshade": {
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"title": "Paint properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"hillshade-illumination-direction",
|
||||||
|
"hillshade-illumination-anchor",
|
||||||
|
"hillshade-exaggeration",
|
||||||
|
"hillshade-shadow-color",
|
||||||
|
"hillshade-highlight-color",
|
||||||
|
"hillshade-accent-color"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"heatmap": {
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"title": "Paint properties",
|
||||||
|
"type": "properties",
|
||||||
|
"fields": [
|
||||||
|
"heatmap-radius",
|
||||||
|
"heatmap-weight",
|
||||||
|
"heatmap-intensity",
|
||||||
|
"heatmap-opacity"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,25 +3,25 @@
|
||||||
"id": "klokantech-basic",
|
"id": "klokantech-basic",
|
||||||
"title": "Klokantech Basic",
|
"title": "Klokantech Basic",
|
||||||
"url": "https://rawgit.com/openmaptiles/klokantech-basic-gl-style/master/style.json",
|
"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",
|
"id": "dark-matter",
|
||||||
"title": "Dark Matter",
|
"title": "Dark Matter",
|
||||||
"url": "https://rawgit.com/openmaptiles/dark-matter-gl-style/master/style.json",
|
"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",
|
"id": "positron",
|
||||||
"title": "Positron",
|
"title": "Positron",
|
||||||
"url": "https://rawgit.com/openmaptiles/positron-gl-style/master/style.json",
|
"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",
|
"id": "osm-bright",
|
||||||
"title": "OSM Bright",
|
"title": "OSM Bright",
|
||||||
"url": "https://rawgit.com/openmaptiles/osm-bright-gl-style/master/style.json",
|
"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",
|
"id": "osm-liberty",
|
||||||
|
@ -39,24 +39,18 @@
|
||||||
"id": "mapbox-satellite",
|
"id": "mapbox-satellite",
|
||||||
"title": "Mapbox Satellite",
|
"title": "Mapbox Satellite",
|
||||||
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/satellite-v9.json",
|
"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",
|
"id": "mapbox-bright",
|
||||||
"title": "Mapbox Bright",
|
"title": "Mapbox Bright",
|
||||||
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/bright-v9.json",
|
"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",
|
"id": "mapbox-basic",
|
||||||
"title": "Mapbox Basic",
|
"title": "Mapbox Basic",
|
||||||
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/basic-v9.json",
|
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/basic-v9.json",
|
||||||
"thumbnail": "http://maputnik.com/thumbnails/mapbox-basic.png"
|
"thumbnail": "https://maputnik.github.io/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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,14 +8,5 @@
|
||||||
"type": "vector",
|
"type": "vector",
|
||||||
"url": "https://free.tilehosting.com/data/v3.json?key={key}",
|
"url": "https://free.tilehosting.com/data/v3.json?key={key}",
|
||||||
"title": "OpenMapTiles"
|
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
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(' '))
|
return changes.map(cmd => cmd.command + ' ' + cmd.args.join(' '))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
9
src/libs/document-uid.js
Normal file
9
src/libs/document-uid.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/**
|
||||||
|
* A unique id for the current document.
|
||||||
|
*/
|
||||||
|
let REF = 0;
|
||||||
|
|
||||||
|
export default function(prefix="") {
|
||||||
|
REF++;
|
||||||
|
return prefix+REF;
|
||||||
|
}
|
|
@ -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 combiningFilterOps = ['all', 'any', 'none']
|
||||||
export const setFilterOps = ['in', '!in']
|
export const setFilterOps = ['in', '!in']
|
||||||
export const otherFilterOps = Object
|
export const otherFilterOps = Object
|
||||||
.keys(GlSpec.filter_operator.values)
|
.keys(styleSpec.latest.filter_operator.values)
|
||||||
.filter(op => combiningFilterOps.indexOf(op) < 0)
|
.filter(op => combiningFilterOps.indexOf(op) < 0)
|
||||||
|
|
|
@ -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) {
|
export function changeType(layer, newType) {
|
||||||
const changedPaintProps = { ...layer.paint }
|
const changedPaintProps = { ...layer.paint }
|
||||||
Object.keys(changedPaintProps).forEach(propertyName => {
|
Object.keys(changedPaintProps).forEach(propertyName => {
|
||||||
if(!(propertyName in GlSpec['paint_' + newType])) {
|
if(!(propertyName in styleSpec.latest['paint_' + newType])) {
|
||||||
delete changedPaintProps[propertyName]
|
delete changedPaintProps[propertyName]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const changedLayoutProps = { ...layer.layout }
|
const changedLayoutProps = { ...layer.layout }
|
||||||
Object.keys(changedLayoutProps).forEach(propertyName => {
|
Object.keys(changedLayoutProps).forEach(propertyName => {
|
||||||
if(!(propertyName in GlSpec['layout_' + newType])) {
|
if(!(propertyName in styleSpec.latest['layout_' + newType])) {
|
||||||
delete changedLayoutProps[propertyName]
|
delete changedLayoutProps[propertyName]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
|
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
|
||||||
|
|
||||||
// Load mapbox-gl-rtl-text using object urls without needing http://localhost for AJAX.
|
// Load mapbox-gl-rtl-text using object urls without needing http://localhost for AJAX.
|
||||||
const data = require("base64?mimetype=text/javascript!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.js");
|
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 blob = new window.Blob([window.atob(data)]);
|
||||||
const objectUrl = window.URL.createObjectURL(blob, {
|
const objectUrl = window.URL.createObjectURL(blob, {
|
||||||
|
|
14
src/libs/sort-numerically.js
Normal file
14
src/libs/sort-numerically.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import spec from 'mapbox-gl-style-spec/reference/latest.min.js'
|
import deref from '@mapbox/mapbox-gl-style-spec/deref'
|
||||||
import derefLayers from 'mapbox-gl-style-spec/lib/deref'
|
|
||||||
import tokens from '../config/tokens.json'
|
import tokens from '../config/tokens.json'
|
||||||
|
|
||||||
// Empty style is always used if no style could be restored or fetched
|
// Empty style is always used if no style could be restored or fetched
|
||||||
|
@ -37,7 +36,7 @@ function ensureHasNoInteractive(style) {
|
||||||
function ensureHasNoRefs(style) {
|
function ensureHasNoRefs(style) {
|
||||||
const derefedStyle = {
|
const derefedStyle = {
|
||||||
...style,
|
...style,
|
||||||
layers: derefLayers(style.layers)
|
layers: deref(style.layers)
|
||||||
}
|
}
|
||||||
return derefedStyle
|
return derefedStyle
|
||||||
}
|
}
|
||||||
|
@ -55,12 +54,23 @@ function indexOfLayer(layers, layerId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceAccessToken(mapStyle) {
|
function replaceAccessToken(mapStyle, opts={}) {
|
||||||
const omtSource = mapStyle.sources.openmaptiles
|
const omtSource = mapStyle.sources.openmaptiles
|
||||||
if(!omtSource) return mapStyle
|
if(!omtSource) return mapStyle
|
||||||
|
if(!omtSource.hasOwnProperty("url")) return mapStyle
|
||||||
|
|
||||||
const metadata = mapStyle.metadata || {}
|
const metadata = mapStyle.metadata || {}
|
||||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
let accessToken = metadata['maputnik:openmaptiles_access_token'];
|
||||||
|
|
||||||
|
if(opts.allowFallback && !accessToken) {
|
||||||
|
accessToken = tokens.openmaptiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!accessToken) {
|
||||||
|
// Early exit.
|
||||||
|
return mapStyle;
|
||||||
|
}
|
||||||
|
|
||||||
const changedSources = {
|
const changedSources = {
|
||||||
...mapStyle.sources,
|
...mapStyle.sources,
|
||||||
openmaptiles: {
|
openmaptiles: {
|
||||||
|
|
26
src/libs/zoomcontrol.js
Normal file
26
src/libs/zoomcontrol.js
Normal 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
9
src/manifest.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "Maputnik",
|
||||||
|
"short_name": "Maputnik",
|
||||||
|
"description": "Visual Map Designer",
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "browser",
|
||||||
|
"background_color": "#1c1f24",
|
||||||
|
"theme_color": "#1c1f24"
|
||||||
|
}
|
|
@ -25,6 +25,13 @@
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-zoom {
|
||||||
|
color: rgb(138, 138, 138);
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 4px 8px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
.mapboxgl-ctrl-group {
|
.mapboxgl-ctrl-group {
|
||||||
background: rgb(28, 31, 36);
|
background: rgb(28, 31, 36);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,15 +35,18 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-doc-target:hover .maputnik-doc-popup {
|
.maputnik-doc-target:hover .maputnik-doc-popup {
|
||||||
display: block;
|
display: block;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
// BUTTON
|
// BUTTON
|
||||||
.maputnik-button {
|
.maputnik-button {
|
||||||
|
display: inline-block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: $color-midgray;
|
background-color: $color-midgray;
|
||||||
color: $color-lowgray;
|
color: $color-lowgray;
|
||||||
|
@ -104,13 +107,17 @@
|
||||||
.maputnik-action-block {
|
.maputnik-action-block {
|
||||||
.maputnik-input-block-label {
|
.maputnik-input-block-label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 43%;
|
width: 35%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-input-block-action {
|
.maputnik-input-block-action {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 7%;
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-input-block-action > div {
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,3 +3,15 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maputnik-render-gist {
|
||||||
|
p {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.maputnik-string {
|
||||||
|
margin-left: 5px;
|
||||||
|
width: 60%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -52,6 +52,25 @@
|
||||||
> * {
|
> * {
|
||||||
margin-bottom: $margin-3;
|
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
|
// SELECT
|
||||||
|
@ -122,7 +141,8 @@
|
||||||
&-menu {
|
&-menu {
|
||||||
border: none;
|
border: none;
|
||||||
padding: 2px 0;
|
padding: 2px 0;
|
||||||
position: fixed;
|
margin-right: 10px;
|
||||||
|
position: absolute;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
max-height: 50%;
|
max-height: 50%;
|
||||||
background: $color-gray;
|
background: $color-gray;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// LAYER LIST
|
// LAYER LIST
|
||||||
.maputnik-layer-list {
|
.maputnik-layer-list {
|
||||||
&-header {
|
&-header {
|
||||||
padding: $margin-2;
|
padding: $margin-2 $margin-2 $margin-3;
|
||||||
|
|
||||||
@include flex-row;
|
@include flex-row;
|
||||||
|
|
||||||
|
|
22
src/styles/_map.scss
Normal file
22
src/styles/_map.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,9 +2,11 @@
|
||||||
.maputnik-modal {
|
.maputnik-modal {
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
|
overflow: hidden;
|
||||||
background-color: $color-black;
|
background-color: $color-black;
|
||||||
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.3);
|
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.3);
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-modal-section {
|
.maputnik-modal-section {
|
||||||
|
@ -19,6 +21,10 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maputnik-modal-sub-section {
|
||||||
|
margin-top: $margin-1;
|
||||||
|
}
|
||||||
|
|
||||||
.maputnik-modal-section--shrink {
|
.maputnik-modal-section--shrink {
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
|
@ -39,9 +45,13 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maputnik-modal-scroller {
|
||||||
|
max-height: calc(100vh - 35px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.maputnik-modal-content {
|
.maputnik-modal-content {
|
||||||
padding: $margin-3;
|
padding: $margin-3;
|
||||||
max-height: 90vh;
|
|
||||||
|
|
||||||
@include flex-column;
|
@include flex-column;
|
||||||
}
|
}
|
||||||
|
@ -70,6 +80,7 @@
|
||||||
position: fixed;
|
position: fixed;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
z-index: 9;
|
||||||
|
|
||||||
@include flex-row;
|
@include flex-row;
|
||||||
}
|
}
|
||||||
|
@ -80,7 +91,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-style-gallery-container {
|
.maputnik-style-gallery-container {
|
||||||
overflow-y: scroll;
|
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
.maputnik-popup-layer {
|
.maputnik-popup-layer {
|
||||||
display: block;
|
display: block;
|
||||||
color: $color-lowgray;
|
color: $color-lowgray;
|
||||||
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
padding: $margin-2;
|
padding: $margin-2;
|
||||||
|
|
|
@ -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;
|
background-color: #26282e;
|
||||||
width: 5px;
|
width: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||||
background-color: #666;
|
background-color: #666;
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -22,9 +22,8 @@
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
|
||||||
padding-right: $margin-2;
|
padding-right: $margin-2;
|
||||||
vertical-align: middle;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,12 +36,24 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
line-height: 20px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $color-midgray;
|
background-color: $color-midgray;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maputnik-toolbar-version {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 10px;
|
||||||
|
bottom: -2px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.maputnik-toolbar-action {
|
.maputnik-toolbar-action {
|
||||||
@extend .maputnik-toolbar-link;
|
@extend .maputnik-toolbar-link;
|
||||||
}
|
}
|
||||||
|
@ -55,3 +66,17 @@
|
||||||
display: inline;
|
display: inline;
|
||||||
margin-left: $margin-1;
|
margin-left: $margin-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maputnik-toolbar-logo {
|
||||||
|
flex: 0 0 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-toolbar__inner {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-toolbar__actions {
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
|
@ -67,3 +67,68 @@
|
||||||
.maputnik-zoom-spec-property .maputnik-input-block:not(:first-child) .maputnik-input-block-label {
|
.maputnik-zoom-spec-property .maputnik-input-block:not(:first-child) .maputnik-input-block-label {
|
||||||
visibility: hidden;
|
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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -35,3 +35,4 @@ $toolbar-offset: 0;
|
||||||
@import 'filtereditor';
|
@import 'filtereditor';
|
||||||
@import 'zoomproperty';
|
@import 'zoomproperty';
|
||||||
@import 'popup';
|
@import 'popup';
|
||||||
|
@import 'map';
|
||||||
|
|
|
@ -4,8 +4,73 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="manifest" href="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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">Loading...</div>
|
<div id="app">
|
||||||
|
<div id="loader">Loading...</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -9,7 +9,7 @@ describe('maputnik', function() {
|
||||||
browser.waitForExist(".maputnik-toolbar-link");
|
browser.waitForExist(".maputnik-toolbar-link");
|
||||||
|
|
||||||
var src = browser.getAttribute(".maputnik-toolbar-link img", "src");
|
var src = browser.getAttribute(".maputnik-toolbar-link img", "src");
|
||||||
assert.equal(src, config.baseUrl+'/img/maputnik.png');
|
assert.equal(src, config.baseUrl+'/img/logo-color.svg');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue