index.tsx 30 KB

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