Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add slippy tiles #71

Merged
merged 6 commits into from
Aug 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;