diff options
author | Logan Hunt <loganhunt@simponic.xyz> | 2022-03-30 15:18:16 -0600 |
---|---|---|
committer | Logan Hunt <loganhunt@simponic.xyz> | 2022-03-30 15:18:16 -0600 |
commit | acff469ba069b6f090adfd5ed91379c9f146aa77 (patch) | |
tree | e600e951d2e88ffde9252214fe31b8042ca129aa /client/components | |
parent | 042e3b9862b253fb3c3e59ee628dd9e30edf7e35 (diff) | |
download | locchat-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.jsx | 27 | ||||
-rw-r--r-- | client/components/map/_map.jsx | 47 | ||||
-rw-r--r-- | client/components/map/chat_room_geoman.jsx | 175 | ||||
-rw-r--r-- | client/components/map/legend.jsx | 28 | ||||
-rw-r--r-- | client/components/router.jsx | 2 |
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='© OpenStreetMap | © <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> ); }; |