|
@@ -1,39 +1,68 @@
|
|
-import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
-import { View, Text, Platform, TouchableOpacity } from 'react-native';
|
|
|
|
|
|
+import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
+import { View, Text, TouchableOpacity } from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
-import MapView, { Geojson, UrlTile } from 'react-native-maps';
|
|
|
|
-import * as turf from '@turf/turf';
|
|
|
|
-import { Feature } from '@turf/turf';
|
|
|
|
import { useNavigation } from '@react-navigation/native';
|
|
import { useNavigation } from '@react-navigation/native';
|
|
|
|
+import * as turf from '@turf/turf';
|
|
|
|
+import MapLibreGL, { CameraRef, MapViewRef } from '@maplibre/maplibre-react-native';
|
|
|
|
|
|
import { Header, Modal, FlatList as List } from 'src/components';
|
|
import { Header, Modal, FlatList as List } from 'src/components';
|
|
|
|
|
|
-import { FASTEST_MAP_HOST } from 'src/constants';
|
|
|
|
|
|
+import { VECTOR_MAP_HOST } from 'src/constants';
|
|
import { Colors } from 'src/theme';
|
|
import { Colors } from 'src/theme';
|
|
-import { findRegionInDataset } from 'src/utils/mapHelpers';
|
|
|
|
-import { calculateMapRegion } from '../utils/calculateRegion';
|
|
|
|
-import { FeatureCollection } from 'src/types/map';
|
|
|
|
import { NAVIGATION_PAGES } from 'src/types';
|
|
import { NAVIGATION_PAGES } from 'src/types';
|
|
import { RegionAddData } from '../utils/types';
|
|
import { RegionAddData } from '../utils/types';
|
|
import { useGetRegionsForTripsQuery } from '@api/trips';
|
|
import { useGetRegionsForTripsQuery } from '@api/trips';
|
|
|
|
+import { useGetListRegionsQuery } from '@api/regions';
|
|
import { styles } from './styles';
|
|
import { styles } from './styles';
|
|
|
|
|
|
-import regionsGeojson from '../../../../../assets/geojson/nm2022.json';
|
|
|
|
import SearchSvg from '../../../../../assets/icons/search.svg';
|
|
import SearchSvg from '../../../../../assets/icons/search.svg';
|
|
import SaveSvg from '../../../../../assets/icons/travels-screens/save.svg';
|
|
import SaveSvg from '../../../../../assets/icons/travels-screens/save.svg';
|
|
|
|
|
|
|
|
+MapLibreGL.setAccessToken(null);
|
|
|
|
+
|
|
|
|
+const generateFilter = (ids: number[]) => {
|
|
|
|
+ return ids?.length ? ['any', ...ids.map((id) => ['==', 'id', id])] : ['==', 'id', -1];
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+let nm_regions = {
|
|
|
|
+ id: 'regions',
|
|
|
|
+ type: 'fill',
|
|
|
|
+ source: 'regions',
|
|
|
|
+ 'source-layer': 'regions',
|
|
|
|
+ style: {
|
|
|
|
+ fillColor: 'rgba(15, 63, 79, 0)'
|
|
|
|
+ },
|
|
|
|
+ filter: ['all'],
|
|
|
|
+ maxzoom: 16
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+let selected_region = {
|
|
|
|
+ id: 'selected_region',
|
|
|
|
+ type: 'fill',
|
|
|
|
+ source: 'regions',
|
|
|
|
+ 'source-layer': 'regions',
|
|
|
|
+ style: {
|
|
|
|
+ fillColor: 'rgba(237, 147, 52, 0.7)'
|
|
|
|
+ },
|
|
|
|
+ maxzoom: 12
|
|
|
|
+};
|
|
|
|
+
|
|
const AddRegionsScreen = ({ route }: { route: any }) => {
|
|
const AddRegionsScreen = ({ route }: { route: any }) => {
|
|
const { regionsParams }: { regionsParams: RegionAddData[] } = route.params;
|
|
const { regionsParams }: { regionsParams: RegionAddData[] } = route.params;
|
|
const { data } = useGetRegionsForTripsQuery(true);
|
|
const { data } = useGetRegionsForTripsQuery(true);
|
|
|
|
+ const { data: regionsList } = useGetListRegionsQuery(true);
|
|
const navigation = useNavigation();
|
|
const navigation = useNavigation();
|
|
|
|
|
|
const [regions, setRegions] = useState<RegionAddData[] | null>(null);
|
|
const [regions, setRegions] = useState<RegionAddData[] | null>(null);
|
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
|
- const [selectedRegions, setSelectedRegions] = useState<FeatureCollection[]>([]);
|
|
|
|
|
|
+ const [selectedRegions, setSelectedRegions] = useState<any[]>([]);
|
|
const [regionsToSave, setRegionsToSave] = useState<RegionAddData[]>([]);
|
|
const [regionsToSave, setRegionsToSave] = useState<RegionAddData[]>([]);
|
|
const [regionData, setRegionData] = useState<RegionAddData | null>(null);
|
|
const [regionData, setRegionData] = useState<RegionAddData | null>(null);
|
|
const [regionPopupVisible, setRegionPopupVisible] = useState(false);
|
|
const [regionPopupVisible, setRegionPopupVisible] = useState(false);
|
|
- const mapRef = useRef<MapView>(null);
|
|
|
|
|
|
+ const mapRef = useRef<MapViewRef>(null);
|
|
|
|
+ const cameraRef = useRef<CameraRef>(null);
|
|
|
|
+
|
|
|
|
+ const [filterSelectedRegions, setFilterSelectedRegions] = useState<any[]>(generateFilter([]));
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
if (data && data.regions) {
|
|
if (data && data.regions) {
|
|
@@ -41,34 +70,18 @@ const AddRegionsScreen = ({ route }: { route: any }) => {
|
|
}
|
|
}
|
|
}, [data]);
|
|
}, [data]);
|
|
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
+ const ids = selectedRegions.map((region) => region.id);
|
|
|
|
+ setFilterSelectedRegions(generateFilter(ids));
|
|
|
|
+ }, [selectedRegions]);
|
|
|
|
+
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
const addRegionsAsync = async () => {
|
|
const addRegionsAsync = async () => {
|
|
if (regionsParams) {
|
|
if (regionsParams) {
|
|
- const promises = regionsParams.map((param) => {
|
|
|
|
- const foundRegion: Feature | undefined = (
|
|
|
|
- regionsGeojson as FeatureCollection
|
|
|
|
- ).features.find((region) => region.properties?.id === param.id);
|
|
|
|
- if (foundRegion) {
|
|
|
|
- setRegionsToSave((prevRegions) => [...prevRegions, param]);
|
|
|
|
- return {
|
|
|
|
- type: 'FeatureCollection',
|
|
|
|
- features: [
|
|
|
|
- {
|
|
|
|
- geometry: foundRegion.geometry,
|
|
|
|
- properties: foundRegion.properties,
|
|
|
|
- type: 'Feature'
|
|
|
|
- }
|
|
|
|
- ]
|
|
|
|
- };
|
|
|
|
- }
|
|
|
|
- return null;
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- const results = await Promise.all(promises);
|
|
|
|
- const validRegions = results.filter(Boolean);
|
|
|
|
|
|
+ setRegionsToSave((prevRegions) => [...prevRegions, ...regionsParams]);
|
|
|
|
|
|
setSelectedRegions(
|
|
setSelectedRegions(
|
|
- (prevSelectedRegions) => [...prevSelectedRegions, ...validRegions] as FeatureCollection[]
|
|
|
|
|
|
+ (prevSelectedRegions) => [...prevSelectedRegions, ...regionsParams] as any
|
|
);
|
|
);
|
|
}
|
|
}
|
|
};
|
|
};
|
|
@@ -77,35 +90,30 @@ const AddRegionsScreen = ({ route }: { route: any }) => {
|
|
}, [regionsParams]);
|
|
}, [regionsParams]);
|
|
|
|
|
|
const addRegionFromSearch = async (searchRegion: RegionAddData) => {
|
|
const addRegionFromSearch = async (searchRegion: RegionAddData) => {
|
|
- const foundRegion = (regionsGeojson as FeatureCollection).features.find(
|
|
|
|
- (region) => region.properties?.id === searchRegion.id
|
|
|
|
- );
|
|
|
|
-
|
|
|
|
- if (foundRegion) {
|
|
|
|
- const regionIndex = selectedRegions.findIndex(
|
|
|
|
- (region) => region.features[0].properties?.id === searchRegion.id
|
|
|
|
- );
|
|
|
|
- const regionFromApi = regions?.find((region) => region.id === searchRegion.id);
|
|
|
|
-
|
|
|
|
- if (regionIndex < 0 && regionFromApi) {
|
|
|
|
- const newRegion = {
|
|
|
|
- type: 'FeatureCollection',
|
|
|
|
- features: [
|
|
|
|
- {
|
|
|
|
- geometry: foundRegion.geometry,
|
|
|
|
- properties: foundRegion.properties,
|
|
|
|
- type: 'Feature'
|
|
|
|
- }
|
|
|
|
- ]
|
|
|
|
- };
|
|
|
|
-
|
|
|
|
- setSelectedRegions([...selectedRegions, newRegion as FeatureCollection]);
|
|
|
|
- setRegionsToSave((prevRegions) => [...prevRegions, regionFromApi]);
|
|
|
|
- setRegionPopupVisible(true);
|
|
|
|
-
|
|
|
|
- const bounds = turf.bbox(foundRegion);
|
|
|
|
- const region = calculateMapRegion(bounds);
|
|
|
|
- mapRef.current?.animateToRegion(region, 1000);
|
|
|
|
|
|
+ const regionIndex = selectedRegions.findIndex((region) => region.id === searchRegion.id);
|
|
|
|
+ const regionFromApi = regions?.find((region) => region.id === searchRegion.id);
|
|
|
|
+
|
|
|
|
+ if (regionIndex < 0 && regionFromApi) {
|
|
|
|
+ const newRegion = {
|
|
|
|
+ id: searchRegion.id,
|
|
|
|
+ name: searchRegion.name
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ setSelectedRegions([...selectedRegions, newRegion] as any);
|
|
|
|
+ setRegionsToSave((prevRegions) => [...prevRegions, regionFromApi]);
|
|
|
|
+ setRegionPopupVisible(true);
|
|
|
|
+
|
|
|
|
+ if (regionsList) {
|
|
|
|
+ const region = regionsList.data.find((region) => region.id === searchRegion.id);
|
|
|
|
+ if (region) {
|
|
|
|
+ const bounds = turf.bbox(region.bbox);
|
|
|
|
+ cameraRef.current?.fitBounds(
|
|
|
|
+ [bounds[2], bounds[3]],
|
|
|
|
+ [bounds[0], bounds[1]],
|
|
|
|
+ [50, 50, 50, 50],
|
|
|
|
+ 600
|
|
|
|
+ );
|
|
|
|
+ }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
};
|
|
@@ -129,77 +137,63 @@ const AddRegionsScreen = ({ route }: { route: any }) => {
|
|
};
|
|
};
|
|
|
|
|
|
const handleMapPress = useCallback(
|
|
const handleMapPress = useCallback(
|
|
- async (event: {
|
|
|
|
- nativeEvent: { coordinate: { latitude: any; longitude: any }; action?: string };
|
|
|
|
- }) => {
|
|
|
|
- if (event.nativeEvent?.action === 'polygon-press') return;
|
|
|
|
-
|
|
|
|
- const { latitude, longitude } = event.nativeEvent.coordinate;
|
|
|
|
- const point = turf.point([longitude, latitude]);
|
|
|
|
-
|
|
|
|
- let foundRegion = regionsGeojson ? findRegionInDataset(regionsGeojson, point) : null;
|
|
|
|
-
|
|
|
|
- if (foundRegion) {
|
|
|
|
- const id = foundRegion.properties?.id;
|
|
|
|
-
|
|
|
|
- const newRegion = {
|
|
|
|
- type: 'FeatureCollection',
|
|
|
|
- features: [
|
|
|
|
- {
|
|
|
|
- geometry: foundRegion.geometry,
|
|
|
|
- properties: foundRegion.properties,
|
|
|
|
- type: 'Feature'
|
|
|
|
- }
|
|
|
|
- ]
|
|
|
|
- };
|
|
|
|
|
|
+ async (event: any) => {
|
|
|
|
+ if (!mapRef.current) return;
|
|
|
|
|
|
- const regionIndex = selectedRegions.findIndex(
|
|
|
|
- (region) => region.features[0].properties?.id === id
|
|
|
|
|
|
+ try {
|
|
|
|
+ const { screenPointX, screenPointY } = event.properties;
|
|
|
|
+
|
|
|
|
+ const { features } = await mapRef.current.queryRenderedFeaturesAtPoint(
|
|
|
|
+ [screenPointX, screenPointY],
|
|
|
|
+ undefined,
|
|
|
|
+ ['regions']
|
|
);
|
|
);
|
|
|
|
|
|
- if (regionIndex >= 0) {
|
|
|
|
- const newSelectedRegions = [...selectedRegions];
|
|
|
|
- newSelectedRegions.splice(regionIndex, 1);
|
|
|
|
- setSelectedRegions(newSelectedRegions);
|
|
|
|
- setRegionsToSave(regionsToSave.filter((region) => region.id !== id));
|
|
|
|
- setRegionPopupVisible(false);
|
|
|
|
- return;
|
|
|
|
- } else {
|
|
|
|
- setSelectedRegions([...selectedRegions, newRegion] as FeatureCollection[]);
|
|
|
|
- }
|
|
|
|
|
|
+ if (features?.length) {
|
|
|
|
+ const selectedRegion = features[0];
|
|
|
|
|
|
- handleSetRegionData(id);
|
|
|
|
- setRegionPopupVisible(true);
|
|
|
|
|
|
+ if (selectedRegion.properties) {
|
|
|
|
+ const id = selectedRegion.properties.id;
|
|
|
|
|
|
- const bounds = turf.bbox(foundRegion);
|
|
|
|
- const region = calculateMapRegion(bounds);
|
|
|
|
|
|
+ const regionIndex = selectedRegions.findIndex((region) => region.id === id);
|
|
|
|
+
|
|
|
|
+ if (regionIndex >= 0) {
|
|
|
|
+ const newSelectedRegions = [...selectedRegions];
|
|
|
|
+ newSelectedRegions.splice(regionIndex, 1);
|
|
|
|
+ setSelectedRegions(newSelectedRegions);
|
|
|
|
+ setRegionsToSave(regionsToSave.filter((region) => region.id !== id));
|
|
|
|
+ setRegionPopupVisible(false);
|
|
|
|
+ return;
|
|
|
|
+ } else {
|
|
|
|
+ setSelectedRegions([...selectedRegions, selectedRegion.properties] as any);
|
|
|
|
+ }
|
|
|
|
|
|
- mapRef.current?.animateToRegion(region, 1000);
|
|
|
|
|
|
+ handleSetRegionData(id);
|
|
|
|
+ setRegionPopupVisible(true);
|
|
|
|
+
|
|
|
|
+ if (regionsList) {
|
|
|
|
+ const region = regionsList.data.find((region) => region.id === id);
|
|
|
|
+ if (region) {
|
|
|
|
+ const bounds = turf.bbox(region.bbox);
|
|
|
|
+ cameraRef.current?.fitBounds(
|
|
|
|
+ [bounds[2], bounds[3]],
|
|
|
|
+ [bounds[0], bounds[1]],
|
|
|
|
+ [50, 50, 50, 50],
|
|
|
|
+ 600
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('Failed to get coordinates on AddRegionsScreen', error);
|
|
}
|
|
}
|
|
},
|
|
},
|
|
[selectedRegions, regions]
|
|
[selectedRegions, regions]
|
|
);
|
|
);
|
|
|
|
|
|
- function renderGeoJSON() {
|
|
|
|
- if (!selectedRegions || !selectedRegions.length) return null;
|
|
|
|
-
|
|
|
|
- return selectedRegions.map((region, index) => (
|
|
|
|
- <Geojson
|
|
|
|
- key={index}
|
|
|
|
- geojson={region as any}
|
|
|
|
- fillColor="rgba(237, 147, 52, 0.5)"
|
|
|
|
- strokeColor={Colors.ORANGE}
|
|
|
|
- strokeWidth={Platform.OS == 'android' ? 2 : 1}
|
|
|
|
- zIndex={3}
|
|
|
|
- tracksViewChanges={false}
|
|
|
|
- />
|
|
|
|
- ));
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- const renderedGeoJSON = useMemo(() => renderGeoJSON(), [selectedRegions]);
|
|
|
|
-
|
|
|
|
return (
|
|
return (
|
|
- <SafeAreaView style={{ height: '100%' }}>
|
|
|
|
|
|
+ <SafeAreaView style={{ height: '100%' }} edges={['top']}>
|
|
<View style={styles.wrapper}>
|
|
<View style={styles.wrapper}>
|
|
<Header label={'Add Regions'} />
|
|
<Header label={'Add Regions'} />
|
|
<View style={styles.searchContainer}>
|
|
<View style={styles.searchContainer}>
|
|
@@ -221,40 +215,51 @@ const AddRegionsScreen = ({ route }: { route: any }) => {
|
|
</View>
|
|
</View>
|
|
|
|
|
|
<View style={styles.container}>
|
|
<View style={styles.container}>
|
|
- <MapView
|
|
|
|
|
|
+ <MapLibreGL.MapView
|
|
ref={mapRef}
|
|
ref={mapRef}
|
|
style={styles.map}
|
|
style={styles.map}
|
|
- showsMyLocationButton={false}
|
|
|
|
- showsCompass={false}
|
|
|
|
- zoomControlEnabled={false}
|
|
|
|
- mapType={Platform.OS == 'android' ? 'none' : 'standard'}
|
|
|
|
- maxZoomLevel={15}
|
|
|
|
- minZoomLevel={0}
|
|
|
|
- initialRegion={{
|
|
|
|
- latitude: 0,
|
|
|
|
- longitude: 0,
|
|
|
|
- latitudeDelta: 180,
|
|
|
|
- longitudeDelta: 180
|
|
|
|
- }}
|
|
|
|
|
|
+ styleJSON={VECTOR_MAP_HOST + '/nomadmania-maps.json'}
|
|
|
|
+ rotateEnabled={false}
|
|
|
|
+ attributionEnabled={false}
|
|
onPress={handleMapPress}
|
|
onPress={handleMapPress}
|
|
>
|
|
>
|
|
- <UrlTile
|
|
|
|
- urlTemplate={`${FASTEST_MAP_HOST}/tiles_osm/{z}/{x}/{y}`}
|
|
|
|
- maximumZ={15}
|
|
|
|
- maximumNativeZ={13}
|
|
|
|
- shouldReplaceMapContent
|
|
|
|
- minimumZ={0}
|
|
|
|
|
|
+ <MapLibreGL.Camera ref={cameraRef} />
|
|
|
|
+
|
|
|
|
+ <MapLibreGL.LineLayer
|
|
|
|
+ id="nm-regions-line-layer"
|
|
|
|
+ sourceID={nm_regions.source}
|
|
|
|
+ sourceLayerID={nm_regions['source-layer']}
|
|
|
|
+ filter={nm_regions.filter as any}
|
|
|
|
+ maxZoomLevel={nm_regions.maxzoom}
|
|
|
|
+ style={{
|
|
|
|
+ lineColor: 'rgba(14, 80, 109, 1)',
|
|
|
|
+ lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 0.2, 4, 1, 5, 1.5, 12, 3],
|
|
|
|
+ lineWidthTransition: { duration: 300, delay: 0 }
|
|
|
|
+ }}
|
|
|
|
+ belowLayerID="waterway-name"
|
|
/>
|
|
/>
|
|
- <UrlTile
|
|
|
|
- urlTemplate={`${FASTEST_MAP_HOST}/tiles_nm/grid/{z}/{x}/{y}`}
|
|
|
|
- maximumZ={15}
|
|
|
|
- maximumNativeZ={13}
|
|
|
|
- shouldReplaceMapContent
|
|
|
|
- minimumZ={0}
|
|
|
|
- opacity={0.3}
|
|
|
|
|
|
+ <MapLibreGL.FillLayer
|
|
|
|
+ id={nm_regions.id}
|
|
|
|
+ sourceID={nm_regions.source}
|
|
|
|
+ sourceLayerID={nm_regions['source-layer']}
|
|
|
|
+ filter={nm_regions.filter as any}
|
|
|
|
+ style={nm_regions.style}
|
|
|
|
+ maxZoomLevel={nm_regions.maxzoom}
|
|
|
|
+ belowLayerID="nm-regions-line-layer"
|
|
/>
|
|
/>
|
|
- {renderedGeoJSON}
|
|
|
|
- </MapView>
|
|
|
|
|
|
+
|
|
|
|
+ {selectedRegions && selectedRegions.length > 0 ? (
|
|
|
|
+ <MapLibreGL.FillLayer
|
|
|
|
+ id={selected_region.id}
|
|
|
|
+ sourceID={nm_regions.source}
|
|
|
|
+ sourceLayerID={nm_regions['source-layer']}
|
|
|
|
+ filter={filterSelectedRegions as any}
|
|
|
|
+ style={selected_region.style}
|
|
|
|
+ maxZoomLevel={selected_region.maxzoom}
|
|
|
|
+ belowLayerID="nm-regions-line-layer"
|
|
|
|
+ />
|
|
|
|
+ ) : null}
|
|
|
|
+ </MapLibreGL.MapView>
|
|
</View>
|
|
</View>
|
|
{regionPopupVisible && regionData && (
|
|
{regionPopupVisible && regionData && (
|
|
<View style={styles.popupWrapper}>
|
|
<View style={styles.popupWrapper}>
|