mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 21:38:35 +03:00
Abstract L and SVG pane to React
This commit is contained in:
103
src/components/Map.jsx
Normal file
103
src/components/Map.jsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
import hash from 'object-hash';
|
||||
|
||||
import MapLogic from '../js/map/map.js'
|
||||
|
||||
import MapDefsMarkers from './MapDefsMarkers.jsx';
|
||||
|
||||
class Map extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
isInitialized: false,
|
||||
map: null
|
||||
}
|
||||
}
|
||||
|
||||
initializeMap() {
|
||||
/**
|
||||
* Creates a Leaflet map and a tilelayer for the map background
|
||||
* @param {string} id: DOM element to create map onto
|
||||
* @param {array} center: [lat, long] coordinates the map will be centered on
|
||||
* @param {number} zoom: zoom level
|
||||
*/
|
||||
const map = L.map(this.props.mapId)
|
||||
.setView(this.props.app.mapAnchor, 14)
|
||||
.setMinZoom(10)
|
||||
.setMaxZoom(18)
|
||||
.setMaxBounds([[180, -180], [-180, 180]])
|
||||
|
||||
let s
|
||||
if (process.env.MAPBOX_TOKEN && process.env.MAPBOX_TOKEN !== 'your_token') {
|
||||
s = L.tileLayer(
|
||||
`http://a.tiles.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}@2x.png?access_token=${process.env.MAPBOX_TOKEN}`
|
||||
);
|
||||
} else {
|
||||
// eslint-disable-next-line
|
||||
alert(`No mapbox token specified in config.
|
||||
Timemap does not currently support any other tiling layer,
|
||||
so you will need to sign up for one at:
|
||||
|
||||
https://www.mapbox.com/
|
||||
|
||||
Stop and start the development process in terminal after you have added your token to config.js`
|
||||
)
|
||||
return
|
||||
}
|
||||
s = s.addTo(map);
|
||||
|
||||
map.keyboard.disable();
|
||||
|
||||
this.setState({ map });
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
if (this.state.map === null) {
|
||||
this.initializeMap();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (!this.state.isInitialized) {
|
||||
const pane = d3.select(this.state.map.getPanes().overlayPane);
|
||||
const boundingClient = d3.select(`#${this.props.mapId}`).node().getBoundingClientRect();
|
||||
const width = boundingClient.width;
|
||||
const height = boundingClient.height;
|
||||
|
||||
let svg = pane.append('svg')
|
||||
.attr('class', 'leaflet-svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height);
|
||||
|
||||
let g = svg.append('g');
|
||||
|
||||
this.state.map.on('zoomstart', () => {
|
||||
svg.classed('hide', true);
|
||||
});
|
||||
this.state.map.on('zoomend', () => {
|
||||
svg.classed('hide', false);
|
||||
});
|
||||
|
||||
this.mapLogic = new MapLogic(this.state.map, svg, g, this.props.app, this.props.ui, this.props.methods)
|
||||
this.mapLogic.update(this.props.domain, this.props.app)
|
||||
|
||||
this.setState({ isInitialized: true })
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (hash(nextProps) !== hash(this.props)) {
|
||||
this.mapLogic.update(nextProps.domain, nextProps.app)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id={this.props.mapId} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Map;
|
||||
14
src/components/MapDefsMarkers.jsx
Normal file
14
src/components/MapDefsMarkers.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
const MapDefsMarkers = ({}) => (
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 6 6" refX="3" refY="3" markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M0,3v-3l6,3l-6,3z" style="fill: red;"></path>
|
||||
</marker>
|
||||
<marker id="arrow-off" viewBox="0 0 6 6" refX="3" refY="3" markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M0,3v-3l6,3l-6,3z" style="fill: black; fill-opacity: 0.2;"></path>
|
||||
</marker>
|
||||
</defs>
|
||||
);
|
||||
|
||||
export default MapDefsMarkers;
|
||||
@@ -3,7 +3,7 @@ import { connect } from 'react-redux'
|
||||
import * as selectors from '../selectors'
|
||||
import hash from 'object-hash';
|
||||
|
||||
import Map from '../js/map/map.js'
|
||||
import Map from './Map.jsx';
|
||||
import { areEqual } from '../js/utilities.js'
|
||||
|
||||
class Viewport extends React.Component {
|
||||
@@ -11,22 +11,17 @@ class Viewport extends React.Component {
|
||||
super(props)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.map = new Map(this.props.app, this.props.ui, this.props.methods)
|
||||
this.map.update(this.props.domain, this.props.app)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (hash(nextProps) !== hash(this.props)) {
|
||||
this.map.update(nextProps.domain, nextProps.app)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper';
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div id="map" />
|
||||
<Map
|
||||
mapId="map"
|
||||
domain={this.props.domain}
|
||||
app={this.props.app}
|
||||
ui={this.props.ui}
|
||||
methods={this.props.methods}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ import {
|
||||
import hash from 'object-hash';
|
||||
import 'leaflet-polylinedecorator';
|
||||
|
||||
export default function(newApp, ui, methods) {
|
||||
let svg, g, defs;
|
||||
export default function(lMap, svg, g, newApp, ui, methods) {
|
||||
|
||||
const domain = {
|
||||
locations: [],
|
||||
@@ -24,14 +23,8 @@ export default function(newApp, ui, methods) {
|
||||
const getCategoryColor = methods.getCategoryColor;
|
||||
const narrativeProps = ui.narratives;
|
||||
|
||||
// Map Settings
|
||||
const center = newApp.mapAnchor;
|
||||
const maxBoundaries = [[180, -180], [-180, 180]];
|
||||
const zoomLevel = 14;
|
||||
|
||||
// Initialize layer
|
||||
const sitesLayer = L.layerGroup();
|
||||
const pathLayer = L.layerGroup();
|
||||
|
||||
// Icons for markPoint flags (a yellow ring around a location)
|
||||
const eventCircleMarkers = {};
|
||||
@@ -44,94 +37,6 @@ export default function(newApp, ui, methods) {
|
||||
direction: 'top',
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates a Leaflet map and a tilelayer for the map background
|
||||
* @param {string} id: DOM element to create map onto
|
||||
* @param {array} center: [lat, long] coordinates the map will be centered on
|
||||
* @param {number} zoom: zoom level
|
||||
*/
|
||||
function initBackgroundMap(id, zoom) {
|
||||
/* http://bl.ocks.org/sumbera/10463358 */
|
||||
|
||||
const map = L.map(id)
|
||||
.setView(center, zoom)
|
||||
.setMinZoom(10)
|
||||
.setMaxZoom(19)
|
||||
.setMaxBounds(maxBoundaries)
|
||||
|
||||
// NB: configure tile endpoint
|
||||
let s
|
||||
if (process.env.MAPBOX_TOKEN && process.env.MAPBOX_TOKEN !== 'your_token') {
|
||||
s = L.tileLayer(
|
||||
`http://a.tiles.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}@2x.png?access_token=${process.env.MAPBOX_TOKEN}`
|
||||
);
|
||||
} else {
|
||||
// eslint-disable-next-line
|
||||
alert(`No mapbox token specified in config.
|
||||
Timemap does not currently support any other tiling layer,
|
||||
so you will need to sign up for one at:
|
||||
|
||||
https://www.mapbox.com/
|
||||
|
||||
Stop and start the development process in terminal after you have added your token to config.js`)
|
||||
return
|
||||
}
|
||||
s = s.addTo(map);
|
||||
|
||||
map.keyboard.disable();
|
||||
const pane = d3.select(map.getPanes().overlayPane);
|
||||
const boundingClient = d3.select(`#${id}`).node().getBoundingClientRect();
|
||||
const width = boundingClient.width;
|
||||
const height = boundingClient.height;
|
||||
|
||||
svg = pane.append('svg')
|
||||
.attr('class', 'leaflet-svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height);
|
||||
|
||||
g = svg.append('g');
|
||||
|
||||
svg.insert('defs', 'g')
|
||||
.append('marker')
|
||||
.attr('id', 'arrow')
|
||||
.attr('viewBox', '0 0 6 6')
|
||||
.attr('refX', 3)
|
||||
.attr('refY', 3)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.style('fill', 'red')
|
||||
.attr('d', 'M0,3v-3l6,3l-6,3z');
|
||||
|
||||
svg.insert('defs', 'g')
|
||||
.append('marker')
|
||||
.attr('id', 'arrow-off')
|
||||
.attr('viewBox', '0 0 6 6')
|
||||
.attr('refX', 3)
|
||||
.attr('refY', 3)
|
||||
.attr('markerWidth', 6)
|
||||
.attr('markerHeight', 6)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.style('fill', 'black')
|
||||
.style('fill-opacity', 0.2)
|
||||
.attr('d', 'M0,3v-3l6,3l-6,3z');
|
||||
|
||||
map.on('zoomstart', () => {
|
||||
svg.classed('hide', true);
|
||||
});
|
||||
map.on('zoomend', () => {
|
||||
svg.classed('hide', false);
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
// Initialize leaflet map and layers for each type of data
|
||||
const lMap = initBackgroundMap(ui.dom.map, zoomLevel);
|
||||
|
||||
function projectPoint(location) {
|
||||
const latLng = new L.LatLng(location[0], location[1]);
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user