Skip to content

Instantly share code, notes, and snippets.

@basith374
Created May 19, 2020 20:32
Show Gist options
  • Save basith374/a2c87be5a45636646d3dc7d90a526752 to your computer and use it in GitHub Desktop.
Save basith374/a2c87be5a45636646d3dc7d90a526752 to your computer and use it in GitHub Desktop.
google maps wrapper
import React, { useEffect, useRef, useState } from "react";
import "./map.css";
import moment from "moment";
import _ from "lodash";
import { resolvePlugin } from "@babel/core";
/**
* geofences format
{
geofenceid: 1,
type: 'polygon',
paths: [
{lat: 12.947014, lng: 77.207108},
{lat: 13.054061, lng: 77.119217},
{lat: 13.163737, lng: 77.226334},
{lat: 13.094192, lng: 77.327957},
{lat: 12.914891, lng: 77.410355},
],
},
{
geofenceid: 2,
type: 'circle',
center: {lat: 12.960398, lng: 78.050309},
radius: 10,
},
{
geofenceid: 3,
type: 'rectangle',
bounds: {
north: 12.596114,
west: 77.624588,
south: 12.515687,
east: 77.816849,
}
},
*/
/*
props
hideControls bool - hides the maximize & fit screen buttons
mapClicked func - get lat lng coords when you click on map
*/
let assets = {
blueMarker: require("./assets/blue-dot.png"),
greenMarker: require("./assets/green-dot.png"),
nonCommMarker: require("./assets/nocomm-dot.png"),
};
window.googleMapKey = "AIzaSyBWP1I3WaF2vAUI82JpdwfW3vGhfROCFTA";
window.vehicleMap = {};
let defaultCenter = { lat: 1.330025, lng: 103.853081 }; // singapore
// let defaultCenter = {lat: 12.972442, lng: 77.580643}; // bengaluru
let vehicleDataMap = {
direction: "dr",
};
let defaultMapZoom = 9;
let polygonColor = "#00ff00";
let circleColor = "#ff0000";
let rectangleColor = "#0000ff";
let getScale = (zoom) => {
if (zoom >= 16) return 1;
else if (zoom === 15) return 0.9;
else if (zoom === 14) return 0.8;
else if (zoom === 13) return 0.7;
return 0.6;
};
let getVehicleData = (v, k) => {
let ac = _.get(vehicleDataMap, k, ""); // actual key
return _.get(v, ac, "");
};
let getIcon = (vehicle, refs) => {
let scale = refs.markerScale.current;
let size = 32 * scale;
let offset = Math.floor(getVehicleData(vehicle, "direction") / 10) * size;
let url = assets.blueMarker;
if (vehicle.ig) url = assets.greenMarker;
if (vehicle.im) url = assets.nonCommMarker;
return {
url,
origin: new window.google.maps.Point(0, offset),
size: new window.google.maps.Size(size, size),
anchor: new window.google.maps.Point(size / 2, size / 2),
scaledSize: new window.google.maps.Size(size, size * 36),
};
};
function updateControls(props, map) {
function toggleButtonIcon() {
let position = window.google.maps.ControlPosition.TOP_RIGHT;
let controls = _.get(map.current.controls, position + ".g.0.children", []);
let expandButton = _.find(controls, ["className", "exp"]);
let bState = props.expanded ? "minimize" : "maximize";
if (expandButton) {
expandButton.innerHTML = '<i class="fa fa-window-' + bState + '"></i>';
if (props.expand) expandButton.onclick = props.expand;
}
}
function toggleControls() {
let position = window.google.maps.ControlPosition.TOP_LEFT;
let controls = _.get(map.current.controls, position + ".g", []);
let dropdown = _.find(controls, (f) => f.classList.contains("map-lft"));
if (dropdown) dropdown.style.display = props.expanded ? "flex" : "none";
}
toggleButtonIcon();
toggleControls();
}
function initMap(props, refs, setMapInitialized) {
let mapLoaded = () => {
let zoomChanged = () => {
let setMarkerScale = (scale) => {
if (scale) refs.markerScale.current = scale;
if (props.vehicles)
Object.keys(props.vehicles.data).forEach((k) => {
let marker = _.get(refs, "vehicleMarkers.current." + k + ".marker");
let v = _.get(props, "vehicles.data." + k);
if (marker) marker.setIcon(getIcon(v, refs));
});
};
// resize markers
let level = refs.map.current.getZoom();
let scale = getScale(level);
if (scale !== refs.markerScale.current) setMarkerScale(scale);
if (props.zoomChanged) props.zoomChanged(level);
};
const center = props.center || defaultCenter;
let map = (refs.map.current = new window.google.maps.Map(
document.getElementById("map"),
{
center,
zoom: defaultMapZoom,
fullscreenControl: false,
mapTypeControl: false,
streetViewControl: false,
}
));
if (!props.hideControls) attachControls(props, refs);
map.addListener("zoom_changed", zoomChanged);
map.addListener("click", (e) => {
if (props.mapClicked)
props.mapClicked(e.latLng.lat().toFixed(6), e.latLng.lng().toFixed(6));
console.log(e.latLng.lat().toFixed(6), e.latLng.lng().toFixed(6));
showHideDropdown(refs, false);
});
if (props.geofence === true) {
map.addListener("rightclick", (e) => {
console.log("right click clicked on map");
props.openGeoCreate(e.latLng);
});
}
let mapLoadedListener = map.addListener(
"tilesloaded",
function mapFullyLoaded() {
drawEvents(refs, props);
drawHotspots(refs, props);
drawTrace(props, refs);
drawVehicles(props, refs);
drawGeofences(props, refs);
updateControls(props, refs.map);
setMapInitialized(true);
}
);
refs.onMapInitialized.current = () =>
window.google.maps.event.removeListener(mapLoadedListener);
};
if (window.google) {
mapLoaded();
} else {
let loadJS = (src) => {
let ref = window.document.getElementsByTagName("script")[0];
let script = window.document.createElement("script");
script.src = src;
script.async = true;
ref.parentNode.insertBefore(script, ref);
};
window.initMap = mapLoaded;
let libraries = [
"places",
// 'visualization',
];
let params = ["callback=initMap", "key=" + window.googleMapKey];
if (libraries.length) params.push("libraries=" + libraries.join(","));
loadJS("https://maps.googleapis.com/maps/api/js?" + params.join("&"));
}
}
const makePolyline = (path, map) => {
return new window.google.maps.Polyline({
path,
icons: [
{
icon: {
path: window.google.maps.SymbolPath.FORWARD_CLOSED_ARROW,
strokeWeight: 1,
strokeColor: "#222122",
fillColor: "#222122",
fillOpacity: 1,
scale: 1.5,
},
offset: "100%",
repeat: "100px",
},
],
geodesic: true,
strokeColor: "#2569ff",
strokeOpacity: 1.0,
strokeWeight: 3,
map,
});
};
const makeMarker = (position, map, icon, label) => {
return new window.google.maps.Marker({
position,
map,
icon,
label,
});
};
const drawEvents = (refs, props) => {
if (refs.eventMarkers.current)
refs.eventMarkers.current.forEach((f) => f.setMap(null));
if (props.events)
refs.eventMarkers.current = props.events.data.map((f) => {
let icon = null;
if (f.icon) {
icon = {
url: f.icon,
};
}
let marker = makeMarker(f, refs.map.current, icon);
if (f.content) {
let infowindow = new window.google.maps.InfoWindow({
content: f.content,
});
marker.addListener("mouseover", () =>
infowindow.open(refs.map.current, marker)
);
marker.addListener("mouseout", () => infowindow.close());
}
return marker;
});
};
const drawHotspots = (refs, props) => {
if (refs.hotspotMarkers.current)
refs.hotspotMarkers.current.forEach((f) => f.setMap(null));
if (props.hotspot)
refs.hotspotMarkers.current = props.hotspot.data.map((f) => {
let icon = null;
if (f.icon) {
let size = 32;
icon = {
url: f.icon,
// size: new window.google.maps.Size(size, size),
anchor: new window.google.maps.Point(size / 2, size / 2),
scaledSize: new window.google.maps.Size(size, size),
};
}
let label = null;
if (f.text) {
label = {
text: f.text,
color: "#fff",
fontSize: "10px",
};
}
return makeMarker(f, refs.map.current, icon, label);
});
};
function drawTrace(props, refs) {
if (props.trace) {
if (!refs.trace.current)
refs.trace.current = makePolyline(props.trace.data, refs.map.current);
else refs.trace.current.setPath(props.trace.data);
} else {
if (refs.trace.current) {
refs.trace.current.setMap(null);
refs.trace.current = null;
}
}
}
function drawVehicles(props, refs) {
if (props.vehicles) {
Object.keys(props.vehicles.data).forEach((k) => {
let marker;
let v = _.get(props, "vehicles.data." + k);
let vid = v.vp;
if (refs.vehicleMarkers.current[vid])
marker = refs.vehicleMarkers.current[vid].marker;
if (!marker) {
marker = makeMarker(
new window.google.maps.LatLng(v.lt, v.ln),
refs.map.current,
getIcon(v, refs)
);
refs.vehicleMarkers.current[vid] = {
marker,
updatedAt: moment().valueOf(),
};
} else {
marker.setPosition(new window.google.maps.LatLng(v.lt, v.ln));
marker.setMap(refs.map.current);
marker.setIcon(getIcon(v, refs));
}
});
} else {
Object.keys(refs.vehicleMarkers.current).forEach((f) => {
let marker = _.get(refs.vehicleMarkers.current, f + ".marker");
if (marker) marker.setMap(null);
});
}
}
function drawGeofences(props, refs) {
console.log("geofence data is:", props.geofences);
let geofences = _.keyBy(props.geofences, 'geofenceid');
let prevGeofences = refs.geofences.current;
(props.geofences || []).forEach((g) => {
if(!g.geofenceid) {
return console.error('No geofenceid in provided geofence props');
}
let geofence = prevGeofences[g.geofenceid];
if (!geofence) {
if (g.structure === "polygon") {
let color = g.color || polygonColor;
geofence = new window.google.maps.Polygon({
paths: g.paths,
strokeColor: color,
strokeOpacity: 0.8,
strokeWeight: 1,
fillColor: color,
fillOpacity: 0.35,
map: refs.map.current,
});
let pointsChanged = () => {
let paths = geofence.getPath().g.map((p) => {
return {
lat: parseFloat(p.lat().toFixed(6)),
lng: parseFloat(p.lng().toFixed(6)),
};
});
props.changeGeofence(g.geofenceid, { paths });
};
geofence.getPath().addListener("set_at", pointsChanged);
geofence.getPath().addListener("insert_at", pointsChanged);
geofence.getPath().addListener("remove_at", pointsChanged);
} else if (g.structure === "circle") {
let color = g.color || circleColor;
geofence = new window.google.maps.Circle({
strokeColor: color,
strokeOpacity: 0.8,
strokeWeight: 1,
fillColor: color,
fillOpacity: 0.35,
center: g.center,
radius: g.radius * 100,
map: refs.map.current,
});
geofence.addListener("radius_changed", () => {
props.changeGeofence(g.geofenceid, {
radius: parseFloat((geofence.getRadius() / 100).toFixed(2)),
});
});
geofence.addListener(
"center_changed",
_.debounce(() => {
let center = geofence.getCenter();
props.changeGeofence(g.geofenceid, {
center: {
lat: parseFloat(center.lat().toFixed(6)),
lng: parseFloat(center.lng().toFixed(6)),
},
});
}, 200)
);
} else if (g.structure === "rectangle") {
let color = g.color || rectangleColor;
geofence = new window.google.maps.Rectangle({
bounds: g.bounds,
strokeColor: color,
strokeOpacity: 0.8,
strokeWeight: 1,
fillColor: color,
fillOpacity: 0.35,
map: refs.map.current,
});
let boundsChanged = () => {
props.changeGeofence(g.geofenceid, {
bounds: {
north: parseFloat(geofence.bounds.pa.h.toFixed(6)),
west: parseFloat(geofence.bounds.ka.g.toFixed(6)),
south: parseFloat(geofence.bounds.pa.g.toFixed(6)),
east: parseFloat(geofence.bounds.ka.h.toFixed(6)),
},
});
};
// debounce is for drag event
geofence.addListener(
"bounds_changed",
_.debounce(boundsChanged, 200)
);
}
prevGeofences[g.geofenceid] = geofence;
} else {
// geofence.setDraggable(g.draggable);
// geofence.setEditable(g.editable);
geofence.setDraggable(true);
geofence.setEditable(true);
geofence.setMap(refs.map.current);
}
});
Object.keys(prevGeofences).forEach((k) => {
if(!(k in geofences)) {
prevGeofences[k].setMap(null);
delete prevGeofences[k];
}
});
}
const fitBounds = (refs) => {
return () => {
var bounds = new window.google.maps.LatLngBounds();
let list = [];
let markers = [
...refs.eventMarkers.current,
...refs.hotspotMarkers.current,
];
markers.forEach((f) => {
let lat = f.position.lat(),
lng = f.position.lng();
if (!(lat === 0 && lng === 0)) {
bounds.extend(f.position);
list.push([lat, lng]);
}
});
if (refs.trace.current)
refs.trace.current
.getPath()
.getArray()
.forEach((f) => {
let lat = f.lat(),
lng = f.lng();
bounds.extend(f);
list.push([lat, lng]);
});
Object.keys(refs.vehicleMarkers.current).forEach((f) => {
let marker = refs.vehicleMarkers.current[f].marker;
let lat = marker.position.lat(),
lng = marker.position.lng();
if (marker.getMap() && !(lat === 0 && lng === 0)) {
bounds.extend(marker.position);
list.push([lat, lng]);
}
});
if (list.length === 1) {
let lat = list[0][0],
lng = list[0][1];
bounds.extend({ lat: lat * 0.999, lng: lng * 0.999 });
bounds.extend({ lat: lat * 1.001, lng: lng * 1.001 });
}
if (list.length) refs.map.current.fitBounds(bounds);
};
};
function attachControls(props, refs) {
let attachRightControls = () => {
let expandButton = () => {
let el = document.createElement("button");
el.className = "exp";
el.innerHTML = '<i class="fa fa-window-maximize"></i>';
if (props.expand) el.onclick = props.expand;
return el;
};
let fitViewButton = () => {
let el = document.createElement("button");
el.innerHTML = '<i class="fa fa-crosshairs"></i>';
el.onclick = fitBounds(refs);
return el;
};
let position = window.google.maps.ControlPosition.TOP_RIGHT;
let controls = document.createElement("div");
controls.className = "map-grc";
controls.appendChild(expandButton());
controls.appendChild(fitViewButton());
refs.map.current.controls[position].push(controls);
};
let attachLeftControls = () => {
let position = window.google.maps.ControlPosition.TOP_LEFT;
let controls = document.createElement("div");
controls.className = "map-lft";
controls.style.display = "none";
controls.appendChild(makeDropdown(props, refs));
controls.appendChild(makeSearch(refs));
refs.map.current.controls[position].push(controls);
};
attachLeftControls();
attachRightControls();
}
const dropdownLabel = (label, props) => {
if (label) {
let [lbl, cls] = label.split(",");
return (
'<div class="' +
cls +
'">' +
lbl +
' <i class="fa fa-caret-down"></i></div>'
);
}
if (!props.hotspot) return "No hotspots";
return '<div>Apply filter <i class="fa fa-caret-down"></i><div>';
};
const updateDropdownLabel = (map, label, props) => {
let position = window.google.maps.ControlPosition.TOP_LEFT;
let controls = _.get(map.controls, position + ".g.0.children", []);
let dropdown = _.find(controls, (f) => f.classList.contains("map-grd"));
// let button = dropdown.querySelector("button");
// button.innerHTML = dropdownLabel(label, props);
};
const makeDropdown = (props, refs) => {
let dropdownButton = () => {
let el = document.createElement("button");
el.innerHTML = dropdownLabel("", props);
el.onclick = () => {
if (el.innerHTML !== "No hotspots") showHideDropdown(refs);
};
return el;
};
let dropdownList = () => {
let makeOpt = (cls, lbl) => {
let el = document.createElement("div");
el.className = cls;
el.innerText = lbl;
el.onclick = () => {
updateDropdownLabel(refs.map.current, lbl + "," + cls, props);
showHideDropdown(refs);
if (props.selectOpt) props.selectOpt(lbl);
};
return el;
};
let el = document.createElement("div");
el.className = "list";
el.appendChild(makeOpt("red", "Acceleration"));
el.appendChild(makeOpt("blu", "Braking"));
el.appendChild(makeOpt("pur", "All"));
return el;
};
let dropdown = document.createElement("div");
dropdown.className = "map-grd";
dropdown.appendChild(dropdownButton());
dropdown.appendChild(dropdownList());
return dropdown;
};
const makeSearch = (refs) => {
let search = document.createElement("div");
search.className = "map-sch";
let input = document.createElement("input");
input.placeholder = "Search";
let searchBox = new window.google.maps.places.SearchBox(input);
window.google.maps.event.addListener(searchBox, "places_changed", () => {
let places = searchBox.getPlaces();
if (places.length === 0) return;
let bounds = new window.google.maps.LatLngBounds();
places.forEach((place) => {
if (place.geometry.viewport) {
bounds.union(place.geometry.viewport);
} else {
bounds.extend(place.geometry.location);
}
});
refs.map.current.fitBounds(bounds);
});
search.appendChild(input);
return search;
};
const showHideDropdown = (refs, show) => {
let position = window.google.maps.ControlPosition.TOP_LEFT;
let controls = _.get(
refs.map.current.controls,
position + ".g.0.children",
[]
);
let dropdown = _.find(controls, (f) => f.classList.contains("map-grd"));
if (dropdown)
if (show !== undefined) dropdown.classList[show ? "add" : "remove"]("open");
else dropdown.classList.toggle("open");
};
const propsUpdated = (props, refs) => {
// update trace
if (_.get(props, "trace.updatedAt") !== refs.traceUpdatedAt.current) {
drawTrace(props, refs);
}
// update markers (events)
if (_.get(props, "events.updatedAt") !== refs.eventsUpdatedAt.current) {
drawEvents(refs, props);
}
// update markers (vehicles)
if (_.get(props, "vehicles.updatedAt") !== refs.vehiclesUpdatedAt.current) {
drawVehicles(props, refs);
}
if (!props.hotspot && refs.prevHotspotRef.current)
showHideDropdown(refs, false);
};
export default function GoogleMap(props) {
let { busy, msg, statusbar } = props;
if (!msg) msg = "";
let vehicleMarkers = useRef({});
let eventMarkers = useRef([]);
let hotspotMarkers = useRef([]);
let markerScale = useRef(getScale(defaultMapZoom));
let map = useRef();
let trace = useRef();
let geofences = useRef({}); // shapes
let prevHotspotRef = useRef();
let eventsUpdatedAt = useRef();
let vehiclesUpdatedAt = useRef();
let traceUpdatedAt = useRef();
// used to mark map ready status
// any markers are drawn only after initial tile loaded event
let [mapInitialized, setMapInitialized] = useState(false);
let onMapInitialized = useRef();
useEffect(() => {
let refs = {
markerScale,
vehicleMarkers,
map,
eventMarkers,
hotspotMarkers,
trace,
onMapInitialized,
geofences,
};
initMap(props, refs, setMapInitialized);
}, []);
useEffect(() => {
let refs = {
markerScale,
vehicleMarkers,
map,
eventMarkers,
hotspotMarkers,
prevHotspotRef,
eventsUpdatedAt,
vehiclesUpdatedAt,
traceUpdatedAt,
trace,
};
if (window.google && mapInitialized) {
propsUpdated(props, refs);
}
eventsUpdatedAt.current = _.get(props, "events.updatedAt");
traceUpdatedAt.current = _.get(props, "trace.updatedAt");
vehiclesUpdatedAt.current = _.get(props, "vehicles.updatedAt");
}, [props]);
function hotspotUpdated() {
drawHotspots(
{
hotspotMarkers,
map,
},
props
);
updateDropdownLabel(map.current, "", props);
}
useEffect(() => {
if (window.google && mapInitialized) {
if (
_.get(props, "hotspot.updatedAt") !==
_.get(prevHotspotRef, "current.updatedAt")
) {
hotspotUpdated();
}
}
prevHotspotRef.current = props.hotspot;
}, [props.hotspot]);
useEffect(() => {
console.log("google map recv new geofence data");
if (window.google && mapInitialized) {
let refs = {
geofences,
map,
};
drawGeofences(props, refs);
}
}, [props.geofences]);
// one time, used as callback to map initialized
useEffect(() => {
if (mapInitialized) {
onMapInitialized.current();
hotspotUpdated();
}
}, [mapInitialized]);
useEffect(() => {
if (!window.google) return;
updateControls(props, map);
}, [props.expanded]);
return (
<div className="map-gc">
<div className="map-gm" id="map"></div>
{busy && <div className="map-gb"></div>}
{msg && <div className="map-gh">{msg}</div>}
{statusbar && <div className="map-sb"></div>}
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment