Skip to content

Commit

Permalink
Merge pull request #1370 from NSUWAL123/feat-project-details-geolocat…
Browse files Browse the repository at this point in the history
…ion-integration

Feat project details geolocation integration
  • Loading branch information
varun2948 authored Mar 20, 2024
2 parents fb7381c + 00238a5 commit e9ecfdd
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 119 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import React, { useState } from 'react';
import AssetModules from '@/shared/AssetModules';
import VectorLayer from 'ol/layer/Vector';
import CoreModules from '@/shared/CoreModules.js';
import { ProjectActions } from '@/store/slices/ProjectSlice';
import { useAppSelector } from '@/types/reduxTypes';

const MapControlComponent = ({ map }) => {
const btnList = [
Expand All @@ -28,9 +25,7 @@ const MapControlComponent = ({ map }) => {
title: 'Zoom to Project',
},
];
const dispatch = CoreModules.useAppDispatch();
const [toggleCurrentLoc, setToggleCurrentLoc] = useState(false);
const geolocationStatus = useAppSelector((state) => state.project.geolocationStatus);

const handleOnClick = (btnId) => {
if (btnId === 'add') {
const actualZoom = map.getView().getZoom();
Expand All @@ -39,8 +34,19 @@ const MapControlComponent = ({ map }) => {
const actualZoom = map.getView().getZoom();
map.getView().setZoom(actualZoom - 1);
} else if (btnId === 'currentLocation') {
setToggleCurrentLoc(!toggleCurrentLoc);
dispatch(ProjectActions.ToggleGeolocationStatus(!geolocationStatus));
const layers = map.getAllLayers();
let extent;
layers.map((layer) => {
if (layer instanceof VectorLayer) {
const layerName = layer.getProperties().name;
if (layerName === 'geolocation') {
extent = layer.getSource().getExtent();
}
}
});
map.getView().fit(extent, {
padding: [10, 10, 10, 10],
});
} else if (btnId === 'taskBoundries') {
const layers = map.getAllLayers();
let extent;
Expand Down
4 changes: 0 additions & 4 deletions src/frontend/src/store/slices/ProjectSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const initialState: ProjectStateTypes = {
taskModalStatus: false,
toggleGenerateMbTilesModal: false,
mobileFooterSelection: 'explore',
geolocationStatus: false,
projectDetailsLoading: true,
projectDashboardDetail: {
project_name_prefix: '',
Expand Down Expand Up @@ -87,9 +86,6 @@ const ProjectSlice = createSlice({
SetMobileFooterSelection(state, action) {
state.mobileFooterSelection = action.payload;
},
ToggleGeolocationStatus(state, action) {
state.geolocationStatus = action.payload;
},
SetProjectDetialsLoading(state, action) {
state.projectDetailsLoading = action.payload;
},
Expand Down
1 change: 0 additions & 1 deletion src/frontend/src/store/types/IProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export type ProjectStateTypes = {
taskModalStatus: boolean;
toggleGenerateMbTilesModal: boolean;
mobileFooterSelection: string;
geolocationStatus: boolean;
projectDetailsLoading: boolean;
projectDashboardDetail: projectDashboardDetailTypes;
projectDashboardLoading: boolean;
Expand Down
122 changes: 122 additions & 0 deletions src/frontend/src/utilfunctions/Geolocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import pngbluedot from '@/assets/images/png bluedot.png';
import { Fill } from 'ol/style';
import VectorSource from 'ol/source/Vector';
import OLVectorLayer from 'ol/layer/Vector';
import { Feature } from 'ol';
import { Point } from 'ol/geom';
import { fromLonLat } from 'ol/proj';
import { circular } from 'ol/geom/Polygon';
import { Style } from 'ol/style';
import { Icon } from 'ol/style';

const locationIconStyle = new Style({
fill: new Fill({
color: 'rgba(0, 0, 255, 0.2)',
}),
image: new Icon({
src: pngbluedot,
scale: 0.09,
imgSize: [27, 55],
rotateWithView: true,
}),
});

export const Geolocation = (map) => {
if (!map) return;
const source = new VectorSource();
const layer = new OLVectorLayer({
source: source,
properties: {
name: 'geolocation',
},
});
map?.addLayer(layer);

navigator.geolocation.watchPosition(
function (pos) {
const coords = [pos.coords.longitude, pos.coords.latitude];
source.clear(true);
source.addFeatures([
new Feature(circular(coords, pos.coords.accuracy).transform('EPSG:4326', map.getView().getProjection())),
new Feature(new Point(fromLonLat(coords))),
]);
},
function (error) {
alert(`ERROR: ${error.message}`);
},
{
enableHighAccuracy: true,
},
);

layer.setStyle(locationIconStyle);

function handleReading(quaternion) {
// https://w3c.github.io/orientation-sensor/#model explains the order of
// the 4 elements in the sensor.quaternion array.
let [qx, qy, qz, qw] = quaternion;

// When the phone is lying flat, we want to treat the direction toward the
// top of the phone as the "forward" direction; when the phone is held
// upright, we want to treat the direction out the back of the phone as the
// "forward" direction. So, let's determine the compass heading of the
// phone based on the vector between these directions, i.e. at a 45-degree
// angle between the positive Y-axis and the negative Z-axis in this figure:
// https://w3c.github.io/orientation-sensor/#absoluteorientationsensor-model

// To find the current "forward" direction of the phone, we want to take this
// vector, (0, 1, -1), and apply the same rotation as the phone's rotation.
const y = 1;
const z = -1;

// From experimentation, it looks like the quaternion from the sensor is
// the inverse rotation, so we need to flip the fourth component.
qw = -qw;

// This section explains how to convert the quaternion to a rotation matrix:
// https://w3c.github.io/orientation-sensor/#convert-quaternion-to-rotation-matrix
// Now let's multiply the forward vector by the rotation matrix.
const rx = y * (2 * qx * qy + 2 * qw * qz) + z * (2 * qx * qz - 2 * qw * qy);
const ry = y * (1 - 2 * qx * qx - 2 * qz * qz) + z * (2 * qy * qz + 2 * qw * qx);
const rz = y * (2 * qy * qz + 2 * qw * qx) + z * (1 - 2 * qx * qx - 2 * qy * qy);

// This gives us a rotated vector indicating the "forward" direction of the
// phone with respect to the earth. We only care about the orientation of
// this vector in the XY plane (the plane tangential to the ground), i.e.
// the heading of the (rx, ry) vector, where (0, 1) is north.

const radians = Math.atan2(ry, rx);
const degrees = (radians * 180) / Math.PI; // counterclockwise from +X axis
let heading = 90 - degrees;
if (heading < 0) heading += 360;
heading = Math.round(heading);

// To make the arrow point north, rotate it opposite to the phone rotation.
locationIconStyle.getImage().setRotation((Math.PI / 180) * heading);
}

// See the API specification at: https://w3c.github.io/orientation-sensor
// We use referenceFrame: 'screen' because the web page will rotate when
// the phone switches from portrait to landscape.
const sensor = new AbsoluteOrientationSensor({
frequency: 60,
referenceFrame: 'screen',
});
sensor.addEventListener('reading', (event) => {
layer.on('postrender', handleReading(sensor.quaternion));
});
// handleReading([0.509, -0.071, -0.19, 0.836]);

Promise.all([
navigator.permissions.query({ name: 'accelerometer' }),
navigator.permissions.query({ name: 'magnetometer' }),
navigator.permissions.query({ name: 'gyroscope' }),
]).then((results) => {
if (results.every((result) => result.state === 'granted')) {
sensor.start();
// stat.value = "Sensor started!";
} else {
// stat.value = "No permissions to use AbsoluteOrientationSensor.";
}
});
};
113 changes: 7 additions & 106 deletions src/frontend/src/views/ProjectDetailsV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,18 @@ import LayerSwitcherControl from '@/components/MapComponent/OpenLayersComponent/
import MapControlComponent from '@/components/ProjectDetailsV2/MapControlComponent';
import { VectorLayer } from '@/components/MapComponent/OpenLayersComponent/Layers';
import { geojsonObjectModel } from '@/constants/geojsonObjectModal';
// import { basicGeojsonTemplate } from '@/utilities/mapUtils';
import getTaskStatusStyle from '@/utilfunctions/getTaskStatusStyle';
import { defaultStyles } from '@/components/MapComponent/OpenLayersComponent/helpers/styleUtils';
import MapLegends from '@/components/MapLegends';
import Accordion from '@/components/common/Accordion';
import AsyncPopup from '@/components/MapComponent/OpenLayersComponent/AsyncPopup/AsyncPopup';
import { Geolocation } from '@capacitor/geolocation';
import { Icon, Style } from 'ol/style';
import { Motion } from '@capacitor/motion';
import locationArc from '@/assets/images/locationArc.png';
import { CommonActions } from '@/store/slices/CommonSlice';
import Button from '@/components/common/Button';
import ProjectInfo from '@/components/ProjectDetailsV2/ProjectInfo';
import useOutsideClick from '@/hooks/useOutsideClick';
import { dataExtractPropertyType } from '@/models/project/projectModel';
import { isValidUrl } from '@/utilfunctions/urlChecker';
import { useAppSelector } from '@/types/reduxTypes';
import { Geolocation } from '@/utilfunctions/Geolocation';

const Home = () => {
const dispatch = CoreModules.useAppDispatch();
Expand All @@ -54,27 +49,19 @@ const Home = () => {

const [mainView, setView] = useState<any>();
const [featuresLayer, setFeaturesLayer] = useState();
const [toggleGenerateModal, setToggleGenerateModal] = useState(false);
const [dataExtractUrl, setDataExtractUrl] = useState(null);
const [dataExtractExtent, setDataExtractExtent] = useState(null);
const [taskBoundariesLayer, setTaskBoundariesLayer] = useState<null | Record<string, any>>(null);
const [currentCoordinate, setCurrentCoordinate] = useState<{ latitude: null | number; longitude: null | number }>({
latitude: null,
longitude: null,
});
const [positionGeojson, setPositionGeojson] = useState<any>(null);
const [deviceRotation, setDeviceRotation] = useState(0);

const [viewState, setViewState] = useState('project_info');
const encodedId: string = params.id;
const decodedId: number = environment.decode(encodedId);
const defaultTheme = useAppSelector((state) => state.theme.hotTheme);
const state = CoreModules.useAppSelector((state) => state.project);
const projectInfo = useAppSelector((state) => state.home.selectedProject);
const selectedTask = useAppSelector((state) => state.task.selectedTask);
const stateSnackBar = useAppSelector((state) => state.home.snackbar);
const mobileFooterSelection = useAppSelector((state) => state.project.mobileFooterSelection);
const mapTheme = useAppSelector((state) => state.theme.hotTheme);
const geolocationStatus = useAppSelector((state) => state.project.geolocationStatus);
const projectDetailsLoading = useAppSelector((state) => state?.project?.projectDetailsLoading);

//snackbar handle close funtion
Expand Down Expand Up @@ -117,6 +104,11 @@ const Home = () => {
zoom: 4,
});

useEffect(() => {
if (!map) return;
Geolocation(map);
}, [map]);

const { y } = OnScroll(map, windowSize.width);

useEffect(() => {
Expand Down Expand Up @@ -201,87 +193,11 @@ const Home = () => {
}
}, [mobileFooterSelection]);

const handlePositionChange = (position) => {
setCurrentCoordinate({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
});

const geojson = {
type: 'Point',
coordinates: [position.coords.longitude, position.coords.latitude],
};
setPositionGeojson(geojson);
};

useEffect(async () => {
if (geolocationStatus) {
const checkPermission = await Geolocation.checkPermissions();
if (checkPermission.location === 'denied') {
await Geolocation.requestPermissions(['location']);
}
}
}, [geolocationStatus]);

useEffect(() => {
if (geolocationStatus) {
const getCurrentPosition = async () => {
try {
const position = await Geolocation.getCurrentPosition();
handlePositionChange(position);
// Watch for position changes
const watchId = Geolocation.watchPosition({ enableHighAccuracy: true }, handlePositionChange);
// Clean up the watchPosition when the component unmounts
return () => {
Geolocation.clearWatch({ id: watchId });
};
} catch (error) {
dispatch(
CommonActions.SetSnackBar({
open: true,
message: `Error getting current position. Please ensure location permissions has been granted.`,
variant: 'error',
duration: 2000,
}),
);
dispatch(ProjectActions.ToggleGeolocationStatus(false));
}
};

getCurrentPosition();
}
}, [geolocationStatus]);

const locationArcStyle = new Style({
image: new Icon({
src: locationArc,
}),
});

const startOrientation = async () => {
const handler = await Motion.addListener('orientation', (event) => {
var alphaRad = event?.alpha * (Math.PI / 180);
var betaRad = event?.beta * (Math.PI / 180);
var gammaRad = event?.gamma * (Math.PI / 180);

setDeviceRotation(alphaRad + betaRad + gammaRad);
});
};

useEffect(() => {
// Cleanup when the component unmounts
if (geolocationStatus) {
startOrientation();
}
return () => {};
}, [geolocationStatus]);

return (
<div className="fmtm-bg-[#F5F5F5] fmtm-h-[100vh] sm:fmtm-h-[90vh]">
{/* Customized Modal For Generate Tiles */}
<div>
<GenerateBasemap projectInfo={state.projectInfo} />

{/* Home snackbar */}
<CustomizedSnackbar
duration={stateSnackBar.duration}
Expand Down Expand Up @@ -429,21 +345,6 @@ const Home = () => {
/>
)}
<AsyncPopup map={map} popupUI={dataExtractDataPopup} primaryKey={'osm_id'} />
{geolocationStatus && currentCoordinate?.latitude && currentCoordinate?.longitude && (
<VectorLayer
map={map}
geojson={positionGeojson}
setStyle={locationArcStyle}
viewProperties={{
size: map?.getSize(),
padding: [50, 50, 50, 50],
constrainResolution: true,
duration: 2000,
}}
zIndex={5}
rotation={deviceRotation}
/>
)}
<div className="fmtm-top-28 fmtm-left-5">{window.DeviceMotionEvent}</div>
<div className="fmtm-hidden sm:fmtm-block fmtm-absolute fmtm-bottom-5 fmtm-left-5 fmtm-z-50 fmtm-rounded-lg">
<Accordion
Expand Down

0 comments on commit e9ecfdd

Please sign in to comment.