Streamline aligning layers

This commit is contained in:
Franc Camps-Febrer
2018-12-19 14:09:28 +01:00
parent c416c12698
commit 3057bfd14d
6 changed files with 111 additions and 190 deletions

View File

@@ -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,

View File

@@ -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>
);
}

View File

@@ -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>
);

View File

@@ -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>
);

View File

@@ -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); })}

View File

@@ -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)