diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
index af9e597..7816639 100644
--- a/app/(tabs)/_layout.tsx
+++ b/app/(tabs)/_layout.tsx
@@ -1,46 +1,104 @@
import { Tabs } from "expo-router";
import React from "react";
-
+import { View, Image, StyleSheet } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
import { HapticTab } from "@/components/haptic-tab";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { Colors } from "@/constants/theme";
import { useColorScheme } from "@/hooks/use-color-scheme";
import Feather from "@expo/vector-icons/Feather";
+import lightIcon from "@/assets/logo/logo-light.png";
+import darkIcon from "@/assets/logo/logo-dark.png";
+
+interface TabHeaderProps {
+ readonly backgroundColor: string;
+ readonly logoSource: number;
+}
+
+function TabHeader({ backgroundColor, logoSource }: TabHeaderProps) {
+ const insets = useSafeAreaInsets();
+
+ return (
+
+
+
+ );
+}
+
+function MapTabIcon({ color }: { readonly color: string }) {
+ return ;
+}
+
+function HomeTabIcon({ color }: { readonly color: string }) {
+ return ;
+}
+
+function CalendarTabIcon({ color }: { readonly color: string }) {
+ return ;
+}
export default function TabLayout() {
- const colorScheme = useColorScheme();
+ const colorScheme = useColorScheme() ?? "light";
+ const logos = {
+ light: lightIcon,
+ dark: darkIcon,
+ };
+
+ const logoSource = logos[colorScheme];
return (
(
+
+ ),
tabBarButton: HapticTab,
+ tabBarActiveTintColor: Colors[colorScheme].tint,
+ tabBarInactiveTintColor: Colors[colorScheme].text,
+ tabBarStyle: {
+ backgroundColor: Colors[colorScheme].background,
+ borderTopWidth: 0,
+ elevation: 5,
+ },
}}
>
,
+ tabBarIcon: MapTabIcon,
}}
/>
(
-
- ),
+ tabBarIcon: HomeTabIcon,
}}
/>
,
+ tabBarIcon: CalendarTabIcon,
}}
/>
);
}
+
+const styles = StyleSheet.create({
+ header: {
+ paddingBottom: 15,
+ justifyContent: "center",
+ alignItems: "center",
+ borderBottomWidth: StyleSheet.hairlineWidth
+ },
+ logo: {
+ width: 120,
+ height: 40,
+ }
+});
diff --git a/app/(tabs)/calendar.tsx b/app/(tabs)/calendar.tsx
index 71518f9..64aa3df 100644
--- a/app/(tabs)/calendar.tsx
+++ b/app/(tabs)/calendar.tsx
@@ -8,6 +8,7 @@ import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Fonts } from '@/constants/theme';
+import React from 'react';
export default function TabTwoScreen() {
return (
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
index 786b736..48917ef 100644
--- a/app/(tabs)/index.tsx
+++ b/app/(tabs)/index.tsx
@@ -6,6 +6,7 @@ import ParallaxScrollView from '@/components/parallax-scroll-view';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { Link } from 'expo-router';
+import React from 'react';
export default function HomeScreen() {
return (
diff --git a/app/(tabs)/map-tab.tsx b/app/(tabs)/map-tab.tsx
index 8b12526..efeddfa 100644
--- a/app/(tabs)/map-tab.tsx
+++ b/app/(tabs)/map-tab.tsx
@@ -1,4 +1,5 @@
import MapViewer from "@/components/map/map-viewer";
+import React from "react";
export default function MapTab() {
return ;
diff --git a/app/_layout.tsx b/app/_layout.tsx
index f518c9b..9dc4e6a 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -4,6 +4,7 @@ import { StatusBar } from 'expo-status-bar';
import 'react-native-reanimated';
import { useColorScheme } from '@/hooks/use-color-scheme';
+import React from 'react';
export const unstable_settings = {
anchor: '(tabs)',
diff --git a/app/modal.tsx b/app/modal.tsx
index 6dfbc1a..548a55a 100644
--- a/app/modal.tsx
+++ b/app/modal.tsx
@@ -3,6 +3,7 @@ import { StyleSheet } from 'react-native';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
+import React from 'react';
export default function ModalScreen() {
return (
diff --git a/assets/logo/logo-dark.png b/assets/logo/logo-dark.png
new file mode 100644
index 0000000..8a7988b
Binary files /dev/null and b/assets/logo/logo-dark.png differ
diff --git a/assets/logo/logo-light.png b/assets/logo/logo-light.png
new file mode 100644
index 0000000..5ac2e47
Binary files /dev/null and b/assets/logo/logo-light.png differ
diff --git a/components/map/building-info-popup.tsx b/components/map/building-info-popup.tsx
new file mode 100644
index 0000000..94ccbc2
--- /dev/null
+++ b/components/map/building-info-popup.tsx
@@ -0,0 +1,341 @@
+import React, { useEffect, useRef, useState } from "react";
+import {
+ Animated,
+ PanResponder,
+ StyleSheet,
+ Text,
+ View,
+ ScrollView,
+ TouchableOpacity,
+ Linking,
+ useColorScheme,
+} from "react-native";
+import { Ionicons } from "@expo/vector-icons";
+import { BuildingInfo } from "@/data/parsedBuildings";
+import { Colors } from "@/constants/theme";
+
+interface Props {
+ readonly building: BuildingInfo | null;
+}
+
+const CLOSE_HEIGHT = 520;
+const COLLAPSED_HEIGHT = 175;
+const OPEN_TRANSLATE_Y = 0;
+const COLLAPSED_TRANSLATE_Y = CLOSE_HEIGHT - COLLAPSED_HEIGHT;
+
+const WEEKDAYS = [
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+ "Sunday",
+];
+
+const DEFAULT_OPENING_HOURS = [
+ "7:00 AM – 11:00 PM",
+ "7:00 AM – 11:00 PM",
+ "7:00 AM – 11:00 PM",
+ "7:00 AM – 11:00 PM",
+ "7:00 AM – 11:00 PM",
+ "7:00 AM – 9:00 PM",
+ "7:00 AM – 9:00 PM",
+];
+
+export default function BuildingInfoPopup({ building }: Props) {
+ const colorScheme = useColorScheme() ?? "light";
+ const theme = Colors[colorScheme];
+ const styles = makeStyles(theme);
+
+ const translateY = useRef(new Animated.Value(COLLAPSED_TRANSLATE_Y)).current;
+ const currentTranslateY = useRef(COLLAPSED_TRANSLATE_Y);
+ const [expanded, setExpanded] = useState(false);
+
+ useEffect(() => {
+ if (building) {
+ Animated.spring(translateY, {
+ toValue: COLLAPSED_TRANSLATE_Y,
+ useNativeDriver: true,
+ }).start(() => {
+ currentTranslateY.current = COLLAPSED_TRANSLATE_Y;
+ });
+ setExpanded(false);
+ } else {
+ Animated.spring(translateY, {
+ toValue: CLOSE_HEIGHT,
+ useNativeDriver: true,
+ }).start();
+ }
+ }, [building, translateY]);
+
+ const panResponder = useRef(
+ PanResponder.create({
+ onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dy) > 5,
+ onPanResponderGrant: () =>
+ translateY.stopAnimation((y) => {
+ currentTranslateY.current = y;
+ }),
+ onPanResponderMove: (_, g) => {
+ const next = Math.max(
+ OPEN_TRANSLATE_Y,
+ Math.min(
+ COLLAPSED_TRANSLATE_Y,
+ currentTranslateY.current + g.dy
+ )
+ );
+ translateY.setValue(next);
+ },
+ onPanResponderRelease: (_, g) => {
+ const midpoint =
+ (OPEN_TRANSLATE_Y + COLLAPSED_TRANSLATE_Y) / 2;
+ const expand =
+ currentTranslateY.current + g.dy < midpoint || g.vy < -0.5;
+ const snapPoint = expand
+ ? OPEN_TRANSLATE_Y
+ : COLLAPSED_TRANSLATE_Y;
+
+ Animated.spring(translateY, {
+ toValue: snapPoint,
+ velocity: g.vy,
+ tension: 80,
+ friction: 14,
+ useNativeDriver: true,
+ }).start(() => {
+ currentTranslateY.current = snapPoint;
+ });
+
+ setExpanded(expand);
+ },
+ })
+ ).current;
+
+ if (!building) return null;
+
+ const todayIdx =
+ new Date().getDay() === 0 ? 6 : new Date().getDay() - 1;
+
+ const ACTIONS = [
+ { label: "Directions", icon: "navigate-outline", type: "directions" as const },
+ { label: "Website", icon: "globe-outline", type: "website" as const },
+ ];
+
+ const handleAction = async (type: "directions" | "website") => {
+ const urls: Record = {
+ directions: `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(building.address)}`,
+ website: building.link || "",
+ };
+
+ const url = urls[type];
+ if (!url) {
+ console.warn(`No URL available for action: ${type}`);
+ return;
+ }
+
+ try {
+ const canOpen = await Linking.canOpenURL(url);
+ if (!canOpen) {
+ console.warn(`Cannot open URL: ${url}`);
+ return;
+ }
+ await Linking.openURL(url);
+ } catch (error) {
+ console.error(`Failed to open URL (${type}):`, error);
+ }
+ };
+
+ const formatCamelCase = (text: string) =>
+ text
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
+ .replace(/\s+/g, " ")
+ .trim()
+ .replace(/^./, (c) => c.toUpperCase());
+
+ return (
+
+
+
+
+ {building.buildingCode} – {building.buildingName}
+
+
+
+ {building.campus} Campus | {building.address}
+
+
+
+ Today: {DEFAULT_OPENING_HOURS[todayIdx]}
+
+
+
+ {ACTIONS.map((a) => (
+ handleAction(a.type)}
+ theme={theme}
+ />
+ ))}
+
+
+ {expanded && (
+
+
+
+ {building.accessibility.length > 0 && (
+ <>
+ Accessibility
+ {building.accessibility.map((item) => (
+
+ ))}
+ >
+ )}
+
+ Opening Hours
+ {DEFAULT_OPENING_HOURS.map((h, i) => (
+
+ {" "}
+ {WEEKDAYS[i]}: {h}
+
+ ))}
+
+ )}
+
+ );
+}
+
+/* ---------- Subcomponents ---------- */
+
+const ListItem = ({
+ text,
+ theme,
+}: {
+ readonly text: string;
+ readonly theme: typeof Colors.light;
+}) => (
+
+
+ {text}
+
+);
+
+const ActionButton = ({
+ label,
+ icon,
+ onPress,
+ theme,
+}: {
+ readonly label: string;
+ readonly icon: string;
+ readonly onPress: () => void;
+ readonly theme: typeof Colors.light;
+}) => (
+
+
+
+ {label}
+
+
+);
+
+const makeStyles = (theme: typeof Colors.light) =>
+ StyleSheet.create({
+ card: {
+ position: "absolute",
+ bottom: 0,
+ left: 0,
+ right: 0,
+ backgroundColor: theme.buildingInfoPopup.background,
+ paddingHorizontal: 20,
+ paddingTop: 10,
+ borderTopLeftRadius: 20,
+ borderTopRightRadius: 20,
+ elevation: 15
+ },
+ handle: {
+ width: 40,
+ height: 5,
+ backgroundColor: theme.buildingInfoPopup.handle,
+ borderRadius: 3,
+ alignSelf: "center",
+ marginBottom: 8
+ },
+ title: {
+ fontSize: 22,
+ fontWeight: "600",
+ color: theme.buildingInfoPopup.title,
+ marginBottom: 4
+ },
+ line: {
+ color: theme.buildingInfoPopup.text,
+ marginTop: 4
+ },
+ openStatus: {
+ color: theme.buildingInfoPopup.openStatus,
+ marginTop: 4,
+ fontWeight: "500"
+ },
+ actionsRow: {
+ flexDirection: "row",
+ marginTop: 12,
+ gap: 10
+ },
+ sectionTitle: {
+ marginTop: 14,
+ fontWeight: "600",
+ color: theme.buildingInfoPopup.title
+ },
+ rule: {
+ borderBottomColor: theme.buildingInfoPopup.divider,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ marginVertical: 12
+ },
+ todayHighlight: {
+ fontWeight: "700",
+ color: theme.buildingInfoPopup.openStatus
+ }
+ });
diff --git a/components/map/map-viewer.tsx b/components/map/map-viewer.tsx
index 216d6d1..8f3873d 100644
--- a/components/map/map-viewer.tsx
+++ b/components/map/map-viewer.tsx
@@ -1,117 +1,260 @@
-import { CAMPUS_LOCATIONS } from "@/constants/mapData";
-import { Coordinate, CoordinateDelta } from "@/types/mapTypes";
+import React, { useRef, useState, useCallback, useMemo } from "react";
+import { StyleSheet, View, Text, Platform } from "react-native";
+import MapViewCluster from "react-native-map-clustering";
+import MapView, { Marker, Polygon, Region } from "react-native-maps";
import * as LocationPermissions from "expo-location";
-import { useRef, useState } from "react";
-import { StyleSheet, View } from "react-native";
-import MapView, { Polygon, Region } from "react-native-maps";
+import { useColorScheme } from "@/hooks/use-color-scheme";
+import { Colors } from "@/constants/theme";
+import { CAMPUS_LOCATIONS } from "@/constants/mapData";
+import { concordiaBuildings, BuildingInfo } from "@/data/parsedBuildings";
+import { Coordinate, CoordinateDelta, Building as MapBuilding } from "@/types/mapTypes";
import LocationButton, { LocationButtonProps } from "./location-button";
import LocationModal from "./location-modal";
+import BuildingInfoPopup from "./building-info-popup";
interface Props {
- userLocationDelta?: CoordinateDelta;
- initialRegion?: Region;
- polygonFillColor?: string;
- polygonStrokeColor?: string;
+ readonly userLocationDelta?: CoordinateDelta;
+ readonly initialRegion?: Region;
+}
+
+interface Cluster {
+ id: string | number;
+ geometry: {
+ coordinates: [number, number]; // [longitude, latitude]
+ };
+ properties: {
+ point_count: number;
+ };
+ onPress: () => void;
}
export default function MapViewer({
userLocationDelta = defaultFocusDelta,
initialRegion = defaultInitialRegion,
- polygonFillColor = "rgba(255,0,0,0.5)",
- polygonStrokeColor = "black",
}: Props) {
+ const colorScheme = useColorScheme() ?? "light";
+ const mapColors = Colors[colorScheme].map;
+ const mapViewRef = useRef(null);
+ const suppressNextMapPress = useRef(false);
const [userLocation, setUserLocation] = useState(null);
const [locationState, setLocationState] = useState("off");
const [modalOpen, setModalOpen] = useState(false);
- const mapViewRef = useRef(null);
+ const [selectedBuilding, setSelectedBuilding] = useState(null);
+ const currentRegion = useRef(defaultInitialRegion);
+
+ const focusBuilding = useCallback((building: MapBuilding) => {
+ mapViewRef.current?.animateToRegion({
+ latitude: building.location.latitude,
+ longitude: building.location.longitude,
+ latitudeDelta: currentRegion?.current.latitudeDelta < 0.0025 ? currentRegion?.current.latitudeDelta : 0.0025,
+ longitudeDelta: currentRegion?.current.longitudeDelta < 0.0025 ? currentRegion?.current.longitudeDelta : 0.0025,
+ });
+ }, []);
+
+ const selectBuildingByCode = useCallback((code: string) => {
+ const info = concordiaBuildings.find((b) => b.buildingCode === code) ?? null;
+ setSelectedBuilding(info);
+ }, []);
+
+ const handlePolygonPress = useCallback((building: MapBuilding) => {
+ suppressNextMapPress.current = true;
+ selectBuildingByCode(building.code);
+ focusBuilding(building);
+
+ requestAnimationFrame(() => {
+ suppressNextMapPress.current = false;
+ });
+ }, [selectBuildingByCode, focusBuilding]);
+
+ const requestLocation = useCallback(async () => {
+ if (userLocation) return;
- const requestLocation = async () => {
- if (userLocation) {
- return;
- }
const locationEnabled = await LocationPermissions.hasServicesEnabledAsync();
if (!locationEnabled) {
setModalOpen(true);
return;
}
+
const { status } = await LocationPermissions.requestForegroundPermissionsAsync();
- if (status !== "granted") {
- return;
- }
+ if (status !== "granted") return;
+
const location = await LocationPermissions.getCurrentPositionAsync();
- setUserLocation({
- longitude: location.coords.longitude,
- latitude: location.coords.latitude,
- });
+ setUserLocation({ latitude: location.coords.latitude, longitude: location.coords.longitude });
setLocationState("on");
- return;
- };
+ }, [userLocation]);
+
+ const centerLocation = useCallback(() => {
+ if (!userLocation) return;
- const centerLocation = () => {
- if (!userLocation) {
- return;
- }
setLocationState("centered");
- const region = {
- ...userLocation,
- latitudeDelta: userLocationDelta.latitudeDelta,
- longitudeDelta: userLocationDelta.longitudeDelta,
- };
- mapViewRef.current?.animateToRegion(region);
- };
+ mapViewRef.current?.animateToRegion({ ...userLocation, ...userLocationDelta });
+ }, [userLocation, userLocationDelta]);
+
+ const renderPolygons = useMemo(() => {
+ return CAMPUS_LOCATIONS.flatMap((building) =>
+ building.polygons.map((polygon, index) => {
+ const isSelected = selectedBuilding?.buildingCode === building.code;
+
+ return (
+ handlePolygonPress(building)}
+ />
+ );
+ })
+ );
+ }, [mapColors, selectedBuilding?.buildingCode, handlePolygonPress]);
+
+ const renderMarkers = useMemo(() => {
+ return CAMPUS_LOCATIONS.map((building) => {
+ const isSelected = selectedBuilding?.buildingCode === building.code;
+
+ // Offset markers to prevent overlaps
+ let coordinate = building.location;
+ if (building.code === "VE") {
+ // VE overlaps with VL
+ coordinate = {
+ latitude: building.location.latitude + 0.00008,
+ longitude: building.location.longitude - 0.00015,
+ };
+ } else if (building.code === "RA") {
+ // RA overlaps with PC
+ coordinate = {
+ latitude: building.location.latitude - 0.00008,
+ longitude: building.location.longitude - 0.00015,
+ };
+ }
+
+ return (
+ {
+ selectBuildingByCode(building.code);
+ focusBuilding(building);
+ }}
+ >
+
+
+ {building.code}
+
+
+
+ );
+ });
+ }, [mapColors, selectedBuilding?.buildingCode, focusBuilding, selectBuildingByCode]);
+
+ const renderCluster = useCallback(
+ (cluster: Cluster) => {
+ const { id, geometry, properties, onPress } = cluster;
+ const count = properties.point_count ?? 0;
+
+ return (
+
+
+ {count > 9 ? "9+" : count}
+
+
+ );
+ },
+ [mapColors]
+ );
return (
- (userLocation ? setLocationState("on") : null)}
- onUserLocationChange={({ nativeEvent: { coordinate } }) => {
- if (!coordinate) {
- return;
- }
- if (!userLocation) {
- setLocationState("on");
+ clusteringEnabled={Platform.OS !== "ios"}
+ onRegionChangeComplete={(region) => {currentRegion.current = region}}
+ onPanDrag={() => {
+ if (userLocation) setLocationState("on");
+ }}
+ onUserLocationChange={({ nativeEvent }) => {
+ const coordinate = nativeEvent?.coordinate;
+ if (!coordinate || typeof coordinate.latitude !== "number" || typeof coordinate.longitude !== "number") return;
+ if (!userLocation) setLocationState("on");
+ setUserLocation({ latitude: coordinate.latitude, longitude: coordinate.longitude });
+ }}
+ spiralEnabled={false}
+ onPress={(e) => {
+ if (suppressNextMapPress.current) return;
+ const action = e?.nativeEvent?.action;
+
+ if (!action || action === "press") {
+ setSelectedBuilding(null);
}
- setUserLocation(coordinate);
}}
+ renderCluster={renderCluster}
>
- {CAMPUS_LOCATIONS.map((building, i) => {
- return building.polygons.map((polygon, i) => (
-
- ));
- })}
-
+ {renderPolygons}
+ {renderMarkers}
+
+
{
- if (locationState === "on") {
- centerLocation();
- } else if (locationState === "off") {
- requestLocation();
- }
+ onPress={() => {
+ if (locationState === "on") centerLocation();
+ else if (locationState === "off") requestLocation();
}}
/>
- setModalOpen(false)} visible={modalOpen} />
+ setModalOpen(false)} />
+
);
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
+ container: { flex: 1 },
+ map: { width: "100%", height: "100%" },
+ marker: {
+ paddingHorizontal: 5,
+ paddingVertical: 4,
+ borderRadius: 12,
+ borderWidth: 2
+ },
+ markerText: {
+ fontWeight: "700",
+ fontSize: 12,
+ },
+ clusterMarker: {
+ paddingHorizontal: 6,
+ paddingVertical: 4,
+ borderRadius: 50,
+ borderWidth: 2
},
- map: {
- width: "100%",
- height: "100%",
+ clusterText: {
+ fontWeight: "800",
+ fontSize: 12
},
});
diff --git a/components/themed-view.tsx b/components/themed-view.tsx
index 6f181d8..983228b 100644
--- a/components/themed-view.tsx
+++ b/components/themed-view.tsx
@@ -1,6 +1,6 @@
import { View, type ViewProps } from 'react-native';
-
import { useThemeColor } from '@/hooks/use-theme-color';
+import React from 'react';
export type ThemedViewProps = ViewProps & {
lightColor?: string;
diff --git a/constants/theme.ts b/constants/theme.ts
index f06facd..75ff3f6 100644
--- a/constants/theme.ts
+++ b/constants/theme.ts
@@ -1,30 +1,84 @@
-/**
- * Below are the colors that are used in the app. The colors are defined in the light and dark mode.
- * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
- */
+import { Platform } from "react-native";
-import { Platform } from 'react-native';
-
-const tintColorLight = '#0a7ea4';
-const tintColorDark = '#fff';
+const tintColorLight = "#5e0e16";
+const tintColorDark = "#ffffff";
export const Colors = {
light: {
- text: '#11181C',
- background: '#fff',
+ text: "#11181C",
+ background: "#fbf6ec",
tint: tintColorLight,
- icon: '#687076',
- tabIconDefault: '#687076',
+ icon: "#5e0e16",
+ tabIconDefault: "#11181C",
tabIconSelected: tintColorLight,
+
+ map: {
+ polygonFill: "#a0686d",
+ polygonHighlighted: "#701922",
+ polygonStroke: "black",
+ marker: "#200003",
+ markerSelected: "#fff",
+ markerText: "#fff",
+ markerTextSelected: "#200003",
+ markerBorder: "#fff",
+ markerBorderSelected: "#200003",
+ clusterMarker: "#200003",
+ clusterText: "#fff",
+ },
+
+ buildingInfoPopup: {
+ background: "#ffffff",
+ handle: "#cccccc",
+ title: "#11181C",
+ text: "#333333",
+ divider: "#dddddd",
+
+ openStatus: "#1e8e3e",
+ accessibilityIcon: "#1e8e3e",
+
+ actionButtonBackground: "#e8f0fe",
+ actionButtonText: "#1a73e8",
+ actionButtonIcon: "#1a73e8",
+ }
},
+
dark: {
- text: '#ECEDEE',
- background: '#151718',
+ text: "#b28e8b",
+ background: "#5e0e16",
tint: tintColorDark,
- icon: '#9BA1A6',
- tabIconDefault: '#9BA1A6',
+ icon: "#ffffff",
+ tabIconDefault: "#ffffff",
tabIconSelected: tintColorDark,
- },
+
+ map: {
+ polygonFill: "#a0686d",
+ polygonHighlighted: "#701922",
+ polygonStroke: "black",
+ marker: "#330703",
+ markerSelected: "#fff",
+ markerText: "#fff",
+ markerTextSelected: "#330703",
+ markerBorder: "#fff",
+ markerBorderSelected: "#330703",
+ clusterMarker: "#330703",
+ clusterText: "#fff",
+ },
+
+ buildingInfoPopup: {
+ background: "#ffffff",
+ handle: "#cccccc",
+ title: "#11181C",
+ text: "#333333",
+ divider: "#dddddd",
+
+ openStatus: "#1e8e3e",
+ accessibilityIcon: "#1e8e3e",
+
+ actionButtonBackground: "#e8f0fe",
+ actionButtonText: "#1a73e8",
+ actionButtonIcon: "#1a73e8",
+ }
+ }
};
export const Fonts = Platform.select({
diff --git a/data/buildings-polygons.json b/data/buildings-polygons.json
index a6d3a64..3ed35f3 100644
--- a/data/buildings-polygons.json
+++ b/data/buildings-polygons.json
@@ -127,8 +127,8 @@
"formatted_address": "1665 Rue Sainte-Catherine E, Montréal, QC H2L 2J5, Canada",
"geometry": {
"location": {
- "lat": 45.52217,
- "lng": -73.5531
+ "lat": 45.494258641914726,
+ "lng": -73.5790133953522
},
"location_type": "ROOFTOP",
"viewport": {
@@ -145,8 +145,8 @@
"navigation_points": [
{
"location": {
- "latitude": 45.5220977,
- "longitude": -73.5529365
+ "latitude": 45.494258641914726,
+ "longitude": -73.5790133953522
}
}
],
diff --git a/package-lock.json b/package-lock.json
index 7ea22b4..e5621fc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,6 +29,7 @@
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-gesture-handler": "~2.28.0",
+ "react-native-map-clustering": "^4.0.0",
"react-native-maps": "^1.20.1",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
@@ -4053,6 +4054,26 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@mapbox/geo-viewport": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@mapbox/geo-viewport/-/geo-viewport-0.4.1.tgz",
+ "integrity": "sha512-5g6eM3EOSl7+0p0VY+vHWEYjUlNzof936VKHTi/NuJVABjbYe8D2NAVJ0qt5C9Np4glUlhKFepgAgQ0OEybrjQ==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@mapbox/sphericalmercator": "~1.1.0"
+ }
+ },
+ "node_modules/@mapbox/sphericalmercator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/sphericalmercator/-/sphericalmercator-1.1.0.tgz",
+ "integrity": "sha512-pEsfZyG4OMThlfFQbCte4gegvHUjxXCjz0KZ4Xk8NdOYTQBLflj6U8PL05RPAiuRAMAQNUUKJuL6qYZ5Y4kAWA==",
+ "bin": {
+ "bbox": "bin/bbox.js",
+ "to4326": "bin/to4326.js",
+ "to900913": "bin/to900913.js",
+ "xyz": "bin/xyz.js"
+ }
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
@@ -15382,6 +15403,12 @@
"node": ">=4.0"
}
},
+ "node_modules/kdbush": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
+ "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
+ "license": "ISC"
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -17504,6 +17531,20 @@
"react-native": "*"
}
},
+ "node_modules/react-native-map-clustering": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/react-native-map-clustering/-/react-native-map-clustering-4.0.0.tgz",
+ "integrity": "sha512-+YNh4frhZIHQReURxYGHNy9MJ50GYWpW6psoBEjvTG6vb33eYu00GmO8Pu/9VwMB1YL5lOxZ9+sJClJ8Mz1Bxw==",
+ "license": "MIT",
+ "dependencies": {
+ "@mapbox/geo-viewport": "^0.4.1",
+ "supercluster": "^8.0.0"
+ },
+ "peerDependencies": {
+ "react-native": "*",
+ "react-native-maps": "*"
+ }
+ },
"node_modules/react-native-maps": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.20.1.tgz",
@@ -18971,6 +19012,15 @@
"node": ">= 6"
}
},
+ "node_modules/supercluster": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
+ "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
+ "license": "ISC",
+ "dependencies": {
+ "kdbush": "^4.0.2"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
diff --git a/package.json b/package.json
index c02a12f..e35be1e 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-gesture-handler": "~2.28.0",
+ "react-native-map-clustering": "^4.0.0",
"react-native-maps": "^1.20.1",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
diff --git a/tsconfig.json b/tsconfig.json
index 32f91a4..fbbfa60 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,7 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
+ "jsx": "react",
"strict": true,
"paths": {
"@/*": ["./*"]