index.tsx 27 KB

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