diff --git a/CHANGELOG.md b/CHANGELOG.md
index c0b0232..c62d0f5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
+## [2.0.0-beta.1] - 2019-02-07
+
+### Changed
+
+- [BREAKING] If using a map style hosted by Mapbox you must pass a [valid Mapbox
+ Access Token](https://docs.mapbox.com/help/how-mapbox-works/access-tokens/)
+ with the prop `mapboxToken`.
+- [BREAKING] The `mapPosition` and `onChangeMapPosition` props have been
+ removed, and replaced by `mapViewState` and `onChangeMapViewState`.
+
## [1.1.0] - 2018-06-21
### Added
diff --git a/example/index.html b/example/index.html
index 7070939..86e73bb 100644
--- a/example/index.html
+++ b/example/index.html
@@ -27,6 +27,11 @@
padding: 0;
overflow: hidden;
}
+ #root {
+ width: 100vw;
+ height: 100vh;
+ position: absolute;
+ }
diff --git a/example/index.js b/example/index.js
index db857ff..aba203c 100644
--- a/example/index.js
+++ b/example/index.js
@@ -1,12 +1,14 @@
const React = require('react')
const ReactDOM = require('react-dom')
-const MapFilter = require('../src/index.js').default
+const MapView = require('../src/components/MapView').default
const createHistory = require('history').createBrowserHistory
const { MuiThemeProvider, createMuiTheme } = require('@material-ui/core/styles')
const blue = require('@material-ui/core/colors/blue').default
const pink = require('@material-ui/core/colors/pink').default
-const mapboxgl = require('mapbox-gl')
const MenuItem = require('@material-ui/core/MenuItem').default
+const { randomPoint } = require('@turf/random')
+const {LinearInterpolator, FlyToInterpolator} = require('react-map-gl')
+const d3 = require('d3-ease')
const theme = createMuiTheme({
palette: {
@@ -15,6 +17,8 @@ const theme = createMuiTheme({
}
})
+const MAPBOX_TOKEN = require('../config.json').mapboxToken
+
const features = require('./sample.json').features
const history = createHistory()
@@ -60,10 +64,25 @@ class Example extends React.Component {
this.unlisten = history.listen(this.handleHistoryChange)
this.state = {
ui: uiFromPath(history.location.pathname),
- features: features,
- mapPosition: {center: [-59.43943162023362, 2.6563784112334616], zoom: 10}
+ features: features
}
}
+ componentDidMount () {
+ this.intervalId = setInterval(() => {
+ const coords = randomPoint(1, {bbox: [-76.65, -16.96, -51.92, 6.64]}).features[0].geometry.coordinates
+ console.log('fly to', coords)
+ console.log('from', this.state.mapViewState)
+ this.setState(state => ({
+ mapViewState: Object.assign({}, this.state.mapViewState, {
+ longitude: coords[0],
+ latitude: coords[1],
+ zoom: 7 + (Math.random() * 5),
+ transitionInterpolator: new FlyToInterpolator(),
+ transitionDuration: 2000
+ })
+ }))
+ }, 5000)
+ }
handleHistoryChange = (location, action) => {
if (action === 'POP') {
this.setState({ui: uiFromPath(location.pathname)})
@@ -77,22 +96,17 @@ class Example extends React.Component {
handleChangeFeatures = (_) => {
this.setState({features: _})
}
- handleChangeMapPosition = (pos) => {
- this.setState({mapPosition: pos})
+ handleChangeMapViewState = (pos) => {
+ this.setState({mapViewState: pos})
}
render () {
return
-
+
}
}
diff --git a/example/prev-index.js b/example/prev-index.js
new file mode 100644
index 0000000..43957e8
--- /dev/null
+++ b/example/prev-index.js
@@ -0,0 +1,122 @@
+const React = require('react')
+const ReactDOM = require('react-dom')
+const MapFilter = require('../src/index.js').default
+const createHistory = require('history').createBrowserHistory
+const { MuiThemeProvider, createMuiTheme } = require('@material-ui/core/styles')
+const blue = require('@material-ui/core/colors/blue').default
+const pink = require('@material-ui/core/colors/pink').default
+const MenuItem = require('@material-ui/core/MenuItem').default
+const { randomPoint } = require('@turf/random')
+const {LinearInterpolator, FlyToInterpolator} = require('react-map-gl')
+const d3 = require('d3-ease')
+
+const theme = createMuiTheme({
+ palette: {
+ primary: blue,
+ secondary: pink
+ }
+})
+
+const fieldOrder = {caption: 1, public: 0}
+
+const MAPBOX_TOKEN = require('../config.json').mapboxToken
+
+const features = require('./sample.json').features
+
+const history = createHistory()
+
+const pathRegExp = /^(?:\/((?:[^/]+?)))?(?:\/((?:[^/]+?)))?(?:\/((?:[^/]+?)))?(?:\/(?=$))?$/
+
+function uiFromPath (path) {
+ const match = pathRegExp.exec(path)
+ if (!match) return {}
+ return {
+ activeView: match[1],
+ activeModal: match[2] && match[2].replace('features', 'feature'),
+ settingsTab: match[2] === 'settings' && match[3],
+ featureId: match[2] === 'features' && match[3]
+ }
+}
+
+function pathFromUi (ui) {
+ let path = '/'
+ if (ui.activeView) path += ui.activeView + '/'
+ if (ui.activeModal === 'settings') path += 'settings/' + ui.settingsTab + '/'
+ if (ui.activeModal === 'feature') path += 'features/' + ui.featureId + '/'
+ return path
+}
+
+function resizer (src, size) {
+ return 'https://resizer.digital-democracy.org/{width}/{height}/{url}'
+ .replace('{width}', size)
+ .replace('{height}', size)
+ .replace('{url}', src)
+}
+
+const MyMenuItem = () => (
+
+)
+
+class Example extends React.Component {
+ constructor (props) {
+ super(props)
+ this.history = createHistory()
+ this.unlisten = history.listen(this.handleHistoryChange)
+ this.state = {
+ ui: uiFromPath(history.location.pathname),
+ features: features
+ }
+ }
+ componentDidMount () {
+ this.intervalId = setTimeout(() => {
+ const coords = randomPoint(1, {bbox: [-76.65, -16.96, -51.92, 6.64]}).features[0].geometry.coordinates
+ console.log('fly to', coords)
+ console.log('from', this.state.mapViewState)
+ this.setState(state => ({
+ mapViewState: Object.assign({}, this.state.mapViewState, {
+ longitude: coords[0],
+ latitude: coords[1],
+ zoom: 10,
+ transitionInterpolator: new LinearInterpolator(),
+ transitionDuration: 1000,
+ transitionEasing: d3.easeCubic
+ })
+ }))
+ }, 10000)
+ }
+ handleHistoryChange = (location, action) => {
+ if (action === 'POP') {
+ this.setState({ui: uiFromPath(location.pathname)})
+ }
+ }
+ handleChangeUi = (ui) => {
+ const path = pathFromUi(ui)
+ ui.amberirect ? history.replace(path) : history.push(path)
+ this.setState({ui})
+ }
+ handleChangeFeatures = (_) => {
+ this.setState({features: _})
+ }
+ handleChangeMapViewState = (pos) => {
+ this.setState({mapViewState: pos})
+ }
+ render () {
+ return
+
+
+ }
+}
+
+ReactDOM.render(, document.getElementById('root'))
diff --git a/package-lock.json b/package-lock.json
index df2afc4..6a82349 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -112,9 +112,9 @@
}
},
"@babel/runtime": {
- "version": "7.0.0-rc.1",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0-rc.1.tgz",
- "integrity": "sha512-Nifv2kwP/nwR39cAOasNxzjYfpeuf/ZbZNtQz5eYxWTC9yHARU9wItFnAwz1GTZ62MU+AtSjzZPMbLK5Q9hmbg==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz",
+ "integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==",
"requires": {
"regenerator-runtime": "^0.12.0"
},
@@ -205,6 +205,11 @@
"wgs84": "0.0.0"
}
},
+ "@mapbox/geojson-types": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz",
+ "integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw=="
+ },
"@mapbox/jsonlint-lines-primitives": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.1.tgz",
@@ -293,6 +298,14 @@
"warning": "^4.0.1"
},
"dependencies": {
+ "@babel/runtime": {
+ "version": "7.0.0-rc.1",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0-rc.1.tgz",
+ "integrity": "sha512-Nifv2kwP/nwR39cAOasNxzjYfpeuf/ZbZNtQz5eYxWTC9yHARU9wItFnAwz1GTZ62MU+AtSjzZPMbLK5Q9hmbg==",
+ "requires": {
+ "regenerator-runtime": "^0.12.0"
+ }
+ },
"recompose": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/recompose/-/recompose-0.28.2.tgz",
@@ -654,6 +667,21 @@
"react-treebeard": "^2.1.0"
}
},
+ "@turf/helpers": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.1.4.tgz",
+ "integrity": "sha512-vJvrdOZy1ngC7r3MDA7zIGSoIgyrkWcGnNIEaqn/APmw+bVLF2gAW7HIsdTxd12s5wQMqEpqIQrmrbRRZ0xC7g==",
+ "dev": true
+ },
+ "@turf/random": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@turf/random/-/random-6.0.2.tgz",
+ "integrity": "sha512-kAJhZ2QZ4kM2ArI0lrFLtq1LCH4ZSjleUlj1izi/405XenjddjrbgtewpZte8IxCSeV4yK46lsDRS1911cF5lg==",
+ "dev": true,
+ "requires": {
+ "@turf/helpers": "6.x"
+ }
+ },
"@types/jss": {
"version": "9.5.7",
"resolved": "https://registry.npmjs.org/@types/jss/-/jss-9.5.7.tgz",
@@ -669,9 +697,9 @@
"integrity": "sha512-3AQoUxQcQtLHsK25wtTWIoIpgYjH3vSDroZOUr7PpCHw/jLY1RB9z9E8dBT/OSmwStVgkRNvdh+ZHNiomRieaw=="
},
"@types/react": {
- "version": "16.7.21",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-16.7.21.tgz",
- "integrity": "sha512-8BPxwygC83LgaIjOVVLrzB4mpP2u1ih01fbfy76L3h9OgKN+fNyMVPXj/0mGpWnxImjiM/2lqb3YOeT2Ca+NYQ==",
+ "version": "16.8.2",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.2.tgz",
+ "integrity": "sha512-6mcKsqlqkN9xADrwiUz2gm9Wg4iGnlVGciwBRYFQSMWG6MQjhOZ/AVnxn+6v8nslFgfYTV8fNdE6XwKu6va5PA==",
"requires": {
"@types/prop-types": "*",
"csstype": "^2.2.0"
@@ -4717,6 +4745,12 @@
"rw": "1"
}
},
+ "d3-ease": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz",
+ "integrity": "sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ==",
+ "dev": true
+ },
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -5987,6 +6021,11 @@
"integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==",
"dev": true
},
+ "esm": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/esm/-/esm-3.1.1.tgz",
+ "integrity": "sha512-Md2pR4IbR37UqubbgbA4+wiBorOEFB05Oo+g4WJW7W2ajiOhUfjZt77NzzCoQdrCb40GdKcflitm+XHDF053OQ=="
+ },
"espree": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz",
@@ -7837,6 +7876,11 @@
"duplexer": "^0.1.1"
}
},
+ "hammerjs": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
+ "integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE="
+ },
"har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
@@ -9955,6 +9999,15 @@
}
}
},
+ "mjolnir.js": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/mjolnir.js/-/mjolnir.js-2.0.3.tgz",
+ "integrity": "sha512-3AvoMwJCR3m9QQYzsE+D+LWZ9N2uWbl7prixSJGRZNOpaagRgiXJeVvDEHTiXAGmNhdn/VAtgWrx3lpdrj2sIQ==",
+ "requires": {
+ "@babel/runtime": "^7.0.0",
+ "hammerjs": "^2.0.8"
+ }
+ },
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
@@ -10070,6 +10123,11 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
+ "murmurhash-js": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
+ "integrity": "sha1-sGJ44h/Gw3+lMTcysEEry2rhX1E="
+ },
"mute-stream": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
@@ -10964,9 +11022,9 @@
"dev": true
},
"popper.js": {
- "version": "1.14.6",
- "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.6.tgz",
- "integrity": "sha512-AGwHGQBKumlk/MDfrSOf0JHhJCImdDMcGNoqKmKkU+68GFazv3CQ6q9r7Ja1sKDZmYWTckY/uLyEznheTDycnA=="
+ "version": "1.14.7",
+ "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.7.tgz",
+ "integrity": "sha512-4q1hNvoUre/8srWsH7hnoSJ5xVmIL4qgz+s4qf2TnJIMyZFUFMGH+9vE7mXynAlHSZ/NdTmmow86muD0myUkVQ=="
},
"posix-character-classes": {
"version": "0.1.1",
@@ -12287,6 +12345,11 @@
}
}
},
+ "potpack": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.1.tgz",
+ "integrity": "sha512-15vItUAbViaYrmaB/Pbw7z6qX2xENbFSTA7Ii4tgbPtasxm5v6ryKhKtL91tpWovDJzTiZqdwzhcFBCwiMVdVw=="
+ },
"precinct": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/precinct/-/precinct-4.2.0.tgz",
@@ -12770,28 +12833,15 @@
"dev": true
},
"react-event-listener": {
- "version": "0.6.5",
- "resolved": "https://registry.npmjs.org/react-event-listener/-/react-event-listener-0.6.5.tgz",
- "integrity": "sha512-//lCxOM3DQ0+xmTa/u9mI9mm55zCPdIKp89d8MGjlNsOOnXQ5sFDD1eed+sMBzQXKiRBLBMtSg/2T9RJFtfovw==",
+ "version": "0.6.6",
+ "resolved": "https://registry.npmjs.org/react-event-listener/-/react-event-listener-0.6.6.tgz",
+ "integrity": "sha512-+hCNqfy7o9wvO6UgjqFmBzARJS7qrNoda0VqzvOuioEpoEXKutiKuv92dSz6kP7rYLmyHPyYNLesi5t/aH1gfw==",
"requires": {
- "@babel/runtime": "7.2.0",
+ "@babel/runtime": "^7.2.0",
"prop-types": "^15.6.0",
"warning": "^4.0.1"
},
"dependencies": {
- "@babel/runtime": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz",
- "integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==",
- "requires": {
- "regenerator-runtime": "^0.12.0"
- }
- },
- "regenerator-runtime": {
- "version": "0.12.1",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
- "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg=="
- },
"warning": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz",
@@ -12894,6 +12944,85 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
+ "react-map-gl": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-4.0.9.tgz",
+ "integrity": "sha512-ob57Ph9v7bvShcTUTJRsGrXE+sQrYJJVbUPMWOatOYYFGbyhyPkPrIu7X8/BSJ2zPMy0MXbkl6ihMPwzlsaggg==",
+ "requires": {
+ "@babel/runtime": "^7.0.0",
+ "mapbox-gl": "~0.52.0",
+ "mjolnir.js": "^2.0.3",
+ "prop-types": "^15.5.7",
+ "react-virtualized-auto-sizer": "^1.0.2",
+ "viewport-mercator-project": "^6.1.0"
+ },
+ "dependencies": {
+ "@mapbox/jsonlint-lines-primitives": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
+ "integrity": "sha1-zlblOfg1UrWNENZy6k1vya3HsjQ="
+ },
+ "@mapbox/whoots-js": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
+ "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q=="
+ },
+ "geojson-vt": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz",
+ "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg=="
+ },
+ "kdbush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz",
+ "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew=="
+ },
+ "mapbox-gl": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-0.52.0.tgz",
+ "integrity": "sha512-jiZMGI7LjBNiSwYpFA3drzbZXrgEGERGJRpNS95t5BLZoc8Z+ggOOI1Fz2X+zLlh1j32iNDtf4j6En+caWwYiQ==",
+ "requires": {
+ "@mapbox/geojson-types": "^1.0.2",
+ "@mapbox/jsonlint-lines-primitives": "^2.0.2",
+ "@mapbox/mapbox-gl-supported": "^1.4.0",
+ "@mapbox/point-geometry": "^0.1.0",
+ "@mapbox/tiny-sdf": "^1.1.0",
+ "@mapbox/unitbezier": "^0.0.0",
+ "@mapbox/vector-tile": "^1.3.1",
+ "@mapbox/whoots-js": "^3.1.0",
+ "csscolorparser": "~1.0.2",
+ "earcut": "^2.1.3",
+ "esm": "^3.0.84",
+ "geojson-rewind": "^0.3.0",
+ "geojson-vt": "^3.2.1",
+ "gl-matrix": "^2.6.1",
+ "grid-index": "^1.0.0",
+ "minimist": "0.0.8",
+ "murmurhash-js": "^1.0.0",
+ "pbf": "^3.0.5",
+ "potpack": "^1.0.1",
+ "quickselect": "^1.0.0",
+ "rw": "^1.3.3",
+ "supercluster": "^5.0.0",
+ "tinyqueue": "^1.1.0",
+ "vt-pbf": "^3.0.1"
+ }
+ },
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
+ },
+ "supercluster": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-5.0.0.tgz",
+ "integrity": "sha512-9eeD5Q3908+tqdz+wYHHzi5mLKgnqtpO5mrjUfqr67UmGuOwBtVoQ9pJJrfcVHwMwC0wEBvfNRF9PgFOZgsOpw==",
+ "requires": {
+ "kdbush": "^3.0.0"
+ }
+ }
+ }
+ },
"react-modal": {
"version": "3.4.5",
"resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.4.5.tgz",
@@ -12991,6 +13120,11 @@
"react-lifecycles-compat": "^3.0.4"
}
},
+ "react-virtualized-auto-sizer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz",
+ "integrity": "sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg=="
+ },
"read-only-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz",
@@ -15819,6 +15953,22 @@
"extsprintf": "^1.2.0"
}
},
+ "viewport-mercator-project": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/viewport-mercator-project/-/viewport-mercator-project-6.1.0.tgz",
+ "integrity": "sha512-fy28QiLZ5t4SRMS7orqpRiE3tJ+ruvbjw6S6wguy15eRTc+RDwqCBDBt1YOd7WMHMBmBAjVNgYThQTNTDK/nIw==",
+ "requires": {
+ "@babel/runtime": "^7.0.0",
+ "gl-matrix": "^3.0.0-0"
+ },
+ "dependencies": {
+ "gl-matrix": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.0.0.tgz",
+ "integrity": "sha512-PD4mVH/C/Zs64kOozeFnKY8ybhgwxXXQYGWdB4h68krAHknWJgk9uKOn6z8YElh5//vs++90pb6csrTIDWnexA=="
+ }
+ }
+ },
"vlq": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz",
diff --git a/package.json b/package.json
index 02b91d7..fb1cfe6 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
},
"dependencies": {
"@mapbox/sexagesimal": "^1.0.0",
+ "@material-ui/core": "^1.5.1",
"@material-ui/icons": "^1.1.1",
"@segment/isodate": "^1.0.2",
"autosuggest-highlight": "^3.1.0",
@@ -41,6 +42,7 @@
"react-intl": "^2.3.0",
"react-intl-redux": "^0.6.0",
"react-isolated-scroll": "^0.1.1",
+ "react-map-gl": "^4.0.9",
"react-redux": "^5.0.5",
"react-virtualized": "^9.8.0",
"recompose": "^0.27.1",
@@ -52,11 +54,13 @@
"shallow-equal": "^1.0.0",
"turf-extent": "^1.0.4",
"url-regex": "^4.1.1",
- "utm": "^1.1.1"
+ "utm": "^1.1.1",
+ "viewport-mercator-project": "^6.1.0"
},
"devDependencies": {
"@material-ui/codemod": "^1.1.0",
"@storybook/react": "^3.2.3",
+ "@turf/random": "^6.0.2",
"babel-cli": "^6.18.0",
"babel-eslint": "^8.0.1",
"babel-plugin-react-intl": "^2.3.1",
@@ -71,6 +75,7 @@
"budo": "^10.0.3",
"bundle-collapser": "^1.2.1",
"copyfiles": "^2.0.0",
+ "d3-ease": "^1.0.5",
"dependency-check": "^3.1.0",
"dotenv": "^6.0.0",
"envify": "^4.1.0",
diff --git a/src/components/MapView/MapStyleLoader.js b/src/components/MapView/MapStyleLoader.js
new file mode 100644
index 0000000..07a80cf
--- /dev/null
+++ b/src/components/MapView/MapStyleLoader.js
@@ -0,0 +1,84 @@
+/* global fetch, AbortController */
+
+import { Component } from 'react'
+import PropTypes from 'prop-types'
+import { normalizeStyleURL } from '../../util/mapbox'
+
+// Load a map style object from a URL and pass it to children as object
+// Will interpret
+export default class MapStyleLoader extends Component {
+ static propTypes = {
+ /**
+ * children must be a function, called with params (error, mapStyle)
+ * error will be null if no error
+ * mapStyle will be undefined during loading
+ * mapStyle will be a style object once loaded
+ */
+ children: PropTypes.func.isRequired,
+ /**
+ * If the style is hosted on Mapbox you must pass a valid access token
+ */
+ mapboxToken: PropTypes.string,
+ /**
+ * Map style. This must be an an object conforming to the schema described
+ * in the [style reference](https://mapbox.com/mapbox-gl-style-spec/), or a
+ * URL to a JSON style. To load a style from the Mapbox API, you can use a
+ * URL of the form `mapbox://styles/:owner/:style`, where `:owner` is your
+ * Mapbox account name and `:style` is the style ID. Or you can use one of
+ * the predefined Mapbox styles.
+ */
+ mapStyle: PropTypes.oneOfType([PropTypes.string, PropTypes.object])
+ }
+
+ state = {
+ error: null
+ }
+
+ static getDerivedStateFromProps (props, state) {
+ // If the mapStyle prop is an object, we don't need to load anything, just
+ // pass it directly to the state
+ if (typeof props.mapStyle === 'object' && props.mapStyle !== state.mapStyle) {
+ return { mapStyle: props.mapStyle }
+ }
+ return null
+ }
+
+ componentDidMount () {
+ const {mapStyle} = this.props
+ if (typeof mapStyle === 'string') this.loadStyle(mapStyle)
+ }
+
+ componentDidUpdate (prevProps) {
+ const {mapStyle} = this.props
+ if (typeof mapStyle === 'string' && mapStyle !== prevProps.mapStyle) {
+ this.loadStyle(mapStyle)
+ }
+ }
+
+ componentWillUnmount () {
+ if (this.styleRequest) this.styleRequest.abort()
+ }
+
+ render () {
+ const { error, mapStyle } = this.state
+ return this.props.children(error, mapStyle)
+ }
+
+ loadStyle (styleUrl) {
+ const url = normalizeStyleURL(styleUrl, this.props.mapboxToken)
+ if (this.styleRequest) this.styleRequest.abort()
+ const signal = this.styleRequest = (new AbortController()).signal
+ fetch(url, { signal })
+ .then(res => res.json())
+ .then(data => {
+ this.setState({ mapStyle: data, error: null })
+ })
+ .catch(error => {
+ if (error.name === 'AbortError') return
+ this.setState({ error })
+ })
+ .finally(() => {
+ this.styleRequest = null
+ })
+ }
+}
diff --git a/src/components/MapView/MapView.js b/src/components/MapView/MapView.js
index 679c74c..d60e20b 100644
--- a/src/components/MapView/MapView.js
+++ b/src/components/MapView/MapView.js
@@ -1,54 +1,60 @@
+
+import React from 'react'
import debug from 'debug'
import PropTypes from 'prop-types'
-import React from 'react'
-import mapboxgl from 'mapbox-gl'
-import deepEqual from 'deep-equal'
+import memoize from 'memoize-one'
import assign from 'object-assign'
import featureFilter from 'feature-filter-geojson'
+import ReactMapGL, {Popup, NavigationControl} from 'react-map-gl'
+import WebMercatorViewport from 'viewport-mercator-project'
import { withStyles } from '@material-ui/core/styles'
+import { connect } from 'react-redux'
import * as MFPropTypes from '../../util/prop_types'
import { getBoundsOrWorld } from '../../util/map_helpers'
-import config from '../../../config.json'
-import Popup from './Popup'
+import PopupContent from './PopupContent'
+import MapStyleLoader from './MapStyleLoader'
require('mapbox-gl/dist/mapbox-gl.css')
-/* Mapbox [API access token](https://www.mapbox.com/help/create-api-access-token/) */
-mapboxgl.accessToken = config.mapboxToken
-
const log = debug('mf:mapview')
+const LABEL_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+
const styles = {
- root: {
- width: '100%',
- height: '100%',
- position: 'absolute'
- },
- map: {
- width: '100%',
- height: '100%'
+ popup: {
+ '& .mapboxgl-popup-content': {
+ padding: 0
+ },
+ // The rules below override the style for anchoring the popup in the middle
+ // of a side, instead forcing the anchor to a corner
+ '&.mapboxgl-popup-anchor-left': {
+ transform: 'translate(0, 0) !important'
+ },
+ '&.mapboxgl-popup-anchor-bottom': {
+ transform: 'translate(-100%, -100%) !important'
+ },
+ '&.mapboxgl-popup-anchor-right': {
+ transform: 'translate(-100%, 0) !important'
+ },
+ '&.mapboxgl-popup-anchor-top': {
+ transform: 'translate(-100%, 0) !important'
+ }
}
}
-const emptyGeoJson = {
- type: 'FeatureCollection',
- features: []
-}
-
const labelStyleLayer = {
- id: 'labels',
+ id: 'mapfilter_labels',
type: 'symbol',
source: 'features',
layout: {
- 'text-field': '',
+ 'text-field': '{__mf_label}',
'text-allow-overlap': true,
'text-ignore-placement': true,
'text-size': 9,
'text-font': [
- 'DIN Offc Pro Bold',
- 'Arial Unicode MS Bold'
+ 'DIN Offc Pro Bold'
]
},
paint: {
@@ -59,11 +65,11 @@ const labelStyleLayer = {
}
const pointStyleLayer = {
- id: 'points',
+ id: 'mapfilter_points',
type: 'circle',
source: 'features',
paint: {
- // make circles larger as the user zooms from z12 to z22
+ // make circles larger as the user zooms from z7 to z18
'circle-radius': {
'base': 1.5,
'stops': [[7, 5], [18, 25]]
@@ -72,49 +78,57 @@ const pointStyleLayer = {
'property': '__mf_color',
'type': 'identity'
},
- 'circle-opacity': 0.75,
- 'circle-stroke-width': 1.5,
+ 'circle-opacity': ['case',
+ ['boolean', ['feature-state', 'hover'], false],
+ 1,
+ 0.75
+ ],
+ 'circle-stroke-width': ['case',
+ ['boolean', ['feature-state', 'hover'], false],
+ 2.5,
+ 1.5
+ ],
'circle-stroke-color': '#ffffff',
- 'circle-stroke-opacity': 0.9
+ 'circle-stroke-opacity': ['case',
+ ['boolean', ['feature-state', 'hover'], false],
+ 1,
+ 0.9
+ ]
}
}
-const pointHoverStyleLayer = {
- id: 'points-hover',
- type: 'circle',
- source: 'hover',
- paint: assign({}, pointStyleLayer.paint, {
- 'circle-opacity': 1,
- 'circle-stroke-width': 2.5,
- 'circle-stroke-color': '#ffffff',
- 'circle-stroke-opacity': 1
- })
-}
-
const noop = (x) => x
class MapView extends React.Component {
static defaultProps = {
- center: [0, 0],
- zoom: 0,
features: [],
showFeatureDetail: noop,
moveMap: noop,
interactive: true,
- labelPoints: false,
- mapControls: []
+ labelPoints: false
}
static propTypes = {
/* map center point [lon, lat] */
- center: PropTypes.array,
+ viewState: PropTypes.shape({
+ longitude: PropTypes.number,
+ latitude: PropTypes.number,
+ zoom: PropTypes.number,
+ transitionInterpolator: PropTypes.object,
+ transitionDuration: PropTypes.number
+ }),
/* Geojson FeatureCollection of features to show on map */
features: PropTypes.arrayOf(MFPropTypes.mapViewFeature).isRequired,
/* Current filter (See https://www.mapbox.com/mapbox-gl-style-spec/#types-filter) */
filter: MFPropTypes.mapboxFilter,
+ mapboxToken: PropTypes.string,
/**
- * - NOT yet dynamic e.g. if you change it the map won't change
- * Map style. This must be an an object conforming to the schema described in the [style reference](https://mapbox.com/mapbox-gl-style-spec/), or a URL to a JSON style. To load a style from the Mapbox API, you can use a URL of the form `mapbox://styles/:owner/:style`, where `:owner` is your Mapbox account name and `:style` is the style ID. Or you can use one of the predefined Mapbox styles.
+ * Map style. This must be an an object conforming to the schema described
+ * in the [style reference](https://mapbox.com/mapbox-gl-style-spec/), or a
+ * URL to a JSON style. To load a style from the Mapbox API, you can use a
+ * URL of the form `mapbox://styles/:owner/:style`, where `:owner` is your
+ * Mapbox account name and `:style` is the style ID. Or you can use one of
+ * the predefined Mapbox styles.
*/
mapStyle: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
labelPoints: PropTypes.bool,
@@ -127,264 +141,166 @@ class MapView extends React.Component {
moveMap: PropTypes.func.isRequired,
fieldMapping: MFPropTypes.fieldMapping,
/* map zoom */
- zoom: PropTypes.number,
- interactive: PropTypes.bool,
- mapControls: PropTypes.arrayOf(PropTypes.shape({
- onAdd: PropTypes.func.isRequired,
- onRemove: PropTypes.func.isRequired
- }))
+ interactive: PropTypes.bool
}
- state = {}
-
- handleMapMoveOrZoom = (e) => {
- if (e.internal) return
- this.props.moveMap({
- center: this.map.getCenter().toArray(),
- zoom: this.map.getZoom(),
- bearing: this.map.getBearing()
- })
+ state = {
+ hoveredId: null
}
- handleMapClick = (e) => {
- // if (!this.map.loaded()) return
- var features = this.map.queryRenderedFeatures(
- e.point,
- {layers: ['points-hover']}
- )
- if (!features.length) return
- this.setState({lngLat: null})
- this.props.showFeatureDetail(features[0].properties.__mf_id)
+ constructor (props) {
+ super(props)
+ log('Creating new map instance with props:', props)
+ this.mapRef = window.mmap = React.createRef()
}
- handleMouseMove = (e) => {
- if (!this.map.loaded()) return
- var features = this.map.queryRenderedFeatures(
- e.point,
- {layers: ['points', 'points-hover']}
- )
- this.map.getCanvas().style.cursor = (features.length) ? 'pointer' : ''
- if (!features.length) {
- this.setState({lngLat: null})
- this.map.getSource('hover').setData(emptyGeoJson)
- return
- }
- this.map.getSource('hover').setData(features[0])
- this.setState({lngLat: features[0].geometry.coordinates})
- this.setState({id: features[0].properties.__mf_id})
- }
-
- ready (fn) {
- if (this.map.loaded() && !this._styleDirty) {
- fn()
- } else {
- this.map.once('load', () => fn.call(this))
- }
- }
-
- render () {
- const {classes} = this.props
- return (
-
-
(this.mapContainer = el)}
- className={classes.map}
- />
- {this.state.lngLat &&
}
-
- )
- }
-
- centerMap (geojson) {
- this.map.fitBounds(getBoundsOrWorld(geojson), {padding: 15, duration: 0})
- if (this.map.getZoom() > 13) {
- this.map.setZoom(13)
- }
- }
-
- // The first time our component mounts, render a new map into `mapDiv`
- // with settings from props.
- componentDidMount () {
- const { center, interactive, mapStyle, zoom, mapControls } = this.props
-
- const mapDiv = document.createElement('div')
- mapDiv.style.height = '100%'
- mapDiv.style.width = '100%'
- this.mapContainer.appendChild(mapDiv)
-
- const map = window.map = this.map = new mapboxgl.Map({
- style: mapStyle,
- container: mapDiv,
- center: center || [0, 0],
- zoom: zoom || 0
- })
- map._prevStyle = mapStyle
-
- if (!interactive) {
- map.scrollZoom.disable()
- }
-
- // Add zoom and rotation controls to the map.
- map.addControl(new mapboxgl.NavigationControl())
- map.dragRotate.disable()
- map.touchZoomRotate.disableRotation()
-
- this.geojson = this.getGeoJson(this.props)
-
- map.once('load', () => {
- if (interactive) {
- map.on('click', this.handleMapClick)
- map.on('mousemove', this.handleMouseMove)
- }
- map.on('moveend', this.handleMapMoveOrZoom)
- this.setupLayers(this.props)
- })
- map.once('style.load', () => {
- mapControls.forEach(function (control) {
- map.addControl.bind(map)(control)
- })
+ addLayers = memoize((style, showLabels) => {
+ const layers = showLabels ? [pointStyleLayer, labelStyleLayer] : [pointStyleLayer]
+ log('Adding layers to base style:', layers.map(l => l.id))
+ return assign({}, style, {
+ layers: style.layers.concat(layers)
})
+ })
- // If no map center or zoom passed, set map extent to extent of marker layer
- if (!center || !zoom) {
- this.centerMap(this.geojson)
- }
- }
-
- componentWillReceiveProps (nextProps) {
- this.updateIfNeeded(nextProps, this.props)
- }
-
- componentWillUnmount () {
- this.map.off('moveend', this.handleMapMoveOrZoom)
- this.map.off('click', this.handleMapClick)
- this.map.off('mousemove', this.handleMouseMove)
- this.map.remove()
- }
-
- setupLayers (props) {
- const {filter, labelPoints} = props
- this.map.addSource('features', {type: 'geojson', data: this.geojson})
- // TODO: Should choose style based on whether features are point, line or polygon
- this.map.addSource('hover', {type: 'geojson', data: emptyGeoJson})
- this.map.addLayer(pointStyleLayer)
- this.map.addLayer(pointHoverStyleLayer)
- this.map.addLayer(labelStyleLayer)
- this.map.setFilter('points', filter)
- this.map.setFilter('labels', filter)
- if (labelPoints) {
- this.map.setLayoutProperty('labels', 'text-field', '{__mf_label}')
- this.map.setPaintProperty('points', 'circle-radius', 7)
- }
- }
-
- updateIfNeeded (nextProps, props = {}) {
- const {disableScrollToZoom} = props
-
- if (this.map._prevStyle !== nextProps.mapStyle) {
- log('updating style')
- this._styleDirty = true
- this.map.setStyle(nextProps.mapStyle)
- this.map._prevStyle = nextProps.mapStyle
- this.map.once('style.load', () => {
- this.setupLayers(nextProps)
- this._styleDirty = false
- if (!this.map._loaded) return
- this.map.fire('load')
+ addSource = memoize((style, data) => {
+ log('Adding GeoJSON source to base style')
+ return assign({}, style, {
+ sources: assign({}, style.sources, {
+ features: {
+ type: 'geojson',
+ data: data
+ }
})
- }
-
- var shouldMapZoom = this.map.getZoom() !== nextProps.zoom
- var shouldMapMove = !deepEqual(this.map.getCenter().toArray(), nextProps.center)
-
- if (shouldMapZoom || shouldMapMove) {
- this.map.flyTo({center: nextProps.center, zoom: nextProps.zoom}, {internal: true})
- }
-
- let shouldDataUpdate = nextProps.features !== props.features ||
- nextProps.fieldMapping !== props.fieldMapping ||
- nextProps.colorIndex !== props.colorIndex ||
- (nextProps.filter !== props.filter && props.labelPoints)
-
- this.ready(() => {
- if (shouldDataUpdate) {
- this.geojson = this.getGeoJson(nextProps)
-
- log('updating source', this.geojson)
- this.map.getSource('features').setData(this.geojson)
-
- this.centerMap(this.geojson)
- }
-
- this.updateFilterIfNeeded(nextProps.filter)
-
- if (disableScrollToZoom !== nextProps.disableScrollToZoom) {
- nextProps.disableScrollToZoom ? this.map.scrollZoom.disable() : this.map.scrollZoom.enable()
- }
-
- const textField = nextProps.labelPoints ? '{__mf_label}' : ''
-
- if (this.map.getLayoutProperty('labels', 'text-field') !== textField) {
- log('updating labels "' + textField + '"')
- this.map.setLayoutProperty('labels', 'text-field', textField)
- }
})
- }
-
- /**
- * Moves the map to a new position if it is different from the current position
- * @param {array} center new coordinates for center of map
- * @param {number} zoom new zoom level for map
- * @return {boolean} true if map has moved, otherwise false
- */
- moveIfNeeded (center, zoom) {
- const currentPosition = {
- center: this.map.getCenter().toArray(),
- zoom: this.map.getZoom()
- }
- const newMapPosition = {
- center,
- zoom
- }
- const shouldMapMove = center && zoom &&
- !deepEqual(currentPosition, newMapPosition)
- if (shouldMapMove) {
- log('Moving map')
- this.map.jumpTo(newMapPosition)
- return true
- }
- return false
- }
-
- updateFilterIfNeeded (filter) {
- if (filter !== this.props.filter && filter) {
- log('updating filter')
- this.map.setFilter('points', filter)
- this.map.setFilter('labels', filter)
- }
- }
+ })
- // Construct GeoJSON for map
- getGeoJson ({features = [], fieldMapping = {}, colorIndex = {}, filter = []}) {
- let i = 0
+ getGeoJson = memoize((
+ features = [],
+ fieldMapping = {},
+ colorIndex = {},
+ filter = []
+ ) => {
+ log('Updating GeoJSON with current filter')
const ff = featureFilter(filter)
return {
type: 'FeatureCollection',
features: features
- .filter(f => f.geometry)
- .map(f => {
+ .filter(f => f.geometry && ff(f))
+ .map((f, i) => {
+ const colorValue = f.properties[fieldMapping.color] ||
+ f.properties[fieldMapping.color + '.0']
const newFeature = {
+ // Mapbox-gl still (2019-01-22) does not support string ids,
+ // so we need to generate an id here and save the real is to props
+ id: i,
type: 'feature',
geometry: f.geometry,
properties: assign({}, f.properties, {
__mf_id: f.id,
- __mf_color: colorIndex[f.properties[fieldMapping.color] || f.properties[fieldMapping.color + '.0']]
+ __mf_color: colorIndex[colorValue],
+ __mf_label: LABEL_CHARS.charAt(i)
})
}
- if (ff(f)) newFeature.properties.__mf_label = config.labelChars.charAt(i++)
return newFeature
})
}
+ })
+
+ onViewStateChange = ({viewState, interactionState, oldViewState}) => {
+ console.log('nextvp', viewState, interactionState, oldViewState)
+ const { viewport, moveMap } = this.props
+ const propsSpecifyMapLocation = viewport && typeof viewport.longitude === 'number'
+ if (propsSpecifyMapLocation) return moveMap(viewState)
+ // If the props for viewport are not set, zoom to bounds of data
+ const fittedViewport = new WebMercatorViewport(viewState)
+ const {longitude, latitude, zoom} = fittedViewport.fitBounds(
+ getBoundsOrWorld(this.props.features),
+ {padding: 15}
+ )
+ log('viewport not set on props, zooming to bounding box:', longitude, latitude, zoom)
+ moveMap(assign({}, viewState, {
+ longitude,
+ latitude,
+ zoom
+ }))
+ }
+
+ onHover = event => {
+ if (!this.props.interactive) return
+ // The more declarative way of doing this would be to set a new style on
+ // every render of the map if hoverId changes, but we use map feature state
+ // for performance reasons here
+ const map = this.mapRef.current.getMap()
+ const hoveredId = event.features.length ? event.features[0].id : null
+ if (hoveredId !== this.state.hoveredId) {
+ if (hoveredId) log('Hover of feature id:', event.features[0].properties.__mf_id)
+ map.setFeatureState({source: 'features', id: hoveredId}, { hover: true })
+ map.setFeatureState({source: 'features', id: this.state.hoveredId}, { hover: false })
+ this.setState({hoveredId})
+ }
+ }
+
+ onClick = event => {
+ if (!this.props.interactive) return
+ if (!event.features.length) return log('Click detected but no feature')
+ log('Clicked feature id:', event.features[0].properties.__mf_id)
+ this.props.showFeatureDetail(event.features[0].properties.__mf_id)
+ }
+
+ getCursor = ({isHovering, isDragging}) => {
+ if (isDragging) return 'grabbing'
+ if (isHovering && this.props.interactive) return 'pointer'
+ return 'grab'
+ }
+
+ render () {
+ const { features, fieldMapping, colorIndex, filter,
+ labelPoints, mapStyle, mapboxToken, classes, viewport } = this.props
+ const { hoveredId } = this.state
+ return
+ {(error, mapStyle) => {
+ if (error) return {error.text}
+ if (!mapStyle) return null // TODO: show loading indicator
+ const geojson = this.getGeoJson(features, fieldMapping, colorIndex, filter)
+ const styleWithLayers = this.addLayers(mapStyle, labelPoints)
+ const styleWithData = this.addSource(styleWithLayers, geojson)
+ const hoveredFeature = hoveredId !== null && geojson.features[hoveredId]
+ console.log(viewport)
+ return
+
+
+
+ {hoveredFeature &&
+
+ }
+
+ }}
+
}
}
diff --git a/src/components/MapView/Popup.js b/src/components/MapView/Popup.js
deleted file mode 100644
index e196990..0000000
--- a/src/components/MapView/Popup.js
+++ /dev/null
@@ -1,156 +0,0 @@
-import React from 'react'
-import {compose} from 'redux'
-import {connect} from 'react-redux'
-import Typography from '@material-ui/core/Typography'
-import { withStyles } from '@material-ui/core/styles'
-import classNames from 'classnames'
-
-import Image from '../Image'
-import FormattedValue from '../Shared/FormattedValue'
-import getFeaturesById from '../../selectors/features_by_id'
-import getFieldMapping from '../../selectors/field_mapping'
-import getColorIndex from '../../selectors/color_index'
-import getFieldAnalysis from '../../selectors/field_analysis'
-
-const styles = {
- wrapper: {
- width: 200,
- padding: 0,
- backgroundColor: 'black',
- cursor: 'pointer',
- position: 'absolute',
- willChange: 'transform',
- top: 0,
- left: 0,
- pointerEvents: 'none'
- },
- wrapperImage: {
- height: 200
- },
- image: {
- width: 200,
- height: 200,
- objectFit: 'cover',
- display: 'block',
- background: '#000000'
- },
- titleBox: {
- position: 'absolute',
- bottom: 0,
- width: '100%',
- backgroundColor: 'rgba(0,0,0,0.5)',
- color: 'white',
- padding: '0.25em 0.5em',
- boxSizing: 'border-box'
- },
- title: {
- color: 'white'
- },
- subheading: {
- color: 'white'
- }
-}
-
-class Popup extends React.Component {
- static defaultProps = {
- offset: {
- x: 0,
- y: 0
- }
- }
- constructor (props) {
- super(props)
- this.map = props.map
- }
-
- state = {}
-
- update = (lngLat) => {
- if (!Array.isArray(lngLat) || lngLat.length !== 2) lngLat = this.props.lngLat
- const w = this._el.offsetWidth
- const h = this._el.offsetHeight
- this.setState({
- transform: getPopupTransform(this.map, lngLat, w, h, this.props.offset)
- })
- }
-
- componentDidMount () {
- const {lngLat} = this.props
- this.map.on('move', this.update)
- this.update(lngLat)
- }
-
- componentWillReceiveProps ({lngLat}) {
- this.update(lngLat)
- }
-
- componentWillUnmount () {
- this.map.off('move', this.update)
- }
-
- render () {
- const {media, title, subtitle, classes, titleType, subtitleType} = this.props
- const {transform} = this.state
-
- return
(this._el = el)}>
- {media &&
}
-
- {title &&
-
- }
- {subtitle &&
-
- }
-
-
- }
-}
-
-function getPopupTransform (map, lngLat, width, height, offset = {x: 0, y: 0}) {
- const pos = map.project(lngLat).round()
- let anchor
-
- if (pos.y < height) {
- anchor = 'top'
- } else {
- anchor = 'bottom'
- }
-
- if (pos.x > map.transform.width - width) {
- anchor += '-right'
- } else {
- anchor += '-left'
- }
-
- const anchorTranslate = {
- 'top-left': 'translate(0,0)',
- 'top-right': 'translate(-100%,0)',
- 'bottom-left': 'translate(0,-100%)',
- 'bottom-right': 'translate(-100%,-100%)'
- }
-
- return `${anchorTranslate[anchor]} translate(${pos.x + offset.x}px,${pos.y + offset.y}px)`
-}
-
-const mapStateToProps = (state, ownProps) => {
- const featuresById = getFeaturesById(state)
- const colorIndex = getColorIndex(state)
- const fieldMapping = getFieldMapping(state)
- const feature = featuresById[ownProps.id]
- if (!feature) return {}
- const geojsonProps = feature.properties
- const fieldAnalysisProps = getFieldAnalysis(state).properties
- return {
- media: geojsonProps[fieldMapping.media],
- title: geojsonProps[fieldMapping.title],
- subtitle: geojsonProps[fieldMapping.subtitle],
- color: colorIndex[geojsonProps[fieldMapping.color]],
- titleType: fieldAnalysisProps[fieldMapping.title] && fieldAnalysisProps[fieldMapping.title].type,
- subtitleType: fieldAnalysisProps[fieldMapping.subtitle] && fieldAnalysisProps[fieldMapping.subtitle].type
- }
-}
-
-export default compose(
- connect(mapStateToProps),
- withStyles(styles)
-)(Popup)
diff --git a/src/components/MapView/PopupContent.js b/src/components/MapView/PopupContent.js
new file mode 100644
index 0000000..9bba332
--- /dev/null
+++ b/src/components/MapView/PopupContent.js
@@ -0,0 +1,84 @@
+import React from 'react'
+import {compose} from 'redux'
+import {connect} from 'react-redux'
+import Typography from '@material-ui/core/Typography'
+import { withStyles } from '@material-ui/core/styles'
+import classNames from 'classnames'
+
+import Image from '../Image'
+import FormattedValue from '../Shared/FormattedValue'
+import getFeaturesById from '../../selectors/features_by_id'
+import getFieldMapping from '../../selectors/field_mapping'
+import getFieldAnalysis from '../../selectors/field_analysis'
+
+const styles = {
+ wrapper: {
+ width: 200,
+ padding: 0,
+ backgroundColor: 'black',
+ cursor: 'pointer',
+ position: 'relative',
+ willChange: 'transform',
+ pointerEvents: 'none'
+ },
+ wrapperImage: {
+ height: 200
+ },
+ image: {
+ width: 200,
+ height: 200,
+ objectFit: 'cover',
+ display: 'block',
+ background: '#000000'
+ },
+ titleBox: {
+ position: 'absolute',
+ bottom: 0,
+ width: '100%',
+ backgroundColor: 'rgba(0,0,0,0.5)',
+ color: 'white',
+ padding: '0.25em 0.5em',
+ boxSizing: 'border-box'
+ },
+ title: {
+ color: 'white'
+ },
+ subheading: {
+ color: 'white'
+ }
+}
+
+const PopupContent = ({media, title, subtitle, classes, titleType, subtitleType}) => (
+
+ {media &&
}
+
+ {title &&
+
+ }
+ {subtitle &&
+
+ }
+
+
+)
+
+const mapStateToProps = (state, ownProps) => {
+ const featuresById = getFeaturesById(state)
+ const fieldMapping = getFieldMapping(state)
+ const feature = featuresById[ownProps.id]
+ if (!feature) return {}
+ const geojsonProps = feature.properties
+ const fieldAnalysisProps = getFieldAnalysis(state).properties
+ return {
+ media: geojsonProps[fieldMapping.media],
+ title: geojsonProps[fieldMapping.title],
+ subtitle: geojsonProps[fieldMapping.subtitle],
+ titleType: fieldAnalysisProps[fieldMapping.title] && fieldAnalysisProps[fieldMapping.title].type,
+ subtitleType: fieldAnalysisProps[fieldMapping.subtitle] && fieldAnalysisProps[fieldMapping.subtitle].type
+ }
+}
+
+export default compose(
+ connect(mapStateToProps),
+ withStyles(styles)
+)(PopupContent)
diff --git a/src/components/ReportView/ReportView.js b/src/components/ReportView/ReportView.js
index a626397..acc10a6 100644
--- a/src/components/ReportView/ReportView.js
+++ b/src/components/ReportView/ReportView.js
@@ -183,11 +183,12 @@ class ReportView extends React.Component {
})
}
- memoizedSlice = memoize((arr, begin, end) => arr.slice(begin, end))
+ memoizedSlice = memoize((arr, start, end) => arr.slice(start, end))
render () {
const { filteredFeatures, featuresById, showFeatureDetail, fieldAnalysis, classes, requestPrint, paperSize, viewState } = this.props
const featuresSlice = this.memoizedSlice(filteredFeatures, 0, MAX_REPORT_LEN)
+ console.log('report render')
return (
-
+
{actionButton && {createElement(actionButton)}
}
diff --git a/src/containers/MapFilter.js b/src/containers/MapFilter.js
index 77d2f21..7c3717d 100644
--- a/src/containers/MapFilter.js
+++ b/src/containers/MapFilter.js
@@ -70,7 +70,7 @@ const controllableProps = [
'filters',
'filterFields',
'features',
- 'mapPosition',
+ 'mapViewState',
'mapStyle',
'fieldTypes',
'fieldOrder',
@@ -202,15 +202,6 @@ class MapFilter extends React.Component {
viewToolbarButtons: PropTypes.arrayOf(PropTypes.shape({
MfViewId: PropTypes.string.isRequired,
button: PropTypes.oneOfType([PropTypes.element, PropTypes.func])
- })),
- /**
- * An array of controls to add to the map. Each control should implement
- * the IControl interface of mapbox-gl-js https://www.mapbox.com/mapbox-gl-js/api/#icontrol
- * To set the position of the control, add a `getDefaultPosition()` method.
- */
- mapControls: PropTypes.arrayOf(PropTypes.shape({
- onAdd: PropTypes.func.isRequired,
- onRemove: PropTypes.func.isRequired
}))
}
@@ -242,6 +233,9 @@ class MapFilter extends React.Component {
persistStore(this.store, assign({}, reduxPersistOptions, {
keyPrefix: 'reduxPersist:' + props.datasetName + ':'
}))
+ if (props.mapStyle.startsWith('mapbox:') && !props.mapboxToken) {
+ console.warn('To use a Mapbox style you must pass a mapboxToken prop')
+ }
}
componentWillReceiveProps (nextProps) {
@@ -251,11 +245,26 @@ class MapFilter extends React.Component {
}
render () {
- const {actionButton, detailViewButtons, views, appBarButtons, appBarMenuItems, appBarTitle, locale, mapControls} = this.props
+ const {
+ actionButton,
+ detailViewButtons,
+ mapboxToken,
+ views,
+ appBarButtons,
+ appBarMenuItems,
+ appBarTitle,
+ locale
+ } = this.props
return
-
+
}
diff --git a/src/containers/ViewContainer.js b/src/containers/ViewContainer.js
index cc7f75b..eda6980 100644
--- a/src/containers/ViewContainer.js
+++ b/src/containers/ViewContainer.js
@@ -28,8 +28,6 @@ function mapStateToProps (state, ownProps) {
viewState: state.viewStates[component.MfViewId],
willPrint: state.print.willPrint,
paperSize: state.print.paperSize,
- center: state.mapPosition.center,
- zoom: state.mapPosition.zoom,
mapStyle: state.mapStyle,
settings: state.settings,
features: getFilterableFeatures(state),
diff --git a/src/reducers/index.js b/src/reducers/index.js
index 4c10b12..49be409 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -4,7 +4,7 @@ import {intlReducer} from 'react-intl-redux'
import features from './features'
import filters from './filters'
import filterFields from './filter_fields'
-import mapPosition from './map_position'
+import mapViewState from './map_view_state'
import mapStyle from './map_style'
import ui from './ui'
import fieldMapping from './field_mapping'
@@ -19,7 +19,7 @@ export default combineReducers({
features,
filters,
filterFields,
- mapPosition,
+ mapViewState,
mapStyle,
ui,
print,
diff --git a/src/reducers/map_position.js b/src/reducers/map_view_state.js
similarity index 69%
rename from src/reducers/map_position.js
rename to src/reducers/map_view_state.js
index 48c5d3e..d19c187 100644
--- a/src/reducers/map_position.js
+++ b/src/reducers/map_view_state.js
@@ -1,6 +1,6 @@
import assign from 'object-assign'
-const mapPosition = (state = {}, action) => {
+const mapViewState = (state = {}, action) => {
switch (action.type) {
case 'MOVE_MAP':
return assign({}, state, action.payload)
@@ -9,4 +9,4 @@ const mapPosition = (state = {}, action) => {
}
}
-export default mapPosition
+export default mapViewState
diff --git a/src/util/map_helpers.js b/src/util/map_helpers.js
index b1fead1..2fbe031 100644
--- a/src/util/map_helpers.js
+++ b/src/util/map_helpers.js
@@ -2,22 +2,23 @@ import extent from 'turf-extent'
/**
* @private
- * For a given geojson FeatureCollection, return the geographic bounds.
+ * For a given an array of geojson Features, return the geographic bounds.
* For a missing or invalid FeatureCollection, return the bounds for
* the whole world.
- * @param {object} fc Geojson FeatureCollection
- * @return {array} Bounds in format `[minLng, minLat, maxLng, maxLat]``
+ * @param {array} features Array of GeoJson Features
+ * @return {array} Bounds in format `[[minLng, minLat], [maxLng, maxLat]]``
*/
-export function getBoundsOrWorld (fc) {
+export function getBoundsOrWorld (features) {
// If we don't have data, default to the extent of the whole world
// NB. Web mercator goes to infinity at lat 90! Use lat 85.
- if (!fc || !fc.features || !fc.features.length) {
- return [-180, -85, 180, 85]
+ if (!features || !features.length) {
+ return [[-180, -85], [180, 85]]
}
- return extent({
+ const [minLng, minLat, maxLng, maxLat] = extent({
type: 'FeatureCollection',
- features: fc.features.filter(f => f.geometry)
+ features: features.filter(f => f.geometry)
})
+ return [[minLng, minLat], [maxLng, maxLat]]
}
/**
diff --git a/src/util/mapbox.js b/src/util/mapbox.js
new file mode 100644
index 0000000..6697813
--- /dev/null
+++ b/src/util/mapbox.js
@@ -0,0 +1,53 @@
+// Code from https://github.com/mapbox/mapbox-gl-js/blob/master/src/util/mapbox.js
+
+const urlRe = /^(\w+):\/\/([^/?]*)(\/[^?]+)?\??(.+)?/
+const MAPBOX_API_URL = 'https://api.mapbox.com'
+
+export const normalizeStyleURL = function (url, accessToken) {
+ if (!isMapboxURL(url)) return url
+ const urlObject = parseUrl(url)
+ urlObject.path = `/styles/v1${urlObject.path}`
+ return makeAPIURL(urlObject, accessToken)
+}
+
+function isMapboxURL (url) {
+ return url.indexOf('mapbox:') === 0
+}
+
+function makeAPIURL (urlObject, accessToken) {
+ const apiUrlObject = parseUrl(MAPBOX_API_URL)
+ urlObject.protocol = apiUrlObject.protocol
+ urlObject.authority = apiUrlObject.authority
+
+ if (apiUrlObject.path !== '/') {
+ urlObject.path = `${apiUrlObject.path}${urlObject.path}`
+ }
+
+ if (!accessToken) {
+ throw new Error(`An API access token is required to use Mapbox GL.`)
+ }
+ if (accessToken[0] === 's') {
+ throw new Error(`Use a public access token (pk.*) with Mapbox GL, not a secret access token (sk.*). ${help}`)
+ }
+
+ urlObject.params.push(`access_token=${accessToken}`)
+ return formatUrl(urlObject)
+}
+
+function parseUrl (url) {
+ const parts = url.match(urlRe)
+ if (!parts) {
+ throw new Error('Unable to parse URL object')
+ }
+ return {
+ protocol: parts[1],
+ authority: parts[2],
+ path: parts[3] || '/',
+ params: parts[4] ? parts[4].split('&') : []
+ }
+}
+
+function formatUrl (obj) {
+ const params = obj.params.length ? `?${obj.params.join('&')}` : ''
+ return `${obj.protocol}://${obj.authority}${obj.path}${params}`
+}