diff --git a/package.json b/package.json index be401e2..2d10611 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index a462491..34c4687 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -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} /> - this.getCategoryColor(category) + getCategoryColor: this.getCategoryColor, }} /> 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 ( + + + + + ); + } + + renderSites() { + return ( + + ); + } + + renderNarratives() { + return ( + + ); + } + + renderEvents() { + return ( + + ); + } + + renderSelected() { + return ( + + ); + } + + + renderMarkers() { + return ( + + + + ) + } + + + render() { + const classes = this.props.app.narrative ? 'map-wrapper narrative-mode' : 'map-wrapper'; + + return ( +
+
+ {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() : ''} +
+ ); + } +} + +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) + diff --git a/src/components/MapDefsMarkers.jsx b/src/components/MapDefsMarkers.jsx new file mode 100644 index 0000000..a6c4ed1 --- /dev/null +++ b/src/components/MapDefsMarkers.jsx @@ -0,0 +1,14 @@ +import React from 'react'; + +const MapDefsMarkers = ({}) => ( + + + + + + + + +); + +export default MapDefsMarkers; \ No newline at end of file diff --git a/src/components/MapEvents.jsx b/src/components/MapEvents.jsx new file mode 100644 index 0000000..479736b --- /dev/null +++ b/src/components/MapEvents.jsx @@ -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 ( + this.props.onSelect(events)} + > + + ); + } + + renderLocation(location) { + const { x, y } = this.projectPoint([location.latitude, location.longitude]); + const eventsByCategory = this.getLocationEventsDistribution(location); + + return ( + + {Object.keys(eventsByCategory).map(cat => { + return this.renderCategory(eventsByCategory[cat], cat) + })} + + ) + } + + render() { + return ( + + {this.props.locations.map(loc => this.renderLocation(loc))} + + ); + } +} + +export default MapEvents; \ No newline at end of file diff --git a/src/components/MapNarratives.jsx b/src/components/MapNarratives.jsx new file mode 100644 index 0000000..666df0e --- /dev/null +++ b/src/components/MapNarratives.jsx @@ -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 ( + 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) + }} + > + + ); + } + + renderNarrative(n) { + const steps = n.steps.slice(0, n.steps.length - 1); + + return ( + + {steps.map((s, idx) => this.renderNarrativeStep(n.steps, s, idx, n))} + + ) + } + + render() { + if (this.props.narrative === null) return (
); + + return ( + + {this.props.narratives.map(n => this.renderNarrative(n))} + + ); + } +} + +export default MapNarratives; \ No newline at end of file diff --git a/src/components/MapSelectedEvents.jsx b/src/components/MapSelectedEvents.jsx new file mode 100644 index 0000000..97f298f --- /dev/null +++ b/src/components/MapSelectedEvents.jsx @@ -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 ( + + + + + ); + } + + render() { + return ( + + {this.props.selected.map(s => this.renderMarker(s))} + + ) + } +} +export default MapSelectedEvents; \ No newline at end of file diff --git a/src/components/MapSites.jsx b/src/components/MapSites.jsx new file mode 100644 index 0000000..16d4a92 --- /dev/null +++ b/src/components/MapSites.jsx @@ -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 (
+ {site.site} +
+ ); + } + + render () { + if (!this.props.sites || !this.props.sites.length) return
; + + return ( +
+ {this.props.sites.map(site => { return this.renderSite(site); })} +
+ ) + } + +} + +export default MapSites; \ No newline at end of file diff --git a/src/components/Viewport.jsx b/src/components/Viewport.jsx deleted file mode 100644 index 49f772d..0000000 --- a/src/components/Viewport.jsx +++ /dev/null @@ -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 ( -
-
-
- ) - } -} - -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) diff --git a/src/js/map/map.js b/src/js/map/map.js deleted file mode 100644 index 05739ce..0000000 --- a/src/js/map/map.js +++ /dev/null @@ -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 - }; -} diff --git a/src/scss/map.scss b/src/scss/map.scss index 4a761f0..7067756 100644 --- a/src/scss/map.scss +++ b/src/scss/map.scss @@ -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; - } - } - } -} diff --git a/yarn.lock b/yarn.lock index 587220a..0f75a41 100644 --- a/yarn.lock +++ b/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"