From acff469ba069b6f090adfd5ed91379c9f146aa77 Mon Sep 17 00:00:00 2001 From: Logan Hunt Date: Wed, 30 Mar 2022 15:18:16 -0600 Subject: [PATCH] Ability to add, remove, update radius and location of chatrooms with a leaflet --- client/app.css | 35 ++- client/app.jsx | 3 + client/components/home/_home.jsx | 27 +- client/components/map/_map.jsx | 47 ++++ client/components/map/chat_room_geoman.jsx | 175 +++++++++++++ client/components/map/legend.jsx | 28 +++ client/components/router.jsx | 2 + client/files/marker.png | Bin 0 -> 9779 bytes client/package.json | 4 + client/yarn.lock | 233 +++++++++++++++++- server/app.module.ts | 5 +- server/controllers/chat_room.controller.ts | 51 +++- server/controllers/users.controller.ts | 1 + .../migrations/1648605030863-AddChatRoom.ts | 20 +- .../1648669551959-AddDistanceFunction.ts | 45 ++++ server/entities/chat_room.entity.ts | 9 +- server/entities/user.entity.ts | 4 + server/modules/chat_room.module.ts | 8 +- server/modules/users.module.ts | 3 +- .../providers/services/chat_room.service.ts | 18 +- 20 files changed, 683 insertions(+), 35 deletions(-) create mode 100644 client/components/map/_map.jsx create mode 100644 client/components/map/chat_room_geoman.jsx create mode 100644 client/components/map/legend.jsx create mode 100644 client/files/marker.png create mode 100644 server/database/migrations/1648669551959-AddDistanceFunction.ts diff --git a/client/app.css b/client/app.css index 8a90c80..63eab6b 100644 --- a/client/app.css +++ b/client/app.css @@ -2,4 +2,37 @@ @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; + +.leaflet-container { + height: 100%; + width: 100%; +} + +.info { + padding: 6px 8px; + font: 14px/16px Arial, Helvetica, sans-serif; + background: white; + background: rgba(255, 255, 255, 0.8); + box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); + border-radius: 5px; +} + +.info h4 { + margin: 0 0 5px; + color: #777; +} + +.legend { + text-align: left; + line-height: 18px; + color: #555; +} + +.legend i { + width: 18px; + height: 18px; + float: left; + margin-right: 8px; + opacity: 0.7; +} diff --git a/client/app.jsx b/client/app.jsx index c92df86..e69b297 100644 --- a/client/app.jsx +++ b/client/app.jsx @@ -7,7 +7,9 @@ import { useApi } from './utils/use_api'; import { useJwtRefresh } from './utils/use_jwt_refresh'; import { RolesContext } from './utils/roles_context'; import { parseJwt } from './utils/parse_jwt'; +import { Toaster } from 'react-hot-toast'; import './app.css'; +import 'leaflet/dist/leaflet.css'; export const App = () => { const [authToken, setAuthToken] = useState(null); @@ -38,6 +40,7 @@ export const App = () => { + 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 ( -
-

Welcome {user.firstName}

- - {roles.includes('admin') && ( - - )} -
- -
-
+ {roles.includes('admin') && ( + + )} + + + ); }; 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 ( + + + + + + ); + } + return
Getting current location...
; +}; 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('Current position'); + labels.push('Unjoinable'); + labels.push('Joinable'); + labels.push('Editable'); + + div.innerHTML = labels.join('
'); + 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 = () => { } /> } /> } /> + } /> ); }; diff --git a/client/files/marker.png b/client/files/marker.png new file mode 100644 index 0000000000000000000000000000000000000000..ec898df1f9d2829b86a3ad67e7452ca402665843 GIT binary patch literal 9779 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 literal 0 HcmV?d00001 diff --git a/client/package.json b/client/package.json index 6dd0978..75e8bf0 100644 --- a/client/package.json +++ b/client/package.json @@ -15,8 +15,12 @@ "tailwindcss": "^2.2.19" }, "dependencies": { + "@geoman-io/leaflet-geoman-free": "^2.11.4", + "leaflet": "^1.7.1", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-hot-toast": "^2.2.0", + "react-leaflet": "^3.2.5", "react-router-dom": "6", "socket.io-client": "^4.4.1" } diff --git a/client/yarn.lock b/client/yarn.lock index ce2984f..801b087 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -30,6 +30,18 @@ dependencies: regenerator-runtime "^0.13.4" +"@geoman-io/leaflet-geoman-free@^2.11.4": + version "2.11.4" + resolved "https://registry.yarnpkg.com/@geoman-io/leaflet-geoman-free/-/leaflet-geoman-free-2.11.4.tgz#4a43fa8d3d5d2bca751135b775c19c6cc0063699" + integrity sha512-uWfgaGDhrtoCMHdHi2oNVKb8WXFMQvyNnan1sS/+Yn5jMPuhijWFyAjy0G5kTCamXhGXg4vUvlEpiRSrBwewKg== + dependencies: + "@turf/boolean-contains" "^6.5.0" + "@turf/kinks" "^6.5.0" + "@turf/line-intersect" "^6.5.0" + "@turf/line-split" "^6.5.0" + lodash "4.17.21" + polygon-clipping "0.15.3" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -603,6 +615,11 @@ chrome-trace-event "^1.0.2" nullthrows "^1.1.1" +"@react-leaflet/core@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@react-leaflet/core/-/core-1.1.1.tgz#827fd05bb542cf874116176d8ef48d5b12163f81" + integrity sha512-7PGLWa9MZ5x/cWy8EH2VzI4T8q5WpuHbixzCDXqixP/WyqwIrg5NDUPgYuFnB4IEIZF+6nA265mYzswFo/h1Pw== + "@socket.io/base64-arraybuffer@~1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#568d9beae00b0d835f4f8c53fd55714986492e61" @@ -623,6 +640,161 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== +"@turf/bbox@*", "@turf/bbox@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/bbox/-/bbox-6.5.0.tgz#bec30a744019eae420dac9ea46fb75caa44d8dc5" + integrity sha512-RBbLaao5hXTYyyg577iuMtDB8ehxMlUqHEJiMs8jT1GHkFhr6sYre3lmLsPeYEi/ZKj5TP5tt7fkzNdJ4GIVyw== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/meta" "^6.5.0" + +"@turf/bearing@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/bearing/-/bearing-6.5.0.tgz#462a053c6c644434bdb636b39f8f43fb0cd857b0" + integrity sha512-dxINYhIEMzgDOztyMZc20I7ssYVNEpSv04VbMo5YPQsqa80KO3TFvbuCahMsCAW5z8Tncc8dwBlEFrmRjJG33A== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + +"@turf/boolean-contains@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/boolean-contains/-/boolean-contains-6.5.0.tgz#f802e7432fb53109242d5bf57393ef2f53849bbf" + integrity sha512-4m8cJpbw+YQcKVGi8y0cHhBUnYT+QRfx6wzM4GI1IdtYH3p4oh/DOBJKrepQyiDzFDaNIjxuWXBh0ai1zVwOQQ== + dependencies: + "@turf/bbox" "^6.5.0" + "@turf/boolean-point-in-polygon" "^6.5.0" + "@turf/boolean-point-on-line" "^6.5.0" + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + +"@turf/boolean-point-in-polygon@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz#6d2e9c89de4cd2e4365004c1e51490b7795a63cf" + integrity sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + +"@turf/boolean-point-on-line@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/boolean-point-on-line/-/boolean-point-on-line-6.5.0.tgz#a8efa7bad88760676f395afb9980746bc5b376e9" + integrity sha512-A1BbuQ0LceLHvq7F/P7w3QvfpmZqbmViIUPHdNLvZimFNLo4e6IQunmzbe+8aSStH9QRZm3VOflyvNeXvvpZEQ== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + +"@turf/destination@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/destination/-/destination-6.5.0.tgz#30a84702f9677d076130e0440d3223ae503fdae1" + integrity sha512-4cnWQlNC8d1tItOz9B4pmJdWpXqS0vEvv65bI/Pj/genJnsL7evI0/Xw42RvEGROS481MPiU80xzvwxEvhQiMQ== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + +"@turf/distance@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/distance/-/distance-6.5.0.tgz#21f04d5f86e864d54e2abde16f35c15b4f36149a" + integrity sha512-xzykSLfoURec5qvQJcfifw/1mJa+5UwByZZ5TZ8iaqjGYN0vomhV9aiSLeYdUGtYRESZ+DYC/OzY+4RclZYgMg== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + +"@turf/helpers@6.x", "@turf/helpers@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.5.0.tgz#f79af094bd6b8ce7ed2bd3e089a8493ee6cae82e" + integrity sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw== + +"@turf/invariant@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/invariant/-/invariant-6.5.0.tgz#970afc988023e39c7ccab2341bd06979ddc7463f" + integrity sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg== + dependencies: + "@turf/helpers" "^6.5.0" + +"@turf/kinks@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/kinks/-/kinks-6.5.0.tgz#80e7456367535365012f658cf1a988b39a2c920b" + integrity sha512-ViCngdPt1eEL7hYUHR2eHR662GvCgTc35ZJFaNR6kRtr6D8plLaDju0FILeFFWSc+o8e3fwxZEJKmFj9IzPiIQ== + dependencies: + "@turf/helpers" "^6.5.0" + +"@turf/line-intersect@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/line-intersect/-/line-intersect-6.5.0.tgz#dea48348b30c093715d2195d2dd7524aee4cf020" + integrity sha512-CS6R1tZvVQD390G9Ea4pmpM6mJGPWoL82jD46y0q1KSor9s6HupMIo1kY4Ny+AEYQl9jd21V3Scz20eldpbTVA== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + "@turf/line-segment" "^6.5.0" + "@turf/meta" "^6.5.0" + geojson-rbush "3.x" + +"@turf/line-segment@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/line-segment/-/line-segment-6.5.0.tgz#ee73f3ffcb7c956203b64ed966d96af380a4dd65" + integrity sha512-jI625Ho4jSuJESNq66Mmi290ZJ5pPZiQZruPVpmHkUw257Pew0alMmb6YrqYNnLUuiVVONxAAKXUVeeUGtycfw== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + "@turf/meta" "^6.5.0" + +"@turf/line-split@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/line-split/-/line-split-6.5.0.tgz#116d7fbf714457878225187f5820ef98db7b02c2" + integrity sha512-/rwUMVr9OI2ccJjw7/6eTN53URtGThNSD5I0GgxyFXMtxWiloRJ9MTff8jBbtPWrRka/Sh2GkwucVRAEakx9Sw== + dependencies: + "@turf/bbox" "^6.5.0" + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + "@turf/line-intersect" "^6.5.0" + "@turf/line-segment" "^6.5.0" + "@turf/meta" "^6.5.0" + "@turf/nearest-point-on-line" "^6.5.0" + "@turf/square" "^6.5.0" + "@turf/truncate" "^6.5.0" + geojson-rbush "3.x" + +"@turf/meta@6.x", "@turf/meta@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/meta/-/meta-6.5.0.tgz#b725c3653c9f432133eaa04d3421f7e51e0418ca" + integrity sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA== + dependencies: + "@turf/helpers" "^6.5.0" + +"@turf/nearest-point-on-line@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/nearest-point-on-line/-/nearest-point-on-line-6.5.0.tgz#8e1cd2cdc0b5acaf4c8d8b3b33bb008d3cb99e7b" + integrity sha512-WthrvddddvmymnC+Vf7BrkHGbDOUu6Z3/6bFYUGv1kxw8tiZ6n83/VG6kHz4poHOfS0RaNflzXSkmCi64fLBlg== + dependencies: + "@turf/bearing" "^6.5.0" + "@turf/destination" "^6.5.0" + "@turf/distance" "^6.5.0" + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + "@turf/line-intersect" "^6.5.0" + "@turf/meta" "^6.5.0" + +"@turf/square@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/square/-/square-6.5.0.tgz#ab43eef99d39c36157ab5b80416bbeba1f6b2122" + integrity sha512-BM2UyWDmiuHCadVhHXKIx5CQQbNCpOxB6S/aCNOCLbhCeypKX5Q0Aosc5YcmCJgkwO5BERCC6Ee7NMbNB2vHmQ== + dependencies: + "@turf/distance" "^6.5.0" + "@turf/helpers" "^6.5.0" + +"@turf/truncate@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/truncate/-/truncate-6.5.0.tgz#c3a16cad959f1be1c5156157d5555c64b19185d8" + integrity sha512-pFxg71pLk+eJj134Z9yUoRhIi8vqnnKvCYwdT4x/DQl/19RVdq1tV3yqOT3gcTQNfniteylL5qV1uTBDV5sgrg== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/meta" "^6.5.0" + +"@types/geojson@7946.0.8": + version "7946.0.8" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.8.tgz#30744afdb385e2945e22f3b033f897f76b1f12ca" + integrity sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -1188,6 +1360,17 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +geojson-rbush@3.x: + version "3.2.0" + resolved "https://registry.yarnpkg.com/geojson-rbush/-/geojson-rbush-3.2.0.tgz#8b543cf0d56f99b78faf1da52bb66acad6dfc290" + integrity sha512-oVltQTXolxvsz1sZnutlSuLDEcQAKYC/uXt9zDzJJ6bu0W+baTI8LZBaTup5afzibEH4N3jlq2p+a152wlBJ7w== + dependencies: + "@turf/bbox" "*" + "@turf/helpers" "6.x" + "@turf/meta" "6.x" + "@types/geojson" "7946.0.8" + rbush "^3.0.1" + get-port@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119" @@ -1226,6 +1409,11 @@ globals@^13.2.0: dependencies: type-fest "^0.20.2" +goober@^2.1.1: + version "2.1.8" + resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.8.tgz#e592c04d093cb38f77b38cfcb012b7811c85765e" + integrity sha512-S0C85gCzcfFCMSdjD/CxyQMt1rbf2qEg6hmDzxk2FfD7+7Ogk55m8ZFUMtqNaZM4VVX/qaU9AzSORG+Gf4ZpAQ== + graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.8" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" @@ -1423,6 +1611,11 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +leaflet@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.7.1.tgz#10d684916edfe1bf41d688a3b97127c0322a2a19" + integrity sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw== + lilconfig@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082" @@ -1459,7 +1652,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.17.21: +lodash@4.17.21, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -1684,6 +1877,13 @@ picomatch@^2.2.3: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +polygon-clipping@0.15.3: + version "0.15.3" + resolved "https://registry.yarnpkg.com/polygon-clipping/-/polygon-clipping-0.15.3.tgz#0215840438470ba2e9e6593625e4ea5c1087b4b7" + integrity sha512-ho0Xx5DLkgxRx/+n4O74XyJ67DcyN3Tu9bGYKsnTukGAW6ssnuak6Mwcyb1wHy9MZc9xsUWqIoiazkZB5weECg== + dependencies: + splaytree "^3.1.0" + postcss-calc@^8.2.0: version "8.2.4" resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" @@ -2002,6 +2202,18 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== +quickselect@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" + integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== + +rbush@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/rbush/-/rbush-3.0.1.tgz#5fafa8a79b3b9afdfe5008403a720cc1de882ecf" + integrity sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w== + dependencies: + quickselect "^2.0.0" + react-dom@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" @@ -2011,6 +2223,20 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" +react-hot-toast@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/react-hot-toast/-/react-hot-toast-2.2.0.tgz#ab6f4caed4214b9534f94bb8cfaaf21b051e62b9" + integrity sha512-248rXw13uhf/6TNDVzagX+y7R8J183rp7MwUMNkcrBRyHj/jWOggfXTGlM8zAOuh701WyVW+eUaWG2LeSufX9g== + dependencies: + goober "^2.1.1" + +react-leaflet@^3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/react-leaflet/-/react-leaflet-3.2.5.tgz#bec0bfab9dd8c2b030ea630f7a0687a60322ca7d" + integrity sha512-Z3KZ+4SijsRbbrt2I1a3ZDY6+V6Pm91eYTdxTN18G6IOkFRsJo1BuSPLFnyFrlF3WDjQFPEcTPkEgD1VEeAoBg== + dependencies: + "@react-leaflet/core" "^1.1.1" + react-refresh@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf" @@ -2179,6 +2405,11 @@ source-map@~0.7.2: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +splaytree@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/splaytree/-/splaytree-3.1.1.tgz#e1bc8e68e64ef5a9d5f09d36e6d9f3621795a438" + integrity sha512-9FaQ18FF0+sZc/ieEeXHt+Jw2eSpUgUtTLDYB/HXKWvhYVyOc7h1hzkn5MMO3GPib9MmXG1go8+OsBBzs/NMww== + stable@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" diff --git a/server/app.module.ts b/server/app.module.ts index bbc3c1c..5fc4654 100644 --- a/server/app.module.ts +++ b/server/app.module.ts @@ -3,17 +3,19 @@ import { APP_GUARD } from '@nestjs/core'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { config } from './database/config'; +import { ChatRoomModule } from './modules/chat_room.module'; import { UsersModule } from './modules/users.module'; import { PingGateway } from './providers/gateways/ping.gateway'; import { AuthGuard } from './providers/guards/auth.guard'; import { RolesGuard } from './providers/guards/roles.guard'; +import { ChatRoomService } from './providers/services/chat_room.service'; import { JwtService } from './providers/services/jwt.service'; import { RolesService } from './providers/services/roles.service'; import { UsersService } from './providers/services/users.service'; import { GuardUtil } from './providers/util/guard.util'; @Module({ - imports: [TypeOrmModule.forRoot(config), UsersModule], + imports: [TypeOrmModule.forRoot(config), UsersModule, ChatRoomModule], controllers: [AppController], providers: [ PingGateway, @@ -21,6 +23,7 @@ import { GuardUtil } from './providers/util/guard.util'; RolesService, JwtService, GuardUtil, + ChatRoomService, { provide: APP_GUARD, useClass: AuthGuard }, // auth guard should come before roles guard { provide: APP_GUARD, useClass: RolesGuard }, // otherwise users won't be authenticated before roles check ], diff --git a/server/controllers/chat_room.controller.ts b/server/controllers/chat_room.controller.ts index 5a8da66..bd9ba4a 100644 --- a/server/controllers/chat_room.controller.ts +++ b/server/controllers/chat_room.controller.ts @@ -1,12 +1,55 @@ -import { Controller, Get } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; +import { JwtBody } from 'server/decorators/jwt_body.decorator'; +import { JwtBodyDto } from 'server/dto/jwt_body.dto'; import { ChatRoomService } from 'server/providers/services/chat_room.service'; +import { UsersService } from 'server/providers/services/users.service'; @Controller() export class ChatRoomController { - constructor(private chatRoomService: ChatRoomService) {} + constructor(private chatRoomService: ChatRoomService, private usersService: UsersService) {} @Get('/chat_rooms') - async get() { - return await this.chatRoomService.all(); + async get(@Query() query: any) { + console.log(query); + return await this.chatRoomService.near(query); + } + + @Post('/chat_rooms') + async create(@JwtBody() jwtBody: JwtBodyDto, @Body() chatRoom: any) { + chatRoom.user = await this.usersService.find(jwtBody.userId); + console.log(jwtBody); + return await this.chatRoomService.create(chatRoom); + } + + private async authorized(jwtBody: JwtBodyDto, chatRoom: any) { + const user = await this.usersService.find(jwtBody.userId); + if (user.id !== chatRoom.user.id) { + return { + error: 'You are not the owner of this chat room', + }; + } + return true; + } + + @Put('/chat_rooms/:id') + async update(@JwtBody() jwtBody: JwtBodyDto, @Param('id') id: number, @Body() chatRoom: any) { + console.log(id); + const chat_room = await this.chatRoomService.findById(id, ['user']); + if (!(await this.authorized(jwtBody, chat_room))) { + return chat_room; + } + chat_room.latitude = chatRoom.latitude; + chat_room.longitude = chatRoom.longitude; + chat_room.radius = chatRoom.radius; + return await this.chatRoomService.save(chat_room); + } + + @Delete('/chat_rooms/:id') + async delete(@JwtBody() jwtBody: JwtBodyDto, @Param('id') id: number) { + const chat_room = await this.chatRoomService.findById(id, ['user']); + if (!(await this.authorized(jwtBody, chat_room))) { + return false; + } + return await this.chatRoomService.remove(chat_room); } } diff --git a/server/controllers/users.controller.ts b/server/controllers/users.controller.ts index 652b5b6..82d6215 100644 --- a/server/controllers/users.controller.ts +++ b/server/controllers/users.controller.ts @@ -35,6 +35,7 @@ export class UsersController { @Get('/users/me') async getCurrentUser(@JwtBody() jwtBody: JwtBodyDto) { const user = await this.usersService.find(jwtBody.userId); + delete user.passwordHash; return { user }; } diff --git a/server/database/migrations/1648605030863-AddChatRoom.ts b/server/database/migrations/1648605030863-AddChatRoom.ts index d8eed52..e4b5ca9 100644 --- a/server/database/migrations/1648605030863-AddChatRoom.ts +++ b/server/database/migrations/1648605030863-AddChatRoom.ts @@ -1,10 +1,10 @@ -import { MigrationInterface, QueryRunner, Table } from 'typeorm'; +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; export class AddChatRoom1648605030863 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.createTable( new Table({ - name: 'chatroom', + name: 'chat_room', columns: [ { name: 'id', @@ -13,8 +13,8 @@ export class AddChatRoom1648605030863 implements MigrationInterface { isGenerated: true, }, { - name: 'name', - type: 'text', + name: 'userId', + type: 'int', isNullable: false, }, { @@ -35,9 +35,19 @@ export class AddChatRoom1648605030863 implements MigrationInterface { ], }), ); + + await queryRunner.createForeignKey( + 'chat_room', + new TableForeignKey({ + columnNames: ['userId'], + referencedColumnNames: ['id'], + referencedTableName: 'user', + onDelete: 'CASCADE', + }), + ); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropTable('chatroom'); + await queryRunner.dropTable('chat_room'); } } diff --git a/server/database/migrations/1648669551959-AddDistanceFunction.ts b/server/database/migrations/1648669551959-AddDistanceFunction.ts new file mode 100644 index 0000000..0890936 --- /dev/null +++ b/server/database/migrations/1648669551959-AddDistanceFunction.ts @@ -0,0 +1,45 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddDistanceFunction1648669551959 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // From https://stackoverflow.com/questions/61135374/postgresql-calculate-distance-between-two-points-without-using-postgis + await queryRunner.query(` +CREATE OR REPLACE FUNCTION calculate_distance(lat1 float, lon1 float, lat2 float, lon2 float, units varchar) + RETURNS float AS $dist$ + DECLARE + dist float = 0; + radlat1 float; + radlat2 float; + theta float; + radtheta float; + BEGIN + IF lat1 = lat2 OR lon1 = lon2 + THEN RETURN dist; + ELSE + radlat1 = pi() * lat1 / 180; + radlat2 = pi() * lat2 / 180; + theta = lon1 - lon2; + radtheta = pi() * theta / 180; + dist = sin(radlat1) * sin(radlat2) + cos(radlat1) * cos(radlat2) * cos(radtheta); + + IF dist > 1 THEN dist = 1; END IF; + + dist = acos(dist); + dist = dist * 180 / pi(); + dist = dist * 60 * 1.1515; + + IF units = 'K' THEN dist = dist * 1.609344; END IF; + IF units = 'N' THEN dist = dist * 0.8684; END IF; + + RETURN dist; + END IF; + END; + $dist$ LANGUAGE plpgsql;`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP FUNCTION IF EXISTS calculate_distance(lat1 float, lon1 float, lat2 float, lon2 float, units varchar);`, + ); + } +} diff --git a/server/entities/chat_room.entity.ts b/server/entities/chat_room.entity.ts index 9550d26..6f46c97 100644 --- a/server/entities/chat_room.entity.ts +++ b/server/entities/chat_room.entity.ts @@ -1,13 +1,11 @@ -import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user.entity'; @Entity() export class ChatRoom { @PrimaryGeneratedColumn() id: number; - @Column() - name: string; - @Column() latitude: number; @@ -16,4 +14,7 @@ export class ChatRoom { @Column() radius: number; + + @ManyToOne(() => User, (user) => user.chatRooms) + user: User; } diff --git a/server/entities/user.entity.ts b/server/entities/user.entity.ts index aeef107..c3ece96 100644 --- a/server/entities/user.entity.ts +++ b/server/entities/user.entity.ts @@ -1,4 +1,5 @@ import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; +import { ChatRoom } from './chat_room.entity'; import { RefreshToken } from './refresh_token.entity'; import { UserRole } from './user_role.entity'; @@ -24,4 +25,7 @@ export class User { @OneToMany(() => UserRole, (userRole) => userRole.user, { cascade: true }) userRoles: UserRole[]; + + @OneToMany(() => ChatRoom, (chatRoom) => chatRoom.user) + chatRooms: ChatRoom[]; } diff --git a/server/modules/chat_room.module.ts b/server/modules/chat_room.module.ts index 07b2553..7acc672 100644 --- a/server/modules/chat_room.module.ts +++ b/server/modules/chat_room.module.ts @@ -3,11 +3,13 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ChatRoomController } from 'server/controllers/chat_room.controller'; import { ChatRoom } from 'server/entities/chat_room.entity'; import { ChatRoomService } from 'server/providers/services/chat_room.service'; +import { UsersService } from 'server/providers/services/users.service'; +import { UsersModule } from './users.module'; @Module({ - imports: [TypeOrmModule.forFeature([ChatRoom])], + imports: [TypeOrmModule.forFeature([ChatRoom]), UsersModule], controllers: [ChatRoomController], - providers: [ChatRoomService], + providers: [ChatRoomService, UsersService], exports: [TypeOrmModule], }) -export class UsersModule {} +export class ChatRoomModule {} diff --git a/server/modules/users.module.ts b/server/modules/users.module.ts index 69c533b..5b5b2ff 100644 --- a/server/modules/users.module.ts +++ b/server/modules/users.module.ts @@ -11,9 +11,10 @@ import { RefreshTokensController } from 'server/controllers/refresh_tokens.contr import { Role } from 'server/entities/role.entity'; import { RolesService } from 'server/providers/services/roles.service'; import { UserRole } from 'server/entities/user_role.entity'; +import { ChatRoom } from 'server/entities/chat_room.entity'; @Module({ - imports: [TypeOrmModule.forFeature([User, RefreshToken, Role, UserRole])], + imports: [TypeOrmModule.forFeature([User, RefreshToken, Role, UserRole, ChatRoom])], controllers: [SessionsController, UsersController, RefreshTokensController], providers: [UsersService, RolesService, RefreshTokensService, JwtService], exports: [TypeOrmModule], diff --git a/server/providers/services/chat_room.service.ts b/server/providers/services/chat_room.service.ts index 6c87539..a6af023 100644 --- a/server/providers/services/chat_room.service.ts +++ b/server/providers/services/chat_room.service.ts @@ -18,7 +18,21 @@ export class ChatRoomService { return this.chatRoomRepository.find(); } - findById(id: number) { - return this.chatRoomRepository.findOne(id); + near({ lat, lng }: { lat: number; lng: number }) { + return this.chatRoomRepository.query( + `SELECT * FROM chat_room WHERE calculate_distance(latitude, longitude, ${lat}, ${lng}, 'M') < 5`, + ); + } + + findById(id: number, relations: string[] = []) { + return this.chatRoomRepository.findOne(id, { relations }); + } + + save(chatRoom: ChatRoom) { + return this.chatRoomRepository.save(chatRoom); + } + + remove(chatRoom: ChatRoom) { + return this.chatRoomRepository.remove(chatRoom); } }