index.tsx 103 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152
  1. import {
  2. Dimensions,
  3. Linking,
  4. Platform,
  5. Text,
  6. TextInput,
  7. TouchableOpacity,
  8. View,
  9. Image,
  10. StatusBar,
  11. ActivityIndicator,
  12. ScrollView
  13. } from 'react-native';
  14. import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
  15. import * as MapLibreRN from '@maplibre/maplibre-react-native';
  16. import { styles } from './style';
  17. import { SafeAreaView } from 'react-native-safe-area-context';
  18. import { Colors } from 'src/theme';
  19. import { storage, StoreType } from 'src/storage';
  20. import * as turf from '@turf/turf';
  21. import * as Location from 'expo-location';
  22. import SearchIcon from 'assets/icons/search.svg';
  23. import LocationIcon from 'assets/icons/location.svg';
  24. import CloseSvg from 'assets/icons/close.svg';
  25. import ProfileIcon from 'assets/icons/bottom-navigation/profile.svg';
  26. import RegionPopup from 'src/components/RegionPopup';
  27. import { useRegion } from 'src/contexts/RegionContext';
  28. import { qualityOptions } from '../TravelsScreen/utils/constants';
  29. import { AvatarWithInitials, EditNmModal, WarningModal } from 'src/components';
  30. import { API_HOST, VECTOR_MAP_HOST } from 'src/constants';
  31. import { NAVIGATION_PAGES } from 'src/types';
  32. import Animated, {
  33. configureReanimatedLogger,
  34. Easing,
  35. useAnimatedStyle,
  36. useSharedValue,
  37. withTiming
  38. } from 'react-native-reanimated';
  39. import { getData } from 'src/modules/map/regionData';
  40. import {
  41. getCountriesDatabase,
  42. getFirstDatabase,
  43. getSecondDatabase,
  44. refreshDatabases
  45. } from 'src/db';
  46. import { fetchUserData, fetchUserDataDare, useGetListRegionsQuery } from '@api/regions';
  47. import { SQLiteDatabase } from 'expo-sqlite';
  48. import { useFocusEffect } from '@react-navigation/native';
  49. import { useGetUniversalSearch } from '@api/search';
  50. import { fetchCountryUserData, useGetListCountriesQuery } from '@api/countries';
  51. import SearchModal from './UniversalSearch';
  52. import EditModal from '../TravelsScreen/Components/EditSlowModal';
  53. import * as FileSystem from 'expo-file-system/legacy';
  54. import moment from 'moment';
  55. import {
  56. usePostGetVisitedCountriesIdsQuery,
  57. usePostGetVisitedDareIdsQuery,
  58. usePostGetVisitedRegionsIdsQuery,
  59. usePostGetVisitedSeriesIdsQuery
  60. } from '@api/maps';
  61. import FilterModal, { FilterModalRef } from './FilterModal';
  62. import { useGetListDareQuery } from '@api/myDARE';
  63. import { useGetIconsQuery, usePostSetToggleItem } from '@api/series';
  64. import MarkerItem from './MarkerItem';
  65. import {
  66. usePostGetSettingsQuery,
  67. usePostGetUsersCountQuery,
  68. usePostGetUsersLocationFilteredMutation,
  69. usePostGetUsersLocationQuery,
  70. usePostUpdateLocationMutation
  71. } from '@api/location';
  72. import UserItem from './UserItem';
  73. import { useConnection } from 'src/contexts/ConnectionContext';
  74. import TravelsIcon from 'assets/icons/bottom-navigation/globe-solid.svg';
  75. import SeriesIcon from 'assets/icons/travels-section/series.svg';
  76. import NomadsIcon from 'assets/icons/bottom-navigation/travellers.svg';
  77. import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
  78. import MapButton from 'src/components/MapButton';
  79. import { useAvatarStore } from 'src/stores/avatarVersionStore';
  80. import _ from 'lodash';
  81. import ScaleBar from 'src/components/ScaleBar';
  82. import MessagesDot from 'src/components/MessagesDot';
  83. import { stopBackgroundLocationUpdates } from 'src/utils/backgroundLocation';
  84. import { SheetManager } from 'react-native-actions-sheet';
  85. import MultipleSeriesModal from './MultipleSeriesModal';
  86. import { useSubscription } from 'src/screens/OfflineMapsScreen/useSubscription';
  87. import { point } from '@turf/turf';
  88. import { booleanPointInPolygon } from '@turf/turf';
  89. import { area } from '@turf/turf';
  90. configureReanimatedLogger({
  91. strict: false
  92. });
  93. const clusteredUsersIcon = require('assets/icons/icon-clustered-users.png');
  94. const logo = require('assets/logo-world.png');
  95. MapLibreRN.Logger.setLogLevel('error');
  96. const isValidNumber = (val: any) => typeof val === 'number' && !isNaN(val) && isFinite(val);
  97. const sanitizeNomadsGeoJSON = (
  98. collection: GeoJSON.FeatureCollection
  99. ): GeoJSON.FeatureCollection => {
  100. const validFeatures = collection.features.filter((feature) => {
  101. if (!feature || !feature.geometry || !feature.properties) return false;
  102. const geom = feature.geometry;
  103. if (geom.type !== 'Point') return false;
  104. const coords = (geom as GeoJSON.Point).coordinates;
  105. if (!Array.isArray(coords) || coords.length < 2) return false;
  106. const [lng, lat] = coords;
  107. if (lng == null || lat == null) return false;
  108. if (!isValidNumber(lng) || !isValidNumber(lat)) return false;
  109. if (lng < -180 || lng > 180 || lat < -90 || lat > 90) return false;
  110. return true;
  111. });
  112. return { type: 'FeatureCollection', features: validFeatures };
  113. };
  114. const generateFilter = (ids: number[]) => {
  115. const validIds = ids?.filter(isValidNumber) || [];
  116. return validIds.length ? ['any', ...validIds.map((id) => ['==', 'id', id])] : ['==', 'id', -1];
  117. };
  118. // to do refactor
  119. let regions_visited = {
  120. id: 'regions_visited',
  121. type: 'fill',
  122. source: 'regions',
  123. 'source-layer': 'regions',
  124. style: {
  125. fillColor: 'rgba(255, 126, 0, 1)',
  126. fillOpacity: 0.6,
  127. fillOutlineColor: 'rgba(14, 80, 109, 0)'
  128. },
  129. filter: generateFilter([]),
  130. maxzoom: 10
  131. };
  132. let countries_visited = {
  133. id: 'countries_visited',
  134. type: 'fill',
  135. source: 'countries',
  136. 'source-layer': 'countries',
  137. style: {
  138. fillColor: 'rgba(255, 126, 0, 1)',
  139. fillOpacity: 0.6,
  140. fillOutlineColor: 'rgba(14, 80, 109, 0)'
  141. },
  142. filter: generateFilter([]),
  143. maxzoom: 10
  144. };
  145. let dare_visited = {
  146. id: 'dare_visited',
  147. type: 'fill',
  148. source: 'dare',
  149. 'source-layer': 'dare',
  150. style: {
  151. fillColor: 'rgba(255, 126, 0, 0.6)',
  152. fillOutlineColor: 'rgba(255, 126, 0, 0)'
  153. },
  154. filter: generateFilter([]),
  155. maxzoom: 12
  156. };
  157. let regions = {
  158. id: 'regions',
  159. type: 'fill',
  160. source: 'regions',
  161. 'source-layer': 'regions',
  162. style: {
  163. fillColor: 'rgba(15, 63, 79, 0)',
  164. fillOutlineColor: 'rgba(14, 80, 109, 0)'
  165. },
  166. filter: ['all'],
  167. maxzoom: 16
  168. };
  169. let countries = {
  170. id: 'countries',
  171. type: 'fill',
  172. source: 'countries',
  173. 'source-layer': 'countries',
  174. style: {
  175. fillColor: 'rgba(15, 63, 79, 0)',
  176. fillOutlineColor: 'rgba(14, 80, 109, 0)'
  177. },
  178. filter: ['all'],
  179. maxzoom: 16
  180. };
  181. let dare = {
  182. id: 'dare',
  183. type: 'fill',
  184. source: 'dare',
  185. 'source-layer': 'dare',
  186. style: {
  187. fillColor: 'rgba(14, 80, 109, 0.6)',
  188. fillOutlineColor: 'rgba(14, 80, 109, 0)'
  189. },
  190. filter: ['all'],
  191. maxzoom: 16
  192. };
  193. let selected_region = {
  194. id: 'selected_region',
  195. type: 'fill',
  196. source: 'regions',
  197. 'source-layer': 'regions',
  198. style: {
  199. fillColor: 'rgba(57, 115, 172, 0.3)'
  200. },
  201. maxzoom: 12
  202. };
  203. let selected_region_outline = {
  204. id: 'selected_region_outline',
  205. type: 'line',
  206. source: 'regions',
  207. 'source-layer': 'regions',
  208. style: {
  209. lineColor: '#ED9334',
  210. lineTranslate: [0, 0],
  211. lineTranslateAnchor: 'map',
  212. lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 2, 4, 3, 5, 4, 12, 5]
  213. },
  214. maxzoom: 12
  215. };
  216. let series_layer = {
  217. id: 'series_layer',
  218. type: 'symbol',
  219. source: 'nomadmania_series',
  220. 'source-layer': 'series',
  221. minzoom: 5,
  222. maxzoom: 60,
  223. layout: {
  224. 'symbol-spacing': 1,
  225. 'icon-image': '{series_id}',
  226. 'icon-size': 0.15,
  227. 'icon-allow-overlap': true,
  228. 'icon-ignore-placement': true,
  229. 'text-anchor': 'top',
  230. 'text-field': '{name}',
  231. 'text-font': ['Noto Sans Regular'],
  232. 'text-max-width': 9,
  233. 'text-offset': [0, 0.6],
  234. 'text-padding': 2,
  235. 'text-size': 12,
  236. visibility: 'visible',
  237. 'text-optional': true,
  238. 'text-ignore-placement': false,
  239. 'text-allow-overlap': false
  240. },
  241. paint: {
  242. 'text-color': '#666',
  243. 'text-halo-blur': 0.5,
  244. 'text-halo-color': '#ffffff',
  245. 'text-halo-width': 1
  246. },
  247. filter: generateFilter([])
  248. };
  249. let series_visited = {
  250. id: 'series_visited',
  251. type: 'symbol',
  252. source: 'nomadmania_series',
  253. 'source-layer': 'series',
  254. minzoom: 5,
  255. maxzoom: 60,
  256. layout: {
  257. 'symbol-spacing': 1,
  258. 'icon-image': '{series_id}v',
  259. 'icon-size': 0.15,
  260. 'icon-allow-overlap': true,
  261. 'icon-ignore-placement': true,
  262. 'text-anchor': 'top',
  263. 'text-field': '{name}',
  264. 'text-font': ['Noto Sans Regular'],
  265. 'text-max-width': 9,
  266. 'text-offset': [0, 0.6],
  267. 'text-padding': 2,
  268. 'text-size': 12,
  269. visibility: 'visible',
  270. 'text-optional': true,
  271. 'text-ignore-placement': false,
  272. 'text-allow-overlap': false
  273. },
  274. paint: {
  275. 'text-color': '#666',
  276. 'text-halo-blur': 0.5,
  277. 'text-halo-color': '#ffffff',
  278. 'text-halo-width': 1
  279. },
  280. filter: generateFilter([])
  281. };
  282. const INITIAL_REGION = {
  283. latitude: 0,
  284. longitude: 0,
  285. latitudeDelta: 180,
  286. longitudeDelta: 180
  287. };
  288. const ICONS_DIR = FileSystem.documentDirectory + 'series_icons/';
  289. const SERIES_ICON_SIZE = [
  290. 'interpolate',
  291. ['linear'],
  292. ['zoom'],
  293. 3,
  294. 0.3,
  295. 4,
  296. 0.36,
  297. 5,
  298. 0.43,
  299. 6,
  300. 0.5,
  301. 7,
  302. 0.57,
  303. 8,
  304. 0.63,
  305. 9,
  306. 0.7,
  307. 10,
  308. 0.77,
  309. 11,
  310. 0.84,
  311. 12,
  312. 0.91,
  313. 13,
  314. 0.98,
  315. 14,
  316. 1.05,
  317. 15,
  318. 1.12,
  319. 16,
  320. 1.18,
  321. 17,
  322. 1.25
  323. ] as unknown as number;
  324. const SERIES_CIRCLE_RADIUS = [
  325. 'interpolate',
  326. ['linear'],
  327. ['zoom'],
  328. 3,
  329. 5,
  330. 4,
  331. 6,
  332. 5,
  333. 7,
  334. 6,
  335. 8,
  336. 7,
  337. 9,
  338. 8,
  339. 10,
  340. 9,
  341. 11,
  342. 10,
  343. 12,
  344. 11,
  345. 13,
  346. 12,
  347. 14,
  348. 13,
  349. 16,
  350. 14,
  351. 17,
  352. 15,
  353. 18,
  354. 16,
  355. 19,
  356. 17,
  357. 20
  358. ] as unknown as number;
  359. const SERIES_CIRCLE_STROKE = [
  360. 'interpolate',
  361. ['linear'],
  362. ['zoom'],
  363. 3,
  364. 1,
  365. 7,
  366. 1.5,
  367. 13,
  368. 2,
  369. 17,
  370. 2.5
  371. ] as unknown as number;
  372. const SERIES_TEXT_SIZE = [
  373. 'interpolate',
  374. ['linear'],
  375. ['zoom'],
  376. 3,
  377. 6,
  378. 4,
  379. 7,
  380. 5,
  381. 8,
  382. 6,
  383. 9,
  384. 7,
  385. 10,
  386. 8,
  387. 10,
  388. 9,
  389. 11,
  390. 10,
  391. 11,
  392. 11,
  393. 12,
  394. 12,
  395. 12,
  396. 13,
  397. 13,
  398. 14,
  399. 13,
  400. 15,
  401. 14,
  402. 16,
  403. 14,
  404. 17,
  405. 15
  406. ] as unknown as number;
  407. const processedSeriesShares = new Set<number>();
  408. const MapScreen: any = ({ navigation, route }: { navigation: any; route: any }) => {
  409. const tabBarHeight = useBottomTabBarHeight();
  410. const userId = storage.get('uid', StoreType.STRING) as string;
  411. const token = storage.get('token', StoreType.STRING) as string;
  412. const [isConnected, setIsConnected] = useState<boolean>(true);
  413. const netInfo = useConnection();
  414. const { avatarVersion } = useAvatarStore();
  415. const { data: usersOnMapCount } = usePostGetUsersCountQuery(token, !!token && isConnected);
  416. const { data: regionsList } = useGetListRegionsQuery(isConnected);
  417. const { data: countriesList } = useGetListCountriesQuery(isConnected);
  418. const { data: dareList } = useGetListDareQuery(isConnected);
  419. const [usersCount, setUsersCount] = useState(0);
  420. const [tilesType, setTilesType] = useState({ label: 'NM regions', value: 0 });
  421. const tilesTypes = [
  422. { label: 'Blank', value: -1 },
  423. { label: 'NM regions', value: 0 },
  424. { label: 'UN countries', value: 1 },
  425. { label: 'DARE places', value: 2 }
  426. ];
  427. const [type, setType] = useState<'regions' | 'countries' | 'dare' | 'blank'>('regions');
  428. const [seriesFilter, setSeriesFilter] = useState<any>({
  429. visible: true,
  430. groups: [],
  431. applied: false,
  432. status: -1,
  433. labelIds: []
  434. });
  435. const [regionsFilter, setRegionsFilter] = useState<any>({
  436. visitedLabel: 'by',
  437. year: moment().year()
  438. });
  439. const [nomadsFilter, setNomadsFilter] = useState<any>({
  440. friends: 0,
  441. trusted: 0,
  442. countries: []
  443. });
  444. const [showNomads, setShowNomads] = useState(
  445. (storage.get('showNomads', StoreType.BOOLEAN) as boolean) ?? false
  446. );
  447. const { data: locationSettings, refetch } = usePostGetSettingsQuery(
  448. token,
  449. !!token && isConnected
  450. );
  451. const { mutateAsync: updateLocation } = usePostUpdateLocationMutation();
  452. const [forceRefetch, setForceRefetch] = useState(0);
  453. const { data: visitedRegionIds, refetch: refetchVisitedRegions } =
  454. usePostGetVisitedRegionsIdsQuery(
  455. token,
  456. regionsFilter.visitedLabel,
  457. regionsFilter.year,
  458. +userId,
  459. type === 'regions' && !!userId && isConnected,
  460. forceRefetch
  461. );
  462. const { data: visitedCountryIds, refetch: refetchVisitedCountries } =
  463. usePostGetVisitedCountriesIdsQuery(
  464. token,
  465. regionsFilter.visitedLabel,
  466. regionsFilter.year,
  467. +userId,
  468. type === 'countries' && !!userId && isConnected,
  469. forceRefetch
  470. );
  471. const { data: visitedDareIds, refetch: refetchVisitedDare } = usePostGetVisitedDareIdsQuery(
  472. token,
  473. +userId,
  474. type === 'dare' && !!userId && isConnected
  475. );
  476. const { data: visitedSeriesIds } = usePostGetVisitedSeriesIdsQuery(
  477. token,
  478. !!userId && isConnected
  479. );
  480. const { data: seriesIcons } = useGetIconsQuery(isConnected);
  481. const userInfo = storage.get('currentUserData', StoreType.STRING) as string;
  482. const { mutateAsync: mutateUserData } = fetchUserData();
  483. const { mutateAsync: mutateUserDataDare } = fetchUserDataDare();
  484. const { mutateAsync: mutateCountriesData } = fetchCountryUserData();
  485. const [selectedRegion, setSelectedRegion] = useState<number | null>(null);
  486. const [initialRegion, setInitialRegion] = useState(INITIAL_REGION);
  487. const [regionPopupVisible, setRegionPopupVisible] = useState<boolean | null>(false);
  488. const [regionData, setRegionData] = useState<any | null>(null);
  489. const [location, setLocation] = useState<any | null>(null);
  490. const [userAvatars, setUserAvatars] = useState<string[]>([]);
  491. const [userInfoData, setUserInfoData] = useState<any>(null);
  492. const [selectedMarker, setSelectedMarker] = useState<any>(null);
  493. const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
  494. const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
  495. const [isWarningModalVisible, setIsWarningModalVisible] = useState<boolean>(false);
  496. const [isEditSlowModalVisible, setIsEditSlowModalVisible] = useState<boolean>(false);
  497. const [isEditModalVisible, setIsEditModalVisible] = useState(false);
  498. const filterModalRef = useRef<FilterModalRef>(null);
  499. const [isLocationLoading, setIsLocationLoading] = useState(false);
  500. const [premiumModalVisible, setPremiumModalVisible] = useState(false);
  501. const [modalState, setModalState] = useState({
  502. selectedFirstYear: 2021,
  503. selectedLastYear: 2021,
  504. selectedQuality: qualityOptions[2],
  505. selectedNoOfVisits: 1,
  506. years: [],
  507. id: null
  508. });
  509. const [isExpanded, setIsExpanded] = useState(false);
  510. const [search, setSearch] = useState('');
  511. const { data: searchData } = useGetUniversalSearch(search, search.length > 0);
  512. const [searchInput, setSearchInput] = useState('');
  513. const [searchVisible, setSearchVisible] = useState<boolean>(false);
  514. const [index, setIndex] = useState<number>(0);
  515. const width = useSharedValue(48);
  516. const usableWidth = Dimensions.get('window').width - 32;
  517. const { handleUpdateNM, handleUpdateDare, handleUpdateSlow, userData, setUserData } = useRegion();
  518. const [db1, setDb1] = useState<SQLiteDatabase | null>(null);
  519. const [db2, setDb2] = useState<SQLiteDatabase | null>(null);
  520. const [db3, setDb3] = useState<SQLiteDatabase | null>(null);
  521. const [regionsVisitedFilter, setRegionsVisitedFilter] = useState(generateFilter([]));
  522. const [countriesVisitedFilter, setCountriesVisitedFilter] = useState(generateFilter([]));
  523. const [dareVisitedFilter, setDareVisitedFilter] = useState(generateFilter([]));
  524. const [seriesVisitedFilter, setSeriesVisitedFilter] = useState(generateFilter([]));
  525. const [seriesNotVisitedFilter, setSeriesNotVisitedFilter] = useState(generateFilter([]));
  526. const [regionsVisited, setRegionsVisited] = useState<any[]>([]);
  527. const [countriesVisited, setCountriesVisited] = useState<any[]>([]);
  528. const [dareVisited, setDareVisited] = useState<any[]>([]);
  529. const [seriesVisited, setSeriesVisited] = useState<any[]>([]);
  530. const [images, setImages] = useState<any>({});
  531. const { mutateAsync: updateSeriesItem } = usePostSetToggleItem();
  532. const [nomads, setNomads] = useState<GeoJSON.FeatureCollection | null>(null);
  533. const { data: usersLocation, refetch: refetchUsersLocation } = usePostGetUsersLocationQuery(
  534. token,
  535. !!token && showNomads && Boolean(location) && isConnected
  536. );
  537. const { mutateAsync: getLocationFiltered } = usePostGetUsersLocationFilteredMutation();
  538. const [selectedUser, setSelectedUser] = useState<any>(null);
  539. const [zoom, setZoom] = useState(0);
  540. const [center, setCenter] = useState<number[] | null>(null);
  541. const [isZooming, setIsZooming] = useState(true);
  542. const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  543. const [renderCamera, setRenderCamera] = useState(Platform.OS === 'ios');
  544. const isAnimatingRef = useRef(false);
  545. const animationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  546. const handledSeriesShareRef = useRef<number | null>(null);
  547. const [markerCoords, setMarkerCoords] = useState<any>(null);
  548. const [refreshInterval, setRefreshInterval] = useState(0);
  549. const isSmallScreen = Dimensions.get('window').width < 383;
  550. const processedImages = useRef(new Set<string>());
  551. const [didFinishLoadingStyle, setDidFinishLoadingStyle] = useState(false);
  552. const { isPremium, loading } = useSubscription();
  553. useEffect(() => {
  554. if (netInfo && netInfo.isConnected !== null) {
  555. setIsConnected(netInfo.isConnected);
  556. }
  557. }, [netInfo]);
  558. useEffect(() => {
  559. if (Platform.OS === 'android') {
  560. MapLibreRN.setConnected(isConnected);
  561. }
  562. }, [isConnected])
  563. useEffect(() => {
  564. if (showNomads) {
  565. refetchUsersLocation();
  566. }
  567. }, [showNomads]);
  568. useEffect(() => {
  569. if (usersLocation && usersLocation.geojson && showNomads) {
  570. const rawFiltered: GeoJSON.FeatureCollection = {
  571. type: 'FeatureCollection',
  572. features: usersLocation.geojson.features.filter(
  573. (feature: GeoJSON.Feature) => feature.properties?.id !== +userId
  574. )
  575. };
  576. const filteredNomads = sanitizeNomadsGeoJSON(rawFiltered);
  577. if (!nomads || filteredNomads.features.length !== nomads.features.length) {
  578. setNomads(filteredNomads);
  579. }
  580. if (usersOnMapCount && usersOnMapCount.count) {
  581. setUsersCount(usersOnMapCount.count);
  582. }
  583. }
  584. }, [usersLocation, showNomads, refreshInterval]);
  585. useEffect(() => {
  586. if (
  587. usersOnMapCount &&
  588. usersOnMapCount.count &&
  589. !nomadsFilter.friends &&
  590. !nomadsFilter.countries?.length
  591. ) {
  592. setUsersCount(usersOnMapCount.count);
  593. }
  594. }, [usersOnMapCount]);
  595. useEffect(() => {
  596. const loadCachedIcons = async () => {
  597. try {
  598. const dirInfo = await FileSystem.getInfoAsync(ICONS_DIR);
  599. if (!dirInfo.exists) return;
  600. const files = await FileSystem.readDirectoryAsync(ICONS_DIR);
  601. const cachedImages: Record<string, { uri: string }> = {};
  602. files.forEach((fileName) => {
  603. if (!fileName.endsWith('.png')) return;
  604. const key = fileName.replace('.png', '');
  605. cachedImages[key] = {
  606. uri: ICONS_DIR + fileName
  607. };
  608. processedImages.current.add(key);
  609. });
  610. setImages((prev: any) => ({ ...prev, ...cachedImages }));
  611. } catch (e) {
  612. console.warn('Error loading cached icons:', e);
  613. }
  614. };
  615. didFinishLoadingStyle && loadCachedIcons();
  616. }, [didFinishLoadingStyle]);
  617. useEffect(() => {
  618. if (!seriesIcons || !didFinishLoadingStyle) return;
  619. const updateCacheFromAPI = async () => {
  620. const loadedImages: Record<string, { uri: string }> = {};
  621. const dirInfo = await FileSystem.getInfoAsync(ICONS_DIR);
  622. if (!dirInfo.exists) {
  623. await FileSystem.makeDirectoryAsync(ICONS_DIR, { intermediates: true });
  624. }
  625. const promises = seriesIcons.data.map(async (icon) => {
  626. const id = icon.id?.toString();
  627. if (!id || processedImages.current.has(id)) return;
  628. const imgUrl = `${API_HOST}/static/img/series_new2_small/${icon.new_icon_png}`;
  629. const imgVisitedUrl = `${API_HOST}/static/img/series_new2_small/${icon.new_icon_visited_png}`;
  630. const localPath = `${ICONS_DIR}${id}.png`;
  631. const localPathVisited = `${ICONS_DIR}${id}v.png`;
  632. const [imgInfo, visitedInfo] = await Promise.all([
  633. FileSystem.getInfoAsync(localPath),
  634. FileSystem.getInfoAsync(localPathVisited)
  635. ]);
  636. try {
  637. if (!imgInfo.exists) {
  638. await FileSystem.downloadAsync(imgUrl, localPath);
  639. }
  640. if (!visitedInfo.exists) {
  641. await FileSystem.downloadAsync(imgVisitedUrl, localPathVisited);
  642. }
  643. } catch (e) {
  644. console.warn(`Download failed for ${id}:`, e);
  645. return;
  646. }
  647. processedImages.current.add(id);
  648. processedImages.current.add(`${id}v`);
  649. loadedImages[id] = { uri: localPath };
  650. loadedImages[`${id}v`] = { uri: localPathVisited };
  651. });
  652. await Promise.all(promises);
  653. setImages((prev: any) => ({ ...prev, ...loadedImages }));
  654. };
  655. updateCacheFromAPI();
  656. }, [seriesIcons, didFinishLoadingStyle]);
  657. useEffect(() => {
  658. const loadDatabases = async () => {
  659. const firstDb = await getFirstDatabase();
  660. const secondDb = await getSecondDatabase();
  661. const countriesDb = await getCountriesDatabase();
  662. setDb1(firstDb);
  663. setDb2(secondDb);
  664. setDb3(countriesDb);
  665. };
  666. if (!db1 || !db2 || !db3) {
  667. loadDatabases();
  668. }
  669. }, [db1, db2, db3]);
  670. useEffect(() => {
  671. const savedFilterSettings = storage.get('filterSettings', StoreType.STRING) as string;
  672. const storageShowNomads = storage.get('showNomads', StoreType.BOOLEAN) as boolean;
  673. if (savedFilterSettings) {
  674. const filterSettings = JSON.parse(savedFilterSettings);
  675. setTilesType(filterSettings.tilesType);
  676. setType(filterSettings.type);
  677. setRegionsFilter({
  678. visitedLabel:
  679. filterSettings.selectedVisible?.value && filterSettings.selectedVisible.value === 1
  680. ? 'in'
  681. : 'by',
  682. year: filterSettings.selectedYear?.value ?? moment().year()
  683. });
  684. setShowNomads(storageShowNomads ?? false);
  685. setNomadsFilter({
  686. friends: filterSettings.nomadsFilter ? filterSettings.nomadsFilter.friends : 0,
  687. trusted: filterSettings.nomadsFilter ? filterSettings.nomadsFilter.trusted : 0,
  688. countries: filterSettings.nomadsFilter?.countries ?? []
  689. });
  690. setSeriesFilter(filterSettings.seriesFilter);
  691. }
  692. }, []);
  693. useFocusEffect(
  694. useCallback(() => {
  695. if (token) {
  696. setForceRefetch((prev) => prev + 1);
  697. refetchVisitedCountries();
  698. refetchVisitedDare();
  699. }
  700. if (userData && type === 'regions') {
  701. handleClosePopup();
  702. }
  703. }, [navigation, token])
  704. );
  705. useEffect(() => {
  706. if (visitedRegionIds) {
  707. setRegionsVisited(visitedRegionIds.ids);
  708. storage.set('visitedRegions', JSON.stringify(visitedRegionIds.ids));
  709. } else {
  710. const storedVisited = storage.get('visitedRegions', StoreType.STRING) as string;
  711. setRegionsVisited(storedVisited ? JSON.parse(storedVisited) : []);
  712. }
  713. }, [visitedRegionIds]);
  714. useEffect(() => {
  715. if (visitedCountryIds) {
  716. setCountriesVisited(visitedCountryIds.ids);
  717. storage.set('visitedCountries', JSON.stringify(visitedCountryIds.ids));
  718. } else {
  719. const storedVisited = storage.get('visitedCountries', StoreType.STRING) as string;
  720. setCountriesVisited(storedVisited ? JSON.parse(storedVisited) : []);
  721. }
  722. }, [visitedCountryIds]);
  723. useEffect(() => {
  724. if (visitedDareIds) {
  725. setDareVisited(visitedDareIds.ids);
  726. storage.set('visitedDares', JSON.stringify(visitedDareIds.ids));
  727. } else {
  728. const storedVisited = storage.get('visitedDares', StoreType.STRING) as string;
  729. setDareVisited(storedVisited ? JSON.parse(storedVisited) : []);
  730. }
  731. }, [visitedDareIds]);
  732. useEffect(() => {
  733. if (visitedSeriesIds && token) {
  734. setSeriesVisited(visitedSeriesIds.ids);
  735. storage.set('visitedSeries', JSON.stringify(visitedSeriesIds.ids));
  736. } else {
  737. const storedVisited = storage.get('visitedSeries', StoreType.STRING) as string;
  738. setSeriesVisited(storedVisited ? JSON.parse(storedVisited) : []);
  739. }
  740. }, [visitedSeriesIds]);
  741. useEffect(() => {
  742. if (regionsVisited && regionsVisited.length) {
  743. setRegionsVisitedFilter(generateFilter(regionsVisited));
  744. } else {
  745. setRegionsVisitedFilter(['==', 'id', -1]);
  746. }
  747. }, [regionsVisited]);
  748. useEffect(() => {
  749. if (countriesVisited && countriesVisited.length) {
  750. setCountriesVisitedFilter(generateFilter(countriesVisited));
  751. } else {
  752. setCountriesVisitedFilter(['==', 'id', -1]);
  753. }
  754. }, [countriesVisited]);
  755. useEffect(() => {
  756. if (dareVisited && dareVisited.length) {
  757. setDareVisitedFilter(generateFilter(dareVisited));
  758. } else {
  759. setDareVisitedFilter(['==', 'id', -1]);
  760. }
  761. }, [dareVisited]);
  762. useEffect(() => {
  763. if (loading) return;
  764. if (
  765. !isPremium ||
  766. !token ||
  767. showNomads ||
  768. (!nomadsFilter.friends && !nomadsFilter.countries?.length)
  769. )
  770. return;
  771. const countriesFilter = nomadsFilter.countries
  772. ? nomadsFilter.countries.map((country: any) => country.country)
  773. : [];
  774. getLocationFiltered(
  775. {
  776. token,
  777. friends: nomadsFilter.friends,
  778. trusted: nomadsFilter.trusted,
  779. countries: nomadsFilter.countries ? JSON.stringify(countriesFilter) : undefined
  780. },
  781. {
  782. onSuccess: (data: any) => {
  783. if (data && data?.geojson) {
  784. setUsersCount(data.geojson.features?.length);
  785. const rawFiltered: GeoJSON.FeatureCollection = {
  786. type: 'FeatureCollection',
  787. features: data.geojson.features.filter(
  788. (feature: GeoJSON.Feature) => feature.properties?.id !== +userId
  789. )
  790. };
  791. const filteredNomads = sanitizeNomadsGeoJSON(rawFiltered);
  792. if (!nomads || JSON.stringify(filteredNomads) !== JSON.stringify(nomads)) {
  793. setNomads(filteredNomads);
  794. }
  795. } else {
  796. setNomads(null);
  797. }
  798. },
  799. onError: (error) => {
  800. console.error('Error fetching filtered users location:', error);
  801. }
  802. }
  803. );
  804. }, [nomadsFilter, loading]);
  805. // const isMustModeEnabled = useMemo(
  806. // () => seriesFilter.visible && seriesFilter.groups?.includes(-1),
  807. // [seriesFilter]
  808. // );
  809. // const isGroupEnabled = useCallback(
  810. // (groupId: number) => {
  811. // if (!seriesFilter.visible) return false;
  812. // if (!seriesFilter.groups?.length) return true;
  813. // return seriesFilter.groups.includes(groupId);
  814. // },
  815. // [seriesFilter]
  816. // );
  817. // const seriesGroups = useMemo(() => {
  818. // if (!seriesIcons?.data) return [];
  819. // return seriesIcons.data.map((icon) => ({
  820. // id: icon.id
  821. // }));
  822. // }, [seriesIcons]);
  823. const seriesTextField = useMemo(() => {
  824. const ids = seriesFilter.labelIds;
  825. if (!ids) return ['step', ['zoom'], '', 9, ['get', 'name']] as unknown as string;
  826. if (ids.length === 0) return '';
  827. return [
  828. 'step',
  829. ['zoom'],
  830. '',
  831. 9,
  832. ['case', ['in', ['get', 'series_id'], ['literal', ids]], ['get', 'name'], '']
  833. ] as unknown as string;
  834. }, [seriesFilter.labelIds]);
  835. useEffect(() => {
  836. if (!seriesFilter.visible) {
  837. setSeriesVisitedFilter(generateFilter([]));
  838. setSeriesNotVisitedFilter(generateFilter([]));
  839. return;
  840. }
  841. if (seriesFilter.applied) {
  842. if (seriesVisited?.length) {
  843. setSeriesVisitedFilter([
  844. 'all',
  845. ['any', ...seriesVisited.filter(isValidNumber).map((id) => ['==', 'id', id])],
  846. [
  847. 'any',
  848. ...seriesFilter.groups.filter(isValidNumber).map((groupId: number) => {
  849. if (groupId === -1) {
  850. return ['==', 'must', 1];
  851. }
  852. return ['==', 'series_id', groupId];
  853. })
  854. ]
  855. ]);
  856. setSeriesNotVisitedFilter([
  857. 'all',
  858. ['all', ...seriesVisited.filter(isValidNumber).map((id) => ['!=', 'id', id])],
  859. [
  860. 'any',
  861. ...seriesFilter.groups.filter(isValidNumber).map((groupId: number) => {
  862. if (groupId === -1) {
  863. return ['==', 'must', 1];
  864. }
  865. return ['==', 'series_id', groupId];
  866. })
  867. ]
  868. ]);
  869. } else {
  870. setSeriesNotVisitedFilter([
  871. 'any',
  872. ...seriesFilter.groups.filter(isValidNumber).map((groupId: number) => {
  873. if (groupId === -1) {
  874. return ['==', 'must', 1];
  875. }
  876. return ['==', 'series_id', groupId];
  877. })
  878. ]);
  879. }
  880. } else {
  881. setSeriesVisitedFilter(
  882. seriesVisited.length
  883. ? ['any', ...seriesVisited.filter(isValidNumber).map((id) => ['==', 'id', id])]
  884. : generateFilter([])
  885. );
  886. setSeriesNotVisitedFilter(
  887. seriesVisited.length
  888. ? ['all', ...seriesVisited.filter(isValidNumber).map((id) => ['!=', 'id', id])]
  889. : ['all']
  890. );
  891. }
  892. }, [seriesVisited, seriesFilter]);
  893. useEffect(() => {
  894. if (isValidNumber(route.params?.lon) && isValidNumber(route.params?.lat)) {
  895. setMarkerCoords([route.params.lon, route.params.lat]);
  896. const timeoutId = setTimeout(() => {
  897. cameraController.setCamera({
  898. centerCoordinate: [route.params?.lon, route.params?.lat],
  899. zoomLevel: 15,
  900. animationDuration: 800,
  901. animationMode: 'flyTo'
  902. });
  903. }, 800);
  904. return () => clearTimeout(timeoutId);
  905. }
  906. if (route.params?.seriesItemShare) {
  907. const itemToFind = route.params.seriesItemShare;
  908. const shareId = itemToFind._timestamp || itemToFind.item_id;
  909. if (!processedSeriesShares.has(shareId)) {
  910. processedSeriesShares.add(shareId);
  911. handledSeriesShareRef.current = shareId;
  912. navigation.setParams({ seriesItemShare: undefined });
  913. handleFindSeries(itemToFind);
  914. }
  915. } else if (route.params?.id && route.params?.type && db1 && db2 && db3) {
  916. handleFindRegion(route.params?.id, route.params?.type);
  917. }
  918. }, [route, db1, db2, db3]);
  919. useFocusEffect(
  920. useCallback(() => {
  921. if (token) {
  922. refetch();
  923. }
  924. return () => {
  925. if (handledSeriesShareRef.current !== null) {
  926. setSelectedMarker(null);
  927. }
  928. };
  929. }, [])
  930. );
  931. useEffect(() => {
  932. if (refreshInterval > 0 && showNomads) {
  933. const intervalId = setInterval(() => {
  934. refetchUsersLocation();
  935. }, refreshInterval);
  936. return () => clearInterval(intervalId);
  937. }
  938. }, [refreshInterval, showNomads]);
  939. useEffect(() => {
  940. (async () => {
  941. let { status } = await Location.getForegroundPermissionsAsync();
  942. const isServicesEnabled = await Location.hasServicesEnabledAsync();
  943. if (locationSettings && locationSettings.sharing_refresh_interval) {
  944. setRefreshInterval(locationSettings.sharing_refresh_interval * 1000);
  945. }
  946. if (
  947. status !== 'granted' ||
  948. !token ||
  949. (locationSettings && locationSettings.sharing === 0) ||
  950. !isServicesEnabled
  951. ) {
  952. setShowNomads(false);
  953. storage.set('showNomads', false);
  954. await stopBackgroundLocationUpdates();
  955. return;
  956. }
  957. const bgStatus = await Location.getBackgroundPermissionsAsync();
  958. if (bgStatus.status !== 'granted') {
  959. // const { status } = await requestBackgroundPermissionSafe();
  960. // if (status === Location.PermissionStatus.GRANTED) {
  961. // await startBackgroundLocationUpdates();
  962. // } else {
  963. await stopBackgroundLocationUpdates();
  964. // }
  965. } else {
  966. // await startBackgroundLocationUpdates();
  967. // await restartBackgroundLocationUpdates();
  968. }
  969. try {
  970. let currentLocation = await Location.getCurrentPositionAsync({
  971. accuracy: Location.Accuracy.Balanced
  972. });
  973. setLocation(currentLocation.coords);
  974. if (locationSettings && locationSettings.sharing === 1 && token) {
  975. updateLocation({
  976. token,
  977. lat: currentLocation.coords.latitude,
  978. lng: currentLocation.coords.longitude
  979. });
  980. showNomads && refetchUsersLocation();
  981. }
  982. } catch (error) {
  983. console.error('Error fetching user location:', error);
  984. }
  985. })();
  986. }, [locationSettings]);
  987. useEffect(() => {
  988. const currentYear = moment().year();
  989. let yearSelector: { label: string; value: number }[] = [{ label: 'visited', value: 1 }];
  990. for (let i = currentYear; i >= 1951; i--) {
  991. yearSelector.push({ label: i.toString(), value: i });
  992. }
  993. handleModalStateChange({ years: yearSelector });
  994. }, []);
  995. useFocusEffect(
  996. useCallback(() => {
  997. navigation.getParent()?.setOptions({
  998. tabBarStyle: {
  999. display: regionPopupVisible ? 'none' : 'flex',
  1000. position: 'absolute',
  1001. ...Platform.select({
  1002. android: {
  1003. // height: 58
  1004. }
  1005. })
  1006. }
  1007. });
  1008. }, [regionPopupVisible, navigation])
  1009. );
  1010. const mapRef = useRef<MapLibreRN.MapViewRef>(null);
  1011. const cameraRef = useRef<MapLibreRN.CameraRef>(null);
  1012. const shapeSourceRef = useRef<MapLibreRN.ShapeSourceRef>(null);
  1013. const androidCancelRef = useRef<null | (() => void)>(null);
  1014. const setCameraAndroid = (config: any) => {
  1015. androidCancelRef.current?.();
  1016. androidCancelRef.current = null;
  1017. setRenderCamera(true);
  1018. let frame = 0;
  1019. let cancelled = false;
  1020. const startedAt = Date.now();
  1021. const MAX_WAIT_MS = 1500;
  1022. const cleanup = () => {
  1023. cancelled = true;
  1024. if (frame) cancelAnimationFrame(frame);
  1025. isAnimatingRef.current = false;
  1026. setRenderCamera(false);
  1027. };
  1028. androidCancelRef.current = cleanup;
  1029. const trySetCamera = () => {
  1030. if (cancelled) return;
  1031. if (cameraRef.current) {
  1032. cameraRef.current.setCamera(config);
  1033. const duration = (config.animationDuration || 1000) + 200;
  1034. animationTimeoutRef.current && clearTimeout(animationTimeoutRef.current);
  1035. animationTimeoutRef.current = setTimeout(() => {
  1036. if (!cancelled) cleanup();
  1037. }, duration);
  1038. return;
  1039. }
  1040. if (Date.now() - startedAt > MAX_WAIT_MS) {
  1041. cleanup();
  1042. return;
  1043. }
  1044. frame = requestAnimationFrame(trySetCamera);
  1045. };
  1046. frame = requestAnimationFrame(trySetCamera);
  1047. };
  1048. const cameraController = {
  1049. setCamera: useCallback((config: any) => {
  1050. isAnimatingRef.current = true;
  1051. if (animationTimeoutRef.current) {
  1052. clearTimeout(animationTimeoutRef.current);
  1053. }
  1054. if (Platform.OS === 'android') {
  1055. setRenderCamera(true);
  1056. setCameraAndroid(config);
  1057. return;
  1058. } else {
  1059. cameraRef.current?.setCamera(config);
  1060. animationTimeoutRef.current = setTimeout(
  1061. () => {
  1062. isAnimatingRef.current = false;
  1063. },
  1064. (config.animationDuration || 1000) + 100
  1065. );
  1066. }
  1067. }, []),
  1068. fitBounds: useCallback((ne: number[], sw: number[], padding: number[], duration: number) => {
  1069. isAnimatingRef.current = true;
  1070. if (animationTimeoutRef.current) {
  1071. clearTimeout(animationTimeoutRef.current);
  1072. }
  1073. if (Platform.OS === 'android') {
  1074. setRenderCamera(true);
  1075. requestAnimationFrame(() => {
  1076. cameraRef.current?.fitBounds(ne, sw, padding, duration);
  1077. });
  1078. animationTimeoutRef.current = setTimeout(() => {
  1079. isAnimatingRef.current = false;
  1080. setRenderCamera(false);
  1081. }, duration + 200);
  1082. } else {
  1083. cameraRef.current?.fitBounds(ne, sw, padding, duration);
  1084. animationTimeoutRef.current = setTimeout(() => {
  1085. isAnimatingRef.current = false;
  1086. }, duration + 100);
  1087. }
  1088. }, []),
  1089. flyTo: useCallback((coordinates: number[], duration: number = 1000) => {
  1090. isAnimatingRef.current = true;
  1091. if (animationTimeoutRef.current) {
  1092. clearTimeout(animationTimeoutRef.current);
  1093. }
  1094. if (Platform.OS === 'android') {
  1095. setRenderCamera(true);
  1096. requestAnimationFrame(() => {
  1097. cameraRef.current?.flyTo(coordinates, duration);
  1098. });
  1099. animationTimeoutRef.current = setTimeout(() => {
  1100. isAnimatingRef.current = false;
  1101. setRenderCamera(false);
  1102. }, duration + 200);
  1103. } else {
  1104. cameraRef.current?.flyTo(coordinates, duration);
  1105. animationTimeoutRef.current = setTimeout(() => {
  1106. isAnimatingRef.current = false;
  1107. }, duration + 100);
  1108. }
  1109. }, []),
  1110. moveTo: useCallback((coordinates: number[], duration: number = 0) => {
  1111. isAnimatingRef.current = true;
  1112. if (animationTimeoutRef.current) {
  1113. clearTimeout(animationTimeoutRef.current);
  1114. }
  1115. if (Platform.OS === 'android') {
  1116. setRenderCamera(true);
  1117. requestAnimationFrame(() => {
  1118. cameraRef.current?.moveTo(coordinates, duration);
  1119. });
  1120. animationTimeoutRef.current = setTimeout(() => {
  1121. isAnimatingRef.current = false;
  1122. setRenderCamera(false);
  1123. }, duration + 200);
  1124. } else {
  1125. cameraRef.current?.moveTo(coordinates, duration);
  1126. animationTimeoutRef.current = setTimeout(() => {
  1127. isAnimatingRef.current = false;
  1128. }, duration + 100);
  1129. }
  1130. }, []),
  1131. zoomTo: useCallback((zoomLevel: number, duration: number = 1000) => {
  1132. isAnimatingRef.current = true;
  1133. if (animationTimeoutRef.current) {
  1134. clearTimeout(animationTimeoutRef.current);
  1135. }
  1136. if (Platform.OS === 'android') {
  1137. setRenderCamera(true);
  1138. requestAnimationFrame(() => {
  1139. cameraRef.current?.zoomTo(zoomLevel, duration);
  1140. });
  1141. animationTimeoutRef.current = setTimeout(() => {
  1142. isAnimatingRef.current = false;
  1143. setRenderCamera(false);
  1144. }, duration + 200);
  1145. } else {
  1146. cameraRef.current?.zoomTo(zoomLevel, duration);
  1147. animationTimeoutRef.current = setTimeout(() => {
  1148. isAnimatingRef.current = false;
  1149. }, duration + 100);
  1150. }
  1151. }, [])
  1152. };
  1153. useEffect(() => {
  1154. return () => {
  1155. androidCancelRef.current?.();
  1156. if (animationTimeoutRef.current) clearTimeout(animationTimeoutRef.current);
  1157. };
  1158. }, []);
  1159. useEffect(() => {
  1160. if (userInfo) {
  1161. setUserInfoData(JSON.parse(userInfo));
  1162. }
  1163. }, [userInfo]);
  1164. const requestBackgroundPermissionSafe = async () => {
  1165. await new Promise((resolve) => setTimeout(resolve, 300));
  1166. return await Location.requestBackgroundPermissionsAsync();
  1167. };
  1168. const handlePress = () => {
  1169. if (isExpanded) {
  1170. setSearchInput('');
  1171. }
  1172. setIsExpanded((prev) => !prev);
  1173. width.value = withTiming(isExpanded ? 48 : usableWidth, {
  1174. duration: 300,
  1175. easing: Easing.inOut(Easing.ease)
  1176. });
  1177. };
  1178. const animatedStyle = useAnimatedStyle(() => {
  1179. return {
  1180. width: width.value
  1181. };
  1182. });
  1183. const loadInitialRegion = () => {
  1184. try {
  1185. const savedInitialRegion = storage.get('initialRegion', StoreType.STRING) as string;
  1186. if (savedInitialRegion) {
  1187. const region = JSON.parse(savedInitialRegion);
  1188. setInitialRegion(region);
  1189. }
  1190. } catch (e) {
  1191. console.error('Failed to load saved initial region:', e);
  1192. }
  1193. };
  1194. useEffect(() => {
  1195. loadInitialRegion();
  1196. }, []);
  1197. useEffect(() => {
  1198. if (
  1199. initialRegion &&
  1200. !route.params?.id &&
  1201. isValidNumber(initialRegion.longitude) &&
  1202. isValidNumber(initialRegion.latitude)
  1203. ) {
  1204. const timeoutId = setTimeout(() => {
  1205. cameraController.setCamera({
  1206. centerCoordinate: [initialRegion.longitude, initialRegion.latitude],
  1207. zoomLevel:
  1208. isValidNumber(initialRegion.latitudeDelta) && initialRegion.latitudeDelta > 0
  1209. ? Math.log2(360 / initialRegion.latitudeDelta)
  1210. : 0,
  1211. animationDuration: 500,
  1212. animationMode: 'flyTo'
  1213. });
  1214. }, 500);
  1215. return () => clearTimeout(timeoutId);
  1216. }
  1217. }, [initialRegion]);
  1218. const handleMapChange = async () => {
  1219. if (!mapRef.current || isAnimatingRef.current) return;
  1220. if (hideTimer.current) clearTimeout(hideTimer.current);
  1221. setIsZooming(true);
  1222. const newZoom = await mapRef.current.getZoom();
  1223. const newCenter = await mapRef.current.getCenter();
  1224. setZoom((prevZoom) => {
  1225. if (prevZoom !== newZoom) return newZoom;
  1226. return prevZoom;
  1227. });
  1228. setCenter((prevCenter) => {
  1229. if (!prevCenter || prevCenter[0] !== newCenter[0] || prevCenter[0] !== newCenter[0]) {
  1230. return newCenter;
  1231. }
  1232. return prevCenter;
  1233. });
  1234. };
  1235. useEffect(() => {
  1236. return () => {
  1237. if (animationTimeoutRef.current) {
  1238. clearTimeout(animationTimeoutRef.current);
  1239. }
  1240. };
  1241. }, []);
  1242. const onMapPress = async (event: any) => {
  1243. if (!mapRef.current) return;
  1244. // const { screenPointX, screenPointY } = event.properties;
  1245. // const seriesLayerIds = seriesGroups.flatMap((g) => [
  1246. // `series-${g.id}-unvisited`,
  1247. // `series-${g.id}-must-icon`,
  1248. // `series-${g.id}-visited-normal`,
  1249. // `series-${g.id}-visited-must`
  1250. // ]);
  1251. // const { features: seriesFeatures } = await mapRef.current.queryRenderedFeaturesAtPoint(
  1252. // [screenPointX, screenPointY],
  1253. // undefined,
  1254. // seriesLayerIds
  1255. // );
  1256. // if (seriesFeatures?.length) {
  1257. // handleMarkerPress({ features: seriesFeatures });
  1258. // return;
  1259. // }
  1260. if (selectedMarker || selectedUser) {
  1261. closeCallout();
  1262. return;
  1263. }
  1264. if (type === 'blank') return;
  1265. try {
  1266. const { screenPointX, screenPointY } = event.properties;
  1267. const [lng, lat] = event.geometry?.coordinates;
  1268. const { features } = await mapRef.current.queryRenderedFeaturesAtPoint(
  1269. [screenPointX, screenPointY],
  1270. undefined,
  1271. ['regions', 'countries', 'dare']
  1272. );
  1273. if (features?.length) {
  1274. let region = features[0];
  1275. try {
  1276. if (typeof lng === 'number' && typeof lat === 'number') {
  1277. const p = point([lng, lat]);
  1278. const inside = features
  1279. .filter(
  1280. (f) => f?.geometry?.type === 'Polygon' || f?.geometry?.type === 'MultiPolygon'
  1281. )
  1282. .filter((f) => booleanPointInPolygon(p, f as any));
  1283. const best = inside.slice().sort((a, b) => area(a as any) - area(b as any))[0];
  1284. if (best) {
  1285. region = best;
  1286. }
  1287. }
  1288. } catch (e) {
  1289. console.warn('region detection failed', e);
  1290. }
  1291. if (selectedRegion === region.properties?.id) return;
  1292. let db = type === 'regions' ? db1 : type === 'countries' ? db3 : db2;
  1293. let tableName = type === 'dare' ? 'places' : type;
  1294. let foundRegion = region.properties?.id;
  1295. setSelectedRegion(region.properties?.id);
  1296. if (region.properties?.alt_id) {
  1297. setSelectedRegion(region.properties?.alt_id);
  1298. db = db1;
  1299. tableName = 'regions';
  1300. }
  1301. await getData(db, foundRegion, tableName, handleRegionData)
  1302. .then(() => {
  1303. setRegionPopupVisible(true);
  1304. })
  1305. .catch((error) => {
  1306. console.error('Error fetching data', error);
  1307. refreshDatabases();
  1308. });
  1309. if (tableName === 'regions') {
  1310. const isVisitedLocal = regionsVisited.includes(+foundRegion);
  1311. setUserData({ type: 'nm', id: +foundRegion, visited: isVisitedLocal });
  1312. if (token) {
  1313. await mutateUserData(
  1314. { region_id: +foundRegion, token: String(token) },
  1315. {
  1316. onSuccess: (data) => {
  1317. setUserData({ type: 'nm', id: +foundRegion, ...data });
  1318. }
  1319. }
  1320. );
  1321. } else {
  1322. setUserData({ type: 'nm', id: +foundRegion });
  1323. }
  1324. if (regionsList && regionsList.data) {
  1325. const region = regionsList.data.find((region) => region.id === +foundRegion);
  1326. if (region && region.bbox) {
  1327. const bounds = turf.bbox(region.bbox);
  1328. if (bounds.every(isValidNumber)) {
  1329. cameraController.fitBounds(
  1330. [bounds[2], bounds[3]],
  1331. [bounds[0], bounds[1]],
  1332. [10, 10, 50, 10],
  1333. 1000
  1334. );
  1335. }
  1336. }
  1337. }
  1338. } else if (tableName === 'countries') {
  1339. const isVisitedLocal = countriesVisited.includes(+foundRegion);
  1340. setUserData({ type: 'countries', id: +foundRegion, visited: isVisitedLocal });
  1341. if (token) {
  1342. await mutateCountriesData(
  1343. { id: +foundRegion, token },
  1344. {
  1345. onSuccess: (data) => {
  1346. setUserData({ type: 'countries', id: +foundRegion, ...data.data });
  1347. }
  1348. }
  1349. );
  1350. } else {
  1351. setUserData({ type: 'countries', id: +foundRegion });
  1352. }
  1353. if (countriesList && countriesList.data) {
  1354. const region = countriesList.data.find((region) => region.id === +foundRegion);
  1355. if (region && region.bbox) {
  1356. const bounds = turf.bbox(region.bbox);
  1357. if (bounds.every(isValidNumber)) {
  1358. cameraController.fitBounds(
  1359. [bounds[2], bounds[3]],
  1360. [bounds[0], bounds[1]],
  1361. [10, 10, 50, 10],
  1362. 1000
  1363. );
  1364. }
  1365. }
  1366. }
  1367. } else {
  1368. const isVisitedLocal = dareVisited.includes(+foundRegion);
  1369. setUserData({ type: 'dare', id: +foundRegion, visited: isVisitedLocal });
  1370. if (token) {
  1371. await mutateUserDataDare(
  1372. { dare_id: +foundRegion, token: String(token) },
  1373. {
  1374. onSuccess: (data) => {
  1375. setUserData({ type: 'dare', id: +foundRegion, ...data });
  1376. }
  1377. }
  1378. );
  1379. } else {
  1380. setUserData({ type: 'dare', id: +foundRegion });
  1381. }
  1382. if (dareList && dareList.data) {
  1383. const region = dareList.data.find((region) => region.id === +foundRegion);
  1384. if (region && region.bbox) {
  1385. const bounds = turf.bbox(region.bbox);
  1386. if (bounds.every(isValidNumber)) {
  1387. cameraController.fitBounds(
  1388. [bounds[2], bounds[3]],
  1389. [bounds[0], bounds[1]],
  1390. [10, 10, 50, 10],
  1391. 1000
  1392. );
  1393. }
  1394. }
  1395. }
  1396. }
  1397. } else {
  1398. handleClosePopup();
  1399. }
  1400. } catch (error) {
  1401. console.error('Error onMapPress features:', error);
  1402. }
  1403. };
  1404. const handleRegionDidChange = async (feature: GeoJSON.Feature<GeoJSON.Point, any>) => {
  1405. hideTimer.current = setTimeout(() => {
  1406. setIsZooming(false);
  1407. }, 2000);
  1408. if (!feature) return;
  1409. const { zoomLevel } = feature.properties;
  1410. const { coordinates } = feature.geometry;
  1411. if (!zoomLevel || !coordinates) return;
  1412. const latitudeDelta = 360 / 2 ** zoomLevel;
  1413. const longitudeDelta = latitudeDelta;
  1414. const region = {
  1415. latitude: coordinates[1],
  1416. longitude: coordinates[0],
  1417. latitudeDelta,
  1418. longitudeDelta
  1419. };
  1420. storage.set('initialRegion', JSON.stringify(region));
  1421. };
  1422. const handleClosePopup = async () => {
  1423. setSelectedRegion(null);
  1424. setRegionPopupVisible(false);
  1425. setRegionData(null);
  1426. };
  1427. const handleGetLocation = async () => {
  1428. setIsLocationLoading(true);
  1429. try {
  1430. let { status, canAskAgain } = await Location.getForegroundPermissionsAsync();
  1431. const isServicesEnabled = await Location.hasServicesEnabledAsync();
  1432. if (status === 'granted' && isServicesEnabled) {
  1433. const bgStatus = await Location.getBackgroundPermissionsAsync();
  1434. if (bgStatus.status !== 'granted') {
  1435. // const { status } = await requestBackgroundPermissionSafe();
  1436. // if (status === Location.PermissionStatus.GRANTED) {
  1437. // await startBackgroundLocationUpdates();
  1438. // } else {
  1439. await stopBackgroundLocationUpdates();
  1440. // }
  1441. } else {
  1442. // await startBackgroundLocationUpdates();
  1443. }
  1444. await getLocation();
  1445. } else if (!canAskAgain || !isServicesEnabled) {
  1446. setOpenSettingsVisible(true);
  1447. } else {
  1448. setAskLocationVisible(true);
  1449. }
  1450. } finally {
  1451. setIsLocationLoading(false);
  1452. }
  1453. };
  1454. const getLocation = async () => {
  1455. try {
  1456. let currentLocation = await Location.getCurrentPositionAsync({
  1457. accuracy: Platform.OS === 'ios' ? Location.Accuracy.Balanced : Location.Accuracy.Low
  1458. });
  1459. setLocation(currentLocation.coords);
  1460. if (currentLocation.coords) {
  1461. cameraController.flyTo(
  1462. [currentLocation.coords.longitude, currentLocation.coords.latitude],
  1463. 1000
  1464. );
  1465. }
  1466. if (locationSettings && locationSettings.sharing === 1 && token) {
  1467. updateLocation({
  1468. token,
  1469. lat: currentLocation.coords.latitude,
  1470. lng: currentLocation.coords.longitude
  1471. });
  1472. showNomads && refetchUsersLocation();
  1473. }
  1474. handleClosePopup();
  1475. } catch (error) {
  1476. console.error('Error fetching user location:', error);
  1477. }
  1478. };
  1479. const handleAcceptPermission = async () => {
  1480. setAskLocationVisible(false);
  1481. let { status, canAskAgain } = await Location.requestForegroundPermissionsAsync();
  1482. const isServicesEnabled = await Location.hasServicesEnabledAsync();
  1483. if (status === 'granted' && isServicesEnabled) {
  1484. await getLocation();
  1485. } else if (!canAskAgain || !isServicesEnabled) {
  1486. setOpenSettingsVisible(true);
  1487. }
  1488. };
  1489. const handleOpenEditModal = () => {
  1490. navigation.navigate(NAVIGATION_PAGES.TRIPS_2025, {
  1491. region: { id: regionData.id, name: regionData.name }
  1492. });
  1493. };
  1494. const handleOpenEditSlowModal = () => {
  1495. setIsEditSlowModalVisible(true);
  1496. };
  1497. const handleSearch = async () => {
  1498. setSearch(searchInput);
  1499. setSearchVisible(true);
  1500. };
  1501. const handleCloseModal = () => {
  1502. setSearchInput('');
  1503. setSearchVisible(false);
  1504. handlePress();
  1505. };
  1506. const handleRegionData = async (regionData: any, avatars: string[]) => {
  1507. if (!regionData) {
  1508. await refreshDatabases();
  1509. }
  1510. setRegionData(regionData);
  1511. setUserAvatars(avatars);
  1512. };
  1513. const handleFindRegion = async (id: number, type: 'regions' | 'countries' | 'places') => {
  1514. setType(type === 'places' ? 'dare' : type);
  1515. if (!db1 || !db2 || !db3) {
  1516. return;
  1517. }
  1518. const db = type === 'regions' ? db1 : type === 'countries' ? db3 : db2;
  1519. if (id) {
  1520. setSelectedRegion(id);
  1521. await getData(db, id, type, handleRegionData)
  1522. .then(() => {
  1523. setRegionPopupVisible(true);
  1524. })
  1525. .catch((error) => {
  1526. console.error('Error fetching data', error);
  1527. refreshDatabases();
  1528. });
  1529. if (type === 'regions') {
  1530. const isVisitedLocal = regionsVisited.includes(+id);
  1531. setUserData({ type: 'nm', id, visited: isVisitedLocal });
  1532. if (token) {
  1533. await mutateUserData(
  1534. { region_id: id, token: String(token) },
  1535. {
  1536. onSuccess: (data) => {
  1537. setUserData({ type: 'nm', id, ...data });
  1538. }
  1539. }
  1540. );
  1541. } else {
  1542. setUserData({ type: 'nm', id });
  1543. }
  1544. if (regionsList && regionsList.data) {
  1545. const region = regionsList.data.find((region) => region.id === +id);
  1546. if (region && region.bbox) {
  1547. const bounds = turf.bbox(region.bbox);
  1548. if (bounds.every(isValidNumber)) {
  1549. cameraController.fitBounds(
  1550. [bounds[2], bounds[3]],
  1551. [bounds[0], bounds[1]],
  1552. [10, 10, 50, 10],
  1553. 1000
  1554. );
  1555. }
  1556. }
  1557. }
  1558. } else if (type === 'countries') {
  1559. const isVisitedLocal = countriesVisited.includes(+id);
  1560. setUserData({ type: 'countries', id, visited: isVisitedLocal });
  1561. if (token) {
  1562. await mutateCountriesData(
  1563. { id, token },
  1564. {
  1565. onSuccess: (data) => {
  1566. setUserData({ type: 'countries', id, ...data.data });
  1567. }
  1568. }
  1569. );
  1570. } else {
  1571. setUserData({ type: 'countries', id });
  1572. }
  1573. if (countriesList && countriesList.data) {
  1574. const region = countriesList.data.find((region) => region.id === +id);
  1575. if (region && region.bbox) {
  1576. const bounds = turf.bbox(region.bbox);
  1577. if (bounds.every(isValidNumber)) {
  1578. cameraController.fitBounds(
  1579. [bounds[2], bounds[3]],
  1580. [bounds[0], bounds[1]],
  1581. [10, 10, 50, 10],
  1582. 1000
  1583. );
  1584. }
  1585. }
  1586. }
  1587. } else {
  1588. const isVisitedLocal = dareVisited.includes(+id);
  1589. setUserData({ type: 'dare', id: +id, visited: isVisitedLocal });
  1590. if (token) {
  1591. await mutateUserDataDare(
  1592. { dare_id: +id, token: String(token) },
  1593. {
  1594. onSuccess: (data) => {
  1595. setUserData({ type: 'dare', id: +id, ...data });
  1596. }
  1597. }
  1598. );
  1599. } else {
  1600. setUserData({ type: 'dare', id: +id });
  1601. }
  1602. if (dareList && dareList.data) {
  1603. const region = dareList.data.find((region) => region.id === +id);
  1604. if (region && region.bbox) {
  1605. const bounds = turf.bbox(region.bbox);
  1606. if (bounds.every(isValidNumber)) {
  1607. cameraController.fitBounds(
  1608. [bounds[2], bounds[3]],
  1609. [bounds[0], bounds[1]],
  1610. [10, 10, 50, 10],
  1611. 1000
  1612. );
  1613. }
  1614. }
  1615. }
  1616. }
  1617. } else {
  1618. handleClosePopup();
  1619. }
  1620. };
  1621. const handleFindSeries = async (seriesItem: any) => {
  1622. let coordinates = null;
  1623. try {
  1624. if (seriesItem.location_geojson) {
  1625. const geojsonObj = JSON.parse(seriesItem.location_geojson);
  1626. if (geojsonObj.type === 'Point') {
  1627. const parsedCoords = geojsonObj.coordinates;
  1628. if (parsedCoords && parsedCoords.length === 2) {
  1629. coordinates = [parsedCoords[1], parsedCoords[0]];
  1630. }
  1631. } else if (
  1632. geojsonObj.type === 'Polygon' ||
  1633. geojsonObj.type === 'MultiPolygon' ||
  1634. geojsonObj.type === 'GeometryCollection'
  1635. ) {
  1636. try {
  1637. const center = turf.centroid(geojsonObj);
  1638. if (center && center.geometry && center.geometry.coordinates) {
  1639. const parsedCoords = center.geometry.coordinates;
  1640. coordinates = [parsedCoords[1], parsedCoords[0]];
  1641. }
  1642. } catch (err) {
  1643. console.error('Error finding centroid for series', err);
  1644. let firstPoint;
  1645. if (geojsonObj.type === 'Polygon' && geojsonObj.coordinates?.[0]?.[0]) {
  1646. firstPoint = geojsonObj.coordinates[0][0];
  1647. } else if (geojsonObj.type === 'MultiPolygon' && geojsonObj.coordinates?.[0]?.[0]?.[0]) {
  1648. firstPoint = geojsonObj.coordinates[0][0][0];
  1649. } else if (geojsonObj.type === 'GeometryCollection' && geojsonObj.geometries?.[0]) {
  1650. const firstGeom = geojsonObj.geometries[0];
  1651. if (firstGeom.type === 'Point' && firstGeom.coordinates) {
  1652. firstPoint = firstGeom.coordinates;
  1653. } else if (firstGeom.type === 'Polygon' && firstGeom.coordinates?.[0]?.[0]) {
  1654. firstPoint = firstGeom.coordinates[0][0];
  1655. }
  1656. }
  1657. if (firstPoint && firstPoint.length >= 2) {
  1658. coordinates = [firstPoint[1], firstPoint[0]];
  1659. }
  1660. }
  1661. }
  1662. }
  1663. } catch (e) {
  1664. console.error('Error parsing coordinates for series', e);
  1665. }
  1666. if (coordinates) {
  1667. setTimeout(() => {
  1668. cameraController.setCamera({
  1669. centerCoordinate: coordinates,
  1670. zoomLevel: 10,
  1671. animationDuration: 1000,
  1672. animationMode: 'flyTo'
  1673. });
  1674. }, 800);
  1675. handledSeriesShareRef.current = seriesItem.item_id;
  1676. setSelectedMarker({
  1677. coordinates,
  1678. name: seriesItem.item_name,
  1679. icon: seriesItem.series_icon ? { uri: API_HOST + seriesItem.series_icon } : null,
  1680. description: seriesItem.series_name,
  1681. series_name: seriesItem.series_name,
  1682. visited: seriesItem.visited,
  1683. series_id: seriesItem.series_id,
  1684. id: seriesItem.item_id,
  1685. avatars: null,
  1686. no_visited: 0
  1687. });
  1688. setSelectedUser(null);
  1689. setTimeout(async () => {
  1690. try {
  1691. if (!mapRef.current) return;
  1692. const pointInView = await mapRef.current.getPointInView(coordinates);
  1693. if (pointInView) {
  1694. const { features } = await mapRef.current.queryRenderedFeaturesAtPoint(
  1695. pointInView,
  1696. undefined,
  1697. []
  1698. );
  1699. if (features && features.length > 0) {
  1700. const feature = features.find(
  1701. (f: any) => f.properties && f.properties.id === seriesItem.item_id
  1702. );
  1703. if (feature) {
  1704. feature.properties.override_visited = seriesItem.visited;
  1705. handleMarkerPress({ features: [feature] });
  1706. }
  1707. }
  1708. }
  1709. } catch (e) {
  1710. console.log('Failed to query map features for series', e);
  1711. }
  1712. }, 1500);
  1713. }
  1714. };
  1715. const handleMarkerPress = async (event: any) => {
  1716. const { features } = event;
  1717. if (features?.length) {
  1718. const uniqueFeatures = features
  1719. ? Array.from(new Map(features.map((f: any) => [f.properties.id, f])).values())
  1720. : [];
  1721. if (uniqueFeatures.length > 1) {
  1722. const markers = uniqueFeatures
  1723. .map((f: any) => {
  1724. const markerCoordinates = f.geometry.coordinates;
  1725. if (!markerCoordinates) return null;
  1726. return {
  1727. coordinates: markerCoordinates,
  1728. name: f.properties.name,
  1729. icon: images[f.properties.series_id],
  1730. description: f.properties.description,
  1731. series_name: f.properties.series_name,
  1732. visited: f.properties.override_visited !== undefined ? f.properties.override_visited : (seriesVisited.includes(f.properties.id) ? 1 : 0),
  1733. series_id: f.properties.series_id,
  1734. id: f.properties.id,
  1735. avatars: f.properties.avatars,
  1736. no_visited: f.properties.no_visited
  1737. };
  1738. })
  1739. .filter(Boolean)
  1740. .sort((a: any, b: any) => a.visited - b.visited);
  1741. SheetManager.show('multiple-series-modal', {
  1742. payload: {
  1743. markers,
  1744. token,
  1745. toggleSeries,
  1746. setSelectedMarker,
  1747. setIsWarningModalVisible,
  1748. setPremiumModalVisible
  1749. } as any
  1750. });
  1751. setSelectedUser(null);
  1752. return;
  1753. }
  1754. const selectedFeature = features[0];
  1755. const { coordinates } = selectedFeature.geometry;
  1756. const { name, description, series_name, series_id, id, avatars, no_visited, override_visited } = selectedFeature.properties;
  1757. const visited = override_visited !== undefined ? override_visited : (seriesVisited.includes(id) ? 1 : 0);
  1758. const icon = images[series_id];
  1759. if (coordinates) {
  1760. setSelectedMarker((prev: any) => {
  1761. const isSameMarker = prev && prev.id === id;
  1762. const currentVisited = isSameMarker ? prev.visited : visited;
  1763. return {
  1764. coordinates,
  1765. name,
  1766. icon: icon || (isSameMarker ? prev.icon : null),
  1767. description,
  1768. series_name,
  1769. visited: currentVisited,
  1770. series_id,
  1771. id,
  1772. avatars,
  1773. no_visited
  1774. };
  1775. });
  1776. setSelectedUser(null);
  1777. }
  1778. }
  1779. };
  1780. const closeCallout = () => {
  1781. setSelectedMarker(null);
  1782. setSelectedUser(null);
  1783. };
  1784. const toggleSeries = useCallback(
  1785. async (item: any) => {
  1786. if (!token) {
  1787. setIsWarningModalVisible(true);
  1788. return;
  1789. }
  1790. const itemData = {
  1791. token,
  1792. series_id: item.series_id,
  1793. item_id: item.id,
  1794. checked: (item.visited === 0 ? 1 : 0) as 0 | 1,
  1795. double: 0 as 0 | 1
  1796. };
  1797. try {
  1798. updateSeriesItem(itemData);
  1799. if (item.visited === 1) {
  1800. setSeriesVisited((current) => current.filter((id) => id !== item.id));
  1801. setSelectedMarker((current: any) =>
  1802. current && current.id === item.id ? { ...current, visited: 0 } : current
  1803. );
  1804. } else {
  1805. setSeriesVisited((current) => [...current, item.id]);
  1806. setSelectedMarker((current: any) =>
  1807. current && current.id === item.id ? { ...current, visited: 1 } : current
  1808. );
  1809. }
  1810. } catch (error) {
  1811. console.error('Failed to update series state', error);
  1812. }
  1813. },
  1814. [token, updateSeriesItem]
  1815. );
  1816. const handleModalStateChange = (updates: { [key: string]: any }) => {
  1817. setModalState((prevState) => ({ ...prevState, ...updates }));
  1818. };
  1819. const handleUserPress = (event: any) => {
  1820. const selectedFeature = event.features[0];
  1821. const { coordinates } = selectedFeature.geometry;
  1822. const { avatar, first_name, last_name, flag, id, last_seen } = selectedFeature.properties;
  1823. if (selectedFeature) {
  1824. setSelectedUser({
  1825. coordinates,
  1826. avatar: avatar ? { uri: API_HOST + avatar } : logo,
  1827. first_name,
  1828. last_name,
  1829. flag: { uri: API_HOST + flag },
  1830. id,
  1831. last_seen
  1832. });
  1833. setSelectedMarker(null);
  1834. }
  1835. };
  1836. return (
  1837. <SafeAreaView style={{ height: '100%' }}>
  1838. <StatusBar translucent backgroundColor="transparent" />
  1839. <MapLibreRN.MapView
  1840. ref={mapRef}
  1841. style={styles.map}
  1842. mapStyle={VECTOR_MAP_HOST + '/nomadmania-maps2025.json'}
  1843. // mapStyle={VECTOR_MAP_HOST + '/nomadmania-maps2026.json'}
  1844. rotateEnabled={false}
  1845. attributionEnabled={false}
  1846. onPress={onMapPress}
  1847. onRegionDidChange={handleRegionDidChange}
  1848. onRegionWillChange={_.debounce(handleMapChange, 200)}
  1849. onDidFinishLoadingStyle={() => setDidFinishLoadingStyle(true)}
  1850. >
  1851. {/* <MapLibreRN.Images
  1852. images={{
  1853. ...images,
  1854. 'default-series-icon': defaultSeriesIcon
  1855. }}
  1856. onImageMissing={(image) => {
  1857. try {
  1858. if (processedImages.current.has(image)) {
  1859. return;
  1860. }
  1861. processedImages.current.add(image);
  1862. setImages((prevImages: any) => ({
  1863. ...prevImages,
  1864. [image]: defaultSeriesIcon
  1865. }));
  1866. } catch (error) {
  1867. console.error('Error in onImageMissing:', error);
  1868. }
  1869. }}
  1870. >
  1871. <View />
  1872. </MapLibreRN.Images> */}
  1873. {markerCoords && (
  1874. <MapLibreRN.PointAnnotation id="marker" coordinate={markerCoords}>
  1875. <View
  1876. style={{
  1877. height: 24,
  1878. width: 24,
  1879. backgroundColor: Colors.ORANGE,
  1880. borderRadius: 12,
  1881. borderColor: Colors.WHITE,
  1882. borderWidth: 2
  1883. }}
  1884. />
  1885. </MapLibreRN.PointAnnotation>
  1886. )}
  1887. <>
  1888. <MapLibreRN.LineLayer
  1889. id="nm-regions-line-layer"
  1890. sourceID={regions.source}
  1891. sourceLayerID={regions['source-layer']}
  1892. filter={regions.filter as any}
  1893. maxZoomLevel={regions.maxzoom}
  1894. style={{
  1895. lineColor: 'rgba(14, 80, 109, 1)',
  1896. lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 0.2, 4, 1, 5, 1.5, 12, 3],
  1897. lineWidthTransition: { duration: 300, delay: 0 },
  1898. visibility: type === 'regions' ? 'visible' : 'none'
  1899. }}
  1900. belowLayerID="waterway-name"
  1901. />
  1902. <MapLibreRN.FillLayer
  1903. id={regions.id}
  1904. sourceID={regions.source}
  1905. sourceLayerID={regions['source-layer']}
  1906. filter={regions.filter as any}
  1907. style={{
  1908. ...regions.style,
  1909. visibility: type === 'regions' ? 'visible' : 'none'
  1910. }}
  1911. maxZoomLevel={regions.maxzoom}
  1912. belowLayerID={regions_visited.id}
  1913. />
  1914. <MapLibreRN.FillLayer
  1915. id={regions_visited.id}
  1916. sourceID={regions_visited.source}
  1917. sourceLayerID={regions_visited['source-layer']}
  1918. filter={regionsVisitedFilter as any}
  1919. style={{
  1920. ...regions_visited.style,
  1921. visibility: type === 'regions' ? 'visible' : 'none'
  1922. }}
  1923. maxZoomLevel={regions_visited.maxzoom}
  1924. belowLayerID="waterway-name"
  1925. />
  1926. </>
  1927. <>
  1928. <MapLibreRN.LineLayer
  1929. id="countries-line-layer"
  1930. sourceID={countries.source}
  1931. sourceLayerID={countries['source-layer']}
  1932. filter={countries.filter as any}
  1933. maxZoomLevel={countries.maxzoom}
  1934. style={{
  1935. lineColor: 'rgba(14, 80, 109, 1)',
  1936. lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 0.2, 4, 1, 5, 1.5, 12, 3],
  1937. lineWidthTransition: { duration: 300, delay: 0 },
  1938. visibility: type === 'countries' ? 'visible' : 'none'
  1939. }}
  1940. belowLayerID="waterway-name"
  1941. />
  1942. <MapLibreRN.FillLayer
  1943. id={countries.id}
  1944. sourceID={countries.source}
  1945. sourceLayerID={countries['source-layer']}
  1946. filter={countries.filter as any}
  1947. style={{
  1948. ...countries.style,
  1949. visibility: type === 'countries' ? 'visible' : 'none'
  1950. }}
  1951. maxZoomLevel={countries.maxzoom}
  1952. belowLayerID={countries_visited.id}
  1953. />
  1954. <MapLibreRN.FillLayer
  1955. id={countries_visited.id}
  1956. sourceID={countries_visited.source}
  1957. sourceLayerID={countries_visited['source-layer']}
  1958. filter={countriesVisitedFilter as any}
  1959. style={{
  1960. ...countries_visited.style,
  1961. visibility: type === 'countries' ? 'visible' : 'none'
  1962. }}
  1963. maxZoomLevel={countries_visited.maxzoom}
  1964. belowLayerID="waterway-name"
  1965. />
  1966. </>
  1967. <>
  1968. <MapLibreRN.FillLayer
  1969. id={dare.id}
  1970. sourceID={dare.source}
  1971. sourceLayerID={dare['source-layer']}
  1972. filter={dare.filter as any}
  1973. style={{
  1974. ...dare.style,
  1975. visibility: type === 'dare' ? 'visible' : 'none'
  1976. }}
  1977. maxZoomLevel={dare.maxzoom}
  1978. belowLayerID={dare_visited.id}
  1979. />
  1980. <MapLibreRN.FillLayer
  1981. id={dare_visited.id}
  1982. sourceID={dare_visited.source}
  1983. sourceLayerID={dare_visited['source-layer']}
  1984. filter={dareVisitedFilter as any}
  1985. style={{
  1986. ...dare_visited.style,
  1987. visibility: type === 'dare' ? 'visible' : 'none'
  1988. }}
  1989. maxZoomLevel={dare_visited.maxzoom}
  1990. belowLayerID="waterway-name"
  1991. />
  1992. </>
  1993. {selectedRegion && type && (
  1994. <>
  1995. <MapLibreRN.FillLayer
  1996. id={selected_region.id}
  1997. sourceID={type}
  1998. sourceLayerID={type}
  1999. filter={['==', 'id', selectedRegion]}
  2000. style={selected_region.style}
  2001. maxZoomLevel={selected_region.maxzoom}
  2002. belowLayerID="waterway-name"
  2003. />
  2004. <MapLibreRN.LineLayer
  2005. id={selected_region_outline.id}
  2006. sourceID={type}
  2007. sourceLayerID={type}
  2008. filter={['==', 'id', selectedRegion]}
  2009. style={selected_region_outline.style as any}
  2010. maxZoomLevel={selected_region_outline.maxzoom}
  2011. belowLayerID="waterway-name"
  2012. />
  2013. </>
  2014. )}
  2015. {/* {didFinishLoadingStyle &&
  2016. seriesGroups?.length > 0 &&
  2017. seriesGroups.map((group) => {
  2018. const visible = isGroupEnabled(group.id);
  2019. const mustVisible = seriesFilter.visible && (visible || isMustModeEnabled);
  2020. return (
  2021. <React.Fragment key={`series-group-${group.id}`}>
  2022. {seriesFilter.status !== 1
  2023. ? (() => {
  2024. try {
  2025. return [
  2026. <MapLibreRN.SymbolLayer
  2027. key={`symbol_unvisited_normal_${group.id}`}
  2028. id={`series-${group.id}-unvisited`}
  2029. sourceID={`series-${group.id}`}
  2030. sourceLayerID={`series-${group.id}`}
  2031. aboveLayerID={'waterway-name'}
  2032. filter={['all', seriesNotVisitedFilter, ['!=', 'must', 1]] as any}
  2033. style={{
  2034. symbolSpacing: 1,
  2035. iconImage: '{series_id}',
  2036. iconAllowOverlap: true,
  2037. iconIgnorePlacement: true,
  2038. iconSize: SERIES_ICON_SIZE,
  2039. visibility: visible ? 'visible' : 'none',
  2040. iconColor: '#666',
  2041. iconOpacity: 1,
  2042. iconHaloColor: '#ffffff',
  2043. iconHaloWidth: 1,
  2044. iconHaloBlur: 0.5,
  2045. textAnchor: 'top',
  2046. textField: '{name}',
  2047. textFont: ['Noto Sans Regular'],
  2048. textMaxWidth: 9,
  2049. textOffset: [0, 1.3],
  2050. textPadding: 2,
  2051. textSize: 12,
  2052. textOptional: true,
  2053. textIgnorePlacement: false,
  2054. textAllowOverlap: false,
  2055. textColor: '#666',
  2056. textHaloColor: '#ffffff',
  2057. textHaloWidth: 1,
  2058. textHaloBlur: 0.5
  2059. }}
  2060. />,
  2061. <MapLibreRN.CircleLayer
  2062. key={`circle_unvisited_must_${group.id}`}
  2063. id={`series-${group.id}-must-wrapper`}
  2064. sourceID={`series-${group.id}`}
  2065. sourceLayerID={`series-${group.id}`}
  2066. aboveLayerID={`series-${group.id}-unvisited`}
  2067. filter={['all', seriesNotVisitedFilter, ['==', 'must', 1]] as any}
  2068. style={{
  2069. circleRadius: SERIES_CIRCLE_RADIUS,
  2070. circleColor: Colors.ORANGE,
  2071. circleOpacity: 1,
  2072. circleStrokeWidth: SERIES_CIRCLE_STROKE,
  2073. circleStrokeColor: Colors.ORANGE,
  2074. visibility: mustVisible ? 'visible' : 'none'
  2075. }}
  2076. />,
  2077. <MapLibreRN.SymbolLayer
  2078. key={`symbol_unvisited_must_${group.id}`}
  2079. id={`series-${group.id}-must-icon`}
  2080. sourceID={`series-${group.id}`}
  2081. sourceLayerID={`series-${group.id}`}
  2082. aboveLayerID={`series-${group.id}-must-wrapper`}
  2083. filter={['all', seriesNotVisitedFilter, ['==', 'must', 1]] as any}
  2084. style={{
  2085. symbolSpacing: 1,
  2086. iconImage: '{series_id}',
  2087. iconAllowOverlap: true,
  2088. iconIgnorePlacement: true,
  2089. iconSize: SERIES_ICON_SIZE,
  2090. visibility: mustVisible ? 'visible' : 'none',
  2091. iconColor: '#666',
  2092. iconOpacity: 1,
  2093. iconHaloColor: '#ffffff',
  2094. iconHaloWidth: 1,
  2095. iconHaloBlur: 0.5,
  2096. textAnchor: 'top',
  2097. textField: '{name}',
  2098. textFont: ['Noto Sans Regular'],
  2099. textMaxWidth: 9,
  2100. textOffset: [0, 1.3],
  2101. textPadding: 2,
  2102. textSize: 12,
  2103. textOptional: true,
  2104. textIgnorePlacement: false,
  2105. textAllowOverlap: false,
  2106. textColor: '#666',
  2107. textHaloColor: '#ffffff',
  2108. textHaloWidth: 1,
  2109. textHaloBlur: 0.5
  2110. }}
  2111. />
  2112. ];
  2113. } catch (error) {
  2114. console.warn('SymbolLayer render error:', error);
  2115. return null;
  2116. }
  2117. })()
  2118. : null}
  2119. {seriesFilter.status !== 0
  2120. ? (() => {
  2121. try {
  2122. return [
  2123. <MapLibreRN.SymbolLayer
  2124. key={`symbol_visited_normal_${group.id}`}
  2125. id={`series-${group.id}-visited-normal`}
  2126. sourceID={`series-${group.id}`}
  2127. sourceLayerID={`series-${group.id}`}
  2128. aboveLayerID={'waterway-name'}
  2129. filter={['all', seriesVisitedFilter, ['!=', 'must', 1]] as any}
  2130. style={{
  2131. symbolSpacing: 1,
  2132. iconImage: '{series_id}v',
  2133. iconAllowOverlap: true,
  2134. iconIgnorePlacement: true,
  2135. iconSize: SERIES_ICON_SIZE,
  2136. visibility: visible ? 'visible' : 'none',
  2137. iconColor: '#666',
  2138. iconOpacity: 1,
  2139. iconHaloColor: '#ffffff',
  2140. iconHaloWidth: 1,
  2141. iconHaloBlur: 0.5,
  2142. textAnchor: 'top',
  2143. textField: '{name}',
  2144. textFont: ['Noto Sans Regular'],
  2145. textMaxWidth: 9,
  2146. textOffset: [0, 1.3],
  2147. textPadding: 2,
  2148. textSize: 12,
  2149. textOptional: true,
  2150. textIgnorePlacement: false,
  2151. textAllowOverlap: false,
  2152. textColor: '#666',
  2153. textHaloColor: '#ffffff',
  2154. textHaloWidth: 1,
  2155. textHaloBlur: 0.5
  2156. }}
  2157. />,
  2158. <MapLibreRN.CircleLayer
  2159. key={`circle_visited_must_${group.id}`}
  2160. id={`series-${group.id}-visited-must-wrapper`}
  2161. sourceID={`series-${group.id}`}
  2162. sourceLayerID={`series-${group.id}`}
  2163. aboveLayerID={`series-${group.id}-visited-normal`}
  2164. filter={['all', seriesVisitedFilter, ['==', 'must', 1]] as any}
  2165. style={{
  2166. circleRadius: SERIES_CIRCLE_RADIUS,
  2167. circleColor: Colors.ORANGE,
  2168. circleOpacity: 1,
  2169. circleStrokeWidth: SERIES_CIRCLE_STROKE,
  2170. circleStrokeColor: Colors.ORANGE,
  2171. visibility: mustVisible ? 'visible' : 'none'
  2172. }}
  2173. />,
  2174. <MapLibreRN.SymbolLayer
  2175. key={`symbol_visited_must_${group.id}`}
  2176. id={`series-${group.id}-visited-must`}
  2177. sourceID={`series-${group.id}`}
  2178. sourceLayerID={`series-${group.id}`}
  2179. aboveLayerID={`series-${group.id}-visited-must-wrapper`}
  2180. filter={['all', seriesVisitedFilter, ['==', 'must', 1]] as any}
  2181. style={{
  2182. symbolSpacing: 1,
  2183. iconImage: '{series_id}v',
  2184. iconAllowOverlap: true,
  2185. iconIgnorePlacement: true,
  2186. iconSize: SERIES_ICON_SIZE,
  2187. visibility: mustVisible ? 'visible' : 'none',
  2188. iconColor: '#666',
  2189. iconOpacity: 1,
  2190. iconHaloColor: '#ffffff',
  2191. iconHaloWidth: 1,
  2192. iconHaloBlur: 0.5,
  2193. textAnchor: 'top',
  2194. textField: '{name}',
  2195. textFont: ['Noto Sans Regular'],
  2196. textMaxWidth: 9,
  2197. textOffset: [0, 1.3],
  2198. textPadding: 2,
  2199. textSize: 12,
  2200. textOptional: true,
  2201. textIgnorePlacement: false,
  2202. textAllowOverlap: false,
  2203. textColor: '#666',
  2204. textHaloColor: '#ffffff',
  2205. textHaloWidth: 1,
  2206. textHaloBlur: 0.5
  2207. }}
  2208. />
  2209. ];
  2210. } catch (error) {
  2211. console.warn('SymbolLayer render error:', error);
  2212. return null;
  2213. }
  2214. })()
  2215. : null}
  2216. </React.Fragment>
  2217. );
  2218. })} */}
  2219. <MapLibreRN.VectorSource
  2220. id="nomadmania_series"
  2221. tileUrlTemplates={[VECTOR_MAP_HOST + '/tiles/series/{z}/{x}/{y}.pbf']}
  2222. onPress={handleMarkerPress}
  2223. >
  2224. {seriesFilter.status !== 1
  2225. ? (() => {
  2226. try {
  2227. return [
  2228. <MapLibreRN.SymbolLayer
  2229. key="symbol_unvisited_normal"
  2230. id={`${series_layer.id}_normal`}
  2231. sourceID={series_layer.source}
  2232. sourceLayerID={series_layer['source-layer']}
  2233. aboveLayerID={'waterway-name'}
  2234. filter={['all', seriesNotVisitedFilter, ['!=', 'must', 1]] as any}
  2235. minZoomLevel={series_layer.minzoom}
  2236. maxZoomLevel={series_layer.maxzoom}
  2237. style={{
  2238. symbolSpacing: 1,
  2239. iconImage: '{series_id}',
  2240. iconAllowOverlap: true,
  2241. iconIgnorePlacement: true,
  2242. iconSize: SERIES_ICON_SIZE,
  2243. visibility: 'visible',
  2244. iconColor: '#666',
  2245. iconOpacity: 1,
  2246. iconHaloColor: '#ffffff',
  2247. iconHaloWidth: 1,
  2248. iconHaloBlur: 0.5,
  2249. textAnchor: 'top',
  2250. textField: seriesTextField,
  2251. textFont: ['Noto Sans Regular'],
  2252. textMaxWidth: 9,
  2253. textOffset: [0, 1.3],
  2254. textPadding: 2,
  2255. textSize: SERIES_TEXT_SIZE,
  2256. textOptional: true,
  2257. textIgnorePlacement: false,
  2258. textAllowOverlap: false,
  2259. textColor: '#666',
  2260. textHaloColor: '#ffffff',
  2261. textHaloWidth: 1,
  2262. textHaloBlur: 0.5
  2263. }}
  2264. />,
  2265. <MapLibreRN.CircleLayer
  2266. key="circle_unvisited_must"
  2267. id={`${series_layer.id}_must_wrapper`}
  2268. sourceID={series_layer.source}
  2269. sourceLayerID={series_layer['source-layer']}
  2270. aboveLayerID={`${series_layer.id}_normal`}
  2271. filter={['all', seriesNotVisitedFilter, ['==', 'must', 1]] as any}
  2272. minZoomLevel={series_layer.minzoom}
  2273. maxZoomLevel={series_layer.maxzoom}
  2274. style={{
  2275. circleRadius: SERIES_CIRCLE_RADIUS,
  2276. circleColor: Colors.ORANGE,
  2277. circleOpacity: 1,
  2278. circleStrokeWidth: SERIES_CIRCLE_STROKE,
  2279. circleStrokeColor: Colors.ORANGE,
  2280. visibility: 'visible'
  2281. }}
  2282. />,
  2283. <MapLibreRN.SymbolLayer
  2284. key="symbol_unvisited_must"
  2285. id={series_layer.id}
  2286. sourceID={series_layer.source}
  2287. sourceLayerID={series_layer['source-layer']}
  2288. aboveLayerID={`${series_layer.id}_must_wrapper`}
  2289. filter={['all', seriesNotVisitedFilter, ['==', 'must', 1]] as any}
  2290. minZoomLevel={series_layer.minzoom}
  2291. maxZoomLevel={series_layer.maxzoom}
  2292. style={{
  2293. symbolSpacing: 1,
  2294. iconImage: '{series_id}',
  2295. iconAllowOverlap: true,
  2296. iconIgnorePlacement: true,
  2297. iconSize: SERIES_ICON_SIZE,
  2298. visibility: 'visible',
  2299. iconColor: '#666',
  2300. iconOpacity: 1,
  2301. iconHaloColor: '#ffffff',
  2302. iconHaloWidth: 1,
  2303. iconHaloBlur: 0.5,
  2304. textAnchor: 'top',
  2305. textField: seriesTextField,
  2306. textFont: ['Noto Sans Regular'],
  2307. textMaxWidth: 9,
  2308. textOffset: [0, 1.3],
  2309. textPadding: 2,
  2310. textSize: SERIES_TEXT_SIZE,
  2311. textOptional: true,
  2312. textIgnorePlacement: false,
  2313. textAllowOverlap: false,
  2314. textColor: '#666',
  2315. textHaloColor: '#ffffff',
  2316. textHaloWidth: 1,
  2317. textHaloBlur: 0.5
  2318. }}
  2319. />
  2320. ];
  2321. } catch (error) {
  2322. console.warn('SymbolLayer render error:', error);
  2323. return null;
  2324. }
  2325. })()
  2326. : null}
  2327. {seriesFilter.status !== 0
  2328. ? (() => {
  2329. try {
  2330. return [
  2331. <MapLibreRN.SymbolLayer
  2332. key="symbol_visited_normal"
  2333. id={`${series_visited.id}_normal`}
  2334. sourceID={series_visited.source}
  2335. sourceLayerID={series_visited['source-layer']}
  2336. aboveLayerID={'waterway-name'}
  2337. filter={['all', seriesVisitedFilter, ['!=', 'must', 1]] as any}
  2338. minZoomLevel={series_visited.minzoom}
  2339. maxZoomLevel={series_visited.maxzoom}
  2340. style={{
  2341. symbolSpacing: 1,
  2342. iconImage: '{series_id}v',
  2343. iconAllowOverlap: true,
  2344. iconIgnorePlacement: true,
  2345. iconSize: SERIES_ICON_SIZE,
  2346. visibility: 'visible',
  2347. iconColor: '#666',
  2348. iconOpacity: 1,
  2349. iconHaloColor: '#ffffff',
  2350. iconHaloWidth: 1,
  2351. iconHaloBlur: 0.5,
  2352. textAnchor: 'top',
  2353. textField: seriesTextField,
  2354. textFont: ['Noto Sans Regular'],
  2355. textMaxWidth: 9,
  2356. textOffset: [0, 1.3],
  2357. textPadding: 2,
  2358. textSize: SERIES_TEXT_SIZE,
  2359. textOptional: true,
  2360. textIgnorePlacement: false,
  2361. textAllowOverlap: false,
  2362. textColor: '#666',
  2363. textHaloColor: '#ffffff',
  2364. textHaloWidth: 1,
  2365. textHaloBlur: 0.5
  2366. }}
  2367. />,
  2368. <MapLibreRN.CircleLayer
  2369. key="circle_visited_must"
  2370. id={`${series_visited.id}_must_wrapper`}
  2371. sourceID={series_visited.source}
  2372. sourceLayerID={series_visited['source-layer']}
  2373. aboveLayerID={`${series_visited.id}_normal`}
  2374. filter={['all', seriesVisitedFilter, ['==', 'must', 1]] as any}
  2375. minZoomLevel={series_visited.minzoom}
  2376. maxZoomLevel={series_visited.maxzoom}
  2377. style={{
  2378. circleRadius: SERIES_CIRCLE_RADIUS,
  2379. circleColor: Colors.ORANGE,
  2380. circleOpacity: 1,
  2381. circleStrokeWidth: SERIES_CIRCLE_STROKE,
  2382. circleStrokeColor: Colors.ORANGE,
  2383. visibility: 'visible'
  2384. }}
  2385. />,
  2386. <MapLibreRN.SymbolLayer
  2387. key="symbol_visited_must"
  2388. id={series_visited.id}
  2389. sourceID={series_visited.source}
  2390. sourceLayerID={series_visited['source-layer']}
  2391. aboveLayerID={`${series_visited.id}_must_wrapper`}
  2392. filter={['all', seriesVisitedFilter, ['==', 'must', 1]] as any}
  2393. minZoomLevel={series_visited.minzoom}
  2394. maxZoomLevel={series_visited.maxzoom}
  2395. style={{
  2396. symbolSpacing: 1,
  2397. iconImage: '{series_id}v',
  2398. iconAllowOverlap: true,
  2399. iconIgnorePlacement: true,
  2400. iconSize: SERIES_ICON_SIZE,
  2401. visibility: 'visible',
  2402. iconColor: '#666',
  2403. iconOpacity: 1,
  2404. iconHaloColor: '#ffffff',
  2405. iconHaloWidth: 1,
  2406. iconHaloBlur: 0.5,
  2407. textAnchor: 'top',
  2408. textField: seriesTextField,
  2409. textFont: ['Noto Sans Regular'],
  2410. textMaxWidth: 9,
  2411. textOffset: [0, 1.3],
  2412. textPadding: 2,
  2413. textSize: SERIES_TEXT_SIZE,
  2414. textOptional: true,
  2415. textIgnorePlacement: false,
  2416. textAllowOverlap: false,
  2417. textColor: '#666',
  2418. textHaloColor: '#ffffff',
  2419. textHaloWidth: 1,
  2420. textHaloBlur: 0.5
  2421. }}
  2422. />
  2423. ];
  2424. } catch (error) {
  2425. console.warn('SymbolLayer render error:', error);
  2426. return null;
  2427. }
  2428. })()
  2429. : null}
  2430. </MapLibreRN.VectorSource>
  2431. <MapLibreRN.ShapeSource
  2432. ref={shapeSourceRef}
  2433. tolerance={20}
  2434. id="nomads"
  2435. shape={
  2436. nomads && (showNomads || nomadsFilter.friends || nomadsFilter.countries?.length)
  2437. ? nomads
  2438. : { type: 'FeatureCollection', features: [] }
  2439. }
  2440. onPress={async (event) => {
  2441. const feature = event.features[0];
  2442. const isCluster = feature.properties?.cluster;
  2443. if (isCluster) {
  2444. const clusterCoordinates = (feature.geometry as GeoJSON.Point).coordinates;
  2445. const zoom = await shapeSourceRef.current?.getClusterExpansionZoom(
  2446. feature as GeoJSON.Feature<GeoJSON.Geometry>
  2447. );
  2448. const newZoom = zoom ?? 2;
  2449. cameraController.setCamera({
  2450. centerCoordinate: clusterCoordinates,
  2451. zoomLevel: newZoom,
  2452. animationDuration: 500,
  2453. animationMode: 'flyTo'
  2454. });
  2455. return;
  2456. } else {
  2457. handleUserPress(event);
  2458. }
  2459. }}
  2460. cluster={true}
  2461. clusterRadius={50}
  2462. >
  2463. <MapLibreRN.SymbolLayer
  2464. id="nomads_circle"
  2465. filter={['has', 'point_count']}
  2466. aboveLayerID={Platform.OS === 'android' ? 'place-continent' : undefined}
  2467. style={{
  2468. iconImage: clusteredUsersIcon,
  2469. iconSize: [
  2470. 'interpolate',
  2471. ['linear'],
  2472. ['get', 'point_count'],
  2473. 0,
  2474. 0.33,
  2475. 10,
  2476. 0.35,
  2477. 20,
  2478. 0.37,
  2479. 50,
  2480. 0.39,
  2481. 75,
  2482. 0.41,
  2483. 100,
  2484. 0.43
  2485. ],
  2486. iconAllowOverlap: true
  2487. }}
  2488. ></MapLibreRN.SymbolLayer>
  2489. <MapLibreRN.SymbolLayer
  2490. id="nomads_count"
  2491. filter={['has', 'point_count']}
  2492. aboveLayerID={Platform.OS === 'android' ? 'nomads_circle' : undefined}
  2493. style={{
  2494. textField: [
  2495. 'case',
  2496. ['<', ['get', 'point_count'], 1000],
  2497. ['get', 'point_count'],
  2498. ['concat', ['/', ['round', ['/', ['get', 'point_count'], 100]], 10], 'k']
  2499. ],
  2500. textFont: ['Noto Sans Bold'],
  2501. textSize: [
  2502. 'interpolate',
  2503. ['linear'],
  2504. ['get', 'point_count'],
  2505. 0,
  2506. 13.5,
  2507. 20,
  2508. 14,
  2509. 75,
  2510. 15
  2511. ],
  2512. textColor: '#FFFFFF',
  2513. textAnchor: 'center',
  2514. textOffset: [
  2515. 'interpolate',
  2516. ['linear'],
  2517. ['get', 'point_count'],
  2518. 0,
  2519. ['literal', [0, 0.85]],
  2520. 20,
  2521. ['literal', [0, 0.92]],
  2522. 75,
  2523. ['literal', [0, 1]]
  2524. ],
  2525. textAllowOverlap: true
  2526. }}
  2527. />
  2528. <MapLibreRN.SymbolLayer
  2529. id="nomads_symbol"
  2530. filter={['!', ['has', 'point_count']]}
  2531. aboveLayerID={Platform.OS === 'android' ? 'place-continent' : undefined}
  2532. style={{
  2533. iconImage: ['case', ['==', ['get', 'friend'], 1], '02', '00'],
  2534. iconAllowOverlap: true
  2535. }}
  2536. ></MapLibreRN.SymbolLayer>
  2537. </MapLibreRN.ShapeSource>
  2538. {selectedUser && <UserItem marker={selectedUser} />}
  2539. {selectedMarker && (
  2540. <MarkerItem
  2541. marker={selectedMarker}
  2542. toggleSeries={toggleSeries}
  2543. token={token}
  2544. isPremium={isPremium}
  2545. setPremiumModalVisible={setPremiumModalVisible}
  2546. />
  2547. )}
  2548. {(renderCamera || Platform.OS === 'ios') && (
  2549. <MapLibreRN.Camera ref={cameraRef} followUserLocation={undefined} animationMode="flyTo" />
  2550. )}
  2551. {location && (
  2552. <MapLibreRN.UserLocation
  2553. animated={true}
  2554. showsUserHeadingIndicator={true}
  2555. onPress={async () => {
  2556. const currentZoom = await mapRef.current?.getZoom();
  2557. const newZoom = (currentZoom || 0) + 2;
  2558. cameraController.setCamera({
  2559. centerCoordinate: [location.longitude, location.latitude],
  2560. zoomLevel: newZoom,
  2561. animationDuration: 500,
  2562. animationMode: 'flyTo'
  2563. });
  2564. }}
  2565. >
  2566. {/* to do custom user location */}
  2567. </MapLibreRN.UserLocation>
  2568. )}
  2569. </MapLibreRN.MapView>
  2570. {center ? (
  2571. <ScaleBar
  2572. zoom={zoom}
  2573. latitude={center[1]}
  2574. isVisible={isZooming}
  2575. bottom={tabBarHeight + 80}
  2576. />
  2577. ) : null}
  2578. {regionPopupVisible && regionData ? (
  2579. <>
  2580. <TouchableOpacity
  2581. style={[styles.cornerButton, styles.topLeftButton, styles.closeLeftButton]}
  2582. onPress={handleClosePopup}
  2583. >
  2584. <CloseSvg fill="white" width={13} height={13} />
  2585. <Text style={styles.textClose}>Close</Text>
  2586. </TouchableOpacity>
  2587. <TouchableOpacity
  2588. onPress={handleGetLocation}
  2589. style={[
  2590. styles.cornerButton,
  2591. styles.topRightButton,
  2592. styles.bottomButton,
  2593. { bottom: tabBarHeight + 20 }
  2594. ]}
  2595. >
  2596. {isLocationLoading ? (
  2597. <ActivityIndicator size="small" color={Colors.DARK_BLUE} />
  2598. ) : (
  2599. <LocationIcon />
  2600. )}
  2601. </TouchableOpacity>
  2602. <RegionPopup
  2603. region={regionData}
  2604. userAvatars={userAvatars}
  2605. userData={userData}
  2606. openEditModal={handleOpenEditModal}
  2607. updateNM={(id, first, last, visits, quality) => {
  2608. if (!token) {
  2609. setIsWarningModalVisible(true);
  2610. return;
  2611. }
  2612. handleUpdateNM(id, first, last, visits, quality);
  2613. const updatedIds = regionsVisited.includes(id)
  2614. ? regionsVisited.filter((visitedId) => visitedId !== id)
  2615. : [...regionsVisited, id];
  2616. setRegionsVisited(updatedIds);
  2617. refetchVisitedCountries();
  2618. }}
  2619. updateDare={(id, visits) => {
  2620. if (!token) {
  2621. setIsWarningModalVisible(true);
  2622. return;
  2623. }
  2624. handleUpdateDare(id, visits);
  2625. const updatedIds = dareVisited.includes(id)
  2626. ? dareVisited.filter((visitedId) => visitedId !== id)
  2627. : [...dareVisited, id];
  2628. setDareVisited(updatedIds);
  2629. }}
  2630. disabled={!token || !isConnected}
  2631. updateSlow={(id, v, s11, s31, s101) => {
  2632. if (!token) {
  2633. setIsWarningModalVisible(true);
  2634. return;
  2635. }
  2636. handleUpdateSlow(id, v, s11, s31, s101);
  2637. const updatedIds = countriesVisited.includes(id)
  2638. ? countriesVisited.filter((visitedId) => visitedId !== id)
  2639. : [...countriesVisited, id];
  2640. setCountriesVisited(updatedIds);
  2641. }}
  2642. openEditSlowModal={handleOpenEditSlowModal}
  2643. onAddVisitSuccess={(id, isFuture) => {
  2644. if (!isFuture && !regionsVisited.includes(id)) {
  2645. setRegionsVisited([...regionsVisited, id]);
  2646. }
  2647. refetchVisitedCountries();
  2648. }}
  2649. />
  2650. </>
  2651. ) : (
  2652. <>
  2653. {!isExpanded ? (
  2654. <TouchableOpacity
  2655. style={[styles.cornerButton, styles.topRightButton]}
  2656. onPress={() => navigation.navigate(NAVIGATION_PAGES.PROFILE_TAB)}
  2657. >
  2658. {token ? (
  2659. userInfoData?.avatar ? (
  2660. <Image
  2661. style={styles.avatar}
  2662. source={{
  2663. uri: API_HOST + '/img/avatars/' + userInfoData?.avatar + '?v=' + avatarVersion
  2664. }}
  2665. />
  2666. ) : (
  2667. <AvatarWithInitials
  2668. text={`${userInfoData?.first_name ? userInfoData?.first_name[0] : ''}${userInfoData?.last_name ? userInfoData?.last_name[0] : ''}`}
  2669. flag={API_HOST + '/img/flags_new/' + userInfoData?.homebase_flag}
  2670. size={48}
  2671. borderColor={Colors.WHITE}
  2672. />
  2673. )
  2674. ) : (
  2675. <ProfileIcon fill={Colors.DARK_BLUE} />
  2676. )}
  2677. </TouchableOpacity>
  2678. ) : null}
  2679. <Animated.View
  2680. style={[
  2681. styles.searchContainer,
  2682. styles.cornerButton,
  2683. styles.topLeftButton,
  2684. animatedStyle,
  2685. { padding: 5 }
  2686. ]}
  2687. >
  2688. {isExpanded ? (
  2689. <>
  2690. <TouchableOpacity onPress={handlePress} style={styles.iconButton}>
  2691. <CloseSvg fill={'#0F3F4F'} />
  2692. </TouchableOpacity>
  2693. <TextInput
  2694. style={styles.input}
  2695. placeholder="Search regions, places, nomads"
  2696. placeholderTextColor={Colors.LIGHT_GRAY}
  2697. value={searchInput}
  2698. onChangeText={(text) => setSearchInput(text)}
  2699. onSubmitEditing={handleSearch}
  2700. />
  2701. <TouchableOpacity onPress={handleSearch} style={styles.iconButton}>
  2702. <SearchIcon fill={'#0F3F4F'} />
  2703. </TouchableOpacity>
  2704. </>
  2705. ) : (
  2706. <TouchableOpacity onPress={handlePress} style={[styles.iconButton]}>
  2707. <SearchIcon fill={'#0F3F4F'} />
  2708. </TouchableOpacity>
  2709. )}
  2710. </Animated.View>
  2711. <View style={[styles.tabs, { bottom: tabBarHeight + 20 }]}>
  2712. <ScrollView
  2713. horizontal
  2714. showsHorizontalScrollIndicator={false}
  2715. contentContainerStyle={{
  2716. paddingHorizontal: isSmallScreen ? 8 : 12,
  2717. paddingTop: 6,
  2718. gap: isSmallScreen ? 6 : 10,
  2719. flexDirection: 'row'
  2720. }}
  2721. >
  2722. <MapButton
  2723. onPress={() => {
  2724. try {
  2725. filterModalRef.current?.openFilter('regions');
  2726. closeCallout();
  2727. } catch (error) {
  2728. console.error('Error opening filter:', error);
  2729. }
  2730. }}
  2731. icon={TravelsIcon}
  2732. text={
  2733. tilesType.value === 0 || tilesType.value === 1
  2734. ? token
  2735. ? `${tilesType.value === 0 ? 'NM' : 'UN'} ${regionsFilter.visitedLabel} ${regionsFilter.year}`
  2736. : `${tilesType.value === 0 ? 'NM' : 'UN'}`
  2737. : tilesType.value === 2
  2738. ? 'DARE'
  2739. : 'Travels'
  2740. }
  2741. active={type !== 'blank'}
  2742. />
  2743. <MapButton
  2744. onPress={() => {
  2745. try {
  2746. filterModalRef.current?.openFilter('series');
  2747. closeCallout();
  2748. } catch (error) {
  2749. console.error('Error opening filter:', error);
  2750. }
  2751. }}
  2752. icon={SeriesIcon}
  2753. text="Series"
  2754. active={seriesFilter.visible}
  2755. />
  2756. {token ? (
  2757. <MapButton
  2758. onPress={() => {
  2759. try {
  2760. filterModalRef.current?.openFilter('nomads');
  2761. closeCallout();
  2762. } catch (error) {
  2763. console.error('Error opening filter:', error);
  2764. }
  2765. }}
  2766. icon={NomadsIcon}
  2767. text="Nomads"
  2768. active={showNomads || nomadsFilter.friends || nomadsFilter.countries?.length}
  2769. >
  2770. {usersCount && usersCount > 0 ? (
  2771. <MessagesDot
  2772. messagesCount={usersCount}
  2773. fullNumber={true}
  2774. right={-10}
  2775. top={-8}
  2776. />
  2777. ) : null}
  2778. </MapButton>
  2779. ) : null}
  2780. </ScrollView>
  2781. </View>
  2782. <TouchableOpacity
  2783. onPress={handleGetLocation}
  2784. style={[
  2785. styles.cornerButton,
  2786. styles.bottomButton,
  2787. styles.bottomRightButton,
  2788. { bottom: tabBarHeight + 20 }
  2789. ]}
  2790. >
  2791. {isLocationLoading ? (
  2792. <ActivityIndicator size="small" color={Colors.DARK_BLUE} />
  2793. ) : (
  2794. <LocationIcon />
  2795. )}
  2796. </TouchableOpacity>
  2797. </>
  2798. )}
  2799. <SearchModal
  2800. searchVisible={searchVisible}
  2801. handleCloseModal={handleCloseModal}
  2802. handleFindRegion={handleFindRegion}
  2803. handleFindSeries={handleFindSeries}
  2804. index={index}
  2805. searchData={searchData}
  2806. setIndex={setIndex}
  2807. token={token}
  2808. />
  2809. <WarningModal
  2810. type={'unauthorized'}
  2811. isVisible={isWarningModalVisible}
  2812. onClose={() => setIsWarningModalVisible(false)}
  2813. />
  2814. <EditNmModal
  2815. isVisible={isEditModalVisible}
  2816. onClose={() => setIsEditModalVisible(false)}
  2817. modalState={modalState}
  2818. updateModalState={handleModalStateChange}
  2819. updateNM={handleUpdateNM}
  2820. />
  2821. <FilterModal
  2822. ref={filterModalRef}
  2823. tilesTypes={tilesTypes}
  2824. tilesType={tilesType}
  2825. setTilesType={setTilesType}
  2826. setType={setType}
  2827. userId={userId ? +userId : 0}
  2828. setRegionsFilter={setRegionsFilter}
  2829. setSeriesFilter={setSeriesFilter}
  2830. setShowNomads={setShowNomads}
  2831. setNomadsFilter={setNomadsFilter}
  2832. showNomads={showNomads}
  2833. isPublicView={false}
  2834. isLogged={token ? true : false}
  2835. usersOnMapCount={token && usersOnMapCount?.count ? usersOnMapCount.count : null}
  2836. friendsOnTheMapCount={
  2837. token && usersOnMapCount?.friends_count ? usersOnMapCount.friends_count : null
  2838. }
  2839. isConnected={isConnected}
  2840. isPremium={isPremium}
  2841. />
  2842. <EditModal
  2843. isVisible={isEditSlowModalVisible}
  2844. onClose={() => setIsEditSlowModalVisible(false)}
  2845. item={{ ...userData, country_id: regionData?.id }}
  2846. updateSlow={(id, v, s11, s31, s101) => handleUpdateSlow(id, v, s11, s31, s101)}
  2847. />
  2848. <WarningModal
  2849. type={'success'}
  2850. isVisible={askLocationVisible}
  2851. onClose={() => setAskLocationVisible(false)}
  2852. action={handleAcceptPermission}
  2853. message="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."
  2854. />
  2855. <WarningModal
  2856. type={'success'}
  2857. isVisible={openSettingsVisible}
  2858. onClose={() => setOpenSettingsVisible(false)}
  2859. action={async () => {
  2860. const isServicesEnabled = await Location.hasServicesEnabledAsync();
  2861. if (!isServicesEnabled) {
  2862. Platform.OS === 'ios'
  2863. ? Linking.openURL('app-settings:')
  2864. : Linking.sendIntent('android.settings.LOCATION_SOURCE_SETTINGS');
  2865. } else {
  2866. Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings();
  2867. }
  2868. }}
  2869. message="NomadMania app needs location permissions to function properly. Open settings?"
  2870. />
  2871. <WarningModal
  2872. type={'success'}
  2873. isVisible={premiumModalVisible}
  2874. message={
  2875. 'This feature is available to Premium users. Premium account settings can be managed on our website.'
  2876. }
  2877. action={() => {}}
  2878. onClose={() => setPremiumModalVisible(false)}
  2879. title={'Premium Feature'}
  2880. />
  2881. <MultipleSeriesModal />
  2882. </SafeAreaView>
  2883. );
  2884. };
  2885. export default MapScreen;