From 9da425e4676a2648c7bf32c9f5cdd265bc22046a Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Fri, 3 Feb 2017 23:35:14 +0000 Subject: [PATCH 01/32] Setup typescript and start converting component --- .babelrc | 35 ----- .eslintrc | 24 --- package.json | 30 +--- src/constants/{css.js => css.ts} | 1 + ...projected-layer.js => projected-layer.tsx} | 71 +++++---- src/{scale-control.js => scale-control.tsx} | 106 ++++++++------ src/source.js | 54 ------- src/source.tsx | 50 +++++++ src/util/{diff.js => diff.ts} | 2 +- src/zoom-control.js | 118 --------------- src/zoom-control.tsx | 138 ++++++++++++++++++ tsconfig.json | 25 ++++ tslint.json | 12 ++ 13 files changed, 335 insertions(+), 331 deletions(-) delete mode 100644 .babelrc delete mode 100644 .eslintrc rename src/constants/{css.js => css.ts} (99%) rename src/{projected-layer.js => projected-layer.tsx} (56%) rename src/{scale-control.js => scale-control.tsx} (60%) delete mode 100644 src/source.js create mode 100644 src/source.tsx rename src/util/{diff.js => diff.ts} (84%) delete mode 100644 src/zoom-control.js create mode 100644 src/zoom-control.tsx create mode 100644 tsconfig.json create mode 100644 tslint.json diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 9b963de69..000000000 --- a/.babelrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "presets": [ - "babel-preset-philpl" - ], - "ignore": [ - "/node_modules/", - "/example/", - "/lib/", - "/es/", - "/docs/" - ], - "plugins": [ - "transform-runtime", - "transform-react-constant-elements", - "transform-react-inline-elements" - ], - "env": { - "commonjssimple": { - "plugins": [ - ["transform-es2015-modules-commonjs-simple", { - "noMangle": true, - "addExports": true - }] - ] - }, - "test": { - "plugins": [ - ["transform-es2015-modules-commonjs-simple", { - "noMangle": true, - "addExports": true - }] - ] - } - } -} diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 5a8ac3eb5..000000000 --- a/.eslintrc +++ /dev/null @@ -1,24 +0,0 @@ -{ - "parser": "babel-eslint", - "ecmaFeatures": { - "classes": true, - "jsx": true - }, - "extends": "airbnb-base", - "cache": true, - "plugins": [ - "react" - ], - "rules": { - "react/jsx-uses-vars": 1, - "react/jsx-uses-react": "error", - "class-methods-use-this": 0, - "no-underscore-dangle": 0, - "import/extensions": 0, - "no-mixed-operators": 0 - }, - "globals": { - "window": true, - "document": true - } -} diff --git a/package.json b/package.json index e0b86fa14..0e704f861 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,8 @@ "jsnext:main": "es/index.js", "scripts": { "clean": "rm -rf dist", - "lint": "eslint src --ignore-pattern __tests__", - "test": "jest", - "build:commonjs": "BABEL_ENV=commonjssimple babel src --out-dir lib", "build:watch": "BABEL_ENV=commonjssimple babel src --watch --out-dir lib", - "build:es": "babel src --out-dir es", - "build": "npm run lint && npm run test && npm run build:commonjs && npm run build:es", + "build": "tsc", "prepublish": "npm run clean && npm run build", "version": "npm run build", "postversion": "git push && git push --tags" @@ -49,7 +45,6 @@ }, "homepage": "https://github.com/alex3165/react-mapbox-gl#readme", "dependencies": { - "babel-runtime": "^6.11.6", "deep-equal": "^1.0.1", "mapbox-gl": "^0.32.1", "reduce-object": "^0.1.3", @@ -60,27 +55,16 @@ "react-dom": "^15.0.1" }, "devDependencies": { - "babel": "^6.5.2", - "babel-cli": "^6.7.5", - "babel-core": "^6.18.2", - "babel-eslint": "^7.1.0", - "babel-jest": "^17.0.2", - "babel-loader": "^6.2.4", - "babel-plugin-transform-es2015-modules-commonjs": "^6.10.3", - "babel-plugin-transform-es2015-modules-commonjs-simple": "^6.7.4", - "babel-plugin-transform-react-constant-elements": "^6.9.1", - "babel-plugin-transform-react-inline-elements": "^6.8.0", - "babel-plugin-transform-runtime": "^6.15.0", - "babel-preset-philpl": "^0.4.0", - "eslint": "^3.9.1", - "eslint-config-airbnb-base": "^9.0.0", - "eslint-plugin-import": "^2.1.0", - "eslint-plugin-react": "^6.5.0", + "@types/mapbox-gl": "^0.29.0", + "@types/react": "^15.0.6", + "@types/typescript": "^2.0.0", "jest": "^17.0.1", "react": "^15.4.0", "react-addons-test-utils": "^15.4.0", "react-dom": "^15.4.0", "react-test-renderer": "^15.4.0", - "recompose": "^0.20.2" + "recompose": "^0.20.2", + "tslint": "^4.4.2", + "tslint-react": "^2.3.0" } } diff --git a/src/constants/css.js b/src/constants/css.ts similarity index 99% rename from src/constants/css.js rename to src/constants/css.ts index dd40c6721..783baf4b9 100644 --- a/src/constants/css.js +++ b/src/constants/css.ts @@ -1,3 +1,4 @@ +// tslint:disable export default ` .mapboxgl-map { font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif; diff --git a/src/projected-layer.js b/src/projected-layer.tsx similarity index 56% rename from src/projected-layer.js rename to src/projected-layer.tsx index 3ff04d420..5e6e4202d 100644 --- a/src/projected-layer.js +++ b/src/projected-layer.tsx @@ -1,52 +1,64 @@ -import React, { PropTypes } from 'react'; +import * as React from 'react'; +import { Map } from 'mapbox-gl'; + import { - OverlayPropTypes, overlayState, overlayTransform, - anchors, + anchors } from './util/overlays'; const defaultStyle = { - zIndex: 3, + zIndex: 3 }; -export default class ProjectedLayer extends React.Component { - static contextTypes = { - map: PropTypes.object, - }; +interface Props { + coordinates: number[]; + anchor: any; + offset: any; + children: JSX.Element; + onClick: React.MouseEventHandler; + onMouseEnter: React.MouseEventHandler; + onMouseLeave: React.MouseEventHandler; + style: React.CSSProperties; + className: string; +} - static propTypes = { - coordinates: PropTypes.arrayOf(PropTypes.number).isRequired, - anchor: OverlayPropTypes.anchor, - offset: OverlayPropTypes.offset, - children: PropTypes.node, - onClick: PropTypes.func, - onMouseEnter: PropTypes.func, - onMouseLeave: PropTypes.func, - style: PropTypes.object, +// interface State {} + +interface Context { + map: Map; +} + +export default class ProjectedLayer extends React.Component { + public context: Context; + private container: HTMLElement; + private prevent: boolean; + + public static contextTypes = { + map: React.PropTypes.object }; - static defaultProps = { + public static defaultProps = { anchor: anchors[0], offset: 0, - onClick: (...args) => args, + onClick: (...args: any[]) => args }; - state = {}; + public state = {}; - setContainer = (el) => { + private setContainer = (el: HTMLElement) => { if (el) { this.container = el; } - }; + } - handleMapMove = () => { + private handleMapMove = () => { if (!this.prevent) { this.setState(overlayState(this.props, this.context.map, this.container)); } }; - componentDidMount() { + public componentDidMount() { const { map } = this.context; map.on('move', this.handleMapMove); @@ -55,7 +67,7 @@ export default class ProjectedLayer extends React.Component { this.handleMapMove(); } - componentWillReceiveProps(nextProps) { + public componentWillReceiveProps(nextProps: Props) { const { coordinates } = this.props; if ( @@ -66,7 +78,7 @@ export default class ProjectedLayer extends React.Component { } } - componentWillUnmount() { + public componentWillUnmount() { const { map } = this.context; this.prevent = true; @@ -74,7 +86,7 @@ export default class ProjectedLayer extends React.Component { map.off('move', this.handleMapMove); } - render() { + public render() { const { style, children, @@ -97,8 +109,9 @@ export default class ProjectedLayer extends React.Component { onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} style={finalStyle} - ref={this.setContainer}> - { children } + ref={this.setContainer} + > + {children} ); } diff --git a/src/scale-control.js b/src/scale-control.tsx similarity index 60% rename from src/scale-control.js rename to src/scale-control.tsx index f7f42d400..a71f20996 100644 --- a/src/scale-control.js +++ b/src/scale-control.tsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react'; +import * as React from 'react'; +import { Map } from 'mapbox-gl'; const scales = [ 0.01, 0.02, 0.05, @@ -7,7 +8,7 @@ const scales = [ 10, 20, 50, 100, 200, 500, 1000, 2 * 1000, 5 * 1000, - 10 * 1000, + 10 * 1000 ]; const positions = { @@ -50,66 +51,85 @@ const KILOMETER_IN_METERS = 1000; const MIN_WIDTH_SCALE = 40; -export default class ScaleControl extends Component { - static contextTypes = { - map: PropTypes.object, - }; +type Measurement = 'km' | 'mi'; +type Position = 'topRight' | 'topLeft' | 'bottomRight' | 'bottomLeft'; + +interface Props { + measurement: Measurement; + position: Position; + style: React.CSSProperties; +} + +interface State { + chosenScale: number; + scaleWidth: number; +} + +interface Context { + map: Map; +} + +export default class ScaleControl extends React.Component { + public context: Context; - static propTypes = { - measurement: PropTypes.oneOf(MEASUREMENTS), - style: PropTypes.object, - position: PropTypes.string, + public static contextTypes = { + map: React.PropTypes.object }; - static defaultProps = { + public static defaultProps = { measurement: MEASUREMENTS[0], - position: POSITIONS[2], + position: POSITIONS[2] }; - state = { - chosenScale: false, - scaleWidth: MIN_WIDTH_SCALE, + public state = { + chosenScale: 0, + scaleWidth: MIN_WIDTH_SCALE }; - componentWillMount() { - const { map } = this.context; - this.setScale(map); + public componentWillMount() { + this.setScale(); - map.on('zoomend', () => { - this.setScale(map); - }); + this.context.map.on('zoomend', this.setScale); } - componentWillUnmount() { - if (this.state.map) { - this.state.map.off(); + public componentWillUnmount() { + if (this.context.map) { + this.context.map.off('zoomend', this.setScale); } } - setScale = (map) => { + private setScale = () => { + const { map } = this.context; const { measurement } = this.props; - const clientWidth = map._canvas.clientWidth; - const { _ne, _sw } = map.getBounds(); + const clientWidth = (map as any)._canvas.clientWidth; + const { ne, sw } = map.getBounds() as any; const totalWidth = this._getDistanceTwoPoints( - [_sw.lng, _ne.lat], - [_ne.lng, _ne.lat], + [sw.lng, ne.lat], + [sw.lng, ne.lat], measurement ); const relativeWidth = totalWidth / clientWidth * MIN_WIDTH_SCALE; - const chosenScale = scales.reduce((acc, curr) => acc || (curr > relativeWidth && curr), 0); - const scaleWidth = chosenScale / totalWidth * map._canvas.width; + const chosenScale = scales.reduce((acc, curr) => { + if (curr > relativeWidth) { + return curr; + } + + return acc; + }, 0); + + const scaleWidth = chosenScale / totalWidth * (map as any)._canvas.width; this.setState({ chosenScale, - scaleWidth, + scaleWidth }); }; - _getDistanceTwoPoints(x, y, measurement = 'km') { + private _getDistanceTwoPoints(x: number[], y: number[], measurement = 'km') { const [lng1, lat1] = x; const [lng2, lat2] = y; @@ -129,11 +149,11 @@ export default class ScaleControl extends Component { return d; } - _deg2rad(deg) { + private _deg2rad(deg: number) { return deg * (Math.PI / 180); } - _displayMeasurement(measurement, chosenScale) { + private _displayMeasurement(measurement: Measurement, chosenScale: number) { if (chosenScale >= 1) { return `${chosenScale} ${measurement}`; } @@ -145,23 +165,15 @@ export default class ScaleControl extends Component { return `${Math.floor(chosenScale * KILOMETER_IN_METERS)} m`; } - render() { + public render() { const { measurement, style, position } = this.props; const { chosenScale, scaleWidth } = this.state; return ( -
+
-
- + style={{ ...scaleStyle, width: scaleWidth }} + />
{this._displayMeasurement(measurement, chosenScale)}
diff --git a/src/source.js b/src/source.js deleted file mode 100644 index 0c1b6d048..000000000 --- a/src/source.js +++ /dev/null @@ -1,54 +0,0 @@ -import React, { PropTypes } from 'react'; - -export default class Source extends React.Component { - - static contextTypes = { - map: PropTypes.object, - }; - - static propTypes = { - id: PropTypes.string.isRequired, - sourceOptions: PropTypes.object, - }; - - id = this.props.id; - - source = { - ...this.props.sourceOptions, - }; - - componentWillMount() { - const { map } = this.context; - if (!map.getSource(this.id)) { - map.addSource(this.id, this.source); - } - } - - componentWillUnmount() { - const { map } = this.context; - if (map.getSource(this.id)) { - map.removeSource(this.id); - } - } - - componentWillReceiveProps(props) { - const { id } = this; - const { sourceOptions } = this.props; - const { map } = this.context; - - if (props.sourceOptions.data !== sourceOptions.data) { - map - .getSource(id) - .setData(props.sourceOptions.data); - } - } - - shouldComponentUpdate(nextProps) { - return nextProps.sourceOptions.data !== this.props.sourceOptions.data; - } - - render() { - return null; - } - -} diff --git a/src/source.tsx b/src/source.tsx new file mode 100644 index 000000000..862088e18 --- /dev/null +++ b/src/source.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import { + Map, + VectorSource, + RasterSource, + GeoJSONSource, + ImageSource, + VideoSource, + GeoJSONSourceRaw +} from 'mapbox-gl'; + +interface Context { + map: Map; +} + +type Sources = VectorSource | RasterSource | GeoJSONSource | ImageSource | VideoSource | GeoJSONSourceRaw; + +interface Props { + id: string; + sourceOptions: Sources; +} + +export default class Source extends React.Component { + public context: Context; + + public static contextTypes = { + map: React.PropTypes.object + }; + + private id = this.props.id; + + public componentWillMount() { + const { map } = this.context; + if (!map.getSource(this.id)) { + map.addSource(this.id, this.props.sourceOptions); + } + } + + public componentWillUnmount() { + const { map } = this.context; + if (map.getSource(this.id)) { + map.removeSource(this.id); + } + } + + public render() { + return null; + } + +} diff --git a/src/util/diff.js b/src/util/diff.ts similarity index 84% rename from src/util/diff.js rename to src/util/diff.ts index 3058203ea..b7c894b85 100644 --- a/src/util/diff.js +++ b/src/util/diff.ts @@ -1,7 +1,7 @@ import reduce from 'reduce-object'; const find = (obj, predicate) => ( - Object.keys(obj).find(key => predicate(obj[key], key)) + Object.keys(obj).find((key) => predicate(obj[key], key)) ); const diff = (obj1, obj2) => ( diff --git a/src/zoom-control.js b/src/zoom-control.js deleted file mode 100644 index 580170fa2..000000000 --- a/src/zoom-control.js +++ /dev/null @@ -1,118 +0,0 @@ -import React, { Component, PropTypes } from 'react'; - -const containerStyle = { - position: 'absolute', - zIndex: 10, - display: 'flex', - flexDirection: 'column', - boxShadow: '0px 1px 4px rgba(0, 0, 0, .3)', - border: '1px solid rgba(0, 0, 0, 0.1)', -}; - -const positions = { - topRight: { top: 10, right: 10, bottom: 'auto', left: 'auto' }, - topLeft: { top: 10, left: 10, bottom: 'auto', right: 'auto' }, - bottomRight: { bottom: 10, right: 10, top: 'auto', left: 'auto' }, - bottomLeft: { bottom: 10, left: 10, top: 'auto', right: 'auto' }, -}; - -const buttonStyle = { - backgroundColor: '#f9f9f9', - opacity: 0.95, - transition: 'background-color 0.16s ease-out', - cursor: 'pointer', - border: 0, - height: 26, - width: 26, - backgroundImage: "url('https://api.mapbox.com/mapbox.js/v2.4.0/images/icons-000000@2x.png')", - backgroundPosition: '0px 0px', - backgroundSize: '26px 260px', - outline: 0, -}; - -const buttonStyleHovered = { - backgroundColor: '#fff', - opacity: 1, -}; - -const buttonStylePlus = { - borderBottom: '1px solid rgba(0, 0, 0, 0.1)', - borderTopLeftRadius: 2, - borderTopRightRadius: 2, -}; - -const buttonStyleMinus = { - backgroundPosition: '0px -26px', - borderBottomLeftRadius: 2, - borderBottomRightRadius: 2, -}; - -const [PLUS, MINUS] = [0, 1]; -const POSITIONS = Object.keys(positions); - -export default class ZoomControl extends Component { - static propTypes = { - zoomDiff: PropTypes.number, - onControlClick: PropTypes.func, - position: PropTypes.oneOf(POSITIONS), - style: PropTypes.object, - }; - - static defaultProps = { - position: POSITIONS[0], - zoomDiff: 0.5, - onControlClick: (map, zoomDiff) => { - map.zoomTo(map.getZoom() + zoomDiff); - }, - }; - - state = { - hover: undefined, - }; - - static contextTypes = { - map: PropTypes.object, - }; - - onMouseAction = (hover) => { - if (hover !== this.state.hover) { - this.setState({ hover }); - } - }; - - render() { - const { onControlClick, zoomDiff, position, style } = this.props; - const { hover } = this.state; - const { map } = this.context; - - return ( -
- - -
- ); - } -} diff --git a/src/zoom-control.tsx b/src/zoom-control.tsx new file mode 100644 index 000000000..e4aef70c7 --- /dev/null +++ b/src/zoom-control.tsx @@ -0,0 +1,138 @@ +import * as React from 'react'; +import { Map } from 'mapbox-gl'; + +const containerStyle = { + position: 'absolute', + zIndex: 10, + display: 'flex', + flexDirection: 'column', + boxShadow: '0px 1px 4px rgba(0, 0, 0, .3)', + border: '1px solid rgba(0, 0, 0, 0.1)', +}; + +const positions = { + topRight: { top: 10, right: 10, bottom: 'auto', left: 'auto' }, + topLeft: { top: 10, left: 10, bottom: 'auto', right: 'auto' }, + bottomRight: { bottom: 10, right: 10, top: 'auto', left: 'auto' }, + bottomLeft: { bottom: 10, left: 10, top: 'auto', right: 'auto' }, +}; + +const buttonStyle = { + backgroundColor: '#f9f9f9', + opacity: 0.95, + transition: 'background-color 0.16s ease-out', + cursor: 'pointer', + border: 0, + height: 26, + width: 26, + backgroundImage: 'url(\'https://api.mapbox.com/mapbox.js/v2.4.0/images/icons-000000@2x.png\')', + backgroundPosition: '0px 0px', + backgroundSize: '26px 260px', + outline: 0, +}; + +const buttonStyleHovered = { + backgroundColor: '#fff', + opacity: 1, +}; + +const buttonStylePlus = { + borderBottom: '1px solid rgba(0, 0, 0, 0.1)', + borderTopLeftRadius: 2, + borderTopRightRadius: 2, +}; + +const buttonStyleMinus = { + backgroundPosition: '0px -26px', + borderBottomLeftRadius: 2, + borderBottomRightRadius: 2, +}; + +const [PLUS, MINUS] = [0, 1]; +const POSITIONS = Object.keys(positions); + +interface Props { + zoomDiff: number; + onControlClick: (map: Map, zoomDiff: number) => void; + position: 'topRight' | 'topLeft' | 'bottomRight' | 'bottomLeft'; + style: React.CSSProperties; +} + +interface State { + hover?: number; +} + +interface Context { + map: Map; +} + +export default class ZoomControl extends React.Component { + + public context: Context; + + public static defaultProps = { + position: POSITIONS[0], + zoomDiff: 0.5, + onControlClick: (map: Map, zoomDiff: number) => { + map.zoomTo(map.getZoom() + zoomDiff); + }, + }; + + public state = { + hover: undefined + }; + + public static contextTypes = { + map: React.PropTypes.object + }; + + private onMouseOut = () => { + if (!this.state.hover) { + this.setState({ hover: undefined }); + } + } + + private plusOver = () => { + if (PLUS !== this.state.hover) { + this.setState({ hover: PLUS }); + } + }; + + private minusOver = () => { + if (MINUS !== this.state.hover) { + this.setState({ hover: MINUS }); + } + }; + + private onClickPlus = () => { + this.props.onControlClick(this.context.map, this.props.zoomDiff); + } + + private onClickMinus = () => { + this.props.onControlClick(this.context.map, -this.props.zoomDiff); + } + + public render() { + const { position, style } = this.props; + const { hover } = this.state; + + return ( +
+
+ ); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..059d0fc82 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "outDir": "lib", + "module": "commonjs", + "target": "es6", + "sourceMap": true, + "allowJs": true, + "moduleResolution": "node", + "rootDir": "src", + "jsx": "preserve", + "removeComments": true, + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true, + "suppressImplicitAnyIndexErrors": true, + "noUnusedLocals": true + }, + "exclude": [ + "node_modules", + "lib", + "src/__tests__" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 000000000..bc4693883 --- /dev/null +++ b/tslint.json @@ -0,0 +1,12 @@ +{ + "extends": ["tslint:latest", "tslint-react"], + "rules": { + "quotemark": [true, "single", "jsx-double"], + "ordered-imports": false, + "trailing-comma": [true, {"multiline": "never", "singleline": "never"}], + "interface-name": [true, "never-prefix"], + "no-console": false, + "object-literal-sort-keys": false, + "member-ordering": false + } +} From 082a0b84a6c25ccfb7693c400bcf577bd6b772ad Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Fri, 3 Feb 2017 23:55:16 +0000 Subject: [PATCH 02/32] Convert all files to ts --- src/cluster.js | 100 ---------------- src/constants/css.ts | 2 +- src/feature.js | 17 --- src/geojson-layer.js | 130 --------------------- src/index.js | 30 ----- src/layer.js | 223 ----------------------------------- src/map.js | 257 ----------------------------------------- src/marker.js | 30 ----- src/popup.js | 40 ------- src/util/diff.ts | 2 +- src/util/inject-css.js | 12 -- src/util/overlays.js | 136 ---------------------- 12 files changed, 2 insertions(+), 977 deletions(-) delete mode 100644 src/cluster.js delete mode 100644 src/feature.js delete mode 100644 src/geojson-layer.js delete mode 100644 src/index.js delete mode 100644 src/layer.js delete mode 100644 src/map.js delete mode 100644 src/marker.js delete mode 100644 src/popup.js delete mode 100644 src/util/inject-css.js delete mode 100644 src/util/overlays.js diff --git a/src/cluster.js b/src/cluster.js deleted file mode 100644 index b760caefd..000000000 --- a/src/cluster.js +++ /dev/null @@ -1,100 +0,0 @@ -import React, { PropTypes, Component } from 'react'; -import supercluster from 'supercluster'; - -export default class Cluster extends Component { - - static propTypes = { - ClusterMarkerFactory: PropTypes.func.isRequired, - clusterThreshold: PropTypes.number, - radius: PropTypes.number, - minZoom: PropTypes.number, - maxZoom: PropTypes.number, - extent: PropTypes.number, - nodeSize: PropTypes.number, - log: PropTypes.bool, - }; - - static contextTypes = { - map: PropTypes.object, - }; - - static defaultProps = { - clusterThreshold: 1, - radius: 60, - minZoom: 0, - maxZoom: 16, - extent: 512, - nodeSize: 64, - log: false, - }; - - state = { - clusterIndex: supercluster({ - radius: this.props.radius, - maxZoom: this.props.maxZoom, - }), - clusterPoints: [], - }; - - componentWillMount() { - const { map } = this.context; - const { clusterIndex } = this.state; - - const features = this.childrenToFeatures(this.props.children); - clusterIndex.load(features); - - // TODO: Debounce ? - map.on('move', this.mapChange); - map.on('zoom', this.mapChange); - this.mapChange(); - } - - mapChange = () => { - const { map } = this.context; - const { clusterIndex, clusterPoints } = this.state; - - const { _sw, _ne } = map.getBounds(); - const zoom = map.getZoom(); - const newPoints = clusterIndex.getClusters( - [_sw.lng, _sw.lat, _ne.lng, _ne.lat], - Math.round(zoom) - ); - - if (newPoints.length !== clusterPoints.length) { - this.setState({ clusterPoints: newPoints }); - } - }; - - feature(coordinates) { - return { - type: 'Feature', - geometry: { - type: 'point', - coordinates, - }, - properties: { - point_count: 1, - }, - }; - } - - childrenToFeatures(children) { - return children.map(child => this.feature(child.props.coordinates)); - } - - render() { - const { children, ClusterMarkerFactory, clusterThreshold } = this.props; - const { clusterPoints } = this.state; - - return ( -
- { - clusterPoints.length <= clusterThreshold ? - children : - clusterPoints.map(({ geometry, properties }) => - ClusterMarkerFactory(geometry.coordinates, properties.point_count)) - } -
- ); - } -} diff --git a/src/constants/css.ts b/src/constants/css.ts index 783baf4b9..6dd20c98f 100644 --- a/src/constants/css.ts +++ b/src/constants/css.ts @@ -245,4 +245,4 @@ export default ` display:none; } } -`; +` as string; diff --git a/src/feature.js b/src/feature.js deleted file mode 100644 index b9f3260ea..000000000 --- a/src/feature.js +++ /dev/null @@ -1,17 +0,0 @@ -import React, { PropTypes } from 'react'; - -class Feature extends React.PureComponent { - render() { - return null; - } -} - -Feature.propTypes = { - coordinates: PropTypes.array.isRequired, - onClick: PropTypes.func, - onHover: PropTypes.func, - onEndHover: PropTypes.func, - properties: PropTypes.object, -}; - -export default Feature; diff --git a/src/geojson-layer.js b/src/geojson-layer.js deleted file mode 100644 index c85d33124..000000000 --- a/src/geojson-layer.js +++ /dev/null @@ -1,130 +0,0 @@ -import React, { PropTypes } from 'react'; -import isEqual from 'deep-equal'; -import diff from './util/diff'; - -let index = 0; -const generateID = () => { - const newId = index + 1; - index = newId; - return index; -}; - -export default class GeoJSONLayer extends React.PureComponent { - static contextTypes = { - map: PropTypes.object, - }; - - static propTypes = { - id: PropTypes.string, - - data: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.object, - ]).isRequired, - - lineLayout: PropTypes.object, - symbolLayout: PropTypes.object, - circleLayout: PropTypes.object, - fillLayout: PropTypes.object, - - linePaint: PropTypes.object, - symbolPaint: PropTypes.object, - circlePaint: PropTypes.object, - fillPaint: PropTypes.object, - - sourceOptions: PropTypes.string, - before: PropTypes.string, - }; - - id = this.props.id || `geojson-${generateID()}`; - - source = { - type: 'geojson', - ...this.props.sourceOptions, - data: this.props.data, - }; - - layerIds = []; - - createLayer = (type) => { - const { id, layerIds } = this; - const { before } = this.props; - const { map } = this.context; - - const layerId = `${id}-${type}`; - layerIds.push(layerId); - - const paint = this.props[`${type}Paint`] || {}; - const layout = this.props[`${type}Layout`] || {}; - - map.addLayer({ - id: layerId, - source: id, - type, - paint, - layout, - }, before); - }; - - componentWillMount() { - const { id, source } = this; - const { map } = this.context; - - map.addSource(id, source); - - this.createLayer('symbol'); - this.createLayer('line'); - this.createLayer('fill'); - this.createLayer('circle'); - } - - componentWillUnmount() { - const { id, layerIds } = this; - const { map } = this.context; - - map.removeSource(id); - - layerIds.forEach(key => map.removeLayer(key)); - } - - componentWillReceiveProps(props) { - const { id } = this; - const { data, paint, layout } = this.props; - const { map } = this.context; - - if (!isEqual(props.paint, paint)) { - const paintDiff = diff(paint, props.paint); - - Object.keys(paintDiff).forEach((key) => { - map.setPaintProperty(this.id, key, paintDiff[key]); - }); - } - - if (!isEqual(props.layout, layout)) { - const layoutDiff = diff(layout, props.layout); - - Object.keys(layoutDiff).forEach((key) => { - map.setLayoutProperty(this.id, key, layoutDiff[key]); - }); - } - - if (props.data !== data) { - map - .getSource(id) - .setData(props.data); - } - } - - shouldComponentUpdate(nextProps) { - return ( - !isEqual(nextProps.paint, this.props.paint) || - !isEqual(nextProps.layout, this.props.layout) || - nextProps.data !== this.props.data - ); - } - - render() { - return null; - } -} - diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 908ec87ee..000000000 --- a/src/index.js +++ /dev/null @@ -1,30 +0,0 @@ -// Add a style tag to the document's head for the map's styling -import injectCSS from './util/inject-css'; -import Map from './map'; -import Layer from './layer'; -import GeoJSONLayer from './geojson-layer'; -import Feature from './feature'; -import ZoomControl from './zoom-control'; -import Popup from './popup'; -import ScaleControl from './scale-control'; -import Marker from './marker'; -import Source from './source'; -import Cluster from './cluster'; - -injectCSS(window); - -export { - Feature, - Layer, - GeoJSONLayer, - Map, - Popup, - ZoomControl, - ScaleControl, - Marker, - Source, - Cluster, -}; - -export default Map; - diff --git a/src/layer.js b/src/layer.js deleted file mode 100644 index bba78350c..000000000 --- a/src/layer.js +++ /dev/null @@ -1,223 +0,0 @@ -import React, { PropTypes } from 'react'; -import isEqual from 'deep-equal'; -import diff from './util/diff'; - -let index = 0; -const generateID = () => { - const newId = index + 1; - index = newId; - return index; -}; - -export default class Layer extends React.PureComponent { - static contextTypes = { - map: PropTypes.object, - }; - - static propTypes = { - id: PropTypes.string, - - type: PropTypes.oneOf([ - 'symbol', - 'line', - 'fill', - 'circle', - 'raster', - ]), - - layout: PropTypes.object, - paint: PropTypes.object, - sourceOptions: PropTypes.object, - layerOptions: PropTypes.object, - sourceId: PropTypes.string, - before: PropTypes.string, - }; - - static defaultProps = { - type: 'symbol', - layout: {}, - paint: {}, - }; - - hover = []; - - id = this.props.id || `layer-${generateID()}`; - - source = { - type: 'geojson', - ...this.props.sourceOptions, - data: { - type: 'FeatureCollection', - features: [], - }, - }; - - geometry = (coordinates) => { - switch (this.props.type) { - case 'symbol': - case 'circle': return { - type: 'Point', - coordinates, - }; - - case 'fill': return { - type: coordinates.length > 1 ? 'MultiPolygon' : 'Polygon', - coordinates, - }; - - case 'line': return { - type: 'LineString', - coordinates, - }; - - default: return null; - } - }; - - feature = (props, id) => ({ - type: 'Feature', - geometry: this.geometry(props.coordinates), - properties: { - ...props.properties, - id, - }, - }) - - onClick = (evt) => { - const children = [].concat(this.props.children); - const { map } = this.context; - const { id } = this; - const features = map.queryRenderedFeatures(evt.point, { layers: [id] }); - - features.forEach((feature) => { - const { properties } = feature; - const child = children[properties.id]; - - const onClick = child && child.props.onClick; - if (onClick) { - onClick({ ...evt, feature, map }); - } - }); - }; - - onMouseMove = (evt) => { - const children = [].concat(this.props.children); - const { map } = this.context; - const { id } = this; - - const oldHover = this.hover; - const hover = []; - - const features = map.queryRenderedFeatures(evt.point, { layers: [id] }); - - features.forEach((feature) => { - const { properties } = feature; - const child = children[properties.id]; - hover.push(properties.id); - - const onHover = child && child.props.onHover; - if (onHover) { - onHover({ ...evt, feature, map }); - } - }); - - oldHover - .filter(prevHoverId => hover.indexOf(prevHoverId) === -1) - .forEach((key) => { - const onEndHover = children[key] && children[key].props.onEndHover; - if (onEndHover) { - onEndHover({ ...evt, map }); - } - }); - - this.hover = hover; - }; - - componentWillMount() { - const { id, source } = this; - const { type, layout, paint, layerOptions, sourceId, before } = this.props; - const { map } = this.context; - - const layer = { - id, - source: sourceId || id, - type, - layout, - paint, - ...layerOptions, - }; - - if (!sourceId) { - map.addSource(id, source); - } - - map.addLayer(layer, before); - - map.on('click', this.onClick); - map.on('mousemove', this.onMouseMove); - } - - componentWillUnmount() { - const { id } = this; - - const { map } = this.context; - - map.removeLayer(id); - // if pointing to an existing source, don't remove - // as other layers may be dependent upon it - if (!this.props.sourceId) { - map.removeSource(id); - } - - map.off('click', this.onClick); - map.off('mousemove', this.onMouseMove); - } - - componentWillReceiveProps(props) { - const { paint, layout } = this.props; - const { map } = this.context; - - if (!isEqual(props.paint, paint)) { - const paintDiff = diff(paint, props.paint); - - Object.keys(paintDiff).forEach((key) => { - map.setPaintProperty(this.id, key, paintDiff[key]); - }); - } - - if (!isEqual(props.layout, layout)) { - const layoutDiff = diff(layout, props.layout); - - Object.keys(layoutDiff).forEach((key) => { - map.setLayoutProperty(this.id, key, layoutDiff[key]); - }); - } - } - - shouldComponentUpdate(nextProps) { - return !isEqual(nextProps.children, this.props.children) - || !isEqual(nextProps.paint, this.props.paint) - || !isEqual(nextProps.layout, this.props.layout); - } - - render() { - const { map } = this.context; - - if (this.props.children) { - const children = [].concat(this.props.children); - - const features = children - .map(({ props }, id) => this.feature(props, id)) - .filter(Boolean); - - const source = map.getSource(this.props.sourceId || this.id); - source.setData({ - type: 'FeatureCollection', - features, - }); - } - - return null; - } -} - diff --git a/src/map.js b/src/map.js deleted file mode 100644 index 04ab820e6..000000000 --- a/src/map.js +++ /dev/null @@ -1,257 +0,0 @@ -import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'; -import React, { Component, PropTypes } from 'react'; -import isEqual from 'deep-equal'; - -const events = { - onStyleLoad: 'style.load', // Should remain first - onResize: 'resize', - onDblClick: 'dblclick', - onClick: 'click', - onMouseMove: 'mousemove', - onMoveStart: 'mousestart', - onMove: 'move', - onMoveEnd: 'moveend', - onMouseUp: 'mouseup', - onDragStart: 'dragstart', - onDrag: 'drag', - onDragEnd: 'dragend', - onZoomStart: 'zoomstart', - onZoom: 'zoom', - onZoomEnd: 'zoomend', -}; - -export default class ReactMapboxGl extends Component { - static propTypes = { - // Events propTypes - ...Object.keys(events) - .reduce((acc, event) => ( - Object.assign({}, acc, { [event]: PropTypes.func }) - ), {}), - - // Main propTypes - style: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.object, - ]).isRequired, - accessToken: PropTypes.string.isRequired, - center: PropTypes.arrayOf(PropTypes.number), - zoom: PropTypes.arrayOf(PropTypes.number), - minZoom: PropTypes.number, - maxZoom: PropTypes.number, - maxBounds: PropTypes.array, - fitBounds: PropTypes.array, - fitBoundsOptions: PropTypes.object, - bearing: PropTypes.number, - pitch: PropTypes.number, - containerStyle: PropTypes.object, - hash: PropTypes.bool, - preserveDrawingBuffer: PropTypes.bool, - scrollZoom: PropTypes.bool, - movingMethod: PropTypes.oneOf([ - 'jumpTo', - 'easeTo', - 'flyTo', - ]), - attributionPosition: PropTypes.oneOf([ - 'top-left', - 'top-right', - 'bottom-left', - 'bottom-right', - ]), - interactive: PropTypes.bool, - dragRotate: PropTypes.bool, - }; - - static defaultProps = { - hash: false, - onStyleLoad: (...args) => args, - preserveDrawingBuffer: false, - center: [ - -0.2416815, - 51.5285582, - ], - zoom: [11], - minZoom: 0, - maxZoom: 20, - bearing: 0, - scrollZoom: true, - movingMethod: 'flyTo', - pitch: 0, - attributionPosition: 'bottom-right', - interactive: true, - dragRotate: true, - }; - - static childContextTypes = { - map: React.PropTypes.object, - }; - - state = {}; - - getChildContext = () => ({ - map: this.state.map, - }); - - componentDidMount() { - const { - style, - hash, - preserveDrawingBuffer, - accessToken, - center, - pitch, - zoom, - minZoom, - maxZoom, - maxBounds, - fitBounds, - fitBoundsOptions, - bearing, - scrollZoom, - attributionPosition, - interactive, - dragRotate, - } = this.props; - - MapboxGl.accessToken = accessToken; - - const map = new MapboxGl.Map({ - preserveDrawingBuffer, - hash, - zoom: zoom[0], - minZoom, - maxZoom, - maxBounds, - bearing, - container: this.container, - center, - pitch, - style, - scrollZoom, - attributionControl: { - position: attributionPosition, - }, - interactive, - dragRotate, - }); - - if (fitBounds) { - map.fitBounds(fitBounds, fitBoundsOptions); - } - - Object.keys(events).forEach((event, index) => { - const propEvent = this.props[event]; - - if (propEvent) { - map.on(events[event], (...args) => { - propEvent(map, ...args); - - if (index === 0) { - this.setState({ map }); - } - }); - } - }); - } - - componentWillUnmount() { - const { map } = this.state; - - if (map) { - // Remove all events attached to the map - map.off(); - - // NOTE: We need to defer removing the map to after all children have unmounted - setTimeout(() => { - map.remove(); - }); - } - } - - shouldComponentUpdate(nextProps, nextState) { - return ( - nextProps.children !== this.props.children || - nextProps.containerStyle !== this.props.containerStyle || - nextState.map !== this.state.map || - nextProps.style !== this.props.style || - nextProps.fitBounds !== this.props.fitBounds - ); - } - - componentWillReceiveProps(nextProps) { - const { map } = this.state; - if (!map) { - return null; - } - - const center = map.getCenter(); - const zoom = map.getZoom(); - const bearing = map.getBearing(); - const pitch = map.getPitch(); - - const didZoomUpdate = ( - this.props.zoom !== nextProps.zoom && - nextProps.zoom[0] !== zoom - ); - - const didCenterUpdate = ( - this.props.center !== nextProps.center && - (nextProps.center[0] !== center.lng || nextProps.center[1] !== center.lat) - ); - - const didBearingUpdate = ( - this.props.bearing !== nextProps.bearing && - nextProps.bearing !== bearing - ); - - const didPitchUpdate = ( - this.props.pitch !== nextProps.pitch && - nextProps.pitch !== pitch - ); - - if (nextProps.fitBounds) { - const { fitBounds } = this.props; - - const didFitBoundsUpdate = ( - fitBounds !== nextProps.fitBounds || // Check for reference equality - nextProps.fitBounds.length !== (fitBounds && fitBounds.length) || // Added element - !!fitBounds.find((c, i) => { // Check for equality - const nc = nextProps.fitBounds[i]; - return c[0] !== nc[0] || c[1] !== nc[1]; - }) - ); - - if (didFitBoundsUpdate) { - map.fitBounds(nextProps.fitBounds, nextProps.fitBoundsOptions); - } - } - - if (didZoomUpdate || didCenterUpdate || didBearingUpdate || didPitchUpdate) { - map[this.props.movingMethod]({ - zoom: didZoomUpdate ? nextProps.zoom[0] : zoom, - center: didCenterUpdate ? nextProps.center : center, - bearing: didBearingUpdate ? nextProps.bearing : bearing, - pitch: didPitchUpdate ? nextProps.pitch : pitch, - }); - } - - if (!isEqual(this.props.style, nextProps.style)) { - map.setStyle(nextProps.style); - } - - return null; - } - - render() { - const { containerStyle, children } = this.props; - const { map } = this.state; - - return ( -
{ this.container = x; }} style={containerStyle}> - { - map && children - } -
- ); - } -} diff --git a/src/marker.js b/src/marker.js deleted file mode 100644 index 95d43f194..000000000 --- a/src/marker.js +++ /dev/null @@ -1,30 +0,0 @@ -import React, { PropTypes } from 'react'; -import ProjectedLayer from './projected-layer'; -import { - OverlayPropTypes, -} from './util/overlays'; - -const propsToRemove = { children: undefined }; - -export default class Marker extends React.Component { - static propTypes = { - coordinates: PropTypes.arrayOf(PropTypes.number).isRequired, - anchor: OverlayPropTypes.anchor, - offset: OverlayPropTypes.offset, - children: PropTypes.node, - style: PropTypes.object, - }; - - render() { - const { children } = this.props; - const nestedProps = Object.assign({}, this.props, propsToRemove); - - return ( - - { children } - - ); - } -} diff --git a/src/popup.js b/src/popup.js deleted file mode 100644 index a2a6eef2c..000000000 --- a/src/popup.js +++ /dev/null @@ -1,40 +0,0 @@ -import React, { PropTypes } from 'react'; -import ProjectedLayer from './projected-layer'; -import { - anchors, - OverlayPropTypes, -} from './util/overlays'; - -export default class Popup extends React.Component { - static propTypes = { - coordinates: PropTypes.arrayOf(PropTypes.number).isRequired, - anchor: OverlayPropTypes.anchor, - offset: OverlayPropTypes.offset, - children: PropTypes.node, - onClick: PropTypes.func, - style: PropTypes.object, - }; - - static defaultProps = { - anchor: anchors[0], - }; - - render() { - const { coordinates, anchor, offset, onClick, children, style } = this.props; - - return ( - -
-
- { children } -
-
- ); - } -} diff --git a/src/util/diff.ts b/src/util/diff.ts index b7c894b85..5969b8f0c 100644 --- a/src/util/diff.ts +++ b/src/util/diff.ts @@ -1,4 +1,4 @@ -import reduce from 'reduce-object'; +const reduce = require('reduce-object'); const find = (obj, predicate) => ( Object.keys(obj).find((key) => predicate(obj[key], key)) diff --git a/src/util/inject-css.js b/src/util/inject-css.js deleted file mode 100644 index a2e79e344..000000000 --- a/src/util/inject-css.js +++ /dev/null @@ -1,12 +0,0 @@ -import cssRules from '../constants/css'; - -export default function injectCSS(window) { - if (window && typeof window === 'object' && window.document) { - const { document } = window; - const head = (document.head || document.getElementsByTagName('head')[0]); - - const styleElement = document.createElement('style'); - styleElement.innerHTML = cssRules; - head.appendChild(styleElement); - } -} diff --git a/src/util/overlays.js b/src/util/overlays.js deleted file mode 100644 index 1914cc96b..000000000 --- a/src/util/overlays.js +++ /dev/null @@ -1,136 +0,0 @@ -import { LngLat, Point } from 'mapbox-gl/dist/mapbox-gl.js'; -import { PropTypes } from 'react'; - -export const anchors = [ - 'center', - 'top', - 'bottom', - 'left', - 'right', - 'top-left', - 'top-right', - 'bottom-left', - 'bottom-right', -]; - -const anchorTranslates = { - center: 'translate(-50%,-50%)', - top: 'translate(-50%,0)', - left: 'translate(0,-50%)', - right: 'translate(-100%,-50%)', - bottom: 'translate(-50%,-100%)', - 'top-left': 'translate(0,0)', - 'top-right': 'translate(-100%,0)', - 'bottom-left': 'translate(0,-100%)', - 'bottom-right': 'translate(-100%,-100%)', -}; - -const defaultElement = { offsetWidth: 0, offsetHeight: 0 }; - -const isPointLike = input => (input instanceof Point || Array.isArray(input)); - -const projectCoordinates = (map, coordinates) => map.project(LngLat.convert(coordinates)); - -const calculateAnchor = (map, offsets, position, { offsetHeight, offsetWidth }) => { - let anchor = null; - - if (position.y + offsets.bottom.y - offsetHeight < 0) { - anchor = [anchors[1]]; - } else if (position.y + offsets.top.y + offsetHeight > map.transform.height) { - anchor = [anchors[2]]; - } else { - anchor = []; - } - - if (position.x < offsetWidth / 2) { - anchor.push(anchors[3]); - } else if (position.x > map.transform.width - offsetWidth / 2) { - anchor.push(anchors[4]); - } - - if (anchor.length === 0) { - anchor = anchors[2]; - } else { - anchor = anchor.join('-'); - } - return anchor; -}; - -const normalizedOffsets = (offset) => { - if (!offset) { - return normalizedOffsets(new Point(0, 0)); - } - - if (typeof offset === 'number') { - // input specifies a radius from which to calculate offsets at all positions - const cornerOffset = Math.round(Math.sqrt(0.5 * Math.pow(offset, 2))); - return { - center: new Point(offset, offset), - top: new Point(0, offset), - bottom: new Point(0, -offset), - left: new Point(offset, 0), - right: new Point(-offset, 0), - 'top-left': new Point(cornerOffset, cornerOffset), - 'top-right': new Point(-cornerOffset, cornerOffset), - 'bottom-left': new Point(cornerOffset, -cornerOffset), - 'bottom-right': new Point(-cornerOffset, -cornerOffset), - }; - } - - if (isPointLike(offset)) { - // input specifies a single offset to be applied to all positions - return anchors.reduce((res, anchor) => { - const tmp = Object.assign({}, res); - tmp[anchor] = Point.convert(offset); - return tmp; - }, {}); - } - - // input specifies an offset per position - return anchors.reduce((res, anchor) => { - const tmp = Object.assign({}, res); - tmp[anchor] = Point.convert(offset[anchor] || [0, 0]); - return tmp; - }, {}); -}; - -export const OverlayPropTypes = { - anchor: PropTypes.oneOf(anchors), - offset: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.arrayOf(PropTypes.number), - PropTypes.object, - ]), -}; - -export const overlayState = (props, map, { offsetWidth, offsetHeight } = defaultElement) => { - const position = projectCoordinates(map, props.coordinates); - const offsets = normalizedOffsets(props.offset); - const anchor = props.anchor - || calculateAnchor(map, offsets, position, { offsetWidth, offsetHeight }); - - return { - anchor, - position, - offset: offsets[anchor], - }; -}; - -const moveTranslate = point => ( - point ? `translate(${point.x.toFixed(0)}px,${point.y.toFixed(0)}px)` : '' -); - -export const overlayTransform = (args) => { - const { anchor, position, offset } = args; - const res = [moveTranslate(position)]; - - if (offset && offset.x !== undefined && offset.y !== undefined) { - res.push(moveTranslate(offset)); - } - - if (anchor) { - res.push(anchorTranslates[anchor]); - } - - return res; -}; From a14c00af2e358312858ea935ebfd4972844fa279 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Fri, 3 Feb 2017 23:55:30 +0000 Subject: [PATCH 03/32] Add missing files --- src/cluster.tsx | 100 ++++++++++++++++ src/feature.ts | 17 +++ src/geojson-layer.ts | 130 +++++++++++++++++++++ src/index.ts | 30 +++++ src/layer.ts | 223 +++++++++++++++++++++++++++++++++++ src/map.tsx | 257 +++++++++++++++++++++++++++++++++++++++++ src/marker.tsx | 26 +++++ src/popup.tsx | 37 ++++++ src/util/inject-css.ts | 14 +++ src/util/overlays.ts | 135 ++++++++++++++++++++++ 10 files changed, 969 insertions(+) create mode 100644 src/cluster.tsx create mode 100644 src/feature.ts create mode 100644 src/geojson-layer.ts create mode 100644 src/index.ts create mode 100644 src/layer.ts create mode 100644 src/map.tsx create mode 100644 src/marker.tsx create mode 100644 src/popup.tsx create mode 100644 src/util/inject-css.ts create mode 100644 src/util/overlays.ts diff --git a/src/cluster.tsx b/src/cluster.tsx new file mode 100644 index 000000000..b760caefd --- /dev/null +++ b/src/cluster.tsx @@ -0,0 +1,100 @@ +import React, { PropTypes, Component } from 'react'; +import supercluster from 'supercluster'; + +export default class Cluster extends Component { + + static propTypes = { + ClusterMarkerFactory: PropTypes.func.isRequired, + clusterThreshold: PropTypes.number, + radius: PropTypes.number, + minZoom: PropTypes.number, + maxZoom: PropTypes.number, + extent: PropTypes.number, + nodeSize: PropTypes.number, + log: PropTypes.bool, + }; + + static contextTypes = { + map: PropTypes.object, + }; + + static defaultProps = { + clusterThreshold: 1, + radius: 60, + minZoom: 0, + maxZoom: 16, + extent: 512, + nodeSize: 64, + log: false, + }; + + state = { + clusterIndex: supercluster({ + radius: this.props.radius, + maxZoom: this.props.maxZoom, + }), + clusterPoints: [], + }; + + componentWillMount() { + const { map } = this.context; + const { clusterIndex } = this.state; + + const features = this.childrenToFeatures(this.props.children); + clusterIndex.load(features); + + // TODO: Debounce ? + map.on('move', this.mapChange); + map.on('zoom', this.mapChange); + this.mapChange(); + } + + mapChange = () => { + const { map } = this.context; + const { clusterIndex, clusterPoints } = this.state; + + const { _sw, _ne } = map.getBounds(); + const zoom = map.getZoom(); + const newPoints = clusterIndex.getClusters( + [_sw.lng, _sw.lat, _ne.lng, _ne.lat], + Math.round(zoom) + ); + + if (newPoints.length !== clusterPoints.length) { + this.setState({ clusterPoints: newPoints }); + } + }; + + feature(coordinates) { + return { + type: 'Feature', + geometry: { + type: 'point', + coordinates, + }, + properties: { + point_count: 1, + }, + }; + } + + childrenToFeatures(children) { + return children.map(child => this.feature(child.props.coordinates)); + } + + render() { + const { children, ClusterMarkerFactory, clusterThreshold } = this.props; + const { clusterPoints } = this.state; + + return ( +
+ { + clusterPoints.length <= clusterThreshold ? + children : + clusterPoints.map(({ geometry, properties }) => + ClusterMarkerFactory(geometry.coordinates, properties.point_count)) + } +
+ ); + } +} diff --git a/src/feature.ts b/src/feature.ts new file mode 100644 index 000000000..b9f3260ea --- /dev/null +++ b/src/feature.ts @@ -0,0 +1,17 @@ +import React, { PropTypes } from 'react'; + +class Feature extends React.PureComponent { + render() { + return null; + } +} + +Feature.propTypes = { + coordinates: PropTypes.array.isRequired, + onClick: PropTypes.func, + onHover: PropTypes.func, + onEndHover: PropTypes.func, + properties: PropTypes.object, +}; + +export default Feature; diff --git a/src/geojson-layer.ts b/src/geojson-layer.ts new file mode 100644 index 000000000..c85d33124 --- /dev/null +++ b/src/geojson-layer.ts @@ -0,0 +1,130 @@ +import React, { PropTypes } from 'react'; +import isEqual from 'deep-equal'; +import diff from './util/diff'; + +let index = 0; +const generateID = () => { + const newId = index + 1; + index = newId; + return index; +}; + +export default class GeoJSONLayer extends React.PureComponent { + static contextTypes = { + map: PropTypes.object, + }; + + static propTypes = { + id: PropTypes.string, + + data: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + ]).isRequired, + + lineLayout: PropTypes.object, + symbolLayout: PropTypes.object, + circleLayout: PropTypes.object, + fillLayout: PropTypes.object, + + linePaint: PropTypes.object, + symbolPaint: PropTypes.object, + circlePaint: PropTypes.object, + fillPaint: PropTypes.object, + + sourceOptions: PropTypes.string, + before: PropTypes.string, + }; + + id = this.props.id || `geojson-${generateID()}`; + + source = { + type: 'geojson', + ...this.props.sourceOptions, + data: this.props.data, + }; + + layerIds = []; + + createLayer = (type) => { + const { id, layerIds } = this; + const { before } = this.props; + const { map } = this.context; + + const layerId = `${id}-${type}`; + layerIds.push(layerId); + + const paint = this.props[`${type}Paint`] || {}; + const layout = this.props[`${type}Layout`] || {}; + + map.addLayer({ + id: layerId, + source: id, + type, + paint, + layout, + }, before); + }; + + componentWillMount() { + const { id, source } = this; + const { map } = this.context; + + map.addSource(id, source); + + this.createLayer('symbol'); + this.createLayer('line'); + this.createLayer('fill'); + this.createLayer('circle'); + } + + componentWillUnmount() { + const { id, layerIds } = this; + const { map } = this.context; + + map.removeSource(id); + + layerIds.forEach(key => map.removeLayer(key)); + } + + componentWillReceiveProps(props) { + const { id } = this; + const { data, paint, layout } = this.props; + const { map } = this.context; + + if (!isEqual(props.paint, paint)) { + const paintDiff = diff(paint, props.paint); + + Object.keys(paintDiff).forEach((key) => { + map.setPaintProperty(this.id, key, paintDiff[key]); + }); + } + + if (!isEqual(props.layout, layout)) { + const layoutDiff = diff(layout, props.layout); + + Object.keys(layoutDiff).forEach((key) => { + map.setLayoutProperty(this.id, key, layoutDiff[key]); + }); + } + + if (props.data !== data) { + map + .getSource(id) + .setData(props.data); + } + } + + shouldComponentUpdate(nextProps) { + return ( + !isEqual(nextProps.paint, this.props.paint) || + !isEqual(nextProps.layout, this.props.layout) || + nextProps.data !== this.props.data + ); + } + + render() { + return null; + } +} + diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 000000000..908ec87ee --- /dev/null +++ b/src/index.ts @@ -0,0 +1,30 @@ +// Add a style tag to the document's head for the map's styling +import injectCSS from './util/inject-css'; +import Map from './map'; +import Layer from './layer'; +import GeoJSONLayer from './geojson-layer'; +import Feature from './feature'; +import ZoomControl from './zoom-control'; +import Popup from './popup'; +import ScaleControl from './scale-control'; +import Marker from './marker'; +import Source from './source'; +import Cluster from './cluster'; + +injectCSS(window); + +export { + Feature, + Layer, + GeoJSONLayer, + Map, + Popup, + ZoomControl, + ScaleControl, + Marker, + Source, + Cluster, +}; + +export default Map; + diff --git a/src/layer.ts b/src/layer.ts new file mode 100644 index 000000000..bba78350c --- /dev/null +++ b/src/layer.ts @@ -0,0 +1,223 @@ +import React, { PropTypes } from 'react'; +import isEqual from 'deep-equal'; +import diff from './util/diff'; + +let index = 0; +const generateID = () => { + const newId = index + 1; + index = newId; + return index; +}; + +export default class Layer extends React.PureComponent { + static contextTypes = { + map: PropTypes.object, + }; + + static propTypes = { + id: PropTypes.string, + + type: PropTypes.oneOf([ + 'symbol', + 'line', + 'fill', + 'circle', + 'raster', + ]), + + layout: PropTypes.object, + paint: PropTypes.object, + sourceOptions: PropTypes.object, + layerOptions: PropTypes.object, + sourceId: PropTypes.string, + before: PropTypes.string, + }; + + static defaultProps = { + type: 'symbol', + layout: {}, + paint: {}, + }; + + hover = []; + + id = this.props.id || `layer-${generateID()}`; + + source = { + type: 'geojson', + ...this.props.sourceOptions, + data: { + type: 'FeatureCollection', + features: [], + }, + }; + + geometry = (coordinates) => { + switch (this.props.type) { + case 'symbol': + case 'circle': return { + type: 'Point', + coordinates, + }; + + case 'fill': return { + type: coordinates.length > 1 ? 'MultiPolygon' : 'Polygon', + coordinates, + }; + + case 'line': return { + type: 'LineString', + coordinates, + }; + + default: return null; + } + }; + + feature = (props, id) => ({ + type: 'Feature', + geometry: this.geometry(props.coordinates), + properties: { + ...props.properties, + id, + }, + }) + + onClick = (evt) => { + const children = [].concat(this.props.children); + const { map } = this.context; + const { id } = this; + const features = map.queryRenderedFeatures(evt.point, { layers: [id] }); + + features.forEach((feature) => { + const { properties } = feature; + const child = children[properties.id]; + + const onClick = child && child.props.onClick; + if (onClick) { + onClick({ ...evt, feature, map }); + } + }); + }; + + onMouseMove = (evt) => { + const children = [].concat(this.props.children); + const { map } = this.context; + const { id } = this; + + const oldHover = this.hover; + const hover = []; + + const features = map.queryRenderedFeatures(evt.point, { layers: [id] }); + + features.forEach((feature) => { + const { properties } = feature; + const child = children[properties.id]; + hover.push(properties.id); + + const onHover = child && child.props.onHover; + if (onHover) { + onHover({ ...evt, feature, map }); + } + }); + + oldHover + .filter(prevHoverId => hover.indexOf(prevHoverId) === -1) + .forEach((key) => { + const onEndHover = children[key] && children[key].props.onEndHover; + if (onEndHover) { + onEndHover({ ...evt, map }); + } + }); + + this.hover = hover; + }; + + componentWillMount() { + const { id, source } = this; + const { type, layout, paint, layerOptions, sourceId, before } = this.props; + const { map } = this.context; + + const layer = { + id, + source: sourceId || id, + type, + layout, + paint, + ...layerOptions, + }; + + if (!sourceId) { + map.addSource(id, source); + } + + map.addLayer(layer, before); + + map.on('click', this.onClick); + map.on('mousemove', this.onMouseMove); + } + + componentWillUnmount() { + const { id } = this; + + const { map } = this.context; + + map.removeLayer(id); + // if pointing to an existing source, don't remove + // as other layers may be dependent upon it + if (!this.props.sourceId) { + map.removeSource(id); + } + + map.off('click', this.onClick); + map.off('mousemove', this.onMouseMove); + } + + componentWillReceiveProps(props) { + const { paint, layout } = this.props; + const { map } = this.context; + + if (!isEqual(props.paint, paint)) { + const paintDiff = diff(paint, props.paint); + + Object.keys(paintDiff).forEach((key) => { + map.setPaintProperty(this.id, key, paintDiff[key]); + }); + } + + if (!isEqual(props.layout, layout)) { + const layoutDiff = diff(layout, props.layout); + + Object.keys(layoutDiff).forEach((key) => { + map.setLayoutProperty(this.id, key, layoutDiff[key]); + }); + } + } + + shouldComponentUpdate(nextProps) { + return !isEqual(nextProps.children, this.props.children) + || !isEqual(nextProps.paint, this.props.paint) + || !isEqual(nextProps.layout, this.props.layout); + } + + render() { + const { map } = this.context; + + if (this.props.children) { + const children = [].concat(this.props.children); + + const features = children + .map(({ props }, id) => this.feature(props, id)) + .filter(Boolean); + + const source = map.getSource(this.props.sourceId || this.id); + source.setData({ + type: 'FeatureCollection', + features, + }); + } + + return null; + } +} + diff --git a/src/map.tsx b/src/map.tsx new file mode 100644 index 000000000..04ab820e6 --- /dev/null +++ b/src/map.tsx @@ -0,0 +1,257 @@ +import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'; +import React, { Component, PropTypes } from 'react'; +import isEqual from 'deep-equal'; + +const events = { + onStyleLoad: 'style.load', // Should remain first + onResize: 'resize', + onDblClick: 'dblclick', + onClick: 'click', + onMouseMove: 'mousemove', + onMoveStart: 'mousestart', + onMove: 'move', + onMoveEnd: 'moveend', + onMouseUp: 'mouseup', + onDragStart: 'dragstart', + onDrag: 'drag', + onDragEnd: 'dragend', + onZoomStart: 'zoomstart', + onZoom: 'zoom', + onZoomEnd: 'zoomend', +}; + +export default class ReactMapboxGl extends Component { + static propTypes = { + // Events propTypes + ...Object.keys(events) + .reduce((acc, event) => ( + Object.assign({}, acc, { [event]: PropTypes.func }) + ), {}), + + // Main propTypes + style: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + ]).isRequired, + accessToken: PropTypes.string.isRequired, + center: PropTypes.arrayOf(PropTypes.number), + zoom: PropTypes.arrayOf(PropTypes.number), + minZoom: PropTypes.number, + maxZoom: PropTypes.number, + maxBounds: PropTypes.array, + fitBounds: PropTypes.array, + fitBoundsOptions: PropTypes.object, + bearing: PropTypes.number, + pitch: PropTypes.number, + containerStyle: PropTypes.object, + hash: PropTypes.bool, + preserveDrawingBuffer: PropTypes.bool, + scrollZoom: PropTypes.bool, + movingMethod: PropTypes.oneOf([ + 'jumpTo', + 'easeTo', + 'flyTo', + ]), + attributionPosition: PropTypes.oneOf([ + 'top-left', + 'top-right', + 'bottom-left', + 'bottom-right', + ]), + interactive: PropTypes.bool, + dragRotate: PropTypes.bool, + }; + + static defaultProps = { + hash: false, + onStyleLoad: (...args) => args, + preserveDrawingBuffer: false, + center: [ + -0.2416815, + 51.5285582, + ], + zoom: [11], + minZoom: 0, + maxZoom: 20, + bearing: 0, + scrollZoom: true, + movingMethod: 'flyTo', + pitch: 0, + attributionPosition: 'bottom-right', + interactive: true, + dragRotate: true, + }; + + static childContextTypes = { + map: React.PropTypes.object, + }; + + state = {}; + + getChildContext = () => ({ + map: this.state.map, + }); + + componentDidMount() { + const { + style, + hash, + preserveDrawingBuffer, + accessToken, + center, + pitch, + zoom, + minZoom, + maxZoom, + maxBounds, + fitBounds, + fitBoundsOptions, + bearing, + scrollZoom, + attributionPosition, + interactive, + dragRotate, + } = this.props; + + MapboxGl.accessToken = accessToken; + + const map = new MapboxGl.Map({ + preserveDrawingBuffer, + hash, + zoom: zoom[0], + minZoom, + maxZoom, + maxBounds, + bearing, + container: this.container, + center, + pitch, + style, + scrollZoom, + attributionControl: { + position: attributionPosition, + }, + interactive, + dragRotate, + }); + + if (fitBounds) { + map.fitBounds(fitBounds, fitBoundsOptions); + } + + Object.keys(events).forEach((event, index) => { + const propEvent = this.props[event]; + + if (propEvent) { + map.on(events[event], (...args) => { + propEvent(map, ...args); + + if (index === 0) { + this.setState({ map }); + } + }); + } + }); + } + + componentWillUnmount() { + const { map } = this.state; + + if (map) { + // Remove all events attached to the map + map.off(); + + // NOTE: We need to defer removing the map to after all children have unmounted + setTimeout(() => { + map.remove(); + }); + } + } + + shouldComponentUpdate(nextProps, nextState) { + return ( + nextProps.children !== this.props.children || + nextProps.containerStyle !== this.props.containerStyle || + nextState.map !== this.state.map || + nextProps.style !== this.props.style || + nextProps.fitBounds !== this.props.fitBounds + ); + } + + componentWillReceiveProps(nextProps) { + const { map } = this.state; + if (!map) { + return null; + } + + const center = map.getCenter(); + const zoom = map.getZoom(); + const bearing = map.getBearing(); + const pitch = map.getPitch(); + + const didZoomUpdate = ( + this.props.zoom !== nextProps.zoom && + nextProps.zoom[0] !== zoom + ); + + const didCenterUpdate = ( + this.props.center !== nextProps.center && + (nextProps.center[0] !== center.lng || nextProps.center[1] !== center.lat) + ); + + const didBearingUpdate = ( + this.props.bearing !== nextProps.bearing && + nextProps.bearing !== bearing + ); + + const didPitchUpdate = ( + this.props.pitch !== nextProps.pitch && + nextProps.pitch !== pitch + ); + + if (nextProps.fitBounds) { + const { fitBounds } = this.props; + + const didFitBoundsUpdate = ( + fitBounds !== nextProps.fitBounds || // Check for reference equality + nextProps.fitBounds.length !== (fitBounds && fitBounds.length) || // Added element + !!fitBounds.find((c, i) => { // Check for equality + const nc = nextProps.fitBounds[i]; + return c[0] !== nc[0] || c[1] !== nc[1]; + }) + ); + + if (didFitBoundsUpdate) { + map.fitBounds(nextProps.fitBounds, nextProps.fitBoundsOptions); + } + } + + if (didZoomUpdate || didCenterUpdate || didBearingUpdate || didPitchUpdate) { + map[this.props.movingMethod]({ + zoom: didZoomUpdate ? nextProps.zoom[0] : zoom, + center: didCenterUpdate ? nextProps.center : center, + bearing: didBearingUpdate ? nextProps.bearing : bearing, + pitch: didPitchUpdate ? nextProps.pitch : pitch, + }); + } + + if (!isEqual(this.props.style, nextProps.style)) { + map.setStyle(nextProps.style); + } + + return null; + } + + render() { + const { containerStyle, children } = this.props; + const { map } = this.state; + + return ( +
{ this.container = x; }} style={containerStyle}> + { + map && children + } +
+ ); + } +} diff --git a/src/marker.tsx b/src/marker.tsx new file mode 100644 index 000000000..c33915ed6 --- /dev/null +++ b/src/marker.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import ProjectedLayer from './projected-layer'; + +interface Props { + coordinates: number[]; + anchor: any; + offset: any; + children: JSX.Element; + onClick: React.MouseEventHandler; + onMouseEnter: React.MouseEventHandler; + onMouseLeave: React.MouseEventHandler; + style: React.CSSProperties; +} + +export default class Marker extends React.Component { + public render() { + return ( + + {this.props.children} + + ); + } +} diff --git a/src/popup.tsx b/src/popup.tsx new file mode 100644 index 000000000..3c6179dd1 --- /dev/null +++ b/src/popup.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import ProjectedLayer from './projected-layer'; +import { + anchors, + OverlayPropTypes +} from './util/overlays'; + +interface Props { + coordinates: number[]; + anchor: any; + offset: any; + children: JSX.Element; + onClick: React.MouseEventHandler; + onMouseEnter: React.MouseEventHandler; + onMouseLeave: React.MouseEventHandler; + style: React.CSSProperties; +} + +export default class Popup extends React.Component { + public static defaultProps = { + anchor: anchors[0] + }; + + public render() { + return ( + +
+
+ {this.props.children} +
+ + ); + } +} diff --git a/src/util/inject-css.ts b/src/util/inject-css.ts new file mode 100644 index 000000000..c3332c917 --- /dev/null +++ b/src/util/inject-css.ts @@ -0,0 +1,14 @@ +import cssRules from '../constants/css'; + +const injectCSS = (window: Window) => { + if (window && typeof window === 'object' && window.document) { + const { document } = window; + const head = (document.head || document.getElementsByTagName('head')[0]); + + const styleElement = document.createElement('style'); + styleElement.innerHTML = cssRules; + head.appendChild(styleElement); + } +} + +export default injectCSS; \ No newline at end of file diff --git a/src/util/overlays.ts b/src/util/overlays.ts new file mode 100644 index 000000000..1b08a0049 --- /dev/null +++ b/src/util/overlays.ts @@ -0,0 +1,135 @@ +import { LngLat, Point } from 'mapbox-gl/dist/mapbox-gl.js'; +// import * as React from 'react'; +// export const OverlayPropTypes = { +// anchor: PropTypes.oneOf(anchors), +// offset: PropTypes.oneOfType([ +// PropTypes.number, +// PropTypes.arrayOf(PropTypes.number), +// PropTypes.object, +// ]), +// }; + +export const anchors = [ + 'center', + 'top', + 'bottom', + 'left', + 'right', + 'top-left', + 'top-right', + 'bottom-left', + 'bottom-right', +]; + +const anchorTranslates = { + center: 'translate(-50%,-50%)', + top: 'translate(-50%,0)', + left: 'translate(0,-50%)', + right: 'translate(-100%,-50%)', + bottom: 'translate(-50%,-100%)', + 'top-left': 'translate(0,0)', + 'top-right': 'translate(-100%,0)', + 'bottom-left': 'translate(0,-100%)', + 'bottom-right': 'translate(-100%,-100%)', +}; + +const defaultElement = { offsetWidth: 0, offsetHeight: 0 }; + +const isPointLike = input => (input instanceof Point || Array.isArray(input)); + +const projectCoordinates = (map, coordinates) => map.project(LngLat.convert(coordinates)); + +const calculateAnchor = (map, offsets, position, { offsetHeight, offsetWidth }) => { + let anchor = null; + + if (position.y + offsets.bottom.y - offsetHeight < 0) { + anchor = [anchors[1]]; + } else if (position.y + offsets.top.y + offsetHeight > map.transform.height) { + anchor = [anchors[2]]; + } else { + anchor = []; + } + + if (position.x < offsetWidth / 2) { + anchor.push(anchors[3]); + } else if (position.x > map.transform.width - offsetWidth / 2) { + anchor.push(anchors[4]); + } + + if (anchor.length === 0) { + anchor = anchors[2]; + } else { + anchor = anchor.join('-'); + } + return anchor; +}; + +const normalizedOffsets = (offset) => { + if (!offset) { + return normalizedOffsets(new Point(0, 0)); + } + + if (typeof offset === 'number') { + // input specifies a radius from which to calculate offsets at all positions + const cornerOffset = Math.round(Math.sqrt(0.5 * Math.pow(offset, 2))); + return { + center: new Point(offset, offset), + top: new Point(0, offset), + bottom: new Point(0, -offset), + left: new Point(offset, 0), + right: new Point(-offset, 0), + 'top-left': new Point(cornerOffset, cornerOffset), + 'top-right': new Point(-cornerOffset, cornerOffset), + 'bottom-left': new Point(cornerOffset, -cornerOffset), + 'bottom-right': new Point(-cornerOffset, -cornerOffset), + }; + } + + if (isPointLike(offset)) { + // input specifies a single offset to be applied to all positions + return anchors.reduce((res, anchor) => { + const tmp = Object.assign({}, res); + tmp[anchor] = Point.convert(offset); + return tmp; + }, {}); + } + + // input specifies an offset per position + return anchors.reduce((res, anchor) => { + const tmp = Object.assign({}, res); + tmp[anchor] = Point.convert(offset[anchor] || [0, 0]); + return tmp; + }, {}); +}; + +export const overlayState = (props, map, { offsetWidth, offsetHeight } = defaultElement) => { + const position = projectCoordinates(map, props.coordinates); + const offsets = normalizedOffsets(props.offset); + const anchor = props.anchor + || calculateAnchor(map, offsets, position, { offsetWidth, offsetHeight }); + + return { + anchor, + position, + offset: offsets[anchor], + }; +}; + +const moveTranslate = point => ( + point ? `translate(${point.x.toFixed(0)}px,${point.y.toFixed(0)}px)` : '' +); + +export const overlayTransform = (args) => { + const { anchor, position, offset } = args; + const res = [moveTranslate(position)]; + + if (offset && offset.x !== undefined && offset.y !== undefined) { + res.push(moveTranslate(offset)); + } + + if (anchor) { + res.push(anchorTranslates[anchor]); + } + + return res; +}; From d6f54f409ce3833fecfc3f9d78018d7d48371282 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Sat, 4 Feb 2017 12:23:02 +0000 Subject: [PATCH 04/32] Convert popup and map to typescript, start layer --- package.json | 3 +- src/layer.ts | 99 ++++++++++++++------------ src/map.tsx | 193 ++++++++++++++++++++++++++++---------------------- src/popup.tsx | 3 +- 4 files changed, 166 insertions(+), 132 deletions(-) diff --git a/package.json b/package.json index 0e704f861..03ccb3f44 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,8 @@ "version": "0.29.2", "description": "A React binding of mapbox-gl-js", "main": "lib/index.js", - "jsnext:main": "es/index.js", "scripts": { "clean": "rm -rf dist", - "build:watch": "BABEL_ENV=commonjssimple babel src --watch --out-dir lib", "build": "tsc", "prepublish": "npm run clean && npm run build", "version": "npm run build", @@ -56,6 +54,7 @@ }, "devDependencies": { "@types/mapbox-gl": "^0.29.0", + "@types/node": "^7.0.5", "@types/react": "^15.0.6", "@types/typescript": "^2.0.0", "jest": "^17.0.1", diff --git a/src/layer.ts b/src/layer.ts index bba78350c..63eb6ac84 100644 --- a/src/layer.ts +++ b/src/layer.ts @@ -1,5 +1,5 @@ -import React, { PropTypes } from 'react'; -import isEqual from 'deep-equal'; +import * as React from 'react'; +const isEqual = require('deep-equal'); // tslint:disable-line import diff from './util/diff'; let index = 0; @@ -9,72 +9,83 @@ const generateID = () => { return index; }; -export default class Layer extends React.PureComponent { - static contextTypes = { - map: PropTypes.object, - }; + // public static propTypes = { + // id: PropTypes.string, + + // type: PropTypes.oneOf([ + // 'symbol', + // 'line', + // 'fill', + // 'circle', + // 'raster' + // ]), + + // layout: PropTypes.object, + // paint: PropTypes.object, + // sourceOptions: PropTypes.object, + // layerOptions: PropTypes.object, + // sourceId: PropTypes.string, + // before: PropTypes.string + // }; + +interface Props { + id?: string; + type?: 'symbol' | 'line' | 'fill' | 'circle' | 'raster'; + sourceId?: string; + before?: string; +} + +interface State { - static propTypes = { - id: PropTypes.string, - - type: PropTypes.oneOf([ - 'symbol', - 'line', - 'fill', - 'circle', - 'raster', - ]), - - layout: PropTypes.object, - paint: PropTypes.object, - sourceOptions: PropTypes.object, - layerOptions: PropTypes.object, - sourceId: PropTypes.string, - before: PropTypes.string, +} + +export default class Layer extends React.PureComponent { + public static contextTypes = { + map: React.PropTypes.object }; - static defaultProps = { + public static defaultProps = { type: 'symbol', layout: {}, - paint: {}, + paint: {} }; - hover = []; + private hover: string[] = []; - id = this.props.id || `layer-${generateID()}`; + private id: string = this.props.id || `layer-${generateID()}`; - source = { + private source = { type: 'geojson', ...this.props.sourceOptions, data: { type: 'FeatureCollection', - features: [], - }, + features: [] + } }; - geometry = (coordinates) => { + private geometry = (coordinates: number[]) => { switch (this.props.type) { case 'symbol': case 'circle': return { type: 'Point', - coordinates, + coordinates }; case 'fill': return { type: coordinates.length > 1 ? 'MultiPolygon' : 'Polygon', - coordinates, + coordinates }; case 'line': return { type: 'LineString', - coordinates, + coordinates }; default: return null; } - }; + } - feature = (props, id) => ({ + private feature = (props, id) => ({ type: 'Feature', geometry: this.geometry(props.coordinates), properties: { @@ -83,7 +94,7 @@ export default class Layer extends React.PureComponent { }, }) - onClick = (evt) => { + private onClick = (evt) => { const children = [].concat(this.props.children); const { map } = this.context; const { id } = this; @@ -100,7 +111,7 @@ export default class Layer extends React.PureComponent { }); }; - onMouseMove = (evt) => { + private onMouseMove = (evt) => { const children = [].concat(this.props.children); const { map } = this.context; const { id } = this; @@ -131,9 +142,9 @@ export default class Layer extends React.PureComponent { }); this.hover = hover; - }; + } - componentWillMount() { + public componentWillMount() { const { id, source } = this; const { type, layout, paint, layerOptions, sourceId, before } = this.props; const { map } = this.context; @@ -157,7 +168,7 @@ export default class Layer extends React.PureComponent { map.on('mousemove', this.onMouseMove); } - componentWillUnmount() { + public componentWillUnmount() { const { id } = this; const { map } = this.context; @@ -173,7 +184,7 @@ export default class Layer extends React.PureComponent { map.off('mousemove', this.onMouseMove); } - componentWillReceiveProps(props) { + public componentWillReceiveProps(props) { const { paint, layout } = this.props; const { map } = this.context; @@ -194,13 +205,13 @@ export default class Layer extends React.PureComponent { } } - shouldComponentUpdate(nextProps) { + public shouldComponentUpdate(nextProps) { return !isEqual(nextProps.children, this.props.children) || !isEqual(nextProps.paint, this.props.paint) || !isEqual(nextProps.layout, this.props.layout); } - render() { + public render() { const { map } = this.context; if (this.props.children) { diff --git a/src/map.tsx b/src/map.tsx index 04ab820e6..d765af6a1 100644 --- a/src/map.tsx +++ b/src/map.tsx @@ -1,6 +1,6 @@ -import MapboxGl from 'mapbox-gl/dist/mapbox-gl.js'; -import React, { Component, PropTypes } from 'react'; -import isEqual from 'deep-equal'; +import * as MapboxGl from 'mapbox-gl/dist/mapbox-gl'; +import * as React from 'react'; +const isEqual = require('deep-equal'); // tslint:disable-line const events = { onStyleLoad: 'style.load', // Should remain first @@ -17,82 +17,102 @@ const events = { onDragEnd: 'dragend', onZoomStart: 'zoomstart', onZoom: 'zoom', - onZoomEnd: 'zoomend', + onZoomEnd: 'zoomend' }; -export default class ReactMapboxGl extends Component { - static propTypes = { - // Events propTypes - ...Object.keys(events) - .reduce((acc, event) => ( - Object.assign({}, acc, { [event]: PropTypes.func }) - ), {}), - - // Main propTypes - style: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.object, - ]).isRequired, - accessToken: PropTypes.string.isRequired, - center: PropTypes.arrayOf(PropTypes.number), - zoom: PropTypes.arrayOf(PropTypes.number), - minZoom: PropTypes.number, - maxZoom: PropTypes.number, - maxBounds: PropTypes.array, - fitBounds: PropTypes.array, - fitBoundsOptions: PropTypes.object, - bearing: PropTypes.number, - pitch: PropTypes.number, - containerStyle: PropTypes.object, - hash: PropTypes.bool, - preserveDrawingBuffer: PropTypes.bool, - scrollZoom: PropTypes.bool, - movingMethod: PropTypes.oneOf([ - 'jumpTo', - 'easeTo', - 'flyTo', - ]), - attributionPosition: PropTypes.oneOf([ - 'top-left', - 'top-right', - 'bottom-left', - 'bottom-right', - ]), - interactive: PropTypes.bool, - dragRotate: PropTypes.bool, - }; +interface Events { + onStyleLoad: Function; + onResize: Function; + onDblClick: Function; + onClick: Function; + onMouseMove: Function; + onMoveStart: Function; + onMove: Function; + onMoveEnd: Function; + onMouseUp: Function; + onDragStart: Function; + onDragEnd: Function; + onDrag: Function; + onZoomStart: Function; + onZoom: Function; + onZoomEnd: Function; +} + +interface FitBoundsOptions { + linear?: boolean; + easing?: Function; + padding?: number; + offset?: MapboxGl.Point | number[]; + maxZoom?: number; +} + +interface Props { + style: string | MapboxGl.Style; + accessToken: string; + center?: number[]; + zoom?: number[]; + minZoom?: number; + maxZoom?: number; + maxBounds?: MapboxGl.LngLatBounds | number[][]; + fitBounds?: number[][]; + fitBoundsOptions?: FitBoundsOptions; + bearing?: number; + pitch?: number; + containerStyle?: React.CSSProperties; + hash?: boolean; + preserveDrawingBuffer?: boolean; + scrollZoom?: boolean; + interactive?: boolean; + dragRotate?: boolean; + movingMethod?: 'jumpTo' | 'easeTo' | 'flyTo'; + attributionControl?: boolean; + children?: JSX.Element; +} + +interface State { + map?: MapboxGl.Map; +} - static defaultProps = { +// Satisfy typescript pitfall with defaultProps +const defaultZoom = [11]; +const defaultMovingMethod = 'flyTo'; + +export default class ReactMapboxGl extends React.Component { + public static defaultProps = { hash: false, - onStyleLoad: (...args) => args, + onStyleLoad: (...args: any[]) => args, preserveDrawingBuffer: false, center: [ -0.2416815, - 51.5285582, + 51.5285582 ], - zoom: [11], + zoom: defaultZoom, minZoom: 0, maxZoom: 20, bearing: 0, scrollZoom: true, - movingMethod: 'flyTo', + movingMethod: defaultMovingMethod, pitch: 0, attributionPosition: 'bottom-right', interactive: true, - dragRotate: true, + dragRotate: true + }; + + public static childContextTypes = { + map: React.PropTypes.object }; - static childContextTypes = { - map: React.PropTypes.object, + public state = { + map: undefined }; - state = {}; + public getChildContext = () => ({ + map: this.state.map + }) - getChildContext = () => ({ - map: this.state.map, - }); + private container: HTMLElement; - componentDidMount() { + public componentDidMount() { const { style, hash, @@ -108,17 +128,18 @@ export default class ReactMapboxGl extends Component { fitBoundsOptions, bearing, scrollZoom, - attributionPosition, + attributionControl, interactive, - dragRotate, + dragRotate } = this.props; - MapboxGl.accessToken = accessToken; + (MapboxGl as any).accessToken = accessToken; const map = new MapboxGl.Map({ preserveDrawingBuffer, hash, - zoom: zoom[0], + // Duplicated default because Typescript can't figure out there is a defaultProps and zoom will never be undefined + zoom: zoom ? zoom[0] : defaultZoom[0], minZoom, maxZoom, maxBounds, @@ -128,11 +149,9 @@ export default class ReactMapboxGl extends Component { pitch, style, scrollZoom, - attributionControl: { - position: attributionPosition, - }, + attributionControl, interactive, - dragRotate, + dragRotate }); if (fitBounds) { @@ -143,7 +162,7 @@ export default class ReactMapboxGl extends Component { const propEvent = this.props[event]; if (propEvent) { - map.on(events[event], (...args) => { + map.on(events[event], (...args: any[]) => { propEvent(map, ...args); if (index === 0) { @@ -154,8 +173,8 @@ export default class ReactMapboxGl extends Component { }); } - componentWillUnmount() { - const { map } = this.state; + public componentWillUnmount() { + const { map } = this.state as State; if (map) { // Remove all events attached to the map @@ -168,7 +187,7 @@ export default class ReactMapboxGl extends Component { } } - shouldComponentUpdate(nextProps, nextState) { + public shouldComponentUpdate(nextProps: Props, nextState: State) { return ( nextProps.children !== this.props.children || nextProps.containerStyle !== this.props.containerStyle || @@ -178,8 +197,8 @@ export default class ReactMapboxGl extends Component { ); } - componentWillReceiveProps(nextProps) { - const { map } = this.state; + public componentWillReceiveProps(nextProps: Props) { + const { map } = this.state as State; if (!map) { return null; } @@ -191,12 +210,15 @@ export default class ReactMapboxGl extends Component { const didZoomUpdate = ( this.props.zoom !== nextProps.zoom && - nextProps.zoom[0] !== zoom + (nextProps.zoom && nextProps.zoom[0]) !== zoom ); const didCenterUpdate = ( this.props.center !== nextProps.center && - (nextProps.center[0] !== center.lng || nextProps.center[1] !== center.lat) + ( + (nextProps.center && nextProps.center[0]) !== center.lng || + (nextProps.center && nextProps.center[1]) !== center.lat + ) ); const didBearingUpdate = ( @@ -216,8 +238,8 @@ export default class ReactMapboxGl extends Component { fitBounds !== nextProps.fitBounds || // Check for reference equality nextProps.fitBounds.length !== (fitBounds && fitBounds.length) || // Added element !!fitBounds.find((c, i) => { // Check for equality - const nc = nextProps.fitBounds[i]; - return c[0] !== nc[0] || c[1] !== nc[1]; + const nc = nextProps.fitBounds && nextProps.fitBounds[i]; + return c[0] !== (nc && nc[0]) || c[1] !== (nc && nc[1]); }) ); @@ -227,11 +249,12 @@ export default class ReactMapboxGl extends Component { } if (didZoomUpdate || didCenterUpdate || didBearingUpdate || didPitchUpdate) { - map[this.props.movingMethod]({ - zoom: didZoomUpdate ? nextProps.zoom[0] : zoom, + const mm: string = this.props.movingMethod || defaultMovingMethod; + map[mm]({ + zoom: (didZoomUpdate && nextProps.zoom) ? nextProps.zoom[0] : zoom, center: didCenterUpdate ? nextProps.center : center, bearing: didBearingUpdate ? nextProps.bearing : bearing, - pitch: didPitchUpdate ? nextProps.pitch : pitch, + pitch: didPitchUpdate ? nextProps.pitch : pitch }); } @@ -242,15 +265,17 @@ export default class ReactMapboxGl extends Component { return null; } - render() { + private setRef = (x: HTMLElement) => { + this.container = x; + } + + public render() { const { containerStyle, children } = this.props; const { map } = this.state; return ( -
{ this.container = x; }} style={containerStyle}> - { - map && children - } +
+ {map && children}
); } diff --git a/src/popup.tsx b/src/popup.tsx index 3c6179dd1..5cab9438f 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; import ProjectedLayer from './projected-layer'; import { - anchors, - OverlayPropTypes + anchors } from './util/overlays'; interface Props { From b9d1209e6b50465c7b4f906e0959da4e0b541f61 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Sat, 4 Feb 2017 16:51:06 +0000 Subject: [PATCH 05/32] Convert all components --- src/cluster.tsx | 120 ++++++++++++++++++++++++++-------------- src/feature.ts | 20 +++---- src/geojson-layer.ts | 102 +++++++++++++--------------------- src/layer.ts | 87 ++++++++++++++++------------- src/marker.tsx | 2 +- src/projected-layer.tsx | 2 - src/scale-control.tsx | 6 +- src/util/overlays.ts | 2 +- tsconfig.json | 3 + 9 files changed, 180 insertions(+), 164 deletions(-) diff --git a/src/cluster.tsx b/src/cluster.tsx index b760caefd..afa5a140d 100644 --- a/src/cluster.tsx +++ b/src/cluster.tsx @@ -1,47 +1,77 @@ -import React, { PropTypes, Component } from 'react'; -import supercluster from 'supercluster'; - -export default class Cluster extends Component { - - static propTypes = { - ClusterMarkerFactory: PropTypes.func.isRequired, - clusterThreshold: PropTypes.number, - radius: PropTypes.number, - minZoom: PropTypes.number, - maxZoom: PropTypes.number, - extent: PropTypes.number, - nodeSize: PropTypes.number, - log: PropTypes.bool, +import * as React from 'react'; +import * as MapboxGL from 'mapbox-gl'; +import { Props as MarkerProps } from './marker'; + +const supercluster = require('supercluster'); //tslint:disable-line + +interface Props { + ClusterMarkerFactory: (coordinates: number[], pointCount: number) => JSX.Element; + clusterThreshold?: number; + radius?: number; + maxZoom?: number; + minZoom?: number; + extent?: number; + nodeSize?: number; + log?: boolean; + children?: Array>; +} + +interface State { + superC: any; + clusterPoints: any[]; +} + +interface Context { + map: MapboxGL.Map; +} + +interface Point { + geometry: { + coordinates: number[]; }; + properties: { + point_count: number; + }; +} - static contextTypes = { - map: PropTypes.object, +export default class Cluster extends React.Component { + public context: Context; + + public static contextTypes = { + map: React.PropTypes.object }; - static defaultProps = { + public static defaultProps = { clusterThreshold: 1, radius: 60, minZoom: 0, maxZoom: 16, extent: 512, nodeSize: 64, - log: false, + log: false }; - state = { - clusterIndex: supercluster({ + public state = { + superC: supercluster({ radius: this.props.radius, maxZoom: this.props.maxZoom, + minZoom: this.props.minZoom, + extent: this.props.extent, + nodeSize: this.props.nodeSize, + log: this.props.log }), - clusterPoints: [], + clusterPoints: [] }; - componentWillMount() { + public componentWillMount() { const { map } = this.context; - const { clusterIndex } = this.state; + const { superC } = this.state; + const { children } = this.props; - const features = this.childrenToFeatures(this.props.children); - clusterIndex.load(features); + if (children) { + const features = this.childrenToFeatures(children as any); + superC.load(features); + } // TODO: Debounce ? map.on('move', this.mapChange); @@ -49,13 +79,13 @@ export default class Cluster extends Component { this.mapChange(); } - mapChange = () => { + private mapChange = () => { const { map } = this.context; - const { clusterIndex, clusterPoints } = this.state; + const { superC, clusterPoints } = this.state; - const { _sw, _ne } = map.getBounds(); + const { _sw, _ne } = map.getBounds() as any; const zoom = map.getZoom(); - const newPoints = clusterIndex.getClusters( + const newPoints = superC.getClusters( [_sw.lng, _sw.lat, _ne.lng, _ne.lat], Math.round(zoom) ); @@ -65,35 +95,41 @@ export default class Cluster extends Component { } }; - feature(coordinates) { + private feature(coordinates: number[]) { return { type: 'Feature', geometry: { type: 'point', - coordinates, + coordinates }, properties: { - point_count: 1, - }, + point_count: 1 + } }; } - childrenToFeatures(children) { - return children.map(child => this.feature(child.props.coordinates)); - } + private childrenToFeatures = (children: Array>) => ( + children.map((child) => this.feature(child && child.props.coordinates)) + ); - render() { + public render() { const { children, ClusterMarkerFactory, clusterThreshold } = this.props; const { clusterPoints } = this.state; + if (clusterPoints.length <= clusterThreshold) { + return ( +
+ {children} +
+ ); + } + return (
- { - clusterPoints.length <= clusterThreshold ? - children : - clusterPoints.map(({ geometry, properties }) => + {// tslint:disable-line + clusterPoints.map(({ geometry, properties }: Point) => ( ClusterMarkerFactory(geometry.coordinates, properties.point_count)) - } + )}
); } diff --git a/src/feature.ts b/src/feature.ts index b9f3260ea..3fe684568 100644 --- a/src/feature.ts +++ b/src/feature.ts @@ -1,17 +1,13 @@ -import React, { PropTypes } from 'react'; +import * as React from 'react'; -class Feature extends React.PureComponent { - render() { - return null; - } +interface Props { + coordinates: number[]; + properties: any; + onClick?: Function; + onHover?: Function; + onEndHover?: Function; } -Feature.propTypes = { - coordinates: PropTypes.array.isRequired, - onClick: PropTypes.func, - onHover: PropTypes.func, - onEndHover: PropTypes.func, - properties: PropTypes.object, -}; +const Feature: React.StatelessComponent = () => null; export default Feature; diff --git a/src/geojson-layer.ts b/src/geojson-layer.ts index c85d33124..232aad42f 100644 --- a/src/geojson-layer.ts +++ b/src/geojson-layer.ts @@ -1,6 +1,5 @@ -import React, { PropTypes } from 'react'; -import isEqual from 'deep-equal'; -import diff from './util/diff'; +import * as React from 'react'; +import * as MapboxGL from 'mapbox-gl/dist/mapbox-gl'; let index = 0; const generateID = () => { @@ -9,44 +8,43 @@ const generateID = () => { return index; }; -export default class GeoJSONLayer extends React.PureComponent { - static contextTypes = { - map: PropTypes.object, - }; - - static propTypes = { - id: PropTypes.string, - - data: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.object, - ]).isRequired, +interface Props { + id?: string; + data: GeoJSON.Feature | GeoJSON.FeatureCollection | string; + sourceOptions: MapboxGL.VectorSource | MapboxGL.RasterSource | MapboxGL.GeoJSONSource | MapboxGL.GeoJSONSourceRaw; + before?: string; + fillLayout?: MapboxGL.FillLayout; + symbolLayout?: MapboxGL.SymbolLayout; + circleLayout?: MapboxGL.CircleLayout; + lineLayout?: MapboxGL.LineLayout; + linePaint?: MapboxGL.LinePaint; + symbolPaint?: MapboxGL.SymbolPaint; + circlePaint?: MapboxGL.CirclePaint; + fillPaint?: MapboxGL.FillPaint; +} - lineLayout: PropTypes.object, - symbolLayout: PropTypes.object, - circleLayout: PropTypes.object, - fillLayout: PropTypes.object, +interface Context { + map: MapboxGL.Map; +} - linePaint: PropTypes.object, - symbolPaint: PropTypes.object, - circlePaint: PropTypes.object, - fillPaint: PropTypes.object, +export default class GeoJSONLayer extends React.Component { + public context: Context; - sourceOptions: PropTypes.string, - before: PropTypes.string, + public static contextTypes = { + map: React.PropTypes.object }; - id = this.props.id || `geojson-${generateID()}`; + private id: string = this.props.id || `geojson-${generateID()}`; - source = { + private source = { type: 'geojson', ...this.props.sourceOptions, - data: this.props.data, + data: this.props.data }; - layerIds = []; + private layerIds: string[] = []; - createLayer = (type) => { + private createLayer = (type: string) => { const { id, layerIds } = this; const { before } = this.props; const { map } = this.context; @@ -62,11 +60,11 @@ export default class GeoJSONLayer extends React.PureComponent { source: id, type, paint, - layout, + layout }, before); - }; + } - componentWillMount() { + public componentWillMount() { const { id, source } = this; const { map } = this.context; @@ -78,52 +76,26 @@ export default class GeoJSONLayer extends React.PureComponent { this.createLayer('circle'); } - componentWillUnmount() { + public componentWillUnmount() { const { id, layerIds } = this; const { map } = this.context; map.removeSource(id); - layerIds.forEach(key => map.removeLayer(key)); + layerIds.forEach(map.removeLayer); } - componentWillReceiveProps(props) { + public componentWillReceiveProps(props: Props) { const { id } = this; - const { data, paint, layout } = this.props; + const { data } = this.props; const { map } = this.context; - if (!isEqual(props.paint, paint)) { - const paintDiff = diff(paint, props.paint); - - Object.keys(paintDiff).forEach((key) => { - map.setPaintProperty(this.id, key, paintDiff[key]); - }); - } - - if (!isEqual(props.layout, layout)) { - const layoutDiff = diff(layout, props.layout); - - Object.keys(layoutDiff).forEach((key) => { - map.setLayoutProperty(this.id, key, layoutDiff[key]); - }); - } - if (props.data !== data) { - map - .getSource(id) - .setData(props.data); + (map.getSource(id) as MapboxGL.GeoJSONSource).setData(props.data); } } - shouldComponentUpdate(nextProps) { - return ( - !isEqual(nextProps.paint, this.props.paint) || - !isEqual(nextProps.layout, this.props.layout) || - nextProps.data !== this.props.data - ); - } - - render() { + public render() { return null; } } diff --git a/src/layer.ts b/src/layer.ts index 63eb6ac84..2dda1bba2 100644 --- a/src/layer.ts +++ b/src/layer.ts @@ -1,4 +1,6 @@ import * as React from 'react'; +import * as MapboxGL from 'mapbox-gl'; + const isEqual = require('deep-equal'); // tslint:disable-line import diff from './util/diff'; @@ -9,37 +11,46 @@ const generateID = () => { return index; }; - // public static propTypes = { - // id: PropTypes.string, - - // type: PropTypes.oneOf([ - // 'symbol', - // 'line', - // 'fill', - // 'circle', - // 'raster' - // ]), - - // layout: PropTypes.object, - // paint: PropTypes.object, - // sourceOptions: PropTypes.object, - // layerOptions: PropTypes.object, - // sourceId: PropTypes.string, - // before: PropTypes.string - // }; +type Sources = MapboxGL.VectorSource | MapboxGL.RasterSource | MapboxGL.GeoJSONSource | MapboxGL.GeoJSONSourceRaw; +type Paint = ( + MapboxGL.BackgroundPaint | + MapboxGL.FillPaint | + MapboxGL.FillExtrusionPaint | + MapboxGL.LinePaint | + MapboxGL.SymbolPaint | + MapboxGL.RasterPaint | + MapboxGL.CirclePaint +); + +type Layout = ( + MapboxGL.BackgroundLayout | + MapboxGL.FillLayout | + MapboxGL.FillExtrusionLayout | + MapboxGL.LineLayout | + MapboxGL.SymbolLayout | + MapboxGL.RasterLayout | + MapboxGL.CircleLayout +); interface Props { id?: string; type?: 'symbol' | 'line' | 'fill' | 'circle' | 'raster'; sourceId?: string; before?: string; + sourceOptions?: Sources; + paint?: Paint; + layout?: Layout; + layerOptions?: MapboxGL.Layer; + children?: JSX.Element; } -interface State { - +interface Context { + map: MapboxGL.Map; } -export default class Layer extends React.PureComponent { +export default class Layer extends React.PureComponent { + public context: Context; + public static contextTypes = { map: React.PropTypes.object }; @@ -54,7 +65,7 @@ export default class Layer extends React.PureComponent { private id: string = this.props.id || `layer-${generateID()}`; - private source = { + private source: Sources = { type: 'geojson', ...this.props.sourceOptions, data: { @@ -85,17 +96,17 @@ export default class Layer extends React.PureComponent { } } - private feature = (props, id) => ({ + private feature = (props: any, id: string) => ({ type: 'Feature', geometry: this.geometry(props.coordinates), properties: { ...props.properties, - id, - }, + id + } }) - private onClick = (evt) => { - const children = [].concat(this.props.children); + private onClick = (evt: any) => { + const children = ([] as any).concat(this.props.children); const { map } = this.context; const { id } = this; const features = map.queryRenderedFeatures(evt.point, { layers: [id] }); @@ -111,13 +122,13 @@ export default class Layer extends React.PureComponent { }); }; - private onMouseMove = (evt) => { - const children = [].concat(this.props.children); + private onMouseMove = (evt: any) => { + const children = ([] as any).concat(this.props.children); const { map } = this.context; const { id } = this; const oldHover = this.hover; - const hover = []; + const hover: string[] = []; const features = map.queryRenderedFeatures(evt.point, { layers: [id] }); @@ -133,7 +144,7 @@ export default class Layer extends React.PureComponent { }); oldHover - .filter(prevHoverId => hover.indexOf(prevHoverId) === -1) + .filter((prevHoverId) => hover.indexOf(prevHoverId) === -1) .forEach((key) => { const onEndHover = children[key] && children[key].props.onEndHover; if (onEndHover) { @@ -155,7 +166,7 @@ export default class Layer extends React.PureComponent { type, layout, paint, - ...layerOptions, + ...layerOptions }; if (!sourceId) { @@ -184,7 +195,7 @@ export default class Layer extends React.PureComponent { map.off('mousemove', this.onMouseMove); } - public componentWillReceiveProps(props) { + public componentWillReceiveProps(props: Props) { const { paint, layout } = this.props; const { map } = this.context; @@ -205,7 +216,7 @@ export default class Layer extends React.PureComponent { } } - public shouldComponentUpdate(nextProps) { + public shouldComponentUpdate(nextProps: Props) { return !isEqual(nextProps.children, this.props.children) || !isEqual(nextProps.paint, this.props.paint) || !isEqual(nextProps.layout, this.props.layout); @@ -215,16 +226,16 @@ export default class Layer extends React.PureComponent { const { map } = this.context; if (this.props.children) { - const children = [].concat(this.props.children); + const children = ([] as any).concat(this.props.children); const features = children - .map(({ props }, id) => this.feature(props, id)) + .map(({ props }: any, id: string) => this.feature(props, id)) .filter(Boolean); const source = map.getSource(this.props.sourceId || this.id); - source.setData({ + (source as MapboxGL.GeoJSONSource).setData({ type: 'FeatureCollection', - features, + features }); } diff --git a/src/marker.tsx b/src/marker.tsx index c33915ed6..e0951b9a3 100644 --- a/src/marker.tsx +++ b/src/marker.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import ProjectedLayer from './projected-layer'; -interface Props { +export interface Props { coordinates: number[]; anchor: any; offset: any; diff --git a/src/projected-layer.tsx b/src/projected-layer.tsx index 5e6e4202d..e6370b6b3 100644 --- a/src/projected-layer.tsx +++ b/src/projected-layer.tsx @@ -23,8 +23,6 @@ interface Props { className: string; } -// interface State {} - interface Context { map: Map; } diff --git a/src/scale-control.tsx b/src/scale-control.tsx index a71f20996..6a8cc614e 100644 --- a/src/scale-control.tsx +++ b/src/scale-control.tsx @@ -103,11 +103,11 @@ export default class ScaleControl extends React.Component { const { measurement } = this.props; const clientWidth = (map as any)._canvas.clientWidth; - const { ne, sw } = map.getBounds() as any; + const { _ne, _sw } = map.getBounds() as any; const totalWidth = this._getDistanceTwoPoints( - [sw.lng, ne.lat], - [sw.lng, ne.lat], + [_sw.lng, _ne.lat], + [_sw.lng, _ne.lat], measurement ); diff --git a/src/util/overlays.ts b/src/util/overlays.ts index 1b08a0049..34b52c770 100644 --- a/src/util/overlays.ts +++ b/src/util/overlays.ts @@ -1,4 +1,4 @@ -import { LngLat, Point } from 'mapbox-gl/dist/mapbox-gl.js'; +import { LngLat, Point } from 'mapbox-gl/dist/mapbox-gl'; // import * as React from 'react'; // export const OverlayPropTypes = { // anchor: PropTypes.oneOf(anchors), diff --git a/tsconfig.json b/tsconfig.json index 059d0fc82..a401bde0f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,9 @@ "suppressImplicitAnyIndexErrors": true, "noUnusedLocals": true }, + "include": [ + "src/**/*" + ], "exclude": [ "node_modules", "lib", From 777b42cb45cfa1297fcb567682472fa7c1ce86d9 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Sat, 4 Feb 2017 17:23:36 +0000 Subject: [PATCH 06/32] Fix overlays --- src/projected-layer.tsx | 7 +-- src/util/overlays.ts | 115 ++++++++++++++++++++++------------------ 2 files changed, 66 insertions(+), 56 deletions(-) diff --git a/src/projected-layer.tsx b/src/projected-layer.tsx index e6370b6b3..08d6cc90e 100644 --- a/src/projected-layer.tsx +++ b/src/projected-layer.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { Map } from 'mapbox-gl'; +import { Anchor } from './util/overlays'; import { overlayState, @@ -11,10 +12,10 @@ const defaultStyle = { zIndex: 3 }; -interface Props { +export interface Props { coordinates: number[]; - anchor: any; - offset: any; + anchor: Anchor; + offset: number | number[] | Object; children: JSX.Element; onClick: React.MouseEventHandler; onMouseEnter: React.MouseEventHandler; diff --git a/src/util/overlays.ts b/src/util/overlays.ts index 34b52c770..c1b49e327 100644 --- a/src/util/overlays.ts +++ b/src/util/overlays.ts @@ -1,13 +1,22 @@ import { LngLat, Point } from 'mapbox-gl/dist/mapbox-gl'; -// import * as React from 'react'; -// export const OverlayPropTypes = { -// anchor: PropTypes.oneOf(anchors), -// offset: PropTypes.oneOfType([ -// PropTypes.number, -// PropTypes.arrayOf(PropTypes.number), -// PropTypes.object, -// ]), -// }; +import * as MapboxGL from 'mapbox-gl/dist/mapbox-gl'; +import { Props } from '../projected-layer'; + +export type Anchor = ( + 'center' | 'top' | 'bottom' | 'left' | 'right' | + 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' +); + +export interface PointDef { + x: number; + y: number; +} + +export interface OverlayProps { + anchor: Anchor; + offset: PointDef; + position: PointDef; +} export const anchors = [ 'center', @@ -18,36 +27,40 @@ export const anchors = [ 'top-left', 'top-right', 'bottom-left', - 'bottom-right', + 'bottom-right' ]; const anchorTranslates = { - center: 'translate(-50%,-50%)', - top: 'translate(-50%,0)', - left: 'translate(0,-50%)', - right: 'translate(-100%,-50%)', - bottom: 'translate(-50%,-100%)', + 'center': 'translate(-50%,-50%)', + 'top': 'translate(-50%,0)', + 'left': 'translate(0,-50%)', + 'right': 'translate(-100%,-50%)', + 'bottom': 'translate(-50%,-100%)', 'top-left': 'translate(0,0)', 'top-right': 'translate(-100%,0)', 'bottom-left': 'translate(0,-100%)', 'bottom-right': 'translate(-100%,-100%)', }; -const defaultElement = { offsetWidth: 0, offsetHeight: 0 }; +// Hack /o\ +const defaultElement = { offsetWidth: 0, offsetHeight: 0 } as HTMLElement; -const isPointLike = input => (input instanceof Point || Array.isArray(input)); +const isPointLike = (input: Point | any[]): boolean => (input instanceof Point || Array.isArray(input)); -const projectCoordinates = (map, coordinates) => map.project(LngLat.convert(coordinates)); +const projectCoordinates = (map: MapboxGL.Map, coordinates: number[]) => map.project(LngLat.convert(coordinates)); -const calculateAnchor = (map, offsets, position, { offsetHeight, offsetWidth }) => { - let anchor = null; +const calculateAnchor = ( + map: MapboxGL.Map, + offsets, + position: PointDef, + { offsetHeight, offsetWidth }: HTMLElement = defaultElement +) => { + let anchor: string[] = []; if (position.y + offsets.bottom.y - offsetHeight < 0) { anchor = [anchors[1]]; } else if (position.y + offsets.top.y + offsetHeight > map.transform.height) { anchor = [anchors[2]]; - } else { - anchor = []; } if (position.x < offsetWidth / 2) { @@ -57,70 +70,66 @@ const calculateAnchor = (map, offsets, position, { offsetHeight, offsetWidth }) } if (anchor.length === 0) { - anchor = anchors[2]; - } else { - anchor = anchor.join('-'); + return anchors[2]; } - return anchor; + + return anchor.join('-'); }; -const normalizedOffsets = (offset) => { +const normalizedOffsets = (offset: any): any => { if (!offset) { - return normalizedOffsets(new Point(0, 0)); + return normalizedOffsets(new (Point as any)(0, 0)); } if (typeof offset === 'number') { // input specifies a radius from which to calculate offsets at all positions const cornerOffset = Math.round(Math.sqrt(0.5 * Math.pow(offset, 2))); return { - center: new Point(offset, offset), - top: new Point(0, offset), - bottom: new Point(0, -offset), - left: new Point(offset, 0), - right: new Point(-offset, 0), - 'top-left': new Point(cornerOffset, cornerOffset), - 'top-right': new Point(-cornerOffset, cornerOffset), - 'bottom-left': new Point(cornerOffset, -cornerOffset), - 'bottom-right': new Point(-cornerOffset, -cornerOffset), + 'center': new (Point as any)(offset, offset), + 'top': new (Point as any)(0, offset), + 'bottom': new (Point as any)(0, -offset), + 'left': new (Point as any)(offset, 0), + 'right': new (Point as any)(-offset, 0), + 'top-left': new (Point as any)(cornerOffset, cornerOffset), + 'top-right': new (Point as any)(-cornerOffset, cornerOffset), + 'bottom-left': new (Point as any)(cornerOffset, -cornerOffset), + 'bottom-right': new (Point as any)(-cornerOffset, -cornerOffset), }; } if (isPointLike(offset)) { // input specifies a single offset to be applied to all positions - return anchors.reduce((res, anchor) => { - const tmp = Object.assign({}, res); - tmp[anchor] = Point.convert(offset); - return tmp; - }, {}); + return anchors.reduce((res, anchor) => ({ + ...res, + [anchor]: (Point as any).convert(offset) + }), {}); } // input specifies an offset per position - return anchors.reduce((res, anchor) => { - const tmp = Object.assign({}, res); - tmp[anchor] = Point.convert(offset[anchor] || [0, 0]); - return tmp; - }, {}); + return anchors.reduce((res, anchor) => ({ + ...res, + [anchor]: (Point as any).convert(offset[anchor] || [0, 0]) + }), {}); }; -export const overlayState = (props, map, { offsetWidth, offsetHeight } = defaultElement) => { +export const overlayState = (props: Props, map: MapboxGL.Map, container: HTMLElement) => { const position = projectCoordinates(map, props.coordinates); const offsets = normalizedOffsets(props.offset); const anchor = props.anchor - || calculateAnchor(map, offsets, position, { offsetWidth, offsetHeight }); + || calculateAnchor(map, offsets, position, container); return { anchor, position, - offset: offsets[anchor], + offset: offsets[anchor] }; }; -const moveTranslate = point => ( +const moveTranslate = (point: PointDef ) => ( point ? `translate(${point.x.toFixed(0)}px,${point.y.toFixed(0)}px)` : '' ); -export const overlayTransform = (args) => { - const { anchor, position, offset } = args; +export const overlayTransform = ({ anchor, position, offset }: OverlayProps) => { const res = [moveTranslate(position)]; if (offset && offset.x !== undefined && offset.y !== undefined) { From bab35ca87d683c0498a7b6204e945b52bdac653a Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Sat, 4 Feb 2017 17:39:10 +0000 Subject: [PATCH 07/32] Fix all typings --- src/projected-layer.tsx | 8 ++++---- src/util/diff.ts | 8 ++++---- src/util/overlays.ts | 20 +++++++++++--------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/projected-layer.tsx b/src/projected-layer.tsx index 08d6cc90e..aed47329d 100644 --- a/src/projected-layer.tsx +++ b/src/projected-layer.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Map } from 'mapbox-gl'; -import { Anchor } from './util/overlays'; +import { Anchor, PointDef, OverlayProps } from './util/overlays'; import { overlayState, @@ -15,7 +15,7 @@ const defaultStyle = { export interface Props { coordinates: number[]; anchor: Anchor; - offset: number | number[] | Object; + offset: number | number[] | PointDef; children: JSX.Element; onClick: React.MouseEventHandler; onMouseEnter: React.MouseEventHandler; @@ -43,7 +43,7 @@ export default class ProjectedLayer extends React.Component { onClick: (...args: any[]) => args }; - public state = {}; + public state: OverlayProps = {}; private setContainer = (el: HTMLElement) => { if (el) { @@ -98,7 +98,7 @@ export default class ProjectedLayer extends React.Component { const finalStyle = { ...defaultStyle, ...style, - transform: overlayTransform(this.state).join(' '), + transform: overlayTransform(this.state as OverlayProps).join(' '), }; return ( diff --git a/src/util/diff.ts b/src/util/diff.ts index 5969b8f0c..9d2a6739a 100644 --- a/src/util/diff.ts +++ b/src/util/diff.ts @@ -1,11 +1,11 @@ -const reduce = require('reduce-object'); +const reduce = require('reduce-object'); // tslint:disable-line -const find = (obj, predicate) => ( +const find = (obj: any, predicate: (...args: any[]) => boolean) => ( Object.keys(obj).find((key) => predicate(obj[key], key)) ); -const diff = (obj1, obj2) => ( - reduce(obj2, (res, value, key) => { +const diff = (obj1: any, obj2: any) => ( + reduce(obj2, (res: any, value: any, key: string) => { const toMutate = res; if (find(obj1, (v, k) => key === k && value !== v)) { toMutate[key] = value; diff --git a/src/util/overlays.ts b/src/util/overlays.ts index c1b49e327..11dcdfa9b 100644 --- a/src/util/overlays.ts +++ b/src/util/overlays.ts @@ -13,9 +13,9 @@ export interface PointDef { } export interface OverlayProps { - anchor: Anchor; - offset: PointDef; - position: PointDef; + anchor?: Anchor; + offset?: PointDef; + position?: PointDef; } export const anchors = [ @@ -47,11 +47,13 @@ const defaultElement = { offsetWidth: 0, offsetHeight: 0 } as HTMLElement; const isPointLike = (input: Point | any[]): boolean => (input instanceof Point || Array.isArray(input)); -const projectCoordinates = (map: MapboxGL.Map, coordinates: number[]) => map.project(LngLat.convert(coordinates)); +const projectCoordinates = (map: MapboxGL.Map, coordinates: number[]) => ( + map.project(LngLat.convert(coordinates)) +); const calculateAnchor = ( map: MapboxGL.Map, - offsets, + offsets: any, position: PointDef, { offsetHeight, offsetWidth }: HTMLElement = defaultElement ) => { @@ -59,13 +61,13 @@ const calculateAnchor = ( if (position.y + offsets.bottom.y - offsetHeight < 0) { anchor = [anchors[1]]; - } else if (position.y + offsets.top.y + offsetHeight > map.transform.height) { + } else if (position.y + offsets.top.y + offsetHeight > (map as any).transform.height) { anchor = [anchors[2]]; } if (position.x < offsetWidth / 2) { anchor.push(anchors[3]); - } else if (position.x > map.transform.width - offsetWidth / 2) { + } else if (position.x > (map as any).transform.width - offsetWidth / 2) { anchor.push(anchors[4]); } @@ -116,7 +118,7 @@ export const overlayState = (props: Props, map: MapboxGL.Map, container: HTMLEle const position = projectCoordinates(map, props.coordinates); const offsets = normalizedOffsets(props.offset); const anchor = props.anchor - || calculateAnchor(map, offsets, position, container); + || calculateAnchor(map, offsets, position as any, container); return { anchor, @@ -130,7 +132,7 @@ const moveTranslate = (point: PointDef ) => ( ); export const overlayTransform = ({ anchor, position, offset }: OverlayProps) => { - const res = [moveTranslate(position)]; + const res = [moveTranslate(position as any)]; if (offset && offset.x !== undefined && offset.y !== undefined) { res.push(moveTranslate(offset)); From e67284db16440dbeede9008e977032ba3b5e2f9e Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Sat, 4 Feb 2017 17:49:42 +0000 Subject: [PATCH 08/32] Add declaration files to build --- src/cluster.tsx | 8 ++++---- src/feature.ts | 2 +- src/geojson-layer.ts | 4 ++-- src/layer.ts | 16 +++++++++++----- src/map.tsx | 8 ++++---- src/popup.tsx | 2 +- src/projected-layer.tsx | 2 +- src/scale-control.tsx | 10 +++++----- src/source.tsx | 6 +++--- src/zoom-control.tsx | 8 ++++---- tsconfig.json | 10 ++++++---- 11 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/cluster.tsx b/src/cluster.tsx index afa5a140d..37ffc854f 100644 --- a/src/cluster.tsx +++ b/src/cluster.tsx @@ -4,7 +4,7 @@ import { Props as MarkerProps } from './marker'; const supercluster = require('supercluster'); //tslint:disable-line -interface Props { +export interface Props { ClusterMarkerFactory: (coordinates: number[], pointCount: number) => JSX.Element; clusterThreshold?: number; radius?: number; @@ -16,16 +16,16 @@ interface Props { children?: Array>; } -interface State { +export interface State { superC: any; clusterPoints: any[]; } -interface Context { +export interface Context { map: MapboxGL.Map; } -interface Point { +export interface Point { geometry: { coordinates: number[]; }; diff --git a/src/feature.ts b/src/feature.ts index 3fe684568..21b4fcffb 100644 --- a/src/feature.ts +++ b/src/feature.ts @@ -1,6 +1,6 @@ import * as React from 'react'; -interface Props { +export interface Props { coordinates: number[]; properties: any; onClick?: Function; diff --git a/src/geojson-layer.ts b/src/geojson-layer.ts index 232aad42f..b592df73a 100644 --- a/src/geojson-layer.ts +++ b/src/geojson-layer.ts @@ -8,7 +8,7 @@ const generateID = () => { return index; }; -interface Props { +export interface Props { id?: string; data: GeoJSON.Feature | GeoJSON.FeatureCollection | string; sourceOptions: MapboxGL.VectorSource | MapboxGL.RasterSource | MapboxGL.GeoJSONSource | MapboxGL.GeoJSONSourceRaw; @@ -23,7 +23,7 @@ interface Props { fillPaint?: MapboxGL.FillPaint; } -interface Context { +export interface Context { map: MapboxGL.Map; } diff --git a/src/layer.ts b/src/layer.ts index 2dda1bba2..94fc18971 100644 --- a/src/layer.ts +++ b/src/layer.ts @@ -11,8 +11,14 @@ const generateID = () => { return index; }; -type Sources = MapboxGL.VectorSource | MapboxGL.RasterSource | MapboxGL.GeoJSONSource | MapboxGL.GeoJSONSourceRaw; -type Paint = ( +export type Sources = ( + MapboxGL.VectorSource | + MapboxGL.RasterSource | + MapboxGL.GeoJSONSource | + MapboxGL.GeoJSONSourceRaw +); + +export type Paint = ( MapboxGL.BackgroundPaint | MapboxGL.FillPaint | MapboxGL.FillExtrusionPaint | @@ -22,7 +28,7 @@ type Paint = ( MapboxGL.CirclePaint ); -type Layout = ( +export type Layout = ( MapboxGL.BackgroundLayout | MapboxGL.FillLayout | MapboxGL.FillExtrusionLayout | @@ -32,7 +38,7 @@ type Layout = ( MapboxGL.CircleLayout ); -interface Props { +export interface Props { id?: string; type?: 'symbol' | 'line' | 'fill' | 'circle' | 'raster'; sourceId?: string; @@ -44,7 +50,7 @@ interface Props { children?: JSX.Element; } -interface Context { +export interface Context { map: MapboxGL.Map; } diff --git a/src/map.tsx b/src/map.tsx index d765af6a1..71670b4e4 100644 --- a/src/map.tsx +++ b/src/map.tsx @@ -20,7 +20,7 @@ const events = { onZoomEnd: 'zoomend' }; -interface Events { +export interface Events { onStyleLoad: Function; onResize: Function; onDblClick: Function; @@ -38,7 +38,7 @@ interface Events { onZoomEnd: Function; } -interface FitBoundsOptions { +export interface FitBoundsOptions { linear?: boolean; easing?: Function; padding?: number; @@ -46,7 +46,7 @@ interface FitBoundsOptions { maxZoom?: number; } -interface Props { +export interface Props { style: string | MapboxGl.Style; accessToken: string; center?: number[]; @@ -69,7 +69,7 @@ interface Props { children?: JSX.Element; } -interface State { +export interface State { map?: MapboxGl.Map; } diff --git a/src/popup.tsx b/src/popup.tsx index 5cab9438f..1bc28f48b 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -4,7 +4,7 @@ import { anchors } from './util/overlays'; -interface Props { +export interface Props { coordinates: number[]; anchor: any; offset: any; diff --git a/src/projected-layer.tsx b/src/projected-layer.tsx index aed47329d..416925596 100644 --- a/src/projected-layer.tsx +++ b/src/projected-layer.tsx @@ -24,7 +24,7 @@ export interface Props { className: string; } -interface Context { +export interface Context { map: Map; } diff --git a/src/scale-control.tsx b/src/scale-control.tsx index 6a8cc614e..86be5f5d4 100644 --- a/src/scale-control.tsx +++ b/src/scale-control.tsx @@ -51,21 +51,21 @@ const KILOMETER_IN_METERS = 1000; const MIN_WIDTH_SCALE = 40; -type Measurement = 'km' | 'mi'; -type Position = 'topRight' | 'topLeft' | 'bottomRight' | 'bottomLeft'; +export type Measurement = 'km' | 'mi'; +export type Position = 'topRight' | 'topLeft' | 'bottomRight' | 'bottomLeft'; -interface Props { +export interface Props { measurement: Measurement; position: Position; style: React.CSSProperties; } -interface State { +export interface State { chosenScale: number; scaleWidth: number; } -interface Context { +export interface Context { map: Map; } diff --git a/src/source.tsx b/src/source.tsx index 862088e18..5b5e8f623 100644 --- a/src/source.tsx +++ b/src/source.tsx @@ -9,13 +9,13 @@ import { GeoJSONSourceRaw } from 'mapbox-gl'; -interface Context { +export interface Context { map: Map; } -type Sources = VectorSource | RasterSource | GeoJSONSource | ImageSource | VideoSource | GeoJSONSourceRaw; +export type Sources = VectorSource | RasterSource | GeoJSONSource | ImageSource | VideoSource | GeoJSONSourceRaw; -interface Props { +export interface Props { id: string; sourceOptions: Sources; } diff --git a/src/zoom-control.tsx b/src/zoom-control.tsx index e4aef70c7..d6d286fc0 100644 --- a/src/zoom-control.tsx +++ b/src/zoom-control.tsx @@ -51,18 +51,18 @@ const buttonStyleMinus = { const [PLUS, MINUS] = [0, 1]; const POSITIONS = Object.keys(positions); -interface Props { +export interface Props { zoomDiff: number; onControlClick: (map: Map, zoomDiff: number) => void; position: 'topRight' | 'topLeft' | 'bottomRight' | 'bottomLeft'; style: React.CSSProperties; } -interface State { +export interface State { hover?: number; } -interface Context { +export interface Context { map: Map; } @@ -75,7 +75,7 @@ export default class ZoomControl extends React.Component { zoomDiff: 0.5, onControlClick: (map: Map, zoomDiff: number) => { map.zoomTo(map.getZoom() + zoomDiff); - }, + } }; public state = { diff --git a/tsconfig.json b/tsconfig.json index a401bde0f..601f78ba9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,9 +4,8 @@ "module": "commonjs", "target": "es6", "sourceMap": true, - "allowJs": true, "moduleResolution": "node", - "rootDir": "src", + "rootDirs": ["src"], "jsx": "preserve", "removeComments": true, "forceConsistentCasingInFileNames": true, @@ -15,7 +14,8 @@ "noImplicitAny": true, "strictNullChecks": true, "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true + "noUnusedLocals": true, + "declaration": true }, "include": [ "src/**/*" @@ -23,6 +23,8 @@ "exclude": [ "node_modules", "lib", - "src/__tests__" + "src/__tests__", + "example", + "lib" ] } From 1a6ce2495e626fe903f13f46a198d2d7bf284725 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Sat, 4 Feb 2017 17:54:05 +0000 Subject: [PATCH 09/32] Move source to ts --- src/{source.tsx => source.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{source.tsx => source.ts} (100%) diff --git a/src/source.tsx b/src/source.ts similarity index 100% rename from src/source.tsx rename to src/source.ts From dadd1d904a8a12172c59fdfc151cadab3850c01a Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Sun, 5 Feb 2017 18:17:37 +0000 Subject: [PATCH 10/32] Add correct configuration --- package.json | 1 + src/cluster.tsx | 2 +- tsconfig.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 03ccb3f44..32ecc285c 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "lib/index.js", "scripts": { "clean": "rm -rf dist", + "lint": "tslint", "build": "tsc", "prepublish": "npm run clean && npm run build", "version": "npm run build", diff --git a/src/cluster.tsx b/src/cluster.tsx index 37ffc854f..d4e706397 100644 --- a/src/cluster.tsx +++ b/src/cluster.tsx @@ -93,7 +93,7 @@ export default class Cluster extends React.Component { if (newPoints.length !== clusterPoints.length) { this.setState({ clusterPoints: newPoints }); } - }; + } private feature(coordinates: number[]) { return { diff --git a/tsconfig.json b/tsconfig.json index 601f78ba9..dd0b9faf5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "sourceMap": true, "moduleResolution": "node", "rootDirs": ["src"], - "jsx": "preserve", + "jsx": "react", "removeComments": true, "forceConsistentCasingInFileNames": true, "noImplicitReturns": true, From d05f8de1f42220121570cec17cddf831ef534779 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Sun, 5 Feb 2017 19:00:07 +0000 Subject: [PATCH 11/32] Fix small error and add build wathc --- example/src/cluster.js | 2 +- package.json | 1 + src/geojson-layer.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/example/src/cluster.js b/example/src/cluster.js index 0074cb268..1b00ce6e2 100644 --- a/example/src/cluster.js +++ b/example/src/cluster.js @@ -40,7 +40,7 @@ export default class ClusterExample extends Component { } clusterMarker = (coordinates, pointCount) => ( - + { pointCount } ); diff --git a/package.json b/package.json index 32ecc285c..80053c731 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "clean": "rm -rf dist", "lint": "tslint", "build": "tsc", + "build:watch": "tsc --watch", "prepublish": "npm run clean && npm run build", "version": "npm run build", "postversion": "git push && git push --tags" diff --git a/src/geojson-layer.ts b/src/geojson-layer.ts index b592df73a..5d7d23b58 100644 --- a/src/geojson-layer.ts +++ b/src/geojson-layer.ts @@ -82,7 +82,7 @@ export default class GeoJSONLayer extends React.Component { map.removeSource(id); - layerIds.forEach(map.removeLayer); + layerIds.forEach((lId) => map.removeLayer(lId)); } public componentWillReceiveProps(props: Props) { From a86fc7d6b6311c332e5563dbf21110cbd3813df3 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Sun, 5 Feb 2017 19:42:02 +0000 Subject: [PATCH 12/32] Fix tests and lint --- package.json | 19 +++++++++++---- .../{layer.test.js => layer.test.tsx} | 24 +++++++++++-------- src/geojson-layer.ts | 1 - src/index.ts | 3 +-- src/layer.ts | 3 +-- src/marker.tsx | 14 +++++------ src/popup.tsx | 14 +++++------ src/projected-layer.tsx | 20 ++++++++-------- src/scale-control.tsx | 16 +++++++------ src/util/inject-css.ts | 4 ++-- src/util/overlays.ts | 4 ++-- src/zoom-control.tsx | 16 ++++++------- tslint.json | 3 ++- 13 files changed, 77 insertions(+), 64 deletions(-) rename src/__tests__/{layer.test.js => layer.test.tsx} (68%) diff --git a/package.json b/package.json index 80053c731..0a4d93c69 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "lib/index.js", "scripts": { "clean": "rm -rf dist", - "lint": "tslint", + "test": "jest", + "lint": "tslint --project tsconfig.json", "build": "tsc", "build:watch": "tsc --watch", "prepublish": "npm run clean && npm run build", @@ -13,10 +14,14 @@ "postversion": "git push && git push --tags" }, "jest": { - "testPathIgnorePatterns": [ - "/node_modules/", - "/lib/", - "/es/" + "transform": { + ".(ts|tsx)": "/node_modules/ts-jest/preprocessor.js" + }, + "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$", + "moduleFileExtensions": [ + "ts", + "tsx", + "js" ], "verbose": true }, @@ -55,9 +60,12 @@ "react-dom": "^15.0.1" }, "devDependencies": { + "@types/jest": "^18.1.1", "@types/mapbox-gl": "^0.29.0", "@types/node": "^7.0.5", "@types/react": "^15.0.6", + "@types/react-addons-test-utils": "^0.14.17", + "@types/recompose": "^0.20.3", "@types/typescript": "^2.0.0", "jest": "^17.0.1", "react": "^15.4.0", @@ -65,6 +73,7 @@ "react-dom": "^15.4.0", "react-test-renderer": "^15.4.0", "recompose": "^0.20.2", + "ts-jest": "^18.0.3", "tslint": "^4.4.2", "tslint-react": "^2.3.0" } diff --git a/src/__tests__/layer.test.js b/src/__tests__/layer.test.tsx similarity index 68% rename from src/__tests__/layer.test.js rename to src/__tests__/layer.test.tsx index 88bbd4678..edd423220 100644 --- a/src/__tests__/layer.test.js +++ b/src/__tests__/layer.test.tsx @@ -1,16 +1,18 @@ -import React from 'react'; +import * as React from 'react'; import Layer from '../layer'; -import TestUtils from 'react-addons-test-utils'; +import * as TestUtils from 'react-addons-test-utils'; import { withContext } from 'recompose'; describe('Layer', () => { - let LayerWithContext; - let addLayerMock; - let addSourceMock; + let LayerWithContext: any; + let addLayerMock = jest.fn(); + let addSourceMock = jest.fn(); + let children: any[]; beforeEach(() => { addLayerMock = jest.fn(); addSourceMock = jest.fn(); + children = [{ props: {}}]; LayerWithContext = withContext({ map: React.PropTypes.object @@ -25,9 +27,10 @@ describe('Layer', () => { }); it('Should render layer with default options', () => { - const LayerComponent = TestUtils.renderIntoDocument( + TestUtils.renderIntoDocument( + children={children} + /> as React.ReactElement ); expect(addLayerMock.mock.calls[0]).toEqual([{ @@ -40,16 +43,17 @@ describe('Layer', () => { }); it('Should render layer with default source', () => { - const LayerComponent = TestUtils.renderIntoDocument( + TestUtils.renderIntoDocument( + children={children} + /> as React.ReactElement ); expect(addSourceMock.mock.calls[0]).toEqual(['layer-2', { type: 'geojson', data: { type: 'FeatureCollection', - features: [], + features: [] } }]); }); diff --git a/src/geojson-layer.ts b/src/geojson-layer.ts index 5d7d23b58..d9ec67c5d 100644 --- a/src/geojson-layer.ts +++ b/src/geojson-layer.ts @@ -99,4 +99,3 @@ export default class GeoJSONLayer extends React.Component { return null; } } - diff --git a/src/index.ts b/src/index.ts index 908ec87ee..2e46f5082 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,8 +23,7 @@ export { ScaleControl, Marker, Source, - Cluster, + Cluster }; export default Map; - diff --git a/src/layer.ts b/src/layer.ts index 94fc18971..9f67f9b48 100644 --- a/src/layer.ts +++ b/src/layer.ts @@ -126,7 +126,7 @@ export default class Layer extends React.PureComponent { onClick({ ...evt, feature, map }); } }); - }; + } private onMouseMove = (evt: any) => { const children = ([] as any).concat(this.props.children); @@ -248,4 +248,3 @@ export default class Layer extends React.PureComponent { return null; } } - diff --git a/src/marker.tsx b/src/marker.tsx index e0951b9a3..c9003e01b 100644 --- a/src/marker.tsx +++ b/src/marker.tsx @@ -3,13 +3,13 @@ import ProjectedLayer from './projected-layer'; export interface Props { coordinates: number[]; - anchor: any; - offset: any; - children: JSX.Element; - onClick: React.MouseEventHandler; - onMouseEnter: React.MouseEventHandler; - onMouseLeave: React.MouseEventHandler; - style: React.CSSProperties; + anchor?: any; + offset?: any; + children?: JSX.Element; + onClick?: React.MouseEventHandler; + onMouseEnter?: React.MouseEventHandler; + onMouseLeave?: React.MouseEventHandler; + style?: React.CSSProperties; } export default class Marker extends React.Component { diff --git a/src/popup.tsx b/src/popup.tsx index 1bc28f48b..11fc344b7 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -6,13 +6,13 @@ import { export interface Props { coordinates: number[]; - anchor: any; - offset: any; - children: JSX.Element; - onClick: React.MouseEventHandler; - onMouseEnter: React.MouseEventHandler; - onMouseLeave: React.MouseEventHandler; - style: React.CSSProperties; + anchor?: any; + offset?: any; + children?: JSX.Element; + onClick?: React.MouseEventHandler; + onMouseEnter?: React.MouseEventHandler; + onMouseLeave?: React.MouseEventHandler; + style?: React.CSSProperties; } export default class Popup extends React.Component { diff --git a/src/projected-layer.tsx b/src/projected-layer.tsx index 416925596..9f2b0e250 100644 --- a/src/projected-layer.tsx +++ b/src/projected-layer.tsx @@ -14,13 +14,13 @@ const defaultStyle = { export interface Props { coordinates: number[]; - anchor: Anchor; - offset: number | number[] | PointDef; - children: JSX.Element; - onClick: React.MouseEventHandler; - onMouseEnter: React.MouseEventHandler; - onMouseLeave: React.MouseEventHandler; - style: React.CSSProperties; + anchor?: Anchor; + offset?: number | number[] | PointDef; + children?: JSX.Element; + onClick?: React.MouseEventHandler; + onMouseEnter?: React.MouseEventHandler; + onMouseLeave?: React.MouseEventHandler; + style?: React.CSSProperties; className: string; } @@ -55,7 +55,7 @@ export default class ProjectedLayer extends React.Component { if (!this.prevent) { this.setState(overlayState(this.props, this.context.map, this.container)); } - }; + } public componentDidMount() { const { map } = this.context; @@ -92,13 +92,13 @@ export default class ProjectedLayer extends React.Component { className, onClick, onMouseEnter, - onMouseLeave, + onMouseLeave } = this.props; const finalStyle = { ...defaultStyle, ...style, - transform: overlayTransform(this.state as OverlayProps).join(' '), + transform: overlayTransform(this.state as OverlayProps).join(' ') }; return ( diff --git a/src/scale-control.tsx b/src/scale-control.tsx index 86be5f5d4..94a92ee27 100644 --- a/src/scale-control.tsx +++ b/src/scale-control.tsx @@ -11,11 +11,13 @@ const scales = [ 10 * 1000 ]; +const defaultPosition = { top: 10, right: 10, bottom: 'auto', left: 'auto' }; + const positions = { - topRight: { top: 10, right: 10, bottom: 'auto', left: 'auto' }, - topLeft: { top: 10, left: 10, bottom: 'auto', right: 'auto' }, - bottomRight: { bottom: 10, right: 10, top: 'auto', left: 'auto' }, - bottomLeft: { bottom: 10, left: 10, top: 'auto', right: 'auto' }, + topRight: defaultPosition, + topLeft: defaultPosition, + bottomRight: defaultPosition, + bottomLeft: defaultPosition }; const containerStyle = { @@ -29,7 +31,7 @@ const containerStyle = { display: 'flex', flexDirection: 'row', alignItems: 'baseline', - padding: '3px 7px', + padding: '3px 7px' }; const scaleStyle = { @@ -38,7 +40,7 @@ const scaleStyle = { borderTop: 'none', height: 7, borderBottomLeftRadius: 1, - borderBottomRightRadius: 1, + borderBottomRightRadius: 1 }; const POSITIONS = Object.keys(positions); @@ -127,7 +129,7 @@ export default class ScaleControl extends React.Component { chosenScale, scaleWidth }); - }; + } private _getDistanceTwoPoints(x: number[], y: number[], measurement = 'km') { const [lng1, lat1] = x; diff --git a/src/util/inject-css.ts b/src/util/inject-css.ts index c3332c917..0d9db549a 100644 --- a/src/util/inject-css.ts +++ b/src/util/inject-css.ts @@ -9,6 +9,6 @@ const injectCSS = (window: Window) => { styleElement.innerHTML = cssRules; head.appendChild(styleElement); } -} +}; -export default injectCSS; \ No newline at end of file +export default injectCSS; diff --git a/src/util/overlays.ts b/src/util/overlays.ts index 11dcdfa9b..001f8ca87 100644 --- a/src/util/overlays.ts +++ b/src/util/overlays.ts @@ -39,7 +39,7 @@ const anchorTranslates = { 'top-left': 'translate(0,0)', 'top-right': 'translate(-100%,0)', 'bottom-left': 'translate(0,-100%)', - 'bottom-right': 'translate(-100%,-100%)', + 'bottom-right': 'translate(-100%,-100%)' }; // Hack /o\ @@ -95,7 +95,7 @@ const normalizedOffsets = (offset: any): any => { 'top-left': new (Point as any)(cornerOffset, cornerOffset), 'top-right': new (Point as any)(-cornerOffset, cornerOffset), 'bottom-left': new (Point as any)(cornerOffset, -cornerOffset), - 'bottom-right': new (Point as any)(-cornerOffset, -cornerOffset), + 'bottom-right': new (Point as any)(-cornerOffset, -cornerOffset) }; } diff --git a/src/zoom-control.tsx b/src/zoom-control.tsx index d6d286fc0..2d080f183 100644 --- a/src/zoom-control.tsx +++ b/src/zoom-control.tsx @@ -7,14 +7,14 @@ const containerStyle = { display: 'flex', flexDirection: 'column', boxShadow: '0px 1px 4px rgba(0, 0, 0, .3)', - border: '1px solid rgba(0, 0, 0, 0.1)', + border: '1px solid rgba(0, 0, 0, 0.1)' }; const positions = { topRight: { top: 10, right: 10, bottom: 'auto', left: 'auto' }, topLeft: { top: 10, left: 10, bottom: 'auto', right: 'auto' }, bottomRight: { bottom: 10, right: 10, top: 'auto', left: 'auto' }, - bottomLeft: { bottom: 10, left: 10, top: 'auto', right: 'auto' }, + bottomLeft: { bottom: 10, left: 10, top: 'auto', right: 'auto' } }; const buttonStyle = { @@ -28,24 +28,24 @@ const buttonStyle = { backgroundImage: 'url(\'https://api.mapbox.com/mapbox.js/v2.4.0/images/icons-000000@2x.png\')', backgroundPosition: '0px 0px', backgroundSize: '26px 260px', - outline: 0, + outline: 0 }; const buttonStyleHovered = { backgroundColor: '#fff', - opacity: 1, + opacity: 1 }; const buttonStylePlus = { borderBottom: '1px solid rgba(0, 0, 0, 0.1)', borderTopLeftRadius: 2, - borderTopRightRadius: 2, + borderTopRightRadius: 2 }; const buttonStyleMinus = { backgroundPosition: '0px -26px', borderBottomLeftRadius: 2, - borderBottomRightRadius: 2, + borderBottomRightRadius: 2 }; const [PLUS, MINUS] = [0, 1]; @@ -96,13 +96,13 @@ export default class ZoomControl extends React.Component { if (PLUS !== this.state.hover) { this.setState({ hover: PLUS }); } - }; + } private minusOver = () => { if (MINUS !== this.state.hover) { this.setState({ hover: MINUS }); } - }; + } private onClickPlus = () => { this.props.onControlClick(this.context.map, this.props.zoomDiff); diff --git a/tslint.json b/tslint.json index bc4693883..f83b7ac6a 100644 --- a/tslint.json +++ b/tslint.json @@ -7,6 +7,7 @@ "interface-name": [true, "never-prefix"], "no-console": false, "object-literal-sort-keys": false, - "member-ordering": false + "member-ordering": false, + "object-literal-key-quotes": false } } From 0186822d4bf3aba4d5f057c6764aae9db048510b Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Sun, 5 Feb 2017 19:56:11 +0000 Subject: [PATCH 13/32] Fix scale control --- package.json | 2 +- src/scale-control.tsx | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 0a4d93c69..d86836537 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "clean": "rm -rf dist", "test": "jest", "lint": "tslint --project tsconfig.json", - "build": "tsc", + "build": "npm run lint && npm run test && tsc", "build:watch": "tsc --watch", "prepublish": "npm run clean && npm run build", "version": "npm run build", diff --git a/src/scale-control.tsx b/src/scale-control.tsx index 94a92ee27..62259b2f4 100644 --- a/src/scale-control.tsx +++ b/src/scale-control.tsx @@ -11,13 +11,11 @@ const scales = [ 10 * 1000 ]; -const defaultPosition = { top: 10, right: 10, bottom: 'auto', left: 'auto' }; - const positions = { - topRight: defaultPosition, - topLeft: defaultPosition, - bottomRight: defaultPosition, - bottomLeft: defaultPosition + topRight: { top: 10, right: 10, bottom: 'auto', left: 'auto' }, + topLeft: { top: 10, left: 10, bottom: 'auto', right: 'auto' }, + bottomRight: { bottom: 10, right: 10, top: 'auto', left: 'auto' }, + bottomLeft: { bottom: 10, left: 10, top: 'auto', right: 'auto' } }; const containerStyle = { @@ -109,14 +107,14 @@ export default class ScaleControl extends React.Component { const totalWidth = this._getDistanceTwoPoints( [_sw.lng, _ne.lat], - [_sw.lng, _ne.lat], + [_ne.lng, _ne.lat], measurement ); const relativeWidth = totalWidth / clientWidth * MIN_WIDTH_SCALE; const chosenScale = scales.reduce((acc, curr) => { - if (curr > relativeWidth) { + if (!acc && curr > relativeWidth) { return curr; } From 1bd8dc54c5c029af18bb204b71cc77b7210f0dd7 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Sun, 5 Feb 2017 20:09:15 +0000 Subject: [PATCH 14/32] Fix from merge of source update --- src/source.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/source.ts b/src/source.ts index 5b5e8f623..ed1dead7c 100644 --- a/src/source.ts +++ b/src/source.ts @@ -7,7 +7,7 @@ import { ImageSource, VideoSource, GeoJSONSourceRaw -} from 'mapbox-gl'; +} from 'mapbox-gl/dist/mapbox-gl'; export interface Context { map: Map; @@ -43,6 +43,22 @@ export default class Source extends React.Component { } } + public componentWillReceiveProps(props: Props) { + const { id } = this; + const { sourceOptions } = this.props; + const { map } = this.context; + + if ((props.sourceOptions as GeoJSONSourceRaw).data !== (sourceOptions as GeoJSONSourceRaw).data) { + (map + .getSource(id) as GeoJSONSource) + .setData((props.sourceOptions as GeoJSONSourceRaw).data as any); + } + } + + public shouldComponentUpdate(nextProps: Props) { + return (nextProps.sourceOptions as GeoJSONSourceRaw).data !== (this.props.sourceOptions as GeoJSONSourceRaw).data; + } + public render() { return null; } From db173f658b2d9531f7b9f412cdabe216d76e8e58 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Tue, 7 Feb 2017 09:17:52 +0000 Subject: [PATCH 15/32] Improve readme file --- README.md | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 944225202..35800332b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,17 @@ # react-mapbox-gl - ![London cycle example gif](docs/london-cycle-example.gif "London cycle example gif") -React wrapper of [mapbox-gl-js](https://www.mapbox.com/mapbox-gl-js/api/) which bring the API to a react friendly way and expose projected react components. +React wrapper of [mapbox-gl-js](https://www.mapbox.com/mapbox-gl-js/api/) which bring the API to a react friendly way. +On top of the layers provided by `react-mapbox-gl` add some React layers, projected using `map.project`. + +Do you need `mapbox-gl-js` and `react-mapbox-gl`? `mapbox-gl-js` expose a map rendered in a canvas using web gl this mean: +- All the shapes are in vector +- Fast rendering +- Smooth transitions +- All the data are on the client side, you can interact with anything on the map +- You can customize everything on the map using [mapbox studio](https://www.mapbox.com/mapbox-studio/) + +See all the features of the map exposed by [mapbox-gl-js](https://www.mapbox.com/maps/) Include the following elements : - ReactMapboxGl @@ -20,6 +29,8 @@ Include the following elements : - Popup (Projected component) - Cluster +> The source files are written in Typescript, you can consume the compiled files in Javascript or Typescript and get the type definition files. + ## How to start ``` @@ -55,20 +66,23 @@ var Feature = ReactMapboxGl.Feature; ## Disclaimer -The zoom property is an array on purpose. With a float as a value we can't tell whether the zoom has changed because `7 === 7 // true`. We did a work around using array so that `[7] !== [7] // true`, this way we can reliably update the zoom value. +The zoom property is an array on purpose. With a float as a value we can't tell whether the zoom has changed when checking for value equality `7 === 7 // true`. +We changed it to an array so that between 2 render it check for a reference equality `[7] === [7] // false`, +this way we can reliably update the zoom value. See https://github.com/alex3165/react-mapbox-gl/issues/57 for more informations. ## Examples -- See the example to display a big amount of markers : [London cycle example](example/src/london-cycle.js) -- See the example to display all the availables shapes : [All shapes example](example/src/all-shapes.js) -- See the example to display a GEOJson file : [geojson example](example/src/geojson-example.js) +- Display a big amount of markers: [London cycle example](example/src/london-cycle.js) +- Display all the availables shapes: [All shapes example](example/src/all-shapes.js) +- Display a GEOJson file: [geojson example](example/src/geojson-example.js) +- Display Cluster of Markers: [cluster example](example/src/cluster.js) ### Run the examples - Clone the repository -- Go to example folder +- Go to the example folder - Install the dependencies: `npm install` - Run the example - Build the library `npm run build` From 5ba2483c7e4ffa220e8d312182a8e4dae33d0f86 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Tue, 7 Feb 2017 09:24:59 +0000 Subject: [PATCH 16/32] Improve readme again --- README.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 35800332b..805cb861f 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,7 @@ React wrapper of [mapbox-gl-js](https://www.mapbox.com/mapbox-gl-js/api/) which bring the API to a react friendly way. On top of the layers provided by `react-mapbox-gl` add some React layers, projected using `map.project`. -Do you need `mapbox-gl-js` and `react-mapbox-gl`? `mapbox-gl-js` expose a map rendered in a canvas using web gl this mean: -- All the shapes are in vector -- Fast rendering -- Smooth transitions -- All the data are on the client side, you can interact with anything on the map -- You can customize everything on the map using [mapbox studio](https://www.mapbox.com/mapbox-studio/) - -See all the features of the map exposed by [mapbox-gl-js](https://www.mapbox.com/maps/) - -Include the following elements : +Include the following elements: - ReactMapboxGl - Layer - Source @@ -31,15 +22,27 @@ Include the following elements : > The source files are written in Typescript, you can consume the compiled files in Javascript or Typescript and get the type definition files. + +## Do you need `mapbox-gl-js` and `react-mapbox-gl` +Mapbox-gl expose a map rendered in a canvas using web gl this mean: +- All the shapes are in vector +- Fast rendering +- Smooth transitions +- All the data are on the client side, you can interact with anything on the map +- You can customize everything on the map using [mapbox studio](https://www.mapbox.com/mapbox-studio/) + +See all the features of the map exposed by [mapbox-gl-js](https://www.mapbox.com/maps/) + + ## How to start -``` +```javascript npm install react-mapbox-gl --save ``` Example: -``` +```javascript // ES6 import ReactMapboxGl, { Layer, Feature } from "react-mapbox-gl"; @@ -65,7 +68,6 @@ var Feature = ReactMapboxGl.Feature; ``` ## Disclaimer - The zoom property is an array on purpose. With a float as a value we can't tell whether the zoom has changed when checking for value equality `7 === 7 // true`. We changed it to an array so that between 2 render it check for a reference equality `[7] === [7] // false`, this way we can reliably update the zoom value. @@ -73,14 +75,12 @@ this way we can reliably update the zoom value. See https://github.com/alex3165/react-mapbox-gl/issues/57 for more informations. ## Examples - - Display a big amount of markers: [London cycle example](example/src/london-cycle.js) - Display all the availables shapes: [All shapes example](example/src/all-shapes.js) - Display a GEOJson file: [geojson example](example/src/geojson-example.js) - Display Cluster of Markers: [cluster example](example/src/cluster.js) ### Run the examples - - Clone the repository - Go to the example folder - Install the dependencies: `npm install` From ec97fb744f3555de9d2e8bd4482816af6ecd73ea Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Wed, 8 Feb 2017 21:18:50 +0000 Subject: [PATCH 17/32] Target es5 and switch from find to filter --- src/map.tsx | 4 ++-- src/util/diff.ts | 2 +- tsconfig.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/map.tsx b/src/map.tsx index 71670b4e4..31c01e265 100644 --- a/src/map.tsx +++ b/src/map.tsx @@ -237,10 +237,10 @@ export default class ReactMapboxGl extends React.Component { // Check for equality + !!fitBounds.filter((c, i) => { // Check for equality const nc = nextProps.fitBounds && nextProps.fitBounds[i]; return c[0] !== (nc && nc[0]) || c[1] !== (nc && nc[1]); - }) + })[0] ); if (didFitBoundsUpdate) { diff --git a/src/util/diff.ts b/src/util/diff.ts index 9d2a6739a..2272adc68 100644 --- a/src/util/diff.ts +++ b/src/util/diff.ts @@ -1,7 +1,7 @@ const reduce = require('reduce-object'); // tslint:disable-line const find = (obj: any, predicate: (...args: any[]) => boolean) => ( - Object.keys(obj).find((key) => predicate(obj[key], key)) + Object.keys(obj).filter((key) => predicate(obj[key], key))[0] ); const diff = (obj1: any, obj2: any) => ( diff --git a/tsconfig.json b/tsconfig.json index dd0b9faf5..47e730588 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "outDir": "lib", "module": "commonjs", - "target": "es6", + "target": "es5", "sourceMap": true, "moduleResolution": "node", "rootDirs": ["src"], From d78a0163e2b8ae72f4a1a00ba89307e410eabe75 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Wed, 8 Feb 2017 22:52:56 +0100 Subject: [PATCH 18/32] Fix typescript, fix feature geojson object to the standard in the meantime --- src/feature.ts | 7 +++++-- src/layer.ts | 40 ++++++++++++++++++++-------------------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/feature.ts b/src/feature.ts index 21b4fcffb..5cce63c46 100644 --- a/src/feature.ts +++ b/src/feature.ts @@ -7,7 +7,10 @@ export interface Props { onHover?: Function; onEndHover?: Function; } - -const Feature: React.StatelessComponent = () => null; +class Feature extends React.Component { + public render() { + return null; + } +} export default Feature; diff --git a/src/layer.ts b/src/layer.ts index 9f67f9b48..710e2d02a 100644 --- a/src/layer.ts +++ b/src/layer.ts @@ -105,25 +105,24 @@ export default class Layer extends React.PureComponent { private feature = (props: any, id: string) => ({ type: 'Feature', geometry: this.geometry(props.coordinates), - properties: { - ...props.properties, - id - } + properties: { ...props.properties }, + id }) private onClick = (evt: any) => { const children = ([] as any).concat(this.props.children); const { map } = this.context; - const { id } = this; - const features = map.queryRenderedFeatures(evt.point, { layers: [id] }); + const features = map.queryRenderedFeatures(evt.point, { layers: [this.id] }); features.forEach((feature) => { - const { properties } = feature; - const child = children[properties.id]; + const { id } = feature; + if (children && id) { + const child = children[id]; - const onClick = child && child.props.onClick; - if (onClick) { - onClick({ ...evt, feature, map }); + const onClick = child && child.props.onClick; + if (onClick) { + onClick({ ...evt, feature, map }); + } } }); } @@ -131,21 +130,22 @@ export default class Layer extends React.PureComponent { private onMouseMove = (evt: any) => { const children = ([] as any).concat(this.props.children); const { map } = this.context; - const { id } = this; const oldHover = this.hover; const hover: string[] = []; - const features = map.queryRenderedFeatures(evt.point, { layers: [id] }); + const features = map.queryRenderedFeatures(evt.point, { layers: [this.id] }); features.forEach((feature) => { - const { properties } = feature; - const child = children[properties.id]; - hover.push(properties.id); - - const onHover = child && child.props.onHover; - if (onHover) { - onHover({ ...evt, feature, map }); + const { id } = feature; + if (children && id) { + const child = children[id]; + hover.push(id); + + const onHover = child && child.props.onHover; + if (onHover) { + onHover({ ...evt, feature, map }); + } } }); From 425de49d580f8dc8dd27f647abdf1ddaf6008fac Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Wed, 8 Feb 2017 23:12:00 +0100 Subject: [PATCH 19/32] Switch to real typescript package --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d86836537..69e1153cc 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,6 @@ "@types/react": "^15.0.6", "@types/react-addons-test-utils": "^0.14.17", "@types/recompose": "^0.20.3", - "@types/typescript": "^2.0.0", "jest": "^17.0.1", "react": "^15.4.0", "react-addons-test-utils": "^15.4.0", @@ -75,6 +74,7 @@ "recompose": "^0.20.2", "ts-jest": "^18.0.3", "tslint": "^4.4.2", - "tslint-react": "^2.3.0" + "tslint-react": "^2.3.0", + "typescript": "^2.1.5" } } From 2ea4e373a3ebdcaf5376990755760b4be3932de2 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Wed, 8 Feb 2017 23:24:13 +0100 Subject: [PATCH 20/32] Remove node types and introduce custom any types --- package.json | 1 - src/cluster.tsx | 3 +-- src/layer.ts | 3 +-- src/map.tsx | 2 +- src/util/diff.ts | 2 +- src/util/global.d.ts | 14 ++++++++++++++ 6 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 src/util/global.d.ts diff --git a/package.json b/package.json index 69e1153cc..ce023ce08 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "devDependencies": { "@types/jest": "^18.1.1", "@types/mapbox-gl": "^0.29.0", - "@types/node": "^7.0.5", "@types/react": "^15.0.6", "@types/react-addons-test-utils": "^0.14.17", "@types/recompose": "^0.20.3", diff --git a/src/cluster.tsx b/src/cluster.tsx index d4e706397..66677a628 100644 --- a/src/cluster.tsx +++ b/src/cluster.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; import * as MapboxGL from 'mapbox-gl'; import { Props as MarkerProps } from './marker'; - -const supercluster = require('supercluster'); //tslint:disable-line +import supercluster from 'supercluster'; export interface Props { ClusterMarkerFactory: (coordinates: number[], pointCount: number) => JSX.Element; diff --git a/src/layer.ts b/src/layer.ts index 710e2d02a..06e85db08 100644 --- a/src/layer.ts +++ b/src/layer.ts @@ -1,7 +1,6 @@ import * as React from 'react'; import * as MapboxGL from 'mapbox-gl'; - -const isEqual = require('deep-equal'); // tslint:disable-line +import isEqual from 'deep-equal'; import diff from './util/diff'; let index = 0; diff --git a/src/map.tsx b/src/map.tsx index 31c01e265..09cfb0df3 100644 --- a/src/map.tsx +++ b/src/map.tsx @@ -1,6 +1,6 @@ import * as MapboxGl from 'mapbox-gl/dist/mapbox-gl'; import * as React from 'react'; -const isEqual = require('deep-equal'); // tslint:disable-line +import isEqual from 'deep-equal'; const events = { onStyleLoad: 'style.load', // Should remain first diff --git a/src/util/diff.ts b/src/util/diff.ts index 2272adc68..b639de089 100644 --- a/src/util/diff.ts +++ b/src/util/diff.ts @@ -1,4 +1,4 @@ -const reduce = require('reduce-object'); // tslint:disable-line +import reduce from 'reduce-object'; const find = (obj: any, predicate: (...args: any[]) => boolean) => ( Object.keys(obj).filter((key) => predicate(obj[key], key))[0] diff --git a/src/util/global.d.ts b/src/util/global.d.ts new file mode 100644 index 000000000..1f001cd2d --- /dev/null +++ b/src/util/global.d.ts @@ -0,0 +1,14 @@ +declare module 'supercluster' { + const supercluster: any; + export default supercluster; +} + +declare module 'deep-equal' { + const isEqual: any; + export default isEqual; +} + +declare module 'reduce-object' { + const reduce: any; + export default reduce; +} \ No newline at end of file From 8b8fb1d7c697a91a1d98b425e5010e719eeade39 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Wed, 8 Feb 2017 23:32:34 +0100 Subject: [PATCH 21/32] Fix lint --- src/cluster.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cluster.tsx b/src/cluster.tsx index 66677a628..5e1e03625 100644 --- a/src/cluster.tsx +++ b/src/cluster.tsx @@ -4,7 +4,7 @@ import { Props as MarkerProps } from './marker'; import supercluster from 'supercluster'; export interface Props { - ClusterMarkerFactory: (coordinates: number[], pointCount: number) => JSX.Element; + ClusterMarkerFactory(coordinates: number[], pointCount: number): JSX.Element; clusterThreshold?: number; radius?: number; maxZoom?: number; From 9cc651edc913d1d0148cd43376ba39e7d368f813 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Wed, 8 Feb 2017 23:36:54 +0100 Subject: [PATCH 22/32] Fix some types in cluster --- src/cluster.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/cluster.tsx b/src/cluster.tsx index 5e1e03625..fe8bea275 100644 --- a/src/cluster.tsx +++ b/src/cluster.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import * as MapboxGL from 'mapbox-gl'; import { Props as MarkerProps } from './marker'; import supercluster from 'supercluster'; +import * as GeoJSON from 'geojson'; export interface Props { ClusterMarkerFactory(coordinates: number[], pointCount: number): JSX.Element; @@ -25,8 +26,10 @@ export interface Context { } export interface Point { + type: string; geometry: { - coordinates: number[]; + type: string; + coordinates: GeoJSON.Position; }; properties: { point_count: number; @@ -94,7 +97,7 @@ export default class Cluster extends React.Component { } } - private feature(coordinates: number[]) { + private feature(coordinates: GeoJSON.Position): Point { return { type: 'Feature', geometry: { From 2d40152c36adbf0e88d35e53cd45d0ff68f90900 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Fri, 10 Feb 2017 10:47:08 +0100 Subject: [PATCH 23/32] Fix coordinates typings --- src/cluster.tsx | 2 +- src/feature.ts | 3 ++- src/layer.ts | 3 ++- src/marker.tsx | 3 ++- src/popup.tsx | 3 ++- src/projected-layer.tsx | 3 ++- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/cluster.tsx b/src/cluster.tsx index fe8bea275..e4c7465f3 100644 --- a/src/cluster.tsx +++ b/src/cluster.tsx @@ -5,7 +5,7 @@ import supercluster from 'supercluster'; import * as GeoJSON from 'geojson'; export interface Props { - ClusterMarkerFactory(coordinates: number[], pointCount: number): JSX.Element; + ClusterMarkerFactory(coordinates: GeoJSON.Position, pointCount: number): JSX.Element; clusterThreshold?: number; radius?: number; maxZoom?: number; diff --git a/src/feature.ts b/src/feature.ts index 5cce63c46..b9be792c1 100644 --- a/src/feature.ts +++ b/src/feature.ts @@ -1,7 +1,8 @@ import * as React from 'react'; +import * as GeoJSON from 'geojson'; export interface Props { - coordinates: number[]; + coordinates: GeoJSON.Position; properties: any; onClick?: Function; onHover?: Function; diff --git a/src/layer.ts b/src/layer.ts index 06e85db08..d4cc19813 100644 --- a/src/layer.ts +++ b/src/layer.ts @@ -2,6 +2,7 @@ import * as React from 'react'; import * as MapboxGL from 'mapbox-gl'; import isEqual from 'deep-equal'; import diff from './util/diff'; +import * as GeoJSON from 'geojson'; let index = 0; const generateID = () => { @@ -79,7 +80,7 @@ export default class Layer extends React.PureComponent { } }; - private geometry = (coordinates: number[]) => { + private geometry = (coordinates: GeoJSON.Position) => { switch (this.props.type) { case 'symbol': case 'circle': return { diff --git a/src/marker.tsx b/src/marker.tsx index c9003e01b..46796e73b 100644 --- a/src/marker.tsx +++ b/src/marker.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import ProjectedLayer from './projected-layer'; +import * as GeoJSON from 'geojson'; export interface Props { - coordinates: number[]; + coordinates: GeoJSON.Position; anchor?: any; offset?: any; children?: JSX.Element; diff --git a/src/popup.tsx b/src/popup.tsx index 11fc344b7..0079a29dc 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -3,9 +3,10 @@ import ProjectedLayer from './projected-layer'; import { anchors } from './util/overlays'; +import * as GeoJSON from 'geojson'; export interface Props { - coordinates: number[]; + coordinates: GeoJSON.Position; anchor?: any; offset?: any; children?: JSX.Element; diff --git a/src/projected-layer.tsx b/src/projected-layer.tsx index 9f2b0e250..26e7ce935 100644 --- a/src/projected-layer.tsx +++ b/src/projected-layer.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Map } from 'mapbox-gl'; import { Anchor, PointDef, OverlayProps } from './util/overlays'; +import * as GeoJSON from 'geojson'; import { overlayState, @@ -13,7 +14,7 @@ const defaultStyle = { }; export interface Props { - coordinates: number[]; + coordinates: GeoJSON.Position; anchor?: Anchor; offset?: number | number[] | PointDef; children?: JSX.Element; From 9bb5f836ef99022f9700620dc089cbc1dba4696d Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Fri, 10 Feb 2017 11:04:22 +0100 Subject: [PATCH 24/32] Killed suppressImplicitAnyIndexErrors with :fire: --- src/geojson-layer.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/geojson-layer.ts b/src/geojson-layer.ts index d9ec67c5d..eaea9fd32 100644 --- a/src/geojson-layer.ts +++ b/src/geojson-layer.ts @@ -23,6 +23,9 @@ export interface Props { fillPaint?: MapboxGL.FillPaint; } +type Paints = MapboxGL.LinePaint | MapboxGL.SymbolPaint | MapboxGL.CirclePaint | MapboxGL.FillPaint; +type Layouts = MapboxGL.FillLayout | MapboxGL.LineLayout | MapboxGL.CircleLayout | MapboxGL.SymbolLayout; + export interface Context { map: MapboxGL.Map; } @@ -52,8 +55,8 @@ export default class GeoJSONLayer extends React.Component { const layerId = `${id}-${type}`; layerIds.push(layerId); - const paint = this.props[`${type}Paint`] || {}; - const layout = this.props[`${type}Layout`] || {}; + const paint: Paints = this.props[`${type}Paint`] || {}; + const layout: Layouts = this.props[`${type}Layout`] || {}; map.addLayer({ id: layerId, From 61cd6d66cea82dcede080e5e1234415bd80eb59e Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Fri, 10 Feb 2017 11:07:25 +0100 Subject: [PATCH 25/32] Fix css.ts --- src/constants/css.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/constants/css.ts b/src/constants/css.ts index 6dd20c98f..121cdcdaa 100644 --- a/src/constants/css.ts +++ b/src/constants/css.ts @@ -1,4 +1,5 @@ -// tslint:disable +// tslint:disable:max-line-length + export default ` .mapboxgl-map { font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif; @@ -245,4 +246,4 @@ export default ` display:none; } } -` as string; +`; From 56324161d165d0483d71196ac4925dfe2006aaf0 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Fri, 10 Feb 2017 11:11:54 +0100 Subject: [PATCH 26/32] Disable multiline only in cluster --- src/cluster.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cluster.tsx b/src/cluster.tsx index e4c7465f3..2c5b2a60f 100644 --- a/src/cluster.tsx +++ b/src/cluster.tsx @@ -128,7 +128,7 @@ export default class Cluster extends React.Component { return (
- {// tslint:disable-line + {// tslint:disable-line:jsx-no-multiline-js clusterPoints.map(({ geometry, properties }: Point) => ( ClusterMarkerFactory(geometry.coordinates, properties.point_count)) )} From bd023a8c37fb243447c953fee3e70601a25be1f2 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Fri, 10 Feb 2017 12:21:47 +0100 Subject: [PATCH 27/32] Remove missing suppressImplicitAnyIndexErrors --- tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 47e730588..f56316c1f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,6 @@ "noImplicitThis": true, "noImplicitAny": true, "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, "noUnusedLocals": true, "declaration": true }, From 563ccd1a2fa9285127563af894daf2de3d098388 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Sat, 11 Feb 2017 13:39:16 +0100 Subject: [PATCH 28/32] Add back suppressImplicitAnyIndexErrors --- tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index f56316c1f..37ee1fe74 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "noImplicitAny": true, "strictNullChecks": true, "noUnusedLocals": true, - "declaration": true + "declaration": true, + "suppressImplicitAnyIndexErrors": true }, "include": [ "src/**/*" From fca4a0928f82190937299f4d26fb559ad5b336cc Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Sat, 11 Feb 2017 16:26:52 +0100 Subject: [PATCH 29/32] Fix remaining type issues --- src/feature.ts | 5 +++-- src/geojson-layer.ts | 13 ++++--------- src/layer.ts | 20 ++++---------------- src/marker.tsx | 22 ++++++++++------------ src/source.ts | 18 +++++++----------- src/util/types.ts | 14 ++++++++++++++ src/util/uid.ts | 5 +++++ 7 files changed, 47 insertions(+), 50 deletions(-) create mode 100644 src/util/types.ts create mode 100644 src/util/uid.ts diff --git a/src/feature.ts b/src/feature.ts index b9be792c1..d0ec29d54 100644 --- a/src/feature.ts +++ b/src/feature.ts @@ -1,4 +1,4 @@ -import * as React from 'react'; +import { Component } from 'react'; import * as GeoJSON from 'geojson'; export interface Props { @@ -8,7 +8,8 @@ export interface Props { onHover?: Function; onEndHover?: Function; } -class Feature extends React.Component { + +class Feature extends Component { public render() { return null; } diff --git a/src/geojson-layer.ts b/src/geojson-layer.ts index eaea9fd32..140eea63f 100644 --- a/src/geojson-layer.ts +++ b/src/geojson-layer.ts @@ -1,16 +1,11 @@ import * as React from 'react'; import * as MapboxGL from 'mapbox-gl/dist/mapbox-gl'; - -let index = 0; -const generateID = () => { - const newId = index + 1; - index = newId; - return index; -}; +import { generateID } from './util/uid'; +import { Sources, SourceOptionData } from './util/types'; export interface Props { id?: string; - data: GeoJSON.Feature | GeoJSON.FeatureCollection | string; + data: SourceOptionData; sourceOptions: MapboxGL.VectorSource | MapboxGL.RasterSource | MapboxGL.GeoJSONSource | MapboxGL.GeoJSONSourceRaw; before?: string; fillLayout?: MapboxGL.FillLayout; @@ -39,7 +34,7 @@ export default class GeoJSONLayer extends React.Component { private id: string = this.props.id || `geojson-${generateID()}`; - private source = { + private source: Sources = { type: 'geojson', ...this.props.sourceOptions, data: this.props.data diff --git a/src/layer.ts b/src/layer.ts index d4cc19813..79e0eca79 100644 --- a/src/layer.ts +++ b/src/layer.ts @@ -3,20 +3,8 @@ import * as MapboxGL from 'mapbox-gl'; import isEqual from 'deep-equal'; import diff from './util/diff'; import * as GeoJSON from 'geojson'; - -let index = 0; -const generateID = () => { - const newId = index + 1; - index = newId; - return index; -}; - -export type Sources = ( - MapboxGL.VectorSource | - MapboxGL.RasterSource | - MapboxGL.GeoJSONSource | - MapboxGL.GeoJSONSourceRaw -); +import { generateID } from './util/uid'; +import { Sources } from './util/types'; export type Paint = ( MapboxGL.BackgroundPaint | @@ -102,7 +90,7 @@ export default class Layer extends React.PureComponent { } } - private feature = (props: any, id: string) => ({ + private makeFeature = (props: any, id: string) => ({ type: 'Feature', geometry: this.geometry(props.coordinates), properties: { ...props.properties }, @@ -235,7 +223,7 @@ export default class Layer extends React.PureComponent { const children = ([] as any).concat(this.props.children); const features = children - .map(({ props }: any, id: string) => this.feature(props, id)) + .map(({ props }: any, id: string) => this.makeFeature(props, id)) .filter(Boolean); const source = map.getSource(this.props.sourceId || this.id); diff --git a/src/marker.tsx b/src/marker.tsx index 46796e73b..f86f1b89b 100644 --- a/src/marker.tsx +++ b/src/marker.tsx @@ -13,15 +13,13 @@ export interface Props { style?: React.CSSProperties; } -export default class Marker extends React.Component { - public render() { - return ( - - {this.props.children} - - ); - } -} +const Marker: React.StatelessComponent = (props) => ( + + {props.children} + +); + +export default Marker; diff --git a/src/source.ts b/src/source.ts index ed1dead7c..04487568e 100644 --- a/src/source.ts +++ b/src/source.ts @@ -1,19 +1,15 @@ import * as React from 'react'; import { Map, - VectorSource, - RasterSource, GeoJSONSource, - ImageSource, - VideoSource, GeoJSONSourceRaw } from 'mapbox-gl/dist/mapbox-gl'; - +import { SourceOptionData } from './util/types'; export interface Context { map: Map; } -export type Sources = VectorSource | RasterSource | GeoJSONSource | ImageSource | VideoSource | GeoJSONSourceRaw; +export type Sources = GeoJSONSourceRaw; export interface Props { id: string; @@ -48,15 +44,15 @@ export default class Source extends React.Component { const { sourceOptions } = this.props; const { map } = this.context; - if ((props.sourceOptions as GeoJSONSourceRaw).data !== (sourceOptions as GeoJSONSourceRaw).data) { - (map - .getSource(id) as GeoJSONSource) - .setData((props.sourceOptions as GeoJSONSourceRaw).data as any); + if (props.sourceOptions.data !== sourceOptions.data) { + const source = map.getSource(id) as GeoJSONSource; + const data = props.sourceOptions.data as SourceOptionData; + source.setData(data); } } public shouldComponentUpdate(nextProps: Props) { - return (nextProps.sourceOptions as GeoJSONSourceRaw).data !== (this.props.sourceOptions as GeoJSONSourceRaw).data; + return nextProps.sourceOptions.data !== this.props.sourceOptions.data; } public render() { diff --git a/src/util/types.ts b/src/util/types.ts new file mode 100644 index 000000000..e2a2237ef --- /dev/null +++ b/src/util/types.ts @@ -0,0 +1,14 @@ +import * as MapboxGL from 'mapbox-gl'; + +export type Sources = ( + MapboxGL.VectorSource | + MapboxGL.RasterSource | + MapboxGL.GeoJSONSource | + MapboxGL.GeoJSONSourceRaw +); + +export type SourceOptionData = ( + GeoJSON.Feature | + GeoJSON.FeatureCollection | + string +); diff --git a/src/util/uid.ts b/src/util/uid.ts new file mode 100644 index 000000000..640152559 --- /dev/null +++ b/src/util/uid.ts @@ -0,0 +1,5 @@ +let index = 0; + +export const generateID = () => { + return index += 1; +}; From 6263da19f9b49a3397823f3e94524de0744fc74b Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Mon, 13 Feb 2017 09:40:01 +0000 Subject: [PATCH 30/32] Fix deep-equal --- package.json | 1 + src/layer.ts | 2 +- src/map.tsx | 2 +- src/util/global.d.ts | 5 ----- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index ce023ce08..69e1153cc 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "devDependencies": { "@types/jest": "^18.1.1", "@types/mapbox-gl": "^0.29.0", + "@types/node": "^7.0.5", "@types/react": "^15.0.6", "@types/react-addons-test-utils": "^0.14.17", "@types/recompose": "^0.20.3", diff --git a/src/layer.ts b/src/layer.ts index 79e0eca79..b5c62d462 100644 --- a/src/layer.ts +++ b/src/layer.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import * as MapboxGL from 'mapbox-gl'; -import isEqual from 'deep-equal'; +const isEqual = require('deep-equal'); //tslint:disable-line import diff from './util/diff'; import * as GeoJSON from 'geojson'; import { generateID } from './util/uid'; diff --git a/src/map.tsx b/src/map.tsx index 09cfb0df3..561a9f32f 100644 --- a/src/map.tsx +++ b/src/map.tsx @@ -1,6 +1,6 @@ import * as MapboxGl from 'mapbox-gl/dist/mapbox-gl'; import * as React from 'react'; -import isEqual from 'deep-equal'; +const isEqual = require('deep-equal'); //tslint:disable-line const events = { onStyleLoad: 'style.load', // Should remain first diff --git a/src/util/global.d.ts b/src/util/global.d.ts index 1f001cd2d..f37fae17f 100644 --- a/src/util/global.d.ts +++ b/src/util/global.d.ts @@ -3,11 +3,6 @@ declare module 'supercluster' { export default supercluster; } -declare module 'deep-equal' { - const isEqual: any; - export default isEqual; -} - declare module 'reduce-object' { const reduce: any; export default reduce; From fada71a929d7ec04a373103174718f7c3690e27f Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Mon, 13 Feb 2017 12:36:43 +0000 Subject: [PATCH 31/32] Fix critical types --- example/server.js | 12 ++++++------ src/cluster.tsx | 18 ++++-------------- src/layer.ts | 19 +++++++++++-------- src/util/diff.ts | 2 +- src/util/global.d.ts | 9 --------- src/util/types.ts | 9 +++++++++ 6 files changed, 31 insertions(+), 38 deletions(-) delete mode 100644 src/util/global.d.ts diff --git a/example/server.js b/example/server.js index 601955655..ef254ee25 100644 --- a/example/server.js +++ b/example/server.js @@ -1,17 +1,17 @@ -var WebpackDevServer = require('webpack-dev-server') -var webpack = require('webpack') -var config = require('./webpack.config') +var WebpackDevServer = require('webpack-dev-server'); +var webpack = require('webpack'); +var config = require('./webpack.config'); var compiler = webpack(config) -var port = 8080; +var port = 8081; var host = 'localhost'; var server = new WebpackDevServer(compiler, { publicPath: config.output.publicPath, historyApiFallback: true, - noInfo: true + noInfo: true, }) server.listen(port, host, function() { console.log(`☕️ Server is listening on http://${host}:${port}.`); -}) +}); diff --git a/src/cluster.tsx b/src/cluster.tsx index 2c5b2a60f..f34d29561 100644 --- a/src/cluster.tsx +++ b/src/cluster.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import * as MapboxGL from 'mapbox-gl'; import { Props as MarkerProps } from './marker'; -import supercluster from 'supercluster'; +const supercluster = require('supercluster'); // tslint:disable-line import * as GeoJSON from 'geojson'; +import { Feature } from './util/types'; export interface Props { ClusterMarkerFactory(coordinates: GeoJSON.Position, pointCount: number): JSX.Element; @@ -25,17 +26,6 @@ export interface Context { map: MapboxGL.Map; } -export interface Point { - type: string; - geometry: { - type: string; - coordinates: GeoJSON.Position; - }; - properties: { - point_count: number; - }; -} - export default class Cluster extends React.Component { public context: Context; @@ -97,7 +87,7 @@ export default class Cluster extends React.Component { } } - private feature(coordinates: GeoJSON.Position): Point { + private feature(coordinates: GeoJSON.Position): Feature { return { type: 'Feature', geometry: { @@ -129,7 +119,7 @@ export default class Cluster extends React.Component { return (
{// tslint:disable-line:jsx-no-multiline-js - clusterPoints.map(({ geometry, properties }: Point) => ( + clusterPoints.map(({ geometry, properties }: Feature) => ( ClusterMarkerFactory(geometry.coordinates, properties.point_count)) )}
diff --git a/src/layer.ts b/src/layer.ts index b5c62d462..cf39197c9 100644 --- a/src/layer.ts +++ b/src/layer.ts @@ -5,6 +5,7 @@ import diff from './util/diff'; import * as GeoJSON from 'geojson'; import { generateID } from './util/uid'; import { Sources } from './util/types'; +import { Feature } from './util/types'; export type Paint = ( MapboxGL.BackgroundPaint | @@ -86,24 +87,26 @@ export default class Layer extends React.PureComponent { coordinates }; - default: return null; + default: return { + type: 'Point', + coordinates + }; } } - private makeFeature = (props: any, id: string) => ({ + private makeFeature = (props: any, id: string): Feature => ({ type: 'Feature', geometry: this.geometry(props.coordinates), - properties: { ...props.properties }, - id + properties: { ...props.properties, id } }) private onClick = (evt: any) => { const children = ([] as any).concat(this.props.children); const { map } = this.context; - const features = map.queryRenderedFeatures(evt.point, { layers: [this.id] }); + const features = map.queryRenderedFeatures(evt.point, { layers: [this.id] }) as Feature[]; features.forEach((feature) => { - const { id } = feature; + const { id } = feature.properties; if (children && id) { const child = children[id]; @@ -122,10 +125,10 @@ export default class Layer extends React.PureComponent { const oldHover = this.hover; const hover: string[] = []; - const features = map.queryRenderedFeatures(evt.point, { layers: [this.id] }); + const features = map.queryRenderedFeatures(evt.point, { layers: [this.id] }) as Feature[]; features.forEach((feature) => { - const { id } = feature; + const { id } = feature.properties; if (children && id) { const child = children[id]; hover.push(id); diff --git a/src/util/diff.ts b/src/util/diff.ts index b639de089..2272adc68 100644 --- a/src/util/diff.ts +++ b/src/util/diff.ts @@ -1,4 +1,4 @@ -import reduce from 'reduce-object'; +const reduce = require('reduce-object'); // tslint:disable-line const find = (obj: any, predicate: (...args: any[]) => boolean) => ( Object.keys(obj).filter((key) => predicate(obj[key], key))[0] diff --git a/src/util/global.d.ts b/src/util/global.d.ts deleted file mode 100644 index f37fae17f..000000000 --- a/src/util/global.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare module 'supercluster' { - const supercluster: any; - export default supercluster; -} - -declare module 'reduce-object' { - const reduce: any; - export default reduce; -} \ No newline at end of file diff --git a/src/util/types.ts b/src/util/types.ts index e2a2237ef..0a805c81e 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -12,3 +12,12 @@ export type SourceOptionData = ( GeoJSON.FeatureCollection | string ); + +export interface Feature { + type: string; + geometry: { + type: string; + coordinates: GeoJSON.Position; + }; + properties: any; +}; From 74118f59d07c4d167bc9f4454b168e9d8760ee15 Mon Sep 17 00:00:00 2001 From: Alexandre Rieux Date: Mon, 13 Feb 2017 12:38:21 +0000 Subject: [PATCH 32/32] Improve readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 805cb861f..0f98884bf 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![London cycle example gif](docs/london-cycle-example.gif "London cycle example gif") React wrapper of [mapbox-gl-js](https://www.mapbox.com/mapbox-gl-js/api/) which bring the API to a react friendly way. -On top of the layers provided by `react-mapbox-gl` add some React layers, projected using `map.project`. +On top of the layers provided, `react-mapbox-gl` add some React rendered layers, projected using `map.project`. Include the following elements: - ReactMapboxGl