index.tsx 31 KB

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