index.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875
  1. import {
  2. Animated as Animation,
  3. Dimensions,
  4. Linking,
  5. Platform,
  6. Text,
  7. TextInput,
  8. TouchableOpacity,
  9. View
  10. } from 'react-native';
  11. import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
  12. import MapView, { Geojson, Marker, UrlTile } from 'react-native-maps';
  13. import * as turf from '@turf/turf';
  14. import * as FileSystem from 'expo-file-system';
  15. import * as Location from 'expo-location';
  16. import { storage, StoreType } from '../../../storage';
  17. import MenuIcon from '../../../../assets/icons/menu.svg';
  18. import SearchIcon from '../../../../assets/icons/search.svg';
  19. import LocationIcon from '../../../../assets/icons/location.svg';
  20. import CloseSvg from '../../../../assets/icons/close.svg';
  21. import FilterIcon from 'assets/icons/filter.svg';
  22. import regions from '../../../../assets/geojson/nm2022.json';
  23. import jsonData, { fetchJsonData } from '../../../database/geojsonService';
  24. import { getFirstDatabase, getSecondDatabase, refreshDatabases } from '../../../db';
  25. import { LocationPopup, WarningModal, EditNmModal, Button } from '../../../components';
  26. import { styles } from './style';
  27. import {
  28. calculateMapRegion,
  29. filterCandidates,
  30. filterCandidatesMarkers,
  31. findRegionInDataset,
  32. processMarkerData
  33. } from '../../../utils/mapHelpers';
  34. import { getData } from '../../../modules/map/regionData';
  35. import { fetchSeriesData, usePostSetToggleItem } from '@api/series';
  36. import MarkerItem from './MarkerItem';
  37. import ClusterItem from './ClusterItem';
  38. import { FeatureCollection, ItemSeries, MapScreenProps, Region, Series } from '../../../types/map';
  39. import { API_HOST, FASTEST_MAP_HOST } from 'src/constants';
  40. import { useConnection } from 'src/contexts/ConnectionContext';
  41. import ClusteredMapView from 'react-native-map-clustering';
  42. import { fetchUserData, fetchUserDataDare } from '@api/regions';
  43. import RegionPopup from 'src/components/RegionPopup';
  44. import { usePostSetNmRegionMutation } from '@api/myRegions';
  45. import { usePostSetDareRegionMutation } from '@api/myDARE';
  46. import moment from 'moment';
  47. import { qualityOptions } from '../TravelsScreen/utils/constants';
  48. import Animated, { Easing } from 'react-native-reanimated';
  49. import { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated';
  50. import { Colors } from 'src/theme';
  51. import { useGetUniversalSearch } from '@api/search';
  52. import SearchModal from './UniversalSearch';
  53. import FilterModal from './FilterModal';
  54. import InfoIcon from 'assets/icons/info-solid.svg';
  55. import { NAVIGATION_PAGES } from 'src/types';
  56. const localTileDir = `${FileSystem.cacheDirectory}tiles/background`;
  57. const localGridDir = `${FileSystem.cacheDirectory}tiles/grid`;
  58. const localVisitedDir = `${FileSystem.cacheDirectory}tiles/user_visited`;
  59. const localDareDir = `${FileSystem.cacheDirectory}tiles/regions_mqp`;
  60. const AnimatedMarker = Animation.createAnimatedComponent(Marker);
  61. const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
  62. const [dareData, setDareData] = useState(jsonData);
  63. const tilesBaseURL = `${FASTEST_MAP_HOST}/tiles_osm`;
  64. const gridUrl = `${FASTEST_MAP_HOST}/tiles_nm/grid`;
  65. const dareTiles = `${FASTEST_MAP_HOST}/tiles_nm/regions_mqp`;
  66. const userId = storage.get('uid', StoreType.STRING);
  67. const token = storage.get('token', StoreType.STRING) as string;
  68. const netInfo = useConnection();
  69. const { mutateAsync } = fetchSeriesData();
  70. const { mutateAsync: mutateUserData } = fetchUserData();
  71. const { mutateAsync: mutateUserDataDare } = fetchUserDataDare();
  72. const { mutate: updateSeriesItem } = usePostSetToggleItem();
  73. const { mutate: updateNM } = usePostSetNmRegionMutation();
  74. const { mutate: updateDARE } = usePostSetDareRegionMutation();
  75. const visitedTiles = `${FASTEST_MAP_HOST}/tiles_nm/user_visited/${userId}`;
  76. const visitedUNTiles = `${FASTEST_MAP_HOST}/tiles_nm/user_visited_un/${userId}`;
  77. const mapRef = useRef<MapView>(null);
  78. const [isConnected, setIsConnected] = useState<boolean | null>(true);
  79. const [selectedRegion, setSelectedRegion] = useState<FeatureCollection | null>(null);
  80. const [regionPopupVisible, setRegionPopupVisible] = useState<boolean | null>(false);
  81. const [regionData, setRegionData] = useState<Region | null>(null);
  82. const [userAvatars, setUserAvatars] = useState<string[]>([]);
  83. const [location, setLocation] = useState<Location.LocationObjectCoords | null>(null);
  84. const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
  85. const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
  86. const [isWarningModalVisible, setIsWarningModalVisible] = useState<boolean>(false);
  87. const [markers, setMarkers] = useState<ItemSeries[]>([]);
  88. const [series, setSeries] = useState<Series[] | null>(null);
  89. const [processedMarkers, setProcessedMarkers] = useState<ItemSeries[]>([]);
  90. const [zoomLevel, setZoomLevel] = useState<number>(0);
  91. const [userData, setUserData] = useState<any>(null);
  92. const [isEditModalVisible, setIsEditModalVisible] = useState(false);
  93. const [modalState, setModalState] = useState({
  94. selectedFirstYear: 2021,
  95. selectedLastYear: 2021,
  96. selectedQuality: qualityOptions[2],
  97. selectedNoOfVisits: 1,
  98. years: [],
  99. id: null
  100. });
  101. const [search, setSearch] = useState('');
  102. const [searchInput, setSearchInput] = useState('');
  103. const { data: searchData } = useGetUniversalSearch(search);
  104. const [isFilterVisible, setIsFilterVisible] = useState(false);
  105. const [tilesType, setTilesType] = useState({ label: 'NM regions', value: 0 });
  106. const tilesTypes = [
  107. { label: 'NM regions', value: 0 },
  108. { label: 'UN countries', value: 1 }
  109. ];
  110. const [type, setType] = useState(0);
  111. useEffect(() => {
  112. if (!dareData) {
  113. const fetchData = async () => {
  114. const fetchedData = await fetchJsonData();
  115. setDareData(fetchedData);
  116. };
  117. fetchData();
  118. }
  119. }, [dareData]);
  120. const handleModalStateChange = (updates: { [key: string]: any }) => {
  121. setModalState((prevState) => ({ ...prevState, ...updates }));
  122. };
  123. const handleOpenEditModal = () => {
  124. handleModalStateChange({
  125. selectedFirstYear: userData?.first_visit_year,
  126. selectedLastYear: userData?.last_visit_year,
  127. selectedQuality:
  128. qualityOptions.find((quality) => quality.id === userData?.best_visit_quality) ||
  129. qualityOptions[2],
  130. selectedNoOfVisits: userData?.no_of_visits || 1,
  131. id: regionData?.id
  132. });
  133. setIsEditModalVisible(true);
  134. };
  135. useEffect(() => {
  136. const currentYear = moment().year();
  137. let yearSelector: { label: string; value: number }[] = [{ label: 'visited', value: 1 }];
  138. for (let i = currentYear; i >= 1951; i--) {
  139. yearSelector.push({ label: i.toString(), value: i });
  140. }
  141. handleModalStateChange({ years: yearSelector });
  142. }, []);
  143. const cancelTokenRef = useRef(false);
  144. const currentTokenRef = useRef(0);
  145. const strokeWidthAnim = useRef(new Animation.Value(2)).current;
  146. const [isExpanded, setIsExpanded] = useState(false);
  147. const [searchVisible, setSearchVisible] = useState(false);
  148. const [index, setIndex] = useState<number>(0);
  149. const width = useSharedValue(48);
  150. const usableWidth = Dimensions.get('window').width - 32;
  151. useEffect(() => {
  152. if (netInfo?.isInternetReachable) {
  153. setIsConnected(true);
  154. } else {
  155. setIsConnected(false);
  156. }
  157. }, [netInfo?.isInternetReachable]);
  158. useEffect(() => {
  159. Animation.loop(
  160. Animation.sequence([
  161. Animation.timing(strokeWidthAnim, {
  162. toValue: 3,
  163. duration: 700,
  164. useNativeDriver: false
  165. }),
  166. Animation.timing(strokeWidthAnim, {
  167. toValue: 2,
  168. duration: 700,
  169. useNativeDriver: false
  170. })
  171. ])
  172. ).start();
  173. }, [strokeWidthAnim]);
  174. useEffect(() => {
  175. navigation.addListener('state', (state) => {
  176. navigation
  177. .getParent()
  178. ?.getParent()
  179. ?.setOptions({
  180. tabBarStyle: {
  181. display:
  182. (state.data.state.history[1] as { status?: string })?.status === 'open' ||
  183. regionPopupVisible
  184. ? 'none'
  185. : 'flex',
  186. position: 'absolute',
  187. ...Platform.select({
  188. android: {
  189. height: 58
  190. }
  191. })
  192. }
  193. });
  194. });
  195. navigation
  196. .getParent()
  197. ?.getParent()
  198. ?.setOptions({
  199. tabBarStyle: {
  200. display: regionPopupVisible ? 'none' : 'flex',
  201. position: 'absolute',
  202. ...Platform.select({
  203. android: {
  204. height: 58
  205. }
  206. })
  207. }
  208. });
  209. }, [regionPopupVisible, navigation]);
  210. useEffect(() => {
  211. (async () => {
  212. let { status } = await Location.getForegroundPermissionsAsync();
  213. if (status !== 'granted') {
  214. return;
  215. }
  216. let currentLocation = await Location.getCurrentPositionAsync({});
  217. setLocation(currentLocation.coords);
  218. })();
  219. }, []);
  220. const findFeaturesInVisibleMapArea = async (visibleMapArea: {
  221. latitude?: any;
  222. longitude?: any;
  223. latitudeDelta: any;
  224. longitudeDelta?: any;
  225. }) => {
  226. if (!isConnected) return;
  227. const currentZoom = Math.log2(360 / visibleMapArea.latitudeDelta);
  228. setZoomLevel(currentZoom);
  229. if (cancelTokenRef.current) {
  230. setMarkers(processedMarkers);
  231. return;
  232. }
  233. const thisToken = ++currentTokenRef.current;
  234. if (!regions || !dareData) return;
  235. if (currentZoom < 7) {
  236. setMarkers([]);
  237. return;
  238. }
  239. const { latitude, longitude, latitudeDelta, longitudeDelta } = visibleMapArea;
  240. const bbox: turf.BBox = [
  241. longitude - longitudeDelta / 2,
  242. latitude - latitudeDelta / 2,
  243. longitude + longitudeDelta / 2,
  244. latitude + latitudeDelta / 2
  245. ];
  246. const visibleAreaPolygon = turf.bboxPolygon(bbox);
  247. const regionsFound = filterCandidates(regions, bbox);
  248. // const daresFound = filterCandidates(dareRegions, bbox);
  249. const regionIds = regionsFound.map(
  250. (region: { properties: { id: any } }) => region.properties.id
  251. );
  252. isConnected &&
  253. (await mutateAsync(
  254. { regions: JSON.stringify(regionIds), token: String(token) },
  255. {
  256. onSuccess: (data) => {
  257. if (thisToken !== currentTokenRef.current) return;
  258. setSeries(data.series);
  259. const markersVisible = filterCandidatesMarkers(data.items, visibleAreaPolygon);
  260. const allMarkers = markersVisible.map(processMarkerData);
  261. setMarkers(allMarkers);
  262. }
  263. }
  264. ));
  265. };
  266. const handleGetLocation = async () => {
  267. let { status, canAskAgain } = await Location.getForegroundPermissionsAsync();
  268. if (status === 'granted') {
  269. getLocation();
  270. } else if (!canAskAgain) {
  271. setOpenSettingsVisible(true);
  272. } else {
  273. setAskLocationVisible(true);
  274. }
  275. };
  276. const getLocation = async () => {
  277. let currentLocation = await Location.getCurrentPositionAsync({
  278. accuracy: Location.Accuracy.Balanced
  279. });
  280. setLocation(currentLocation.coords);
  281. mapRef.current?.animateToRegion(
  282. {
  283. latitude: currentLocation.coords.latitude,
  284. longitude: currentLocation.coords.longitude,
  285. latitudeDelta: 5,
  286. longitudeDelta: 5
  287. },
  288. 800
  289. );
  290. handleClosePopup();
  291. };
  292. const handleAcceptPermission = async () => {
  293. setAskLocationVisible(false);
  294. let { status, canAskAgain } = await Location.requestForegroundPermissionsAsync();
  295. if (status === 'granted') {
  296. getLocation();
  297. } else if (!canAskAgain) {
  298. setOpenSettingsVisible(true);
  299. }
  300. };
  301. const handleRegionData = (regionData: Region, avatars: string[]) => {
  302. setRegionData(regionData);
  303. setUserAvatars(avatars);
  304. };
  305. const handleMapPress = async (event: {
  306. nativeEvent: { coordinate: { latitude: any; longitude: any }; action?: string };
  307. }) => {
  308. if (event.nativeEvent?.action === 'marker-press') return;
  309. cancelTokenRef.current = true;
  310. const { latitude, longitude } = event.nativeEvent.coordinate;
  311. const point = turf.point([longitude, latitude]);
  312. let db = getSecondDatabase();
  313. let tableName = 'places';
  314. let foundRegion = findRegionInDataset(dareData, point);
  315. if (!foundRegion) {
  316. foundRegion = findRegionInDataset(regions, point);
  317. db = getFirstDatabase();
  318. tableName = 'regions';
  319. }
  320. if (foundRegion) {
  321. if (foundRegion.properties?.id === regionData?.id) return;
  322. const id = foundRegion.properties?.id;
  323. setSelectedRegion({
  324. type: 'FeatureCollection',
  325. features: [
  326. {
  327. geometry: foundRegion.geometry,
  328. properties: {
  329. ...foundRegion.properties,
  330. fill: 'rgba(57, 115, 172, 0.2)',
  331. stroke: '#3973AC'
  332. },
  333. type: 'Feature'
  334. }
  335. ]
  336. });
  337. await getData(db, id, tableName, handleRegionData)
  338. .then(() => {
  339. setRegionPopupVisible(true);
  340. })
  341. .catch((error) => {
  342. console.error('Error fetching data', error);
  343. refreshDatabases();
  344. });
  345. const bounds = turf.bbox(foundRegion);
  346. const region = calculateMapRegion(bounds);
  347. mapRef.current?.animateToRegion(region, 1000);
  348. if (tableName === 'regions') {
  349. await mutateUserData(
  350. { region_id: +id, token: String(token) },
  351. {
  352. onSuccess: (data) => {
  353. setUserData({ type: 'nm', ...data });
  354. }
  355. }
  356. );
  357. await mutateAsync(
  358. { regions: JSON.stringify([id]), token: String(token) },
  359. {
  360. onSuccess: (data) => {
  361. setSeries(data.series);
  362. const allMarkers = data.items.map(processMarkerData);
  363. setProcessedMarkers(allMarkers);
  364. }
  365. }
  366. );
  367. } else {
  368. await mutateUserDataDare(
  369. { dare_id: +id, token: String(token) },
  370. {
  371. onSuccess: (data) => {
  372. setUserData({ type: 'dare', ...data });
  373. }
  374. }
  375. );
  376. setProcessedMarkers([]);
  377. }
  378. } else {
  379. handleClosePopup();
  380. }
  381. };
  382. const handleFindRegion = async (id: number, type: string) => {
  383. cancelTokenRef.current = true;
  384. const db = type === 'regions' ? getFirstDatabase() : getSecondDatabase();
  385. const dataset = type === 'regions' ? regions : dareData;
  386. const foundRegion = dataset.features.find((region: any) => region.properties.id === id);
  387. if (foundRegion) {
  388. setSelectedRegion({
  389. type: 'FeatureCollection',
  390. features: [
  391. {
  392. geometry: foundRegion.geometry,
  393. properties: {
  394. ...foundRegion.properties,
  395. fill: 'rgba(57, 115, 172, 0.2)',
  396. stroke: '#3973AC'
  397. },
  398. type: 'Feature'
  399. }
  400. ]
  401. });
  402. await getData(db, id, type, handleRegionData)
  403. .then(() => {
  404. setRegionPopupVisible(true);
  405. })
  406. .catch((error) => {
  407. console.error('Error fetching data', error);
  408. refreshDatabases();
  409. });
  410. const bounds = turf.bbox(foundRegion);
  411. const region = calculateMapRegion(bounds);
  412. mapRef.current?.animateToRegion(region, 1000);
  413. if (type === 'regions') {
  414. await mutateUserData(
  415. { region_id: +id, token: String(token) },
  416. {
  417. onSuccess: (data) => {
  418. setUserData({ type: 'nm', ...data });
  419. }
  420. }
  421. );
  422. await mutateAsync(
  423. { regions: JSON.stringify([id]), token: String(token) },
  424. {
  425. onSuccess: (data) => {
  426. setSeries(data.series);
  427. const allMarkers = data.items.map(processMarkerData);
  428. setProcessedMarkers(allMarkers);
  429. setMarkers(allMarkers);
  430. }
  431. }
  432. );
  433. } else {
  434. await mutateUserDataDare(
  435. { dare_id: +id, token: String(token) },
  436. {
  437. onSuccess: (data) => {
  438. setUserData({ type: 'dare', ...data });
  439. }
  440. }
  441. );
  442. setProcessedMarkers([]);
  443. setMarkers([]);
  444. }
  445. } else {
  446. handleClosePopup();
  447. }
  448. };
  449. const renderMapTiles = (url: string, cacheDir: string, zIndex: number, opacity = 1) => (
  450. <UrlTile
  451. urlTemplate={`${url}/{z}/{x}/{y}`}
  452. maximumZ={15}
  453. maximumNativeZ={13}
  454. tileCachePath={cacheDir !== localVisitedDir ? `${cacheDir}` : undefined}
  455. shouldReplaceMapContent
  456. minimumZ={0}
  457. offlineMode={!isConnected}
  458. opacity={opacity}
  459. zIndex={zIndex}
  460. />
  461. );
  462. function renderGeoJSON() {
  463. if (!selectedRegion) return null;
  464. return (
  465. <Geojson
  466. geojson={selectedRegion as any}
  467. fillColor="rgba(57, 115, 172, 0.2)"
  468. strokeColor="#3973ac"
  469. strokeWidth={Platform.OS == 'android' ? 3 : 2}
  470. zIndex={3}
  471. />
  472. );
  473. }
  474. const handleClosePopup = async () => {
  475. cancelTokenRef.current = false;
  476. setRegionPopupVisible(false);
  477. setMarkers([]);
  478. setSelectedRegion(null);
  479. setRegionData(null);
  480. const boundaries = await mapRef.current?.getMapBoundaries();
  481. const { northEast, southWest } = boundaries || {};
  482. const latitudeDelta = (northEast?.latitude ?? 0) - (southWest?.latitude ?? 0);
  483. const longitudeDelta = (northEast?.longitude ?? 0) - (southWest?.longitude ?? 0);
  484. const latitude = (southWest?.latitude ?? 0) + latitudeDelta / 2;
  485. const longitude = (southWest?.longitude ?? 0) + longitudeDelta / 2;
  486. findFeaturesInVisibleMapArea({ latitude, longitude, latitudeDelta, longitudeDelta });
  487. };
  488. const renderedGeoJSON = useMemo(() => renderGeoJSON(), [selectedRegion]);
  489. const toggleSeries = useCallback(
  490. async (item: any) => {
  491. if (!token) {
  492. setIsWarningModalVisible(true);
  493. return;
  494. }
  495. setMarkers((currentMarkers) =>
  496. currentMarkers.map((marker) =>
  497. marker.id === item.id ? { ...marker, visited: Number(!marker.visited) as 0 | 1 } : marker
  498. )
  499. );
  500. setProcessedMarkers((currentMarkers) =>
  501. currentMarkers.map((marker) =>
  502. marker.id === item.id ? { ...marker, visited: Number(!marker.visited) as 0 | 1 } : marker
  503. )
  504. );
  505. const itemData = {
  506. token: token,
  507. series_id: item.series_id,
  508. item_id: item.id,
  509. checked: (!item.visited ? 1 : 0) as 0 | 1,
  510. double: 0 as 0 | 1
  511. };
  512. try {
  513. updateSeriesItem(itemData);
  514. } catch (error) {
  515. console.error('Failed to update series state', error);
  516. }
  517. },
  518. [token, updateSeriesItem]
  519. );
  520. const renderMarkers = () => {
  521. return markers.map((marker, idx) => {
  522. const coordinate = { latitude: marker.pointJSON[0], longitude: marker.pointJSON[1] };
  523. const markerSeries = series?.find((s) => s.id === marker.series_id);
  524. const iconUrl = markerSeries ? API_HOST + markerSeries.icon : 'default_icon_url';
  525. const seriesName = markerSeries ? markerSeries.name : 'Unknown';
  526. return (
  527. <MarkerItem
  528. marker={marker}
  529. iconUrl={iconUrl}
  530. key={`${idx} - ${marker.id}`}
  531. coordinate={coordinate}
  532. seriesName={seriesName}
  533. toggleSeries={toggleSeries}
  534. token={token}
  535. />
  536. );
  537. });
  538. };
  539. const handleUpdateNM = useCallback(
  540. (region: number, first: number, last: number, visits: number, quality: number) => {
  541. if (!token) {
  542. setIsWarningModalVisible(true);
  543. return;
  544. }
  545. const updatedNM = {
  546. ...userData,
  547. first_visit_year: first,
  548. last_visit_year: last,
  549. best_visit_quality: quality,
  550. no_of_visits: visits,
  551. visited: visits > 0 ? true : false
  552. };
  553. const updatedNMData = {
  554. token,
  555. region,
  556. first,
  557. last,
  558. visits,
  559. quality
  560. };
  561. updateNM(updatedNMData);
  562. updatedNM && setUserData(updatedNM);
  563. },
  564. [userData, token]
  565. );
  566. const handleUpdateDare = useCallback(
  567. (region: number, visits: 0 | 1) => {
  568. if (!token) {
  569. setIsWarningModalVisible(true);
  570. return;
  571. }
  572. const updatedDARE = { ...userData, visited: visits > 0 ? true : false };
  573. const updatedDareData = {
  574. token,
  575. region,
  576. visits
  577. };
  578. updateDARE(updatedDareData);
  579. updatedDARE && setUserData(updatedDARE);
  580. },
  581. [userData, token]
  582. );
  583. const handlePress = () => {
  584. if (isExpanded) {
  585. setIndex(0);
  586. setSearchInput('');
  587. }
  588. setIsExpanded((prev) => !prev);
  589. width.value = withTiming(isExpanded ? 48 : usableWidth, {
  590. duration: 300,
  591. easing: Easing.inOut(Easing.ease)
  592. });
  593. };
  594. const animatedStyle = useAnimatedStyle(() => {
  595. return {
  596. width: width.value
  597. };
  598. });
  599. const handleSearch = async () => {
  600. setSearch(searchInput);
  601. setSearchVisible(true);
  602. };
  603. const handleCloseModal = () => {
  604. setSearchInput('');
  605. setSearchVisible(false);
  606. handlePress();
  607. };
  608. return (
  609. <View style={styles.container}>
  610. <ClusteredMapView
  611. initialRegion={{
  612. latitude: 0,
  613. longitude: 0,
  614. latitudeDelta: 180,
  615. longitudeDelta: 180
  616. }}
  617. ref={mapRef}
  618. showsMyLocationButton={false}
  619. showsCompass={false}
  620. zoomControlEnabled={false}
  621. onPress={handleMapPress}
  622. style={styles.map}
  623. mapType={Platform.OS == 'android' ? 'none' : 'standard'}
  624. maxZoomLevel={15}
  625. minZoomLevel={0}
  626. onRegionChangeComplete={findFeaturesInVisibleMapArea}
  627. minPoints={zoomLevel < 7 ? 0 : 12}
  628. tracksViewChanges={false}
  629. renderCluster={(cluster) => <ClusterItem key={cluster.id} cluster={cluster} />}
  630. >
  631. {renderedGeoJSON}
  632. {renderMapTiles(tilesBaseURL, localTileDir, 1)}
  633. {renderMapTiles(gridUrl, localGridDir, 2)}
  634. {userId &&
  635. renderMapTiles(type === 1 ? visitedUNTiles : visitedTiles, localVisitedDir, 2, 0.5)}
  636. {renderMapTiles(dareTiles, localDareDir, 2, 0.5)}
  637. {location && (
  638. <AnimatedMarker coordinate={location} anchor={{ x: 0.5, y: 0.5 }}>
  639. <Animation.View style={[styles.location, { borderWidth: strokeWidthAnim }]} />
  640. </AnimatedMarker>
  641. )}
  642. {markers && renderMarkers()}
  643. </ClusteredMapView>
  644. <LocationPopup
  645. visible={askLocationVisible}
  646. onClose={() => setAskLocationVisible(false)}
  647. onAccept={handleAcceptPermission}
  648. modalText="To use this feature we need your permission to access your location. If you press OK your system will ask you to approve location sharing with NomadMania app."
  649. />
  650. <LocationPopup
  651. visible={openSettingsVisible}
  652. onClose={() => setOpenSettingsVisible(false)}
  653. onAccept={() =>
  654. Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings()
  655. }
  656. modalText="NomadMania app needs location permissions to function properly. Open settings?"
  657. />
  658. {regionPopupVisible && regionData ? (
  659. <>
  660. <TouchableOpacity
  661. style={[styles.cornerButton, styles.topLeftButton, styles.closeLeftButton]}
  662. onPress={handleClosePopup}
  663. >
  664. <CloseSvg fill="white" width={13} height={13} />
  665. <Text style={styles.textClose}>Close</Text>
  666. </TouchableOpacity>
  667. <TouchableOpacity
  668. onPress={handleGetLocation}
  669. style={[styles.cornerButton, styles.topRightButton, styles.bottomButton]}
  670. >
  671. <LocationIcon />
  672. </TouchableOpacity>
  673. <RegionPopup
  674. region={regionData}
  675. userAvatars={userAvatars}
  676. userData={userData}
  677. openEditModal={handleOpenEditModal}
  678. updateNM={handleUpdateNM}
  679. updateDare={handleUpdateDare}
  680. disabled={!token || !isConnected}
  681. />
  682. </>
  683. ) : (
  684. <>
  685. {!isExpanded ? (
  686. <TouchableOpacity
  687. style={[styles.cornerButton, styles.topRightButton]}
  688. onPress={() => (navigation as any)?.openDrawer()}
  689. >
  690. <MenuIcon />
  691. </TouchableOpacity>
  692. ) : null}
  693. <TouchableOpacity
  694. style={styles.cornerInfoButton}
  695. onPress={() => navigation.navigate(NAVIGATION_PAGES.DISCOVER_INFO)}
  696. >
  697. <InfoIcon />
  698. </TouchableOpacity>
  699. <Animated.View
  700. style={[
  701. styles.searchContainer,
  702. styles.cornerButton,
  703. styles.topLeftButton,
  704. animatedStyle,
  705. { padding: 5 }
  706. ]}
  707. >
  708. {isExpanded ? (
  709. <>
  710. <TouchableOpacity onPress={handlePress} style={styles.iconButton}>
  711. <CloseSvg fill={'#0F3F4F'} />
  712. </TouchableOpacity>
  713. <TextInput
  714. style={styles.input}
  715. placeholder="Search regions, places, nomads"
  716. placeholderTextColor={Colors.LIGHT_GRAY}
  717. value={searchInput}
  718. onChangeText={(text) => setSearchInput(text)}
  719. onSubmitEditing={handleSearch}
  720. />
  721. <TouchableOpacity onPress={handleSearch} style={styles.iconButton}>
  722. <SearchIcon fill={'#0F3F4F'} />
  723. </TouchableOpacity>
  724. </>
  725. ) : (
  726. <TouchableOpacity onPress={handlePress} style={[styles.iconButton]}>
  727. <SearchIcon fill={'#0F3F4F'} />
  728. </TouchableOpacity>
  729. )}
  730. </Animated.View>
  731. <TouchableOpacity
  732. style={[styles.cornerButton, styles.bottomButton, styles.bottomLeftButton]}
  733. onPress={() => {
  734. token ? setIsFilterVisible(true) : setIsWarningModalVisible(true);
  735. }}
  736. >
  737. <FilterIcon />
  738. </TouchableOpacity>
  739. <TouchableOpacity
  740. onPress={handleGetLocation}
  741. style={[styles.cornerButton, styles.bottomButton, styles.bottomRightButton]}
  742. >
  743. <LocationIcon />
  744. </TouchableOpacity>
  745. </>
  746. )}
  747. <WarningModal
  748. type={'unauthorized'}
  749. isVisible={isWarningModalVisible}
  750. onClose={() => setIsWarningModalVisible(false)}
  751. />
  752. <EditNmModal
  753. isVisible={isEditModalVisible}
  754. onClose={() => setIsEditModalVisible(false)}
  755. modalState={modalState}
  756. updateModalState={handleModalStateChange}
  757. updateNM={handleUpdateNM}
  758. />
  759. <SearchModal
  760. searchVisible={searchVisible}
  761. handleCloseModal={handleCloseModal}
  762. handleFindRegion={handleFindRegion}
  763. index={index}
  764. searchData={searchData}
  765. setIndex={setIndex}
  766. token={token}
  767. />
  768. <FilterModal
  769. isFilterVisible={isFilterVisible}
  770. setIsFilterVisible={setIsFilterVisible}
  771. tilesTypes={tilesTypes}
  772. tilesType={tilesType}
  773. setTilesType={setTilesType}
  774. type={type}
  775. setType={setType}
  776. />
  777. </View>
  778. );
  779. };
  780. export default MapScreen;