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
matrix:
include:
- os: linux
node_js: "4"
- os: linux
env: CXX=g++-4.8
node_js: "5"
- os: linux
node_js: "6"
- os: linux
env: CXX=g++-4.8
node_js: "7"
- os: osx
node_js: "4"
- os: osx
node_js: "5"
- os: linux
node_js: "8"
- os: linux
env: CXX=g++-4.8
node_js: "9"
- os: osx
node_js: "6"
- os: osx
node_js: "7"
- os: osx
node_js: "8"
- os: osx
node_js: "9"
before_install:
- export CHROME_BIN=chromium-browser
- export DISPLAY=:99.0

View file

@ -1,11 +1,23 @@
# Maputnik [![Build Status](https://travis-ci.org/maputnik/editor.svg?branch=master)](https://travis-ci.org/maputnik/editor) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/anelbgv6jdb3qnh9/branch/master?svg=true)](https://ci.appveyor.com/project/lukasmartinelli/editor) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://tldrlegal.com/license/mit-license)
# Maputnik
[![Build Status](https://travis-ci.org/maputnik/editor.svg?branch=master)][travis]
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/anelbgv6jdb3qnh9/branch/master?svg=true)][appveyor]
[![Dependency Status](https://david-dm.org/maputnik/editor.svg)][dm-prod]
[![Dev Dependency Status](https://david-dm.org/maputnik/editor/dev-status.svg)][dm-dev]
[![License](https://img.shields.io/badge/license-MIT-blue.svg)][license]
[travis]: https://travis-ci.org/maputnik/editor
[appveyor]: https://ci.appveyor.com/project/lukasmartinelli/editor
[dm-prod]: https://david-dm.org/maputnik/editor
[dm-dev]: https://david-dm.org/maputnik/editor#info=devDependencies
[license]: https://tldrlegal.com/license/mit-license
<img width="200" align="right" alt="Maputnik" src="src/img/maputnik.png" />
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
targeted at developers and map designers.
- :link: Design your maps online at **http://maputnik.com/editor/** (all in local storage)
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independance is an OSS map designer.
@ -37,7 +49,12 @@ npm install
npm start
```
Build a production package for distribution.
The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the webpack-dev-server docs
> webpack uses the file system to get notified of file changes. In some cases this does not work. For example, when using Network File System (NFS). Vagrant also has a lot of problems with this.
Snippet from <https://webpack.js.org/configuration/dev-server/#devserver-watchoptions->
To enable polling add `export WEBPACK_DEV_SERVER_POLLING=1` to your enviroment.
```
npm run build
@ -51,6 +68,10 @@ npm run lint
npm run lint-styles
```
## Related Projects
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
## Sponsors
Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter.com/projects/174808720/maputnik-visual-map-editor-for-mapbox-gl)**. This project would not be possible without these commercial and individual sponsors.

View file

@ -1,8 +1,9 @@
environment:
matrix:
- nodejs_version: "4"
- nodejs_version: "6"
- nodejs_version: "7"
- nodejs_version: "8"
- nodejs_version: "9"
platform:
- x86
- 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 WebpackDevServer = require("webpack-dev-server");
var webpackConfig = require("./webpack.config");
var webpackConfig = require("./webpack.production.config");
var testConfig = require("../test/config/specs");
@ -18,7 +18,7 @@ exports.config = {
browserName: 'firefox'
}],
sync: true,
logLevel: 'silent',
logLevel: 'verbose',
coloredLogs: true,
bail: 0,
screenshotPath: './errorShots/',
@ -29,16 +29,17 @@ exports.config = {
services: ['phantomjs'],
framework: 'mocha',
reporters: ['spec'],
phantomjsOpts: {
webdriverLogfile: 'phantomjs.log'
},
mochaOpts: {
ui: 'bdd',
// Because we don't know how long the initial build will take...
timeout: 2*60*1000
timeout: 4*60*1000
},
onPrepare: function (config, capabilities) {
var compiler = webpack(webpackConfig);
server = new WebpackDevServer(compiler, {
stats: "minimal"
});
server = new WebpackDevServer(compiler, {});
server.listen(testConfig.port);
},
onComplete: function(exitCode) {

View file

@ -3,6 +3,7 @@ var webpack = require('webpack');
var path = require('path');
var loaders = require('./webpack.loaders');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
const HOST = process.env.HOST || "127.0.0.1";
const PORT = process.env.PORT || "8888";
@ -20,10 +21,13 @@ module.exports = {
filename: 'bundle.js'
},
resolve: {
extensions: ['', '.js', '.jsx']
extensions: ['.js', '.jsx']
},
module: {
loaders
noParse: [
/mapbox-gl\/dist\/mapbox-gl.js/
],
loaders: loaders
},
node: {
fs: "empty",
@ -41,14 +45,26 @@ module.exports = {
// serve index.html in place of 404 responses to allow HTML5 history
historyApiFallback: true,
port: PORT,
host: HOST
host: HOST,
watchOptions: {
// Disabled polling by default as it causes lots of CPU usage and hence drains laptop batteries. To enable polling add WEBPACK_DEV_SERVER_POLLING to your environment
// See <https://webpack.js.org/configuration/watch/#watchoptions-poll> for details
poll: (!!process.env.WEBPACK_DEV_SERVER_POLLING ? true : false),
watch: false
}
},
plugins: [
new webpack.NoErrorsPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
title: 'Maputnik',
template: './src/template.html'
})
}),
new CopyWebpackPlugin([
{
from: './src/manifest.json',
to: 'manifest.json'
}
])
]
};

View file

@ -4,26 +4,38 @@ module.exports = [
exclude: /(node_modules|bower_components|public)/,
loaders: ['react-hot-loader/webpack']
},
// HACK: This is a massive hack and reaches into the mapbox-gl private API.
// We have to include this for access to `normalizeSourceURL`. We should
// remove this ASAP, see <https://github.com/mapbox/mapbox-gl-js/issues/2416>
{
test: /.*node_modules[\/\\]mapbox-gl[\/\\]src[\/\\]util[\/\\].*\.js/,
loader: 'babel-loader',
query: {
presets: ['env', 'react', 'flow'],
plugins: ['transform-runtime', 'transform-decorators-legacy', 'transform-class-properties'],
}
},
{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components|public)/,
loader: 'babel',
// Note: These modules aren't ES5 therefore we much compile them.
exclude: /(.*node_modules(?![\/\\](@mapbox[\/\\]mapbox-gl-style-spec|ol|mapbox-to-ol-style))|bower_components|public)/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react'],
presets: ['env', 'react'],
plugins: ['transform-runtime', 'transform-decorators-legacy', 'transform-class-properties'],
}
},
{
test: /\.(eot|ttf|woff|woff2)$/,
loader: 'file?name=fonts/[name].[ext]'
loader: 'file-loader?name=fonts/[name].[ext]'
},
{
test: /\.ico$/,
loader: 'file?name=[name].[ext]'
loader: 'file-loader?name=[name].[ext]'
},
{
test: /\.(svg|gif|jpg|png)$/,
loader: 'file?name=img/[name].[ext]'
loader: 'file-loader?name=img/[name].[ext]'
},
{
test: /\.json$/,
@ -36,8 +48,8 @@ module.exports = [
{
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
loaders: [
'style?sourceMap',
'css'
'style-loader?sourceMap',
'css-loader'
]
}
];

View file

@ -5,6 +5,17 @@ var loaders = require('./webpack.loaders');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
var CopyWebpackPlugin = require('copy-webpack-plugin');
var 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 = {
entry: {
@ -12,8 +23,6 @@ module.exports = {
vendor: [
'file-saver',
'mapbox-gl/dist/mapbox-gl.js',
//TODO: Build failure because cannot resolve migrations file
//"mapbox-gl-style-spec",
"lodash.clonedeep",
"lodash.throttle",
'color',
@ -32,14 +41,17 @@ module.exports = {
]
},
output: {
path: path.join(__dirname, '..', 'public'),
path: OUTPATH,
filename: '[name].[chunkhash].js',
chunkFilename: '[chunkhash].js'
},
resolve: {
extensions: ['', '.js', '.jsx']
extensions: ['.js', '.jsx']
},
module: {
noParse: [
/mapbox-gl\/dist\/mapbox-gl.js/
],
loaders
},
node: {
@ -48,21 +60,15 @@ module.exports = {
tls: 'empty'
},
plugins: [
new webpack.NoErrorsPlugin(),
new webpack.optimize.CommonsChunkPlugin('vendor', '[chunkhash].vendor.js'),
new webpack.NoEmitOnErrorsPlugin(),
new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', filename: '[chunkhash].vendor.js' }),
new WebpackCleanupPlugin(),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
screw_ie8: true,
}
}),
new webpack.optimize.OccurenceOrderPlugin(),
new UglifyJsPlugin(),
new ExtractTextPlugin('[contenthash].css', {
allChunks: true
}),
@ -70,6 +76,19 @@ module.exports = {
template: './src/template.html',
title: 'Maputnik'
}),
new webpack.optimize.DedupePlugin()
new CopyWebpackPlugin([
{
from: './src/manifest.json',
to: 'manifest.json'
}
]),
new BundleAnalyzerPlugin({
analyzerMode: 'static',
defaultSizes: 'gzip',
openAnalyzer: false,
generateStatsFile: true,
reportFilename: 'bundle-stats.html',
statsFilename: 'bundle-stats.json',
})
]
};

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",
"version": "1.0.1",
"version": "1.1.0",
"description": "A MapboxGL visual style editor",
"main": "''",
"scripts": {
@ -8,9 +8,10 @@
"build": "webpack --config config/webpack.production.config.js --progress --profile --colors",
"test": "wdio config/wdio.conf.js",
"test-watch": "wdio config/wdio.conf.js --watch",
"start": "webpack-dev-server --progress --profile --colors --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-styles": "stylelint 'src/styles/*.scss'"
"lint-styles": "stylelint 'src/styles/*.scss'",
"nsp": "nsp check --reporter summary"
},
"repository": {
"type": "git",
@ -20,49 +21,43 @@
"license": "MIT",
"homepage": "https://github.com/maputnik/editor#readme",
"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",
"codemirror": "^5.18.2",
"color": "^1.0.3",
"file-saver": "^1.3.2",
"codemirror": "^5.32.0",
"color": "^2.0.0",
"file-saver": "^1.3.3",
"github-api": "^3.0.0",
"jsonlint": "josdejong/jsonlint#85a19d7",
"jsonlint": "github:josdejong/jsonlint#85a19d7",
"lodash.capitalize": "^4.2.1",
"lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.4.0",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"mapbox-gl": "^0.33.0",
"mapbox-gl-inspect": "^1.2.2",
"mapbox-gl-style-spec": "^8.11.0",
"mousetrap": "^1.6.0",
"ol-mapbox-style": "1.0.1",
"openlayers": "^3.19.1",
"react": "^15.4.0",
"react-addons-pure-render-mixin": "^15.4.0",
"react-autocomplete": "^1.4.0",
"react-codemirror": "^0.3.0",
"react-collapse": "^4.0.2",
"react-color": "^2.10.0",
"react-dom": "^15.4.0",
"react-file-reader-input": "^1.1.0",
"mapbox-gl": "^0.44.1",
"mapbox-gl-inspect": "^1.3.0",
"maputnik-design": "github:maputnik/design",
"mousetrap": "^1.6.1",
"ol-mapbox-style": "^2.10.1",
"ol": "^4.6.4",
"prop-types": "^15.6.0",
"react": "16.0.0",
"react-addons-pure-render-mixin": "^15.6.2",
"react-autocomplete": "^1.7.2",
"react-codemirror2": "^3.0.7",
"react-collapse": "^4.0.3",
"react-color": "^2.13.8",
"react-copy-to-clipboard": "^5.0.1",
"react-dom": "16.0.0",
"react-file-reader-input": "^1.1.4",
"react-height": "^3.0.0",
"react-icon-base": "^2.0.4",
"react-icons": "^2.2.1",
"react-motion": "^0.4.7",
"react-sortable-hoc": "^0.4.5",
"reconnecting-websocket": "^3.0.3",
"request": "^2.79.0",
"react-icon-base": "^2.1.1",
"react-icons": "^2.2.7",
"react-motion": "^0.5.2",
"react-sortable-hoc": "^0.6.8",
"reconnecting-websocket": "^3.2.2",
"request": "^2.83.0",
"url": "^0.11.0"
},
"babel": {
"presets": [
"es2015",
"react"
],
"plugins": [
"transform-object-rest-spread"
]
},
"jshintConfig": {
"esversion": 6
},
@ -73,7 +68,7 @@
"plugins": [
"react"
],
"extend": [
"extends": [
"plugin:react/recommended"
],
"env": {
@ -93,45 +88,51 @@
}
},
"devDependencies": {
"babel-core": "6.21.0",
"babel-eslint": "^7.1.1",
"babel-loader": "6.2.10",
"babel-plugin-transform-class-properties": "^6.11.5",
"babel-core": "^6.26.0",
"babel-eslint": "^8.0.2",
"babel-loader": "7.1.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-flow-strip-types": "^6.21.0",
"babel-plugin-transform-object-rest-spread": "^6.8.0",
"babel-plugin-transform-runtime": "^6.15.0",
"babel-preset-es2015": "6.18.0",
"babel-preset-react": "6.16.0",
"babel-runtime": "^6.11.6",
"babel-plugin-transform-flow-strip-types": "^6.22.0",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.1",
"babel-preset-es2015": "^6.24.1",
"babel-preset-flow": "^6.23.0",
"babel-preset-react": "^6.24.1",
"babel-runtime": "^6.26.0",
"base64-loader": "^1.0.0",
"css-loader": "0.26.1",
"eslint": "^3.5.0",
"eslint-plugin-react": "^6.2.0",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.11.1",
"html-webpack-plugin": "^2.22.0",
"json-loader": "^0.5.4",
"karma": "^1.3.0",
"karma-chrome-launcher": "^2.0.0",
"karma-firefox-launcher": "^1.0.0",
"copy-webpack-plugin": "^4.2.0",
"css-loader": "^0.28.7",
"eslint": "^4.10.0",
"eslint-plugin-react": "^7.4.0",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.5",
"html-webpack-plugin": "^2.30.1",
"json-loader": "^0.5.7",
"karma": "^1.7.1",
"karma-chrome-launcher": "^2.2.0",
"karma-firefox-launcher": "^1.0.1",
"karma-mocha": "^1.3.0",
"karma-webpack": "^2.0.1",
"mocha": "^3.1.2",
"mocha-loader": "^1.0.0",
"node-sass": "^4.2.0",
"react-hot-loader": "^3.0.0-beta.6",
"sass-loader": "^4.0.1",
"style-loader": "0.13.1",
"stylelint": "^7.7.1",
"karma-webpack": "^2.0.5",
"mocha": "^4.0.1",
"mocha-loader": "^1.1.1",
"node-sass": "^4.6.0",
"nsp": "^3.1.0",
"react-hot-loader": "^3.1.1",
"sass-loader": "^6.0.6",
"style-loader": "^0.19.0",
"stylelint": "^7.13.0",
"stylelint-config-standard": "^15.0.1",
"transform-loader": "^0.2.3",
"wdio-mocha-framework": "^0.5.9",
"transform-loader": "^0.2.4",
"uglifyjs-webpack-plugin": "^1.1.8",
"wdio-mocha-framework": "^0.5.11",
"wdio-phantomjs-service": "^0.2.2",
"wdio-spec-reporter": "^0.1.0",
"webdriverio": "^4.6.2",
"webpack": "1.14.0",
"webpack-cleanup-plugin": "^0.4.1",
"webpack-dev-server": "1.16.2"
"wdio-spec-reporter": "^0.1.2",
"webdriverio": "^4.8.0",
"webpack": "^3.8.1",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-cleanup-plugin": "^0.5.1",
"webpack-dev-server": "^2.9.4"
}
}

View file

@ -10,9 +10,7 @@ import AppLayout from './AppLayout'
import MessagePanel from './MessagePanel'
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import validateStyleMin from 'mapbox-gl-style-spec/lib/validate_style.min'
import formatStyle from 'mapbox-gl-style-spec/lib/format'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import style from '../libs/style.js'
import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen'
import { undoMessages, redoMessages } from '../libs/diffmessage'
@ -21,6 +19,11 @@ import { ApiStyleStore } from '../libs/apistore'
import { RevisionStore } from '../libs/revisions'
import LayerWatcher from '../libs/layerwatcher'
import tokens from '../config/tokens.json'
import isEqual from 'lodash.isequal'
import MapboxGl from 'mapbox-gl'
import mapboxUtil from 'mapbox-gl/src/util/mapbox'
function updateRootSpec(spec, fieldName, newValues) {
return {
@ -65,11 +68,10 @@ export default class App extends React.Component {
sources: {},
vectorLayers: {},
inspectModeEnabled: false,
spec: GlSpec,
spec: styleSpec.latest,
}
this.layerWatcher = new LayerWatcher({
onSourcesChange: v => this.setState({ sources: v }),
onVectorLayersChange: v => this.setState({ vectorLayers: v })
})
}
@ -96,7 +98,9 @@ export default class App extends React.Component {
updateFonts(urlTemplate) {
const metadata = this.state.mapStyle.metadata || {}
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)})
})
}
@ -108,15 +112,17 @@ export default class App extends React.Component {
}
onStyleChanged(newStyle, save=true) {
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
this.updateFonts(newStyle.glyphs)
}
if(newStyle.sprite !== this.state.mapStyle.sprite) {
this.updateIcons(newStyle.sprite)
}
const errors = validateStyleMin(newStyle, GlSpec)
const errors = styleSpec.validate(newStyle, styleSpec.latest)
if(errors.length === 0) {
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
this.updateFonts(newStyle.glyphs)
}
if(newStyle.sprite !== this.state.mapStyle.sprite) {
this.updateIcons(newStyle.sprite)
}
this.revisionStore.addRevision(newStyle)
if(save) this.saveStyle(newStyle)
this.setState({
@ -128,6 +134,8 @@ export default class App extends React.Component {
errors: errors.map(err => err.message)
})
}
this.fetchSources();
}
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() {
const mapProps = {
mapStyle: style.replaceAccessToken(this.state.mapStyle),
mapStyle: style.replaceAccessToken(this.state.mapStyle, {allowFallback: true}),
onDataChange: (e) => {
this.layerWatcher.analyzeMap(e.map)
this.fetchSources();
},
}
@ -201,7 +266,8 @@ export default class App extends React.Component {
} else {
return <MapboxGlMap {...mapProps}
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 PropTypes from 'prop-types'
import ScrollContainer from './ScrollContainer'
class AppLayout extends React.Component {
static propTypes = {
toolbar: React.PropTypes.element.isRequired,
layerList: React.PropTypes.element.isRequired,
layerEditor: React.PropTypes.element,
map: React.PropTypes.element.isRequired,
bottom: React.PropTypes.element,
toolbar: PropTypes.element.isRequired,
layerList: PropTypes.element.isRequired,
layerEditor: PropTypes.element,
map: PropTypes.element.isRequired,
bottom: PropTypes.element,
}
static childContextTypes = {
reactIconBase: React.PropTypes.object
reactIconBase: PropTypes.object
}
getChildContext() {

View file

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

View file

@ -1,18 +1,19 @@
import React from 'react'
import PropTypes from 'prop-types'
class MessagePanel extends React.Component {
static propTypes = {
errors: React.PropTypes.array,
infos: React.PropTypes.array,
errors: PropTypes.array,
infos: PropTypes.array,
}
render() {
const errors = this.props.errors.map((m, i) => {
return <p className="maputnik-message-panel-error">{m}</p>
return <p key={"error-"+i} className="maputnik-message-panel-error">{m}</p>
})
const infos = this.props.infos.map((m, i) => {
return <p key={i}>{m}</p>
return <p key={"info-"+i}>{m}</p>
})
return <div className="maputnik-message-panel">

View file

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

View file

@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import FileReaderInput from 'react-file-reader-input'
import classnames from 'classnames'
@ -16,47 +17,71 @@ import MdFontDownload from 'react-icons/lib/md/font-download'
import HelpIcon from 'react-icons/lib/md/help-outline'
import InspectionIcon from 'react-icons/lib/md/find-in-page'
import logoImage from '../img/maputnik.png'
import logoImage from 'maputnik-design/logos/logo-color.svg'
import SettingsModal from './modals/SettingsModal'
import ExportModal from './modals/ExportModal'
import SourcesModal from './modals/SourcesModal'
import OpenModal from './modals/OpenModal'
import pkgJson from '../../package.json'
import style from '../libs/style'
function IconText(props) {
return <span className="maputnik-icon-text">{props.children}</span>
class IconText extends React.Component {
static propTypes = {
children: PropTypes.node,
}
render() {
return <span className="maputnik-icon-text">{this.props.children}</span>
}
}
function ToolbarLink(props) {
return <a
className={classnames('maputnik-toolbar-link', props.className)}
href={props.href}
target={"blank"}
>
{props.children}
</a>
class ToolbarLink extends React.Component {
static propTypes = {
className: PropTypes.string,
children: PropTypes.node,
href: PropTypes.string,
}
render() {
return <a
className={classnames('maputnik-toolbar-link', this.props.className)}
href={this.props.href}
rel="noopener noreferrer"
target="_blank"
>
{this.props.children}
</a>
}
}
function ToolbarAction(props) {
return <a
className='maputnik-toolbar-action'
onClick={props.onClick}
>
{props.children}
</a>
class ToolbarAction extends React.Component {
static propTypes = {
children: PropTypes.node,
onClick: PropTypes.func
}
render() {
return <a
className='maputnik-toolbar-action'
onClick={this.props.onClick}
>
{this.props.children}
</a>
}
}
export default class Toolbar extends React.Component {
static propTypes = {
mapStyle: React.PropTypes.object.isRequired,
inspectModeEnabled: React.PropTypes.bool.isRequired,
onStyleChanged: React.PropTypes.func.isRequired,
mapStyle: PropTypes.object.isRequired,
inspectModeEnabled: PropTypes.bool.isRequired,
onStyleChanged: PropTypes.func.isRequired,
// A new style has been uploaded
onStyleOpen: React.PropTypes.func.isRequired,
onStyleOpen: PropTypes.func.isRequired,
// A dict of source id's and the available source layers
sources: React.PropTypes.object.isRequired,
onInspectModeToggle: React.PropTypes.func.isRequired
sources: PropTypes.object.isRequired,
onInspectModeToggle: PropTypes.func.isRequired,
children: PropTypes.node
}
constructor(props) {
@ -106,40 +131,46 @@ export default class Toolbar extends React.Component {
isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')}
/>
<ToolbarLink
href={"https://github.com/maputnik/editor"}
className="maputnik-toolbar-logo"
>
<img src={logoImage} alt="Maputnik" />
<h1>Maputnik</h1>
</ToolbarLink>
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
<OpenIcon />
<IconText>Open</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.toggleModal.bind(this, 'export')}>
<MdFileDownload />
<IconText>Export</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.toggleModal.bind(this, 'sources')}>
<SourcesIcon />
<IconText>Sources</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.toggleModal.bind(this, 'settings')}>
<SettingsIcon />
<IconText>Style Settings</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.props.onInspectModeToggle}>
<InspectionIcon />
<IconText>
{ this.props.inspectModeEnabled && <span>Map Mode</span> }
{ !this.props.inspectModeEnabled && <span>Inspect Mode</span> }
</IconText>
</ToolbarAction>
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
<HelpIcon />
<IconText>Help</IconText>
</ToolbarLink>
<div className="maputnik-toolbar__inner">
<ToolbarLink
href={"https://github.com/maputnik/editor"}
className="maputnik-toolbar-logo"
>
<img src={logoImage} alt="Maputnik" />
<h1>Maputnik
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
</h1>
</ToolbarLink>
<div className="maputnik-toolbar__actions">
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
<OpenIcon />
<IconText>Open</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.toggleModal.bind(this, 'export')}>
<MdFileDownload />
<IconText>Export</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.toggleModal.bind(this, 'sources')}>
<SourcesIcon />
<IconText>Sources</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.toggleModal.bind(this, 'settings')}>
<SettingsIcon />
<IconText>Style Settings</IconText>
</ToolbarAction>
<ToolbarAction onClick={this.props.onInspectModeToggle}>
<InspectionIcon />
<IconText>
{ this.props.inspectModeEnabled && <span>Map Mode</span> }
{ !this.props.inspectModeEnabled && <span>Inspect Mode</span> }
</IconText>
</ToolbarAction>
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
<HelpIcon />
<IconText>Help</IconText>
</ToolbarLink>
</div>
</div>
</div>
}
}

View file

@ -1,6 +1,7 @@
import React from 'react'
import Color from 'color'
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
import PropTypes from 'prop-types'
function formatColor(color) {
const rgb = color.rgb
@ -10,12 +11,12 @@ function formatColor(color) {
/*** Number fields with support for min, max and units and documentation*/
class ColorField extends React.Component {
static propTypes = {
onChange: React.PropTypes.func.isRequired,
name: React.PropTypes.string.isRequired,
value: React.PropTypes.string,
doc: React.PropTypes.string,
style: React.PropTypes.object,
default: React.PropTypes.string,
onChange: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string,
doc: PropTypes.string,
style: PropTypes.object,
default: PropTypes.string,
}
constructor(props) {
@ -29,7 +30,7 @@ class ColorField extends React.Component {
//but I am too stupid to get it to work together with fixed position
//and scrollbars so I have to fallback to JavaScript
calcPickerOffset() {
const elem = this.refs.colorInput
const elem = this.colorInput
if(elem) {
const pos = elem.getBoundingClientRect()
return {
@ -37,7 +38,6 @@ class ColorField extends React.Component {
left: pos.left + 196,
}
} else {
console.warn('Color field has no element to adjust position')
return {
top: 160,
left: 555,
@ -50,7 +50,14 @@ class ColorField extends React.Component {
}
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() {
@ -91,7 +98,7 @@ class ColorField extends React.Component {
</div>
var swatchStyle = {
"background-color": this.props.value
backgroundColor: this.props.value
};
return <div className="maputnik-color-wrapper">
@ -99,7 +106,7 @@ class ColorField extends React.Component {
<div className="maputnik-color-swatch" style={swatchStyle}></div>
<input
className="maputnik-color"
ref="colorInput"
ref={(input) => this.colorInput = input}
onClick={this.togglePicker.bind(this)}
style={this.props.style}
name={this.props.name}

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import ZoomSpecField from './ZoomSpecField'
import FunctionSpecField from './FunctionSpecField'
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
/** Extract field spec by {@fieldName} from the {@layerType} in the
@ -35,10 +36,10 @@ function getGroupName(spec, layerType, fieldName) {
export default class PropertyGroup extends React.Component {
static propTypes = {
layer: React.PropTypes.object.isRequired,
groupFields: React.PropTypes.array.isRequired,
onChange: React.PropTypes.func.isRequired,
spec: React.PropTypes.object.isRequired,
layer: PropTypes.object.isRequired,
groupFields: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
spec: PropTypes.object.isRequired,
}
onPropertyChange(property, newValue) {
@ -54,7 +55,7 @@ export default class PropertyGroup extends React.Component {
const layout = this.props.layer.layout || {}
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
return <ZoomSpecField
return <FunctionSpecField
onChange={this.onPropertyChange.bind(this)}
key={fieldName}
fieldName={fieldName}

View file

@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import color from 'color'
import ColorField from './ColorField'
@ -8,6 +9,7 @@ import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
import MultiButtonInput from '../inputs/MultiButtonInput'
import ArrayInput from '../inputs/ArrayInput'
import DynamicArrayInput from '../inputs/DynamicArrayInput'
import FontInput from '../inputs/FontInput'
import IconInput from '../inputs/IconInput'
import capitalize from 'lodash.capitalize'
@ -35,16 +37,17 @@ function optionsLabelLength(options) {
* to display @{value}. */
export default class SpecField extends React.Component {
static propTypes = {
onChange: React.PropTypes.func.isRequired,
fieldName: React.PropTypes.string.isRequired,
fieldSpec: React.PropTypes.object.isRequired,
value: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.number,
React.PropTypes.array,
onChange: PropTypes.func.isRequired,
fieldName: PropTypes.string.isRequired,
fieldSpec: PropTypes.object.isRequired,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.array,
PropTypes.bool
]),
/** Override the style of the field */
style: React.PropTypes.object,
style: PropTypes.object,
}
render() {
@ -105,11 +108,18 @@ export default class SpecField extends React.Component {
fonts={this.props.fieldSpec.values}
/>
} else {
return <ArrayInput
{...commonProps}
type={this.props.fieldSpec.value}
length={this.props.fieldSpec.length}
/>
if (this.props.fieldSpec.length) {
return <ArrayInput
{...commonProps}
type={this.props.fieldSpec.value}
length={this.props.fieldSpec.length}
/>
} else {
return <DynamicArrayInput
{...commonProps}
type={this.props.fieldSpec.value}
/>
}
}
default: return null
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
import Button from '../Button'
import DeleteIcon from 'react-icons/lib/md/delete'
class FilterEditorBlock extends React.Component {
static propTypes = {
onDelete: React.PropTypes.func.isRequired,
children: React.PropTypes.element.isRequired,
onDelete: PropTypes.func.isRequired,
children: PropTypes.element.isRequired,
}
render() {

View file

@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import { otherFilterOps } from '../../libs/filterops.js'
import StringInput from '../inputs/StringInput'
@ -11,11 +12,34 @@ function tryParseInt(v) {
return parseFloat(v)
}
function tryParseBool(v) {
const isString = (typeof(v) === "string");
if(!isString) {
return v;
}
if(v.match(/^\s*true\s*$/)) {
return true;
}
else if(v.match(/^\s*false\s*$/)) {
return false;
}
else {
return v;
}
}
function parseFilter(v) {
v = tryParseInt(v);
v = tryParseBool(v);
return v;
}
class SingleFilterEditor extends React.Component {
static propTypes = {
filter: React.PropTypes.array.isRequired,
onChange: React.PropTypes.func.isRequired,
properties: React.PropTypes.object,
filter: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
properties: PropTypes.object,
}
static defaultProps = {
@ -23,7 +47,7 @@ class SingleFilterEditor extends React.Component {
}
onFilterPartChanged(filterOp, propertyName, filterArgs) {
let newFilter = [filterOp, propertyName, ...filterArgs.map(tryParseInt)]
let newFilter = [filterOp, propertyName, ...filterArgs.map(parseFilter)]
if(filterOp === 'has' || filterOp === '!has') {
newFilter = [filterOp, propertyName]
} else if(filterArgs.length === 0) {

View file

@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import LineIcon from './LineIcon.jsx'
import FillIcon from './FillIcon.jsx'
@ -8,8 +9,8 @@ import CircleIcon from './CircleIcon.jsx'
class LayerIcon extends React.Component {
static propTypes = {
type: React.PropTypes.string.isRequired,
style: React.PropTypes.object,
type: PropTypes.string.isRequired,
style: PropTypes.object,
}
render() {
@ -17,6 +18,8 @@ class LayerIcon extends React.Component {
switch(this.props.type) {
case 'fill-extrusion': return <BackgroundIcon {...iconProps} />
case 'raster': return <FillIcon {...iconProps} />
case 'hillshade': return <FillIcon {...iconProps} />
case 'heatmap': return <FillIcon {...iconProps} />
case 'fill': return <FillIcon {...iconProps} />
case 'background': return <BackgroundIcon {...iconProps} />
case 'line': return <LineIcon {...iconProps} />

View file

@ -6,7 +6,7 @@ export default class FillIcon extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<path id="path8" d="M 12.34,1.29 C 12.5114,1.1076 12.7497,1.0029 13,1 13.5523,1 14,1.4477 14,2 14.0047,2.2478 13.907,2.4866 13.73,2.66 9.785626,6.5516986 6.6148407,9.7551593 2.65,13.72 2.4793,13.8963 2.2453,13.9971 2,14 1.4477,14 1,13.5523 1,13 0.9953,12.7522 1.093,12.5134 1.27,12.34 4.9761967,8.7018093 9.0356422,4.5930579 12.34,1.29 Z" transform="translate(2,2)" />
<path d="M 12.34,1.29 C 12.5114,1.1076 12.7497,1.0029 13,1 13.5523,1 14,1.4477 14,2 14.0047,2.2478 13.907,2.4866 13.73,2.66 9.785626,6.5516986 6.6148407,9.7551593 2.65,13.72 2.4793,13.8963 2.2453,13.9971 2,14 1.4477,14 1,13.5523 1,13 0.9953,12.7522 1.093,12.5134 1.27,12.34 4.9761967,8.7018093 9.0356422,4.5930579 12.34,1.29 Z" transform="translate(2,2)" />
</IconBase>
)
}

View file

@ -6,8 +6,8 @@ export default class SymbolIcon extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<g id="svg_1" transform="matrix(1.2718518,0,0,1.2601269,16.559526,-7.4065264)">
<path id="svg_2" d="m -9.7959773,11.060163 c -0.3734787,-0.724437 -0.3580577,-1.2147051 -0.00547,-1.8767873 l 8.6034029,-0.019416 c 0.39670292,0.6865644 0.38365934,1.4750693 -0.011097,1.8864953 l -3.1359613,-0.0033 -0.013695,7.1305 c -0.4055357,0.397083 -1.3146432,0.397083 -1.7769191,-0.02274 l 0.030226,-7.104422 z" />
<g transform="matrix(1.2718518,0,0,1.2601269,16.559526,-7.4065264)">
<path d="m -9.7959773,11.060163 c -0.3734787,-0.724437 -0.3580577,-1.2147051 -0.00547,-1.8767873 l 8.6034029,-0.019416 c 0.39670292,0.6865644 0.38365934,1.4750693 -0.011097,1.8864953 l -3.1359613,-0.0033 -0.013695,7.1305 c -0.4055357,0.397083 -1.3146432,0.397083 -1.7769191,-0.02274 l 0.030226,-7.104422 z" />
</g>
</IconBase>
)

View file

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

View file

@ -1,13 +1,17 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import Autocomplete from 'react-autocomplete'
const MAX_HEIGHT = 140;
class AutocompleteInput extends React.Component {
static propTypes = {
value: React.PropTypes.string,
options: React.PropTypes.array,
onChange: React.PropTypes.func,
value: PropTypes.string,
options: PropTypes.array,
onChange: PropTypes.func,
keepMenuWithinWindowBounds: PropTypes.bool
}
static defaultProps = {
@ -15,35 +19,73 @@ class AutocompleteInput extends React.Component {
options: [],
}
render() {
const AutocompleteMenu = (items, value, style) => <div className={"maputnik-autocomplete-menu"} children={items} />
constructor(props) {
super(props);
this.state = {
maxHeight: MAX_HEIGHT
};
}
return <Autocomplete
wrapperProps={{
className: "maputnik-autocomplete",
style: null
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;
}}
renderMenu={AutocompleteMenu}
inputProps={{
className: "maputnik-string"
}}
value={this.props.value}
items={this.props.options}
getItemValue={(item) => item[0]}
onSelect={v => this.props.onChange(v)}
onChange={(e, v) => this.props.onChange(v)}
renderItem={(item, isHighlighted) => (
<div
key={item[0]}
className={classnames({
"maputnik-autocomplete-menu-item": true,
"maputnik-autocomplete-menu-item-selected": isHighlighted,
})}
>
{item[1]}
</div>
)}
/>
>
<Autocomplete
menuStyle={{
position: "absolute",
overflow: "auto",
maxHeight: this.state.maxHeight
}}
wrapperProps={{
className: "maputnik-autocomplete",
style: null
}}
inputProps={{
className: "maputnik-string"
}}
value={this.props.value}
items={this.props.options}
getItemValue={(item) => item[0]}
onSelect={v => this.props.onChange(v)}
onChange={(e, v) => this.props.onChange(v)}
shouldItemRender={(item, value) => {
return item[0].toLowerCase().indexOf(value.toLowerCase()) > -1
}}
renderItem={(item, isHighlighted) => (
<div
key={item[0]}
className={classnames({
"maputnik-autocomplete-menu-item": true,
"maputnik-autocomplete-menu-item-selected": isHighlighted,
})}
>
{item[1]}
</div>
)}
/>
</div>
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,18 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import DocLabel from '../fields/DocLabel'
/** Wrap a component with a label */
class InputBlock extends React.Component {
static propTypes = {
label: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.element,
label: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
]).isRequired,
doc: React.PropTypes.string,
action: React.PropTypes.element,
children: React.PropTypes.element.isRequired,
style: React.PropTypes.object,
doc: PropTypes.string,
action: PropTypes.element,
children: PropTypes.node.isRequired,
style: PropTypes.object,
onChange: PropTypes.func,
}
onChange(e) {

View file

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

View file

@ -1,12 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
class NumberInput extends React.Component {
static propTypes = {
value: React.PropTypes.number,
default: React.PropTypes.number,
min: React.PropTypes.number,
max: React.PropTypes.number,
onChange: React.PropTypes.func,
value: PropTypes.number,
default: PropTypes.number,
min: PropTypes.number,
max: PropTypes.number,
onChange: PropTypes.func,
}
constructor(props) {

View file

@ -1,11 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
class SelectInput extends React.Component {
static propTypes = {
value: React.PropTypes.string.isRequired,
options: React.PropTypes.array.isRequired,
style: React.PropTypes.object,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
options: PropTypes.array.isRequired,
style: PropTypes.object,
onChange: PropTypes.func.isRequired,
}

View file

@ -1,11 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
class StringInput extends React.Component {
static propTypes = {
value: React.PropTypes.string,
style: React.PropTypes.object,
default: React.PropTypes.string,
onChange: React.PropTypes.func,
value: PropTypes.string,
style: PropTypes.object,
default: PropTypes.string,
onChange: PropTypes.func,
multi: PropTypes.bool,
}
constructor(props) {

View file

@ -1,11 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
import CollapseOpenIcon from 'react-icons/lib/md/arrow-drop-down'
import CollapseCloseIcon from 'react-icons/lib/md/arrow-drop-up'
export default class Collapser extends React.Component {
static propTypes = {
isCollapsed: React.PropTypes.bool.isRequired,
style: React.PropTypes.object,
isCollapsed: PropTypes.bool.isRequired,
style: PropTypes.object,
}
render() {

View file

@ -1,16 +1,17 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
class MetadataBlock extends React.Component {
static propTypes = {
value: React.PropTypes.string,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.string,
onChange: PropTypes.func.isRequired,
}
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
multi={true}
value={this.props.value}

View file

@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import CodeMirror from 'react-codemirror'
import {Controlled as CodeMirror} from 'react-codemirror2'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
@ -18,8 +19,8 @@ import '../../vendor/codemirror/addon/lint/json-lint'
class JSONEditor extends React.Component {
static propTypes = {
layer: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func,
layer: PropTypes.object.isRequired,
onChange: PropTypes.func,
}
constructor(props) {
@ -80,7 +81,7 @@ class JSONEditor extends React.Component {
return <CodeMirror
value={this.state.code}
onChange={this.onCodeUpdate.bind(this)}
onBeforeChange={(editor, data, value) => this.onCodeUpdate(value)}
onFocusChange={focused => focused ? true : this.resetValue()}
options={codeMirrorOptions}
/>

View file

@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import JSONEditor from './JSONEditor'
import FilterEditor from '../filter/FilterEditor'
@ -43,12 +44,12 @@ function layoutGroups(layerType) {
/** Layer editor supporting multiple types of layers. */
export default class LayerEditor extends React.Component {
static propTypes = {
layer: React.PropTypes.object.isRequired,
sources: React.PropTypes.object,
vectorLayers: React.PropTypes.object,
spec: React.PropTypes.object.isRequired,
onLayerChanged: React.PropTypes.func,
onLayerIdChange: React.PropTypes.func,
layer: PropTypes.object.isRequired,
sources: PropTypes.object,
vectorLayers: PropTypes.object,
spec: PropTypes.object.isRequired,
onLayerChanged: PropTypes.func,
onLayerIdChange: PropTypes.func,
}
static defaultProps = {
@ -58,7 +59,7 @@ export default class LayerEditor extends React.Component {
}
static childContextTypes = {
reactIconBase: React.PropTypes.object
reactIconBase: PropTypes.object
}
constructor(props) {
@ -116,6 +117,11 @@ export default class LayerEditor extends React.Component {
comment = this.props.layer.metadata['maputnik:comment']
}
let sourceLayerIds;
if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
sourceLayerIds = this.props.sources[this.props.layer.source].layers;
}
switch(type) {
case 'layer': return <div>
<LayerIdBlock
@ -132,8 +138,9 @@ export default class LayerEditor extends React.Component {
onChange={v => this.changeProperty(null, 'source', v)}
/>
}
{this.props.layer.type !== 'raster' && this.props.layer.type !== 'background' && <LayerSourceLayerBlock
sourceLayerIds={this.props.sources[this.props.layer.source]}
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
<LayerSourceLayerBlock
sourceLayerIds={sourceLayerIds}
value={this.props.layer['source-layer']}
onChange={v => this.changeProperty(null, 'source-layer', v)}
/>

View file

@ -1,13 +1,14 @@
import React from 'react'
import PropTypes from 'prop-types'
import Collapse from 'react-collapse'
import Collapser from './Collapser'
export default class LayerEditorGroup extends React.Component {
static propTypes = {
title: React.PropTypes.string.isRequired,
isActive: React.PropTypes.bool.isRequired,
children: React.PropTypes.element.isRequired,
onActiveToggle: React.PropTypes.func.isRequired
title: PropTypes.string.isRequired,
isActive: PropTypes.bool.isRequired,
children: PropTypes.element.isRequired,
onActiveToggle: PropTypes.func.isRequired
}
render() {

View file

@ -1,17 +1,18 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
class LayerIdBlock extends React.Component {
static propTypes = {
value: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"ID"} doc={GlSpec.layer.id.doc}>
return <InputBlock label={"ID"} doc={styleSpec.latest.layer.id.doc}>
<StringInput
value={this.props.value}
onChange={this.props.onChange}

View file

@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import cloneDeep from 'lodash.clonedeep'
@ -12,11 +13,11 @@ import style from '../../libs/style.js'
import {SortableContainer, SortableHandle, arrayMove} from 'react-sortable-hoc';
const layerListPropTypes = {
layers: React.PropTypes.array.isRequired,
selectedLayerIndex: React.PropTypes.number.isRequired,
onLayersChange: React.PropTypes.func.isRequired,
onLayerSelect: React.PropTypes.func,
sources: React.PropTypes.object.isRequired,
layers: PropTypes.array.isRequired,
selectedLayerIndex: PropTypes.number.isRequired,
onLayersChange: PropTypes.func.isRequired,
onLayerSelect: PropTypes.func,
sources: PropTypes.object.isRequired,
}
function layerPrefix(name) {
@ -49,6 +50,7 @@ class LayerListContainer extends React.Component {
super(props)
this.state = {
collapsedGroups: {},
areAllGroupsExpanded: false,
isOpen: {
add: false,
}
@ -94,6 +96,31 @@ class LayerListContainer extends React.Component {
})
}
toggleLayers() {
let idx=0
let newGroups=[]
this.groupedLayers().forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id)
const lookupKey = [groupPrefix, idx].join('-')
if (layers.length > 1) {
newGroups[lookupKey] = this.state.areAllGroupsExpanded
}
layers.forEach((layer) => {
idx += 1
})
});
this.setState({
collapsedGroups: newGroups,
areAllGroupsExpanded: !this.state.areAllGroupsExpanded
})
}
groupedLayers() {
const groups = []
for (let i = 0; i < this.props.layers.length; i++) {
@ -137,7 +164,7 @@ class LayerListContainer extends React.Component {
const grp = <LayerListGroup
key={[groupPrefix, idx].join('-')}
title={groupPrefix}
isActive={!this.isCollapsed(groupPrefix, idx)}
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
/>
listItems.push(grp)
@ -145,9 +172,10 @@ class LayerListContainer extends React.Component {
layers.forEach((layer, idxInGroup) => {
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
const listItem = <LayerListItem
className={classnames({
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx),
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1
})}
index={idx}
@ -177,11 +205,24 @@ class LayerListContainer extends React.Component {
<header className="maputnik-layer-list-header">
<span className="maputnik-layer-list-header-title">Layers</span>
<span className="maputnik-space" />
<Button
onClick={this.toggleModal.bind(this, 'add')}
className="maputnik-add-layer">
Add Layer
</Button>
<div className="maputnik-default-property">
<div className="maputnik-multibutton">
<a
onClick={this.toggleLayers.bind(this)}
className="maputnik-button">
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
</a>
</div>
</div>
<div className="maputnik-default-property">
<div className="maputnik-multibutton">
<a
onClick={this.toggleModal.bind(this, 'add')}
className="maputnik-button maputnik-button-selected">
Add Layer
</a>
</div>
</div>
</header>
<ul className="maputnik-layer-list-container">
{listItems}

View file

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

View file

@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import Color from 'color'
import classnames from 'classnames'
@ -30,8 +31,8 @@ class LayerTypeDragHandle extends React.Component {
class IconAction extends React.Component {
static propTypes = {
action: React.PropTypes.string.isRequired,
onClick: React.PropTypes.func.isRequired,
action: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
}
renderIcon() {
@ -57,16 +58,16 @@ class IconAction extends React.Component {
@SortableElement
class LayerListItem extends React.Component {
static propTypes = {
layerId: React.PropTypes.string.isRequired,
layerType: React.PropTypes.string.isRequired,
isSelected: React.PropTypes.bool,
visibility: React.PropTypes.string,
className: React.PropTypes.string,
layerId: PropTypes.string.isRequired,
layerType: PropTypes.string.isRequired,
isSelected: PropTypes.bool,
visibility: PropTypes.string,
className: PropTypes.string,
onLayerSelect: React.PropTypes.func.isRequired,
onLayerCopy: React.PropTypes.func,
onLayerDestroy: React.PropTypes.func,
onLayerVisibilityToggle: React.PropTypes.func,
onLayerSelect: PropTypes.func.isRequired,
onLayerCopy: PropTypes.func,
onLayerDestroy: PropTypes.func,
onLayerVisibilityToggle: PropTypes.func,
}
static defaultProps = {
@ -78,7 +79,7 @@ class LayerListItem extends React.Component {
}
static childContextTypes = {
reactIconBase: React.PropTypes.object
reactIconBase: PropTypes.object
}
getChildContext() {

View file

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

View file

@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
@ -8,19 +9,22 @@ import AutocompleteInput from '../inputs/AutocompleteInput'
class LayerSourceLayer extends React.Component {
static propTypes = {
value: React.PropTypes.string,
onChange: React.PropTypes.func,
sourceLayerIds: React.PropTypes.array,
value: PropTypes.string,
onChange: PropTypes.func,
sourceLayerIds: PropTypes.array,
isFixed: PropTypes.bool,
}
static defaultProps = {
onChange: () => {},
sourceLayerIds: [],
isFixed: false
}
render() {
return <InputBlock label={"Source Layer"} doc={GlSpec.layer['source-layer'].doc}>
return <InputBlock label={"Source Layer"} doc={styleSpec.latest.layer['source-layer'].doc}>
<AutocompleteInput
keepMenuWithinWindowBounds={!!this.props.isFixed}
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceLayerIds.map(l => [l, l])}

View file

@ -1,17 +1,18 @@
import React from 'react'
import PropTypes from 'prop-types'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock'
import SelectInput from '../inputs/SelectInput'
class LayerTypeBlock extends React.Component {
static propTypes = {
value: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"Type"} doc={GlSpec.layer.type.doc}>
return <InputBlock label={"Type"} doc={styleSpec.latest.layer.type.doc}>
<SelectInput
options={[
['background', 'Background'],
@ -21,6 +22,8 @@ class LayerTypeBlock extends React.Component {
['raster', 'Raster'],
['circle', 'Circle'],
['fill-extrusion', 'Fill Extrusion'],
['hillshade', 'Hillshade'],
['heatmap', 'Heatmap'],
]}
onChange={this.props.onChange}
value={this.props.value}

View file

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

View file

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

View file

@ -1,19 +1,38 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import LayerIcon from '../icons/LayerIcon'
function groupFeaturesBySourceLayer(features) {
const sources = {}
let returnedFeatures = {};
features.forEach(feature => {
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
sources[feature.layer['source-layer']].push(feature)
if(returnedFeatures.hasOwnProperty(feature.layer.id)) {
returnedFeatures[feature.layer.id]++
const featureObject = sources[feature.layer['source-layer']].find(f => f.layer.id === feature.layer.id)
featureObject.counter = returnedFeatures[feature.layer.id]
} else {
sources[feature.layer['source-layer']] = sources[feature.layer['source-layer']] || []
sources[feature.layer['source-layer']].push(feature)
returnedFeatures[feature.layer.id] = 1
}
})
return sources
}
class FeatureLayerPopup extends React.Component {
static propTypes = {
onLayerSelect: PropTypes.func.isRequired,
features: PropTypes.array
}
render() {
const sources = groupFeaturesBySourceLayer(this.props.features)
@ -22,6 +41,9 @@ class FeatureLayerPopup extends React.Component {
return <label
key={idx}
className="maputnik-popup-layer"
onClick={() => {
this.props.onLayerSelect(feature.layer.id)
}}
>
<LayerIcon type={feature.layer.type} style={{
width: 14,
@ -29,6 +51,7 @@ class FeatureLayerPopup extends React.Component {
paddingRight: 3
}}/>
{feature.layer.id}
{feature.counter && <span> × {feature.counter}</span>}
</label>
})
return <div key={vectorLayerId}>

View file

@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
@ -22,7 +23,7 @@ function renderProperties(feature) {
function renderFeature(feature) {
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"}>
<StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
</InputBlock>
@ -30,10 +31,36 @@ function renderFeature(feature) {
</div>
}
function removeDuplicatedFeatures(features) {
let uniqueFeatures = [];
features.forEach(feature => {
const featureIndex = uniqueFeatures.findIndex(feature2 => {
return feature.layer['source-layer'] === feature2.layer['source-layer']
&& JSON.stringify(feature.properties) === JSON.stringify(feature2.properties)
})
if(featureIndex === -1) {
uniqueFeatures.push(feature)
} else {
if(uniqueFeatures[featureIndex].hasOwnProperty('counter')) {
uniqueFeatures[featureIndex].inspectModeCounter++
} else {
uniqueFeatures[featureIndex].inspectModeCounter = 2
}
}
})
return uniqueFeatures
}
class FeaturePropertyPopup extends React.Component {
static propTypes = {
features: PropTypes.array
}
render() {
const features = this.props.features
const features = removeDuplicatedFeatures(this.props.features)
return <div className="maputnik-feature-property-popup">
{features.map(renderFeature)}
</div>

View file

@ -1,25 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
import MapboxGl from 'mapbox-gl'
import MapboxInspect from 'mapbox-gl-inspect'
import FeatureLayerPopup from './FeatureLayerPopup'
import FeaturePropertyPopup from './FeaturePropertyPopup'
import validateColor from 'mapbox-gl-style-spec/lib/validate/validate_color'
import style from '../../libs/style.js'
import tokens from '../../config/tokens.json'
import colors from 'mapbox-gl-inspect/lib/colors'
import Color from 'color'
import ZoomControl from '../../libs/zoomcontrol'
import { colorHighlightedLayer } from '../../libs/highlight'
import 'mapbox-gl/dist/mapbox-gl.css'
import '../../mapboxgl.css'
import '../../libs/mapbox-rtl'
function renderLayerPopup(features) {
var mountNode = document.createElement('div');
ReactDOM.render(<FeatureLayerPopup features={features} />, mountNode)
return mountNode.innerHTML;
}
function renderPropertyPopup(features) {
var mountNode = document.createElement('div');
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode)
@ -43,7 +38,7 @@ function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
const sources = {}
Object.keys(originalMapStyle.sources).forEach(sourceId => {
const source = originalMapStyle.sources[sourceId]
if(source.type !== 'raster') {
if(source.type !== 'raster' && source.type !== 'raster-dem') {
sources[sourceId] = source
}
})
@ -58,15 +53,17 @@ function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
export default class MapboxGlMap extends React.Component {
static propTypes = {
onDataChange: React.PropTypes.func,
mapStyle: React.PropTypes.object.isRequired,
inspectModeEnabled: React.PropTypes.bool.isRequired,
highlightedLayer: React.PropTypes.object,
onDataChange: PropTypes.func,
onLayerSelect: PropTypes.func.isRequired,
mapStyle: PropTypes.object.isRequired,
inspectModeEnabled: PropTypes.bool.isRequired,
highlightedLayer: PropTypes.object,
}
static defaultProps = {
onMapLoaded: () => {},
onDataChange: () => {},
onLayerSelect: () => {},
mapboxAccessToken: tokens.mapbox,
}
@ -110,6 +107,9 @@ export default class MapboxGlMap extends React.Component {
hash: true,
})
const zoom = new ZoomControl;
map.addControl(zoom, 'top-right');
const nav = new MapboxGl.NavigationControl();
map.addControl(nav, 'top-right');
@ -129,7 +129,9 @@ export default class MapboxGlMap extends React.Component {
if(this.props.inspectModeEnabled) {
return renderPropertyPopup(features)
} 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 PropTypes from 'prop-types'
import style from '../../libs/style.js'
import isEqual from 'lodash.isequal'
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 {
static propTypes = {
onDataChange: React.PropTypes.func,
mapStyle: React.PropTypes.object.isRequired,
accessToken: React.PropTypes.string,
onDataChange: PropTypes.func,
mapStyle: PropTypes.object.isRequired,
accessToken: PropTypes.string,
style: PropTypes.object,
}
static defaultProps = {
@ -63,48 +21,17 @@ class OpenLayers3Map extends React.Component {
constructor(props) {
super(props)
this.tilegrid = null
this.resolutions = null
this.layer = null
this.map = null
}
updateStyle(newMapStyle) {
const oldSource = suitableVectorSource(this.props.mapStyle)
const newSource = suitableVectorSource(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)
}
}
const olms = require('ol-mapbox-style');
const styleFunc = olms.apply(this.map, newMapStyle)
}
componentWillReceiveProps(nextProps) {
require.ensure(["openlayers", "ol-mapbox-style"], () => {
if(!this.map || !this.resolutions) return
require.ensure(["ol", "ol-mapbox-style"], () => {
if(!this.map) return
this.updateStyle(nextProps.mapStyle)
})
}
@ -112,24 +39,22 @@ class OpenLayers3Map extends React.Component {
componentDidMount() {
//Load OpenLayers dynamically once we need it
//TODO: Make this more convenient
require.ensure(["openlayers", "ol-mapbox-style"], ()=> {
require.ensure(["ol", "ol/map", "ol/view", "ol/control/zoom", "ol-mapbox-style"], ()=> {
console.log('Loaded OpenLayers3 renderer')
const ol = require('openlayers')
const olms = require('ol-mapbox-style')
const olMap = require('ol/map').default
const olView = require('ol/view').default
const olZoom = require('ol/control/zoom').default
this.tilegrid = ol.tilegrid.createXYZ({tileSize: 512, maxZoom: 22})
this.resolutions = this.tilegrid.getResolutions()
const map = new ol.Map({
const map = new olMap({
target: this.container,
layers: [],
view: new ol.View({
view: new olView({
zoom: 2,
center: [52.5, -78.4]
})
})
map.addControl(new ol.control.Zoom())
map.addControl(new olZoom())
this.map = map
this.updateStyle(this.props.mapStyle)
})
@ -140,10 +65,10 @@ class OpenLayers3Map extends React.Component {
ref={x => this.container = x}
style={{
position: "fixed",
top: 0,
top: 40,
right: 0,
bottom: 0,
height: "100%",
height: 'calc(100% - 40px)',
width: "75%",
backgroundColor: '#fff',
...this.props.style,

View file

@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import Button from '../Button'
import InputBlock from '../inputs/InputBlock'
@ -13,13 +14,13 @@ import LayerSourceLayerBlock from '../layers/LayerSourceLayerBlock'
class AddModal extends React.Component {
static propTypes = {
layers: React.PropTypes.array.isRequired,
onLayersChange: React.PropTypes.func.isRequired,
isOpen: React.PropTypes.bool.isRequired,
onOpenToggle: React.PropTypes.func.isRequired,
layers: PropTypes.array.isRequired,
onLayersChange: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired,
// A dict of source id's and the available source layers
sources: React.PropTypes.object.isRequired,
sources: PropTypes.object.isRequired,
}
addLayer() {
@ -55,18 +56,65 @@ class AddModal extends React.Component {
}
}
componentWillReceiveProps(nextProps) {
const sourceIds = Object.keys(nextProps.sources)
if(!this.state.source && sourceIds.length > 0) {
componentWillUpdate(nextProps, nextState) {
// Check if source is valid for new type
const oldType = this.state.type;
const newType = nextState.type;
const availableSourcesOld = this.getSources(oldType);
const availableSourcesNew = this.getSources(newType);
if(
// Type has changed
oldType !== newType
&& this.state.source !== ""
// Was a valid source previously
&& availableSourcesOld.indexOf(this.state.source) > -1
// And is not a valid source now
&& availableSourcesNew.indexOf(nextState.source) < 0
) {
// Clear the source
this.setState({
source: sourceIds[0],
'source-layer': this.state['source-layer'] || (nextProps.sources[sourceIds[0]] || [])[0]
})
source: ""
});
}
}
getLayersForSource(source) {
const sourceObj = this.props.sources[source] || {};
return sourceObj.layers || [];
}
getSources(type) {
const sources = [];
const types = {
vector: [
"fill",
"line",
"symbol",
"circle",
"fill-extrusion"
],
raster: [
"raster"
]
}
for(let [key, val] of Object.entries(this.props.sources)) {
if(types[val.type] && types[val.type].indexOf(type) > -1) {
sources.push(key);
}
}
return sources;
}
render() {
const sources = this.getSources(this.state.type);
const layers = this.getLayersForSource(this.state.source);
return <Modal
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
@ -83,14 +131,15 @@ class AddModal extends React.Component {
/>
{this.state.type !== 'background' &&
<LayerSourceBlock
sourceIds={Object.keys(this.props.sources)}
sourceIds={sources}
value={this.state.source}
onChange={v => this.setState({ source: v })}
/>
}
{this.state.type !== 'background' && this.state.type !== 'raster' &&
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
<LayerSourceLayerBlock
sourceLayerIds={this.props.sources[this.state.source] || []}
isFixed={true}
sourceLayerIds={layers}
value={this.state['source-layer']}
onChange={v => this.setState({ 'source-layer': v })}
/>

View file

@ -1,7 +1,8 @@
import React from 'react'
import PropTypes from 'prop-types'
import { saveAs } from 'file-saver'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import SelectInput from '../inputs/SelectInput'
@ -9,21 +10,23 @@ import CheckboxInput from '../inputs/CheckboxInput'
import Button from '../Button'
import Modal from './Modal'
import MdFileDownload from 'react-icons/lib/md/file-download'
import TiClipboard from 'react-icons/lib/ti/clipboard'
import style from '../../libs/style.js'
import formatStyle from 'mapbox-gl-style-spec/lib/format'
import GitHub from 'github-api'
import { CopyToClipboard } from 'react-copy-to-clipboard'
class Gist extends React.Component {
static propTypes = {
mapStyle: React.PropTypes.object.isRequired,
onStyleChanged: React.PropTypes.func.isRequired,
mapStyle: PropTypes.object.isRequired,
onStyleChanged: PropTypes.func.isRequired,
}
constructor(props) {
super(props);
this.state = {
preview: false,
public: false,
saving: false,
latestGist: null,
}
@ -41,11 +44,14 @@ class Gist extends React.Component {
...this.state,
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 ?
formatStyle(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle))) :
formatStyle(stripAccessTokens(this.props.mapStyle));
styleSpec.format(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle))) :
styleSpec.format(stripAccessTokens(this.props.mapStyle));
const styleTitle = this.props.mapStyle.name || 'Style';
const htmlStr = `
<!DOCTYPE html>
@ -54,8 +60,8 @@ class Gist extends React.Component {
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>`+styleTitle+` Preview</title>
<link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v0.28.0/mapbox-gl.css" />
<script src="https://api.mapbox.com/mapbox-gl-js/v0.28.0/mapbox-gl.js"></script>
<link rel="stylesheet" type="text/css" href="https://api.mapbox.com/mapbox-gl-js/v0.44.0/mapbox-gl.css" />
<script src="https://api.mapbox.com/mapbox-gl-js/v0.44.0/mapbox-gl.js"></script>
<style>
body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; }
@ -64,6 +70,7 @@ class Gist extends React.Component {
<body>
<div id='map'></div>
<script>
mapboxgl.accessToken = '${mapboxToken}';
var map = new mapboxgl.Map({
container: 'map',
style: 'style.json',
@ -88,7 +95,7 @@ class Gist extends React.Component {
const gh = new GitHub();
let gist = gh.getGist(); // not a gist yet
gist.create({
public: true,
public: this.state.public,
description: styleTitle,
files: files
}).then(function({data}) {
@ -109,6 +116,13 @@ class Gist extends React.Component {
})
}
onPublicChange(value) {
this.setState({
...this.state,
public: value
})
}
changeMetadataProperty(property, value) {
const changedStyle = {
...this.props.mapStyle,
@ -125,7 +139,7 @@ class Gist extends React.Component {
const user = gist.user || 'anonymous';
const preview = !!gist.files['index.html'];
if(preview) {
return <span><a target="_blank" href={"https://bl.ocks.org/"+user+"/"+gist.id}>Preview</a>,{' '}</span>
return <span><a target="_blank" rel="noopener noreferrer" href={"https://bl.ocks.org/"+user+"/"+gist.id}>Preview</a>,{' '}</span>
}
return null;
}
@ -137,11 +151,21 @@ class Gist extends React.Component {
return <p>Saving...</p>
} else if(gist) {
const user = gist.user || 'anonymous';
return <p>
Latest saved gist:{' '}
{this.renderPreviewLink(this)}
<a target="_blank" href={"https://gist.github.com/"+user+"/"+gist.id}>Source</a>
</p>
const rawGistLink = "https://gist.githubusercontent.com/" + user + "/" + gist.id + "/raw/" + gist.history[0].version + "/style.json"
const maputnikStyleLink = "https://maputnik.github.io/editor/?style=" + rawGistLink
return <div className="maputnik-render-gist">
<p>
Latest saved gist:{' '}
{this.renderPreviewLink(this)}
<a target="_blank" rel="noopener noreferrer" href={"https://gist.github.com/" + user + "/" + gist.id}>Source</a>
</p>
<p>
<CopyToClipboard text={maputnikStyleLink}>
<span>Share this style: <Button><TiClipboard size={18} /></Button></span>
</CopyToClipboard>
<StringInput value={maputnikStyleLink} />
</p>
</div>
}
}
@ -151,13 +175,22 @@ class Gist extends React.Component {
<MdFileDownload />
Save to Gist (anonymous)
</Button>
{' '}
<CheckboxInput
value={this.state.preview}
name='gist-style-preview'
onChange={this.onPreviewChange.bind(this)}
/>
<span> Include preview</span>
<div className="maputnik-modal-sub-section">
<CheckboxInput
value={this.state.public}
name='gist-style-public'
onChange={this.onPublicChange.bind(this)}
/>
<span> Public gist</span>
</div>
<div className="maputnik-modal-sub-section">
<CheckboxInput
value={this.state.preview}
name='gist-style-preview'
onChange={this.onPreviewChange.bind(this)}
/>
<span> Include preview</span>
</div>
{this.state.preview ?
<div>
<InputBlock
@ -166,7 +199,13 @@ class Gist extends React.Component {
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}/>
</InputBlock>
<a target="_blank" href="https://openmaptiles.com/hosting/">Get your free access token</a>
<InputBlock
label={"Mapbox Access Token: "}>
<StringInput
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}/>
</InputBlock>
<a target="_blank" rel="noopener noreferrer" href="https://openmaptiles.com/hosting/">Get your free access token</a>
</div>
: null}
{this.renderLatestGist()}
@ -186,10 +225,10 @@ function stripAccessTokens(mapStyle) {
class ExportModal extends React.Component {
static propTypes = {
mapStyle: React.PropTypes.object.isRequired,
onStyleChanged: React.PropTypes.func.isRequired,
isOpen: React.PropTypes.bool.isRequired,
onOpenToggle: React.PropTypes.func.isRequired,
mapStyle: PropTypes.object.isRequired,
onStyleChanged: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired,
}
constructor(props) {
@ -197,7 +236,7 @@ class ExportModal extends React.Component {
}
downloadStyle() {
const blob = new Blob([formatStyle(stripAccessTokens(this.props.mapStyle))], {type: "application/json;charset=utf-8"});
const blob = new Blob([styleSpec.format(stripAccessTokens(this.props.mapStyle))], {type: "application/json;charset=utf-8"});
saveAs(blob, this.props.mapStyle.id + ".json");
}

View file

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

View file

@ -1,4 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import Modal from './Modal'
import Button from '../Button'
import FileReaderInput from 'react-file-reader-input'
@ -12,10 +13,10 @@ import publicStyles from '../../config/styles.json'
class PublicStyle extends React.Component {
static propTypes = {
url: React.PropTypes.string.isRequired,
thumbnailUrl: React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired,
onSelect: React.PropTypes.func.isRequired,
url: PropTypes.string.isRequired,
thumbnailUrl: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
onSelect: PropTypes.func.isRequired,
}
render() {
@ -41,9 +42,9 @@ class PublicStyle extends React.Component {
class OpenModal extends React.Component {
static propTypes = {
isOpen: React.PropTypes.bool.isRequired,
onOpenToggle: React.PropTypes.func.isRequired,
onStyleOpen: React.PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired,
onStyleOpen: PropTypes.func.isRequired,
}
constructor(props) {
@ -68,12 +69,18 @@ class OpenModal extends React.Component {
const mapStyle = style.ensureStyleValidity(JSON.parse(body))
console.log('Loaded style ', mapStyle.id)
this.props.onStyleOpen(mapStyle)
this.onOpenToggle()
} else {
console.warn('Could not open the style URL', styleUrl)
}
})
}
onOpenUrl() {
const url = this.styleUrlElement.value;
this.onStyleSelect(url);
}
onUpload(_, files) {
const [e, file] = files[0];
const reader = new FileReader();
@ -94,6 +101,7 @@ class OpenModal extends React.Component {
}
mapStyle = style.ensureStyleValidity(mapStyle)
this.props.onStyleOpen(mapStyle);
this.onOpenToggle();
}
reader.onerror = e => console.log(e.target);
}
@ -137,6 +145,18 @@ class OpenModal extends React.Component {
<Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button>
</FileReaderInput>
</section>
<section className="maputnik-modal-section">
<h2>Load from URL</h2>
<p>
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
</p>
<input type="text" ref={(input) => this.styleUrlElement = input} className="maputnik-input" placeholder="Enter URL..."/>
<div>
<Button className="maputnik-big-button" onClick={this.onOpenUrl.bind(this)}>Open URL</Button>
</div>
</section>
<section className="maputnik-modal-section maputnik-modal-section--shrink">
<h2>Gallery Styles</h2>
<p>

View file

@ -1,10 +1,11 @@
import React from 'react'
import PropTypes from 'prop-types'
class Overlay extends React.Component {
static propTypes = {
isOpen: React.PropTypes.bool.isRequired,
children: React.PropTypes.element.isRequired
isOpen: PropTypes.bool.isRequired,
children: PropTypes.element.isRequired
}
render() {

View file

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

View file

@ -1,5 +1,6 @@
import React from 'react'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import PropTypes from 'prop-types'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import Modal from './Modal'
import Button from '../Button'
import InputBlock from '../inputs/InputBlock'
@ -16,10 +17,10 @@ import DeleteIcon from 'react-icons/lib/md/delete'
class PublicSource extends React.Component {
static propTypes = {
id: React.PropTypes.string.isRequired,
type: React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired,
onSelect: React.PropTypes.func.isRequired,
id: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
onSelect: PropTypes.func.isRequired,
}
render() {
@ -44,6 +45,10 @@ function editorMode(source) {
if(source.tiles) return 'tilexyz_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.tiles) return 'tilexyz_vector'
return 'tilejson_vector'
@ -54,10 +59,10 @@ function editorMode(source) {
class ActiveSourceTypeEditor extends React.Component {
static propTypes = {
sourceId: React.PropTypes.string.isRequired,
source: React.PropTypes.object.isRequired,
onDelete: React.PropTypes.func.isRequired,
onChange: React.PropTypes.func.isRequired,
sourceId: PropTypes.string.isRequired,
source: PropTypes.object.isRequired,
onDelete: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
@ -87,7 +92,7 @@ class ActiveSourceTypeEditor extends React.Component {
class AddSource extends React.Component {
static propTypes = {
onAdd: React.PropTypes.func.isRequired,
onAdd: PropTypes.func.isRequired,
}
constructor(props) {
@ -126,6 +131,16 @@ class AddSource extends React.Component {
minzoom: source.minzoom || 0,
maxzoom: source.maxzoom || 14
}
case 'tilejson_raster-dem': return {
type: 'raster-dem',
url: source.url || 'http://localhost:3000/tilejson.json'
}
case 'tilexyz_raster-dem': return {
type: 'raster-dem',
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
minzoom: source.minzoom || 0,
maxzoom: source.maxzoom || 14
}
default: return {}
}
}
@ -138,7 +153,7 @@ class AddSource extends React.Component {
onChange={v => this.setState({ sourceId: v})}
/>
</InputBlock>
<InputBlock label={"Source Type"} doc={GlSpec.source_tile.type.doc}>
<InputBlock label={"Source Type"} doc={styleSpec.latest.source_vector.type.doc}>
<SelectInput
options={[
['geojson', 'GeoJSON'],
@ -146,6 +161,8 @@ class AddSource extends React.Component {
['tilexyz_vector', 'Vector (XYZ URLs)'],
['tilejson_raster', 'Raster (TileJSON URL)'],
['tilexyz_raster', 'Raster (XYZ URL)'],
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
]}
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
value={this.state.mode}
@ -167,10 +184,10 @@ class AddSource extends React.Component {
class SourcesModal extends React.Component {
static propTypes = {
mapStyle: React.PropTypes.object.isRequired,
isOpen: React.PropTypes.bool.isRequired,
onOpenToggle: React.PropTypes.func.isRequired,
onStyleChanged: React.PropTypes.func.isRequired,
mapStyle: PropTypes.object.isRequired,
isOpen: PropTypes.bool.isRequired,
onOpenToggle: PropTypes.func.isRequired,
onStyleChanged: PropTypes.func.isRequired,
}
stripTitle(source) {

View file

@ -1,17 +1,18 @@
import React from 'react'
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import PropTypes from 'prop-types'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
import NumberInput from '../inputs/NumberInput'
class TileJSONSourceEditor extends React.Component {
static propTypes = {
source: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired,
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"TileJSON URL"} doc={GlSpec.source_tile.url.doc}>
return <InputBlock label={"TileJSON URL"} doc={styleSpec.latest.source_vector.url.doc}>
<StringInput
value={this.props.source.url}
onChange={url => this.props.onChange({
@ -25,8 +26,8 @@ class TileJSONSourceEditor extends React.Component {
class TileURLSourceEditor extends React.Component {
static propTypes = {
source: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired,
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
changeTileUrl(idx, value) {
@ -42,7 +43,7 @@ class TileURLSourceEditor extends React.Component {
const prefix = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th']
const tiles = this.props.source.tiles || []
return tiles.map((tileUrl, tileIndex) => {
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"} doc={GlSpec.source_tile.tiles.doc}>
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"} doc={styleSpec.latest.source_vector.tiles.doc}>
<StringInput
value={tileUrl}
onChange={this.changeTileUrl.bind(this, tileIndex)}
@ -54,7 +55,7 @@ class TileURLSourceEditor extends React.Component {
render() {
return <div>
{this.renderTileUrls()}
<InputBlock label={"Min Zoom"} doc={GlSpec.source_tile.minzoom.doc}>
<InputBlock label={"Min Zoom"} doc={styleSpec.latest.source_vector.minzoom.doc}>
<NumberInput
value={this.props.source.minzoom || 0}
onChange={minzoom => this.props.onChange({
@ -63,7 +64,7 @@ class TileURLSourceEditor extends React.Component {
})}
/>
</InputBlock>
<InputBlock label={"Max Zoom"} doc={GlSpec.source_tile.maxzoom.doc}>
<InputBlock label={"Max Zoom"} doc={styleSpec.latest.source_vector.maxzoom.doc}>
<NumberInput
value={this.props.source.maxzoom || 22}
onChange={maxzoom => this.props.onChange({
@ -79,12 +80,12 @@ class TileURLSourceEditor extends React.Component {
class GeoJSONSourceEditor extends React.Component {
static propTypes = {
source: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired,
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
return <InputBlock label={"GeoJSON Data"} doc={GlSpec.source_geojson.data.doc}>
return <InputBlock label={"GeoJSON Data"} doc={styleSpec.latest.source_geojson.data.doc}>
<StringInput
value={this.props.source.data}
onChange={data => this.props.onChange({
@ -98,9 +99,9 @@ class GeoJSONSourceEditor extends React.Component {
class SourceTypeEditor extends React.Component {
static propTypes = {
mode: React.PropTypes.string.isRequired,
source: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired,
mode: PropTypes.string.isRequired,
source: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
}
render() {
@ -114,6 +115,8 @@ class SourceTypeEditor extends React.Component {
case 'tilexyz_vector': return <TileURLSourceEditor {...commonProps} />
case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps} />
default: return null
}
}

View file

@ -91,7 +91,8 @@
"circle-stroke-width",
"circle-pitch-scale",
"circle-translate",
"circle-translate-anchor"
"circle-translate-anchor",
"circle-pitch-alignment"
]
}
]
@ -147,7 +148,9 @@
"icon-rotate",
"icon-padding",
"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",
"title": "Klokantech Basic",
"url": "https://rawgit.com/openmaptiles/klokantech-basic-gl-style/master/style.json",
"thumbnail": "http://maputnik.com/thumbnails/klokantech-basic.png"
"thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png"
},
{
"id": "dark-matter",
"title": "Dark Matter",
"url": "https://rawgit.com/openmaptiles/dark-matter-gl-style/master/style.json",
"thumbnail": "http://maputnik.com/thumbnails/dark-matter.png"
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
},
{
"id": "positron",
"title": "Positron",
"url": "https://rawgit.com/openmaptiles/positron-gl-style/master/style.json",
"thumbnail": "http://maputnik.com/thumbnails/positron.png"
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png"
},
{
"id": "osm-bright",
"title": "OSM Bright",
"url": "https://rawgit.com/openmaptiles/osm-bright-gl-style/master/style.json",
"thumbnail": "http://maputnik.com/thumbnails/osm-bright.png"
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png"
},
{
"id": "osm-liberty",
@ -39,24 +39,18 @@
"id": "mapbox-satellite",
"title": "Mapbox Satellite",
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/satellite-v9.json",
"thumbnail": "http://maputnik.com/thumbnails/mapbox-satellite.png"
"thumbnail": "https://maputnik.github.io/thumbnails/mapbox-satellite.png"
},
{
"id": "mapbox-bright",
"title": "Mapbox Bright",
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/bright-v9.json",
"thumbnail": "http://maputnik.com/thumbnails/mapbox-bright.png"
"thumbnail": "https://maputnik.github.io/thumbnails/mapbox-bright.png"
},
{
"id": "mapbox-basic",
"title": "Mapbox Basic",
"url": "https://rawgit.com/mapbox/mapbox-gl-styles/master/styles/basic-v9.json",
"thumbnail": "http://maputnik.com/thumbnails/mapbox-basic.png"
},
{
"id": "tilezen",
"title": "Tilezen",
"url": "https://rawgit.com/lukasmartinelli/tilezen-gl-style/master/style.json",
"thumbnail": "http://maputnik.com/thumbnails/tilezen.png"
"thumbnail": "https://maputnik.github.io/thumbnails/mapbox-basic.png"
}
]

View file

@ -8,14 +8,5 @@
"type": "vector",
"url": "https://free.tilehosting.com/data/v3.json?key={key}",
"title": "OpenMapTiles"
},
"tilezen": {
"type": "vector",
"tiles": [
"http://tile.mapzen.com/mapzen/vector/v1/{layers}/{z}/{x}/{y}.pbf?api_key=mapzen-RVcyVL7"
],
"minZoom": 0,
"maxZoom": 15,
"title": "Mapzen Vector Tile Service"
}
}

View file

@ -1,7 +1,7 @@
import diffStyles from 'mapbox-gl-style-spec/lib/diff'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
export function diffMessages(beforeStyle, afterStyle) {
const changes = diffStyles(beforeStyle, afterStyle)
const changes = styleSpec.diff(beforeStyle, afterStyle)
return changes.map(cmd => cmd.command + ' ' + cmd.args.join(' '))
}

9
src/libs/document-uid.js Normal file
View file

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

View file

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

View file

@ -1,16 +1,16 @@
import GlSpec from 'mapbox-gl-style-spec/reference/latest.js'
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
export function changeType(layer, newType) {
const changedPaintProps = { ...layer.paint }
Object.keys(changedPaintProps).forEach(propertyName => {
if(!(propertyName in GlSpec['paint_' + newType])) {
if(!(propertyName in styleSpec.latest['paint_' + newType])) {
delete changedPaintProps[propertyName]
}
})
const changedLayoutProps = { ...layer.layout }
Object.keys(changedLayoutProps).forEach(propertyName => {
if(!(propertyName in GlSpec['layout_' + newType])) {
if(!(propertyName in styleSpec.latest['layout_' + newType])) {
delete changedLayoutProps[propertyName]
}
})

View file

@ -1,7 +1,7 @@
import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'
// Load mapbox-gl-rtl-text using object urls without needing http://localhost for AJAX.
const data = require("base64?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 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 spec from 'mapbox-gl-style-spec/reference/latest.min.js'
import derefLayers from 'mapbox-gl-style-spec/lib/deref'
import deref from '@mapbox/mapbox-gl-style-spec/deref'
import tokens from '../config/tokens.json'
// Empty style is always used if no style could be restored or fetched
@ -37,7 +36,7 @@ function ensureHasNoInteractive(style) {
function ensureHasNoRefs(style) {
const derefedStyle = {
...style,
layers: derefLayers(style.layers)
layers: deref(style.layers)
}
return derefedStyle
}
@ -55,12 +54,23 @@ function indexOfLayer(layers, layerId) {
return null
}
function replaceAccessToken(mapStyle) {
function replaceAccessToken(mapStyle, opts={}) {
const omtSource = mapStyle.sources.openmaptiles
if(!omtSource) return mapStyle
if(!omtSource.hasOwnProperty("url")) return mapStyle
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 = {
...mapStyle.sources,
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;
}
.mapboxgl-ctrl-zoom {
color: rgb(138, 138, 138);
font-weight: bold;
padding: 4px 8px;
user-select: none;
}
.mapboxgl-ctrl-group {
background: rgb(28, 31, 36);
}

View file

@ -35,15 +35,18 @@
left: 0;
width: 120px;
z-index: 10;
pointer-events: none;
}
}
.maputnik-doc-target:hover .maputnik-doc-popup {
display: block;
text-align: left;
}
// BUTTON
.maputnik-button {
display: inline-block;
cursor: pointer;
background-color: $color-midgray;
color: $color-lowgray;
@ -104,13 +107,17 @@
.maputnik-action-block {
.maputnik-input-block-label {
display: inline-block;
width: 43%;
width: 35%;
}
.maputnik-input-block-action {
vertical-align: top;
display: inline-block;
width: 7%;
width: 15%;
}
.maputnik-input-block-action > div {
text-align: right;
}
}

View file

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

View file

@ -52,6 +52,25 @@
> * {
margin-bottom: $margin-3;
}
.maputnik-array-block {
.maputnik-array-block-action {
vertical-align: top;
display: inline-block;
width: 14%;
}
.maputnik-array-block-content {
vertical-align: top;
display: inline-block;
width: 86%;
}
}
.maputnik-array-add-value {
display: inline-block;
float: right;
}
}
// SELECT
@ -122,7 +141,8 @@
&-menu {
border: none;
padding: 2px 0;
position: fixed;
margin-right: 10px;
position: absolute;
overflow: auto;
max-height: 50%;
background: $color-gray;

View file

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

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 {
min-width: 350px;
max-width: 600px;
overflow: hidden;
background-color: $color-black;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.3);
z-index: 3;
position: relative;
}
.maputnik-modal-section {
@ -19,6 +21,10 @@
flex-shrink: 0;
}
.maputnik-modal-sub-section {
margin-top: $margin-1;
}
.maputnik-modal-section--shrink {
flex-shrink: 1;
}
@ -39,9 +45,13 @@
cursor: pointer;
}
.maputnik-modal-scroller {
max-height: calc(100vh - 35px);
overflow-y: auto;
}
.maputnik-modal-content {
padding: $margin-3;
max-height: 90vh;
@include flex-column;
}
@ -70,6 +80,7 @@
position: fixed;
align-items: center;
justify-content: center;
z-index: 9;
@include flex-row;
}
@ -80,7 +91,6 @@
}
.maputnik-style-gallery-container {
overflow-y: scroll;
flex-shrink: 1;
}

View file

@ -1,6 +1,7 @@
.maputnik-popup-layer {
display: block;
color: $color-lowgray;
cursor: pointer;
user-select: none;
line-height: 1.2;
padding: $margin-2;

View file

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

View file

@ -22,9 +22,8 @@
img {
width: 30px;
height: 30px;
padding-right: $margin-2;
vertical-align: middle;
vertical-align: top;
}
}
@ -37,12 +36,24 @@
cursor: pointer;
color: $color-white;
text-decoration: none;
line-height: 20px;
h1 {
position: relative;
}
&:hover {
background-color: $color-midgray;
}
}
.maputnik-toolbar-version {
position: absolute;
font-size: 10px;
bottom: -2px;
margin-left: 4px;
}
.maputnik-toolbar-action {
@extend .maputnik-toolbar-link;
}
@ -55,3 +66,17 @@
display: inline;
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 {
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 'zoomproperty';
@import 'popup';
@import 'map';

View file

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

View file

@ -9,7 +9,7 @@ describe('maputnik', function() {
browser.waitForExist(".maputnik-toolbar-link");
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');
});
});