mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-12 05:18:34 +03:00
Streamline aligning layers
This commit is contained in:
@@ -3,10 +3,8 @@ 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';
|
||||
@@ -26,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 = {}
|
||||
}
|
||||
@@ -91,16 +90,9 @@ class Dashboard extends React.Component {
|
||||
methods={{
|
||||
onSelect: this.handleSelect,
|
||||
onSelectNarrative: this.handleSelectNarrative,
|
||||
getCategoryColor: category => this.getCategoryColor(category)
|
||||
getCategoryColor: this.getCategoryColor,
|
||||
}}
|
||||
/>
|
||||
{/*<Viewport
|
||||
methods={{
|
||||
onSelect: this.handleSelect,
|
||||
onSelectNarrative: this.handleSelectNarrative,
|
||||
getCategoryColor: category => this.getCategoryColor(category)
|
||||
}}
|
||||
/>*/}
|
||||
<Timeline
|
||||
methods={{
|
||||
onSelect: this.handleSelect,
|
||||
|
||||
@@ -1,66 +1,42 @@
|
||||
import React from 'react';
|
||||
import hash from 'object-hash';
|
||||
import { Portal } from 'react-portal';
|
||||
|
||||
import { connect } from 'react-redux'
|
||||
import * as selectors from '../selectors'
|
||||
|
||||
import hash from 'object-hash';
|
||||
|
||||
import MapLogic from '../js/map/map.js'
|
||||
import MapSites from './MapSites.jsx';
|
||||
import MapEvents from './MapEvents.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 = {
|
||||
isInitialized: false,
|
||||
map: null,
|
||||
mapTransformX: 0,
|
||||
mapTransformY: 0
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
if (this.state.map === null) {
|
||||
if (this.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;
|
||||
|
||||
this.svg = pane.append('svg')
|
||||
.attr('class', 'leaflet-svg')
|
||||
.attr('width', width)
|
||||
.attr('height', height);
|
||||
|
||||
this.state.map.on('zoomstart', () => {
|
||||
this.svg.classed('hide', true);
|
||||
});
|
||||
this.state.map.on('zoomend', () => {
|
||||
this.svg.classed('hide', false);
|
||||
});
|
||||
|
||||
this.mapLogic = new MapLogic(this.state.map, this.svg, this.props.app, this.props.ui)
|
||||
this.mapLogic.update(this.props.app)
|
||||
|
||||
this.setState({ isInitialized: true })
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (hash(nextProps.app) !== hash(this.props.app)) {
|
||||
this.mapLogic.update(nextProps.app)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
initializeMap() {
|
||||
/**
|
||||
* Creates a Leaflet map and a tilelayer for the map background
|
||||
@@ -93,21 +69,26 @@ class Map extends React.Component {
|
||||
|
||||
map.keyboard.disable();
|
||||
|
||||
map.on("move", () => this.updateSVG());
|
||||
map.on("zoomend viewreset moveend", () => this.updateSVG());
|
||||
map.on("move", () => this.alignLayers());
|
||||
map.on("zoomend viewreset moveend", () => this.alignLayers());
|
||||
this.addResizeListener();
|
||||
|
||||
this.setState({ map });
|
||||
this.mapLogic = new MapLogic(map, this.svgRef.current, this.props.app, this.props.ui);
|
||||
this.mapLogic.update(this.props.app);
|
||||
|
||||
this.map = map;
|
||||
|
||||
this.setState({ isInitialized: true });
|
||||
}
|
||||
|
||||
addResizeListener() {
|
||||
window.addEventListener('resize', () => {
|
||||
this.updateSVG();
|
||||
this.alignLayers();
|
||||
});
|
||||
}
|
||||
|
||||
getSVGBoundaries() {
|
||||
const mapNode = d3.select('.leaflet-map-pane').node();
|
||||
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,
|
||||
@@ -116,94 +97,106 @@ class Map extends React.Component {
|
||||
.getComputedStyle(mapNode)
|
||||
.getPropertyValue('transform');
|
||||
|
||||
// However getComputedStyle returns an awkward string of the format
|
||||
// matrix(0, 0, 1, 0, 0.56523, 123123), hence this awkwardness
|
||||
// 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 {
|
||||
transformX: +transform.split(',')[4],
|
||||
transformY: +transform.split(',')[5].split(')')[0]
|
||||
width: boundingClient.width,
|
||||
height: boundingClient.height
|
||||
}
|
||||
}
|
||||
|
||||
updateSVG() {
|
||||
const boundingClient = d3.select(`#${this.props.mapId}`).node().getBoundingClientRect();
|
||||
renderSVG() {
|
||||
if (this.map === null) return '';
|
||||
const pane = this.map.getPanes().overlayPane;
|
||||
const { width, height } = this.getClientDims();
|
||||
|
||||
let WIDTH = boundingClient.width;
|
||||
let HEIGHT = boundingClient.height;
|
||||
|
||||
// Offset with leaflet map transform boundaries
|
||||
const { transformX, transformY } = this.getSVGBoundaries();
|
||||
|
||||
this.setState({
|
||||
mapTransformX: transformX,
|
||||
mapTransformY: transformY
|
||||
})
|
||||
|
||||
this.svg.attr('width', WIDTH)
|
||||
.attr('height', HEIGHT)
|
||||
.attr('style', `left: ${-transformX}px; top: ${-transformY}px`);
|
||||
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() {
|
||||
if (this.state.isInitialized) {
|
||||
return (
|
||||
<MapSites
|
||||
sites={this.props.domain.sites}
|
||||
map={this.state.map}
|
||||
mapTransformX={this.state.mapTransformX}
|
||||
mapTransformY={this.state.mapTransformY}
|
||||
isEnabled={this.props.app.views.sites}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return '';
|
||||
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() {
|
||||
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 '';
|
||||
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() {
|
||||
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 '';
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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.renderSites()}
|
||||
{this.renderEvents()}
|
||||
{this.renderNarratives()}
|
||||
{this.renderSVG()}
|
||||
{(this.state.isInitialized) ? this.renderMarkers() : ''}
|
||||
{(this.state.isInitialized) ? this.renderSites() : ''}
|
||||
{(this.state.isInitialized) ? this.renderEvents() : ''}
|
||||
{(this.state.isInitialized) ? this.renderNarratives() : ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,33 +16,22 @@ class MapEvents extends React.Component {
|
||||
const categories = this.props.categories;
|
||||
|
||||
categories.forEach(cat => {
|
||||
eventCount[cat.category] = 0
|
||||
eventCount[cat.category] = [];
|
||||
});
|
||||
|
||||
location.events.forEach((event) => {;
|
||||
eventCount[event.category] += 1;
|
||||
eventCount[event.category].push(event);
|
||||
});
|
||||
|
||||
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;
|
||||
return eventCount;
|
||||
}
|
||||
|
||||
renderCategory(counts, events) {
|
||||
renderCategory(events, category) {
|
||||
return (
|
||||
<circle
|
||||
className="location-event-marker"
|
||||
r={(counts) ? Math.sqrt(16 * counts) + 3 : 0}
|
||||
style={{ fill: 'yellow'/*this.props.getCategoryColor(events[0])*/, fillOpacity: 0.2 }}
|
||||
r={(events) ? Math.sqrt(16 * events.length) + 3 : 0}
|
||||
style={{ fill: this.props.getCategoryColor(category), fillOpacity: 0.8 }}
|
||||
onClick={() => this.props.onSelect(events)}
|
||||
>
|
||||
</circle>
|
||||
@@ -51,22 +40,23 @@ class MapEvents extends React.Component {
|
||||
|
||||
renderLocation(location) {
|
||||
const { x, y } = this.projectPoint([location.latitude, location.longitude]);
|
||||
const eventsCounts = this.getLocationEventsDistribution(location);
|
||||
const eventsByCategory = this.getLocationEventsDistribution(location);
|
||||
|
||||
return (
|
||||
<g
|
||||
className="location"
|
||||
transform={`translate(${x}, ${y})`}
|
||||
>
|
||||
{eventsCounts.map(eventsCount => this.renderCategory(eventsCount, location.events))}
|
||||
{Object.keys(eventsByCategory).map(cat => {
|
||||
return this.renderCategory(eventsByCategory[cat], cat)
|
||||
})}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<Portal node={this.props.svg.node()}>
|
||||
<Portal node={this.props.svg}>
|
||||
{this.props.locations.map(loc => this.renderLocation(loc))}
|
||||
</Portal>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Portal } from 'react-portal';
|
||||
|
||||
import MapDefsMarkers from './MapDefsMarkers.jsx';
|
||||
|
||||
class MapNarratives extends React.Component {
|
||||
|
||||
projectPoint(location) {
|
||||
@@ -81,10 +79,9 @@ class MapNarratives extends React.Component {
|
||||
|
||||
render() {
|
||||
if (this.props.narrative === null) return (<div />);
|
||||
/*<MapDefsMarkers />*/
|
||||
|
||||
return (
|
||||
<Portal node={this.props.svg.node()}>
|
||||
<Portal node={this.props.svg}>
|
||||
{this.props.narratives.map(n => this.renderNarrative(n))}
|
||||
</Portal>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ class MapSites extends React.Component {
|
||||
|
||||
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); })}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import * as selectors from '../selectors'
|
||||
import hash from 'object-hash';
|
||||
|
||||
import Map from './Map.jsx';
|
||||
import { areEqual } from '../js/utilities.js'
|
||||
|
||||
class Viewport extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
render() {
|
||||
const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper';
|
||||
return (
|
||||
<div className={classes}>
|
||||
<Map
|
||||
mapId="map"
|
||||
domain={this.props.domain}
|
||||
app={this.props.app}
|
||||
ui={this.props.ui}
|
||||
methods={this.props.methods}
|
||||
/>
|
||||
</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)
|
||||
Reference in New Issue
Block a user