Merge branch 'master' into master

This commit is contained in:
ziveo 2018-03-18 20:02:20 -04:00 committed by GitHub
commit ad83f940a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
98 changed files with 16151 additions and 858 deletions

4
.babelrc Normal file
View file

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

View file

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

View file

@ -1,11 +1,23 @@
# Maputnik [![Build Status](https://travis-ci.org/maputnik/editor.svg?branch=master)](https://travis-ci.org/maputnik/editor) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/anelbgv6jdb3qnh9/branch/master?svg=true)](https://ci.appveyor.com/project/lukasmartinelli/editor) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://tldrlegal.com/license/mit-license) # Maputnik
[![Build Status](https://travis-ci.org/maputnik/editor.svg?branch=master)][travis]
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/anelbgv6jdb3qnh9/branch/master?svg=true)][appveyor]
[![Dependency Status](https://david-dm.org/maputnik/editor.svg)][dm-prod]
[![Dev Dependency Status](https://david-dm.org/maputnik/editor/dev-status.svg)][dm-dev]
[![License](https://img.shields.io/badge/license-MIT-blue.svg)][license]
[travis]: https://travis-ci.org/maputnik/editor
[appveyor]: https://ci.appveyor.com/project/lukasmartinelli/editor
[dm-prod]: https://david-dm.org/maputnik/editor
[dm-dev]: https://david-dm.org/maputnik/editor#info=devDependencies
[license]: https://tldrlegal.com/license/mit-license
<img width="200" align="right" alt="Maputnik" src="src/img/maputnik.png" /> <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.

View file

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

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,140 @@
import React from 'react'
import PropTypes from 'prop-types'
import SpecProperty from './_SpecProperty'
import DataProperty from './_DataProperty'
import ZoomProperty from './_ZoomProperty'
function isZoomField(value) {
return typeof value === 'object' && value.stops && typeof value.property === 'undefined'
}
function isDataField(value) {
return typeof value === 'object' && value.stops && typeof value.property !== 'undefined'
}
/** Supports displaying spec field for zoom function objects
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
*/
export default class FunctionSpecProperty extends React.Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
fieldName: PropTypes.string.isRequired,
fieldSpec: PropTypes.object.isRequired,
value: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string,
PropTypes.number,
PropTypes.bool,
PropTypes.array
]),
}
addStop() {
const stops = this.props.value.stops.slice(0)
const lastStop = stops[stops.length - 1]
if (typeof lastStop[0] === "object") {
stops.push([
{zoom: lastStop[0].zoom + 1, value: lastStop[0].value},
lastStop[1]
])
}
else {
stops.push([lastStop[0] + 1, lastStop[1]])
}
const changedValue = {
...this.props.value,
stops: stops,
}
this.props.onChange(this.props.fieldName, changedValue)
}
deleteStop(stopIdx) {
const stops = this.props.value.stops.slice(0)
stops.splice(stopIdx, 1)
let changedValue = {
...this.props.value,
stops: stops,
}
if(stops.length === 1) {
changedValue = stops[0][1]
}
this.props.onChange(this.props.fieldName, changedValue)
}
makeZoomFunction() {
const zoomFunc = {
stops: [
[6, this.props.value],
[10, this.props.value]
]
}
this.props.onChange(this.props.fieldName, zoomFunc)
}
makeDataFunction() {
const dataFunc = {
property: "",
type: "categorical",
stops: [
[{zoom: 6, value: 0}, this.props.value],
[{zoom: 10, value: 0}, this.props.value]
]
}
this.props.onChange(this.props.fieldName, dataFunc)
}
render() {
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
let specField;
if (isZoomField(this.props.value)) {
specField = (
<ZoomProperty
onChange={this.props.onChange.bind(this)}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onDeleteStop={this.deleteStop.bind(this)}
onAddStop={this.addStop.bind(this)}
/>
)
}
else if (isDataField(this.props.value)) {
specField = (
<DataProperty
onChange={this.props.onChange.bind(this)}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onDeleteStop={this.deleteStop.bind(this)}
onAddStop={this.addStop.bind(this)}
/>
)
}
else {
specField = (
<SpecProperty
onChange={this.props.onChange.bind(this)}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}
onZoomClick={this.makeZoomFunction.bind(this)}
onDataClick={this.makeDataFunction.bind(this)}
/>
)
}
return <div className={propClass}>
{specField}
</div>
}
}

View file

@ -1,6 +1,7 @@
import React from 'react' import 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}

View file

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

View file

@ -1,180 +0,0 @@
import React from 'react'
import Color from 'color'
import Button from '../Button'
import SpecField from './SpecField'
import NumberInput from '../inputs/NumberInput'
import DocLabel from './DocLabel'
import InputBlock from '../inputs/InputBlock'
import AddIcon from 'react-icons/lib/md/add-circle-outline'
import DeleteIcon from 'react-icons/lib/md/delete'
import FunctionIcon from 'react-icons/lib/md/functions'
import capitalize from 'lodash.capitalize'
function isZoomField(value) {
return typeof value === 'object' && value.stops
}
/** Supports displaying spec field for zoom function objects
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
*/
export default class ZoomSpecProperty extends React.Component {
static propTypes = {
onChange: React.PropTypes.func.isRequired,
fieldName: React.PropTypes.string.isRequired,
fieldSpec: React.PropTypes.object.isRequired,
value: React.PropTypes.oneOfType([
React.PropTypes.object,
React.PropTypes.string,
React.PropTypes.number,
React.PropTypes.bool,
]),
}
addStop() {
const stops = this.props.value.stops.slice(0)
const lastStop = stops[stops.length - 1]
stops.push([lastStop[0] + 1, lastStop[1]])
const changedValue = {
...this.props.value,
stops: stops,
}
this.props.onChange(this.props.fieldName, changedValue)
}
deleteStop(stopIdx) {
const stops = this.props.value.stops.slice(0)
stops.splice(stopIdx, 1)
let changedValue = {
...this.props.value,
stops: stops,
}
if(stops.length === 1) {
changedValue = stops[0][1]
}
this.props.onChange(this.props.fieldName, changedValue)
}
makeZoomFunction() {
const zoomFunc = {
stops: [
[6, this.props.value],
[10, this.props.value]
]
}
this.props.onChange(this.props.fieldName, zoomFunc)
}
changeStop(changeIdx, zoomLevel, value) {
const stops = this.props.value.stops.slice(0)
stops[changeIdx] = [zoomLevel, value]
const changedValue = {
...this.props.value,
stops: stops,
}
this.props.onChange(this.props.fieldName, changedValue)
}
renderZoomProperty() {
const zoomFields = this.props.value.stops.map((stop, idx) => {
const zoomLevel = stop[0]
const value = stop[1]
const deleteStopBtn= <DeleteStopButton onClick={this.deleteStop.bind(this, idx)} />
return <InputBlock
key={zoomLevel}
doc={this.props.fieldSpec.doc}
label={labelFromFieldName(this.props.fieldName)}
action={deleteStopBtn}
>
<div>
<div className="maputnik-zoom-spec-property-stop-edit">
<NumberInput
value={zoomLevel}
onChange={changedStop => this.changeStop(idx, changedStop, value)}
min={0}
max={22}
/>
</div>
<div className="maputnik-zoom-spec-property-stop-value">
<SpecField
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={value}
onChange={(_, newValue) => this.changeStop(idx, zoomLevel, newValue)}
/>
</div>
</div>
</InputBlock>
})
return <div className="maputnik-zoom-spec-property">
{zoomFields}
<Button
className="maputnik-add-stop"
onClick={this.addStop.bind(this)}
>
Add stop
</Button>
</div>
}
renderProperty() {
let zoomBtn = null
if(this.props.fieldSpec['zoom-function']) {
zoomBtn = <MakeZoomFunctionButton onClick={this.makeZoomFunction.bind(this)} />
}
return <InputBlock
doc={this.props.fieldSpec.doc}
label={labelFromFieldName(this.props.fieldName)}
action={zoomBtn}
>
<SpecField {...this.props} />
</InputBlock>
}
render() {
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
return <div className={propClass}>
{isZoomField(this.props.value) ? this.renderZoomProperty() : this.renderProperty()}
</div>
}
}
function MakeZoomFunctionButton(props) {
return <Button
className="maputnik-make-zoom-function"
onClick={props.onClick}
>
<DocLabel
label={<FunctionIcon />}
cursorTargetStyle={{ cursor: 'pointer' }}
doc={"Turn property into a zoom function to enable a map feature to change with map's zoom level."}
/>
</Button>
}
function DeleteStopButton(props) {
return <Button
className="maputnik-delete-stop"
onClick={props.onClick}
>
<DocLabel
label={<DeleteIcon />}
doc={"Remove zoom level stop."}
/>
</Button>
}
function labelFromFieldName(fieldName) {
let label = fieldName.split('-').slice(1).join(' ')
return capitalize(label)
}

View file

@ -0,0 +1,176 @@
import React from 'react'
import PropTypes from 'prop-types'
import Button from '../Button'
import SpecField from './SpecField'
import NumberInput from '../inputs/NumberInput'
import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
import DocLabel from './DocLabel'
import InputBlock from '../inputs/InputBlock'
import labelFromFieldName from './_labelFromFieldName'
import DeleteStopButton from './_DeleteStopButton'
export default class DataProperty extends React.Component {
static propTypes = {
onChange: PropTypes.func,
onDeleteStop: PropTypes.func,
onAddStop: PropTypes.func,
fieldName: PropTypes.string,
fieldSpec: PropTypes.object,
value: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string,
PropTypes.number,
PropTypes.bool,
PropTypes.array
]),
}
getFieldFunctionType(fieldSpec) {
if (fieldSpec.function === "interpolated") {
return "exponential"
}
if (fieldSpec.type === "number") {
return "interval"
}
return "categorical"
}
getDataFunctionTypes(functionType) {
if (functionType === "interpolated") {
return ["categorical", "interval", "exponential"]
}
else {
return ["categorical", "interval"]
}
}
changeStop(changeIdx, stopData, value) {
const stops = this.props.value.stops.slice(0)
stops[changeIdx] = [stopData, value]
const changedValue = {
...this.props.value,
stops: stops,
}
this.props.onChange(this.props.fieldName, changedValue)
}
changeDataProperty(propName, propVal) {
if (propVal) {
this.props.value[propName] = propVal
}
else {
delete this.props.value[propName]
}
this.props.onChange(this.props.fieldName, this.props.value)
}
render() {
if (typeof this.props.value.type === "undefined") {
this.props.value.type = this.getFieldFunctionType(this.props.fieldSpec)
}
const dataFields = this.props.value.stops.map((stop, idx) => {
const zoomLevel = stop[0].zoom
const dataLevel = stop[0].value
const value = stop[1]
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
const dataProps = {
label: "Data value",
value: dataLevel,
onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value)
}
let dataInput;
if(this.props.value.type === "categorical") {
dataInput = <StringInput {...dataProps} />
}
else {
dataInput = <NumberInput {...dataProps} />
}
return <InputBlock key={idx} action={deleteStopBtn} label="">
<div className="maputnik-data-spec-property-stop-edit">
<NumberInput
value={zoomLevel}
onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
min={0}
max={22}
/>
</div>
<div className="maputnik-data-spec-property-stop-data">
{dataInput}
</div>
<div className="maputnik-data-spec-property-stop-value">
<SpecField
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={value}
onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)}
/>
</div>
</InputBlock>
})
return <div className="maputnik-data-spec-block">
<div className="maputnik-data-spec-property">
<InputBlock
doc={this.props.fieldSpec.doc}
label={labelFromFieldName(this.props.fieldName)}
>
<div className="maputnik-data-spec-property-group">
<DocLabel
label="Property"
doc={"Input a data property to base styles off of."}
/>
<div className="maputnik-data-spec-property-input">
<StringInput
value={this.props.value.property}
onChange={propVal => this.changeDataProperty("property", propVal)}
/>
</div>
</div>
<div className="maputnik-data-spec-property-group">
<DocLabel
label="Type"
doc={"Select a type of data scale (default is 'categorical')."}
/>
<div className="maputnik-data-spec-property-input">
<SelectInput
value={this.props.value.type}
onChange={propVal => this.changeDataProperty("type", propVal)}
options={this.getDataFunctionTypes(this.props.fieldSpec.function)}
/>
</div>
</div>
<div className="maputnik-data-spec-property-group">
<DocLabel
label="Default"
doc={"Input a default value for data if not covered by the scales."}
/>
<div className="maputnik-data-spec-property-input">
<SpecField
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value.default}
onChange={(_, propVal) => this.changeDataProperty("default", propVal)}
/>
</div>
</div>
</InputBlock>
</div>
{dataFields}
<Button
className="maputnik-add-stop"
onClick={this.props.onAddStop.bind(this)}
>
Add stop
</Button>
</div>
}
}

View file

@ -0,0 +1,25 @@
import React from 'react'
import PropTypes from 'prop-types'
import DocLabel from './DocLabel'
import Button from '../Button'
import DeleteIcon from 'react-icons/lib/md/delete'
export default class DeleteStopButton extends React.Component {
static propTypes = {
onClick: PropTypes.func,
}
render() {
return <Button
className="maputnik-delete-stop"
onClick={this.props.onClick}
>
<DocLabel
label={<DeleteIcon />}
doc={"Remove zoom level stop."}
/>
</Button>
}
}

View file

@ -0,0 +1,49 @@
import React from 'react'
import PropTypes from 'prop-types'
import DocLabel from './DocLabel'
import Button from '../Button'
import FunctionIcon from 'react-icons/lib/md/functions'
import MdInsertChart from 'react-icons/lib/md/insert-chart'
export default class FunctionButtons extends React.Component {
static propTypes = {
fieldSpec: PropTypes.object,
onZoomClick: PropTypes.func,
onDataClick: PropTypes.func,
}
render() {
let makeZoomButton, makeDataButton
if (this.props.fieldSpec['zoom-function']) {
makeZoomButton = <Button
className="maputnik-make-zoom-function"
onClick={this.props.onZoomClick}
>
<DocLabel
label={<FunctionIcon />}
cursorTargetStyle={{ cursor: 'pointer' }}
doc={"Turn property into a zoom function to enable a map feature to change with map's zoom level."}
/>
</Button>
if (this.props.fieldSpec['property-function'] && ['piecewise-constant', 'interpolated'].indexOf(this.props.fieldSpec['function']) !== -1) {
makeDataButton = <Button
className="maputnik-make-data-function"
onClick={this.props.onDataClick}
>
<DocLabel
label={<MdInsertChart />}
cursorTargetStyle={{ cursor: 'pointer' }}
doc={"Turn property into a data function to enable a map feature to change according to data properties and the map's zoom level."}
/>
</Button>
}
return <div>{makeDataButton}{makeZoomButton}</div>
}
else {
return null
}
}
}

View file

@ -0,0 +1,34 @@
import React from 'react'
import PropTypes from 'prop-types'
import SpecField from './SpecField'
import FunctionButtons from './_FunctionButtons'
import InputBlock from '../inputs/InputBlock'
import labelFromFieldName from './_labelFromFieldName'
export default class SpecProperty extends React.Component {
static propTypes = {
onZoomClick: PropTypes.func.isRequired,
onDataClick: PropTypes.func.isRequired,
fieldName: PropTypes.string,
fieldSpec: PropTypes.object
}
render() {
const functionBtn = <FunctionButtons
fieldSpec={this.props.fieldSpec}
onZoomClick={this.props.onZoomClick}
onDataClick={this.props.onDataClick}
/>
return <InputBlock
doc={this.props.fieldSpec.doc}
label={labelFromFieldName(this.props.fieldName)}
action={functionBtn}
>
<SpecField {...this.props} />
</InputBlock>
}
}

View file

@ -0,0 +1,161 @@
import React from 'react'
import PropTypes from 'prop-types'
import Button from '../Button'
import SpecField from './SpecField'
import NumberInput from '../inputs/NumberInput'
import InputBlock from '../inputs/InputBlock'
import DeleteStopButton from './_DeleteStopButton'
import labelFromFieldName from './_labelFromFieldName'
import docUid from '../../libs/document-uid'
import sortNumerically from '../../libs/sort-numerically'
export default class ZoomProperty extends React.Component {
static propTypes = {
onChange: PropTypes.func,
onDeleteStop: PropTypes.func,
onAddStop: PropTypes.func,
fieldName: PropTypes.string,
fieldSpec: PropTypes.object,
value: PropTypes.oneOfType([
PropTypes.object,
PropTypes.string,
PropTypes.number,
PropTypes.bool,
PropTypes.array
]),
}
constructor() {
super()
this.state = {
refs: {}
}
}
componentWillMount() {
this.setState({
refs: this.setStopRefs(this.props)
})
}
/**
* We cache a reference for each stop by its index.
*
* When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus.
*/
setStopRefs(props) {
// This is initialsed below only if required to improved performance.
let newRefs;
if(props.value && props.value.stops) {
props.value.stops.forEach((val, idx) => {
if(!this.state.refs.hasOwnProperty(idx)) {
if(!newRefs) {
newRefs = {...this.state.refs};
}
newRefs[idx] = docUid("stop-");
}
})
}
return newRefs;
}
componentWillReceiveProps(nextProps) {
const newRefs = this.setStopRefs(nextProps);
if(newRefs) {
this.setState({
refs: newRefs
})
}
}
// Order the stops altering the refs to reflect their new position.
orderStopsByZoom(stops) {
const mappedWithRef = stops
.map((stop, idx) => {
return {
ref: this.state.refs[idx],
data: stop
}
})
// Sort by zoom
.sort((a, b) => sortNumerically(a.data[0], b.data[0]));
// Fetch the new position of the stops
const newRefs = {};
mappedWithRef
.forEach((stop, idx) =>{
newRefs[idx] = stop.ref;
})
this.setState({
refs: newRefs
});
return mappedWithRef.map((item) => item.data);
}
changeZoomStop(changeIdx, stopData, value) {
const stops = this.props.value.stops.slice(0);
stops[changeIdx] = [stopData, value];
const orderedStops = this.orderStopsByZoom(stops);
const changedValue = {
...this.props.value,
stops: orderedStops
}
this.props.onChange(this.props.fieldName, changedValue)
}
render() {
const zoomFields = this.props.value.stops.map((stop, idx) => {
const zoomLevel = stop[0]
const key = this.state.refs[idx];
const value = stop[1]
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
return <InputBlock
key={key}
doc={this.props.fieldSpec.doc}
label={labelFromFieldName(this.props.fieldName)}
action={deleteStopBtn}
>
<div>
<div className="maputnik-zoom-spec-property-stop-edit">
<NumberInput
value={zoomLevel}
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
min={0}
max={22}
/>
</div>
<div className="maputnik-zoom-spec-property-stop-value">
<SpecField
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={value}
onChange={(_, newValue) => this.changeZoomStop(idx, zoomLevel, newValue)}
/>
</div>
</div>
</InputBlock>
});
return <div className="maputnik-zoom-spec-property">
{zoomFields}
<Button
className="maputnik-add-stop"
onClick={this.props.onAddStop.bind(this)}
>
Add stop
</Button>
</div>
}
}

View file

@ -0,0 +1,6 @@
import capitalize from 'lodash.capitalize'
export default function labelFromFieldName(fieldName) {
let label = fieldName.split('-').slice(1).join(' ')
return capitalize(label)
}

View file

@ -1,7 +1,8 @@
import React from 'react' import 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}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,106 @@
import React from 'react'
import PropTypes from 'prop-types'
import StringInput from './StringInput'
import NumberInput from './NumberInput'
import Button from '../Button'
import DeleteIcon from 'react-icons/lib/md/delete'
import DocLabel from '../fields/DocLabel'
class DynamicArrayInput extends React.Component {
static propTypes = {
value: PropTypes.array,
type: PropTypes.string,
default: PropTypes.array,
onChange: PropTypes.func,
style: PropTypes.object,
}
changeValue(idx, newValue) {
console.log(idx, newValue)
const values = this.values.slice(0)
values[idx] = newValue
this.props.onChange(values)
}
get values() {
return this.props.value || this.props.default || []
}
addValue() {
const values = this.values.slice(0)
if (this.props.type === 'number') {
values.push(0)
} else {
values.push("")
}
this.props.onChange(values)
}
deleteValue(valueIdx) {
const values = this.values.slice(0)
values.splice(valueIdx, 1)
this.props.onChange(values)
}
render() {
const inputs = this.values.map((v, i) => {
const deleteValueBtn= <DeleteValueButton onClick={this.deleteValue.bind(this, i)} />
const input = this.props.type === 'number'
? <NumberInput
value={v}
onChange={this.changeValue.bind(this, i)}
/>
: <StringInput
value={v}
onChange={this.changeValue.bind(this, i)}
/>
return <div
style={this.props.style}
key={i}
className="maputnik-array-block"
>
<div className="maputnik-array-block-action">
{deleteValueBtn}
</div>
<div className="maputnik-array-block-content">
{input}
</div>
</div>
})
return <div className="maputnik-array">
{inputs}
<Button
className="maputnik-array-add-value"
onClick={this.addValue.bind(this)}
>
Add value
</Button>
</div>
}
}
class DeleteValueButton extends React.Component {
static propTypes = {
onClick: PropTypes.func,
}
render() {
return <Button
className="maputnik-delete-stop"
onClick={this.props.onClick}
>
<DocLabel
label={<DeleteIcon />}
doc={"Remove array entry."}
/>
</Button>
}
}
export default DynamicArrayInput

View file

@ -1,12 +1,14 @@
import React from 'react' import 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 = {

View file

@ -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 = {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"
]
}
]
} }
} }

View file

@ -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"
} }
] ]

View file

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

View file

@ -1,7 +1,7 @@
import diffStyles from 'mapbox-gl-style-spec/lib/diff' import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
export function diffMessages(beforeStyle, afterStyle) { 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
View file

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

View file

@ -1,6 +1,6 @@
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js' import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
export const combiningFilterOps = ['all', 'any', 'none'] export const 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)

View file

@ -1,16 +1,16 @@
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js' import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
export function changeType(layer, newType) { 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]
} }
}) })

View file

@ -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, {

View file

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

View file

@ -1,6 +1,5 @@
import React from 'react'; import 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
View file

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

9
src/manifest.json Normal file
View file

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

View file

@ -25,6 +25,13 @@
color: white; 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);
} }

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -1,12 +1,15 @@
::-webkit-scrollbar { // HACK: ::webkit-scrollbar selector covers to much of the UI. Bigger changes to come so for now just use :not() to ignore the toolbar
div:not(.maputnik-toolbar__actions) {
&::-webkit-scrollbar {
background-color: #26282e; 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;
} }
}

View file

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

View file

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

View file

@ -35,3 +35,4 @@ $toolbar-offset: 0;
@import 'filtereditor'; @import 'filtereditor';
@import 'zoomproperty'; @import 'zoomproperty';
@import 'popup'; @import 'popup';
@import 'map';

View file

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

View file

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