MapContainer.js 16 KB

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