mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-13 13:58:35 +03:00
Make narratives on map into a react component
This commit is contained in:
@@ -6,7 +6,7 @@ import * as selectors from '../selectors'
|
||||
|
||||
import MapLogic from '../js/map/map.js'
|
||||
import MapSites from './MapSites.jsx';
|
||||
import MapDefsMarkers from './MapDefsMarkers.jsx';
|
||||
import MapNarratives from './MapNarratives.jsx';
|
||||
|
||||
class Map extends React.Component {
|
||||
|
||||
@@ -97,19 +97,11 @@ class Map extends React.Component {
|
||||
|
||||
map.keyboard.disable();
|
||||
|
||||
map.on("move", () => this.moveElements());
|
||||
map.on("move", () => this.updateSVG());
|
||||
|
||||
this.setState({ map });
|
||||
}
|
||||
|
||||
projectPoint(location) {
|
||||
const latLng = new L.LatLng(location[0], location[1]);
|
||||
return {
|
||||
x: this.state.map.latLngToLayerPoint(latLng).x + this.state.mapTransformX,
|
||||
y: this.state.map.latLngToLayerPoint(latLng).y + this.state.mapTransformY
|
||||
};
|
||||
}
|
||||
|
||||
getSVGBoundaries() {
|
||||
const mapNode = d3.select('.leaflet-map-pane').node();
|
||||
if (mapNode === null) return { transformX: 0, transformY: 0 };
|
||||
@@ -152,10 +144,6 @@ class Map extends React.Component {
|
||||
});*/
|
||||
}
|
||||
|
||||
moveElements() {
|
||||
this.updateSVG();
|
||||
}
|
||||
|
||||
renderSites() {
|
||||
if (this.state.isInitialized) {
|
||||
return (
|
||||
@@ -171,12 +159,32 @@ class Map extends React.Component {
|
||||
return '';
|
||||
}
|
||||
|
||||
renderNarratives() {
|
||||
if (this.state.isInitialized) {
|
||||
return (
|
||||
<MapNarratives
|
||||
svg={this.svg}
|
||||
narratives={this.props.domain.narratives}
|
||||
map={this.state.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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
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.renderNarratives()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ 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>
|
||||
<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>
|
||||
<path d="M0,3v-3l6,3l-6,3z" style={{ fill: 'black', fillOpacity: 0.2 }}></path>
|
||||
</marker>
|
||||
</defs>
|
||||
);
|
||||
|
||||
94
src/components/MapNarratives.jsx
Normal file
94
src/components/MapNarratives.jsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { Portal } from 'react-portal';
|
||||
|
||||
import MapDefsMarkers from './MapDefsMarkers.jsx';
|
||||
|
||||
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 />);
|
||||
/*<MapDefsMarkers />*/
|
||||
|
||||
return (
|
||||
<Portal node={this.props.svg.node()}>
|
||||
{this.props.narratives.map(n => this.renderNarrative(n))}
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MapNarratives;
|
||||
@@ -11,7 +11,6 @@ export default function(lMap, svg, g, newApp, ui, methods) {
|
||||
locations: [],
|
||||
narratives: [],
|
||||
categories: [],
|
||||
sites: []
|
||||
}
|
||||
const app = {
|
||||
selected: [],
|
||||
@@ -21,22 +20,10 @@ export default function(lMap, svg, g, newApp, ui, methods) {
|
||||
}
|
||||
|
||||
const getCategoryColor = methods.getCategoryColor;
|
||||
const narrativeProps = ui.narratives;
|
||||
|
||||
// Initialize layer
|
||||
const sitesLayer = 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',
|
||||
};
|
||||
|
||||
function projectPoint(location) {
|
||||
const latLng = new L.LatLng(location[0], location[1]);
|
||||
return {
|
||||
@@ -80,9 +67,6 @@ export default function(lMap, svg, g, newApp, ui, methods) {
|
||||
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);
|
||||
@@ -227,33 +211,6 @@ export default function(lMap, svg, g, newApp, ui, methods) {
|
||||
.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 {
|
||||
@@ -262,18 +219,7 @@ export default function(lMap, svg, g, newApp, ui, methods) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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';
|
||||
@@ -281,100 +227,7 @@ export default function(lMap, svg, g, newApp, ui, methods) {
|
||||
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
|
||||
@@ -386,9 +239,7 @@ export default function(lMap, svg, g, newApp, ui, methods) {
|
||||
|
||||
if (isNewDomain) {
|
||||
domain.locations = newDomain.locations;
|
||||
domain.narratives = newDomain.narratives;
|
||||
domain.categories = newDomain.categories;
|
||||
domain.sites = newDomain.sites;
|
||||
}
|
||||
|
||||
if (isNewAppProps) {
|
||||
@@ -407,8 +258,6 @@ export default function(lMap, svg, g, newApp, ui, methods) {
|
||||
* Renders events on the map: takes data, and enters, updates and exits
|
||||
*/
|
||||
function renderDomain () {
|
||||
//renderSites();
|
||||
renderNarratives();
|
||||
renderEvents();
|
||||
}
|
||||
function renderSelectedAndHighlight () {
|
||||
|
||||
@@ -73,6 +73,12 @@
|
||||
left: 110px;
|
||||
}
|
||||
|
||||
.narratives-layer {
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 110px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Leaflet mapping controls
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user