open source

This commit is contained in:
Victor Powell 2015-10-07 18:27:07 -07:00
commit d8047648ea
27 changed files with 68162 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.DS_Store
node_modules
npm-debug.log

102
LICENSE Normal file
View File

@ -0,0 +1,102 @@
Copyright (c) 2015 Uber Technologies, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
This contains code from MapboxGL-js
Copyright (c) 2014, Mapbox
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of Mapbox GL JS nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------
Contains glmatrix.js
Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-------------------------------------------------------------------------------
Contains Hershey Simplex Font: http://paulbourke.net/dataformats/hershey/
-------------------------------------------------------------------------------
Contains code from glfx.js
Copyright (C) 2011 by Evan Wallace
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

88
README.md Normal file
View File

@ -0,0 +1,88 @@
# react-map-gl
react-map-gl provides a [React](http://facebook.github.io/react/) friendly
API wrapper around [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/). A webGL
based vector tile mapping library.
WARNING: This project is still very new and the API may change. There also may
be Mapbox APIs that haven't yet been exposed.
![](react-map-gl-screenshots.png)
## Overview
react-map-gl provides an `overlay` API so you can add visualization overlays.
Supported Overlays:
1. ChoroplethOverlay
2. ScatterplotOverlay
3. DraggablePointsOverlay
4. SVGOverlay
5. CanvasOverlay
## Installation
```
npm install react-map-gl --save
```
## Usage
````js
<MapGL width={400} height={400} latitude={37.7577} longitude={-122.4376}
zoom={8} onChangeViewport={function(opts) {
// opts = {latitude, longitude, zoom, bbox}
}}
/>
````
### Using overlays
````js
<MapGL {...mapProps}>
<ScatterplotOverlay locations={locations} dotRadius={4} globalOpacity={1}
compositeOperation="screen" />
// Add additional overlays here...
])
````
### ImmutableJS all the things
The `mapStyle` property of the `MapGL` as well as several of the built in
overlay properties must be provided as
[ImmutableJS](https://facebook.github.io/immutable-js/) objects. This allows
the library to be fast since computing changes to props only involves checking
if the immutable objects are the same instance.
## Development
To develop on this component, install the dependencies and then build and watch
the static files.
```bash
$ npm install
```
To serve example app:
```bash
$ npm start &
$ open "http://localhost:9966/?access_token="`echo $MapboxAccessToken`
```
Where `echo $MapboxAccessToken` returns your Mapbox access token.
Once complete, you can view the component in your browser at
[localhost:9966](http://localhost:9966). Any changes you make will automatically
run the compiler to build the files again.
## Disclaimer
This project is not affiliated with either Facebook or Mapbox.
## Example Data
1. SF GeoJSON data from: [SF OpenData](http://data.sfgov.org).

12
example/data/cities.json Normal file
View File

@ -0,0 +1,12 @@
[
{
"cityName": "San Francisco",
"latitude": 37.7749,
"longitude": -122.4194
},
{
"cityName": "Pittsburgh",
"latitude": 40.4406,
"longitude": -79.9959
}
]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,274 @@
[
{
"coordinates": [
[
37.794219992184324,
-122.3933557873733
],
[
37.79398165755795,
-122.39367189820892
],
[
37.79388241198406,
-122.39401838092769
],
[
37.794423228499795,
-122.3947071294908
],
[
37.79445423060049,
-122.39494688373745
],
[
37.793858299053696,
-122.39571409732676
],
[
37.79245284608079,
-122.39741853206209
],
[
37.79106459055073,
-122.39918399515115
],
[
37.79035713167143,
-122.40003607642596
],
[
37.790068858430104,
-122.40213357823212
],
[
37.78983463559349,
-122.40388909604812
],
[
37.78960041201444,
-122.4054166245374
],
[
37.78920403195862,
-122.40869967084274
],
[
37.788789632353996,
-122.41193711928267
],
[
37.788429282982534,
-122.41526576345333
],
[
37.78799686141706,
-122.41848041296066
],
[
37.78667030140305,
-122.42820266442968
],
[
37.785191419954316,
-122.43987587617389
],
[
37.78061374219536,
-122.43880657433475
],
[
37.777866999452826,
-122.43827192341516
],
[
37.776704885260315,
-122.44669267539851
],
[
37.7757540509692,
-122.44660356691189
],
[
37.77476798767644,
-122.45453422221897
]
]
},
{
"coordinates": [
[
37.75251037780083,
-122.44080380201595
],
[
37.7523902843234,
-122.4425396821639
],
[
37.75281918870459,
-122.44225217701438
],
[
37.75321806754765,
-122.44200806886859
],
[
37.753471118602434,
-122.44180193310093
],
[
37.75388286084137,
-122.44130829218389
],
[
37.75459482638962,
-122.44084177439416
],
[
37.7546745592597,
-122.44071982406115
],
[
37.75508766756418,
-122.44044034714439
],
[
37.75567049861364,
-122.44012846710689
],
[
37.755907472562626,
-122.44005961047523
],
[
37.75640703677898,
-122.44015276944751
],
[
37.756528133045606,
-122.44018924596475
],
[
37.75666225134219,
-122.44033181327828
],
[
37.75680778368681,
-122.44070537573275
],
[
37.75725150991822,
-122.44023039641965
],
[
37.757200599225015,
-122.43988427866199
],
[
37.75708923196163,
-122.4395582840298
],
[
37.757098777733574,
-122.43904715664357
],
[
37.75743287897778,
-122.4390310581432
],
[
37.757496186354615,
-122.43797228289426
],
[
37.75758520091664,
-122.43690020835358
],
[
37.75763591465913,
-122.43582256860111
],
[
37.75773734204003,
-122.43356465673874
],
[
37.7578184838447,
-122.43248701698633
],
[
37.75940073124414,
-122.4326537945671
],
[
37.76103365621418,
-122.43275642692444
],
[
37.76182474975916,
-122.43285905928187
],
[
37.762663828581225,
-122.43293905683697
],
[
37.764194972084184,
-122.43313584578942
],
[
37.76511200542191,
-122.43195511207469
],
[
37.76584889897153,
-122.4310229538788
],
[
37.766757724237436,
-122.42984222016415
],
[
37.76783028743526,
-122.4285786279431
],
[
37.76905839413027,
-122.42690074319054
],
[
37.77008998799232,
-122.42561643634292
],
[
37.77093326434769,
-122.42458070501422
],
[
37.77189114912355,
-122.42341032861272
],
[
37.77302094598606,
-122.42198101937899
],
[
37.773970616950706,
-122.42074849909775
],
[
37.774813849048584,
-122.41969205314248
],
[
37.77560795203675,
-122.41863560718714
],
[
37.77623831637716,
-122.41794166719687
]
]
}
]

View File

@ -0,0 +1,91 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var assign = require('object-assign');
var React = require('react');
var r = require('r-dom');
var Immutable = require('immutable');
var MapGL = require('../../src/index.js');
var ChoroplethOverlay = require('../../src/overlays/choropleth.react');
// San Francisco
var location = require('./../data/cities.json')[0];
var _zipcodesSF = require('./../data/feature-example-sf.json');
_zipcodesSF.features.forEach(function _forEach(feature) {
feature.properties.value = Math.random() * 1000;
});
var ZIPCODES_SF = Immutable.fromJS(_zipcodesSF);
var ChoroplethOverlayExample = React.createClass({
displayName: 'ChoroplethOverlayExample',
PropTypes: {
width: React.PropTypes.number.isRequired,
height: React.PropTypes.number.isRequired
},
getInitialState: function getInitialState() {
return {
latitude: location.latitude,
longitude: location.longitude,
zoom: 11,
startDragLatLng: null,
isDragging: false
};
},
_onChangeViewport: function _onChangeViewport(opt) {
this.setState({
latitude: opt.latitude,
longitude: opt.longitude,
zoom: opt.zoom,
startDragLatLng: opt.startDragLatLng,
isDragging: opt.isDragging
});
},
render: function render() {
return r(MapGL, assign({
latitude: this.state.latitude,
longitude: this.state.longitude,
zoom: this.state.zoom,
width: this.props.width,
height: this.props.height,
startDragLatLng: this.state.startDragLatLng,
isDragging: this.state.isDragging,
onChangeViewport: this.props.onChangeViewport || this._onChangeViewport
}, this.props), [
r(ChoroplethOverlay, {
globalOpacity: 0.8,
colorDomain: [0, 500, 1000],
colorRange: ['#31a354', '#addd8e', '#f7fcb9'],
renderWhileDragging: false,
features: ZIPCODES_SF.get('features')
})
]);
}
});
module.exports = ChoroplethOverlayExample;

View File

@ -0,0 +1,153 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var assign = require('object-assign');
var React = require('react');
var d3 = require('d3');
var window = require('global/window');
var alphaify = require('alphaify');
var transform = require('svg-transform');
var Immutable = require('immutable');
var r = require('r-dom');
var MapGL = require('../../src/index.js');
var CanvasOverlay = require('../../src/overlays/canvas.react');
var SVGOverlay = require('../../src/overlays/svg.react');
// San Francisco
var location = require('./../data/cities.json')[0];
var wiggle = (function _wiggle() {
var normal = d3.random.normal();
return function __wiggle(scale) {
return normal() * scale;
};
}());
// Example data.
var locations = Immutable.fromJS(d3.range(30).map(function _map() {
return [location.latitude + wiggle(0.01), location.longitude + wiggle(0.01)];
}));
var OverlayExample = React.createClass({
displayName: 'OverlayExample',
PropTypes: {
width: React.PropTypes.number.isRequired,
height: React.PropTypes.number.isRequired
},
getInitialState: function getInitialState() {
return {
latitude: location.latitude,
longitude: location.longitude,
zoom: 12.4,
startDragLatLng: null,
isDragging: false
};
},
_onChangeViewport: function _onChangeViewport(opt) {
this.setState({
latitude: opt.latitude,
longitude: opt.longitude,
zoom: opt.zoom,
startDragLatLng: opt.startDragLatLng,
isDragging: opt.isDragging
});
},
render: function render() {
return r(MapGL, assign({
latitude: this.state.latitude,
longitude: this.state.longitude,
zoom: this.state.zoom,
startDragLatLng: this.state.startDragLatLng,
isDragging: this.state.isDragging,
width: this.props.width,
height: this.props.height,
onChangeViewport: this.props.onChangeViewport || this._onChangeViewport
}, this.props), [
r(CanvasOverlay, {redraw: function _redrawCanvas(opt) {
var p1 = opt.project([location.latitude, location.longitude]);
opt.ctx.clearRect(0, 0, opt.width, opt.height);
opt.ctx.strokeStyle = alphaify('#1FBAD6', 0.4);
opt.ctx.lineWidth = 2;
locations.forEach(function forEach(loc, index) {
opt.ctx.beginPath();
var p2 = opt.project(loc.toArray());
opt.ctx.moveTo(p1.x, p1.y);
opt.ctx.lineTo(p2.x, p2.y);
opt.ctx.stroke();
opt.ctx.beginPath();
opt.ctx.fillStyle = alphaify('#1FBAD6', 0.4);
opt.ctx.arc(p2.x, p2.y, 6, 0, 2 * Math.PI);
opt.ctx.fill();
opt.ctx.beginPath();
opt.ctx.fillStyle = '#FFFFFF';
opt.ctx.textAlign = 'center';
opt.ctx.fillText(index, p2.x, p2.y + 4);
});
}}),
// We use invisible SVG elements to support interactivity.
r(SVGOverlay, {
redraw: function _redrwaSVGOverlay(opt) {
var p1 = opt.project([location.latitude, location.longitude]);
var style = {
// transparent but still clickable.
fill: 'rgba(0, 0, 0, 0)'
};
return r.g({
style: {
pointerEvents: 'all',
cursor: 'pointer'
}
}, [r.circle({
style: assign({}, style, {stroke: alphaify('#1FBAD6', 0.8)}),
r: 10,
onClick: function onClick() {
var windowAlert = window.alert;
windowAlert('center');
},
transform: transform([{translate: [p1.x, p1.y]}]),
key: 0
})].concat(locations.map(function _map(loc, index) {
var p2 = opt.project([loc.get(0), loc.get(1)]);
return r.circle({
style: style,
r: 6,
onClick: function onClick() {
var windowAlert = window.alert;
windowAlert('dot ' + index);
},
transform: transform([{translate: [p2.x, p2.y]}]),
key: index + 1
});
}, this))
);
}.bind(this)
})
]);
}
});
module.exports = OverlayExample;

View File

@ -0,0 +1,149 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var assign = require('object-assign');
var React = require('react');
var r = require('r-dom');
var Immutable = require('immutable');
var alphaify = require('alphaify');
var MapGL = require('../../src/index.js');
var DraggableOverlay = require('../../src/overlays/draggable-points.react');
var SVGOverlay = require('../../src/overlays/svg.react');
// A mock example path.
var initialPoints = [
{location: [37.79450507471435, -122.39508481737994], id: 0},
{location: [37.79227619464379, -122.39750244137034], id: 1},
{location: [37.789251178427776, -122.4013303460217], id: 2},
{location: [37.786862920252986, -122.40475531334141], id: 3},
{location: [37.78861431712821, -122.40505751634022], id: 4},
{location: [37.79060449046487, -122.40556118800487], id: 5},
{location: [37.790047247333675, -122.4088854209916], id: 6},
{location: [37.79275381746233, -122.4091876239904], id: 7},
{location: [37.795619489534374, -122.40989276432093], id: 8},
{location: [37.79792786675678, -122.41049717031848], id: 9},
{location: [37.80031576728801, -122.4109001076502], id: 10},
{location: [37.79920142331301, -122.41916032295062], id: 11}
];
var ids = initialPoints[initialPoints.length - 1].id;
var DemoGeodataCreator = React.createClass({
displayName: 'DemoGeodataCreator',
PropTypes: {
width: React.PropTypes.number.isRequired,
height: React.PropTypes.number.isRequired
},
getInitialState: function getInitialState() {
return {
longitude: -122.40677,
latitude: 37.78949,
zoom: 12.76901,
startDragLatLng: null,
isDragging: false,
points: Immutable.fromJS(initialPoints)
};
},
_onChangeViewport: function _onChangeViewport(opt) {
this.setState({
latitude: opt.latitude,
longitude: opt.longitude,
zoom: opt.zoom,
startDragLatLng: opt.startDragLatLng,
isDragging: opt.isDragging
});
},
_onAddPoint: function _onAddPoint(_location) {
var points = this.state.points.push(new Immutable.Map({
location: new Immutable.List(_location),
id: ++ids
}));
this.setState({points: points});
},
_onUpdatePoint: function _onUpdatePoint(opt) {
var index = this.state.points.findIndex(function filter(p) {
return p.get('id') === opt.key;
});
var point = this.state.points.get(index);
point = point.set('location', new Immutable.List(opt.location));
var points = this.state.points.set(index, point);
this.setState({points: points});
},
render: function render() {
return r.div([
r(MapGL, assign({
latitude: this.state.latitude,
longitude: this.state.longitude,
zoom: this.state.zoom,
width: this.props.width,
height: this.props.height,
startDragLatLng: this.state.startDragLatLng,
isDragging: this.state.isDragging,
style: {float: 'left'},
onChangeViewport: this.props.onChangeViewport || this._onChangeViewport
}, this.props), [
r(SVGOverlay, {redraw: function _redraw(opt) {
if (!this.state.points.size) {
return null;
}
var d = 'M' + this.state.points.map(function _map(point) {
var p = opt.project(point.get('location').toArray());
return [p.x, p.y];
}).join('L');
return r.path({
style: {stroke: '#1FBAD6', strokeWidth: 2, fill: 'none'},
d: d
});
}.bind(this)}),
r(DraggableOverlay, {
points: this.state.points,
onAddPoint: this._onAddPoint,
onUpdatePoint: this._onUpdatePoint,
renderPoint: function renderPoint(point, pixel) {
return r.g({}, [
r.circle({
r: 10,
style: {
fill: alphaify('#1FBAD6', 0.5),
pointerEvents: 'all'
}
}),
r.text({
style: {fill: 'white', textAnchor: 'middle'},
y: 5
}, point.get('id'))
]);
}
})
])
]);
}
});
module.exports = DemoGeodataCreator;

28
example/examples/index.js Normal file
View File

@ -0,0 +1,28 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
module.exports = {
Custom: require('./custom.react'),
Choropleth: require('./choropleth.react'),
GeodataCreator: require('./geodata-creator.react'),
Route: require('./route.react'),
Scatterplot: require('./scatterplot.react')
};

View File

@ -0,0 +1,129 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var assign = require('object-assign');
var React = require('react');
var d3 = require('d3');
var r = require('r-dom');
var alphaify = require('alphaify');
var window = require('global/window');
var windowAlert = window.alert;
var MapGL = require('../../src/index.js');
var SVGOverlay = require('../../src/overlays/svg.react');
var CanvasOverlay = require('../../src/overlays/canvas.react');
var ROUTES = require('./../data/routes-example.json');
var color = d3.scale.category10();
var RouteOverlayExample = React.createClass({
displayName: 'RouteOverlayExample',
propTypes: {
width: React.PropTypes.number.isRequired,
height: React.PropTypes.number.isRequired
},
getInitialState: function getInitialState() {
return {
latitude: 37.7736092599127,
longitude: -122.42312591099463,
zoom: 12.011557070552028,
startDragLatLng: null,
isDragging: false
};
},
_onChangeViewport: function _onChangeViewport(opt) {
this.setState({
latitude: opt.latitude,
longitude: opt.longitude,
zoom: opt.zoom,
startDragLatLng: opt.startDragLatLng,
isDragging: opt.isDragging
});
},
_renderRoute: function _renderRoute(points, index) {
return r.g({style: {pointerEvents: 'click', cursor: 'pointer'}}, [
r.g({
style: {pointerEvents: 'visibleStroke'},
onClick: function onClick() {
windowAlert('route ' + index);
}
}, [
r.path({
d: 'M' + points.join('L'),
style: {
fill: 'none',
stroke: alphaify(color(index), 0.7),
strokeWidth: 6
}
})
])
]);
},
_redrawSVGOverlay: function _redrawSVGOverlay(opt) {
var routes = ROUTES.map(function _map(route, index) {
var points = route.coordinates.map(opt.project).map(function __map(p) {
return [d3.round(p.x, 1), d3.round(p.y, 1)];
});
return r.g({key: index}, this._renderRoute(points, index));
}, this);
return r.g(routes);
},
_redrawCanvasOverlay: function _redrawCanvasOverlay(opt) {
var ctx = opt.ctx;
var width = opt.width;
var height = opt.height;
ctx.clearRect(0, 0, width, height);
ROUTES.map(function _map(route, index) {
route.coordinates.map(opt.project).forEach(function _forEach(p, i) {
var point = [d3.round(p.x, 1), d3.round(p.y, 1)];
ctx.fillStyle = d3.rgb(color(index)).brighter(1).toString();
ctx.beginPath();
ctx.arc(point[0], point[1], 2, 0, Math.PI * 2);
ctx.fill();
});
});
},
render: function render() {
return r(MapGL, assign({
latitude: this.state.latitude,
longitude: this.state.longitude,
zoom: this.state.zoom,
width: this.props.width,
height: this.props.height,
startDragLatLng: this.state.startDragLatLng,
isDragging: this.state.isDragging,
onChangeViewport: this.props.onChangeViewport || this._onChangeViewport
}, this.props), [
r(SVGOverlay, {redraw: this._redrawSVGOverlay}),
r(CanvasOverlay, {redraw: this._redrawCanvasOverlay})
]);
}
});
module.exports = RouteOverlayExample;

View File

@ -0,0 +1,94 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var assign = require('object-assign');
var React = require('react');
var r = require('r-dom');
var Immutable = require('immutable');
var d3 = require('d3');
var MapGL = require('../../src/index.js');
var ScatterplotOverlay = require('../../src/overlays/scatterplot.react');
// San Francisco
var location = require('./../data/cities.json')[0];
var normal = d3.random.normal();
function wiggle(scale) {
return normal() * scale;
}
// Example data.
var locations = Immutable.fromJS(d3.range(4000).map(function _map() {
return [location.latitude + wiggle(0.01), location.longitude + wiggle(0.01)];
}));
var ScatterplotOverlayExample = React.createClass({
displayName: 'ScatterplotOverlayExample',
PropTypes: {
width: React.PropTypes.number.isRequired,
height: React.PropTypes.number.isRequired
},
getInitialState: function getInitialState() {
return {
latitude: location.latitude,
longitude: location.longitude,
zoom: 11,
startDragLatLng: null,
isDragging: false
};
},
_onChangeViewport: function _onChangeViewport(opt) {
this.setState({
latitude: opt.latitude,
longitude: opt.longitude,
zoom: opt.zoom,
startDragLatLng: opt.startDragLatLng,
isDragging: opt.isDragging
});
},
render: function render() {
return r(MapGL, assign({
latitude: this.state.latitude,
longitude: this.state.longitude,
zoom: this.state.zoom,
isDragging: this.state.isDragging,
startDragLatLng: this.state.startDragLatLng,
width: this.props.width,
height: this.props.height,
onChangeViewport: this.props.onChangeViewport || this._onChangeViewport
}, this.props), [
r(ScatterplotOverlay, {
locations: locations,
dotRadius: 2,
globalOpacity: 1,
compositeOperation: 'screen'
})
]);
}
});
module.exports = ScatterplotOverlayExample;

80
example/main.js Normal file
View File

@ -0,0 +1,80 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var document = require('global/document');
var React = require('react');
var r = require('r-dom');
var window = require('global/window');
var ChoroplethExample = require('./examples/choropleth.react');
var CustomExample = require('./examples/custom.react');
var GeodataCreator = require('./examples/geodata-creator.react');
var ScatterplotExample = require('./examples/scatterplot.react');
var RouteExample = require('./examples/route.react');
function getAccessToken() {
var match = window.location.search.match(/access_token=([^&\/]*)/);
var accessToken = match && match[1];
if (accessToken) {
window.localStorage.accessToken = accessToken;
} else {
accessToken = window.localStorage.accessToken;
}
return accessToken;
}
var App = React.createClass({
displayName: 'App',
_onChangeViewport: function _onChangeViewport(opt) {
this.setState({
latitude: opt.latitude,
longitude: opt.longitude,
zoom: opt.zoom,
bbox: opt.bbox
});
},
getInitialState: function getInitialState() {
return {
map: {latitude: 37.7577, longitude: -122.4376, zoom: 8}
};
},
render: function render() {
var common = {
width: 400,
height: 400,
style: {float: 'left'},
mapboxApiAccessToken: getAccessToken()
};
return r.div([
r(RouteExample, common),
r(ScatterplotExample, common),
r(ChoroplethExample, common),
r(CustomExample, common),
r(GeodataCreator, common)
]);
}
});
React.render(r(App), document.body);

47
package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "react-map-gl",
"description": "A React wrapper for MapboxGL-js and overlay API.",
"version": "0.1.0",
"main": "src/index.js",
"keywords": [],
"repository": {
"type": "git",
"url": "https://github.com/uber/react-mapbox-gl.git"
},
"license": "MIT",
"dependencies": {
"alphaify": "^1.0.0",
"bowser": "^0.7.3",
"canvas-composite-types": "^1.0.0",
"d3": "^3.5.5",
"debounce": "^1.0.0",
"gl-matrix": "^2.3.1",
"global": "^4.3.0",
"keymirror": "^0.1.1",
"mapbox-gl": "~0.11.1",
"nop": "^1.0.0",
"object-assign": "^3.0.0",
"svg-transform": "0.0.1"
},
"devDependencies": {
"bistre": "^1.0.1",
"budo": "^4.2.1",
"husky": "^0.8.1",
"uber-standard": "^3.6.4"
},
"scripts": {
"start": "budo ./example/main.js --live -t | bistre",
"lint": "uber-standard",
"precommit": "npm run lint -s",
"prepush": "npm run lint -s"
},
"peerDependencies": {
"immutable": "^3.7.3",
"r-dom": "^1.3.0",
"react": "^0.13.0"
},
"engines": {
"node": "0.10.x",
"npm": "2.x"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 KiB

37
src/config.js Normal file
View File

@ -0,0 +1,37 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var browser = require('bowser').browser;
var prefix =
browser.webkit ? '-webkit-' :
browser.gecko ? '-moz-' :
'';
var config = {
DEFAULTS: {},
CURSOR: {
GRABBING: prefix + 'grabbing',
GRAB: prefix + 'grab'
}
};
module.exports = config;

22
src/index.js Normal file
View File

@ -0,0 +1,22 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
module.exports = require('./map.react');

View File

@ -0,0 +1,222 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var React = require('react');
var MapboxGL = require('mapbox-gl');
var Point = MapboxGL.Point;
var document = require('global/document');
var window = require('global/window');
var r = require('r-dom');
var noop = require('./noop');
var ua = typeof window.navigator !== 'undefined' ?
window.navigator.userAgent.toLowerCase() : '';
var firefox = ua.indexOf('firefox') !== -1;
function mousePos(el, event) {
var rect = el.getBoundingClientRect();
event = event.touches ? event.touches[0] : event;
return new Point(
event.clientX - rect.left - el.clientLeft,
event.clientY - rect.top - el.clientTop
);
}
/* eslint-disable max-len */
// Portions of the code below originally from:
// https://github.com/mapbox/mapbox-gl-js/blob/master/js/ui/handler/scroll_zoom.js
/* eslint-enable max-len */
var MapInteractions = React.createClass({
displayName: 'MapInteractions',
PropTypes: {
width: React.PropTypes.number.isRequired,
height: React.PropTypes.number.isRequired,
onMouseDown: React.PropTypes.func,
onMouseDrag: React.PropTypes.func,
onMouseUp: React.PropTypes.func,
onZoom: React.PropTypes.func,
onZoomEnd: React.PropTypes.func
},
getDefaultProps: function getDefaultProps() {
return {
onMouseDown: noop,
onMouseDrag: noop,
onMouseUp: noop,
onZoom: noop,
onZoomEnd: noop
};
},
getInitialState: function getInitialState() {
return {
startPos: null,
pos: null,
mouseWheelPos: null
};
},
_getMousePos: function _getMousePos(event) {
var el = this.refs.container.getDOMNode();
return mousePos(el, event);
},
_onMouseDown: function _onMouseDown(event) {
var pos = this._getMousePos(event);
this.setState({startPos: pos, pos: pos});
this.props.onMouseDown({pos: pos});
document.addEventListener('mousemove', this._onMouseDrag, false);
document.addEventListener('mouseup', this._onMouseUp, false);
},
_onMouseDrag: function _onMouseDrag(event) {
var pos = this._getMousePos(event);
this.setState({pos: pos});
this.props.onMouseDrag({pos: pos});
},
_onMouseUp: function _onMouseUp(event) {
document.removeEventListener('mousemove', this._onMouseDrag, false);
document.removeEventListener('mouseup', this._onMouseUp, false);
var pos = this._getMousePos(event);
this.setState({pos: pos});
this.props.onMouseUp({pos: pos});
},
_onMouseMove: function _onMouseMove(event) {
var pos = this._getMousePos(event);
this.props.onMouseMove({pos: pos});
},
/* eslint-disable complexity, max-statements */
_onWheel: function _onWheel(event) {
event.stopPropagation();
event.preventDefault();
var value = event.deltaY;
// Firefox doubles the values on retina screens...
if (firefox && event.deltaMode === window.WheelEvent.DOM_DELTA_PIXEL) {
value /= window.devicePixelRatio;
}
if (event.deltaMode === window.WheelEvent.DOM_DELTA_LINE) {
value *= 40;
}
var type = this.state.mouseWheelType;
var timeout = this.state.mouseWheelTimeout;
var lastValue = this.state.mouseWheelLastValue;
var time = this.state.mouseWheelTime;
var now = (window.performance || Date).now();
var timeDelta = now - (time || 0);
var pos = this._getMousePos(event);
time = now;
if (value !== 0 && value % 4.000244140625 === 0) {
// This one is definitely a mouse wheel event.
type = 'wheel';
// Normalize this value to match trackpad.
value = Math.floor(value / 4);
} else if (value !== 0 && Math.abs(value) < 4) {
// This one is definitely a trackpad event because it is so small.
type = 'trackpad';
} else if (timeDelta > 400) {
// This is likely a new scroll action.
type = null;
lastValue = value;
// Start a timeout in case this was a singular event, and delay it by up
// to 40ms.
timeout = window.setTimeout(function setTimeout() {
var _type = 'wheel';
this._zoom(-this.state.mouseWheelLastValue, this.state.mouseWheelPos);
this.setState({mouseWheelType: _type});
}.bind(this), 40);
} else if (!this._type) {
// This is a repeating event, but we don't know the type of event just
// yet.
// If the delta per time is small, we assume it's a fast trackpad;
// otherwise we switch into wheel mode.
type = Math.abs(timeDelta * value) < 200 ? 'trackpad' : 'wheel';
// Make sure our delayed event isn't fired again, because we accumulate
// the previous event (which was less than 40ms ago) into this event.
if (timeout) {
window.clearTimeout(timeout);
timeout = null;
value += lastValue;
}
}
// Slow down zoom if shift key is held for more precise zooming
if (event.shiftKey && value) {
value = value / 4;
}
// Only fire the callback if we actually know what type of scrolling device
// the user uses.
if (type) {
this._zoom(-value, pos);
}
this.setState({
mouseWheelTime: time,
mouseWheelPos: pos,
mouseWheelType: type,
mouseWheelTimeout: timeout,
mouseWheelLastValue: lastValue
});
},
/* eslint-enable complexity, max-statements */
_zoom: function _zoom(delta, pos) {
// Scale by sigmoid of scroll wheel delta.
var scale = 2 / (1 + Math.exp(-Math.abs(delta / 100)));
if (delta < 0 && scale !== 0) {
scale = 1 / scale;
}
this.props.onZoom({pos: pos, scale: scale});
window.clearTimeout(this._zoomEndTimeout);
this._zoomEndTimeout = window.setTimeout(function _setTimeout() {
this.props.onZoomEnd();
}.bind(this), 200);
},
render: function render() {
return r.div({
ref: 'container',
onMouseMove: this._onMouseMove,
onMouseDown: this._onMouseDown,
onWheel: this._onWheel,
style: {
width: this.props.width,
height: this.props.height,
position: 'relative'
}
}, this.props.children);
}
});
module.exports = MapInteractions;

603
src/map.react.js Normal file
View File

@ -0,0 +1,603 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var assert = require('assert');
var React = require('react');
var debounce = require('debounce');
var r = require('r-dom');
var d3 = require('d3');
var noop = require('./noop');
var assign = require('object-assign');
var Immutable = require('immutable');
var MapboxGL = require('mapbox-gl');
var LngLatBounds = MapboxGL.LngLatBounds;
var Point = MapboxGL.Point;
// NOTE: Transform is not a public API so we should be careful to always lock
// down mapbox-gl to a specific major, minor, and patch version.
var Transform = require('mapbox-gl/js/geo/transform');
var vec4 = require('gl-matrix').vec4;
var config = require('./config');
var MapInteractions = require('./map-interactions.react');
function mod(value, divisor) {
var modulus = value % divisor;
return modulus < 0 ? divisor + modulus : modulus;
}
function unproject(transform, point) {
return transform.pointLocation(MapboxGL.Point.convert(point));
}
function getBBoxFromTransform(transform, width, height) {
return [unproject(transform, [0, 0]), unproject(transform, [width, height])];
}
function cloneTransform(original) {
var transform = new Transform(original._minZoom, original._maxZoom);
transform.latRange = original.latRange;
transform.width = original.width;
transform.height = original.height;
transform.zoom = original.zoom;
transform.center = original.center;
transform.angle = original.angle;
transform.altitude = original.altitude;
transform.pitch = original.pitch;
return transform;
}
var MapboxGLMap = React.createClass({
displayName: 'MapGL',
shouldComponentUpdate: function shouldComponentUpdate(nextProps, nextState) {
var allTheSame = Object.keys(nextProps).reduce(function reduce(all, prop) {
var same = nextProps[prop] === this.props[prop];
return all && same;
}.bind(this), true);
if (!allTheSame) {
return true;
}
allTheSame = Object.keys(nextState).reduce(function reduce(all, prop) {
var same = nextState[prop] === this.state[prop];
return all && same;
}.bind(this), true);
return !allTheSame;
},
propTypes: {
/**
* The latitude of the center of the map.
*/
latitude: React.PropTypes.number.isRequired,
/**
* The longitude of the center of the map.
*/
longitude: React.PropTypes.number.isRequired,
/**
* The tile zoom level of the map.
*/
zoom: React.PropTypes.number.isRequired,
/**
* The mapbox style the component should use. Can either be a string url
* or a MapboxGL style Immutable.Map object.
*/
mapStyle: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.instanceOf(Immutable.Map)
]),
/**
* `onChangeViewport` callback is fired when the user interacted with the
* map. The object passed to the callback containers `latitude`,
* `longitude`, `zoom` and `bbox`. information.
*/
onChangeViewport: React.PropTypes.func,
/**
* The width of the map.
*/
width: React.PropTypes.number.isRequired,
/**
* The height of the map.
*/
height: React.PropTypes.number.isRequired,
/**
* Is the component currently being dragged. This is used to show/hide the
* drag cursor. Also used as an optimization in some overlays by preventing
* rendering while dragging.
*/
isDragging: React.PropTypes.bool,
/**
* Required to calculate the mouse projection after the first click event
* during dragging. Where the map is depends on where you first clicked on
* the map.
*/
startDragLatLng: React.PropTypes.array,
/**
* Called when a feature is hovered over. Features must set the
* `interactive` property to `true` for this to work properly. see the
* Mapbox example: https://www.mapbox.com/mapbox-gl-js/example/featuresat/
* The first argument of the callback will be the array of feature the
* mouse is over. This is the same response returned from `featuresAt`.
*/
onHoverFeatures: React.PropTypes.func,
/**
* Show attribution control or not.
*/
attributionControl: React.PropTypes.bool
},
getDefaultProps: function getDefaultProps() {
return {
mapStyle: 'mapbox://styles/mapbox/light-v8',
onChangeViewport: noop,
mapboxApiAccessToken: config.DEFAULTS.MAPBOX_API_ACCESS_TOKEN,
attributionControl: true
};
},
getInitialState: function getInitialState() {
var defaultState = {};
var stateChanges = this._updateStateFromProps(defaultState, this.props);
var state = assign({}, defaultState, stateChanges);
return state;
},
// New props are comin' round the corner!
componentWillReceiveProps: function componentWillReceiveProps(newProps) {
var stateChanges = this._updateStateFromProps(this.state, newProps);
this.setState(stateChanges);
},
// Use props to create an object of state changes.
_updateStateFromProps: function _updateStateFromProps(state, props) {
var stateChanges = {
latitude: props.latitude,
longitude: props.longitude,
zoom: props.zoom,
width: props.width,
height: props.height,
mapStyle: props.mapStyle,
startLatLng: props.startDragLatLng &&
new MapboxGL.LngLat(props.startDragLatLng[1], props.startDragLatLng[0])
};
assign(stateChanges, {
prevLatitude: state.latitude,
prevLongitude: state.longitude,
prevZoom: state.zoom,
prevWidth: state.width,
prevHeight: state.height,
prevMapStyle: state.mapStyle
});
MapboxGL.accessToken = props.mapboxApiAccessToken;
return stateChanges;
},
_onChangeViewport: function _onChangeViewport(_changes) {
var map = this._getMap();
var width = this.props.width;
var height = this.props.height;
var bbox = getBBoxFromTransform(map.transform, width, height);
var center = map.getCenter();
var startLatLng = this.state.startLatLng;
var changes = assign({
latitude: center.lat,
longitude: center.lng,
zoom: map.getZoom(),
bbox: bbox,
isDragging: this.props.isDragging,
startDragLatLng: startLatLng && [startLatLng.lat, startLatLng.lng]
}, _changes);
changes.longitude = mod(changes.longitude + 180, 360) - 180;
this.props.onChangeViewport(changes);
},
_getMap: function _getMap() {
return this._map;
},
componentDidMount: function componentDidMount() {
var mapStyle;
if (this.props.mapStyle instanceof Immutable.Map) {
mapStyle = this.props.mapStyle.toJS();
} else {
mapStyle = this.props.mapStyle;
}
var map = new MapboxGL.Map({
container: this.refs.mapboxMap.getDOMNode(),
center: [this.state.longitude, this.state.latitude],
zoom: this.state.zoom,
style: mapStyle,
interactive: false
// ,
// attributionControl: this.props.attributionControl
});
d3.select(map.getCanvas()).style('outline', 'none');
this._map = map;
this._updateMapViewport();
this._onChangeViewport();
},
_updateMapViewport: function _updateMapViewport() {
var state = this.state;
if (state.latitude !== state.prevLatitude ||
state.longitude !== state.prevLongitude ||
state.zoom !== state.prevZoom
) {
this._getMap().jumpTo({
center: [state.longitude, state.latitude],
zoom: state.zoom,
bearing: 0,
pitch: 0
});
}
if (state.width !== state.prevWidth || state.height !== state.prevHeight) {
this._resizeMap();
}
},
_resizeMap: debounce(function _resizeMap() {
var map = this._getMap();
map.resize();
}, 100),
_diffSources: function _diffSources(prevStyle, nextStyle) {
var prevSources = prevStyle.get('sources');
var nextSources = nextStyle.get('sources');
var enter = [];
var update = [];
var exit = [];
var prevIds = prevSources.keySeq().toArray();
var nextIds = nextSources.keySeq().toArray();
prevIds.forEach(function each(id) {
var nextSource = nextSources.get(id);
if (nextSource) {
if (!nextSource.equals(prevSources.get(id))) {
update.push({id: id, source: nextSources.get(id)});
}
} else {
exit.push({id: id, source: prevSources.get(id)});
}
});
nextIds.forEach(function each(id) {
var prevSource = prevSources.get(id);
if (!prevSource) {
enter.push({id: id, source: nextSources.get(id)});
}
});
return {enter: enter, update: update, exit: exit};
},
_diffLayers: function _diffLayers(prevStyle, nextStyle) {
var prevLayers = prevStyle.get('layers');
var nextLayers = nextStyle.get('layers');
var updates = [];
var exitingIds = [];
var prevMap = {};
var nextMap = {};
nextLayers.forEach(function map(layer, index) {
var id = layer.get('id');
nextMap[id] = {
layer: layer,
id: id,
// The `id` of the layer before this one.
before: index > 0 ? nextLayers.get(index - 1).get('id') : null,
enter: true
};
});
prevLayers.forEach(function map(layer, index) {
var id = layer.get('id');
prevMap[id] = {
layer: layer,
id: id,
before: index > 0 ? prevLayers.get(index - 1).get('id') : null
};
if (nextMap[id]) {
// Not a new layer.
nextMap[id].enter = false;
} else {
// This layer is being removed.
exitingIds.push(id);
}
});
nextLayers.reverse().forEach(function map(layer) {
var id = layer.get('id');
if (
!prevMap[id] ||
!prevMap[id].layer.equals(nextMap[id].layer) ||
prevMap[id].before !== nextMap[id].before
) {
// This layer is being changed.
updates.push(nextMap[id]);
}
});
return {updates: updates, exitingIds: exitingIds};
},
// Individually update the maps source and layers that have changed if all
// other style props haven't changed. This prevents flicking of the map when
// styles only change sources or layers.
_setDiffStyle: function _setDiffStyle(prevStyle, nextStyle) {
var map = this._getMap();
var prevKeysMap = prevStyle && styleKeysMap(prevStyle) || {};
var nextKeysMap = styleKeysMap(nextStyle);
function styleKeysMap(style) {
return style.map(function _map() {
return true;
}).delete('layers').delete('sources').toJS();
}
function propsOtherThanLayersOrSourcesDiffer() {
var prevKeysList = Object.keys(prevKeysMap);
var nextKeysList = Object.keys(nextKeysMap);
if (prevKeysList.length !== nextKeysList.length) {
return true;
}
// `nextStyle` and `prevStyle` should not have the same set of props.
if (nextKeysList.some(function forEach(key) {
// But the value of one of those props is different.
return prevStyle.get(key) !== nextStyle.get(key);
})) {
return true;
}
return false;
}
if (!prevStyle || propsOtherThanLayersOrSourcesDiffer()) {
map.setStyle(nextStyle.toJS());
return;
}
var sourcesDiff = this._diffSources(prevStyle, nextStyle);
var layersDiff = this._diffLayers(prevStyle, nextStyle);
map.batch(function batchStyleUpdates() {
sourcesDiff.enter.forEach(function each(enter) {
map.addSource(enter.id, enter.source.toJS());
});
sourcesDiff.update.forEach(function each(update) {
map.removeSource(update.id);
map.addSource(update.id, update.source.toJS());
});
sourcesDiff.exit.forEach(function each(exit) {
map.removeSource(exit.id);
});
layersDiff.exitingIds.forEach(function forEach(id) {
map.removeLayer(id);
});
layersDiff.updates.forEach(function forEach(update) {
if (!update.enter) {
// This is an old layer that needs to be updated. Remove the old layer
// with the same id and add it back again.
map.removeLayer(update.id);
}
map.addLayer(update.layer.toJS(), update.before);
});
});
},
_updateMapStyle: function _updateMapStyle() {
var mapStyle = this.state.mapStyle;
if (mapStyle !== this.state.prevMapStyle) {
if (mapStyle instanceof Immutable.Map) {
this._setDiffStyle(this.state.prevMapStyle, mapStyle);
} else {
this._getMap().setStyle(mapStyle);
}
}
},
componentDidUpdate: function componentDidUpdate() {
this._updateMapViewport();
this._updateMapStyle();
},
_onMouseDown: function _onMouseDown(opt) {
var map = this._getMap();
var startLatLng = unproject(map.transform, opt.pos);
this._onChangeViewport({
isDragging: true,
startDragLatLng: [startLatLng.lat, startLatLng.lng]
});
},
_onMouseDrag: function _onMouseDrag(opt) {
var p2 = opt.pos;
var map = this._getMap();
var width = this.props.width;
var height = this.props.height;
// take the start latlng and put it where the mouse is down.
var transform = cloneTransform(map.transform);
assert(this.state.startLatLng, '`startDragLatLng` prop is required for ' +
'mouse drag behavior.');
transform.setLocationAtPoint(this.state.startLatLng, p2);
var bbox = getBBoxFromTransform(transform, width, height);
this._onChangeViewport({
latitude: transform.center.lat,
longitude: transform.center.lng,
zoom: transform.zoom,
bbox: bbox,
isDragging: true
});
},
_onMouseMove: function _onMouseMove(opt) {
var map = this._getMap();
var pos = opt.pos;
if (!this.props.onHoverFeatures) {
return;
}
map.featuresAt([pos.x, pos.y], {}, function callback(error, features) {
if (error) {
throw error;
}
if (!features.length) {
return;
}
this.props.onHoverFeatures(features);
}.bind(this));
},
_onMouseUp: function _onMouseUp(opt) {
var map = this._getMap();
var width = this.props.width;
var height = this.props.height;
var transform = cloneTransform(map.transform);
this._onChangeViewport({
latitude: transform.center.lat,
longitude: transform.center.lng,
zoom: transform.zoom,
isDragging: false,
bbox: getBBoxFromTransform(transform, width, height)
});
},
_onZoom: function _onZoom(opt) {
var map = this._getMap();
var props = this.props;
var transform = cloneTransform(map.transform);
var around = unproject(transform, opt.pos);
transform.zoom = transform.scaleZoom(map.transform.scale * opt.scale);
transform.setLocationAtPoint(around, opt.pos);
this._onChangeViewport({
latitude: transform.center.lat,
longitude: transform.center.lng,
zoom: transform.zoom,
isDragging: true,
bbox: getBBoxFromTransform(transform, props.width, props.height)
});
},
_onZoomEnd: function _onZoomEnd() {
this._onChangeViewport({isDragging: false});
},
_renderOverlays: function _renderOverlays(transform) {
var children = [];
// Calculate the transformation matrix once for a given render cycle
// instead of for each point.
// from: mapbox-gl-js/js/geo/transform.js
var tileZoom = transform.tileZoom;
var coordinatePointMatrix = transform.coordinatePointMatrix(tileZoom);
function coordinatePoint(coord) {
var matrix = coordinatePointMatrix;
var p = vec4.transformMat4([], [coord.column, coord.row, 0, 1], matrix);
return new Point(p[0] / p[3], p[1] / p[3]);
}
function locationPoint(latlng) {
return coordinatePoint(transform.locationCoordinate(latlng));
}
function fastProject(latlng) {
return locationPoint(new MapboxGL.LngLat(latlng[1], latlng[0]));
}
React.Children.forEach(this.props.children, function _map(child) {
if (!child) {
return;
}
children.push(React.cloneElement(child, {
width: this.props.width,
height: this.props.height,
isDragging: this.props.isDragging,
project: fastProject,
unproject: unproject.bind(null, transform)
}));
}, this);
return r.div({
className: 'overlays',
style: {position: 'absolute', left: 0, top: 0}
}, children);
},
render: function render() {
var props = this.props;
var style = assign({}, props.style, {
width: props.width,
height: props.height,
cursor: this.props.isDragging ?
config.CURSOR.GRABBING : config.CURSOR.GRAB
});
var transform = new Transform();
transform.width = props.width;
transform.height = props.height;
transform.zoom = this.props.zoom;
transform.center.lat = this.props.latitude;
transform.center.lng = this.props.longitude;
return r.div({
style: assign({}, this.props.style, {
width: this.props.width,
height: this.props.height
})
}, [
r(MapInteractions, {
onMouseDown: this._onMouseDown,
onMouseDrag: this._onMouseDrag,
onMouseUp: this._onMouseUp,
onMouseMove: this._onMouseMove,
onZoom: this._onZoom,
onZoomEnd: this._onZoomEnd,
width: this.props.width,
height: this.props.height
}, [
r.div({ref: 'mapboxMap', style: style, className: props.className}),
this._renderOverlays(transform)
])
]
);
}
});
MapboxGLMap.fitBounds = function fitBounds(width, height, _bounds, options) {
var bounds = new LngLatBounds([_bounds[0].reverse(), _bounds[1].reverse()]);
options = options || {};
var padding = typeof options.padding === 'undefined' ? 0 : options.padding;
var offset = Point.convert([0, 0]);
var tr = new Transform();
tr.width = width;
tr.height = height;
var nw = tr.project(bounds.getNorthWest());
var se = tr.project(bounds.getSouthEast());
var size = se.sub(nw);
var scaleX = (tr.width - padding * 2 - Math.abs(offset.x) * 2) / size.x;
var scaleY = (tr.height - padding * 2 - Math.abs(offset.y) * 2) / size.y;
var center = tr.unproject(nw.add(se).div(2));
var zoom = tr.scaleZoom(tr.scale * Math.min(scaleX, scaleY));
return {
latitude: center.lat,
longitude: center.lng,
zoom: zoom
};
};
module.exports = MapboxGLMap;

22
src/noop.js Normal file
View File

@ -0,0 +1,22 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
module.exports = function noop() {};

View File

@ -0,0 +1,83 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var React = require('react');
var window = require('global/window');
var r = require('r-dom');
function devicePixelRatio() {
return window.devicePixelRatio || 1;
}
var CanvasOverlay = React.createClass({
displayName: 'CanvasOverlay',
propTypes: {
width: React.PropTypes.number,
height: React.PropTypes.number,
redraw: React.PropTypes.func.isRequired,
project: React.PropTypes.func,
isDragging: React.PropTypes.bool
},
componentDidMount: function componentDidMount() {
this._redraw();
},
componentDidUpdate: function componentDidUpdate() {
this._redraw();
},
_redraw: function _redraw() {
var pixelRatio = devicePixelRatio();
var canvas = this.getDOMNode();
var ctx = canvas.getContext('2d');
ctx.save();
ctx.scale(pixelRatio, pixelRatio);
this.props.redraw({
width: this.props.width,
height: this.props.height,
ctx: ctx,
project: this.props.project,
isDragging: this.props.isDragging
});
ctx.restore();
},
render: function render() {
var pixelRatio = devicePixelRatio();
return r.canvas({
ref: 'overlay',
width: this.props.width * pixelRatio,
height: this.props.height * pixelRatio,
style: {
width: this.props.width + 'px',
height: this.props.height + 'px',
position: 'absolute',
pointerEvents: 'none',
left: 0,
top: 0
}
});
}
});
module.exports = CanvasOverlay;

View File

@ -0,0 +1,141 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var React = require('react');
var window = require('global/window');
var d3 = require('d3');
var r = require('r-dom');
var Immutable = require('immutable');
var ChoroplethOverlay = React.createClass({
displayName: 'ChoroplethOverlay',
propTypes: {
width: React.PropTypes.number,
height: React.PropTypes.number,
project: React.PropTypes.func,
isDragging: React.PropTypes.bool,
renderWhileDragging: React.PropTypes.bool,
globalOpacity: React.PropTypes.number,
/**
* An Immutable List of feature objects.
*/
features: React.PropTypes.instanceOf(Immutable.List),
colorDomain: React.PropTypes.array,
colorRange: React.PropTypes.array,
valueAccessor: React.PropTypes.func
},
getDefaultProps: function getDefaultProps() {
return {
latLngAccessor: function latLngAccessor(location) {
return [location.get(0), location.get(1)];
},
renderWhileDragging: true,
globalOpacity: 1,
colorDomain: null,
colorRange: ['#FFFFFF', '#1FBAD6'],
valueAccessor: function valueAccessor(feature) {
return feature.get('properties').get('value');
}
};
},
componentDidMount: function componentDidMount() {
this._redraw();
},
componentDidUpdate: function componentDidUpdate() {
this._redraw();
},
_redraw: function _redraw() {
var pixelRatio = window.devicePixelRatio;
var canvas = this.getDOMNode();
var ctx = canvas.getContext('2d');
var project = this.props.project;
ctx.save();
ctx.scale(pixelRatio, pixelRatio);
ctx.clearRect(0, 0, this.props.width, this.props.height);
function projectPoint(lon, lat) {
var point = project([lat, lon]);
this.stream.point(point.x, point.y);
}
if (this.props.renderWhileDragging || !this.props.isDragging) {
var transform = d3.geo.transform({point: projectPoint});
var path = d3.geo.path().projection(transform).context(ctx);
this._drawFeatures(ctx, path);
}
ctx.restore();
},
_drawFeatures: function _drawFeatures(ctx, path) {
var colorDomain = this.props.colorDomain;
var features = this.props.features;
if (!features) {
return;
}
if (!colorDomain) {
colorDomain = d3.extent(features.toArray(), this.props.valueAccessor);
}
var colorScale = d3.scale.linear()
.domain(colorDomain)
.range(this.props.colorRange)
.clamp(true);
features.forEach(function _forEach(feature) {
ctx.beginPath();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
ctx.lineWidth = '1';
ctx.fillStyle = colorScale(this.props.valueAccessor(feature));
var geometry = feature.get('geometry');
path({
type: geometry.get('type'),
coordinates: geometry.get('coordinates').toJS()
});
ctx.fill();
ctx.stroke();
}, this);
},
render: function render() {
var pixelRatio = window.devicePixelRatio || 1;
return r.canvas({
ref: 'overlay',
width: this.props.width * pixelRatio,
height: this.props.height * pixelRatio,
style: {
width: this.props.width + 'px',
height: this.props.height + 'px',
position: 'absolute',
pointerEvents: 'none',
opacity: this.props.globalOpacity,
left: 0,
top: 0
}
});
}
});
module.exports = ChoroplethOverlay;

View File

@ -0,0 +1,135 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var assign = require('object-assign');
var React = require('react');
var Immutable = require('immutable');
var r = require('r-dom');
var transform = require('svg-transform');
var document = require('global/document');
var nop = require('nop');
var config = require('../config');
var mouse = require('../utils').relativeMousePosition;
var DraggablePointsOverlay = React.createClass({
displayName: 'DraggablePointsOverlay',
PropTypes: {
width: React.PropTypes.number.isRequired,
height: React.PropTypes.number.isRequired,
points: React.PropTypes.instanceOf(Immutable.List),
project: React.PropTypes.func.isRequired,
unproject: React.PropTypes.func.isRequired,
isDragging: React.PropTypes.bool,
keyAccessor: React.PropTypes.func,
locationAccessor: React.PropTypes.func,
onAddPoint: React.PropTypes.func,
onUpdatePoint: React.PropTypes.func,
renderPoint: React.PropTypes.func
},
getDefaultProps: function getDefaultProps() {
return {
keyAccessor: function keyAccessor(point) {
return point.get('id');
},
locationAccessor: function locationAccessor(point) {
return point.get('location').toArray();
},
onAddPoint: nop,
onUpdatePoint: nop,
renderPoint: nop,
isDragging: false
};
},
getInitialState: function _getInitialState() {
return {
draggedPointKey: null
};
},
_onDragStart: function _onDragStart(point, event) {
event.stopPropagation();
document.addEventListener('mousemove', this._onDrag, false);
document.addEventListener('mouseup', this._onDragEnd, false);
this.setState({draggedPointKey: this.props.keyAccessor(point)});
},
_onDrag: function _onDrag(event) {
event.stopPropagation();
var pixel = mouse(this.refs.container.getDOMNode(), event);
var latlng = this.props.unproject(pixel);
this.props.onUpdatePoint({
key: this.state.draggedPointKey,
location: [latlng.lat, latlng.lng]
});
},
_onDragEnd: function _onDragEnd(event) {
event.stopPropagation();
document.removeEventListener('mousemove', this._onDrag, false);
document.removeEventListener('mouseup', this._onDragEnd, false);
this.setState({draggedPoint: null});
},
_addPoint: function _addPoint(event) {
event.stopPropagation();
event.preventDefault();
var pixel = mouse(this.refs.container.getDOMNode(), event);
var location = this.props.unproject(pixel);
this.props.onAddPoint([location.lat, location.lng]);
},
render: function render() {
var points = this.props.points;
return r.svg({
ref: 'container',
width: this.props.width,
height: this.props.height,
style: assign({
pointerEvents: 'all',
position: 'absolute',
left: 0,
top: 0,
cursor: this.props.isDragging ?
config.CURSOR.GRABBING : config.CURSOR.GRAB
}, this.props.style),
onContextMenu: this._addPoint
}, [
r.g({style: {cursor: 'pointer'}}, points.map(function _map(point, index) {
var _pixel = this.props.project(this.props.locationAccessor(point));
var pixel = [_pixel.x, _pixel.y];
return r.g({
key: index,
transform: transform([{translate: pixel}]),
onMouseDown: this._onDragStart.bind(this, point),
style: {
pointerEvents: 'all'
}
}, this.props.renderPoint.call(this, point, pixel));
}, this))
]);
}
});
module.exports = DraggablePointsOverlay;

View File

@ -0,0 +1,56 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var React = require('react');
var r = require('r-dom');
var assign = require('object-assign');
var HTMLOverlay = React.createClass({
displayName: 'HTMLOverlay',
propTypes: {
width: React.PropTypes.number,
height: React.PropTypes.number,
redraw: React.PropTypes.func,
project: React.PropTypes.func,
isDragging: React.PropTypes.bool
},
render: function render() {
var style = assign({}, {
position: 'absolute',
pointerEvents: 'none',
width: this.props.width,
height: this.props.height,
left: 0,
top: 0
}, this.props.style);
return r.div({
ref: 'overlay',
style: style
}, this.props.redraw({
width: this.props.width,
height: this.props.height,
project: this.props.project,
isDragging: this.props.isDragging
}));
}
});
module.exports = HTMLOverlay;

View File

@ -0,0 +1,119 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var React = require('react');
var window = require('global/window');
var d3 = require('d3');
var r = require('r-dom');
var Immutable = require('immutable');
var COMPOSITE_TYPES = require('canvas-composite-types');
var ScatterplotOverlay = React.createClass({
displayName: 'ScatterplotOverlay',
propTypes: {
width: React.PropTypes.number,
height: React.PropTypes.number,
project: React.PropTypes.func,
isDragging: React.PropTypes.bool,
locations: React.PropTypes.instanceOf(Immutable.List),
latLngAccessor: React.PropTypes.func,
renderWhileDragging: React.PropTypes.bool,
globalOpacity: React.PropTypes.number,
dotRadius: React.PropTypes.number,
dotFill: React.PropTypes.string,
compositeOperation: React.PropTypes.oneOf(COMPOSITE_TYPES)
},
getDefaultProps: function getDefaultProps() {
return {
latLngAccessor: function latLngAccessor(location) {
return [location.get(0), location.get(1)];
},
renderWhileDragging: true,
dotRadius: 4,
dotFill: '#1FBAD6',
globalOpacity: 1,
// Same as browser default.
compositeOperation: 'source-over'
};
},
componentDidMount: function componentDidMount() {
this._redraw();
},
componentDidUpdate: function componentDidUpdate() {
this._redraw();
},
_redraw: function _redraw() {
var pixelRatio = window.devicePixelRatio;
var canvas = this.getDOMNode();
var ctx = canvas.getContext('2d');
var props = this.props;
var radius = this.props.dotRadius;
var fill = this.props.dotFill;
ctx.save();
ctx.scale(pixelRatio, pixelRatio);
ctx.clearRect(0, 0, props.width, props.height);
ctx.globalCompositeOperation = this.props.compositeOperation;
if ((this.props.renderWhileDragging || !this.props.isDragging) &&
this.props.locations
) {
this.props.locations.forEach(function _forEach(location) {
var latLng = this.props.latLngAccessor(location);
var pixel = this.props.project(latLng);
var pixelRounded = [d3.round(pixel.x, 1), d3.round(pixel.y, 1)];
if (pixelRounded[0] + radius >= 0 &&
pixelRounded[0] - radius < props.width &&
pixelRounded[1] + radius >= 0 &&
pixelRounded[1] - radius < props.height
) {
ctx.fillStyle = fill;
ctx.beginPath();
ctx.arc(pixelRounded[0], pixelRounded[1], radius, 0, Math.PI * 2);
ctx.fill();
}
}, this);
}
ctx.restore();
},
render: function render() {
var pixelRatio = window.devicePixelRatio;
return r.canvas({
ref: 'overlay',
width: this.props.width * pixelRatio,
height: this.props.height * pixelRatio,
style: {
width: this.props.width + 'px',
height: this.props.height + 'px',
position: 'absolute',
pointerEvents: 'none',
opacity: this.props.globalOpacity,
left: 0,
top: 0
}
});
}
});
module.exports = ScatterplotOverlay;

57
src/overlays/svg.react.js Normal file
View File

@ -0,0 +1,57 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
var React = require('react');
var r = require('r-dom');
var assign = require('object-assign');
var SVGOverlay = React.createClass({
displayName: 'SVGOverlay',
propTypes: {
width: React.PropTypes.number,
height: React.PropTypes.number,
redraw: React.PropTypes.func,
project: React.PropTypes.func,
isDragging: React.PropTypes.bool
},
render: function render() {
var style = assign({}, {
pointerEvents: 'none',
position: 'absolute',
left: 0,
top: 0
}, this.props.style);
return r.svg({
ref: 'overlay',
width: this.props.width,
height: this.props.height,
style: style
}, this.props.redraw({
width: this.props.width,
height: this.props.height,
project: this.props.project,
unproject: this.props.unproject,
isDragging: this.props.isDragging
}));
}
});
module.exports = SVGOverlay;

31
src/utils.js Normal file
View File

@ -0,0 +1,31 @@
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
'use strict';
function relativeMousePosition(container, event) {
var rect = container.getBoundingClientRect();
var x = event.clientX - rect.left - container.clientLeft;
var y = event.clientY - rect.top - container.clientTop;
return [x, y];
}
module.exports = {
relativeMousePosition: relativeMousePosition
};