Merge remote-tracking branch 'upstream/master' into feature/add-range-slider

This commit is contained in:
orangemug 2019-05-19 06:04:21 +01:00
commit b1d097a40f
30 changed files with 1523 additions and 1468 deletions

View file

@ -49,11 +49,6 @@ templates:
path: /tmp/artifacts path: /tmp/artifacts
destination: /artifacts destination: /artifacts
jobs: jobs:
build-linux-node-v6:
docker:
- image: node:6
working_directory: ~/repo-linux-node-v6
steps: *build-steps
build-linux-node-v8: build-linux-node-v8:
docker: docker:
- image: node:8 - image: node:8
@ -65,13 +60,10 @@ jobs:
- image: node:10 - image: node:10
working_directory: ~/repo-linux-node-v10 working_directory: ~/repo-linux-node-v10
steps: *build-steps steps: *build-steps
build-osx-node-v6: build-linux-node-v11:
macos: docker:
xcode: "9.0" - image: node:11
dependencies: working_directory: ~/repo-linux-node-v11
override:
- brew install node@6
working_directory: ~/repo-osx-node-v6
steps: *build-steps steps: *build-steps
build-osx-node-v8: build-osx-node-v8:
macos: macos:
@ -89,15 +81,22 @@ jobs:
- brew install node@10 - brew install node@10
working_directory: ~/repo-osx-node-v10 working_directory: ~/repo-osx-node-v10
steps: *build-steps steps: *build-steps
build-osx-node-v11:
macos:
xcode: "9.0"
dependencies:
override:
- brew install node@11
working_directory: ~/repo-osx-node-v11
steps: *build-steps
workflows: workflows:
version: 2 version: 2
build: build:
jobs: jobs:
- build-linux-node-v6
- build-linux-node-v8 - build-linux-node-v8
- build-linux-node-v10 - build-linux-node-v10
- build-osx-node-v6 - build-linux-node-v11
- build-osx-node-v8 - build-osx-node-v8
- build-osx-node-v10 - build-osx-node-v10
- build-osx-node-v11

View file

@ -1,4 +1,9 @@
FROM node:10 FROM node:10-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
python \
&& rm -rf /var/lib/apt/lists/*
EXPOSE 8888 EXPOSE 8888
@ -9,7 +14,8 @@ COPY . ${HOME}/
WORKDIR ${HOME} WORKDIR ${HOME}
RUN npm install -d --dev RUN npm install -d
RUN npm run build RUN npm run build
CMD npm run start -- --host 0.0.0.0 WORKDIR ${HOME}/build/build
CMD python -m SimpleHTTPServer 8888

View file

@ -1,18 +1,18 @@
# Maputnik # Maputnik
[![Build Status](https://travis-ci.org/maputnik/editor.svg?branch=master)][travis] [![Build Status](https://circleci.com/gh/maputnik/editor/tree/master.svg?style=shield)][circleci]
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/anelbgv6jdb3qnh9/branch/master?svg=true)][appveyor] [![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] [![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] [![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] [![License](https://img.shields.io/badge/license-MIT-blue.svg)][license]
[travis]: https://travis-ci.org/maputnik/editor [circleci]: https://circleci.com/gh/maputnik/editor/tree/master
[appveyor]: https://ci.appveyor.com/project/lukasmartinelli/editor [appveyor]: https://ci.appveyor.com/project/lukasmartinelli/editor
[dm-prod]: https://david-dm.org/maputnik/editor [dm-prod]: https://david-dm.org/maputnik/editor
[dm-dev]: https://david-dm.org/maputnik/editor#info=devDependencies [dm-dev]: https://david-dm.org/maputnik/editor?type=dev
[license]: https://tldrlegal.com/license/mit-license [license]: https://tldrlegal.com/license/mit-license
<img width="200" align="right" alt="Maputnik" src="src/img/maputnik.png" /> <img width="200" align="right" alt="Maputnik" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/src/img/maputnik.png" />
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/) A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
targeted at developers and map designers. targeted at developers and map designers.
@ -20,7 +20,7 @@ targeted at developers and map designers.
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage) - :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development - :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independance is an OSS map designer. Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer.
## Donations ## Donations
@ -40,10 +40,7 @@ The documentation can be found in the [Wiki](https://github.com/maputnik/editor/
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/). Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
We ensure building and developing Maputnik works with We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
- Linux, OSX and Windows
- Node >4
Install the deps, start the dev server and open the web browser on `http://localhost:8888/`. Install the deps, start the dev server and open the web browser on `http://localhost:8888/`.
@ -54,12 +51,18 @@ npm install
npm start npm start
``` ```
The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the webpack-dev-server docs If you want Maputnik to be accessible externally use the [`--host` option](https://webpack.js.org/configuration/dev-server/#devserverhost):
> 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. ```bash
Snippet from <https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-> # start externally accessible dev server
npm start -- --host 0.0.0.0
```
To enable polling add `export WEBPACK_DEV_SERVER_POLLING=1` to your enviroment. The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the [webpack-dev-server docs](https://webpack.js.org/configuration/dev-server/):
> 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 source](https://webpack.js.org/configuration/dev-server/#devserverwatchoptions-))
To enable polling add `export WEBPACK_DEV_SERVER_POLLING=1` to your environment.
``` ```
npm run build npm run build
@ -79,7 +82,7 @@ For testing we use [webdriverio](http://webdriver.io) and [selenium-standalone](
[selenium-standalone](https://github.com/vvo/selenium-standalone) starts a server that will launch browsers on your local machine. We use chrome so you **must** have chrome installed on your machine. [selenium-standalone](https://github.com/vvo/selenium-standalone) starts a server that will launch browsers on your local machine. We use chrome so you **must** have chrome installed on your machine.
Now open and terminal and run the following. This will install the drivers on your local machine Now open a terminal and run the following. This will install the drivers on your local machine
``` ```
./node_modules/.bin/selenium-standalone install ./node_modules/.bin/selenium-standalone install
@ -115,13 +118,13 @@ Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter
- [Terranodo](http://terranodo.io/) - [Terranodo](http://terranodo.io/)
<a href="https://getwemap.com/"> <a href="https://getwemap.com/">
<img width="33%" alt="Wemap" style="display:inline" src="media/sponsors/wemap.jpg" /> <img width="33%" alt="Wemap" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/wemap.jpg" />
</a> </a>
<a href="http://terranodo.io/"> <a href="http://terranodo.io/">
<img width="33%" alt="Terranodo" style="display:inline" src="media/sponsors/terranodo.png" /> <img width="33%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/terranodo.png" />
</a> </a>
<a href="https://www.orbiconinformatik.dk/"> <a href="https://www.orbiconinformatik.dk/">
<img width="32%" alt="Terranodo" style="display:inline" src="media/sponsors/orbicon_informatik.png" /> <img width="32%" alt="Terranodo" style="display:inline" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/orbicon_informatik.png" />
</a> </a>
<br/> <br/>
@ -133,13 +136,13 @@ Thanks to the supporters of the **[Kickstarter campaign](https://www.kickstarter
- [Dreipol](https://www.dreipol.ch/) - [Dreipol](https://www.dreipol.ch/)
<a href="https://www.klokantech.com/"> <a href="https://www.klokantech.com/">
<img width="18%" alt="Klokan Technologies" style="display:inline-block" src="media/sponsors/klokantech.png" /> <img width="18%" alt="Klokan Technologies" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/klokantech.png" />
</a> </a>
<a href="http://www.geofabrik.de/"> <a href="http://www.geofabrik.de/">
<img width="18%" alt="Geofabrik" style="display:inline-block" src="media/sponsors/geofabrik.png" /> <img width="18%" alt="Geofabrik" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/geofabrik.png" />
</a> </a>
<a href="https://www.dreipol.ch/"> <a href="https://www.dreipol.ch/">
<img width="18%" alt="Dreipol" style="display:inline-block" src="media/sponsors/dreipol.png" /> <img width="18%" alt="Dreipol" style="display:inline-block" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/media/sponsors/dreipol.png" />
</a> </a>
<br/> <br/>

View file

@ -1,18 +1,19 @@
image: Visual Studio 2017 image: Visual Studio 2015
environment: environment:
matrix: matrix:
- nodejs_version: "8" - nodejs_version: "8"
- nodejs_version: "9"
- nodejs_version: "10" - nodejs_version: "10"
- nodejs_version: "11"
platform: platform:
- x86 - x86
- x64 - x64
install: install:
- ps: Install-Product node $env:nodejs_version - ps: Install-Product node $env:nodejs_version
- md public - md public
- npm install --global --production windows-build-tools - npm --vs2015 install --global windows-build-tools
- npm install - npm install
build_script: build_script:
- npm run build - npm run build
test_script: test_script:
- npm run lint - npm run lint
- npm run lint-styles

2487
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@
"test-watch": "cross-env NODE_ENV=test wdio config/wdio.conf.js --watch", "test-watch": "cross-env NODE_ENV=test wdio config/wdio.conf.js --watch",
"start": "webpack-dev-server --progress --profile --colors --config config/webpack.config.js", "start": "webpack-dev-server --progress --profile --colors --config config/webpack.config.js",
"lint": "eslint --ext js --ext jsx src test", "lint": "eslint --ext js --ext jsx src test",
"lint-styles": "stylelint 'src/styles/*.scss'" "lint-styles": "stylelint \"src/styles/*.scss\""
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -22,7 +22,7 @@
"dependencies": { "dependencies": {
"@babel/runtime": "^7.1.2", "@babel/runtime": "^7.1.2",
"@mapbox/mapbox-gl-rtl-text": "^0.2.1", "@mapbox/mapbox-gl-rtl-text": "^0.2.1",
"@mapbox/mapbox-gl-style-spec": "^13.3.0", "@mapbox/mapbox-gl-style-spec": "^13.6.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"codemirror": "^5.40.2", "codemirror": "^5.40.2",
"color": "^3.0.0", "color": "^3.0.0",
@ -33,7 +33,7 @@
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"mapbox-gl": "^0.50.0-beta.1", "mapbox-gl": "^0.53.1",
"mapbox-gl-inspect": "^1.3.1", "mapbox-gl-inspect": "^1.3.1",
"maputnik-design": "github:maputnik/design", "maputnik-design": "github:maputnik/design",
"ol": "^5.2.0", "ol": "^5.2.0",
@ -121,18 +121,18 @@
"istanbul-lib-coverage": "^2.0.1", "istanbul-lib-coverage": "^2.0.1",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"node-sass": "^4.9.3", "node-sass": "^4.10.0",
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
"react-hot-loader": "^4.3.11", "react-hot-loader": "^4.3.11",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"selenium-standalone": "^6.15.3", "selenium-standalone": "^6.15.3",
"style-loader": "^0.23.0", "style-loader": "^0.23.0",
"stylelint": "^9.6.0", "stylelint": "^10.0.0",
"stylelint-config-recommended-scss": "^3.2.0", "stylelint-config-recommended-scss": "^3.2.0",
"stylelint-scss": "^3.3.1", "stylelint-scss": "^3.5.4",
"transform-loader": "^0.2.4", "transform-loader": "^0.2.4",
"uuid": "^3.3.2", "uuid": "^3.3.2",
"wdio-mocha-framework": "^0.6.3", "wdio-mocha-framework": "^0.6.4",
"wdio-selenium-standalone-service": "0.0.10", "wdio-selenium-standalone-service": "0.0.10",
"wdio-spec-reporter": "^0.1.5", "wdio-spec-reporter": "^0.1.5",
"webdriverio": "^4.13.2", "webdriverio": "^4.13.2",

View file

@ -49,6 +49,26 @@ function normalizeSourceURL (url, apiToken="") {
} }
} }
function setFetchAccessToken(url, mapStyle) {
const matchesTilehosting = url.match(/\.tilehosting\.com/);
const matchesMaptiler = url.match(/\.maptiler\.com/);
const matchesThunderforest = url.match(/\.thunderforest\.com/);
if (matchesTilehosting || matchesMaptiler) {
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true})
if (accessToken) {
return url.replace('{key}', accessToken)
}
}
else if (matchesThunderforest) {
const accessToken = style.getAccessToken("thunderforest", mapStyle, {allowFallback: true})
if (accessToken) {
return url.replace('{key}', accessToken)
}
}
else {
return url;
}
}
function updateRootSpec(spec, fieldName, newValues) { function updateRootSpec(spec, fieldName, newValues) {
return { return {
@ -73,57 +93,48 @@ export default class App extends React.Component {
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false) onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false)
}) })
const keyCodes = {
"esc": 27,
"?": 191,
"o": 79,
"e": 69,
"s": 83,
"d": 68,
"i": 73,
"m": 77,
}
const shortcuts = [ const shortcuts = [
{ {
keyCode: keyCodes["?"], key: "?",
handler: () => { handler: () => {
this.toggleModal("shortcuts"); this.toggleModal("shortcuts");
} }
}, },
{ {
keyCode: keyCodes["o"], key: "o",
handler: () => { handler: () => {
this.toggleModal("open"); this.toggleModal("open");
} }
}, },
{ {
keyCode: keyCodes["e"], key: "e",
handler: () => { handler: () => {
this.toggleModal("export"); this.toggleModal("export");
} }
}, },
{ {
keyCode: keyCodes["d"], key: "d",
handler: () => { handler: () => {
this.toggleModal("sources"); this.toggleModal("sources");
} }
}, },
{ {
keyCode: keyCodes["s"], key: "s",
handler: () => { handler: () => {
this.toggleModal("settings"); this.toggleModal("settings");
} }
}, },
{ {
keyCode: keyCodes["i"], key: "i",
handler: () => { handler: () => {
this.setMapState("inspect"); this.setMapState(
this.state.mapState === "map" ? "inspect" : "map"
);
} }
}, },
{ {
keyCode: keyCodes["m"], key: "m",
handler: () => { handler: () => {
document.querySelector(".mapboxgl-canvas").focus(); document.querySelector(".mapboxgl-canvas").focus();
} }
@ -131,16 +142,17 @@ export default class App extends React.Component {
] ]
document.body.addEventListener("keyup", (e) => { document.body.addEventListener("keyup", (e) => {
if(e.keyCode === keyCodes["esc"]) { if(e.key === "Escape") {
e.target.blur(); e.target.blur();
document.body.focus(); document.body.focus();
} }
else if(document.activeElement === document.body) { else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
const shortcut = shortcuts.find((shortcut) => { const shortcut = shortcuts.find((shortcut) => {
return (shortcut.keyCode === e.keyCode) return (shortcut.key === e.key)
}) })
if(shortcut) { if(shortcut) {
this.setModal("shortcuts", false);
shortcut.handler(e); shortcut.handler(e);
} }
} }
@ -195,7 +207,8 @@ export default class App extends React.Component {
}, },
mapOptions: { mapOptions: {
showTileBoundaries: queryUtil.asBool(queryObj, "show-tile-boundaries"), showTileBoundaries: queryUtil.asBool(queryObj, "show-tile-boundaries"),
showCollisionBoxes: queryUtil.asBool(queryObj, "show-collision-boxes") showCollisionBoxes: queryUtil.asBool(queryObj, "show-collision-boxes"),
showOverdrawInspector: queryUtil.asBool(queryObj, "show-overdraw-inspector")
}, },
} }
@ -405,6 +418,12 @@ export default class App extends React.Component {
console.warn("Failed to normalizeSourceURL: ", err); console.warn("Failed to normalizeSourceURL: ", err);
} }
try {
url = setFetchAccessToken(url, this.state.mapStyle)
} catch(err) {
console.warn("Failed to setFetchAccessToken: ", err);
}
fetch(url, { fetch(url, {
mode: 'cors', mode: 'cors',
}) })
@ -488,17 +507,21 @@ export default class App extends React.Component {
this.setState({ selectedLayerIndex: idx }) this.setState({ selectedLayerIndex: idx })
} }
toggleModal(modalName) { setModal(modalName, value) {
if(modalName === 'survey' && value === false) {
localStorage.setItem('survey', '');
}
this.setState({ this.setState({
isOpen: { isOpen: {
...this.state.isOpen, ...this.state.isOpen,
[modalName]: !this.state.isOpen[modalName] [modalName]: value
} }
}) })
}
if(modalName === 'survey') { toggleModal(modalName) {
localStorage.setItem('survey', ''); this.setModal(modalName, !this.state.isOpen[modalName]);
}
} }
render() { render() {
@ -553,6 +576,7 @@ export default class App extends React.Component {
const modals = <div> const modals = <div>
<ShortcutsModal <ShortcutsModal
ref={(el) => this.shortcutEl = el}
isOpen={this.state.isOpen.shortcuts} isOpen={this.state.isOpen.shortcuts}
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')} onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
/> />

View file

@ -198,7 +198,7 @@ export default class Toolbar extends React.Component {
<ToolbarSelect wdKey="nav:inspect"> <ToolbarSelect wdKey="nav:inspect">
<MdFindInPage /> <MdFindInPage />
<IconText>View </IconText> <IconText>View </IconText>
<select onChange={(e) => this.handleSelection(e.target.value)}> <select onChange={(e) => this.handleSelection(e.target.value)} value={currentView.id}>
{views.map((item) => { {views.map((item) => {
return ( return (
<option key={item.id} value={item.id}> <option key={item.id} value={item.id}>

View file

@ -81,6 +81,7 @@ export default class SpecField extends React.Component {
options={options} options={options}
/> />
} }
case 'formatted':
case 'string': case 'string':
if(iconProperties.indexOf(this.props.fieldName) >= 0) { if(iconProperties.indexOf(this.props.fieldName) >= 0) {
return <IconInput return <IconInput

View file

@ -54,7 +54,8 @@ class AutocompleteInput extends React.Component {
menuStyle={{ menuStyle={{
position: "fixed", position: "fixed",
overflow: "auto", overflow: "auto",
maxHeight: this.state.maxHeight maxHeight: this.state.maxHeight,
zIndex: '998'
}} }}
wrapperProps={{ wrapperProps={{
className: "maputnik-autocomplete", className: "maputnik-autocomplete",

View file

@ -5,7 +5,7 @@ import AutocompleteInput from './AutocompleteInput'
class IconInput extends React.Component { class IconInput extends React.Component {
static propTypes = { static propTypes = {
value: PropTypes.array, value: PropTypes.string,
icons: PropTypes.array, icons: PropTypes.array,
style: PropTypes.object, style: PropTypes.object,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,

View file

@ -29,20 +29,11 @@ class JSONEditor extends React.Component {
} }
} }
static getDerivedStateFromProps(props, state) { componentDidUpdate(prevProps) {
return { if (prevProps.layer !== this.props.layer) {
code: JSON.stringify(props.layer, null, 2) this.setState({
}; code: JSON.stringify(this.props.layer, null, 2)
} })
shouldComponentUpdate(nextProps, nextState) {
try {
const parsedLayer = JSON.parse(this.state.code)
// If the structure is still the same do not update
// because it affects editing experience by reformatting all the time
return nextState.code !== JSON.stringify(parsedLayer, null, 2)
} catch(err) {
return true
} }
} }

View file

@ -46,7 +46,7 @@ class LayerListContainer extends React.Component {
areAllGroupsExpanded: false, areAllGroupsExpanded: false,
isOpen: { isOpen: {
add: false, add: false,
} }
} }
toggleModal(modalName) { toggleModal(modalName) {
@ -66,12 +66,12 @@ class LayerListContainer extends React.Component {
this.groupedLayers().forEach(layers => { this.groupedLayers().forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id) const groupPrefix = layerPrefix(layers[0].id)
const lookupKey = [groupPrefix, idx].join('-') const lookupKey = [groupPrefix, idx].join('-')
if (layers.length > 1) { if (layers.length > 1) {
newGroups[lookupKey] = this.state.areAllGroupsExpanded newGroups[lookupKey] = this.state.areAllGroupsExpanded
} }
layers.forEach((layer) => { layers.forEach((layer) => {
idx += 1 idx += 1
}) })
@ -204,6 +204,7 @@ export default class LayerList extends React.Component {
render() { render() {
return <LayerListContainerSortable return <LayerListContainerSortable
{...this.props} {...this.props}
helperClass='sortableHelper'
onSortEnd={this.props.onMoveLayer.bind(this)} onSortEnd={this.props.onMoveLayer.bind(this)}
useDragHandle={true} useDragHandle={true}
/> />

View file

@ -7,23 +7,16 @@ import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icon
import LayerIcon from '../icons/LayerIcon' import LayerIcon from '../icons/LayerIcon'
import {SortableElement, SortableHandle} from 'react-sortable-hoc' import {SortableElement, SortableHandle} from 'react-sortable-hoc'
class LayerTypeDragHandle extends React.Component {
static propTypes = LayerIcon.propTypes
render() { const DraggableLabel = SortableHandle((props) => {
return <LayerIcon return <div className="maputnik-layer-list-item-handle">
{...this.props} <LayerIcon
style={{ className="layer-handle__icon"
cursor: 'move', type={props.layerType}
width: 14,
height: 14,
paddingRight: 3,
}}
/> />
} <span className="maputnik-layer-list-item-id">{props.layerId}</span>
} </div>
});
const LayerTypeDragHandleSortable = SortableHandle((props) => <LayerTypeDragHandle {...props} />)
class IconAction extends React.Component { class IconAction extends React.Component {
static propTypes = { static propTypes = {
@ -111,8 +104,7 @@ class LayerListItem extends React.Component {
"maputnik-layer-list-item-selected": this.props.isSelected, "maputnik-layer-list-item-selected": this.props.isSelected,
[this.props.className]: true, [this.props.className]: true,
})}> })}>
<LayerTypeDragHandleSortable type={this.props.layerType} /> <DraggableLabel {...this.props} />
<span className="maputnik-layer-list-item-id">{this.props.layerId}</span>
<span style={{flexGrow: 1}} /> <span style={{flexGrow: 1}} />
<IconAction <IconAction
wdKey={"layer-list-item:"+this.props.layerId+":delete"} wdKey={"layer-list-item:"+this.props.layerId+":delete"}

View file

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import LayerIcon from '../icons/LayerIcon' import LayerIcon from '../icons/LayerIcon'
import {latest, expression, function as styleFunction} from '@mapbox/mapbox-gl-style-spec'
function groupFeaturesBySourceLayer(features) { function groupFeaturesBySourceLayer(features) {
const sources = {} const sources = {}
@ -28,7 +29,59 @@ function groupFeaturesBySourceLayer(features) {
class FeatureLayerPopup extends React.Component { class FeatureLayerPopup extends React.Component {
static propTypes = { static propTypes = {
onLayerSelect: PropTypes.func.isRequired, onLayerSelect: PropTypes.func.isRequired,
features: PropTypes.array features: PropTypes.array,
zoom: PropTypes.number,
}
_getFeatureColor(feature, zoom) {
try {
const paintProps = feature.layer.paint;
let propName;
if(paintProps.hasOwnProperty("text-color") && paintProps["text-color"]) {
propName = "text-color";
}
else if (paintProps.hasOwnProperty("fill-color") && paintProps["fill-color"]) {
propName = "fill-color";
}
else if (paintProps.hasOwnProperty("line-color") && paintProps["line-color"]) {
propName = "line-color";
}
else if (paintProps.hasOwnProperty("fill-extrusion-color") && paintProps["fill-extrusion-color"]) {
propName = "fill-extrusion-color";
}
if(propName) {
const propertySpec = latest["paint_"+feature.layer.type][propName];
let color = feature.layer.paint[propName];
if(typeof(color) === "object") {
if(color.stops) {
color = styleFunction.convertFunction(color, propertySpec);
}
const exprResult = expression.createExpression(color, propertySpec);
const val = exprResult.value.evaluate({
zoom: zoom
}, feature);
return val.toString();
}
else {
return color;
}
}
else {
// Default color
return "black";
}
}
// This is quite complex, just incase there's an edgecase we're missing
// always return black if we get an unexpected error.
catch (err) {
console.error("Unable to get feature color, error:", err);
return "black";
}
} }
render() { render() {
@ -36,21 +89,31 @@ class FeatureLayerPopup extends React.Component {
const items = Object.keys(sources).map(vectorLayerId => { const items = Object.keys(sources).map(vectorLayerId => {
const layers = sources[vectorLayerId].map((feature, idx) => { const layers = sources[vectorLayerId].map((feature, idx) => {
return <label const featureColor = this._getFeatureColor(feature, this.props.zoom);
key={idx}
className="maputnik-popup-layer" return <div
onClick={() => { key={idx}
this.props.onLayerSelect(feature.layer.id) className="maputnik-popup-layer"
}}
> >
<LayerIcon type={feature.layer.type} style={{ <div
width: 14, className="maputnik-popup-layer__swatch"
height: 14, style={{background: featureColor}}
paddingRight: 3 ></div>
}}/> <label
{feature.layer.id} className="maputnik-popup-layer__label"
{feature.counter && <span> × {feature.counter}</span>} onClick={() => {
</label> this.props.onLayerSelect(feature.layer.id)
}}
>
<LayerIcon type={feature.layer.type} style={{
width: 14,
height: 14,
paddingRight: 3
}}/>
{feature.layer.id}
{feature.counter && <span> × {feature.counter}</span>}
</label>
</div>
}) })
return <div key={vectorLayerId}> return <div key={vectorLayerId}>
<div className="maputnik-popup-layer-id">{vectorLayerId}</div> <div className="maputnik-popup-layer-id">{vectorLayerId}</div>

View file

@ -22,7 +22,7 @@ function renderProperties(feature) {
} }
function renderFeature(feature) { function renderFeature(feature) {
return <div key={feature.id}> return <div key={`${feature.sourceLayer}-${feature.id}`}>
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div> <div className="maputnik-popup-layer-id">{feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
<InputBlock key={"property-type"} label={"$type"}> <InputBlock key={"property-type"} label={"$type"}>
<StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} /> <StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
@ -43,7 +43,7 @@ function removeDuplicatedFeatures(features) {
if(featureIndex === -1) { if(featureIndex === -1) {
uniqueFeatures.push(feature) uniqueFeatures.push(feature)
} else { } else {
if(uniqueFeatures[featureIndex].hasOwnProperty('counter')) { if(uniqueFeatures[featureIndex].hasOwnProperty('inspectModeCounter')) {
uniqueFeatures[featureIndex].inspectModeCounter++ uniqueFeatures[featureIndex].inspectModeCounter++
} else { } else {
uniqueFeatures[featureIndex].inspectModeCounter = 2 uniqueFeatures[featureIndex].inspectModeCounter = 2

View file

@ -17,10 +17,10 @@ import '../../libs/mapbox-rtl'
const IS_SUPPORTED = MapboxGl.supported(); const IS_SUPPORTED = MapboxGl.supported();
function renderPropertyPopup(features) { function renderPopup(popup, mountNode) {
var mountNode = document.createElement('div'); ReactDOM.render(popup, mountNode);
ReactDOM.render(<FeaturePropertyPopup features={features} />, mountNode) var content = mountNode.innerHTML;
return mountNode.innerHTML; return content;
} }
function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) { function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
@ -77,9 +77,6 @@ export default class MapboxGlMap extends React.Component {
this.state = { this.state = {
map: null, map: null,
inspect: null, inspect: null,
isPopupOpen: false,
popupX: 0,
popupY: 0,
} }
} }
@ -97,6 +94,16 @@ export default class MapboxGlMap extends React.Component {
} }
} }
shouldComponentUpdate(nextProps, nextState) {
let should = false;
try {
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
} catch(e) {
// no biggie, carry on
}
return should;
}
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if(!IS_SUPPORTED) return; if(!IS_SUPPORTED) return;
@ -114,6 +121,7 @@ export default class MapboxGlMap extends React.Component {
if (map) { if (map) {
map.showTileBoundaries = this.props.options.showTileBoundaries; map.showTileBoundaries = this.props.options.showTileBoundaries;
map.showCollisionBoxes = this.props.options.showCollisionBoxes; map.showCollisionBoxes = this.props.options.showCollisionBoxes;
map.showOverdrawInspector = this.props.options.showOverdrawInspector;
} }
} }
@ -131,6 +139,7 @@ export default class MapboxGlMap extends React.Component {
map.showTileBoundaries = mapOpts.showTileBoundaries; map.showTileBoundaries = mapOpts.showTileBoundaries;
map.showCollisionBoxes = mapOpts.showCollisionBoxes; map.showCollisionBoxes = mapOpts.showCollisionBoxes;
map.showOverdrawInspector = mapOpts.showOverdrawInspector;
const zoom = new ZoomControl; const zoom = new ZoomControl;
map.addControl(zoom, 'top-right'); map.addControl(zoom, 'top-right');
@ -138,6 +147,8 @@ export default class MapboxGlMap extends React.Component {
const nav = new MapboxGl.NavigationControl(); const nav = new MapboxGl.NavigationControl();
map.addControl(nav, 'top-right'); map.addControl(nav, 'top-right');
const tmpNode = document.createElement('div');
const inspect = new MapboxInspect({ const inspect = new MapboxInspect({
popup: new MapboxGl.Popup({ popup: new MapboxGl.Popup({
closeOnClick: false closeOnClick: false
@ -153,18 +164,23 @@ export default class MapboxGlMap extends React.Component {
buildInspectStyle: (originalMapStyle, coloredLayers) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer), buildInspectStyle: (originalMapStyle, coloredLayers) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
renderPopup: features => { renderPopup: features => {
if(this.props.inspectModeEnabled) { if(this.props.inspectModeEnabled) {
return renderPropertyPopup(features) return renderPopup(<FeaturePropertyPopup features={features} />, tmpNode);
} else { } else {
var mountNode = document.createElement('div'); return renderPopup(<FeatureLayerPopup features={features} onLayerSelect={this.props.onLayerSelect} zoom={this.state.zoom} />, tmpNode);
ReactDOM.render(<FeatureLayerPopup features={features} onLayerSelect={this.props.onLayerSelect} />, mountNode)
return mountNode
} }
} }
}) })
map.addControl(inspect) map.addControl(inspect)
map.on("style.load", () => { map.on("style.load", () => {
this.setState({ map, inspect }); this.setState({
map,
inspect,
zoom: map.getZoom()
});
if(this.props.inspectModeEnabled) {
inspect.toggleInspector();
}
}) })
map.on("data", e => { map.on("data", e => {
@ -173,6 +189,12 @@ export default class MapboxGlMap extends React.Component {
map: this.state.map map: this.state.map
}) })
}) })
map.on("zoom", e => {
this.setState({
zoom: map.getZoom()
});
})
} }
render() { render() {

View file

@ -2,25 +2,25 @@
{ {
"id": "klokantech-basic", "id": "klokantech-basic",
"title": "Klokantech Basic", "title": "Klokantech Basic",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@v1.7/style.json", "url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@v1.8/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png" "thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png"
}, },
{ {
"id": "dark-matter", "id": "dark-matter",
"title": "Dark Matter", "title": "Dark Matter",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.6/style.json", "url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.7/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png" "thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
}, },
{ {
"id": "positron", "id": "positron",
"title": "Positron", "title": "Positron",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/positron-gl-style@v1.6/style.json", "url": "https://cdn.jsdelivr.net/gh/openmaptiles/positron-gl-style@v1.7/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png" "thumbnail": "https://maputnik.github.io/thumbnails/positron.png"
}, },
{ {
"id": "osm-bright", "id": "osm-bright",
"title": "OSM Bright", "title": "OSM Bright",
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@v1.7/style.json", "url": "https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@v1.8/style.json",
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png" "thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png"
}, },
{ {
@ -32,7 +32,7 @@
{ {
"id": "empty-style", "id": "empty-style",
"title": "Empty Style", "title": "Empty Style",
"url": "https://cdn.jsdelivr.net/gh/maputnik/editor@v1.5.0/src/config/empty-style.json", "url": "https://cdn.jsdelivr.net/gh/maputnik/editor@9cf74ca405d2be0608b57db8109cf3a6af5b9f49/src/config/empty-style.json",
"thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII=" "thumbnail": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAQAAAAHDYbIAAAAEUlEQVR42mP8/58BDhiJ4wAA974H/U5Xe1oAAAAASUVORK5CYII="
}, },
{ {

View file

@ -1,7 +1,7 @@
{ {
"openmaptiles": { "openmaptiles": {
"type": "vector", "type": "vector",
"url": "https://maps.tilehosting.com/data/v3.json?key={key}", "url": "https://api.maptiler.com/tiles/v3/tiles.json?key={key}",
"title": "OpenMapTiles" "title": "OpenMapTiles"
}, },
"thunderforest_transport": { "thunderforest_transport": {

View file

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

View file

@ -101,7 +101,7 @@ function replaceAccessTokens(mapStyle, opts={}) {
changedStyle = replaceSourceAccessToken(changedStyle, sourceName, opts); changedStyle = replaceSourceAccessToken(changedStyle, sourceName, opts);
}) })
if (mapStyle.glyphs && mapStyle.glyphs.match(/\.tilehosting\.com/)) { if (mapStyle.glyphs && (mapStyle.glyphs.match(/\.tilehosting\.com/) || mapStyle.glyphs.match(/\.maptiler\.com/))) {
const newAccessToken = getAccessToken("openmaptiles", mapStyle, opts); const newAccessToken = getAccessToken("openmaptiles", mapStyle, opts);
if (newAccessToken) { if (newAccessToken) {
changedStyle = { changedStyle = {
@ -119,5 +119,6 @@ export default {
emptyStyle, emptyStyle,
indexOfLayer, indexOfLayer,
generateId, generateId,
getAccessToken,
replaceAccessTokens, replaceAccessTokens,
} }

View file

@ -14,6 +14,22 @@
border-top-color: rgb(28, 31, 36); border-top-color: rgb(28, 31, 36);
} }
.mapboxgl-popup-anchor-top-left .mapboxgl-popup-tip {
border-bottom-color: rgb(28, 31, 36);
}
.mapboxgl-popup-anchor-top-right .mapboxgl-popup-tip {
border-bottom-color: rgb(28, 31, 36);
}
.mapboxgl-popup-anchor-bottom-left .mapboxgl-popup-tip {
border-top-color: rgb(28, 31, 36);
}
.mapboxgl-popup-anchor-bottom-right .mapboxgl-popup-tip {
border-top-color: rgb(28, 31, 36);
}
.mapboxgl-popup-content { .mapboxgl-popup-content {
background-color: rgb(28, 31, 36); background-color: rgb(28, 31, 36);
border-radius: 0px; border-radius: 0px;

View file

@ -3,6 +3,7 @@
src: url('../fonts/Roboto-Regular.ttf') format('truetype'); src: url('../fonts/Roboto-Regular.ttf') format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: swap;
} }
@font-face { @font-face {
@ -10,6 +11,7 @@
src: url('../fonts/Roboto-Medium.ttf') format('truetype'); src: url('../fonts/Roboto-Medium.ttf') format('truetype');
font-weight: bold; font-weight: bold;
font-style: normal; font-style: normal;
font-display: swap;
} }
html { html {

View file

@ -24,6 +24,15 @@
padding-bottom: $margin-5; padding-bottom: $margin-5;
} }
&-item-handle {
flex: 1;
display: flex;
svg {
margin-right: 4px;
}
}
&-item { &-item {
font-weight: 400; font-weight: 400;
color: $color-lowgray; color: $color-lowgray;
@ -36,7 +45,7 @@
z-index: 2000; z-index: 2000;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
padding: 5px 10px; padding: 5px;
line-height: 1.3; line-height: 1.3;
max-height: 50px; max-height: 50px;
opacity: 1; opacity: 1;
@ -62,6 +71,7 @@
background: initial; background: initial;
border: none; border: none;
padding: 0 2px; padding: 0 2px;
height: 15px;
svg { svg {
fill: darken($color-lowgray, 20); fill: darken($color-lowgray, 20);
@ -111,6 +121,8 @@
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
color: inherit;
text-decoration: none;
} }
&-group-header { &-group-header {
@ -160,7 +172,7 @@
// PROPERTY // PROPERTY
.maputnik-default-property { .maputnik-default-property {
.maputnik-input-block-label { .maputnik-input-block-label {
color: darken($color-lowgray, 25%); color: darken($color-lowgray, 20%);
} }
.maputnik-string, .maputnik-string,
@ -169,7 +181,7 @@
.maputnik-select, .maputnik-select,
.maputnik-checkbox-wrapper { .maputnik-checkbox-wrapper {
background-color: darken($color-gray, 2%); background-color: darken($color-gray, 2%);
color: darken($color-lowgray, 25%); color: darken($color-lowgray, 20%);
} }
.maputnik-make-zoom-function svg { .maputnik-make-zoom-function svg {
@ -178,7 +190,7 @@
.maputnik-multibutton .maputnik-button { .maputnik-multibutton .maputnik-button {
background-color: darken($color-midgray, 10%); background-color: darken($color-midgray, 10%);
color: darken($color-lowgray, 25%); color: darken($color-lowgray, 20%);
&:hover { &:hover {
background-color: lighten($color-midgray, 12); background-color: lighten($color-midgray, 12);
@ -195,6 +207,11 @@
.more-menu { .more-menu {
position: relative; position: relative;
svg {
width: 22px;
height: 22px;
}
&__menu { &__menu {
position: absolute; position: absolute;
z-index: 9999; z-index: 9999;
@ -229,3 +246,10 @@
min-width: 28px; min-width: 28px;
} }
} }
// Clone of the element which is sorted
.sortableHelper {
font-family: $font-family;
z-index: 9999;
border: none;
}

View file

@ -247,12 +247,13 @@
} }
.maputnik-modal-survey { .maputnik-modal-survey {
width: 372px; width: 400px;
} }
.maputnik-modal-survey__logo { .maputnik-modal-survey__logo {
display: block; display: block;
margin: 0 auto; margin: 0 auto;
height: 128px;
} }
.maputnik-modal-survey__description { .maputnik-modal-survey__description {

View file

@ -1,4 +1,15 @@
.maputnik-popup-layer { .maputnik-popup-layer {
display: flex;
flex-direction: row;
}
.maputnik-popup-layer__swatch {
display: inline-block;
width: 5px;
align-content: stretch;
}
.maputnik-popup-layer__label {
display: block; display: block;
color: $color-lowgray; color: $color-lowgray;
cursor: pointer; cursor: pointer;

View file

@ -12,4 +12,8 @@ div:not(.maputnik-toolbar__actions) {
padding-left: 2px; padding-left: 2px;
padding-right: 2px; padding-right: 2px;
} }
// Styling for Firefox
scrollbar-width: thin;
scrollbar-color: #666 #26282e;
} }

View file

@ -105,6 +105,7 @@
// HACK: <https://github.com/maputnik/editor/pull/392#issuecomment-427595172> // HACK: <https://github.com/maputnik/editor/pull/392#issuecomment-427595172>
color: $color-black !important; color: $color-black !important;
margin-left: 4px; margin-left: 4px;
border-width: 0;
option { option {
// HACK: <https://github.com/maputnik/editor/pull/392#issuecomment-427595172> // HACK: <https://github.com/maputnik/editor/pull/392#issuecomment-427595172>

View file

@ -1,7 +1,7 @@
$color-black: #1c1f24; $color-black: #191b20;
$color-gray: #26282e; $color-gray: #222429;
$color-midgray: #36383e; $color-midgray: #303237;
$color-lowgray: #8e8e8e; $color-lowgray: #a4a4a4;
$color-white: #f0f0f0; $color-white: #f0f0f0;
$color-red: #cf4a4a; $color-red: #cf4a4a;
$color-green: #53b972; $color-green: #53b972;

View file

@ -162,7 +162,7 @@ describe("modals", function() {
}) })
}) })
it("open map tiles access token", function() { it("maptiler access token", function() {
var apiKey = "testing123"; var apiKey = "testing123";
browser.setValueSafe(wd.$("modal-settings.maputnik:openmaptiles_access_token"), apiKey); browser.setValueSafe(wd.$("modal-settings.maputnik:openmaptiles_access_token"), apiKey);
browser.click(wd.$("modal-settings.name")) browser.click(wd.$("modal-settings.name"))
@ -172,14 +172,24 @@ describe("modals", function() {
assert.equal(styleObj.metadata["maputnik:openmaptiles_access_token"], apiKey); assert.equal(styleObj.metadata["maputnik:openmaptiles_access_token"], apiKey);
}) })
it.skip("style renderer", function() { it("thunderforest access token", function() {
var selector = wd.$("modal-settings.maputnik:renderer"); var apiKey = "testing123";
browser.selectByValue(selector, "ol3"); browser.setValueSafe(wd.$("modal-settings.maputnik:thunderforest_access_token"), apiKey);
browser.click(wd.$("modal-settings.name")) browser.click(wd.$("modal-settings.name"))
browser.flushReactUpdates(); browser.flushReactUpdates();
var styleObj = helper.getStyleStore(browser); var styleObj = helper.getStyleStore(browser);
assert.equal(styleObj.metadata["maputnik:renderer"], "ol3"); assert.equal(styleObj.metadata["maputnik:thunderforest_access_token"], apiKey);
})
it("style renderer", function() {
var selector = wd.$("modal-settings.maputnik:renderer");
browser.selectByValue(selector, "ol");
browser.click(wd.$("modal-settings.name"))
browser.flushReactUpdates();
var styleObj = helper.getStyleStore(browser);
assert.equal(styleObj.metadata["maputnik:renderer"], "ol");
}) })
}) })