|
@@ -0,0 +1,467 @@
|
|
|
+import {
|
|
|
+ StyleSheet,
|
|
|
+ View,
|
|
|
+ Platform,
|
|
|
+ TouchableOpacity,
|
|
|
+ Text
|
|
|
+} from 'react-native';
|
|
|
+import React, { useEffect, useState, useRef, useMemo } from 'react';
|
|
|
+import MapView, { UrlTile, Geojson } from 'react-native-maps';
|
|
|
+import * as turf from '@turf/turf';
|
|
|
+import * as FileSystem from 'expo-file-system';
|
|
|
+import * as Location from 'expo-location';
|
|
|
+
|
|
|
+import MenuIcon from '../../../assets/icons/menu.svg';
|
|
|
+import SearchIcon from '../../../assets/icons/search.svg';
|
|
|
+import RadarIcon from '../../../assets/icons/radar.svg';
|
|
|
+import LocationIcon from '../../../assets/icons/location.svg';
|
|
|
+import CloseSvg from '../../../assets/icons/close.svg';
|
|
|
+
|
|
|
+import regions from '../../../assets/geojson/nm2022.json'
|
|
|
+import dareRegions from '../../../assets/geojson/mqp.json'
|
|
|
+
|
|
|
+import NetInfo from "@react-native-community/netinfo";
|
|
|
+import { getFirstDatabase, getSecondDatabase } from '../../db';
|
|
|
+import RegionPopup from '../../components/RegionPopup';
|
|
|
+
|
|
|
+import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
|
|
+import { SQLiteDatabase } from 'expo-sqlite';
|
|
|
+import { FeatureCollection } from '@turf/turf';
|
|
|
+
|
|
|
+const tilesBaseURL = 'https://maps.nomadmania.com/tiles_osm';
|
|
|
+const localTileDir = `${FileSystem.cacheDirectory}tiles`;
|
|
|
+
|
|
|
+const gridUrl = 'https://maps.nomadmania.com/tiles_nm/grid';
|
|
|
+const localGridDir = `${FileSystem.cacheDirectory}tiles/grid`;
|
|
|
+
|
|
|
+const visitedTiles = 'https://maps.nomadmania.com/tiles_nm/user_visited/51363';
|
|
|
+const localVisitedDir = `${FileSystem.cacheDirectory}tiles/user_visited`;
|
|
|
+
|
|
|
+const dareTiles = 'https://maps.nomadmania.com/tiles_nm/regions_mqp';
|
|
|
+const localDareDir = `${FileSystem.cacheDirectory}tiles/regions_mqp`;
|
|
|
+
|
|
|
+interface RegionData {
|
|
|
+ type?: string;
|
|
|
+ name?: string;
|
|
|
+ crs?: {
|
|
|
+ type: string;
|
|
|
+ properties: { name: string; };
|
|
|
+ };
|
|
|
+ features?: any;
|
|
|
+}
|
|
|
+
|
|
|
+interface Region {
|
|
|
+ id: number;
|
|
|
+ name: string;
|
|
|
+ region_photos: string;
|
|
|
+ visitors_count: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface Feature {
|
|
|
+ geometry: turf.helpers.Geometry;
|
|
|
+ properties: {
|
|
|
+ id: number;
|
|
|
+ fill?: string;
|
|
|
+ stroke?: string;
|
|
|
+ };
|
|
|
+ type: 'Feature';
|
|
|
+}
|
|
|
+
|
|
|
+interface GeojsonRegion {
|
|
|
+ type: 'FeatureCollection';
|
|
|
+ features: Feature[];
|
|
|
+}
|
|
|
+
|
|
|
+type HomeScreenNavigationProp = BottomTabNavigationProp<any>;
|
|
|
+interface HomeScreenProps {
|
|
|
+ navigation: HomeScreenNavigationProp;
|
|
|
+}
|
|
|
+
|
|
|
+const HomeScreen: React.FC<HomeScreenProps> = ({ navigation }) => {
|
|
|
+ const mapRef = useRef<MapView>(null);
|
|
|
+
|
|
|
+ const [isConnected, setIsConnected] = useState<boolean | null>(true);
|
|
|
+ const [selectedRegion, setSelectedRegion] = useState(null);
|
|
|
+ const [location, setLocation] = useState(null);
|
|
|
+ const [popupVisible, setPopupVisible] = useState<boolean | null>(false);
|
|
|
+ const [regionData, setRegionData] = useState<Region | null>(null);
|
|
|
+ const [userAvatars, setUserAvatars] = useState<string[]>([]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ navigation.setOptions({
|
|
|
+ tabBarStyle: {
|
|
|
+ display: popupVisible ? 'none' : 'flex',
|
|
|
+ position: 'absolute',
|
|
|
+ ...Platform.select({
|
|
|
+ android: {
|
|
|
+ height: 58,
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }, [popupVisible, navigation]);
|
|
|
+
|
|
|
+ // useEffect(() => {
|
|
|
+ // (async () => {
|
|
|
+
|
|
|
+ // let { status } = await Location.requestForegroundPermissionsAsync();
|
|
|
+ // if (status !== 'granted') {
|
|
|
+ // return;
|
|
|
+ // }
|
|
|
+
|
|
|
+ // let location = await Location.getCurrentPositionAsync({});
|
|
|
+ // setLocation({
|
|
|
+ // latitude: location.coords.latitude,
|
|
|
+ // longitude: location.coords.longitude,
|
|
|
+ // latitudeDelta: 5,
|
|
|
+ // longitudeDelta: 5,
|
|
|
+ // });
|
|
|
+ // })();
|
|
|
+ // }, []);
|
|
|
+
|
|
|
+ const getData = async (db: SQLiteDatabase | null, regionId: number, name: string) => {
|
|
|
+ return new Promise<void>((resolve, reject) => {
|
|
|
+ db?.transaction(tx => {
|
|
|
+ tx.executeSql(
|
|
|
+ `SELECT * FROM ${name} WHERE id = ${regionId};`,
|
|
|
+ [],
|
|
|
+ (_, { rows }) => {
|
|
|
+ setRegionData(rows._array[0]);
|
|
|
+
|
|
|
+ const avatarPromises = JSON.parse(rows._array[0].visitors_avatars)?.map((avatarId: number) => {
|
|
|
+ return new Promise((resolveAvatar, rejectAvatar) => {
|
|
|
+ tx.executeSql(
|
|
|
+ `SELECT * FROM avatars WHERE id = ${avatarId};`,
|
|
|
+ [],
|
|
|
+ (_, { rows }) => {
|
|
|
+ resolveAvatar(rows._array[0].data);
|
|
|
+ },
|
|
|
+ (_, error) => {
|
|
|
+ console.error('error', error);
|
|
|
+ reject(error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ );
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ Promise.all(avatarPromises)
|
|
|
+ .then(avatars => {
|
|
|
+ setUserAvatars(avatars);
|
|
|
+ resolve();
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ console.error('Error processing avatars', error);
|
|
|
+ reject(error);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ (_, error) => {
|
|
|
+ console.error('error', error);
|
|
|
+ reject(error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ );
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleMapPress = async (event: { nativeEvent: { coordinate: { latitude: any; longitude: any; }; }; }) => {
|
|
|
+ const { latitude, longitude } = event.nativeEvent.coordinate;
|
|
|
+ const point = turf.point([longitude, latitude]);
|
|
|
+ setUserAvatars([]);
|
|
|
+
|
|
|
+ const findRegion = (dataset: RegionData) => {
|
|
|
+ return dataset.features.find((region: any) => {
|
|
|
+ const coordinates = region?.geometry?.coordinates;
|
|
|
+ const type = region?.geometry?.type;
|
|
|
+
|
|
|
+ if (!Array.isArray(coordinates) || coordinates.length === 0) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const polygon: any = type === 'Polygon'
|
|
|
+ ? turf.polygon(coordinates)
|
|
|
+ : turf.multiPolygon(coordinates);
|
|
|
+
|
|
|
+ return turf.booleanPointInPolygon(point, polygon);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error creating polygon:', error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ let db = getSecondDatabase();
|
|
|
+ let tableName = 'places';
|
|
|
+
|
|
|
+ let foundRegion = findRegion(dareRegions);
|
|
|
+
|
|
|
+ if (!foundRegion) {
|
|
|
+ foundRegion = findRegion(regions);
|
|
|
+ db = getFirstDatabase();
|
|
|
+ tableName = 'regions';
|
|
|
+ }
|
|
|
+ if (foundRegion) {
|
|
|
+ const id = foundRegion.properties.id;
|
|
|
+
|
|
|
+
|
|
|
+ // console.log('foundRegion', foundRegion)
|
|
|
+
|
|
|
+ setSelectedRegion({
|
|
|
+ type: 'FeatureCollection',
|
|
|
+ features: [{
|
|
|
+ geometry: foundRegion.geometry,
|
|
|
+ properties: {
|
|
|
+ ...foundRegion.properties,
|
|
|
+ fill: "rgba(57, 115, 172, 0.2)",
|
|
|
+ stroke: "#3973ac",
|
|
|
+ },
|
|
|
+ type: 'Feature',
|
|
|
+ }]
|
|
|
+ });
|
|
|
+
|
|
|
+ await getData(db, id, tableName).then(() => {
|
|
|
+ setPopupVisible(true)
|
|
|
+ });
|
|
|
+
|
|
|
+ const bounds = turf.bbox(foundRegion);
|
|
|
+ const padding = 1;
|
|
|
+
|
|
|
+ const region = {
|
|
|
+ latitude: (bounds[1] + bounds[3]) / 2,
|
|
|
+ longitude: (bounds[0] + bounds[2]) / 2,
|
|
|
+ latitudeDelta: Math.abs(bounds[3] - bounds[1]) + padding,
|
|
|
+ longitudeDelta: Math.abs(bounds[2] - bounds[0]) + padding,
|
|
|
+ };
|
|
|
+
|
|
|
+ mapRef.current?.animateToRegion(region, 1000);
|
|
|
+ } else {
|
|
|
+ handleClosePopup();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ const unsubscribe = NetInfo.addEventListener(state => {
|
|
|
+ setIsConnected(state.isConnected);
|
|
|
+ });
|
|
|
+
|
|
|
+ return () => unsubscribe();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const renderLocalTiles = () => {
|
|
|
+ return (
|
|
|
+ <UrlTile
|
|
|
+ urlTemplate={`${tilesBaseURL}/{z}/{x}/{y}`}
|
|
|
+ maximumZ={15}
|
|
|
+ maximumNativeZ={13}
|
|
|
+ tileCachePath={`${localTileDir}`}
|
|
|
+ shouldReplaceMapContent
|
|
|
+ minimumZ={0}
|
|
|
+ offlineMode={!isConnected}
|
|
|
+ opacity={1}
|
|
|
+ zIndex={1}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderGridTiles = () => {
|
|
|
+ return (
|
|
|
+ <UrlTile
|
|
|
+ urlTemplate={`${gridUrl}/{z}/{x}/{y}`}
|
|
|
+ maximumZ={15}
|
|
|
+ maximumNativeZ={13}
|
|
|
+ tileCachePath={`${localGridDir}`}
|
|
|
+ shouldReplaceMapContent
|
|
|
+ minimumZ={0}
|
|
|
+ offlineMode={!isConnected}
|
|
|
+ opacity={1}
|
|
|
+ zIndex={2}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderVisitedTiles = () => {
|
|
|
+ return (
|
|
|
+ <UrlTile
|
|
|
+ urlTemplate={`${visitedTiles}/{z}/{x}/{y}`}
|
|
|
+ maximumZ={15}
|
|
|
+ maximumNativeZ={13}
|
|
|
+ tileCachePath={`${localVisitedDir}`}
|
|
|
+ shouldReplaceMapContent
|
|
|
+ minimumZ={0}
|
|
|
+ offlineMode={!isConnected}
|
|
|
+ opacity={0.5}
|
|
|
+ zIndex={2}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderDareTiles = () => {
|
|
|
+ return (
|
|
|
+ <UrlTile
|
|
|
+ urlTemplate={`${dareTiles}/{z}/{x}/{y}`}
|
|
|
+ maximumZ={15}
|
|
|
+ maximumNativeZ={13}
|
|
|
+ tileCachePath={`${localDareDir}`}
|
|
|
+ shouldReplaceMapContent
|
|
|
+ minimumZ={0}
|
|
|
+ offlineMode={!isConnected}
|
|
|
+ opacity={0.5}
|
|
|
+ zIndex={2}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ function renderGeoJSON() {
|
|
|
+ if (!selectedRegion) return null;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Geojson
|
|
|
+ geojson={selectedRegion}
|
|
|
+ fillColor="rgba(57, 115, 172, 0.2)"
|
|
|
+ strokeColor="#3973ac"
|
|
|
+ strokeWidth={Platform.OS == 'android' ? 3 : 2}
|
|
|
+ zIndex={3}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleClosePopup = () => {
|
|
|
+ setPopupVisible(false);
|
|
|
+ setSelectedRegion(null);
|
|
|
+ }
|
|
|
+
|
|
|
+ const renderedGeoJSON = useMemo(() => renderGeoJSON(), [selectedRegion]);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <View style={styles.container}>
|
|
|
+ <MapView
|
|
|
+ ref={mapRef}
|
|
|
+ // initialRegion={location}
|
|
|
+ // showsUserLocation={true}
|
|
|
+ showsMyLocationButton={false}
|
|
|
+ showsCompass={false}
|
|
|
+ zoomControlEnabled={false}
|
|
|
+ onPress={handleMapPress}
|
|
|
+ style={styles.map}
|
|
|
+ mapType={Platform.OS == 'android' ? 'none' : 'standard'}
|
|
|
+ offlineMode={!isConnected}
|
|
|
+ maxZoomLevel={15}
|
|
|
+ minZoomLevel={0}
|
|
|
+ >
|
|
|
+ {renderLocalTiles()}
|
|
|
+ {renderedGeoJSON}
|
|
|
+ {renderGridTiles()}
|
|
|
+ {renderVisitedTiles()}
|
|
|
+ {renderDareTiles()}
|
|
|
+ </MapView>
|
|
|
+
|
|
|
+ {!popupVisible ? (
|
|
|
+ <>
|
|
|
+ <TouchableOpacity style={[styles.cornerButton, styles.topLeftButton]}>
|
|
|
+ <MenuIcon />
|
|
|
+ </TouchableOpacity>
|
|
|
+
|
|
|
+ <TouchableOpacity style={[styles.cornerButton, styles.topRightButton]}>
|
|
|
+ <SearchIcon />
|
|
|
+ </TouchableOpacity>
|
|
|
+
|
|
|
+ <TouchableOpacity style={[styles.cornerButton, styles.bottomLeftButton]}>
|
|
|
+ <RadarIcon />
|
|
|
+ </TouchableOpacity>
|
|
|
+
|
|
|
+ <TouchableOpacity style={[styles.cornerButton, styles.bottomRightButton]}>
|
|
|
+ <LocationIcon />
|
|
|
+ </TouchableOpacity>
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ <TouchableOpacity style={[styles.cornerButton, styles.topLeftButton, styles.closeLeftButton]} onPress={handleClosePopup}>
|
|
|
+ <CloseSvg fill="white" width={13} height={13} />
|
|
|
+ <Text style={{fontSize: 12, color: 'white', fontWeight: '500', lineHeight: 14}}>Close</Text>
|
|
|
+ </TouchableOpacity>
|
|
|
+
|
|
|
+ <TouchableOpacity style={[styles.cornerButton, styles.topRightButton, {width: 42, height: 42, borderRadius: 21,}]}>
|
|
|
+ <LocationIcon />
|
|
|
+ </TouchableOpacity>
|
|
|
+
|
|
|
+ <RegionPopup
|
|
|
+ region={regionData}
|
|
|
+ userAvatars={userAvatars}
|
|
|
+ onMarkVisited={() => console.log('Mark as visited')}
|
|
|
+ />
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </View>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export default HomeScreen;
|
|
|
+
|
|
|
+const styles = StyleSheet.create({
|
|
|
+ container: {
|
|
|
+ ...StyleSheet.absoluteFillObject,
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'flex-end',
|
|
|
+ },
|
|
|
+ map: {
|
|
|
+ ...StyleSheet.absoluteFillObject,
|
|
|
+ },
|
|
|
+ btn: {
|
|
|
+ marginBottom: 5,
|
|
|
+ },
|
|
|
+ button: {
|
|
|
+ backgroundColor: '#007bff',
|
|
|
+ padding: 10,
|
|
|
+ borderRadius: 5,
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'center',
|
|
|
+ },
|
|
|
+ cornerButton: {
|
|
|
+ position: 'absolute',
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 1)',
|
|
|
+ padding: 12,
|
|
|
+ width: 48,
|
|
|
+ height: 48,
|
|
|
+ borderRadius: 24,
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'center',
|
|
|
+ shadowColor: 'rgba(33, 37, 41, 0.12)',
|
|
|
+ shadowOffset: { width: 0, height: 4 },
|
|
|
+ shadowRadius: 8,
|
|
|
+ elevation: 5,
|
|
|
+ },
|
|
|
+ topLeftButton: {
|
|
|
+ top: 44,
|
|
|
+ left: 16,
|
|
|
+ },
|
|
|
+ closeLeftButton: {
|
|
|
+ backgroundColor: 'rgba(33, 37, 41, 0.78)',
|
|
|
+ paddingHorizontal: 12,
|
|
|
+ paddingVertical: 8,
|
|
|
+ width: 81,
|
|
|
+ height: 36,
|
|
|
+ borderRadius: 18,
|
|
|
+ flexDirection: 'row',
|
|
|
+ gap: 6,
|
|
|
+ },
|
|
|
+ topRightButton: {
|
|
|
+ top: 44,
|
|
|
+ right: 16,
|
|
|
+ },
|
|
|
+ bottomLeftButton: {
|
|
|
+ bottom: Platform.OS == 'android' ? 80 : 100,
|
|
|
+ left: 16,
|
|
|
+ width: 42,
|
|
|
+ height: 42,
|
|
|
+ borderRadius: 21,
|
|
|
+ },
|
|
|
+ bottomRightButton: {
|
|
|
+ bottom: Platform.OS == 'android' ? 80 : 100,
|
|
|
+ right: 16,
|
|
|
+ width: 42,
|
|
|
+ height: 42,
|
|
|
+ borderRadius: 21,
|
|
|
+ },
|
|
|
+});
|