summaryrefslogtreecommitdiff
path: root/client/components
diff options
context:
space:
mode:
authorLogan Hunt <loganhunt@simponic.xyz>2022-03-30 15:18:16 -0600
committerLogan Hunt <loganhunt@simponic.xyz>2022-03-30 15:18:16 -0600
commitacff469ba069b6f090adfd5ed91379c9f146aa77 (patch)
treee600e951d2e88ffde9252214fe31b8042ca129aa /client/components
parent042e3b9862b253fb3c3e59ee628dd9e30edf7e35 (diff)
downloadlocchat-acff469ba069b6f090adfd5ed91379c9f146aa77.tar.gz
locchat-acff469ba069b6f090adfd5ed91379c9f146aa77.zip
Ability to add, remove, update radius and location of chatrooms with a leaflet
Diffstat (limited to 'client/components')
-rw-r--r--client/components/home/_home.jsx27
-rw-r--r--client/components/map/_map.jsx47
-rw-r--r--client/components/map/chat_room_geoman.jsx175
-rw-r--r--client/components/map/legend.jsx28
-rw-r--r--client/components/router.jsx2
5 files changed, 266 insertions, 13 deletions
diff --git a/client/components/home/_home.jsx b/client/components/home/_home.jsx
index 405a968..7ef051c 100644
--- a/client/components/home/_home.jsx
+++ b/client/components/home/_home.jsx
@@ -4,6 +4,7 @@ import { ApiContext } from '../../utils/api_context';
import { AuthContext } from '../../utils/auth_context';
import { RolesContext } from '../../utils/roles_context';
import { Button } from '../common/button';
+import { Map } from '../map/_map';
import { Ping } from './ping';
export const Home = () => {
@@ -33,19 +34,19 @@ export const Home = () => {
}
return (
- <div className="p-4">
- <h1>Welcome {user.firstName}</h1>
- <Button type="button" onClick={logout}>
- Logout
- </Button>
- {roles.includes('admin') && (
- <Button type="button" onClick={() => navigate('/admin')}>
- Admin
+ <>
+ <div className="p-4">
+ <h1>Welcome {user.firstName}</h1>
+ <Button type="button" onClick={logout}>
+ Logout
</Button>
- )}
- <section>
- <Ping />
- </section>
- </div>
+ {roles.includes('admin') && (
+ <Button type="button" onClick={() => navigate('/admin')}>
+ Admin
+ </Button>
+ )}
+ </div>
+ <Map user={user} />
+ </>
);
};
diff --git a/client/components/map/_map.jsx b/client/components/map/_map.jsx
new file mode 100644
index 0000000..9f6684c
--- /dev/null
+++ b/client/components/map/_map.jsx
@@ -0,0 +1,47 @@
+import { MapContainer, TileLayer } from 'react-leaflet';
+import '@geoman-io/leaflet-geoman-free';
+import '@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css';
+import Geoman from './chat_room_geoman';
+import { useEffect, useState } from 'react';
+import toast from 'react-hot-toast';
+import { Legend } from './legend';
+
+export const Map = ({ user, zoom }) => {
+ const [loading, setLoading] = useState(true);
+ const [position, setPosition] = useState({});
+ const [positionWatcher, setPositionWatcher] = useState();
+
+ zoom = zoom || 18;
+
+ useEffect(() => {
+ if (user) {
+ setPositionWatcher(
+ navigator.geolocation.getCurrentPosition(
+ (pos) => {
+ const { latitude: lat, longitude: lng } = pos.coords;
+ setPosition({ lat, lng });
+ setLoading(false);
+ },
+ (err) => {
+ toast.error(err.message);
+ },
+ ),
+ );
+ }
+ }, [user]);
+
+ if (!loading) {
+ return (
+ <MapContainer center={position} zoom={zoom} minZoom={15}>
+ <TileLayer
+ url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
+ attribution='&copy; OpenStreetMap | &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
+ maxZoom={19}
+ />
+ <Legend />
+ <Geoman joinRoom={console.log} userPos={position} user={user} />
+ </MapContainer>
+ );
+ }
+ return <div>Getting current location...</div>;
+};
diff --git a/client/components/map/chat_room_geoman.jsx b/client/components/map/chat_room_geoman.jsx
new file mode 100644
index 0000000..7806908
--- /dev/null
+++ b/client/components/map/chat_room_geoman.jsx
@@ -0,0 +1,175 @@
+import { useLeafletContext } from '@react-leaflet/core';
+import L from 'leaflet';
+import markerIconPng from 'leaflet/dist/images/marker-icon.png';
+import { useEffect, useContext } from 'react';
+import { ApiContext } from '../../utils/api_context';
+import { AuthContext } from '../../utils/auth_context';
+
+const userPositionBubble = {
+ color: 'black',
+ fillColor: 'black',
+ fillOpacity: 0.6,
+ weight: 5,
+ pmIgnore: true,
+ radius: 5,
+};
+
+const joinable = {
+ color: 'green',
+ weight: 1,
+ pmIgnore: true,
+};
+
+const unjoinable = {
+ color: 'red',
+ weight: 1,
+ pmIgnore: true,
+};
+
+const editable = {
+ color: 'blue',
+ weight: 1,
+ pmIgnore: false,
+};
+
+const icon = new L.Icon({ iconUrl: markerIconPng, iconSize: [25, 41], iconAnchor: [12, 41] });
+
+const haversine = (p1, p2) => {
+ const degreesToRadians = (degrees) => degrees * (Math.PI / 180);
+ const delta = { lat: degreesToRadians(p2.lat - p1.lat), lng: degreesToRadians(p2.lng - p1.lng) };
+ const a =
+ Math.sin(delta.lat / 2) * Math.sin(delta.lat / 2) +
+ Math.cos(degreesToRadians(p1.lat)) *
+ Math.cos(degreesToRadians(p2.lat)) *
+ Math.sin(delta.lng / 2) *
+ Math.sin(delta.lng / 2);
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ const r = 6371 * 1000;
+ return r * c;
+};
+
+// GeoMan code is heavily adapted from this codesandbox: https://codesandbox.io/s/394eq
+export const Geoman = ({ user, userPos, joinRoom }) => {
+ const context = useLeafletContext();
+ const api = useContext(ApiContext);
+ const circleAndMarkerFromChatroom = (chatRoom) => {
+ const circle = new L.Circle(chatRoom.center, chatRoom.radius);
+ const marker = new L.Marker(chatRoom.center, { pmIgnore: !chatRoom.isEditable, icon });
+ circle.setStyle(
+ chatRoom.isEditable
+ ? editable
+ : haversine(userPos, { lat: chatRoom.latitude, lng: chatRoom.longitude }) < chatRoom.radius
+ ? joinable
+ : unjoinable,
+ );
+ marker.addEventListener('click', () => {
+ console.log(chatRoom.id);
+ console.log(haversine(userPos, { lat: chatRoom.latitude, lng: chatRoom.longitude }), chatRoom.radius, userPos);
+ });
+ if (!!chatRoom.isEditable) {
+ [circle, marker].map((x) => {
+ x.on('pm:edit', (e) => {
+ const coords = e.target.getLatLng();
+ marker.setLatLng(coords);
+ circle.setLatLng(coords);
+ api.put(`/chat_rooms/${chatRoom.id}`, {
+ ...chatRoom,
+ latitude: coords.lat,
+ longitude: coords.lng,
+ });
+ });
+ x.on('pm:remove', (e) => {
+ context.map.removeLayer(marker);
+ context.map.removeLayer(circle);
+
+ api.del(`/chat_rooms/${chatRoom.id}`);
+ });
+ });
+ circle.on('pm:drag', (e) => {
+ marker.setLatLng(e.target.getLatLng());
+ });
+ marker.on('pm:drag', (e) => {
+ circle.setLatLng(e.target.getLatLng());
+ });
+ }
+ [circle, marker].map((x) => x.addTo(context.map));
+ return [circle, marker];
+ };
+
+ const reRender = async () => {
+ const layersToRemove = [];
+ context.map.eachLayer((layer) => {
+ if (layer instanceof L.Circle || layer instanceof L.Marker) {
+ layersToRemove.push(layer);
+ }
+ });
+
+ const res = await api.get(`/chat_rooms?lat=${userPos.lat}&lng=${userPos.lng}`);
+ res.map((x) => {
+ circleAndMarkerFromChatroom({
+ center: [x.latitude, x.longitude],
+ ...x,
+ isEditable: user && x.userId == user.id,
+ });
+ });
+ layersToRemove.map((x) => context.map.removeLayer(x));
+
+ const userLocationCircle = new L.Circle(userPos, 5);
+ userLocationCircle.setStyle(userPositionBubble);
+ userLocationCircle.addTo(context.map);
+ };
+
+ useEffect(() => {
+ if (context) {
+ reRender();
+ }
+ }, [userPos]);
+
+ useEffect(() => {
+ const leafletContainer = context.layerContainer || context.map;
+ leafletContainer.pm.addControls({
+ drawMarker: false,
+ editControls: true,
+ dragMode: true,
+ cutPolygon: false,
+ removalMode: true,
+ rotateMode: false,
+ splitMode: false,
+ drawPolyline: false,
+ drawRectangle: false,
+ drawPolygon: false,
+ drawCircleMarker: false,
+ });
+
+ leafletContainer.pm.setGlobalOptions({ pmIgnore: false });
+
+ leafletContainer.on('pm:create', async (e) => {
+ if (e.layer && e.layer.pm) {
+ const shape = e;
+ context.map.removeLayer(shape.layer);
+
+ const { lat: latitude, lng: longitude } = shape.layer.getLatLng();
+ const chatRoom = await api.post('/chat_rooms', {
+ latitude,
+ longitude,
+ radius: shape.layer.getRadius(),
+ });
+ reRender();
+ }
+ });
+
+ leafletContainer.on('pm:remove', (e) => {
+ console.log('object removed');
+ // console.log(leafletContainer.pm.getGeomanLayers(true).toGeoJSON());
+ });
+
+ return () => {
+ leafletContainer.pm.removeControls();
+ leafletContainer.pm.setGlobalOptions({ pmIgnore: true });
+ };
+ }, [context]);
+
+ return null;
+};
+
+export default Geoman;
diff --git a/client/components/map/legend.jsx b/client/components/map/legend.jsx
new file mode 100644
index 0000000..ebd199d
--- /dev/null
+++ b/client/components/map/legend.jsx
@@ -0,0 +1,28 @@
+import L from 'leaflet';
+import { useEffect } from 'react';
+import { useLeafletContext } from '@react-leaflet/core';
+
+export const Legend = () => {
+ const context = useLeafletContext();
+ useEffect(() => {
+ const legend = L.control({ position: 'topright' });
+
+ legend.onAdd = () => {
+ const div = L.DomUtil.create('div', 'info legend');
+ let labels = [];
+
+ labels.push('<i style="background:black"></i><span>Current position</span>');
+ labels.push('<i style="background:red"></i><span>Unjoinable</span>');
+ labels.push('<i style="background:green"></i><span>Joinable</span>');
+ labels.push('<i style="background:blue"></i><span>Editable</span>');
+
+ div.innerHTML = labels.join('<br>');
+ return div;
+ };
+
+ const { map } = context;
+ legend.addTo(map);
+ }, [context]);
+
+ return null;
+};
diff --git a/client/components/router.jsx b/client/components/router.jsx
index 08bb41f..544a15f 100644
--- a/client/components/router.jsx
+++ b/client/components/router.jsx
@@ -5,6 +5,7 @@ import { AuthContext } from '../utils/auth_context';
import { SignIn } from './sign_in/_sign_in';
import { SignUp } from './sign_up/_sign_up';
import { Admin } from './admin/_admin';
+import { Map } from './map/_map';
export const Router = () => {
const [authToken] = useContext(AuthContext);
@@ -18,6 +19,7 @@ export const Router = () => {
<Route path="admin" element={<Admin />} />
<Route path="signin" element={<SignIn />} />
<Route path="signup" element={<SignUp />} />
+ <Route path="map" element={<Map />} />
</Routes>
);
};