Skip to content

Commit

Permalink
Merge pull request #71 from nachtm/mapbox
Browse files Browse the repository at this point in the history
Add slippy tiles
  • Loading branch information
sayas01 authored Aug 28, 2019
2 parents 88d2744 + 93169f8 commit 79ae909
Show file tree
Hide file tree
Showing 8 changed files with 3,161 additions and 2,427 deletions.
5,346 changes: 2,940 additions & 2,406 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@mapbox/geo-viewport": "^0.4.0",
"@material-ui/core": "^3.1.0",
"@material-ui/icons": "^3.0.1",
"@mdi/font": "^2.7.94",
"@mdi/js": "^2.7.94",
"@turf/bbox": "^6.0.1",
"@turf/bbox-polygon": "^6.0.1",
"@turf/helpers": "^6.1.4",
"@turf/square": "^5.1.5",
"@turf/transform-scale": "^5.1.5",
"classnames": "^2.2.6",
"mapbox-gl": "^1.2.1",
"material-ui-icons": "^1.0.0-beta.36",
"prop-types": "^15.6.2",
"react": "^16.5.1",
"react-dom": "^16.5.1",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-ga": "^2.5.3",
"react-hammerjs": "^1.0.1",
"react-router": "^4.3.1",
Expand Down
28 changes: 27 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import React, { Component } from 'react';
import { createPortal } from 'react-dom';

import Main from './components/Main';
import Header from './components/Header';
import Footer from './components/Footer';
import Map from './components/Map';

import MapContext from './helpers/MapContext';

import './App.css';
import * as ReactGA from 'react-ga';
import viewport from '@mapbox/geo-viewport';
import square from '@turf/square';

class App extends Component {
mapContainer = document.createElement('div');
state = {
maxMapBounds: [[0, 0], [0, 0]],
mapStyle: 'mapbox://styles/spatialdev/cjzn6045h1fwd1crrzvg29d88'
};

componentDidMount() {
// Initialize Google Analytics
ReactGA.initialize('UA-126802064-1');
Expand All @@ -17,10 +29,24 @@ class App extends Component {
//Note that we have the fontFamily div to make sure that the font is loaded when the DOM is rendered. This is important
// for rendering the ranking icon on the CityProfileCards
render() {
const context = {
container: this.mapContainer,
updateBounds: (newBounds) => this.setState({maxMapBounds: newBounds}),
updateStyle: (newStyle) => this.setState({mapStyle: newStyle}),
getViewport: () => {
const flatBounds = [...this.state.maxMapBounds[0], ...this.state.maxMapBounds[1]];
return viewport.viewport(square(flatBounds), [400, 400])
},
};
const {maxMapBounds, mapStyle} = this.state;
return (
<div style={{ position: 'relative', minHeight: '100vh' }}>
{/* Thanks to https://github.com/facebook/react/issues/13044#issuecomment-428815909 for the solution here!*/}
{createPortal(<Map maxBounds={maxMapBounds} style={mapStyle}/>, this.mapContainer)}
{window.location.pathname !== '/' ? <Header/> : null}
<Main/>
<MapContext.Provider value={context}>
<Main/>
</MapContext.Provider>
{window.location.pathname !== '/' ? <Footer/> : null}
</div>
);
Expand Down
76 changes: 68 additions & 8 deletions src/components/CityProfileCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,19 @@ import Hammer from 'react-hammerjs';

import CityStatsCard from '../components/CityStatsCard';
import MapLegend from '../components/MapLegend';
import Map from '../components/Map';
import Reparentable from './Reparentable';
import data from '../data/data';

import MapContext from '../helpers/MapContext';

import '../App.css';
import RankingIcon from './RankingIcon';
import {Menu, MenuItem} from "@material-ui/core";
import Button from "@material-ui/core/Button";
import IconButton from "@material-ui/core/IconButton";
import LayersIcon from "@material-ui/icons/LayersOutlined";
import EditIcon from "@material-ui/icons/Edit";

const styles = () => ({
root: {
Expand All @@ -43,13 +52,20 @@ const styles = () => ({
}
});

const mapboxMaps = {
nocar: 'mapbox://styles/spatialdev/cjzmwlydi16yb1cmlney5rj52',
ppl: 'mapbox://styles/spatialdev/cjzn2f2n11cic1cqdem3yxbvc',
wfh: 'mapbox://styles/spatialdev/cjzn6045h1fwd1crrzvg29d88'
};

class CityProfileCard extends Component {
state = {
cityData: null,
prevCityData: null,
nextCityData: null,
checked: false,
direction: null
direction: null,
mapOptionsAnchor: null
};

componentDidMount() {
Expand All @@ -59,6 +75,18 @@ class CityProfileCard extends Component {
this.initializeSlide();
}

componentDidUpdate(prevProps, prevState, snapshot) {
const {cityData: prevCityData} = prevState;
const {cityData: currCityData} = this.state;
// if we've changed data, change the bounds of the map
if ((prevCityData !== null && currCityData !== null && prevCityData.key !== currCityData.key)
|| (prevCityData === null && currCityData !== null))
{
const bbox = [[currCityData.swLong, currCityData.swLat], [currCityData.neLong, currCityData.neLat]];
this.context.updateBounds(bbox);
}
}

initializeSlide = () => {
const { direction } = this.state;

Expand Down Expand Up @@ -115,17 +143,25 @@ class CityProfileCard extends Component {
});
};

handleCloseMapOptionsMenu = () => {
this.setState({mapOptionsAnchor: null});
};

handleMapSelection = (url) => () => {
this.context.updateStyle(url);
this.setState({mapOptionsAnchor: null});
};

render() {
const { cityData, prevCityData, nextCityData, checked, direction } = this.state;
const { classes } = this.props;

const viewport = this.context.getViewport();
if (!cityData) {
return (
<div>
<CircularProgress/>
</div>);
}

return (
<Hammer key={cityData.key} onSwipe={this.handleSwipe}>
<div className="cityProfileCard">
Expand Down Expand Up @@ -156,11 +192,34 @@ class CityProfileCard extends Component {
<div style={{margin: '0 auto'}}>
<CardContent style={{ padding: 0 }}>
</CardContent>
<CardMedia
component="img"
className="media"
image={require('../' + cityData.mapImage)}
/>
{/*This div needs to capture the position:absolute elements inside. Thus, it has an empty
position:relative.*/}
<div style={{position: 'relative'}}>
<IconButton
onClick={(event) => this.setState({mapOptionsAnchor: event.currentTarget})}
style={{position: 'absolute', top: 4, left: 4, zIndex: 1000, backgroundColor: 'white'}}
>
<LayersIcon/>
</IconButton>
<Menu
anchorEl={this.state.mapOptionsAnchor}
keepMounted
open={Boolean(this.state.mapOptionsAnchor)}
onClose={this.handleCloseMapOptionsMenu}
>
<MenuItem onClick={this.handleMapSelection(mapboxMaps.wfh)}>Default</MenuItem>
<MenuItem onClick={this.handleMapSelection(mapboxMaps.nocar)}>Car Ownership</MenuItem>
<MenuItem onClick={this.handleMapSelection(mapboxMaps.ppl)}>Population</MenuItem>
</Menu>
<IconButton style={{position: 'absolute', top: 4, right: 4, zIndex: 1000, backgroundColor: 'white'}}>
<a href={`https://openstreetmap.org/edit#map=${viewport.zoom}/${viewport.center[1]}/${viewport.center[0]}`}
target="_blank"
>
<EditIcon/>
</a>
</IconButton>
<Reparentable el={this.context.container}/>
</div>
<MapLegend/>
</div>

Expand All @@ -186,6 +245,7 @@ class CityProfileCard extends Component {
}
}

CityProfileCard.contextType = MapContext;
CityProfileCard.propTypes = {
classes: PropTypes.object.isRequired,
};
Expand Down
78 changes: 78 additions & 0 deletions src/components/Map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { Component, createRef } from 'react';
import 'mapbox-gl/dist/mapbox-gl.css';
import mapboxgl from 'mapbox-gl';

import bbox from "@turf/bbox";
import transformScale from "@turf/transform-scale";
import bboxPolygon from "@turf/bbox-polygon";
import {multiPoint} from "@turf/helpers";
import square from "@turf/square";

import {withStyles} from '@material-ui/core';

const styles = () => ({
map: {
height: '400px',
width: '400px'
}
});

mapboxgl.accessToken = 'pk.eyJ1Ijoic3BhdGlhbGRldiIsImEiOiJjanpoYWFyZTkwaW4xM25vNWs2cWt6NWFqIn0.pjqihTlW7bHAp8bC8SaiNQ';
class Map extends Component {

state = {
mapboxMapRef: createRef(),
map: null
};

constructor(props) {
super(props);
}

getLooseBounds = (bounds, scale) => {
return bbox(transformScale(bboxPolygon(square(bbox(multiPoint(bounds)))), scale));
};

componentDidMount = () => {
const { mapboxMapRef } = this.state;
const { style } = this.props;
const mapBounds = [[-116.3656827, 43.50939634], [-116.0941571, 43.69918141]];
const options = {
container: mapboxMapRef.current,
style,
bounds: mapBounds,
maxBounds: this.getLooseBounds(mapBounds, 1.25),
};
const map = new mapboxgl.Map(options);
this.setState({map});
};

componentDidUpdate = (prevProps, prevState) => {
const {maxBounds: prevMaxBounds, style: prevStyle } = prevProps;
const {maxBounds: currMaxBounds, style: currStyle } = this.props;
const {map} = this.state;
if (JSON.stringify(prevMaxBounds) !== JSON.stringify(currMaxBounds)) {
// Since we're re-using a map and div container, we need to re-size between loads to ensure that it gets the
// size of its new container.
map.resize();

// Before we can fly to a new location, we need to allow the camera to move
map.setMaxBounds(null);
map.fitBounds(currMaxBounds, {animate: false, padding: 10});
// Once we've moved, we can set the new panning bounds
map.setMaxBounds(this.getLooseBounds(currMaxBounds, 2));
}
if (prevStyle !== currStyle)
{
map.setStyle(currStyle);
}
};

render = () => {
const { classes } = this.props;
const { mapboxMapRef } = this.state;
return <div ref={mapboxMapRef} className={classes.map}/>;
};
}

export default withStyles(styles)(Map);
26 changes: 16 additions & 10 deletions src/components/MapLegend.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import React from 'react';

const MapLegend = () => {
return (<div className='horizontalLegend'>
<div className='horizontalLegend-scale'>
<p>Map Errors</p>
<ul className='horizontalLegend-labels'>
<li><span style={{ background: '#ffffb2', opacity: 0.62 }}/>Low</li>
<li><span style={{ background: '#fd8d3c', opacity: 0.62 }}/>Medium</li>
<li><span style={{ background: '#bd0026', opacity: 0.62 }}/>High</li>
</ul>
</div>
</div>);
return <svg width="400" height="45">
<defs>
<linearGradient id="gradient" x1="0%" y1="100%" x2="100%" y2="100%" spreadMethod="pad">
<stop offset="0%" stopColor="#f7f0ed" stopOpacity="1"/>
<stop offset="50%" stopColor="#f2ac9b" stopOpacity="1"/>
<stop offset="100%" stopColor="#e3645b" stopOpacity="1"/>
</linearGradient>
</defs>
<rect width="200" height="15" transform="translate(0,0)" style={{fill: "url(\"#gradient\")"}}/>
<g className="y axis" transform="translate(0,30)">
<text y="-2" x="0">Low</text>
</g>
<g className="y axis" transform="translate(0,30)">
<text y="-2" x="174">High</text>
</g>
</svg>
};

export default MapLegend;
18 changes: 18 additions & 0 deletions src/components/Reparentable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React, { Component } from 'react';

class Reparentable extends Component {
ref = React.createRef();

componentDidMount() {
const { el } = this.props;
if (this.ref.current) {
this.ref.current.appendChild(el);
}
}

render() {
return <div ref={this.ref}/>;
}
}

export default Reparentable;
5 changes: 5 additions & 0 deletions src/helpers/MapContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {createContext} from 'react';

const MapContext = createContext(null);

export default MapContext;

0 comments on commit 79ae909

Please sign in to comment.