mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 13:28:36 +03:00
Make events on map into a react component
This commit is contained in:
@@ -6,6 +6,7 @@ import * as selectors from '../selectors'
|
||||
|
||||
import MapLogic from '../js/map/map.js'
|
||||
import MapSites from './MapSites.jsx';
|
||||
import MapEvents from './MapEvents.jsx';
|
||||
import MapNarratives from './MapNarratives.jsx';
|
||||
|
||||
class Map extends React.Component {
|
||||
@@ -49,15 +50,15 @@ class Map extends React.Component {
|
||||
});
|
||||
|
||||
this.mapLogic = new MapLogic(this.state.map, this.svg, this.g, this.props.app, this.props.ui, this.props.methods)
|
||||
this.mapLogic.update(this.props.domain, this.props.app)
|
||||
this.mapLogic.update(this.props.app)
|
||||
|
||||
this.setState({ isInitialized: true })
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (hash(nextProps) !== hash(this.props)) {
|
||||
this.mapLogic.update(nextProps.domain, nextProps.app)
|
||||
if (hash(nextProps.app) !== hash(this.props.app)) {
|
||||
this.mapLogic.update(nextProps.app)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,10 +122,10 @@ class Map extends React.Component {
|
||||
}
|
||||
|
||||
updateSVG() {
|
||||
const boundingClient = d3.select(`#${this.props.mapId}`).node().getBoundingClientRect();
|
||||
//const boundingClient = d3.select(`#${this.props.mapId}`).node().getBoundingClientRect();
|
||||
|
||||
let WIDTH = boundingClient.width;
|
||||
let HEIGHT = boundingClient.height;
|
||||
//let WIDTH = boundingClient.width;
|
||||
//let HEIGHT = boundingClient.height;
|
||||
|
||||
// Offset with leaflet map transform boundaries
|
||||
const { transformX, transformY } = this.getSVGBoundaries();
|
||||
@@ -136,12 +137,7 @@ class Map extends React.Component {
|
||||
|
||||
/*this.svg.attr('width', WIDTH)
|
||||
.attr('height', HEIGHT)
|
||||
.attr('style', `left: ${-transformX}px; top: ${-transformY}px`);
|
||||
|
||||
this.g.selectAll('.location').attr('transform', (d) => {
|
||||
const newPoint = projectPoint([+d.latitude, +d.longitude]);
|
||||
return `translate(${newPoint.x},${newPoint.y})`;
|
||||
});*/
|
||||
.attr('style', `left: ${-transformX}px; top: ${-transformY}px`);*/
|
||||
}
|
||||
|
||||
renderSites() {
|
||||
@@ -178,12 +174,32 @@ class Map extends React.Component {
|
||||
return '';
|
||||
}
|
||||
|
||||
renderEvents() {
|
||||
if (this.state.isInitialized) {
|
||||
return (
|
||||
<MapEvents
|
||||
svg={this.svg}
|
||||
locations={this.props.domain.locations}
|
||||
categories={this.props.domain.categories}
|
||||
map={this.state.map}
|
||||
mapTransformX={this.state.mapTransformX}
|
||||
mapTransformY={this.state.mapTransformY}
|
||||
onSelect={this.props.methods.onSelect}
|
||||
onSelectNarrative={this.props.methods.onSelectNarrative}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper';
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div id={this.props.mapId} />
|
||||
{this.renderSites()}
|
||||
{this.renderEvents()}
|
||||
{this.renderNarratives()}
|
||||
</div>
|
||||
);
|
||||
|
||||
76
src/components/MapEvents.jsx
Normal file
76
src/components/MapEvents.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
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] = 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;
|
||||
}
|
||||
|
||||
renderCategory(counts, events) {
|
||||
return (
|
||||
<circle
|
||||
className="location-event-marker"
|
||||
r={(counts) ? Math.sqrt(16 * counts) + 3 : 0}
|
||||
style={{ fill: this.props.getCategoryColor(events), fillOpacity: 0.2 }}
|
||||
onClick={() => this.props.onSelect(events)}
|
||||
>
|
||||
</circle>
|
||||
);
|
||||
}
|
||||
|
||||
renderLocation(location) {
|
||||
const { x, y } = this.projectPoint([location.latitude, location.longitude]);
|
||||
const eventsCounts = this.getLocationEventsDistribution(location);
|
||||
|
||||
return (
|
||||
<g
|
||||
className="location"
|
||||
transform={`translate(${x}, ${y})`}
|
||||
>
|
||||
{eventsCounts.map(eventsCount => this.renderCategory(eventsCount, location.events))}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<Portal node={this.props.svg.node()}>
|
||||
{this.props.locations.map(loc => this.renderLocation(loc))}
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MapEvents;
|
||||
@@ -7,31 +7,14 @@ import 'leaflet-polylinedecorator';
|
||||
|
||||
export default function(lMap, svg, g, newApp, ui, methods) {
|
||||
|
||||
const domain = {
|
||||
locations: [],
|
||||
narratives: [],
|
||||
categories: [],
|
||||
}
|
||||
const app = {
|
||||
selected: [],
|
||||
highlighted: null,
|
||||
narrative: null,
|
||||
views: Object.assign({}, newApp.views),
|
||||
}
|
||||
|
||||
const getCategoryColor = methods.getCategoryColor;
|
||||
|
||||
// Icons for markPoint flags (a yellow ring around a location)
|
||||
const eventCircleMarkers = {};
|
||||
|
||||
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();
|
||||
if (mapNode === null) return { transformX: 0, transformY: 0 };
|
||||
@@ -62,25 +45,10 @@ export default function(lMap, svg, g, newApp, ui, methods) {
|
||||
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})`;
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
@@ -130,136 +98,22 @@ export default function(lMap, svg, g, newApp, ui, methods) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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');
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getMarker (d) {
|
||||
if (!d || app.narrative === null) return 'none';
|
||||
if (d.id === app.narrative.id) return 'url(#arrow)';
|
||||
return 'url(#arrow-off)';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
function update(newApp) {
|
||||
updateSVG();
|
||||
const isNewDomain = (hash(domain) !== hash(newDomain));
|
||||
const isNewAppProps = (hash(app) !== hash(newApp));
|
||||
|
||||
if (isNewDomain) {
|
||||
domain.locations = newDomain.locations;
|
||||
domain.categories = newDomain.categories;
|
||||
}
|
||||
|
||||
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 () {
|
||||
renderEvents();
|
||||
}
|
||||
function renderSelectedAndHighlight () {
|
||||
renderSelected();
|
||||
renderHighlighted();
|
||||
|
||||
Reference in New Issue
Block a user