mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 05:18:34 +03:00
Merge pull request #54 from forensic-architecture/topic/reactify-map
Break down map logic into React components and SVG markup
This commit is contained in:
@@ -16,11 +16,11 @@
|
||||
"es6-promise": "^4.1.1",
|
||||
"joi": "^14.0.1",
|
||||
"leaflet": "^1.0.3",
|
||||
"leaflet-polylinedecorator": "^1.3.2",
|
||||
"normalizr": "^3.2.3",
|
||||
"object-hash": "^1.3.0",
|
||||
"react": "^15.5.4",
|
||||
"react-dom": "^15.5.4",
|
||||
"react": "^16.6.3",
|
||||
"react-dom": "^16.6.3",
|
||||
"react-portal": "^4.2.0",
|
||||
"react-redux": "^5.0.4",
|
||||
"react-tabs": "^1.0.0",
|
||||
"redux": "^3.6.0",
|
||||
|
||||
@@ -3,10 +3,9 @@ import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import * as actions from '../actions';
|
||||
import * as selectors from '../selectors';
|
||||
|
||||
import LoadingOverlay from './presentational/LoadingOverlay';
|
||||
import Viewport from './Viewport.jsx';
|
||||
import Map from './Map.jsx';
|
||||
import Toolbar from './Toolbar.jsx';
|
||||
import CardStack from './CardStack.jsx';
|
||||
import NarrativeCard from './NarrativeCard.js';
|
||||
@@ -25,6 +24,7 @@ class Dashboard extends React.Component {
|
||||
this.handleSelectNarrative = this.handleSelectNarrative.bind(this);
|
||||
this.handleTagFilter = this.handleTagFilter.bind(this);
|
||||
this.updateTimerange = this.updateTimerange.bind(this);
|
||||
this.getCategoryColor = this.getCategoryColor.bind(this);
|
||||
|
||||
this.eventsById = {}
|
||||
}
|
||||
@@ -85,11 +85,12 @@ class Dashboard extends React.Component {
|
||||
onSelectNarrative={this.handleSelectNarrative}
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
<Viewport
|
||||
<Map
|
||||
mapId="map"
|
||||
methods={{
|
||||
onSelect: this.handleSelect,
|
||||
onSelectNarrative: this.handleSelectNarrative,
|
||||
getCategoryColor: category => this.getCategoryColor(category)
|
||||
getCategoryColor: this.getCategoryColor,
|
||||
}}
|
||||
/>
|
||||
<Timeline
|
||||
|
||||
240
src/components/Map.jsx
Normal file
240
src/components/Map.jsx
Normal file
@@ -0,0 +1,240 @@
|
||||
import React from 'react';
|
||||
import { Portal } from 'react-portal';
|
||||
|
||||
import { connect } from 'react-redux'
|
||||
import * as selectors from '../selectors'
|
||||
|
||||
import hash from 'object-hash';
|
||||
import 'leaflet';
|
||||
|
||||
import { isNotNullNorUndefined } from '../js/utilities';
|
||||
|
||||
import MapSites from './MapSites.jsx';
|
||||
import MapEvents from './MapEvents.jsx';
|
||||
import MapSelectedEvents from './MapSelectedEvents.jsx';
|
||||
import MapNarratives from './MapNarratives.jsx';
|
||||
import MapDefsMarkers from './MapDefsMarkers.jsx';
|
||||
|
||||
class Map extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.svgRef = React.createRef();
|
||||
this.map = null;
|
||||
this.state = {
|
||||
mapTransformX: 0,
|
||||
mapTransformY: 0
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
if (this.map === null) {
|
||||
this.initializeMap();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (hash(nextProps.app.selected) !== hash(this.props.app.selected)) {
|
||||
|
||||
// Fly to first of events selected
|
||||
const eventPoint = (nextProps.app.selected.length > 0) ? nextProps.app.selected[0] : null;
|
||||
|
||||
if (eventPoint.latitude && eventPoint.longitude) {
|
||||
this.map.flyTo([eventPoint.latitude, eventPoint.longitude]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initializeMap() {
|
||||
/**
|
||||
* Creates a Leaflet map and a tilelayer for the map background
|
||||
*/
|
||||
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();
|
||||
|
||||
map.on('move zoomend viewreset moveend', () => this.alignLayers());
|
||||
map.on('zoomstart', () => { if (this.svgRef.current !== null) this.svgRef.current.classList.add('hide') });
|
||||
map.on('zoomend', () => { if (this.svgRef.current !== null) this.svgRef.current.classList.remove('hide'); });
|
||||
window.addEventListener('resize', () => { this.alignLayers(); });
|
||||
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
alignLayers() {
|
||||
const mapNode = document.querySelector('.leaflet-map-pane');
|
||||
if (mapNode === null) return { transformX: 0, transformY: 0 };
|
||||
|
||||
// We'll get the transform of the leaflet container,
|
||||
// which will let us offset the SVG by the same quantity
|
||||
const transform = window
|
||||
.getComputedStyle(mapNode)
|
||||
.getPropertyValue('transform');
|
||||
|
||||
// Offset with leaflet map transform boundaries
|
||||
this.setState({
|
||||
mapTransformX: +transform.split(',')[4],
|
||||
mapTransformY: +transform.split(',')[5].split(')')[0]
|
||||
})
|
||||
}
|
||||
|
||||
getClientDims() {
|
||||
const boundingClient = document.querySelector(`#${this.props.mapId}`).getBoundingClientRect();
|
||||
|
||||
return {
|
||||
width: boundingClient.width,
|
||||
height: boundingClient.height
|
||||
}
|
||||
}
|
||||
|
||||
renderSVG() {
|
||||
if (this.map === null) return '';
|
||||
const pane = this.map.getPanes().overlayPane;
|
||||
const { width, height } = this.getClientDims();
|
||||
|
||||
return (
|
||||
<Portal node={pane}>
|
||||
<svg
|
||||
ref={this.svgRef}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{ transform: `translate3d(${-this.state.mapTransformX}px, ${-this.state.mapTransformY}px, 0)`}}
|
||||
className='leaflet-svg'
|
||||
>
|
||||
</svg>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
renderSites() {
|
||||
return (
|
||||
<MapSites
|
||||
sites={this.props.domain.sites}
|
||||
map={this.map}
|
||||
mapTransformX={this.state.mapTransformX}
|
||||
mapTransformY={this.state.mapTransformY}
|
||||
isEnabled={this.props.app.views.sites}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderNarratives() {
|
||||
return (
|
||||
<MapNarratives
|
||||
svg={this.svgRef.current}
|
||||
narratives={this.props.domain.narratives}
|
||||
map={this.map}
|
||||
mapTransformX={this.state.mapTransformX}
|
||||
mapTransformY={this.state.mapTransformY}
|
||||
narrative={this.props.app.narrative}
|
||||
narrativeProps={this.props.ui.narratives}
|
||||
onSelect={this.props.methods.onSelect}
|
||||
onSelectNarrative={this.props.methods.onSelectNarrative}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderEvents() {
|
||||
return (
|
||||
<MapEvents
|
||||
svg={this.svgRef.current}
|
||||
locations={this.props.domain.locations}
|
||||
categories={this.props.domain.categories}
|
||||
map={this.map}
|
||||
mapTransformX={this.state.mapTransformX}
|
||||
mapTransformY={this.state.mapTransformY}
|
||||
narrative={this.props.app.narrative}
|
||||
onSelect={this.props.methods.onSelect}
|
||||
onSelectNarrative={this.props.methods.onSelectNarrative}
|
||||
getCategoryColor={this.props.methods.getCategoryColor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderSelected() {
|
||||
return (
|
||||
<MapSelectedEvents
|
||||
svg={this.svgRef.current}
|
||||
selected={this.props.app.selected}
|
||||
map={this.map}
|
||||
mapTransformX={this.state.mapTransformX}
|
||||
mapTransformY={this.state.mapTransformY}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
renderMarkers() {
|
||||
return (
|
||||
<Portal node={this.svgRef.current}>
|
||||
<MapDefsMarkers />
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper';
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div id={this.props.mapId} />
|
||||
{this.renderSVG()}
|
||||
{(this.map !== null) ? this.renderMarkers() : ''}
|
||||
{(this.map !== null) ? this.renderSites() : ''}
|
||||
{(this.map !== null) ? this.renderEvents() : ''}
|
||||
{(this.map !== null) ? this.renderNarratives() : ''}
|
||||
{(this.map !== null) ? this.renderSelected() : ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
domain: {
|
||||
locations: selectors.selectLocations(state),
|
||||
narratives: selectors.selectNarratives(state),
|
||||
categories: selectors.selectCategories(state),
|
||||
sites: selectors.getSites(state)
|
||||
},
|
||||
app: {
|
||||
views: state.app.filters.views,
|
||||
selected: state.app.selected,
|
||||
highlighted: state.app.highlighted,
|
||||
mapAnchor: state.app.mapAnchor,
|
||||
narrative: state.app.narrative
|
||||
},
|
||||
ui: {
|
||||
dom: state.ui.dom,
|
||||
narratives: state.ui.style.narratives
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(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', fillOpacity: 0.2 }}></path>
|
||||
</marker>
|
||||
</defs>
|
||||
);
|
||||
|
||||
export default MapDefsMarkers;
|
||||
67
src/components/MapEvents.jsx
Normal file
67
src/components/MapEvents.jsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { Portal } from 'react-portal';
|
||||
|
||||
class MapEvents extends React.Component {
|
||||
|
||||
projectPoint(location) {
|
||||
const latLng = new L.LatLng(location[0], location[1]);
|
||||
return {
|
||||
x: this.props.map.latLngToLayerPoint(latLng).x + this.props.mapTransformX,
|
||||
y: this.props.map.latLngToLayerPoint(latLng).y + this.props.mapTransformY
|
||||
};
|
||||
}
|
||||
|
||||
getLocationEventsDistribution(location) {
|
||||
const eventCount = {};
|
||||
const categories = this.props.categories;
|
||||
|
||||
categories.forEach(cat => {
|
||||
eventCount[cat.category] = [];
|
||||
});
|
||||
|
||||
location.events.forEach((event) => {;
|
||||
eventCount[event.category].push(event);
|
||||
});
|
||||
|
||||
return eventCount;
|
||||
}
|
||||
|
||||
renderCategory(events, category) {
|
||||
return (
|
||||
<circle
|
||||
className="location-event-marker"
|
||||
r={(events) ? Math.sqrt(16 * events.length) + 3 : 0}
|
||||
style={{ fill: this.props.getCategoryColor(category), fillOpacity: 0.8 }}
|
||||
onClick={() => this.props.onSelect(events)}
|
||||
>
|
||||
</circle>
|
||||
);
|
||||
}
|
||||
|
||||
renderLocation(location) {
|
||||
const { x, y } = this.projectPoint([location.latitude, location.longitude]);
|
||||
const eventsByCategory = this.getLocationEventsDistribution(location);
|
||||
|
||||
return (
|
||||
<g
|
||||
className="location"
|
||||
transform={`translate(${x}, ${y})`}
|
||||
style={{ transition: 'transform 0.1s' }}
|
||||
>
|
||||
{Object.keys(eventsByCategory).map(cat => {
|
||||
return this.renderCategory(eventsByCategory[cat], cat)
|
||||
})}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Portal node={this.props.svg}>
|
||||
{this.props.locations.map(loc => this.renderLocation(loc))}
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MapEvents;
|
||||
91
src/components/MapNarratives.jsx
Normal file
91
src/components/MapNarratives.jsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import { Portal } from 'react-portal';
|
||||
|
||||
class MapNarratives extends React.Component {
|
||||
|
||||
projectPoint(location) {
|
||||
const latLng = new L.LatLng(location[0], location[1]);
|
||||
return {
|
||||
x: this.props.map.latLngToLayerPoint(latLng).x + this.props.mapTransformX,
|
||||
y: this.props.map.latLngToLayerPoint(latLng).y + this.props.mapTransformY
|
||||
};
|
||||
}
|
||||
|
||||
getNarrativeStyle(narrativeId) {
|
||||
const styleName = (narrativeId && narrativeId in this.props.narrativeProps)
|
||||
? narrativeId
|
||||
: 'default';
|
||||
return this.props.narrativeProps[styleName];
|
||||
}
|
||||
|
||||
getStrokeWidth(narrative, step) {
|
||||
if (!step) return 0;
|
||||
return this.getNarrativeStyle(narrative.id).strokeWidth;
|
||||
}
|
||||
|
||||
getStrokeDashArray(narrative, step) {
|
||||
if (!step) return 'none';
|
||||
return (this.getNarrativeStyle(narrative.id).style === 'dotted') ? "2px 5px" : 'none';
|
||||
}
|
||||
|
||||
getStroke(narrative, step) {
|
||||
if (!step || this.props.narrative === null) return 'none';
|
||||
return this.getNarrativeStyle(narrative.id).stroke;
|
||||
}
|
||||
|
||||
getStrokeOpacity(narrative, step) {
|
||||
if (this.props.narrative === null) return 0;
|
||||
if (!step || narrative.id !== this.props.narrative.id) return 0.2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
renderNarrativeStep(allSteps, step, idx, n) {
|
||||
const { x, y } = this.projectPoint([step.latitude, step.longitude]);
|
||||
|
||||
const step2 = allSteps[idx + 1];
|
||||
const p2 = this.projectPoint([step2.latitude, step2.longitude]);
|
||||
|
||||
return (
|
||||
<line
|
||||
className="narrative-step"
|
||||
x1={x}
|
||||
x2={p2.x}
|
||||
y1={y}
|
||||
y2={p2.y}
|
||||
markerStart="none"
|
||||
markerEnd="url(#arrow)"
|
||||
midMarker="url(#arrow)"
|
||||
onClick={() => this.props.onSelectNarrative(n)}
|
||||
style={{
|
||||
strokeWidth: this.getStrokeWidth(n, step),
|
||||
strokeDasharray: this.getStrokeDashArray(n, step),
|
||||
strokeOpacity: this.getStrokeOpacity(n, step),
|
||||
stroke: this.getStroke(n, step)
|
||||
}}
|
||||
>
|
||||
</line>
|
||||
);
|
||||
}
|
||||
|
||||
renderNarrative(n) {
|
||||
const steps = n.steps.slice(0, n.steps.length - 1);
|
||||
|
||||
return (
|
||||
<g id={`narrative-${n.id.replace(/ /g,"_")}`} className="narrative">
|
||||
{steps.map((s, idx) => this.renderNarrativeStep(n.steps, s, idx, n))}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.narrative === null) return (<div />);
|
||||
|
||||
return (
|
||||
<Portal node={this.props.svg}>
|
||||
{this.props.narratives.map(n => this.renderNarrative(n))}
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MapNarratives;
|
||||
46
src/components/MapSelectedEvents.jsx
Normal file
46
src/components/MapSelectedEvents.jsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { Portal } from 'react-portal';
|
||||
|
||||
class MapSelectedEvents extends React.Component {
|
||||
|
||||
projectPoint(location) {
|
||||
const latLng = new L.LatLng(location[0], location[1]);
|
||||
return {
|
||||
x: this.props.map.latLngToLayerPoint(latLng).x + this.props.mapTransformX,
|
||||
y: this.props.map.latLngToLayerPoint(latLng).y + this.props.mapTransformY
|
||||
};
|
||||
}
|
||||
|
||||
renderMarker (event) {
|
||||
const { x, y } = this.projectPoint([event.latitude, event.longitude]);
|
||||
console.log(x, y)
|
||||
return (
|
||||
<g
|
||||
className="location-marker"
|
||||
transform={`translate(${x - 32}, ${y})`}
|
||||
>
|
||||
<path
|
||||
className="leaflet-interactive"
|
||||
stroke="#ffffff"
|
||||
stroke-opacity="1"
|
||||
stroke-width="3"
|
||||
stroke-linecap=""
|
||||
stroke-linejoin="round"
|
||||
stroke-dasharray="5,2"
|
||||
fill="none"
|
||||
d="M0,0a32,32 0 1,0 64,0 a32,32 0 1,0 -64,0 "
|
||||
>
|
||||
</path>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Portal node={this.props.svg}>
|
||||
{this.props.selected.map(s => this.renderMarker(s))}
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
}
|
||||
export default MapSelectedEvents;
|
||||
36
src/components/MapSites.jsx
Normal file
36
src/components/MapSites.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
|
||||
class MapSites extends React.Component {
|
||||
|
||||
projectPoint(location) {
|
||||
const latLng = new L.LatLng(location[0], location[1]);
|
||||
return {
|
||||
x: this.props.map.latLngToLayerPoint(latLng).x + this.props.mapTransformX,
|
||||
y: this.props.map.latLngToLayerPoint(latLng).y + this.props.mapTransformY
|
||||
};
|
||||
}
|
||||
|
||||
renderSite(site) {
|
||||
const { x, y } = this.projectPoint([site.latitude, site.longitude]);
|
||||
|
||||
return (<div
|
||||
className="leaflet-tooltip site-label leaflet-zoom-animated leaflet-tooltip-top"
|
||||
style={{ opacity: 1, transform: `translate3d(calc(${x}px - 50%), ${y - 25}px, 0px)`}}>
|
||||
{site.site}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
if (!this.props.sites || !this.props.sites.length) return <div />;
|
||||
|
||||
return (
|
||||
<div className="sites-layer">
|
||||
{this.props.sites.map(site => { return this.renderSite(site); })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default MapSites;
|
||||
@@ -1,57 +0,0 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import * as selectors from '../selectors'
|
||||
import hash from 'object-hash';
|
||||
|
||||
import Map from '../js/map/map.js'
|
||||
import { areEqual } from '../js/utilities.js'
|
||||
|
||||
class Viewport extends React.Component {
|
||||
constructor(props) {
|
||||
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" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
domain: {
|
||||
locations: selectors.selectLocations(state),
|
||||
narratives: selectors.selectNarratives(state),
|
||||
categories: selectors.selectCategories(state),
|
||||
sites: selectors.getSites(state)
|
||||
},
|
||||
app: {
|
||||
views: state.app.filters.views,
|
||||
selected: state.app.selected,
|
||||
highlighted: state.app.highlighted,
|
||||
mapAnchor: state.app.mapAnchor,
|
||||
narrative: state.app.narrative
|
||||
},
|
||||
ui: {
|
||||
dom: state.ui.dom,
|
||||
narratives: state.ui.style.narratives
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Viewport)
|
||||
@@ -1,519 +0,0 @@
|
||||
import {
|
||||
areEqual,
|
||||
isNotNullNorUndefined
|
||||
} from '../utilities';
|
||||
import hash from 'object-hash';
|
||||
import 'leaflet-polylinedecorator';
|
||||
|
||||
export default function(newApp, ui, methods) {
|
||||
let svg, g, defs;
|
||||
|
||||
const domain = {
|
||||
locations: [],
|
||||
narratives: [],
|
||||
categories: [],
|
||||
sites: []
|
||||
}
|
||||
const app = {
|
||||
selected: [],
|
||||
highlighted: null,
|
||||
narrative: null,
|
||||
views: Object.assign({}, newApp.views),
|
||||
}
|
||||
|
||||
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 = {};
|
||||
|
||||
// Styles for elements in map
|
||||
const settingsSiteLabel = {
|
||||
className: 'site-label',
|
||||
opacity: 1,
|
||||
permanent: true,
|
||||
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 {
|
||||
x: lMap.latLngToLayerPoint(latLng).x + getSVGBoundaries().transformX,
|
||||
y: lMap.latLngToLayerPoint(latLng).y + getSVGBoundaries().transformY
|
||||
};
|
||||
}
|
||||
|
||||
function getSVGBoundaries() {
|
||||
const mapNode = d3.select('.leaflet-map-pane').node();
|
||||
|
||||
// We'll get the transform of the leaflet container,
|
||||
// which will let us offset the SVG by the same quantity
|
||||
const transform = window
|
||||
.getComputedStyle(mapNode)
|
||||
.getPropertyValue('transform');
|
||||
|
||||
// However getComputedStyle returns an awkward string of the format
|
||||
// matrix(0, 0, 1, 0, 0.56523, 123123), hence this awkwardness
|
||||
return {
|
||||
transformX: +transform.split(',')[4],
|
||||
transformY: +transform.split(',')[5].split(')')[0]
|
||||
}
|
||||
}
|
||||
|
||||
function updateSVG() {
|
||||
const boundingClient = d3.select(`#${ui.dom.map}`).node().getBoundingClientRect();
|
||||
|
||||
let WIDTH = boundingClient.width;
|
||||
let HEIGHT = boundingClient.height;
|
||||
|
||||
// Offset with leaflet map transform boundaries
|
||||
const { transformX, transformY } = getSVGBoundaries();
|
||||
|
||||
svg.attr('width', WIDTH)
|
||||
.attr('height', HEIGHT)
|
||||
.attr('style', `left: ${-transformX}px; top: ${-transformY}px`);
|
||||
|
||||
g.selectAll('.location').attr('transform', (d) => {
|
||||
const newPoint = projectPoint([+d.latitude, +d.longitude]);
|
||||
return `translate(${newPoint.x},${newPoint.y})`;
|
||||
});
|
||||
|
||||
svg.selectAll('.narrative')
|
||||
.each((g, i, nodes) => { return updateNarrativeSteps(g, i, nodes); });
|
||||
}
|
||||
|
||||
lMap.on("zoomend viewreset moveend", updateSVG);
|
||||
|
||||
/**
|
||||
* Returns latitud / longitude
|
||||
* @param {Object} eventPoint: data for an evenPoint - time, loc, tags, etc
|
||||
*/
|
||||
function getEventLocation(eventPoint) {
|
||||
return {
|
||||
latitude: +eventPoint.location.latitude,
|
||||
longitude: +eventPoint.location.longitude,
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* INTERACTIVE FUNCTIONS
|
||||
*/
|
||||
|
||||
/**
|
||||
* Removes the circular ring to mark a particular location
|
||||
*/
|
||||
function unmarkPoint() {
|
||||
Object.keys(eventCircleMarkers).forEach(markerId => {
|
||||
lMap.removeLayer(eventCircleMarkers[markerId]);
|
||||
delete eventCircleMarkers[markerId];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a circular ring mark in one particular location at a time
|
||||
* @param {object} location object, with lat and long
|
||||
*/
|
||||
function renderSelected() {
|
||||
unmarkPoint();
|
||||
app.selected.forEach(eventPoint => {
|
||||
if (isNotNullNorUndefined(eventPoint) && isNotNullNorUndefined(eventPoint.location)) {
|
||||
if (eventPoint.latitude && eventPoint.latitude !== "" && eventPoint.longitude && eventPoint.longitude !== "") {
|
||||
const location = new L.LatLng(eventPoint.latitude, eventPoint.longitude);
|
||||
eventCircleMarkers[eventPoint.id] = L.circleMarker(location, {
|
||||
radius: 32,
|
||||
fill: false,
|
||||
color: '#ffffff',
|
||||
weight: 3,
|
||||
lineCap: '',
|
||||
dashArray: '5,2'
|
||||
});
|
||||
eventCircleMarkers[eventPoint.id].addTo(lMap);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function renderHighlighted() {
|
||||
// Fly to first of events selected
|
||||
const eventPoint = (app.selected.length > 0) ? app.selected[0] : null;
|
||||
if (isNotNullNorUndefined(eventPoint) && isNotNullNorUndefined(eventPoint.location)) {
|
||||
if (eventPoint.latitude && eventPoint.longitude) {
|
||||
const location = new L.LatLng(eventPoint.latitude, eventPoint.longitude);
|
||||
lMap.flyTo(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* RENDERING FUNCTIONS
|
||||
*/
|
||||
|
||||
function getLocationEventsDistribution(location) {
|
||||
const eventCount = {};
|
||||
const categories = domain.categories;
|
||||
|
||||
categories.forEach(cat => {
|
||||
eventCount[cat.category] = 0
|
||||
});
|
||||
|
||||
location.events.forEach((event) => {;
|
||||
eventCount[event.category] += 1;
|
||||
});
|
||||
|
||||
let i = 0;
|
||||
const events = [];
|
||||
|
||||
while (i < categories.length) {
|
||||
let _eventsCount = eventCount[categories[i].category];
|
||||
for (let j = i + 1; j < categories.length; j++) {
|
||||
_eventsCount += eventCount[categories[j].category];
|
||||
}
|
||||
events.push(_eventsCount);
|
||||
i++;
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears existing event layer
|
||||
* Renders all events as markers
|
||||
* Adds eventlayer to map
|
||||
*/
|
||||
function renderEvents() {
|
||||
const locationsDom = g.selectAll('.location')
|
||||
.data(domain.locations, d => d.id)
|
||||
|
||||
locationsDom
|
||||
.exit()
|
||||
.remove();
|
||||
|
||||
locationsDom
|
||||
.enter().append('g')
|
||||
.attr('class', 'location')
|
||||
.attr('transform', (d) => {
|
||||
const newPoint = projectPoint([+d.latitude, +d.longitude]);
|
||||
return `translate(${newPoint.x},${newPoint.y})`;
|
||||
})
|
||||
.on('click', (location) => {
|
||||
methods.onSelect(location.events);
|
||||
});
|
||||
|
||||
const eventsDom = g.selectAll('.location')
|
||||
.selectAll('.location-event-marker')
|
||||
.data((d, i) => getLocationEventsDistribution(domain.locations[i]))
|
||||
|
||||
eventsDom
|
||||
.exit()
|
||||
.attr('r', 0)
|
||||
.remove();
|
||||
|
||||
eventsDom
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr('r', d => (d) ? Math.sqrt(16 * d) + 3 : 0);
|
||||
|
||||
eventsDom
|
||||
.enter().append('circle')
|
||||
.attr('class', 'location-event-marker')
|
||||
.style('fill', (d, i) => getCategoryColor(domain.categories[i].category))
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr('r', d => (d) ? Math.sqrt(16 * d) + 3 : 0);
|
||||
|
||||
eventsDom.selectAll('.location-event-marker')
|
||||
.style('fill-opacity', '0.1 !important');
|
||||
}
|
||||
|
||||
// NB: is this a function to be removed for future features?
|
||||
function renderSites() {
|
||||
sitesLayer.clearLayers();
|
||||
lMap.removeLayer(sitesLayer);
|
||||
|
||||
// Create a label for each attack site, persistent across filtering
|
||||
if (app.views.sites) {
|
||||
domain.sites.forEach((site) => {
|
||||
if (isNotNullNorUndefined(site)) {
|
||||
// Create an invisible marker for each site label
|
||||
const siteMarker = L.circleMarker([+site.latitude, +site.longitude], {
|
||||
radius: 0,
|
||||
stroke: 0
|
||||
});
|
||||
|
||||
siteMarker.bindTooltip(site.site, settingsSiteLabel).openTooltip();
|
||||
|
||||
// Add this one attack marker to group attack layer
|
||||
sitesLayer.addLayer(siteMarker);
|
||||
}
|
||||
});
|
||||
|
||||
lMap.addLayer(sitesLayer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const getCoords = (d) => {
|
||||
d.LatLng = new L.LatLng(+d.latitude, +d.longitude);
|
||||
return {
|
||||
x: lMap.latLngToLayerPoint(d.LatLng).x,
|
||||
y: lMap.latLngToLayerPoint(d.LatLng).y
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears existing narrative layer
|
||||
* Renders all narrativ as paths
|
||||
* Adds eventlayer to map
|
||||
*/
|
||||
|
||||
function getNarrativeStyle(narrativeId) {
|
||||
const styleName = narrativeId && narrativeId in narrativeProps
|
||||
? narrativeId
|
||||
: 'default';
|
||||
return narrativeProps[styleName];
|
||||
}
|
||||
|
||||
function getMarker (d) {
|
||||
if (!d || app.narrative === null) return 'none';
|
||||
if (d.id === app.narrative.id) return 'url(#arrow)';
|
||||
return 'url(#arrow-off)';
|
||||
}
|
||||
|
||||
function renderNarratives() {
|
||||
const narrativesDom = svg.selectAll('.narrative')
|
||||
.data((app.narrative !== null) ? domain.narratives : [])
|
||||
|
||||
narrativesDom
|
||||
.exit()
|
||||
.remove();
|
||||
|
||||
if (app.narrative !== null) {
|
||||
d3.selectAll('#arrow path')
|
||||
.style('fill', getNarrativeStyle(app.narrative.id).stroke);
|
||||
}
|
||||
|
||||
const narrativesEnter = narrativesDom
|
||||
.enter().append('g')
|
||||
.attr('id', d => 'narrative-' + d.id)
|
||||
.attr('class', 'narrative')
|
||||
|
||||
narrativesDom.selectAll('.narrative')
|
||||
.each((g, i, nodes) => { return updateNarrativeSteps(g, i, nodes); });
|
||||
}
|
||||
|
||||
function updateNarrativeSteps(g, i, nodes) {
|
||||
const n = d3.select(nodes[i]).data()[0];
|
||||
const allsteps = n.steps.slice();
|
||||
allsteps.push(n.steps[n.steps.length - 1]);
|
||||
|
||||
const steps = d3.select(nodes[i]).selectAll('.narrative-step')
|
||||
.data(n.steps)
|
||||
|
||||
steps.enter().append('line')
|
||||
.attr('class', 'narrative-step')
|
||||
.attr('x1', d => getCoords(d).x + getSVGBoundaries().transformX)
|
||||
.attr('x2', (d, j) => { return getCoords(allsteps[j + 1]).x + getSVGBoundaries().transformX; })
|
||||
.attr('y1', d => getCoords(d).y + getSVGBoundaries().transformY)
|
||||
.attr('y2', (d, j) => { return getCoords(allsteps[j + 1]).y + getSVGBoundaries().transformY; })
|
||||
.style('stroke-width', d => {
|
||||
if (!d) return 0;
|
||||
const styleProps = getNarrativeStyle(n.id);
|
||||
return styleProps.strokeWidth;
|
||||
})
|
||||
.style('stroke-dasharray', d => {
|
||||
if (!d) return 'none';
|
||||
const styleProps = getNarrativeStyle(n.id);
|
||||
return (styleProps.style === 'dotted') ? "2px 5px" : 'none';
|
||||
})
|
||||
.style('stroke', d => {
|
||||
if (!d || app.narrative === null) return 'none';
|
||||
const styleProps = getNarrativeStyle(n.id);
|
||||
return styleProps.stroke;
|
||||
})
|
||||
.style('stroke-opacity', d => {
|
||||
if (app.narrative === null) return 0;
|
||||
if (!d || d.id !== app.narrative.id) return 0.2;
|
||||
return 1;
|
||||
})
|
||||
.attr('marker-start', (d, j) => !j ? getMarker(n) : 'none')
|
||||
.attr('marker-end', getMarker(n))
|
||||
.attr('mid-marker', getMarker(n))
|
||||
.on('click', () => methods.onSelectNarrative(n) )
|
||||
|
||||
steps
|
||||
.attr('x1', d => getCoords(d).x + getSVGBoundaries().transformX)
|
||||
.attr('x2', (d, j) => { return getCoords(allsteps[j + 1]).x + getSVGBoundaries().transformX; })
|
||||
.attr('y1', d => getCoords(d).y + getSVGBoundaries().transformY)
|
||||
.attr('y2', (d, j) => { return getCoords(allsteps[j + 1]).y + getSVGBoundaries().transformY; })
|
||||
.style('stroke-width', d => {
|
||||
if (!d) return 0;
|
||||
const styleProps = getNarrativeStyle(n.id);
|
||||
return styleProps.strokeWidth;
|
||||
})
|
||||
.style('stroke-dasharray', d => {
|
||||
if (!d) return 'none';
|
||||
const styleProps = getNarrativeStyle(n.id);
|
||||
return (styleProps.style === 'dotted') ? "2px 5px" : 'none';
|
||||
})
|
||||
.style('stroke', d => {
|
||||
if (!d || app.narrative === null) return 'none';
|
||||
const styleProps = getNarrativeStyle(n.id);
|
||||
return styleProps.stroke;
|
||||
})
|
||||
.style('stroke-opacity', d => {
|
||||
if (app.narrative === null) return 0;
|
||||
if (!d || n.id !== app.narrative.id) return 0.2;
|
||||
return 1;
|
||||
})
|
||||
.attr('marker-start', (d, j) => !j ? getMarker(n) : 'none')
|
||||
.attr('marker-end', getMarker(n))
|
||||
.attr('mid-marker', getMarker(n))
|
||||
|
||||
steps
|
||||
.exit()
|
||||
.remove();
|
||||
}
|
||||
/**
|
||||
* Updates displayable data on the map: events, coevents and paths
|
||||
* @param {Object} domain: object of arrays of events, coevs, attacks, paths, sites
|
||||
*/
|
||||
function update(newDomain, newApp) {
|
||||
updateSVG();
|
||||
const isNewDomain = (hash(domain) !== hash(newDomain));
|
||||
const isNewAppProps = (hash(app) !== hash(newApp));
|
||||
|
||||
if (isNewDomain) {
|
||||
domain.locations = newDomain.locations;
|
||||
domain.narratives = newDomain.narratives;
|
||||
domain.categories = newDomain.categories;
|
||||
domain.sites = newDomain.sites;
|
||||
}
|
||||
|
||||
if (isNewAppProps) {
|
||||
app.views = newApp.views;
|
||||
app.selected = newApp.selected;
|
||||
app.highlighted = newApp.highlighted;
|
||||
app.mapAnchor = newApp.mapAnchor;
|
||||
app.narrative = newApp.narrative;
|
||||
}
|
||||
|
||||
if (isNewDomain || isNewAppProps) renderDomain();
|
||||
if (isNewAppProps) renderSelectedAndHighlight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders events on the map: takes data, and enters, updates and exits
|
||||
*/
|
||||
function renderDomain () {
|
||||
renderSites();
|
||||
renderNarratives();
|
||||
renderEvents();
|
||||
}
|
||||
function renderSelectedAndHighlight () {
|
||||
renderSelected();
|
||||
renderHighlighted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose only relevant functions
|
||||
*/
|
||||
return {
|
||||
update
|
||||
};
|
||||
}
|
||||
@@ -53,17 +53,32 @@
|
||||
.site-label {
|
||||
background: rgba($black,0.6);
|
||||
color: #fff;
|
||||
padding: 2px 7px;
|
||||
padding: 5px;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
border: rgba($black,0.6);
|
||||
letter-spacing: 0.05em;
|
||||
transition: transform 0.1s;
|
||||
|
||||
&::before {
|
||||
border-top-color: rgba($black,0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.sites-layer {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 110px;
|
||||
}
|
||||
|
||||
&.narrative-mode {
|
||||
.sites-layer {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -164,69 +179,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.coevent-marker {
|
||||
fill-opacity: 0.1;
|
||||
stroke-dasharray: 8px 4px;
|
||||
stroke-width: 2px;
|
||||
opacity: 1;
|
||||
}
|
||||
.coevent-path {
|
||||
stroke-dasharray: 8px 4px;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.district-boundaries {
|
||||
fill: $red;
|
||||
fill-opacity: 0.3;
|
||||
stroke-width: 2;
|
||||
stroke: $red;
|
||||
}
|
||||
|
||||
.path-polyline {
|
||||
stroke: $darkgrey;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.route-polyline {
|
||||
transition: 0.2s ease;
|
||||
stroke: $darkgrey;
|
||||
|
||||
&:hover {
|
||||
transition: 0.2s ease;
|
||||
stroke: $black;
|
||||
}
|
||||
}
|
||||
|
||||
.district-popup {
|
||||
button {
|
||||
height: 80px;
|
||||
line-height: 80px;
|
||||
width: 200px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
background-size: 100%;
|
||||
color: $offwhite;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
font-family: 'Lato', Helvetica, sans-serif;
|
||||
text-transform: uppercase;
|
||||
|
||||
p {
|
||||
font-size: $normal;
|
||||
margin: 0;
|
||||
transition: 0.2s ease;
|
||||
letter-spacing: 0.1em;
|
||||
&:first-child {
|
||||
font-size: $xsmall;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
p:last-child {
|
||||
transition: 0.2s ease;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
126
yarn.lock
126
yarn.lock
@@ -980,10 +980,6 @@ arrify@^1.0.0, arrify@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
|
||||
integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
|
||||
|
||||
asap@~2.0.3:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
|
||||
|
||||
asn1.js@^4.0.0:
|
||||
version "4.10.1"
|
||||
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
|
||||
@@ -1821,10 +1817,6 @@ copy-descriptor@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
||||
|
||||
core-js@^1.0.0:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
|
||||
|
||||
core-js@^2.0.0, core-js@^2.4.0, core-js@^2.5.0:
|
||||
version "2.5.7"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
|
||||
@@ -1868,14 +1860,6 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
|
||||
safe-buffer "^5.0.1"
|
||||
sha.js "^2.4.8"
|
||||
|
||||
create-react-class@^15.6.0:
|
||||
version "15.6.3"
|
||||
resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036"
|
||||
dependencies:
|
||||
fbjs "^0.8.9"
|
||||
loose-envify "^1.3.1"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
cross-spawn@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
|
||||
@@ -2571,12 +2555,6 @@ encodeurl@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
|
||||
encoding@^0.1.11:
|
||||
version "0.1.12"
|
||||
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
|
||||
dependencies:
|
||||
iconv-lite "~0.4.13"
|
||||
|
||||
end-of-stream@^1.0.0, end-of-stream@^1.1.0:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
|
||||
@@ -2868,18 +2846,6 @@ faye-websocket@~0.11.0:
|
||||
dependencies:
|
||||
websocket-driver ">=0.5.1"
|
||||
|
||||
fbjs@^0.8.9:
|
||||
version "0.8.17"
|
||||
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
|
||||
dependencies:
|
||||
core-js "^1.0.0"
|
||||
isomorphic-fetch "^2.1.1"
|
||||
loose-envify "^1.0.0"
|
||||
object-assign "^4.1.0"
|
||||
promise "^7.1.1"
|
||||
setimmediate "^1.0.5"
|
||||
ua-parser-js "^0.7.18"
|
||||
|
||||
figures@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
|
||||
@@ -3409,7 +3375,7 @@ https-browserify@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
|
||||
|
||||
iconv-lite@0.4, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
|
||||
iconv-lite@0.4, iconv-lite@^0.4.4:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
dependencies:
|
||||
@@ -3779,7 +3745,7 @@ is-retry-allowed@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
|
||||
integrity sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=
|
||||
|
||||
is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0:
|
||||
is-stream@^1.0.0, is-stream@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
|
||||
@@ -3839,13 +3805,6 @@ isobject@^3.0.0, isobject@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
|
||||
|
||||
isomorphic-fetch@^2.1.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
|
||||
dependencies:
|
||||
node-fetch "^1.0.1"
|
||||
whatwg-fetch ">=0.10.0"
|
||||
|
||||
isstream@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||
@@ -3874,6 +3833,7 @@ js-string-escape@^1.0.1:
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
js-tokens@^3.0.2:
|
||||
version "3.0.2"
|
||||
@@ -3980,16 +3940,6 @@ lcid@^2.0.0:
|
||||
dependencies:
|
||||
invert-kv "^2.0.0"
|
||||
|
||||
leaflet-polylinedecorator@^1.3.2:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/leaflet-polylinedecorator/-/leaflet-polylinedecorator-1.6.0.tgz#9ef79fd1b5302d67b72efe959a8ecd2553f27266"
|
||||
dependencies:
|
||||
leaflet-rotatedmarker "^0.2.0"
|
||||
|
||||
leaflet-rotatedmarker@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/leaflet-rotatedmarker/-/leaflet-rotatedmarker-0.2.0.tgz#4467f49f98d1bfd56959bd9c6705203dd2601277"
|
||||
|
||||
leaflet@^1.0.3:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.3.4.tgz#7f006ea5832603b53d7269ef5c595fd773060a40"
|
||||
@@ -4126,6 +4076,7 @@ loglevel@^1.4.1:
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
dependencies:
|
||||
js-tokens "^3.0.0 || ^4.0.0"
|
||||
|
||||
@@ -4511,13 +4462,6 @@ no-case@^2.2.0:
|
||||
dependencies:
|
||||
lower-case "^1.1.1"
|
||||
|
||||
node-fetch@^1.0.1:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
|
||||
dependencies:
|
||||
encoding "^0.1.11"
|
||||
is-stream "^1.0.1"
|
||||
|
||||
node-forge@0.7.5:
|
||||
version "0.7.5"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df"
|
||||
@@ -4687,6 +4631,7 @@ oauth-sign@~0.9.0:
|
||||
object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
||||
|
||||
object-copy@^0.1.0:
|
||||
version "0.1.0"
|
||||
@@ -5147,13 +5092,7 @@ promise-inflight@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
||||
|
||||
promise@^7.1.1:
|
||||
version "7.3.1"
|
||||
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
|
||||
dependencies:
|
||||
asap "~2.0.3"
|
||||
|
||||
prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.6.0:
|
||||
prop-types@^15.5.0, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2:
|
||||
version "15.6.2"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
|
||||
dependencies:
|
||||
@@ -5273,14 +5212,22 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
|
||||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
react-dom@^15.5.4:
|
||||
version "15.6.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730"
|
||||
react-dom@^16.6.3:
|
||||
version "16.6.3"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.6.3.tgz#8fa7ba6883c85211b8da2d0efeffc9d3825cccc0"
|
||||
integrity sha512-8ugJWRCWLGXy+7PmNh8WJz3g1TaTUt1XyoIcFN+x0Zbkoz+KKdUyx1AQLYJdbFXjuF41Nmjn5+j//rxvhFjgSQ==
|
||||
dependencies:
|
||||
fbjs "^0.8.9"
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.0"
|
||||
prop-types "^15.5.10"
|
||||
object-assign "^4.1.1"
|
||||
prop-types "^15.6.2"
|
||||
scheduler "^0.11.2"
|
||||
|
||||
react-portal@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.2.0.tgz#5400831cdb0ae64dccb8128121cf076089ab1afd"
|
||||
integrity sha512-Zf+vGQ/VEAb5XAy+muKEn48yhdCNYPZaB1BWg1xc8sAZWD8pXTgPtQT4ihBdmWzsfCq8p8/kqf0GWydSBqc+Eg==
|
||||
dependencies:
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-redux@^5.0.4:
|
||||
version "5.0.7"
|
||||
@@ -5300,15 +5247,15 @@ react-tabs@^1.0.0:
|
||||
classnames "^2.2.0"
|
||||
prop-types "^15.5.0"
|
||||
|
||||
react@^15.5.4:
|
||||
version "15.6.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72"
|
||||
react@^16.6.3:
|
||||
version "16.6.3"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.6.3.tgz#25d77c91911d6bbdd23db41e70fb094cc1e0871c"
|
||||
integrity sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw==
|
||||
dependencies:
|
||||
create-react-class "^15.6.0"
|
||||
fbjs "^0.8.9"
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.0"
|
||||
prop-types "^15.5.10"
|
||||
object-assign "^4.1.1"
|
||||
prop-types "^15.6.2"
|
||||
scheduler "^0.11.2"
|
||||
|
||||
read-pkg-up@^1.0.1:
|
||||
version "1.0.1"
|
||||
@@ -5664,6 +5611,7 @@ safe-regex@^1.1.0:
|
||||
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
sass-graph@^2.2.4:
|
||||
version "2.2.4"
|
||||
@@ -5689,6 +5637,14 @@ sax@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
|
||||
scheduler@^0.11.2:
|
||||
version "0.11.3"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.11.3.tgz#b5769b90cf8b1464f3f3cfcafe8e3cd7555a2d6b"
|
||||
integrity sha512-i9X9VRRVZDd3xZw10NY5Z2cVMbdYg6gqFecfj79USv1CFN+YrJ3gIPRKf1qlY+Sxly4djoKdfx1T+m9dnRB8kQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
schema-utils@^0.4.4, schema-utils@^0.4.5:
|
||||
version "0.4.7"
|
||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187"
|
||||
@@ -5806,7 +5762,7 @@ set-value@^2.0.0:
|
||||
is-plain-object "^2.0.3"
|
||||
split-string "^3.0.1"
|
||||
|
||||
setimmediate@^1.0.4, setimmediate@^1.0.5:
|
||||
setimmediate@^1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
|
||||
|
||||
@@ -6365,10 +6321,6 @@ typedarray@^0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
|
||||
ua-parser-js@^0.7.18:
|
||||
version "0.7.18"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
|
||||
|
||||
uglify-es@^3.3.4:
|
||||
version "3.3.9"
|
||||
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
|
||||
@@ -6767,10 +6719,6 @@ well-known-symbols@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/well-known-symbols/-/well-known-symbols-1.0.0.tgz#73c78ae81a7726a8fa598e2880801c8b16225518"
|
||||
integrity sha1-c8eK6Bp3Jqj6WY4ogIAcixYiVRg=
|
||||
|
||||
whatwg-fetch@>=0.10.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
|
||||
|
||||
whatwg-fetch@^2.0.3:
|
||||
version "2.0.4"
|
||||
resolved "http://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
|
||||
|
||||
Reference in New Issue
Block a user