Map.js 17 KB

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