import { Pressable, StyleSheet, TouchableOpacity, Dimensions, TextInput, KeyboardAvoidingView, View, NativeMethods, Keyboard, ActivityIndicator, BackHandler, _Text } from 'react-native';
import MapView from 'react-native-map-clustering';
import { Text } from './Themed';
import {getCustomFilter, getDefaultCounty, getKPDCIP, getMarkerByID, getMarkerByLocation, getSor, timeout, testConnection, getStats} from '../components/backend';
import index from '../app/(tabs)/(profile)/profile';
import { createRef, useEffect, useRef, useMemo, useCallback } from 'react';
import React from 'react';
import Colors from '../constants/Colors';
import * as Location from 'expo-location';
import { MapMarker, Marker } from 'react-native-maps';
import map from '../app/(tabs)/(map)/map';
import { storeData, getData } from './storage';
import { Badge, BadgeIcon, BadgeText, Box, Button, CheckIcon, ChevronDownIcon, Heading, HStack, Icon, ModalBackdrop, ModalBody, ModalCloseButton, ModalContent, ModalHeader, Select, SelectBackdrop, SelectContent, SelectDragIndicator, SelectDragIndicatorWrapper, SelectIcon, SelectInput, SelectItem, SelectPortal, SelectTrigger, set, VStack } from '@gluestack-ui/themed';
import mapstyle from '../constants/mapstyle.json';
import { globalStyles } from '../constants/styles';
import { StatusBar } from 'react-native';
import { AntDesign, FontAwesome, FontAwesome5, FontAwesome6, Ionicons, MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons';
import { router, useFocusEffect } from 'expo-router';
import Checkbox from 'expo-checkbox';
import { Modal } from '@gluestack-ui/themed';
import { useNavigation } from 'expo-router';
import { getIncidentEmoji } from '../constants/emojis';
import { FlatList, Gesture, GestureHandlerRootView, PanGestureHandler, ScrollView } from 'react-native-gesture-handler';
import MarkerModalView from './markerModalView';
import BottomSheet, { BottomSheetFlatList, BottomSheetView, BottomSheetModal, BottomSheetModalProvider, BottomSheetScrollView, BottomSheetSectionList } from '@gorhom/bottom-sheet';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import ClusterView from './clusterSheetView';
import { getDist } from './helpers';
import global from '../constants/global';
import { formatWithCommas } from '../app/(tabs)/(integrations)/integrations';
import { GooglePlacesAutocomplete, GooglePlacesAutocompleteRef } from 'react-native-google-places-autocomplete';
import 'react-native-get-random-values';
import Constants from 'expo-constants';
import { formatnum } from './postListView';
import { useDebounce } from '@uidotdev/usehooks';
import { useSettings } from './appSettingsContext';
function findDecimalPlace(num: number){
// Convert the number to a string, and remove leading/trailing spaces.
let str = num.toString().trim();
// If the number is a whole number, return "No decimal place" or similar.
if (!str.includes(".")) {
return 0.1;
}
// Split the string into integer and decimal parts
let parts = str.split(".");
let decimalPart = parts[1];
// Iterate over the decimal part from left to right
for (let i = 0; i < decimalPart.length; i++) {
if (decimalPart[i] !== "0") {
// The position of the non-zero digit corresponds to the place value
const placeValue = Math.pow(10, -(i + 1));
return placeValue; // return 0.1, 0.01, 0.001, etc.
}
}
return 0.1;
}
function formatDecimal(num: number){
if((!num && num!==0)|| typeof num != 'number'){return '?'}
if(num ==100){
return num.toFixed(0);
}
return num.toFixed(2);
}
export default function Map() {
// Expo routing for tracking screen changes
const navigation = useNavigation();
const GOOGLE_API = process.env.GOOGLE_API;
//References
const mapRef = useRef<MapView>(null);
const modal = useRef<FlatList>(null);
const markerRefs = useRef<(MapMarker | null)[]>([]);
const sheetRef = useRef<BottomSheetModal>(null);
const clusterListRef = useRef<FlatList>(null);
const searchRef = useRef<GooglePlacesAutocompleteRef>(null);
//States
const [kpd_cip, set_cip] = React.useState([]); //Stores all KPD_CIP data
const [zones, setZones] = React.useState([]); //Stores all zone data
const [utpd, setUtpd] = React.useState([]); //Stores all UTPD data
const [sor, setSor] = React.useState([]); //Stores all SOR data
const [Region, setRegion] = React.useState({ //Stores current region
latitude: 35.9568879,
longitude: -83.9233003,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
});
const settings = useSettings().settings; //Stores app settings
const [markerCache, setMarkerCache] = React.useState<any>(); //Cache loaded from storage. Stores last KPD_CIP request.
const [markers, setMarkers] = React.useState<any>([]); // Stores markers to be rendered on map.
const [networkError, setNetworkError] = React.useState(true); //Stores flag for failure to connect to backend.
const [userLocation, setUserLocation] = React.useState<any>(); //Stores user location [latitude, longitude]
const [filterColor, setFilterColor] = React.useState(Colors.dark.redPastel); //Stores filter button press color. For clicking appearance. May need to find a better way
const [locationColor, setLocationColor] = React.useState(Colors.dark.redPastel); //Stores location button press color. For clicking appearance. May need to find a better way
const [helpColor, setHelpColor] = React.useState(Colors.dark.redPastel); //Stores help button press color. For clicking appearance. May need to find a better way
const [statColor, setStatColor] = React.useState(Colors.dark.redPastel); //Stores help button press color. For clicking appearance. May need to find a better way
const [mapLayer, setMapLayer] = React.useState("standard"); //Stores map layer state [standard, satellite, hybrid, terrain
const [mapFlag, setMapFlag] = React.useState(false); //Stores flag for initial render
const [locStat, setLocStat] = React.useState<any>(); //Stores location permission state
const [atrcoords, setAtrCoords] = React.useState<any>(); //Stores coordinates of region to animate to
const [atrclustcoords, setAtrClustCoords] = React.useState<any>(); //Stores coordinates of cluster to animate to
const [mapPadding, setMapPadding] = React.useState({top:275, right:0, bottom:0, left:0}); //Stores padding for map
const [stats, setStats] = React.useState<any>(); //Stores stats data]
const [searchStats, setSearchStats] = React.useState<any>(); //Stores search stats data
//Modal fields
const [markerModalFocus, setMarkerModalFocus] = React.useState(false); //Stores marker modal focus state [true, false
const [modalMarker, setModalMarker] = React.useState<any>();//Stores marker of modal
const [clusterFlag, setClusterFlag] = React.useState(false);//Stores flag for cluster modal
const [clusterIndex, setClusterIndex] = React.useState(0);//Stores cluster index
const snapPoints = useMemo(() => ["6%","11%", "29%", "50%", "80%", "96%"], []);
const [showStats, setShowStats] = React.useState(true);//Stores flag for stats modal
const [bottomVisible, setBottomVisible] = React.useState(false);//Stores flag for bottom sheet visibility
const [sheetHeight, setSheetHeight] = React.useState(0.29);//Stores height of bottom sheet
const [showLocalStats, setShowLocalStats] = React.useState(false);//Stores flag for local stats modal
// This will later be changed to be dynamic for each jurisdiction
// Currently, there is an array for each source, with each entry
// being the last date it was pulled.
const fMatrix = [
[0,0,0,0,0], //KPD CIP
[0,0,0,0,0], // KCSO
[0,0,0,0,0] // UTPD
]
//Filter modal fields
const [filterModalVisible, setFilterModalVisible] = React.useState(false);//Stores filter modal visibility state
const [applyFilterColor, setApplyFilterColor] = React.useState(Colors.dark.redPastel);//Stores apply filter button press color. For clicking appearance. May need to find a better way
const [kpdFiltered, setKpdFiltered] = React.useState(true);//Stores KPD filter state
const [utpdFiltered, setUtpdFiltered] = React.useState(true);//Stores UTPD filter state
const [kcsdFiltered, setKcsdFiltered] = React.useState(true);//Stores KCSD filter state
const [sorFiltered, setSorFiltered] = React.useState(false);//Stores SOR filter state
const [class1Filtered, setClass1Filtered] = React.useState(false);//Stores class 1 filter state
const [class2Filtered, setClass2Filtered] = React.useState(true);//Stores class 2 filter state
const [class3Filtered, setClass3Filtered] = React.useState(true);//Stores class 3 filter state
const [class4Filtered, setClass4Filtered] = React.useState(true);//Stores class 4 filter state
const [class5Filtered, setClass5Filtered] = React.useState(true);//Stores class 5 filter state
const [applyFilter, setApplyFilter] = React.useState(false);//Stores apply filter state
const [filtered, setFiltered] = React.useState<any>([]);//Stores filtered markers [kpd, utpd, kcsd, class1, class2, class3, class4, class5]. Buffer array for markers array
const [filterDate, setFilterDate] = React.useState("2");//Stores filter date state [2,4,7,14,30]
const [cachedDate, setCachedDate] = React.useState(2);//Stores how far back cached data goes
const [fetchingFilterFlag, setFetchingFilterFlag] = React.useState(false);//Stores flag for fetching filter data. True is currently fetching custom filter data
const [customFilterFlags, setCustomFilterFlags] = React.useState([false, false, false]);//Stores custom filter flags [class1, dog, sor]
const [filterMatrix, setFilterMatrix] = React.useState<number[][]>(fMatrix);//Stores filter matrix for custom filter
const [cipFlag, setCipFlag] = React.useState(false);//Stores flag for KPD CIP data to prevent setting cache
const [quickSor, setQuickSor] = React.useState(false);//Stores flag for quick SOR filter
const [quickCip, setQuickCip] = React.useState(false);//Stores flag for quick CIP filter
const [quickHighRisk, setQuickHighRisk] = React.useState(false);//Stores flag for quick high risk filter
const [quickMedRisk, setQuickMedRisk] = React.useState(false);//Stores flag for quick med risk filter
const [quickLowRisk, setQuickLowRisk] = React.useState(false);//Stores flag for quick low risk filter
const [searchFlag, setSearchFlag] = React.useState(false);//Stores flag for search bar visibility
const [searchText, setSearchText] = React.useState("");//Stores search text
const [searchLength, setSearchLength] = React.useState(10000);//Stores search text length
const debouncedSearch = useDebounce(searchText, 500);//Debounces search text
const [outOfBounds, setOutOfBounds] = React.useState(false);//Stores flag for out of bounds warning
const [outOfBoundsModal, setOutOfBoundsModal] = React.useState(false);//Stores flag for out of bounds modal
// Knoxville coords
const initReg = {
latitude: 35.9568879,
longitude: -83.9233003,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}
useEffect(()=>{
const backHandler = BackHandler.addEventListener('hardwareBackPress', ()=>{
if(markerModalFocus){
setMarkerModalFocus(false);
return true;
}
if(bottomVisible){
setBottomVisible(false);
sheetRef.current?.snapToIndex(1);
return true;
}
return false;
})
return ()=>backHandler.remove();
}, [markerModalFocus, bottomVisible])
//Sets user location to default if location permissions are denied
useEffect(()=>{
if(locStat && locStat.granted == false){
setUserLocation({coords:{latitude: 35.9568879, longitude: -83.9233003}});
}
},[locStat])
//Fetches data on load
useEffect(() => {
/**
* Gets location permissions and user location on load.
*/
async function getLocPerm(){
//Requests location permissions
try{
setLocStat(await Location.requestForegroundPermissionsAsync());
// setUserLocation(await Location.getCurrentPositionAsync({}));
setUserLocation(await Location.getLastKnownPositionAsync({}));
}catch(e){
console.log("Error getting location perms:",e);
setUserLocation({coords:{latitude: 35.9568879, longitude: -83.9233003}});
}
}
getLocPerm();
/**
* Fetches marker data from backend and stores it in kpd_cip and markerCache.
*/
async function fetchData() {
try {
getData("markers").then((res) => {
console.log(
"STOR: Retrieved",
[...res[0], ...res[1], ...res[2]].length,
"markers on start."
);
setMarkerCache([...res[0], ...res[1], ...res[2]]);
});
} catch (e) {
console.log(e);
}
try {
await getDefaultCounty().then((res) => {
console.log(
"API: Retrieved",
[...res[0], ...res[1], ...res[2]].length,
"markers on start."
);
setNetworkError(false);
set_cip(res[0]);
setZones(res[1]);
setUtpd(res[2]);
setMarkerCache([...res[0], ...res[1], ...res[2]]);
storeData("markers", res);
});
} catch (e) {
console.log(e);
setNetworkError(true);
}
}
// Fetches data and opens bottom sheet on screen load
sheetRef.current?.present();
fetchData();
},[])
//Sets global user location on change/load
useEffect(()=>{
if(!userLocation){return;}
console.log("User location updated.")
console.log("Current user location: ", userLocation.coords.latitude, userLocation.coords.longitude)
global.userLocation = userLocation as any;
//Checks to use whether user is in the range of knoxville
if(userLocation.coords.latitude < 35.8 || userLocation.coords.latitude > 36.1 || userLocation.coords.longitude < -84.4 || userLocation.coords.longitude > -83.8){
//User not in knox
console.log("User out of bounds.")
setOutOfBounds(true);
setOutOfBoundsModal(true);
}
getStats(userLocation.coords.latitude, userLocation.coords.longitude, 1).then((res) => {
res = res as any
let temp = res.crime;
let prev = []
let cur = []
for(let i=0; i<5; i++){
let t = res.monthly as [any]
let l = t.findIndex(function(e:any){
return e.for.indexOf("Class" + (i+1)) == 0;
})
let p = t.findIndex(function(e:any){
return e.for.indexOf("mClass" + (i+1)) == 0;
})
prev[i] = t[l].count
cur[i] = t[p].count
}
// console.log("STATS", res.prediction, temp,[cur,prev])
setStats([res.prediction, temp,[cur,prev]])
}).catch((e) => {
console.log("ERROR GETTING SHEET STATS: ",e);
})
}, [userLocation])
// When user leaves screen, close marker modal without animation.
useEffect(()=>{
const unsubscribe = navigation.addListener('blur', ()=>{
setMarkerModalFocus(false);
})
return unsubscribe;
},[navigation])
//Use state to rerender markers whenever markerCache/kpd_cip is updated
useEffect(()=>{
if(!markerCache){return;}
// console.log("Marker cache updated.", markerCache.length)
renderMarkers();
},[markerCache]);//Only needs to be called for markerCache as it gets updated in all circumstances.
useEffect(()=>{
if(sorFiltered && !(sor.length>0)){
// Fetches SOR data from TBI api when SOR filter is enabled
getSor().then((res) => {
// console.log(res[0])
setSor(res);
}).catch((e) => {
console.log(e);
})
}else{
renderMarkers()
}
// console.log('Filters applied')
}, [applyFilter])
//Whenever filter changes, markers updates
useEffect(()=>{
// console.log("Filter length: "+filtered.length)
setMarkers(filtered);
}, [filtered])
useEffect(()=>{
modal.current?.scrollToIndex({index:clusterIndex, animated:false});
},[clusterIndex])
useEffect(()=>{
if(userLocation){
setMapFlag(true)
}
},[userLocation])
// Rerenderes markers when sor data is received
useEffect(()=>{
if(sor.length>0){
console.log("Sor pop")
renderMarkers();
}
}, [sor])
useEffect(()=>{
if(atrcoords){
//@ts-ignore
mapRef.current?.animateToRegion({
latitude: atrcoords.lat +(findDecimalPlace(Region.latitudeDelta)/10)*4*(Region.latitudeDelta/findDecimalPlace(Region.latitudeDelta)/2),
longitude: atrcoords.long,
latitudeDelta: Region.latitudeDelta,
longitudeDelta: Region.longitudeDelta
})
}
},[atrcoords])
useEffect(()=>{
if(atrclustcoords){
//@ts-ignore
mapRef.current?.animateToRegion({
latitude: atrclustcoords.lat,
longitude: atrclustcoords.long,
latitudeDelta: atrclustcoords.zoom,
longitudeDelta: atrclustcoords.zoom
})
}
}, [atrclustcoords])
useEffect(()=>{
if(searchText.length>4){
setSearchFlag(true);
setSearchLength(4)
searchRef.current?.setAddressText(searchText);
}else{
setSearchLength(10000)
setSearchFlag(false);
}
},[debouncedSearch])
// useEffect(()=>{console.log(markers)},[markers])
// In event of denied location permissions, allows map to render without location
useEffect(()=>{if(locStat){if(!locStat.granted){setMapFlag(true)}}},[locStat])
/**
* Helper filter function to filter by source and class
* @param cip array of cip marker objects
* @param zones array of zone marker objects
* @param utpd array of clery marker objects
*/
function filterByClass(cip:any, zones:any, utpd:any, sors:any=[],dateFlag:Boolean=false, data:any[]=markerCache){
// console.log(sors[0], cip[0])
// console.log(cip.length, zones.length, utpd.length)
if (cip.length>0 || zones.length>0 || utpd.length>0 || sors.length>0){
//Filters markers by source
let tempSource = [...kpdFiltered?cip:[], ...kcsdFiltered?zones:[], ...utpdFiltered?utpd:[]];
// Checks to make sure there are markers to filter
if(tempSource.length>0 || sors.length>0){
//Filters markers by class aka by color set previously
let class1 = class1Filtered?tempSource.filter((marker: any) => marker.props.pinColor == "green"):[];
let class2 = class2Filtered?tempSource.filter((marker: any) => marker.props.pinColor == "yellow"):[];
let class3 = class3Filtered?tempSource.filter((marker: any) => marker.props.pinColor == "orange"):[];
let class4 = class4Filtered?tempSource.filter((marker: any) => marker.props.pinColor == "tomato"):[];
let class5 = class5Filtered?tempSource.filter((marker: any) => marker.props.pinColor == "navy"):[];
// console.log("CLASS COUNTS","1: "+class1.length, "2: "+class2.length, "3: "+class3.length, "4: "+class4.length, "5: "+class5.length, "SOR: "+sors.length)
setFiltered([...class1, ...class2, ...class3, ...class4, ...class5,...sorFiltered?sors:[]]);
// Moved date filter to here to avoid race condition with class filters
if(dateFlag){return;} //Returns if there is no need for date filtering
let date = new Date();
let tempDate;
if(filterDate != "0.24"){
tempDate = new Date(date.setDate(date.getDate()-parseInt(filterDate)));
}else{
tempDate = new Date(date.setHours(date.getHours()-4));
}
// console.log("Filter date: ", filterDate)
let classes = [...class1, ...class2, ...class3, ...class4, ...class5]
// console.log("Classes: ", classes.length)
// Filters
let filtered = classes.filter((marker: any) => {
// Goes through cache to get date of marker
let markerDate = new Date("2016-05-26");//May harambe rest in peace
let sources=["C", "K", "U", "D", "S"]
data.forEach((cacheMarker: any) => {
if(marker.props.identifier.substring(1) == cacheMarker.id && sources[cacheMarker.source-1] ==marker.props.identifier.charAt(0)){
markerDate = new Date(parseInt(cacheMarker.date+"000")); //Have to add 3 zeros due to difference in date format in db
// console.log("Markerdate changed", marker.props.identifier, markerDate)
}
})
// console.log(markerDate>tempDate, markerDate, tempDate, filterDate, markerCache.length)
return markerDate > tempDate;
});
filtered = [...filtered, ...sorFiltered?sors:[]]
// console.log("Changing filtered", filtered.length)
setFiltered(filtered);
}else{
// console.log("Cleared filtered")
setFiltered([])
}
}else{
setMarkers([]);
}
}
/**
* Main filter function for markers.
* Filters markers by class, source, and date.
* Sources: KPD CIP, Zones, UTPD, SOR*
* @param cip array of marker objects
* @param zones array of marker objects
* @param utpd array of marker objects
*/
function filterMarkers(cip:any, zones:any, utpd:any, sors:any=[]){
//Checks to see if a call needs to be made
// Currently only checks for class 1 as these others have not been implemented
let parsedFilterDate = parseInt(filterDate);
if(parsedFilterDate > cachedDate || (!customFilterFlags[0]&&class1Filtered)){
// Checks if a call is already being made
if(fetchingFilterFlag){return;}
console.log("Fetching custom filter data...")
setFetchingFilterFlag(true);
let temp = filterDate
if(filterDate == "0.24"){
temp="2"
}
// Makes a backend call to get custom filter data
getCustomFilter(temp, class1Filtered).then((rows) => {
let tempDate = new Date();
tempDate.setDate(tempDate.getDate()-parseInt(temp))
// Updates cache with new data
let newData = [...rows[0][0], ...rows[0][1], ...rows[0][2]];
setMarkerCache(newData);
// Ensures network error flag is off
setNetworkError(false)
// Sets flag for class1 filter to avoid repeat calls
if(class1Filtered){
let filterFlags = [...customFilterFlags];
filterFlags[0]=true
setCustomFilterFlags(filterFlags);
}
// Sets new cache date if needed
if(parseInt(rows[1].date) > cachedDate){
// console.log("UPDATING CACHED DATE")
setCachedDate(rows[1].date);
// Makes sure class1 data can still be received after custom call for different params
if(!class1Filtered&&customFilterFlags[0]){
let filterFlags = [...customFilterFlags];
filterFlags[0]=false
setCustomFilterFlags(filterFlags);
}
}
// Data needs to be processed after retrieval
let cip: any[] = [];
let zones: any[] = [];
let utpd: any[] = [];
let sors: any[] = [];
// Updates constant states to new data
set_cip(rows[0][0]);
setZones(rows[0][1]);
setUtpd(rows[0][2]);
cip = initMarkers(rows[0][0],1);
zones = initMarkers(rows[0][1],2);
utpd = initMarkers(rows[0][2],3);
sors = initMarkers(sors, 5)
// Does a final filter with new data to populate markers.
// Only by class/source as date has already been filtered in the backend
// Filtering here is mainly to update the markers array
filterByClass(cip, zones, utpd, sors, filterDate == "0.24"? false:true, newData);
setFetchingFilterFlag(false);
}).catch((e) => {
console.log(e);
setFetchingFilterFlag(false);
})
}else{
// Calls helper function to filter by class and source
filterByClass(cip, zones, utpd, sors);
}
}
/**
* Renders markers on the map based on the current region, filter, and network status.
*/
async function renderMarkers(){
let cip: any[] = [];
let zone: any[] = [];
let ut: any[] = [];
let sors: any[] = [];
cip = initMarkers(kpd_cip,1);
zone = initMarkers(zones,2);
ut= initMarkers(utpd,3);
sors = initMarkers(sor,5);
filterMarkers(cip, zone, ut, sors);
}
/**
* Refreshes markers on the map by fetching new data from the backend and updating the cache.
*/
async function refreshMarkers(){
//Empties out modal fields and hides modal
setModalMarker([]);
setMarkerModalFocus(false);
//Closes bottom sheet
sheetRef.current?.collapse()
// Resets states
setCipFlag(false);
setFetchingFilterFlag(false);
setCustomFilterFlags([false, false, false]);
setClass1Filtered(false)
setClass2Filtered(true)
setClass3Filtered(true)
setClass4Filtered(true)
setClass5Filtered(true)
setSorFiltered(false)
setKpdFiltered(true)
setUtpdFiltered(true)
setKcsdFiltered(true)
setFilterDate("2")
setCachedDate(2)
setQuickCip(false);
setQuickSor(false);
setQuickHighRisk(false);
setQuickMedRisk(false);
setQuickLowRisk(false);
// Gets from cache first in case of network failure
getData("markers").then((res) => {
console.log("STOR: Retrieved",[...res[0], ...res[1], ...res[2]].length, "markers on refresh.")
setMarkerCache([...res[0], ...res[1], ...res[2]]);
});
// Gets from backend for data refresh
await getDefaultCounty().then((res) => {
console.log("API: Retrieved",[...res[0], ...res[1], ...res[2]].length, "markers on refresh.")
setNetworkError(false);
set_cip(res[0]);
setZones(res[1]);
setUtpd(res[2]);
setMarkerCache([...res[0], ...res[1], ...res[2]]);
storeData("markers", res)
let cip: any[] = [];
let zone: any[] = [];
let ut: any[] = [];
cip = initMarkers(res[0], 1);
zone = initMarkers(res[1], 2);
ut = initMarkers(res[2], 3);
filterMarkers(cip, zone, ut);
})
}
/**
* Resets filter states and reloads markers from markerCache.
*/
async function clearFilters(){
//Clears modal fields and hides modal
setModalMarker([]);
setMarkerModalFocus(false);
//Closes bottom sheet
sheetRef.current?.collapse();
// Resets filter states
setClass1Filtered(false);
setClass2Filtered(true);
setClass3Filtered(true);
setClass4Filtered(true);
setClass5Filtered(true);
setSorFiltered(false);
setKpdFiltered(true);
setUtpdFiltered(true);
setKcsdFiltered(true);
setFilterDate("2");
setCipFlag(false);
setQuickCip(false);
setQuickSor(false);
setQuickHighRisk(false);
setQuickMedRisk(false);
setQuickLowRisk(false);
// Reloads from cache
set_cip(
markerCache.filter(function (m: any) {
return m.source == 1;
})
);
setZones(
markerCache.filter(function (m: any) {
return m.source == 2;
})
);
setUtpd(
markerCache.filter(function (m: any) {
return m.source == 3;
})
);
// Renders markers
renderMarkers();
setApplyFilter(!applyFilter);
}
/**
* Function for handling press on cluster view element
* Using callback to memoize function
*/
const setModFoc = useCallback((index:any) => {
sheetRef.current?.snapToIndex(0);
setMarkerModalFocus(true);
modal.current?.scrollToIndex({ index: index, animated: false});
setClusterIndex(index)
},[])
function initMarkers(markers:any, source:any=1){
let returnedMarkers: any[] = [];
let sources=["C", "K", "U", "D", "S"];
//Maps markers to array. Sets array to none when region is too large to display efficiently.
markers&&source!=5 ? returnedMarkers=markers.map((marker:any, index:any)=>{//Renders all markers within region radius
let tId = sources[marker.source-1] + marker.id.toString();
return (
<Marker
key={tId}
coordinate={{
latitude: marker.latitude,
longitude: marker.longitude
}}
identifier={tId.toString()}
ref={markers=> (markerRefs.current[index] = markers)}
tracksViewChanges={false}
// Pin color is no longer used for aesthetics, but is still needed for class filtering logic
pinColor={marker.class == 1 ? 'green' :
marker.class == 2 ? 'yellow' :
marker.class == 3 ? 'orange' :
marker.class == 4 ? 'tomato' :
marker.class == 5 ? 'navy':
'violet'}
onPress={(event)=>{
// @ts-ignore
setModalMarker([event._targetInst.return.key]);
setAtrCoords({lat:marker.latitude, long:marker.longitude, zoom:0.001});
sheetRef.current?.snapToIndex(0);
Keyboard.dismiss();
setClusterFlag(false);
searchRef.current?.clear();
searchRef.current?.setAddressText("");
setSearchFlag(false);
// @ts-ignore
modal.current?.scrollToIndex({index:0, animated:false});
setMarkerModalFocus(true);
}}
>
<Text
style={[{backgroundColor:
marker.class == 1 ? Colors.map.class1 :
marker.class == 2 ? Colors.map.class2 :
marker.class == 3 ? Colors.map.class3 :
marker.class == 4 ? Colors.map.class4 :
marker.class == 5 ? Colors.map.class5:
Colors.map.default}, styles.markerIcon]}
>
{getIncidentEmoji(marker.source==1?marker.code:marker.source==2?marker.incident:marker.source==3?marker.incident:"", marker.source.toString())}
</Text>
</Marker>
)
}): null;
markers.length>0&&source==5 ? returnedMarkers=markers.map((marker:any, index:any)=>{//Renders all sor markers
if(marker.attributes.homeless == "YES"){return}
if(marker.geometry?.x == null || marker.geometry?.y == null){return}
return (
<Marker
key={"S"+marker.attributes.Tid}
coordinate={{
latitude: marker.geometry?.y,
longitude: marker.geometry?.x
}}
identifier={"S"+marker.attributes.Tid}
ref={markers=> (markerRefs.current[index] = markers)}
tracksViewChanges={false}
// Pin color is no longer used for aesthetics, but is still needed for class filtering logic
pinColor={marker.attributes.Classification == "SEXUAL"|| marker.attributes.Classification=="VIOLENT" ? 'orange' :
marker.attributes.Classification == "OFFENDER AGAINST CHILDREN" ? 'tomato' :
'violet'}
onPress={(event)=>{
// @ts-ignore
setModalMarker([event._targetInst.return.key]);
setAtrCoords({lat:marker.geometry.y, long:marker.geometry.x, zoom: 0.001});
sheetRef.current?.snapToIndex(0);
Keyboard.dismiss();
searchRef.current?.clear();
searchRef.current?.setAddressText("");
setSearchFlag(false);
setClusterFlag(false);
// @ts-ignore
modal.current?.scrollToIndex({index:0, animated:false});
setMarkerModalFocus(true);
}}
>
<Text
style={[{backgroundColor:
marker.attributes.Classification == "SEXUAL"|| marker.attributes.Classification=="VIOLENT" ? Colors.map.class3 :
marker.attributes.Classification == "OFFENDER AGAINST CHILDREN" ? Colors.map.class4 :
Colors.map.default}, styles.markerIcon]}
>
{getIncidentEmoji("", "5")}
</Text>
</Marker>
)
}): null;
//Loads from cache in the event of a network failure.
if(networkError && markers.length == 0){
markerCache ? returnedMarkers=markerCache.filter(function(m:any){
return m.source == source;
}).map((marker:any, index:any)=>{//Renders all markers within region radius
let tId = sources[marker.source - 1] + marker.id.toString();
return (
<Marker
key={tId}
coordinate={{
latitude: marker.latitude,
longitude: marker.longitude
}}
identifier={tId.toString()}
ref={markers=> (markerRefs.current[index] = markers)}
tracksViewChanges={false}
// title={new Date(parseInt(marker.date)).toLocaleString()}//Need to change to no longer parseInt when DB is updated
// description={marker.id+","+marker.source+","+marker.class+","+marker.code+":"+marker.incident}
pinColor={marker.class == 1 ? 'green' :
marker.class == 2 ? 'yellow' :
marker.class == 3 ? 'orange' :
marker.class == 4 ? 'tomato' :
marker.class == 5 ? 'navy':
'violet'}
onPress={(event)=>{
// @ts-ignore
setModalMarker([event._targetInst.return.key]);
setAtrCoords({lat:marker.latitude, long:marker.longitude, zoom:0.001});
sheetRef.current?.snapToIndex(0);
Keyboard.dismiss();
setClusterFlag(false);
searchRef.current?.clear();
searchRef.current?.setAddressText("");
setSearchFlag(false);
// @ts-ignore
modal.current?.scrollToIndex({index:0, animated:false});
setMarkerModalFocus(true);
}}
>
<Text
style={[{backgroundColor:
marker.class == 1 ? Colors.map.class1 :
marker.class == 2 ? Colors.map.class2 :
marker.class == 3 ? Colors.map.class3 :
marker.class == 4 ? Colors.map.class4 :
marker.class == 5 ? Colors.map.class5:
Colors.map.default}, styles.markerIcon]}
>
{getIncidentEmoji(marker.source==1?marker.code:marker.source==2?marker.incident:marker.source==3?marker.incident:"", marker.source.toString())}
</Text>
</Marker>
)
}):null;
}
return returnedMarkers;
}
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<BottomSheetModalProvider>
<View style={globalStyles.container}>
{/* Out of bounds modal message */}
{settings.outOfBounds && <Modal
isKeyboardDismissable={false}
onClose={() => {setOutOfBoundsModal(false)}}
isOpen={outOfBoundsModal}
>
<ModalBackdrop/>
<ModalContent style={styles.outofboundsalert}>
<VStack rowGap={10}>
<Text style={{fontSize:20, fontWeight:'bold', color:Colors.dark.redBright}}>Warning</Text>
<Text style={styles.outofboundstext}>
You do not appear to be within the bounds of any of our supported locations.{"\n\n"}
If this is a mistake, please check your location settings or contact me.{"\n\n"}
If you would like your location to be supported, please request it by contacting me and I'll be happy to look into it!
</Text>
<Button style={styles.outofboundsbutton} onPress={()=>{setOutOfBoundsModal(false)}}>
<Text style={styles.outofboundsbuttontext}>Sounds like a plan</Text>
</Button>
</VStack>
</ModalContent>
</Modal>}
{/* Go to my location button */}
<Pressable
disabled={!locStat?.granted}
style={styles.mapButton}
onPress={() => {
// @ts-ignore
mapRef.current?.animateToRegion({
latitude: userLocation.coords.latitude,
longitude: userLocation.coords.longitude,
latitudeDelta: 0.03,
longitudeDelta: 0.03,
});
}}
onPressIn={() => {
setLocationColor(Colors.dark.redPastelClicked);
}}
onPressOut={() => {
setLocationColor(Colors.dark.redPastel);
}}
>
<FontAwesome6
name="location-crosshairs"
size={30}
color={locationColor}
/>
</Pressable>
{/* Search bar */}
<View
style={[
styles.mapSearch,
{
backgroundColor: "transparent",
height: searchFlag ? height * 0.3 : height * 0.07,
zIndex: 100000,
width: width * 0.9,
},
]}
>
<GooglePlacesAutocomplete
placeholder="Search address"
onFail={(error) => console.error(error)}
minLength={searchLength}
fetchDetails={true}
// isNewPlacesAPI={true}
listViewDisplayed="auto"
keyboardShouldPersistTaps="handled"
ref={searchRef}
// @ts-ignore
renderRightButton={() => {
if (!searchFlag) {
return null;
} else {
return (
<Pressable
style={{
backgroundColor: Colors.dark.background,
width: width * 0.12,
height: height * 0.07,
justifyContent: "center",
alignItems: "center",
borderTopRightRadius: 10,
borderBottomRightRadius: 10,
}}
onPress={() => {
searchRef.current?.clear();
searchRef.current?.setAddressText("");
setSearchFlag(false);
}}
>
<MaterialIcons
name="clear"
size={20}
color={Colors.dark.redBright}
/>
</Pressable>
);
}
}}
textInputProps={{
onPress: () => {
sheetRef.current?.snapToIndex(1);
setMarkerModalFocus(false);
},
returnKeyType: "search",
onFocus() {
setMarkerModalFocus(false);
sheetRef.current?.snapToIndex(1);
},
onChangeText(text) {
setSearchText(text);
// if (text.length >= 4) {
// setSearchFlag(true);
// searchRef.current?.setAddressText(text);
// } else {
// setSearchFlag(false);
// }
},
placeholderTextColor: Colors.dark.text,
}}
styles={{
container: {
flex: 1,
// backgroundColor: Colors.dark.background,
// width: width * 0.78,
height: searchFlag ? height * 0.3 : height * 0.07,
},
textInputContainer: {
width: width * 0.78,
},
textInput: {
backgroundColor: Colors.dark.background,
color: Colors.dark.text,
fontSize: 16,
textDecorationColor: Colors.dark.text,
textDecorationLine: "none",
height: height * 0.07,
borderRadius: 10,
top: 0,
borderTopRightRadius: searchFlag ? 0 : 10,
borderBottomRightRadius: searchFlag ? 0 : 10,
},
listView: {
backgroundColor: searchFlag? Colors.dark.background : "transparent",
color: Colors.dark.text,
borderRadius: 10,
height: "100%",
padding: 7,
paddingTop: 0,
},
row: {
backgroundColor: Colors.dark.background,
color: Colors.dark.text,
width:'100%'
},
poweredContainer: {
backgroundColor: Colors.dark.background,
bottom: 0,
borderColor: Colors.dark.backgroundModal,
borderRadius: 10,
},
separator: {
backgroundColor: Colors.dark.backgroundModal,
},
description: {
color: Colors.dark.text,
},
powered: {
color: Colors.dark.text,
},
}}
onPress={(data, details) => {
//@ts-ignore
mapRef.current?.animateToRegion({
// @ts-ignore
latitude: details?.geometry.location.lat +0.001,
longitude: details?.geometry.location.lng,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
});
setShowStats(false);
setShowLocalStats(true);
sheetRef.current?.snapToIndex(2);
//Will then get stat data for location
getStats(details?.geometry.location.lat, details?.geometry.location.lng, 2).then((res) => {
setSearchStats([res.prediction, res.crime])
}).catch((e) => {
console.log("ERROR GETTING SEARCH STATS: ",e);
})
setSearchFlag(false);
}}
query={{
language: "en",
location: "35.9606,-83.9207", // Knoxville, TN coordinates
radius: 30000, // 10 km radius around Knoxville
components: "country:us",
key: "AIzaSyC9isrmwdV-eh0uwgSZQVs5sVoLx4WP48Y",
}}
/>
</View>
{/* <TextInput
style={styles.mapSearch}
placeholder="Search address or specific crime"
placeholderTextColor={Colors.dark.text}
autoComplete="street-address"
enterKeyHint="search"
onPress={() => {
//Closes bottom sheet modal on search press
sheetRef.current?.snapToIndex(0);
setMarkerModalFocus(false);
}}
/> */}
{/* Quick filter shortcuts */}
<ScrollView
horizontal={true}
style={styles.mapQuickFilter}
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.mapQuickFilterContent}
>
{/* Quick cancel button */}
<TouchableOpacity
style={styles.mapQuickFilterCancelButton}
onPress={() => {
clearFilters();
}}
>
<MaterialIcons
name="cancel"
size={35}
color={Colors.dark.redPastel}
/>
</TouchableOpacity>
{/* Map type button */}
<TouchableOpacity
style={styles.mapQuickFilterCancelButton}
onPress={() => {
setMapLayer(mapLayer == "standard" ? "hybrid" : "standard");
}}
>
<Ionicons name="layers" size={30} color={Colors.dark.redPastel} />
</TouchableOpacity>
{/* CIP quick button */}
<TouchableOpacity
style={[styles.mapQuickFilterButton, quickSelect(quickCip)]}
onPress={() => {
// Clears quick filter now on repress
if (quickCip) {
clearFilters();
} else {
//Closes bottom sheet
sheetRef.current?.collapse();
setCipFlag(true);
setKpdFiltered(true);
setUtpdFiltered(false);
setKcsdFiltered(false);
setSorFiltered(false);
setClass1Filtered(true);
setClass2Filtered(true);
setClass3Filtered(true);
setClass4Filtered(true);
setClass5Filtered(true);
setFilterDate("0.24");
setQuickSor(false);
setQuickHighRisk(false);
setQuickMedRisk(false);
setQuickLowRisk(false);
setApplyFilter(!applyFilter);
}
setQuickCip(!quickCip);
}}
>
<MaterialIcons
name="local-police"
size={30}
color={Colors.dark.cyan}
/>
<Text style={styles.mapQuickFilterText}>KPD CIP</Text>
</TouchableOpacity>
{/* Quick SOR button */}
<TouchableOpacity
style={[styles.mapQuickFilterButton, quickSelect(quickSor)]}
onPress={() => {
if (quickSor) {
clearFilters();
} else {
//Closes bottom sheet
sheetRef.current?.collapse();
setKcsdFiltered(false);
setKpdFiltered(false);
setUtpdFiltered(false);
setSorFiltered(true);
setQuickCip(false);
setQuickHighRisk(false);
setQuickMedRisk(false);
setQuickLowRisk(false);
setApplyFilter(!applyFilter);
}
setQuickSor(!quickSor);
}}
>
<MaterialCommunityIcons
name="map-marker-check"
size={30}
color={Colors.dark.cyan}
/>
<Text style={styles.mapQuickFilterText}>SOR</Text>
</TouchableOpacity>
{/* High Risk button */}
<TouchableOpacity
style={[styles.mapQuickFilterButton, quickSelect(quickHighRisk)]}
onPress={() => {
if (quickHighRisk) {
clearFilters();
} else {
//Closes bottom sheet
sheetRef.current?.collapse();
setKpdFiltered(true);
setKcsdFiltered(true);
setUtpdFiltered(true);
setClass1Filtered(false);
setClass2Filtered(false);
setClass3Filtered(false);
setClass4Filtered(true);
setClass5Filtered(true);
setFilterDate("7");
setSorFiltered(false);
setQuickCip(false);
setQuickLowRisk(false);
setQuickMedRisk(false);
setQuickSor(false);
setApplyFilter(!applyFilter);
}
setQuickHighRisk(!quickHighRisk);
}}
>
<Ionicons name="warning" size={23} color={Colors.dark.cyan} />
<Text style={styles.mapQuickFilterText}>High Risk</Text>
</TouchableOpacity>
{/* Quick med risk button */}
<TouchableOpacity
style={[styles.mapQuickFilterButton, quickSelect(quickMedRisk)]}
onPress={() => {
if (quickMedRisk) {
clearFilters();
} else {
//Closes bottom sheet
sheetRef.current?.collapse();
setKpdFiltered(true);
setUtpdFiltered(true);
setKcsdFiltered(true);
setSorFiltered(false);
setClass1Filtered(false);
setClass2Filtered(true);
setClass3Filtered(true);
setClass4Filtered(false);
setClass5Filtered(false);
setFilterDate("4");
setQuickCip(false);
setQuickHighRisk(false);
setQuickLowRisk(false);
setQuickSor(false);
setApplyFilter(!applyFilter);
}
setQuickMedRisk(!quickMedRisk);
}}
>
<FontAwesome6
name="circle-exclamation"
size={23}
color={Colors.dark.cyan}
/>
<Text style={styles.mapQuickFilterText}>Med Risk</Text>
</TouchableOpacity>
{/* Quick Low risk button */}
<TouchableOpacity
style={[styles.mapQuickFilterButton, quickSelect(quickLowRisk)]}
onPress={() => {
if (quickLowRisk) {
clearFilters();
} else {
//Closes bottom sheet
sheetRef.current?.collapse();
setKpdFiltered(true);
setUtpdFiltered(true);
setKcsdFiltered(true);
setSorFiltered(false);
setClass1Filtered(true);
setClass2Filtered(false);
setClass3Filtered(false);
setClass4Filtered(false);
setClass5Filtered(false);
setFilterDate("2");
setQuickCip(false);
setQuickSor(false);
setQuickHighRisk(false);
setQuickMedRisk(false);
setApplyFilter(!applyFilter);
}
setQuickLowRisk(!quickLowRisk);
}}
>
<Ionicons name="checkbox" size={23} color={Colors.dark.cyan} />
<Text style={styles.mapQuickFilterText}>Low Risk</Text>
</TouchableOpacity>
{/* Refresh button */}
<TouchableOpacity
style={styles.mapQuickFilterRefresh}
onPress={() => {
refreshMarkers()
.then(() => {
setApplyFilter(!applyFilter);
})
.catch((e) => {
console.log("ERROR refreshing markers:", e);
});
}}
>
<MaterialIcons
name="refresh"
size={35}
color={Colors.dark.redText}
/>
</TouchableOpacity>
</ScrollView>
{/* Filter button */}
<Pressable
style={[styles.filterButton,{zIndex: 100000}]}
onPressIn={() => {
setFilterColor(Colors.dark.redPastelClicked);
}}
onPressOut={() => {
setFilterColor(Colors.dark.redPastel);
}}
onPress={() => {
setFilterModalVisible(true);
setMarkerModalFocus(false);
sheetRef.current?.snapToIndex(1);
}}
>
<FontAwesome6
name="filter"
size={30}
color={filterColor}
style={styles.filterButtonIcon}
/>
</Pressable>
{/* Help button */}
<Pressable
style={styles.mapButtonHelp}
onPressIn={() => {
setHelpColor(Colors.dark.redPastelClicked);
}}
onPressOut={() => {
setHelpColor(Colors.dark.redPastel);
}}
onPress={() => {
//Open help modal
router.push({
pathname: "./help",
});
}}
>
<MaterialCommunityIcons name="help" size={30} color={helpColor} />
</Pressable>
{/* Statistics button */}
<Pressable
style={styles.mapStatButton}
onPress={() => {
// Will populate bottom sheet with statistics
sheetRef.current?.snapToIndex(3);
setShowLocalStats(false);
setShowStats(true);
}}
onPressIn={() => {
setStatColor(Colors.dark.redPastelClicked);
}}
onPressOut={() => {
setStatColor(Colors.dark.redPastel);
}}
>
<MaterialIcons name="auto-graph" size={30} color={statColor} />
</Pressable>
{/* Zoom out button */}
<Pressable
style={styles.mapZoomoutButton}
onPress={() => {
const latDel = 0.2;
const longDel = 0.2;
if (
Region.latitudeDelta > latDel &&
Region.longitudeDelta > longDel
) {
return;
}
// @ts-ignore
mapRef.current?.animateToRegion({
latitude: Region.latitude,
longitude: Region.longitude,
latitudeDelta:
latDel > Region.latitudeDelta ? latDel : Region.latitudeDelta,
longitudeDelta:
longDel > Region.longitudeDelta
? longDel
: Region.longitudeDelta,
});
}}
>
<MaterialCommunityIcons
name="arrow-expand-all"
size={30}
color={Colors.dark.redPastel}
/>
</Pressable>
{/* Filter modal */}
<Modal
onClose={() => {
setFilterModalVisible(false);
}}
isOpen={filterModalVisible}
isKeyboardDismissable={true}
accessibilityLabel="test"
>
<ModalBackdrop />
<ModalContent style={styles.filterModal}>
<VStack style={{ flex: 1 }}>
<Heading size="md" style={{ color: Colors.dark.text }}>
Filter
</Heading>
<HStack style={styles.filterRow}>
<Heading size="sm" style={styles.filterModalHeading}>
Source:{" "}
</Heading>
<Box style={styles.filterCheckboxBox}>
<TouchableOpacity
activeOpacity={1}
style={styles.checkboxContainer}
onPress={() => {
setKpdFiltered(!kpdFiltered);
}}
>
<Checkbox
value={kpdFiltered}
onValueChange={setKpdFiltered}
style={styles.checkbox}
/>
<Text>KPD CIP</Text>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={1}
style={styles.checkboxContainer}
onPress={() => {
setUtpdFiltered(!utpdFiltered);
}}
>
<Checkbox
value={utpdFiltered}
onValueChange={setUtpdFiltered}
style={styles.checkbox}
/>
<Text>UTPD</Text>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={1}
style={styles.checkboxContainer}
onPress={() => {
setKcsdFiltered(!kcsdFiltered);
}}
>
<Checkbox
value={kcsdFiltered}
onValueChange={setKcsdFiltered}
style={styles.checkbox}
/>
<Text>KCSO</Text>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={1}
style={styles.checkboxContainer}
onPress={() => {
setSorFiltered(!sorFiltered);
}}
>
<Checkbox
value={sorFiltered}
onValueChange={setSorFiltered}
style={styles.checkbox}
/>
<Text>SOR</Text>
</TouchableOpacity>
</Box>
</HStack>
<HStack style={styles.filterRow}>
<Heading size="sm" style={styles.filterModalHeading}>
Threat:{" "}
</Heading>
<Box style={styles.filterCheckboxBox}>
<TouchableOpacity
activeOpacity={1}
style={styles.checkboxContainer}
onPress={() => {
setClass1Filtered(!class1Filtered);
}}
>
<Checkbox
value={class1Filtered}
onValueChange={setClass1Filtered}
style={styles.checkbox}
/>
<Text>Class 1</Text>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={1}
style={styles.checkboxContainer}
onPress={() => {
setClass2Filtered(!class2Filtered);
}}
>
<Checkbox
value={class2Filtered}
onValueChange={setClass2Filtered}
style={styles.checkbox}
/>
<Text>Class 2</Text>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={1}
style={styles.checkboxContainer}
onPress={() => {
setClass3Filtered(!class3Filtered);
}}
>
<Checkbox
value={class3Filtered}
onValueChange={setClass3Filtered}
style={styles.checkbox}
/>
<Text>Class 3</Text>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={1}
style={styles.checkboxContainer}
onPress={() => {
setClass4Filtered(!class4Filtered);
}}
>
<Checkbox
value={class4Filtered}
onValueChange={setClass4Filtered}
style={styles.checkbox}
/>
<Text>Class 4</Text>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={1}
style={styles.checkboxContainer}
onPress={() => {
setClass5Filtered(!class5Filtered);
}}
>
<Checkbox
value={class5Filtered}
onValueChange={setClass5Filtered}
style={styles.checkbox}
/>
<Text>Class 5</Text>
</TouchableOpacity>
</Box>
</HStack>
<HStack style={styles.filterRow}>
<Heading size="sm" style={styles.filterModalHeading}>
Date:{" "}
</Heading>
<Select
style={styles.filterDate}
selectedValue={"Last " + filterDate + " days"}
onValueChange={(val) => {
setFilterDate(val);
}}
>
<SelectTrigger
variant="underlined"
size="md"
borderColor={Colors.dark.redPastel}
sx={{ _input: { color: Colors.dark.text } }}
>
<SelectInput placeholder="Filter by date" />
<SelectIcon>
<Icon as={ChevronDownIcon} />
</SelectIcon>
</SelectTrigger>
<SelectPortal>
<SelectBackdrop />
<SelectContent backgroundColor={Colors.dark.background}>
<SelectDragIndicatorWrapper>
<SelectDragIndicator />
</SelectDragIndicatorWrapper>
<SelectItem
label="Last day"
value="1"
sx={{ _text: { color: Colors.dark.text } }}
/>
<SelectItem
label="Last 2 days"
value="2"
sx={{ _text: { color: Colors.dark.text } }}
/>
<SelectItem
label="Last 4 days"
value="4"
sx={{ _text: { color: Colors.dark.text } }}
/>
<SelectItem
label="Last 7 days"
value="7"
sx={{ _text: { color: Colors.dark.text } }}
/>
<SelectItem
label="Last 14 days"
value="14"
sx={{ _text: { color: Colors.dark.text } }}
/>
<SelectItem
label="Last 30 days"
value="30"
sx={{ _text: { color: Colors.dark.text } }}
/>
</SelectContent>
</SelectPortal>
</Select>
</HStack>
</VStack>
<HStack style={styles.filterButtonRow}>
<Button
variant="solid"
style={[
styles.filterModalButton,
{ backgroundColor: applyFilterColor },
]}
onPressIn={() => {
setApplyFilterColor(Colors.dark.redPastelClicked);
}}
onPressOut={() => {
setApplyFilterColor(Colors.dark.redPastel);
}}
onPress={() => {
console.log("Clearing filter");
clearFilters();
setFilterModalVisible(false);
}}
>
<Text style={styles.filterApplyButtonText}>
Clear filters
</Text>
</Button>
<Button
style={[
styles.filterModalButton,
{ backgroundColor: applyFilterColor },
]}
variant="solid"
onPressIn={() => {
setApplyFilterColor(Colors.dark.redPastelClicked);
}}
onPressOut={() => {
setApplyFilterColor(Colors.dark.redPastel);
}}
onPress={() => {
console.log("Applying filter");
setFilterModalVisible(false);
setApplyFilter(!applyFilter);
}}
>
<Text style={styles.filterApplyButtonText}>Apply</Text>
</Button>
</HStack>
</ModalContent>
</Modal>
{/* For individual marker view */}
{markerModalFocus && (
<Animated.FlatList
key="markerModal"
entering={FadeIn.duration(200)}
exiting={FadeOut.duration(200)}
style={styles.markerModalList}
ref={modal}
decelerationRate={"fast"}
disableIntervalMomentum={true}
pagingEnabled={true}
maxToRenderPerBatch={3}
initialNumToRender={3}
showsHorizontalScrollIndicator={false}
horizontal={true}
snapToAlignment="center"
overScrollMode="never"
getItemLayout={(data, index) => ({
length: width * 0.9,
offset: (width * 0.9 + 16) * index - 16,
index,
})}
viewabilityConfig={{ itemVisiblePercentThreshold: 50 }}
data={modalMarker}
contentContainerStyle={styles.markerModalListContent}
renderItem={(item) => (
//@ts-ignore
<MarkerModalView
keyId={
item.item.properties
? item.item.properties.identifier
: item.item
}
maxIndex={modalMarker.length}
markerCache={markerCache}
sor={sor}
/>
)}
onViewableItemsChanged={(viewableItems) => {
// Returns if there are no viewable items
if (!(viewableItems.viewableItems.length > 0)) {
return;
}
if (typeof viewableItems.viewableItems[0].item == "string") {
return;
}
// @ts-ignore
mapRef.current?.animateToRegion({
// @ts-ignore
latitude:
viewableItems.viewableItems[0].item.geometry
.coordinates[1] + 0.0004,
// @ts-ignore
longitude:
viewableItems.viewableItems[0].item.geometry.coordinates[0],
latitudeDelta: 0.001,
longitudeDelta: 0.001,
});
// Resets cluster flag
if (clusterFlag) {
setClusterFlag(false);
}
}}
/>
)}
{mapFlag && locStat && userLocation ? (
<MapView
style={[styles.map]}
showsUserLocation={true}
userLocationPriority="passive"
showsMyLocationButton={false}
// @ts-ignore
mapType={mapLayer}
showsCompass={false}
toolbarEnabled={false}
moveOnMarkerPress={false}
clusterColor={Colors.dark.cyan}
customMapStyle={mapstyle}
minPoints={2}
ref={mapRef}
initialRegion={
locStat.granted
? {
latitude: userLocation.coords.latitude,
longitude: userLocation.coords.longitude,
latitudeDelta: 0.09,
longitudeDelta: 0.09,
}
: initReg
}
// mapPadding={mapPadding}
mapPaadding={{ top: 275, right: 0, bottom: 0, left: 0 }}
// mapPadding={!markerModalFocus ? {top:150,right:0, bottom:340, left:0} :{ top: 275, right: 0, bottom: 0, left: 0 }}
// mapPadding={{ top: 275, right: 0, bottom: 0, left: 0 }}
// onRegionChange={(region)=>setRegion(region)}
onRegionChangeComplete={(region) => {
setRegion(region);
renderMarkers();
}}
onClusterPress={(cluster, markers) => {
setSearchFlag(false);
searchRef.current?.clear();
searchRef.current?.setAddressText("");
//If cluster is too large, the modal will not be displayed
if (cluster.properties.point_count > 20) {
return;
}
if (!markers) {
return;
}
if (!cluster.geometry.coordinates) {
return;
}
//Initializes bounds for cluster
let lat = cluster.geometry.coordinates[1];
let long = cluster.geometry.coordinates[0];
let left, right, top, bottom;
left = lat;
right = lat;
top = long;
bottom = long;
//Finds the bounds of the cluster
for (let i = 0; i < markers.length; i++) {
if (markers[i].geometry.coordinates[1] < left) {
left = markers[i].geometry.coordinates[1];
}
if (markers[i].geometry.coordinates[1] > right) {
right = markers[i].geometry.coordinates[1];
}
if (markers[i].geometry.coordinates[0] > top) {
top = markers[i].geometry.coordinates[0];
}
if (markers[i].geometry.coordinates[0] < bottom) {
bottom = markers[i].geometry.coordinates[0];
}
}
//Sets delta value for zoom
const min = 0.005;
let del = Math.abs(lat - left);
if (del < Math.abs(lat - right)) {
del = Math.abs(lat - right);
}
if (del < Math.abs(long - top)) {
del = Math.abs(long - top);
}
if (del < Math.abs(long - bottom)) {
del = Math.abs(long - bottom);
}
del = del * 6;
if (del < min) {
del = min;
}
setMapPadding({ top: 150, right: 0, bottom: 340, left: 0 });
setAtrClustCoords({
lat: cluster.geometry.coordinates[1] - del / 6,
long: cluster.geometry.coordinates[0],
zoom: del,
});
Keyboard.dismiss();
// Sets states for modal and cluster view
setShowLocalStats(false);
setMarkerModalFocus(false);
setModalMarker(markers);
setClusterFlag(true);
setShowStats(false);
if (markers && markers.length > 0) {
//Scrolls to first element when cluster is clicked
//@ts-ignore
// modal.current?.scrollToIndex({ index: 0 });
}
clusterListRef.current?.scrollToOffset({
offset: 0,
animated: false,
});
sheetRef.current?.snapToIndex(3);
}}
onPress={() => {
// When modal is clicked out of, hide it and empty it.
setMarkerModalFocus(false);
Keyboard.dismiss();
searchRef.current?.clear();
searchRef.current?.setAddressText("");
setSearchFlag(false);
}}
>
{markers}
</MapView>
) : (
<ActivityIndicator
style={{ position: "absolute", marginTop: height * 0.5 }}
size="large"
color={Colors.dark.redPastel}
/>
)}
{/* Cluster and stats view */}
<BottomSheetModal
snapPoints={snapPoints}
index={0}
ref={sheetRef}
contentHeight={height * 0.12}
backgroundStyle={{ backgroundColor: Colors.dark.tabBg }}
enablePanDownToClose={false}
handleIndicatorStyle={{ backgroundColor: Colors.dark.text }}
onChange={(index) => {
if (index > 1) {
//Sets dynamic height for cluster view
setSheetHeight(
parseInt(snapPoints[index].replace("%", "")) * 0.01
);
//Flag for bottom sheet visibility, for back button close
setBottomVisible(true);
} else {
setSheetHeight(0.5);
setBottomVisible(false);
}
}}
>
{/* For showing clustered SOR and crime */}
{!showStats && !showLocalStats && clusterFlag && (
<View
style={{ height: height, overflow: "scroll" }}
>
<FlatList
style={[styles.clusterContent, { paddingBottom: 30 }]}
data={modalMarker}
ref={clusterListRef}
maxToRenderPerBatch={5}
key="clusterView"
initialNumToRender={3}
overScrollMode="never"
scrollEnabled={true}
contentContainerStyle={[
styles.clusterContentContainer,
{ paddingBottom: height * 1.2 - height * sheetHeight },
]}
renderItem={(item: any) => (
<ClusterView
keyId={
item.item.properties
? item.item.properties.identifier
: item.item
}
maxIndex={modalMarker.length}
markerCache={markerCache}
handlePress={setModFoc}
index={item.index}
sor={sor}
distance={
item.item.geometry
? getDist(
{
latitude: item.item.geometry.coordinates[1],
longitude: item.item.geometry.coordinates[0],
},
(locStat.granted = true
? userLocation.coords
: null)
)
: "? miles"
}
/>
)}
/>
</View>
)}
{/* If out of bounds, shows error */}
{((showStats && !showLocalStats && outOfBounds) || (!showLocalStats && !clusterFlag && outOfBounds)) &&(
<View style={{height: height}}>
<Text style={[styles.clusterStatsTitle, {color:Colors.dark.redBright}]}>
Bad news...
</Text>
<Text style={styles.clusterContent}>
Relative statistics are not available outside of supported locations.
{"\n\n"}If this is a mistake, please check your location settings or contact me.
{"\n\n"}Otherwise, you can request for your city to be added to the list of supported locations, and I can look into adding it when I have time!
</Text>
</View>
)}
{/* Shows relative stats */}
{((showStats && !showLocalStats && !outOfBounds) || (!showLocalStats && !clusterFlag && !outOfBounds)) && (
<View style={{ height: height, overflow: "scroll" }}>
{ stats?
(<FlatList
style={styles.clusterStatsView}
scrollEnabled={true}
key="stats"
overScrollMode="never"
data={stats}
contentContainerStyle={[
{
paddingBottom: height * 1.2 - height * sheetHeight,
rowGap: 15,
},
]}
renderItem={function ({ item, index }) {
if (index == 0) {
const risks = ['Low Risk', 'Medium Risk', 'High Risk']
return (
<View style={{ rowGap: 15, marginTop:-15 }}>
<HStack alignSelf='center' columnGap={10}>
<Badge variant="outline" size='md' borderRadius="$full" action="info" alignSelf='center' backgroundColor={Colors.dark.background}>
<BadgeText sx={{_text:{textAlign:'center'}}}>AI</BadgeText>
</Badge>
<Badge variant="outline" size='md' borderRadius="$full" action="warning" alignSelf='center' backgroundColor={Colors.dark.alertBackground}>
<BadgeText sx={{_text:{textAlign:'center'}}}>Beta Feature</BadgeText>
</Badge>
</HStack>
<Text
style={{ textAlign: "center", fontWeight: "bold" }}
>
AI Predictions for the likelihood of a crime being reported
in the next 4 hours within 2 miles of your location.
</Text>
<View
style={{
rowGap: 10,
columnGap: 10,
borderRadius: 10,
backgroundColor: Colors.dark.tabBg,
flexDirection: "row",
width: width * 0.9,
flex: 1,
flexWrap: "wrap",
justifyContent: "center",
alignSelf: "center",
}}
>
{item.map((stat:any, index:any) => {
stat = stat as number;
return (
<View
key={index}
style={{
flexDirection: "column",
justifyContent: "space-between",
padding: 10,
backgroundColor: stat>80?Colors.dark.alertBackground:Colors.dark.background,
borderRadius: 10,
flexWrap: "wrap",
width: "75%",
alignItems: "center",
alignContent: "center",
}}
>
<Text
adjustsFontSizeToFit={true}
numberOfLines={1}
style={{
color:
stat > 40
? Colors.dark.redBright
: Colors.dark.cyan,
textAlign: "center",
fontSize: 50,
}}
>
{formatDecimal(stat)}%
</Text>
<Text
style={{
textAlign: "center",
fontWeight: "bold",
color: Colors.dark.text,
}}
>
{risks[index]}
</Text>
{index==2 && <Text
style={{textAlign:'center'}}
>
{"\n"}** Note that due to the unpredictable nature of high risk crimes, this prediction is not super accurate.
</Text>}
</View>
);
})}
</View>
<Text
style={{textAlign:'center', marginTop: 10}}
>
<Text style={{ fontWeight: "bold", color:Colors.dark.redText }}>Disclaimer:</Text>{" "}
None of these predictions are guarunteed and should always be taken with a grain of salt.
{"\n\n"} Find more information on them in the help section.
</Text>
</View>
);
} else if (index == 1) {
//Counts
return (
<View style={{ marginTop: 20 }}>
<Text style={styles.clusterStatsTitle}>
Crime Counts
</Text>
<View
style={[globalStyles.separator, { marginTop: 5 }]}
></View>
<Text
style={{
textAlign: "center",
fontWeight: "bold",
marginBottom: 15,
}}
>
The total number of crime reports in the last 2 days within
2 miles of your location.
</Text>
<View
style={{
rowGap: 10,
columnGap: 10,
borderRadius: 10,
backgroundColor: Colors.dark.tabBg,
flexDirection: "row",
width: width * 0.9,
flex: 1,
flexWrap: "wrap",
justifyContent: "center",
}}
>
{item.map((stat:any, index:any) => {
return (
<View
key={index}
style={{
flexDirection: "column",
justifyContent: "space-between",
padding: 10,
backgroundColor: Colors.dark.background,
borderRadius: 10,
flexWrap: "wrap",
width: "45%",
alignItems: "center",
alignContent: "center",
}}
>
<Text
style={{
color: Colors.dark.cyan,
textAlign: "center",
fontSize: 50,
}}
adjustsFontSizeToFit={true}
numberOfLines={1}
>
{formatWithCommas(stat)}
</Text>
<Text
style={{
textAlign: "center",
fontWeight: "bold",
color: Colors.dark.text,
}}
>
Class {index + 1}
</Text>
</View>
);
})}
</View>
</View>
);
} else if (index == 2) {
let differences = [];
let c = item[0] as [number];
let p = item[1] as [number];
for (let i = 0; i < 6; i++) {
if (p[i] == 0) {
differences.push(c[i]);
} else {
differences.push(
Math.round((c[i] / p[i]) * 100) - 100
);
}
}
//Counts
return (
<View style={{ marginTop: 20 }}>
<Text style={styles.clusterStatsTitle}>
Monthly Crime Counts
</Text>
<View
style={[globalStyles.separator, { marginTop: 5 }]}
></View>
<Text
style={{
textAlign: "center",
fontWeight: "bold",
marginBottom: 15,
}}
>
Note that the monthly comparison is based upon last
months total reported crime count and not the counts from
this time last month.
</Text>
<View
style={{
rowGap: 10,
columnGap: 10,
borderRadius: 10,
backgroundColor: Colors.dark.tabBg,
flexDirection: "row",
width: width * 0.9,
flex: 1,
flexWrap: "wrap",
justifyContent: "center",
}}
>
{(item[0] as [number]).map((stat, index) => {
return (
<View
key={index}
style={{
flexDirection: "column",
justifyContent: "space-between",
padding: 10,
backgroundColor:
differences[index] > 0
? Colors.dark.alertBackground
: Colors.dark.background,
borderRadius: 10,
flexWrap: "wrap",
width: "45%",
alignItems: "center",
alignContent: "center",
}}
>
<Text
style={{
color: Colors.dark.cyan,
textAlign: "center",
fontSize: 50,
}}
adjustsFontSizeToFit={true}
numberOfLines={1}
>
{formatWithCommas(stat)}
</Text>
<Text
style={{
textAlign: "center",
fontWeight: "bold",
color: Colors.dark.text,
}}
>
Class {index + 1}
</Text>
<Text>
<Text style={{ fontWeight: "bold" }}>
Last Month:
</Text>{" "}
<Text
style={{
color:
differences[index] > 0
? Colors.dark.redBright
: Colors.dark.cyan,
}}
>
{/* {(item[0] as [number])[index]} */}
{differences[index] > 0 ? "+" : ""}
{differences[index]}%
</Text>
</Text>
</View>
);
})}
</View>
</View>
);
} else {
return (
<View>
<Text></Text>
</View>
);
}
}}
ListHeaderComponent={
<View>
<Text style={styles.clusterStatsTitle}>
The{" "}
<Text style={{ color: Colors.dark.cyan }}>Numbers</Text>
</Text>
<View
style={[globalStyles.separator, { marginTop: 5 }]}
></View>
</View>
}
></FlatList>):
<ActivityIndicator size={'large'} style={{justifyContent:'center', height:height*sheetHeight}}/>
}
</View>
)}
{/* Shows stats for searched location */}
{showLocalStats && (
<View style={{ height: height, overflow: "scroll" }}>
{ searchStats ?(
<FlatList
key="localStats"
style={styles.clusterStatsView}
scrollEnabled={true}
overScrollMode="never"
data={searchStats}
contentContainerStyle={[
{
paddingBottom: height * 1.2 - height * sheetHeight,
rowGap: 15,
},
]}
ListHeaderComponent={
<View>
<Text style={styles.clusterStatsTitle}>
Local{" "}
<Text style={{ color: Colors.dark.cyan }}>Numbers</Text> for {"\n"}
</Text>
<Text adjustsFontSizeToFit numberOfLines={2} style={[styles.clusterStatsTitle,{ color: Colors.dark.text, fontSize:17 }]}> {searchRef.current?.getAddressText()} </Text>
<View
style={[globalStyles.separator, { marginTop: 5 }]}
></View>
</View>
}
renderItem={function ({ item, index }) {
if (index == 0) {
const risks = ["Low Risk", "Medium Risk", "High Risk"];
return (
<View style={{ rowGap: 15, marginTop:-15 }}>
<HStack alignSelf='center' columnGap={10}>
<Badge variant="outline" size='md' borderRadius="$full" action="info" alignSelf='center' backgroundColor={Colors.dark.background}>
<BadgeText sx={{_text:{textAlign:'center'}}}>AI</BadgeText>
</Badge>
<Badge variant="outline" size='md' borderRadius="$full" action="warning" alignSelf='center' backgroundColor={Colors.dark.alertBackground}>
<BadgeText sx={{_text:{textAlign:'center'}}}>Beta Feature</BadgeText>
</Badge>
</HStack>
<Text
style={{ textAlign: "center", fontWeight: "bold" }}
>
AI Predictions for the likelihood of a crime being
reported in the next 4 hours within 2 miles of your
location.
</Text>
<View
style={{
rowGap: 10,
columnGap: 10,
borderRadius: 10,
backgroundColor: Colors.dark.tabBg,
flexDirection: "row",
width: width * 0.9,
flex: 1,
flexWrap: "wrap",
justifyContent: "center",
alignSelf: "center",
}}
>
{item.map((stat: any, index: any) => {
stat = stat as number;
return (
<View
key={index}
style={{
flexDirection: "column",
justifyContent: "space-between",
padding: 10,
backgroundColor:
stat > 80
? Colors.dark.alertBackground
: Colors.dark.background,
borderRadius: 10,
flexWrap: "wrap",
width: "75%",
alignItems: "center",
alignContent: "center",
}}
>
<Text
adjustsFontSizeToFit={true}
numberOfLines={1}
style={{
color:
stat > 40
? Colors.dark.redBright
: Colors.dark.cyan,
textAlign: "center",
fontSize: 50,
}}
>
{formatDecimal(stat)}%
</Text>
<Text
style={{
textAlign: "center",
fontWeight: "bold",
color: Colors.dark.text,
}}
>
{risks[index]}
</Text>
{index == 2 && (
<Text style={{ textAlign: "center" }}>
{"\n"}** Note that due to the
unpredictable nature of high risk crimes,
this prediction is not super accurate.
</Text>
)}
</View>
);
})}
</View>
<Text style={{ textAlign: "center", marginTop: 10 }}>
<Text
style={{
fontWeight: "bold",
color: Colors.dark.redText,
}}
>
Disclaimer:
</Text>{" "}
None of these predictions are guarunteed and should
always be taken with a grain of salt.
{"\n\n"} Find more information on them in the help
section.
</Text>
</View>
);
} else {
//Counts
return (
<View style={{ marginTop: 20 }}>
<Text style={styles.clusterStatsTitle}>
Crime Counts
</Text>
<View
style={[globalStyles.separator, { marginTop: 5 }]}
></View>
<Text
style={{
textAlign: "center",
fontWeight: "bold",
marginBottom: 15,
}}
>
The total number of crime reports in the last 2 days within
2 miles of this location.
</Text>
<View
style={{
rowGap: 10,
columnGap: 10,
borderRadius: 10,
backgroundColor: Colors.dark.tabBg,
flexDirection: "row",
width: width * 0.9,
flex: 1,
flexWrap: "wrap",
justifyContent: "center",
}}
>
{item.map((stat:any, index:any) => {
return (
<View
key={index}
style={{
flexDirection: "column",
justifyContent: "space-between",
padding: 10,
backgroundColor: Colors.dark.background,
borderRadius: 10,
flexWrap: "wrap",
width: "45%",
alignItems: "center",
alignContent: "center",
}}
>
<Text
style={{
color: Colors.dark.cyan,
textAlign: "center",
fontSize: 50,
}}
adjustsFontSizeToFit={true}
numberOfLines={1}
>
{formatWithCommas(stat)}
</Text>
<Text
style={{
textAlign: "center",
fontWeight: "bold",
color: Colors.dark.text,
}}
>
Class {index + 1}
</Text>
</View>
);
})}
</View>
</View>
);
}
}}
/>):<ActivityIndicator size={'large'} style={{justifyContent:'center', height:height*sheetHeight}}/>}
</View>
)}
</BottomSheetModal>
</View>
</BottomSheetModalProvider>
</GestureHandlerRootView>
);
}
// Dynamic Styles function for selected quick sort buttons
function quickSelect(flag:Boolean){
if(!flag){
return{
borderColor:Colors.dark.background,
}
}else{
return {
borderColor: Colors.dark.cyan,
};
}
}
let height = Dimensions.get("window").height;
let width = Dimensions.get("window").width;
const styles = StyleSheet.create({
container: {
height: height,
width: width,
},
title: {
fontSize: 20,
fontWeight: "bold",
},
separator: {
marginVertical: 30,
height: 1,
width: "80%",
},
map: {
flex:1,
width: width,
height: height,
},
filterButtons: {
height: "65%",
width: 0.35 * width,
maxWidth: 0.35 * width,
backgroundColor: Colors.dark.background,
color: Colors.dark.text,
top: "1%",
justifyContent: "center",
alignItems: "center",
borderRadius: 10,
marginHorizontal: 5,
},
filterContainer: {
// backgroundColor:Colors.dark.red,
height: 0.1 * height,
position: "absolute",
top: 0,
left: 0.12 * width,
right: 0.14 * width,
zIndex: 10,
},
mapButton: {
position: "absolute",
zIndex: 10,
borderRadius: 10,
padding: 4,
backgroundColor: Colors.dark.background,
bottom: height * 0.13,
alignItems: "center",
right: 10,
},
mapSearch: {
position: "absolute",
zIndex: 10,
borderRadius: 10,
// padding: 10,
backgroundColor: Colors.dark.background,
marginTop: StatusBar.currentHeight
? StatusBar.currentHeight + height * 0.01
: "10%",
color: Colors.dark.text,
width: "78%",
height: height * 0.07,
// textDecorationLine: "none",
alignSelf: "flex-start",
marginLeft: "5%",
},
mapContainer: {
position: "absolute",
width: "100%",
height: "100%",
zIndex: 10,
flex: 1,
},
filterButton: {
position: "absolute",
marginTop: StatusBar.currentHeight
? StatusBar.currentHeight + height * 0.01
: "10%",
backgroundColor: Colors.dark.background,
borderRadius: 10,
zIndex: 8,
alignSelf: "flex-end",
right: width / 32,
height: height * 0.07,
width: width * 0.12,
justifyContent: "center",
},
markerModalContainer: {
position: "absolute",
zIndex: 10,
height: "35%",
width: "90%",
borderRadius: 15,
bottom: height * 0.1,
backgroundColor: Colors.dark.backgroundModal,
},
markerModal: {
backgroundColor: Colors.dark.backgroundModal,
flex: 1,
padding: 20,
},
modalContent: {
width: width * 0.9,
height: height * 0.35,
overflow: "hidden",
},
modalText: {
width: "90%",
},
mapButtonHelp: {
position: "absolute",
zIndex: 10,
borderRadius: 10,
padding: 4,
backgroundColor: Colors.dark.background,
bottom: height * 0.25,
alignItems: "center",
right: 10,
},
mapStatButton: {
position: "absolute",
zIndex: 10,
borderRadius: 10,
padding: 4,
backgroundColor: Colors.dark.background,
bottom: height * 0.19,
alignItems: "center",
right: 10,
},
mapZoomoutButton: {
position: "absolute",
zIndex: 10,
borderRadius: 10,
padding: 4,
backgroundColor: Colors.dark.background,
bottom: height * 0.31,
alignItems: "center",
right: 10,
},
filterModal: {
position: "absolute",
zIndex: 200,
height: "35%",
width: "90%",
borderRadius: 5,
top: StatusBar.currentHeight
? StatusBar.currentHeight + height * 0.09
: height * 0.14,
alignSelf: "center",
backgroundColor: Colors.dark.background,
padding: 20,
},
transparent: {
backgroundColor: "rgba(0,0,0,0)",
},
filterButtonIcon: {
width: "100%",
marginLeft: "18%",
marginRight: "auto",
},
checkboxContainer: {
flexDirection: "row",
marginRight: 5,
marginBottom: 5,
alignItems: "center",
},
checkbox: {
marginRight: 5,
},
filterModalButton: {
width: "30%",
},
filterApplyButtonText: {
color: Colors.dark.redBackgroundText,
},
filterModalHeading: {
color: Colors.dark.text,
width: "20%",
},
filterRow: {
marginBottom: 10,
},
filterCheckboxBox: {
width: "80%",
flexDirection: "row",
flexWrap: "wrap",
},
filterButtonRow: {
flexDirection: "row",
justifyContent: "space-between",
marginTop: 10,
},
filterDate: {
width: "80%",
},
filterSelectTriggerText: {
color: Colors.dark.text,
},
mapQuickFilter: {
flexDirection: "row",
position: "absolute",
zIndex: 10,
top: height * 0.12,
width: width,
height: height * 0.08,
},
mapQuickFilterContent: {
flexGrow: 1,
justifyContent: "center",
alignContent: "center",
columnGap: 8,
overflow: "visible",
paddingHorizontal: 5,
},
mapQuickFilterButton: {
flexDirection: "row",
width: width * 0.3,
height: height * 0.05,
backgroundColor: Colors.dark.background,
alignItems: "center",
paddingHorizontal: 5,
borderRadius: 15,
borderWidth: 2,
borderColor: Colors.dark.background,
},
mapQuickFilterCancelButton: {
width: height * 0.05,
height: height * 0.05,
backgroundColor: Colors.dark.background,
alignItems: "center",
justifyContent: "center",
borderRadius: 40,
},
mapQuickFilterText: {
color: Colors.dark.text,
fontWeight: "bold",
marginLeft: 5,
},
mapQuickFilterRefresh: {
width: height * 0.05,
height: height * 0.05,
backgroundColor: Colors.dark.background,
alignItems: "center",
justifyContent: "center",
borderRadius: 40,
},
markerIcon: {
fontSize: 20,
padding: 2,
fontWeight: "bold",
justifyContent: "flex-start",
textAlign: "center",
paddingBottom: 10,
borderTopRightRadius: 20,
borderTopLeftRadius: 20,
borderBottomLeftRadius: 100,
borderBottomRightRadius: 100,
borderColor: Colors.dark.background,
borderWidth: 2,
paddingHorizontal: 5,
},
markerModalList: {
position: "absolute",
zIndex: 50,
width: width,
top: height * 0.18,
},
markerModalListContent: {
flexGrow: 1,
justifyContent: "center",
alignContent: "center",
overflow: "visible",
paddingBottom: 10,
},
clusterView: {
position: "absolute",
height: height * 0.7,
zIndex: 500,
},
clusterContent: {
backgroundColor: Colors.dark.tabBg,
padding: 20,
// flex:1,
},
clusterContentContainer: {
flexDirection:"column",
rowGap:10,
},
clusterStatsView:{
padding:10,
},
clusterStatsTitle:{
fontWeight:"bold",
fontSize: 20,
textAlign: "center",
},
outofboundsalert:{
position:'absolute',
backgroundColor:Colors.dark.background,
padding:10,
borderRadius:10,
width:width*0.9,
alignSelf:'center',
zIndex:1000000,
},
outofboundstext:{
textAlign:'center',
},
outofboundsbutton:{
backgroundColor:Colors.dark.redBright,
width:"60%",
alignSelf:'center',
},
outofboundsbuttontext:{
color:Colors.dark.redBackgroundText,
fontWeight:'bold',
},
});