import React, {Component} from "react";
import {connect} from "react-redux";
import {Map, View} from "ol";
import {Tile as TileLayer, Vector as VectorLayer} from "ol/layer";
import OSM from 'ol/source/OSM';
import GeoJSON from "ol/format/GeoJSON";
import {Vector as VectorSource} from "ol/source";
import axios from "axios";
import PropTypes from "prop-types";
import Snackbar from '@material-ui/core/Snackbar';
import {Stroke, Style, Fill, Circle as CircleStyle} from "ol/style";
import "./MapComponent.css";
import * as actions from "../../store/actions";
/**
* The map props
* @typedef MapComponentProps
* @type {props}
* @property {string} APIKey key obtained from geOps that enables you to used the previous API services.
* @property {string} routingUrl The API routing url to be used for navigation.
* @property {string} currentMot The current selected mot by user, example 'bus'.
* @property {Object} currentStopsGeoJSON The current stops defined by user in geojson format inside a dictionary, key is the stop index(order) and the value is the geoJSON itself.
* @property {function} onShowNotification A store action that can be dispatched, takes the notification message and type as arguments.
* @property {function} onSetClickLocation A store action that can be dispatched, takes the clicked location on map array of [long,lat] and stores it in the store.
* @category Props
*/
/**
* The only true map that shows inside the application.
* @category Map
*/
class MapComponent extends Component {
/**
* Default constructor, gets called automatically upon initialization.
* @param {...MapComponentProps} props Props received so that the component can function properly.
* @category Map
*/
constructor(props) {
super(props);
this.FindRouteCancelToken = axios.CancelToken;
this.findRouteCancel = null;
this.routeStyleInner = new Style({
stroke: new Stroke({
color: "orange",
width: 3
})
});
this.routeStyleOuter = new Style({
stroke: new Stroke({
color: "black",
width: 5
})
});
this.pointStyle = new Style({
image: new CircleStyle({
radius: 7,
fill: new Fill({color: "orange"}),
stroke: new Stroke({color: 'black', width: 2})
})
});
this.hoveredFeature = null;
this.state = {
hoveredStationOpen: false,
hoveredStationName: ""
};
}
/**
* Create Openlayers map (source, view, layer, etc...).
* Add event listener onClick to handle location selection from map.
* @category Map
*/
componentDidMount() {
const demoAttribution = `${process.env.REACT_APP_NAME} v-${process.env.REACT_APP_VERSION}`;
const openStreetMap = new TileLayer({
source: new OSM()
});
this.map = new Map({
target: "map",
layers: [openStreetMap],
view: new View({
projection: "EPSG:4326",
center: [10, 50],
zoom: 6
})
});
this.map.on("singleclick", evt => {
const {onSetClickLocation} = this.props;
onSetClickLocation(evt.coordinate);
});
this.map.on('pointermove', evt => {
if (this.hoveredFeature) {
this.hoveredFeature = null;
this.setState({hoveredStationOpen: false, hoveredStationName: ""});
}
this.map.forEachFeatureAtPixel(evt.pixel, feature => {
if (feature.getGeometry().getType() === "Point") {
this.hoveredFeature = feature;
this.setState({
hoveredStationOpen: true,
hoveredStationName: feature.get("name") + " - " + feature.get("country_code")
});
}
return true;
});
});
}
/**
* Perform the necessary actions when receiving updated props.
* If new stops are received, then remove any existing stops/routes and draw those stops/routes.
* @category Map
*/
componentDidUpdate(prevProps) {
const {currentStopsGeoJSON, currentMot} = this.props;
const currentMotChanged = (currentMot && currentMot !== prevProps.currentMot);
const currentStopsGeoJSONChanged = (currentStopsGeoJSON && currentStopsGeoJSON !== prevProps.currentStopsGeoJSON);
if (currentMotChanged || currentStopsGeoJSONChanged) {
// First remove layers
this.map.getLayers().forEach(layer => {
if (layer && layer.get("type") === "markers") {
this.map.removeLayer(layer);
}
});
// Then add new ones
Object.keys(currentStopsGeoJSON).forEach(key => {
const vectorSource = new VectorSource({
features: new GeoJSON().readFeatures(currentStopsGeoJSON[key])
});
const vectorLayer = new VectorLayer({
source: vectorSource,
style: this.pointStyle
});
vectorLayer.set("type", "markers");
this.map.addLayer(vectorLayer);
const coordinate = vectorSource
.getFeatures()[0]
.getGeometry()
.getCoordinates();
this.map.getView().animate({
center: coordinate,
duration: 500
});
});
// Remove the old route if exists
this.removeCurrentRoute();
// Draw a new route if more than 1 stop is defined
if (Object.keys(currentStopsGeoJSON).length > 1) {
this.drawNewRoute();
}
}
}
/**
* After receiving the updated stops, send a call to the routingAPI to find a suitable route between
* two points/stations, if a route is found, it's returned and drawn to the map.
* @category Map
*/
drawNewRoute = () => {
if (this.findRouteCancel) this.findRouteCancel();
const hops = [];
const {
currentStopsGeoJSON,
routingUrl,
currentMot,
APIKey,
onShowNotification
} = this.props;
Object.keys(currentStopsGeoJSON).forEach(key => {
if (currentStopsGeoJSON[key].features) {
// If the current item is a point selected on the map, not a station.
hops.push(`@${currentStopsGeoJSON[key].features[0].properties.id}`);
} else {
// The item selected is a station from the stations API.
hops.push(`!${currentStopsGeoJSON[key].properties.id}`);
}
});
axios
.get(routingUrl, {
params: {
via: hops.join("|"),
mot: currentMot,
key: APIKey
},
cancelToken: new this.FindRouteCancelToken(cancel => {
this.findRouteCancel = cancel;
})
})
.then(
response => {
// A route was found, prepare to draw it.
const vectorSource = new VectorSource({
features: new GeoJSON().readFeatures(response.data)
});
const vectorLayer = new VectorLayer({
source: vectorSource,
style: [this.routeStyleOuter, this.routeStyleInner]
});
vectorLayer.set("type", "route");
this.map.addLayer(vectorLayer);
this.map.getView().fit(vectorSource.getExtent(), {
size: this.map.getSize(),
duration: 500
});
},
error => {
// No route was found.
if (error) onShowNotification("Couldn't find route", "error");
}
);
};
/**
* Remove the current route drawn on the map
* @category Map
*/
removeCurrentRoute = () => {
this.map.getLayers().forEach(layer => {
if (layer && layer.get("type") === "route") {
this.map.removeLayer(layer);
}
});
};
/**
* Render the map component to the dom
* @category Map
*/
render() {
return (
<>
<Snackbar
anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
open={this.state.hoveredStationOpen}
message={this.state.hoveredStationName}
/>
<div id="map" className="MapComponent"/>
</>
);
}
}
const mapStateToProps = state => {
return {
currentMot: state.MapReducer.currentMot,
currentStopsGeoJSON: state.MapReducer.currentStopsGeoJSON
};
};
const mapDispatchToProps = dispatch => {
return {
onSetClickLocation: clickLocation =>
dispatch(actions.setClickLocation(clickLocation)),
onShowNotification: (notificationMessage, notificationType) =>
dispatch(actions.showNotification(notificationMessage, notificationType))
};
};
MapComponent.propTypes = {
onSetClickLocation: PropTypes.func.isRequired,
onShowNotification: PropTypes.func.isRequired,
currentStopsGeoJSON: PropTypes.object.isRequired,
APIKey: PropTypes.string.isRequired,
routingUrl: PropTypes.string.isRequired,
currentMot: PropTypes.string.isRequired
};
export default connect(mapStateToProps, mapDispatchToProps)(MapComponent);
Source