Map.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. import React, { useEffect, useRef, useState } from 'react';
  2. import {Image, StyleSheet, View, TouchableOpacity, Text, Alert, Animated,} from 'react-native';
  3. import MapboxGL from '@react-native-mapbox-gl/maps';
  4. import 'react-native-get-random-values'
  5. import Modal from 'react-native-modal';
  6. import SlidingUpPanel from 'rn-sliding-up-panel';
  7. import LandmarkDetails from '../components/LandmarkDetails';
  8. import {Icons} from '../globals.js';
  9. import Icon from 'react-native-vector-icons/FontAwesome';
  10. import VoiceView from '../components/VoiceView';
  11. import RNLocation from 'react-native-location';
  12. import { polygon, point, lineString, booleanPointInPolygon, booleanIntersects, buffer } from '@turf/turf';
  13. import polyline from '@mapbox/polyline';
  14. import PlaceDetails from '../components/PlaceDetails';
  15. import axios from 'axios';
  16. import { useMapState } from '../contexts/MapContext';
  17. import { useAuthState } from '../contexts/AuthContext';
  18. import { API_URL } from '../globals';
  19. import { adaptIcon } from '../globals';
  20. MapboxGL.setAccessToken('pk.eyJ1IjoiY2Rtb3NzIiwiYSI6ImNrbmhuOXJzcDIyd20ycW1pYm8xaGI0aGUifQ.j04Sp636N9Wg4N9j9t2tXw');
  21. const directionsUrl = "https://maps.googleapis.com/maps/api/directions/json?mode=walking&alternatives=true&key=AIzaSyD06YUMazlb4Fu0Q81y_YNyEBz8PmtZyeY";
  22. RNLocation.configure({
  23. distanceFilter: 1
  24. });
  25. const Map = ({navigation}) => {
  26. // state
  27. const [tempPoint, setTempPoint] = useState(null);
  28. const [placesIsVisible, togglePlacesModal] = useState(false);
  29. const [addIsVisible, toggleAddModal] = useState(false);
  30. const [lmDetailsIsVisible, toggleLMDetailsModal] = useState(false);
  31. const [routePoints, setRoutePoints] = useState({first: null, second: null});
  32. const [routes, setRoutes] = useState([]);
  33. const [routingActive, toggleRouting] = useState(false);
  34. // refs
  35. const voiceModal = useRef();
  36. const map = useRef();
  37. const panel = useRef();
  38. // contexts
  39. const { mapState, mapDispatch } = useMapState();
  40. const { authState, authDispatch } = useAuthState();
  41. let panelPosition = new Animated.Value(0);
  42. useEffect(() => {
  43. async function handleLocation() {
  44. if (mapState.location == null && !mapState.locationPermission) {
  45. mapDispatch({type: 'UPDATE_LOCATION_PERMISSION', payload: await RNLocation.requestPermission({
  46. ios: "whenInUse",
  47. android: {
  48. detail: "fine",
  49. rationale: {
  50. title: "We need to access your location",
  51. message: "We use your location to provide convenient features such as geo-activated notifications and adding landmarks via voice",
  52. buttonPositive: "Grant permission",
  53. buttonNegative: "No thanks"
  54. }
  55. }
  56. })})
  57. }
  58. }
  59. handleLocation();
  60. const panelPositionListener = panelPosition.addListener(({value}) => {
  61. if (value == 0) {
  62. toggleRouting(false);
  63. }
  64. if (value > 0) {
  65. toggleRouting(true);
  66. }
  67. })
  68. return() => {
  69. panelPosition.removeListener(panelPositionListener)
  70. // unsubscribePermission();
  71. // unsubscribeLocation();
  72. }
  73. });
  74. const changeSelectedLandmark = (landmark) => {
  75. mapDispatch({ type:"UPDATE_SELECTED_LANDMARK", payload: landmark});
  76. }
  77. const showLandmarkDetails = (landmark) => {
  78. console.log(landmark)
  79. // changeSelectedLandmark(landmark);
  80. // toggleLMDetailsModal(true);
  81. }
  82. const openVoiceModal = () => {
  83. if (!mapState.locationPermission) {
  84. Alert.alert('You need to provide location permission to use this feature.')
  85. }
  86. else {
  87. RNLocation.getLatestLocation({timeout: 100}).then(location => {
  88. mapDispatch({ type:"UPDATE_LOCATION", payload: location});
  89. voiceModal.current.open();
  90. })
  91. }
  92. }
  93. const handleMapTouch = (e) => {
  94. if (routingActive) {
  95. setRoutePoint(e.geometry.coordinates);
  96. }
  97. else {
  98. setTempPoint(e.geometry.coordinates);
  99. // sets selected landmark with barebones object to prime for new landmark
  100. mapDispatch({ type:"UPDATE_SELECTED_LANDMARK", payload: {
  101. longitude: e.geometry.coordinates[0],
  102. latitude: e.geometry.coordinates[1],
  103. icon: 'barrier',
  104. title: '',
  105. desc: ''
  106. }});
  107. toggleAddModal(true);
  108. }
  109. }
  110. const handlePlaceTouch = (e) => {
  111. if (routingActive) {
  112. setRoutePoint([e.coordinates.longitude, e.coordinates.latitude]);
  113. }
  114. else {
  115. setTempPoint([e.coordinates.longitude, e.coordinates.latitude]);
  116. // gathers all places overlapping point that was touched
  117. const touchedPlaces = e.features.filter(f => {
  118. return booleanPointInPolygon(point([e.coordinates.longitude, e.coordinates.latitude]), polygon(f.geometry.coordinates));
  119. });
  120. // set selected landmark to barebones to prime for adding a landmark
  121. mapDispatch({ type:"UPDATE_SELECTED_LANDMARK", payload: {
  122. longitude: e.coordinates.longitude,
  123. latitude: e.coordinates.latitude,
  124. icon: 'barrier',
  125. title: 'New Landmark',
  126. desc: ''
  127. }});
  128. if (touchedPlaces.length == 0) {
  129. toggleAddModal(true);
  130. }
  131. // otherwise set state with gathered places and show places modal
  132. else {
  133. mapDispatch({ type:"UPDATE_SELECTED_PLACES", payload: touchedPlaces});
  134. mapDispatch({ type:"UPDATE_SELECTED_PLACE", payload: {
  135. id: touchedPlaces[0].id,
  136. name: touchedPlaces[0].properties.name,
  137. desc: touchedPlaces[0].properties.desc,
  138. tips: touchedPlaces[0].properties.tips,
  139. dateAdded: touchedPlaces[0].properties.dateAdded,
  140. postedBy: touchedPlaces[0].properties.postedBy,
  141. rating: touchedPlaces[0].properties.rating,
  142. }});
  143. togglePlacesModal(true);
  144. }
  145. }
  146. }
  147. const setRoutePoint = (point) => {
  148. if (routePoints.first) {
  149. setRoutePoints({...routePoints, second: point});
  150. }
  151. else {
  152. setRoutePoints({first: point});
  153. }
  154. }
  155. const buildRoutingUrl = async () => {
  156. const routingUrl = directionsUrl + "&origin=" + routePoints.first[1] + ',' + routePoints.first[0] + '&destination=' + routePoints.second[1] + ',' + routePoints.second[0];
  157. const response = await axios.get(routingUrl);
  158. let routes = response.data.routes.map((route, i) => {
  159. return {
  160. type: 'Feature',
  161. properties: {
  162. color: i == 0 ? '#007FFF' : '#888888',
  163. opacity: i == 0 ? 1 : 0.6
  164. },
  165. geometry: {
  166. coordinates: polyline.decode(route.overview_polyline.points).map(coords => [coords[1], coords[0]]),
  167. type: 'LineString'
  168. }
  169. }
  170. });
  171. mapState.landmarks.forEach(landmark => {
  172. const lmBuffer = buffer(point([landmark.longitude, landmark.latitude]), 10, {units: 'meters'}).geometry.coordinates
  173. routes = routes.map(route => {
  174. if (booleanIntersects(lineString(route.geometry.coordinates), polygon(lmBuffer))) {
  175. console.log(route);
  176. return {...route, properties: {color: 'red', opacity: .5,}};
  177. }
  178. return route;
  179. });
  180. })
  181. setRoutes(routes);
  182. }
  183. const clearRoutes = () => {
  184. setRoutePoints({first: null, second: null});
  185. setRoutes([]);
  186. }
  187. const handleDrag = (value) => {
  188. console.log(value);
  189. }
  190. const addLandmark = () => {
  191. clearModals();
  192. console.log(tempPoint);
  193. navigation.navigate("LandmarkForm");
  194. }
  195. const removeTempPoint = () => {
  196. setTempPoint(null);
  197. }
  198. const editLandmark = () => {
  199. navigation.navigate("LandmarkForm");
  200. }
  201. const closeModal = (message) => {
  202. toggleLMDetailsModal(false);
  203. if (message != null) {
  204. Alert.alert(message);
  205. }
  206. };
  207. const clearModals = () => {
  208. toggleLMDetailsModal(false);
  209. toggleAddModal(false);
  210. togglePlacesModal(false);
  211. };
  212. const updateDisplayedLandmarks = async (e) => {
  213. const neCorner = e.properties.visibleBounds[0];
  214. const swCorner = e.properties.visibleBounds[1];
  215. try {
  216. const response = await axios.get(`${API_URL}/api/landmarkinregion/${neCorner[0]}/${neCorner[1]}/${swCorner[0]}/${swCorner[1]}`, {
  217. headers: {
  218. "Authorization": "Bearer " + authState.accessToken
  219. }
  220. });
  221. const landmarks = JSON.parse(response.data.landmarks);
  222. mapDispatch({type: "UPDATE_LANDMARKS", payload: landmarks.data.features})
  223. //console.log(landmarks.data.features[0])
  224. } catch (error) {
  225. console.log(error.response.data);
  226. }
  227. }
  228. return(
  229. <View style={styles.container}>
  230. <View style={styles.mapHeader}>
  231. <TouchableOpacity style={{width: '25%', height: '100%', justifyContent: 'center'}} onPress={() => {clearModals(); navigation.jumpTo('Account');}}>
  232. <Image style={{width: 40, height: 40, borderRadius: 100, marginLeft: 20}} source={require('../assets/default-pfp.png')}/>
  233. </TouchableOpacity>
  234. <Text style={{color: 'white', fontSize: 20, marginRight: 20}}>atlas</Text>
  235. </View>
  236. <View style={styles.mapContainer}>
  237. <MapboxGL.MapView
  238. ref={map}
  239. style={styles.mapbox}
  240. onRegionIsChanging={updateDisplayedLandmarks}
  241. onPress={e => handleMapTouch(e)}>
  242. {/* <MapboxGL.UserLocation/> */}
  243. {/* <MapboxGL.Camera followUserLocation followUserMode={'normal'} zoomLevel={9} /> */}
  244. <MapboxGL.Camera zoomLevel={9} centerCoordinate={[-113.52511882781982,53.52385492230552]} />
  245. {tempPoint &&
  246. <MapboxGL.PointAnnotation id="temp" coordinate={tempPoint} />
  247. }
  248. {routePoints.first &&
  249. <MapboxGL.PointAnnotation id="first" coordinate={routePoints.first} />
  250. }
  251. {routePoints.second &&
  252. <MapboxGL.PointAnnotation id="second" coordinate={routePoints.second} />
  253. }
  254. {mapState.landmarks.map(landmark => {
  255. return (
  256. <MapboxGL.PointAnnotation onSelected={() => console.log(landmark)} key={landmark.properties.id} id={landmark.properties.id} coordinate={[landmark.geometry.coordinates[0], landmark.geometry.coordinates[1]]} >
  257. <Image source={Icons[adaptIcon(landmark.properties.icon)]} style={{width: 22, height: 30}} />
  258. </MapboxGL.PointAnnotation>
  259. )})}
  260. <MapboxGL.ShapeSource
  261. onPress={e => handlePlaceTouch(e)}
  262. fill
  263. id="test"
  264. shape={{type: "FeatureCollection", features: mapState.places.map(place => {
  265. return {
  266. type: 'Feature',
  267. id: place.id,
  268. properties: {
  269. name: place.name,
  270. tips: place.tips,
  271. rating: place.rating,
  272. postedBy: place.postedBy,
  273. dateAdded: place.dateAdded,
  274. color: place.color,
  275. },
  276. geometry: {
  277. "type": "Polygon",
  278. "coordinates": place.coordinates
  279. }
  280. }
  281. })}}>
  282. <MapboxGL.FillLayer
  283. id="places"
  284. style={{
  285. fillColor: ["get", "color"],
  286. fillOpacity: .3}}/>
  287. </MapboxGL.ShapeSource>
  288. <MapboxGL.ShapeSource
  289. onPress={e => handlePlaceTouch(e)}
  290. fill
  291. id="buffers"
  292. shape={{type: "FeatureCollection", features: mapState.landmarks.map(landmark => {
  293. return {
  294. type: 'Feature',
  295. id: landmark.properties.id,
  296. properties: {
  297. },
  298. geometry: buffer(point([landmark.geometry.coordinates[0], landmark.geometry.coordinates[1]]), 10, {units: "meters"}).geometry
  299. }
  300. })}}>
  301. <MapboxGL.FillLayer
  302. id="buffers"
  303. style={{
  304. fillColor: 'black',
  305. fillOpacity: .3}}/>
  306. </MapboxGL.ShapeSource>
  307. <MapboxGL.ShapeSource
  308. id="line"
  309. shape={{type: "FeatureCollection", features: routes}}>
  310. <MapboxGL.LineLayer
  311. id="lines"
  312. style={{lineWidth: 7, lineColor: ["get", "color"], lineOpacity: ['get', 'opacity'], lineCap: 'round', lineJoin: 'round'}} />
  313. </MapboxGL.ShapeSource>
  314. </MapboxGL.MapView>
  315. {!routingActive ?
  316. <TouchableOpacity style={styles.routeBtn} onPress={() => panel.current.show(100)}>
  317. <Icon name='route' size={20} name="map" color='white' />
  318. </TouchableOpacity> : null}
  319. <TouchableOpacity style={styles.micBtn} name="microphone" onPress={openVoiceModal}>
  320. <Icon name='microphone' size={20} color='white' />
  321. </TouchableOpacity>
  322. </View>
  323. {mapState.locationPermission ? <VoiceView ref={voiceModal} changeSelectedLandmark={changeSelectedLandmark} editLandmark={editLandmark} style={styles.voiceModal} onPress={openVoiceModal}/> : null}
  324. <Modal
  325. isVisible={lmDetailsIsVisible}
  326. style={{height: 520, margin: 0, justifyContent: 'flex-end'}}
  327. onBackButtonPress={() => toggleLMDetailsModal(false)}
  328. onBackdropPress={() => toggleLMDetailsModal(false)}
  329. onSwipeComplete={() => toggleLMDetailsModal(false)}
  330. swipeDirection={['up', 'down']}>
  331. <LandmarkDetails closeModal={clearModals} editLandmark={editLandmark}/>
  332. </Modal>
  333. <Modal
  334. style={{height: 520, margin: 0, justifyContent: 'flex-end',}}
  335. isVisible={placesIsVisible}
  336. onBackButtonPress={() => togglePlacesModal(false)}
  337. onBackdropPress={() => togglePlacesModal(false)}
  338. onSwipeComplete={() => togglePlacesModal(false)}
  339. swipeDirection={['up', 'down']}
  340. onModalWillHide={removeTempPoint}>
  341. <PlaceDetails closeModal={clearModals} addLandmark={addLandmark}/>
  342. </Modal>
  343. <Modal
  344. style={{justifyContent: 'center',}}
  345. isVisible={addIsVisible}
  346. onBackButtonPress={() => toggleAddModal(false)}
  347. onBackdropPress={() => toggleAddModal(false)}
  348. onSwipeComplete={() => toggleAddModal(false)}
  349. swipeDirection={['up','down']}
  350. onModalWillHide={removeTempPoint}>
  351. <View style={{maxHeight: 100, backgroundColor: '#df3f3f'}}>
  352. <Text style={styles.addTitle}>Add landmark here?</Text>
  353. <View style={{flexDirection: 'row', justifyContent: 'flex-end', alignItems: 'center', }}>
  354. <TouchableOpacity style={{marginRight: 40, height: 50}} onPress={() => toggleAddModal(false)}>
  355. <Text style={{color: 'white'}}>Cancel</Text>
  356. </TouchableOpacity>
  357. <TouchableOpacity style={{marginRight: 40, height: 50}} onPress={addLandmark}>
  358. <Text style={{color: 'white'}}>Add</Text>
  359. </TouchableOpacity>
  360. </View>
  361. </View>
  362. </Modal>
  363. <SlidingUpPanel
  364. animatedValue={panelPosition}
  365. ref={panel}
  366. showBackdrop={false}
  367. allowDragging={false}
  368. onDragEnd={() => console.log('test')}
  369. onMomentumDragEnd={() => console.log('test')}
  370. draggableRange={{top: 100, bottom: 0}}>
  371. <View style={{padding: 20, paddingRight: 10, height: 100,backgroundColor: '#df3f3f'}}>
  372. <Text style={{color: 'white'}}>Select points</Text>
  373. {routePoints.first || routePoints.second ?
  374. <View style={{flexDirection: 'row', marginTop: 20, justifyContent: 'flex-end'}}>
  375. {routePoints.first && routePoints.second ?
  376. <TouchableOpacity onPress={buildRoutingUrl} style={{backgroundColor: 'white', alignSelf: 'flex-end', alignItems: 'center', justifyContent: 'center', width: "40%", height: 30, marginRight: 10}}>
  377. <Text style={{color: 'black'}}>Show directions</Text>
  378. </TouchableOpacity> : null }
  379. <TouchableOpacity onPress={clearRoutes} style={{backgroundColor: 'white', alignSelf: 'flex-end', alignItems: 'center', justifyContent: 'center', width: "40%", height: 30,}}>
  380. <Text style={{color: 'black'}}>Clear</Text>
  381. </TouchableOpacity>
  382. </View> : null }
  383. </View>
  384. </SlidingUpPanel>
  385. </View>
  386. )
  387. }
  388. const styles = StyleSheet.create({
  389. container: {
  390. flex: 1
  391. },
  392. mapHeader: {
  393. justifyContent: 'space-between',
  394. flexDirection: 'row',
  395. alignItems: 'center',
  396. height: '9%',
  397. zIndex: 5,
  398. backgroundColor: '#df3f3f',
  399. },
  400. mapContainer: {
  401. height: '100%',
  402. width: '100%',
  403. backgroundColor: 'white',
  404. },
  405. markerContainer: {
  406. height: 50,
  407. width: 50,
  408. backgroundColor: 'white',
  409. },
  410. markerImg: {
  411. height: 25,
  412. width: 25
  413. },
  414. mapbox: {
  415. flex: 1,
  416. },
  417. routeBtn: {
  418. position: 'absolute',
  419. backgroundColor: '#df3f3f',
  420. justifyContent: 'center',
  421. alignItems: 'center',
  422. borderRadius: 40,
  423. bottom: 150,
  424. right: 20,
  425. zIndex: 5,
  426. width: 60,
  427. height: 60,
  428. },
  429. micBtn: {
  430. position: 'absolute',
  431. backgroundColor: '#df3f3f',
  432. justifyContent: 'center',
  433. alignItems: 'center',
  434. borderRadius: 40,
  435. bottom: 70,
  436. right: 20,
  437. zIndex: 5,
  438. width: 60,
  439. height: 60,
  440. },
  441. addModal: {
  442. backgroundColor: '#df3f3f',
  443. height: 100,
  444. },
  445. voiceModal: {
  446. backgroundColor: '#df3f3f',
  447. height: 200,
  448. },
  449. addTitle: {
  450. color: 'white',
  451. fontSize: 13,
  452. marginTop: 20,
  453. marginLeft: 20
  454. },
  455. placesTitle: {
  456. fontSize: 13,
  457. color: 'white',
  458. margin: 4,
  459. alignSelf: 'center'
  460. },
  461. commentContainer: {
  462. padding: 10
  463. },
  464. commentHeader: {
  465. flexDirection: 'row',
  466. justifyContent: 'space-between'
  467. },
  468. commentBody: {
  469. padding: 10
  470. },
  471. commentInputContainer: {
  472. paddingHorizontal: 10,
  473. backgroundColor: 'white',
  474. flexDirection: 'row',
  475. justifyContent: 'space-between',
  476. alignItems: 'center',
  477. },
  478. addComment: {
  479. width: '100%',
  480. borderTopWidth: 1,
  481. borderColor: 'grey',
  482. backgroundColor: 'white',
  483. },
  484. })
  485. export default Map;