Pārlūkot izejas kodu

add ability to group nearby landmarks

aidan 1 gadu atpakaļ
vecāks
revīzija
829de085ae

BIN
assets/group.png


+ 35 - 0
package-lock.json

@@ -2297,6 +2297,19 @@
       "integrity": "sha512-zrBczSbXKxEyK2ijtbRdICDygRqWSRPpZMN5dD1T8VMEW5RIhIbwFWw2phDRXuBQdVDpSjalCIUMWMV2h3JaZA==",
       "optional": true
     },
+    "@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==",
+      "requires": {
+        "@mapbox/sphericalmercator": "~1.1.0"
+      }
+    },
+    "@mapbox/sphericalmercator": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@mapbox/sphericalmercator/-/sphericalmercator-1.1.0.tgz",
+      "integrity": "sha512-pEsfZyG4OMThlfFQbCte4gegvHUjxXCjz0KZ4Xk8NdOYTQBLflj6U8PL05RPAiuRAMAQNUUKJuL6qYZ5Y4kAWA=="
+    },
     "@mischnic/json-sourcemap": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/@mischnic/json-sourcemap/-/json-sourcemap-0.1.0.tgz",
@@ -11364,6 +11377,11 @@
       "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
       "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
     },
+    "kdbush": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz",
+      "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew=="
+    },
     "keychain": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/keychain/-/keychain-1.3.0.tgz",
@@ -14852,6 +14870,15 @@
       "resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz",
       "integrity": "sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg=="
     },
+    "react-native-map-clustering": {
+      "version": "3.4.2",
+      "resolved": "https://registry.npmjs.org/react-native-map-clustering/-/react-native-map-clustering-3.4.2.tgz",
+      "integrity": "sha512-7VN3ZmOG6gH2THY1VCkrklJmLVxoaY3SyQHuPlNG1CPmL/unPkyAyfDxFoZahf+UxRYIWSbLa9C3oI3Lcswi+Q==",
+      "requires": {
+        "@mapbox/geo-viewport": "^0.4.1",
+        "supercluster": "^7.1.0"
+      }
+    },
     "react-native-maps": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.4.0.tgz",
@@ -16949,6 +16976,14 @@
       "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-9.1.1.tgz",
       "integrity": "sha512-es33J1g2HjMpyAhz8lOR+ICmXXAqTuKbuXuUWLhOLew20oN9oUCgCJx615U/v7aioZg7IX5lIh9x34vwneu4pA=="
     },
+    "supercluster": {
+      "version": "7.1.5",
+      "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz",
+      "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==",
+      "requires": {
+        "kdbush": "^3.0.0"
+      }
+    },
     "supports-color": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",

+ 2 - 1
package.json

@@ -84,7 +84,8 @@
     "react-native-fs": "^2.19.0",
     "react-native-gesture-handler": "~1.10.2",
     "react-native-get-random-values": "~1.7.0",
-    "react-native-maps": "^1.0.0",
+    "react-native-map-clustering": "^3.4.2",
+    "react-native-maps": "^1.4.0",
     "react-native-maps-directions": "^1.8.0",
     "react-native-material-menu": "^2.0.0",
     "react-native-modal": "^12.0.3",

+ 37 - 34
src/components/LandmarkTypePicker.tsx

@@ -1,4 +1,4 @@
-import React, {useState, useEffect} from 'react'
+import React, { useState, useEffect } from 'react'
 import { Image } from 'react-native';
 import Picker, { Item } from 'react-native-picker-select'
 import { FontAwesome } from "@expo/vector-icons";
@@ -10,35 +10,19 @@ interface LandmarkTypePickerProps {
     value?: any,
     cats: Item[],
     items: Item[],
-    placeholder?: {} | Item 
+    placeholder?: {} | Item
 }
 
-const LandmarkTypePicker: React.FC<LandmarkTypePickerProps> = ({items, cats, value, onValueChange, placeholder}) => {
+const LandmarkTypePicker: React.FC<LandmarkTypePickerProps> = ({ items, cats, value, onValueChange, placeholder }) => {
 
     const [selectedValueCat, setSelectedValueCat] = useState();
 
     let lmTypes = allLmTypes;
 
-    return (
-        <><View style={{ flexDirection: 'row' }}>
-            <Picker
-                style={{
-                    inputIOS: { color: 'white' },
-                    inputAndroid: { color: 'white', alignItems: 'center', justifyContent: 'center' },
-                    viewContainer: { marginVertical: 5, flex: 1 },
-                    iconContainer: { height: '100%', justifyContent: 'center' },
-                    placeholder: { color: 'white' }
-                }}
-                textInputProps={{ placeholderTextColor: 'white', selectionColor: 'white' }}
-                Icon={() => <FontAwesome name="chevron-down" color='white' style={{ alignSelf: 'center' }} size={20} />}
-                placeholder={placeholder}
-                onValueChange={(value) => setSelectedValueCat(value)}
-                useNativeAndroidPickerStyle={true}
-                items={cats} />
-
-        </View>
-        <View style={{ flexDirection: 'row' }}>
-                {selectedValueCat ?
+    if (value !== 30) {  // Value of 30 is a group landmark so we shouldn't be able to change the category or type in this case
+        return (
+            <>
+                <View style={{ flexDirection: 'row' }}>
                     <Picker
                         style={{
                             inputIOS: { color: 'white' },
@@ -50,19 +34,38 @@ const LandmarkTypePicker: React.FC<LandmarkTypePickerProps> = ({items, cats, val
                         textInputProps={{ placeholderTextColor: 'white', selectionColor: 'white' }}
                         Icon={() => <FontAwesome name="chevron-down" color='white' style={{ alignSelf: 'center' }} size={20} />}
                         placeholder={placeholder}
-                        value={value}
-                        onValueChange={onValueChange}
+                        onValueChange={(value) => setSelectedValueCat(value)}
                         useNativeAndroidPickerStyle={true}
-                        items={items.filter((icon) => {
-                            return icon.key == cats[selectedValueCat[0] - 1].label
-                           })
-                        } /> : null}
+                        items={cats} />
+
+                </View>
+                <View style={{ flexDirection: 'row' }}>
+                    {selectedValueCat ?
+                        <Picker
+                            style={{
+                                inputIOS: { color: 'white' },
+                                inputAndroid: { color: 'white', alignItems: 'center', justifyContent: 'center' },
+                                viewContainer: { marginVertical: 5, flex: 1 },
+                                iconContainer: { height: '100%', justifyContent: 'center' },
+                                placeholder: { color: 'white' }
+                            }}
+                            textInputProps={{ placeholderTextColor: 'white', selectionColor: 'white' }}
+                            Icon={() => <FontAwesome name="chevron-down" color='white' style={{ alignSelf: 'center' }} size={20} />}
+                            placeholder={placeholder}
+                            value={value}
+                            onValueChange={onValueChange}
+                            useNativeAndroidPickerStyle={true}
+                            items={items.filter((icon) => {
+                                return icon.key == cats[selectedValueCat[0] - 1].label
+                            })
+                            } /> : null}
 
-                {value ? <Image style={{ marginLeft: 20 }} source={lmTypes[value].image} />
-                    : null}
-        </View></>
-    )
-    
+                    {value ? <Image style={{ marginLeft: 20 }} source={lmTypes[value].image} />
+                        : null}
+                </View>
+            </>
+        )
+    } else return null
 }
 
 export default React.memo(LandmarkTypePicker)

+ 37 - 25
src/components/Map/MainMapComponent/IndoorMap.tsx

@@ -1,10 +1,10 @@
 import { FontAwesome } from "@expo/vector-icons";
 import ReactNativeZoomableView from '@openspacelabs/react-native-zoomable-view/src/ReactNativeZoomableView';
 import React, { useEffect, useState } from 'react';
-import { ActivityIndicator, Alert, Dimensions, GestureResponderEvent, ImageSourcePropType, StatusBar, StyleSheet, View } from 'react-native';
+import { ActivityIndicator, Alert, Dimensions, GestureResponderEvent, ImageSourcePropType, StatusBar, StyleSheet, View,  } from 'react-native';
 import Picker from 'react-native-picker-select';
 import Toast from 'react-native-root-toast';
-import { Image, Rect, Svg } from 'react-native-svg';
+import { Image, Rect, Svg, Text } from 'react-native-svg';
 import { Landmark } from '../../../data/landmarks';
 import { MapStackNavigationProp } from "../../../navigation/MapNavigator";
 import { colors, lmTypesIndoor } from "../../../utils/GlobalUtils";
@@ -16,7 +16,7 @@ import ArrowButton from './ArrowButton';
 interface IndoorMapProps {
   navigation: MapStackNavigationProp
   landmarks: Landmark[]
-  promptAddLandmark: (longitude?: number, latitude?: number, floor?: number) => void
+  promptAddLandmark: (longitude?: number, latitude?: number, floor?: number, lmCount?: string, parent?: string) => void
   focusLandmark: (landmark: Landmark) => void
   applyFilter: (landmarks: Landmark[]) => Landmark[]
 }
@@ -39,17 +39,29 @@ const IndoorMap: React.FC<IndoorMapProps> = ({ landmarks, promptAddLandmark, foc
     }
     if (item.floor == floor && SVGdim[0] != 1 && SVGdim[1] != 1) {
       return (
-        <Image
-          onPress={() => {
-            //console.log("item = " + item.longitude * SVGdim[0]);
-            focusLandmark(item);
-          }}
-          key={item.id}
-          x={item.longitude * SVGdim[0]}
-          y={item.latitude * SVGdim[1]}
-          width={imageDim}
-          height={imageDim}
-          href={lmTypesIndoor[item.landmark_type].image as ImageSourcePropType} />
+        <View style={{position: "absolute"}}>
+          <Svg>
+            <Image
+              onPress={() => {
+                //console.log("item = " + item.longitude * SVGdim[0]);
+                focusLandmark(item);
+              }}
+              key={item.id}
+              x={item.longitude * SVGdim[0]}
+              y={item.latitude * SVGdim[1]}
+              width={imageDim}
+              height={imageDim}
+              href={lmTypesIndoor[item.landmark_type].image as ImageSourcePropType} />
+            <Text
+              x={item.longitude * SVGdim[0] + imageDim * .4}
+              y={item.latitude * SVGdim[1] + imageDim * .5}
+              fontFamily="sans-serif-medium"
+              fontWeight="bold"
+              fontSize="8"
+              fill="black"
+              >{item.groupCount}</Text>
+          </Svg>
+        </View>
       )
     }
   })
@@ -99,7 +111,7 @@ const IndoorMap: React.FC<IndoorMapProps> = ({ landmarks, promptAddLandmark, foc
 
   // TODO: wire up promptaddlandmark, applyfilters, and focuslandmark methods passed from MapNavigator
   return (
-    <View style={{ height: '100%', width: Dimensions.get("screen").width, backgroundColor:colors.red }}>
+    <View style={{ height: '100%', width: Dimensions.get("screen").width, backgroundColor: colors.red }}>
 
       <StatusBar backgroundColor={colors.red} />
       {/* <CustomModal /> */}
@@ -109,17 +121,17 @@ const IndoorMap: React.FC<IndoorMapProps> = ({ landmarks, promptAddLandmark, foc
 
         {floor == 0 ? <ArrowButton num={0} fontAweIcon={""} /> : <ArrowButton num={-1} fontAweIcon={"chevron-left"} propEvent={() => changer(-1)} />}
 
-        <View style={{flex: 5, height: 53.5, width: 200 }}>
+        <View style={{ flex: 5, height: 53.5, width: 200 }}>
           <Picker
             placeholder={{}}
             value={floor}
-            style={{ 
-              inputIOSContainer: {width:'70%', justifyContent: 'center', alignSelf: 'center'},
-              inputAndroid: {textAlign: 'center', color:"white", },
-              inputIOS: { color: 'white', textAlign: 'center', height: '100%', alignSelf: 'center'} ,
-              iconContainer: {height: '100%', justifyContent: 'center',}
+            style={{
+              inputIOSContainer: { width: '70%', justifyContent: 'center', alignSelf: 'center' },
+              inputAndroid: { textAlign: 'center', color: "white", },
+              inputIOS: { color: 'white', textAlign: 'center', height: '100%', alignSelf: 'center' },
+              iconContainer: { height: '100%', justifyContent: 'center', }
             }}
-            Icon={() => <FontAwesome name="chevron-down"  color='white' size={15} />}
+            Icon={() => <FontAwesome name="chevron-down" color='white' size={15} />}
             onValueChange={(value) => {
               setFloor(value)
               setShowME(false)
@@ -165,7 +177,7 @@ const IndoorMap: React.FC<IndoorMapProps> = ({ landmarks, promptAddLandmark, foc
               }}
               movementSensibility={3}
               longPressDuration={200}
-              >
+            >
 
               <Svg onLayout={event => {
                 setSVGdim([event.nativeEvent.layout.width, event.nativeEvent.layout.height])
@@ -202,8 +214,8 @@ const styles = StyleSheet.create({
     height: '100%',
     maxWidth: "100%",
     maxHeight: "100%",
-    backgroundColor:"white",
-    
+    backgroundColor: "white",
+
   },
   image: {
     alignItems: 'center',

+ 37 - 22
src/components/Map/MainMapComponent/OutdoorMap.tsx

@@ -11,8 +11,8 @@ import { booleanPointInPolygon, circle } from '@turf/turf';
 import * as Notifications from 'expo-notifications';
 import { observer } from "mobx-react";
 import React, { useEffect, useState } from "react";
-import { Button, ActivityIndicator, Alert, Image, ImageBackground, Keyboard, Modal, Text, TouchableOpacity, TouchableWithoutFeedback, View, StyleProp, TextStyle } from "react-native";
-import MapView, { LatLng, MapEvent, Marker, Polygon, Polyline } from "react-native-maps";
+import { Button, ActivityIndicator, Alert, Image, ImageBackground, Keyboard, Modal, Text, TouchableOpacity, TouchableWithoutFeedback, View, StyleProp, TextStyle, GestureResponderEvent } from "react-native";
+import { LatLng, LongPressEvent, MapEvent, Marker, Polygon, Polyline } from "react-native-maps";
 import { openSettings } from "react-native-permissions";
 import Spokestack, { activate } from 'react-native-spokestack';
 import { useAuth } from "../../../data/Auth/AuthContext";
@@ -28,6 +28,7 @@ import NearbyLandmarksPanel from "../Panels/NearbyLandmarksPanel";
 import { VoicePanel } from "../Panels/VoicePanel";
 import mapStyles from "./Map.styles";
 import { useOutdoorMapState, useMapState } from "./useMapState";
+import MapView from "react-native-map-clustering"
 
 /**
  * An interface representing the user location retrieved from [expo-location]{@link https://docs.expo.dev/versions/latest/sdk/location/}.
@@ -62,8 +63,8 @@ interface OutdoorMapProps {
     toggleLmDetails: (state: boolean) => void,
     toggleLmAdd: (state: boolean) => void,
     landmarks: Landmark[]
-    applyFilters: (landmarks: Landmark[]) => Landmark[]
-    promptAddLandmark: (longitude: number, latitude: number) => void
+    applyFilters: (landmarks?: Landmark[], getGroupList?: boolean, parent?: string) => Landmark[]
+    promptAddLandmark: (longitude: number, latitude: number, floor?: number, lmCount?: string, parent?: string) => void
     setMarkerWindowPosition: (point: {x: number, y: number}) => void
     getLmUri: (uri: String) => void
 }
@@ -171,6 +172,9 @@ const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
         if (mapStateOutdoor.userLocation) {
             return { latitude: mapStateOutdoor.userLocation.latitude, longitude: mapStateOutdoor.userLocation.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01 }
         }
+        else {
+            return { latitude: 53.5232221, longitude: -113.5285073, latitudeDelta: 0.01, longitudeDelta: 0.01 }
+        }
     }
 
     /**
@@ -263,6 +267,7 @@ const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
                     onUserLocationChange={e => updateLocation(e.nativeEvent.coordinate)}
                     followsUserLocation={mapStateOutdoor.followUser}
                     showsMyLocationButton={false}
+                    clusterColor={colors.red}
                     
                     onRegionChangeComplete={async (Region) => {
                         if (props.selectedLandmarkId) {
@@ -290,25 +295,35 @@ const OutdoorMap: React.FC<OutdoorMapProps> = (props) => {
                     />
 
                     {props.applyFilters(props.landmarks)?.map((landmark) => {
-                        if (landmark.floor == null) {
-                            let trackChanges = false;
-                            if (landmark?.id == props.selectedLandmarkId) {
-                                trackChanges = true;
+                        if (landmark.groupCount !== -1) {  // -1 means it is part of a group so don't render a marker for it
+                            if (landmark.floor == null) {
+                                let trackChanges = false;
+                                if (landmark?.id == props.selectedLandmarkId) {
+                                    trackChanges = true;
+                                }
+                                return (
+                                    <Marker
+                                        tracksViewChanges={trackChanges}
+                                        coordinate={{ latitude: landmark.latitude as number, longitude: landmark.longitude as number }}
+                                        onPress={async e => {
+                                            e.persist()
+                                            props.focusLandmark(landmark, e)
+                                            await takeSnapshot();
+                                            await delay(500)
+                                            props.getLmUri(lmUri)
+                                        }}
+                                        key={landmark.id} >
+                                        {landmark.landmark_type ? 
+                                            landmark.landmark_type == 30 ?  // Landmark group
+                                                <ImageBackground style={{ height: 35, width: 25, zIndex: 10 }} source={lmTypes[landmark.landmark_type]?.image}> 
+                                                    <View style={{height: 35, position: 'absolute', top: 0, left: 0, right: 0, bottom: "40%", alignItems: 'center'}}>
+                                                        <Text style={{fontFamily: "sans-serif-medium", fontWeight: "bold"}}>{landmark.groupCount}</Text>
+                                                    </View>
+                                                </ImageBackground>
+                                                : <Image style={{ height: 35, width: 25, zIndex: 10 }} source={lmTypes[landmark.landmark_type]?.image} />
+                                            : null}
+                                    </Marker>)
                             }
-                            return (
-                                <Marker
-                                    tracksViewChanges={trackChanges}
-                                    coordinate={{ latitude: landmark.latitude as number, longitude: landmark.longitude as number }}
-                                    onPress={async e => {
-                                        e.persist()
-                                        props.focusLandmark(landmark, e)
-                                        await takeSnapshot();
-                                        await delay(500)
-                                        props.getLmUri(lmUri)
-                                    }}
-                                    key={landmark.id} >
-                                    {landmark.landmark_type ? <Image style={{ height: 35, width: 25, zIndex: 10 }} source={lmTypes[landmark.landmark_type]?.image} /> : null}
-                                </Marker>)
                         }
                     }
                     )}

+ 20 - 4
src/components/Map/Panels/AddLandmarkPanel.tsx

@@ -13,7 +13,7 @@ import { ScrollView } from "react-native-gesture-handler";
 import Modal from 'react-native-modal';
 import Picker from 'react-native-picker-select';
 import { Landmark, LMPhoto, useAddLandmark } from "../../../data/landmarks";
-import { colors, lmTypes as allLmTypes, lmTypesIndoor, catTypes } from "../../../utils/GlobalUtils";
+import { colors, lmTypes as allLmTypes, lmTypesIndoor, catTypes, catTypesInGroup } from "../../../utils/GlobalUtils";
 import { IconButton, SecondaryButton } from "../../Buttons";
 import { PhotoPicker } from "../../PhotoPicker";
 import TouchOpaq from "./LandmarkDetailsPanel/TouchOpaq";
@@ -32,6 +32,7 @@ import { Icon } from "react-native-paper/lib/typescript/components/Avatar/Avatar
 import DatePicker from 'react-native-date-picker'
 import CheckBox from "@react-native-community/checkbox";
 import { propertiesContainsFilter } from "@turf/turf";
+import { useOwnedProfile, useToggleGroupLMTip } from "../../../data/profiles";
 
 
 /**
@@ -55,6 +56,7 @@ export interface AddLandmarkProps {
      */
     setVisible: (state: boolean) => void;
     visible: boolean;
+    setLandmark: (landmark: string) => void; 
 }
 
 /**
@@ -62,13 +64,15 @@ export interface AddLandmarkProps {
  * @component
  * @category Map
  */
-const AddLandmarkPanel: React.FC<AddLandmarkProps> = ({ newLandmark, setNewLandmark, setVisible, visible }) => {
+const AddLandmarkPanel: React.FC<AddLandmarkProps> = ({ newLandmark, setNewLandmark, setVisible, visible, setLandmark }) => {
     const [photos, setPhotos] = useState<LMPhoto[]>([])
     const [photoSourceMenuOpened, togglePhotoSourceMenu] = useState<boolean>(false)
     const [keyboardOpened, setKeyboardOpened] = useState<boolean>(false);
     const [date, setDate] = useState(new Date())
     const [showExpire, toggleShowExpire] = useState<boolean>(false)
     var minDate = new Date();
+    const {profile} = useOwnedProfile()
+    const toggleGroupLMTip = useToggleGroupLMTip()
 
     const addLandmarkMutation = useAddLandmark()
 
@@ -157,7 +161,6 @@ const AddLandmarkPanel: React.FC<AddLandmarkProps> = ({ newLandmark, setNewLandm
     
     useEffect(() => {
         if (newLandmark) {
-            console.log(date)
             newLandmark.expiry_date = date
         }
     }, [date])
@@ -183,6 +186,19 @@ const AddLandmarkPanel: React.FC<AddLandmarkProps> = ({ newLandmark, setNewLandm
         }
         else {
             await addLandmarkMutation.mutateAsync({ landmarkValue: newLandmark, photos: photos })
+            if (newLandmark?.landmark_type == 30) {
+                if (profile?.show_group_lm_tip) {
+                    console.log('[Profile]: User has group tips configured, showing tips.')
+                    Alert.alert(
+                        'New landmark group added!', 
+                        "Please navigate to the new group and add individual landmarks.", 
+                        [{text: "Don't show this again", onPress: async () => await toggleGroupLMTip.mutateAsync()}, {text: 'Ok'}]
+                    )   
+                }
+                else {
+                    console.log('[Profile]: User does not have group tips configured, not showing tips')
+                }
+            }
         }
 
 
@@ -280,7 +296,7 @@ const AddLandmarkPanel: React.FC<AddLandmarkProps> = ({ newLandmark, setNewLandm
                                                 setNewLandmark({ ...newLandmark, landmark_type: undefined, title: 'no title' })
                                             }
                                         }}
-                                        cats={Object.keys(catTypes)?.map(icon => {
+                                        cats={Object.keys(newLandmark?.groupCount == -1 ? catTypesInGroup : catTypes)?.map(icon => {
                                             return (
                                                 { label: catTypes[parseInt(icon)]?.cat.toUpperCase(), value: icon, key: icon }
                                             )

+ 108 - 23
src/components/Map/Panels/LandmarkDetailsPanel/DetailsBody.tsx

@@ -8,8 +8,7 @@
 import { FontAwesome } from "@expo/vector-icons";
 import { useNavigationState } from "@react-navigation/native";
 import React, { MutableRefObject, useEffect, useRef, useState } from "react";
-import { FlatList, Dimensions, Image, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";
-import { TouchableOpacity } from "react-native-gesture-handler";
+import { FlatList, Dimensions, Image, ScrollView, StyleSheet, Text, TextInput, View, TouchableOpacity } from "react-native";
 import Picker from "react-native-picker-select";
 import { QueryStatus } from "react-query";
 import { LMComment } from "../../../../data/comments";
@@ -17,7 +16,7 @@ import { Landmark, LMPhoto } from "../../../../data/landmarks";
 import MapView, {Marker} from "react-native-maps";
 import { usePermissions } from "../../../../data/PermissionsContext";
 import { MainTabsNavigationProp } from "../../../../navigation/MainTabsNavigator";
-import { lmTypes as allLmTypes, lmTypesIndoor, catTypes } from "../../../../utils/GlobalUtils";
+import { lmTypes as allLmTypes, lmTypesIndoor, catTypes, catTypesInGroup } from "../../../../utils/GlobalUtils";
 import LandmarkTypePicker from "../../../LandmarkTypePicker";
 import { Separator } from "../../../Separator";
 import { CommentsContainer } from "./CommentsContainer";
@@ -26,6 +25,7 @@ import { useOutdoorMapState } from "../../MainMapComponent/useMapState";
 import { TagContainer } from "./TagContainer";
 import CheckBox from "@react-native-community/checkbox";
 import DatePicker from "react-native-date-picker";
+import { useAuth } from "../../../../data/Auth/AuthContext";
 
 
 
@@ -62,6 +62,11 @@ interface DetailsBodyProps {
     uri: String
     tagLandmark: (tagType: string) => void
     taggedByUser?: Array<boolean>
+    applyFilters: (landmarks?: Landmark[], getGroupList?: boolean, parent?: string) => Landmark[]
+    landmarks: Landmark[]
+    toggleDetailsPanel: (state: boolean) => void,
+    setLandmark: (landmark: string) => void; 
+    promptAddLandmark: (longitude: number, latitude: number, floor?: number, lmCount?: string, parent?: string) => void
 }
 
 /**
@@ -70,6 +75,7 @@ interface DetailsBodyProps {
 export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
     const {locationPermissionsGranted, checkLocationPermissions, voicePermissionsGranted, checkVoicePermissions} = usePermissions();
     const mapState = useOutdoorMapState()
+    const {landmarkOwnedByUser} = useAuth()
 
     const navigationState = useNavigationState(state => state)
     const [currentRoute, setCurrentRoute] = useState<string>()
@@ -108,6 +114,7 @@ export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
     
     const [date, setDate] = useState(new Date())
     const [showExpire, toggleShowExpire] = useState<boolean>(false)
+    const [groupList, toggleGroupList] = useState<boolean>(props.landmark?.landmark_type == 30 ? true : false)
 
     useEffect(() => {
         if (props.editingEnabled && !showExpire)
@@ -115,7 +122,7 @@ export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
         else if (props.editingEnabled && showExpire)
             props.setUpdatedLandmark({...props.updatedLandmark, expiry_date: date})
     }, [showExpire])
-    
+
     useEffect(() => {
         if (showExpire)
             props.setUpdatedLandmark({...props.updatedLandmark, expiry_date: date})
@@ -132,20 +139,27 @@ export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
             return(
             <>
                 <LandmarkTypePicker 
-                    placeholder={{}}
-                    value={props.updatedLandmark?.landmark_type} 
+                    placeholder={{ label: "Select a landmark type...", value: 0 }}
+                    value={props.updatedLandmark?.landmark_type}
                     onValueChange={(value) => {
-                        props.setUpdatedLandmark({...props.updatedLandmark, landmark_type: value, title: lmTypes[value].label})
-                    }}  
-                    cats={Object.keys(lmTypes)?.filter(icon => parseInt(icon) != props.landmark?.landmark_type).map(icon => {
+                        if (value) {
+                            props.setUpdatedLandmark({ ...props.updatedLandmark, landmark_type: value, title: lmTypes[value].label })
+                        }
+                        else {
+                            props.setUpdatedLandmark({ ...props.updatedLandmark, landmark_type: undefined, title: 'no title' })
+                        }
+                    }}
+                    cats={Object.keys(props.landmark?.groupCount == -1 ? catTypesInGroup : catTypes)?.map(icon => {
                         return (
-                            {label: catTypes[parseInt(icon)]?.cat.toUpperCase(), value: icon, key: icon}
-                        )})}
-                    items={Object.keys(lmTypes)?.filter(icon => parseInt(icon) != props.landmark?.landmark_type).map(icon => {
+                            { label: catTypes[parseInt(icon)]?.cat.toUpperCase(), value: icon, key: icon }
+                        )
+                    })}
+                    items={Object.keys(lmTypes)?.map(icon => {
                         return (
-                            {label: lmTypes[parseInt(icon)].label.toUpperCase(), value: icon, 
-                                key: lmTypes[parseInt(icon)].cat.toUpperCase()}
-                        )})}/>
+                            { label: lmTypes[parseInt(icon)]?.label.toUpperCase(), 
+                            value: icon, key: lmTypes[parseInt(icon)]?.cat.toUpperCase() }
+                        )
+                    })}/>
             </>
             )
         }
@@ -176,9 +190,16 @@ export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
      * @param 
      */
     const EditingDisabledUpperView: React.FC = () => {
+        if (!groupList) {
         return (
             <View style={{flexDirection: 'row', justifyContent: 'space-between'}}>
                 <View style={{flex: 8, flexDirection: 'column', marginBottom: 20}}>
+                    {props.landmark?.groupCount == -1 ?
+                        <TouchableOpacity style={styles.groupReturnButton} onPress={() => {props.setLandmark(props.landmark?.parent); toggleGroupList(true)}}>
+                            <FontAwesome size={15} color="white" name="chevron-left"/>
+                            <Text style={{color: "white", marginLeft: 5}}>Return to group</Text>
+                        </TouchableOpacity>
+                        : null}
                     <Text style={{color: 'white', marginBottom: 10, fontSize: 15}}>{lmTypes[props.landmark?.landmark_type]?.label.toUpperCase()}</Text>
                     <ScrollView nestedScrollEnabled={true}>
                         <Text style={{color: 'white', fontSize: 13}}>{props.landmark?.description}</Text>
@@ -188,6 +209,44 @@ export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
                 </View>
                 {props.landmark?.landmark_type ? <Image source={lmTypes[props.landmark?.landmark_type]?.image} /> : null}
             </View>
+        )
+                        } else return null
+    }
+
+    /**
+     * Sub-component that renders the list of landmarks in a group
+     */
+    const LandmarkGroupListView: React.FC = () => {
+        return (
+            <View>
+                <Text style={{color: 'white', fontSize: 13}}>{props.landmark?.description}</Text>
+                {props.landmark?.expiry_date ?
+                <Text style={{color: 'white', fontSize: 13}}>Landmark group expires on {props.landmark.expiry_date.toString().substring(0,10)}</Text> : null}
+                <Text style={{fontSize: 16, color: "white", marginBottom: 10, marginTop: 5}}>Landmarks in this group:</Text>
+                <ScrollView>
+                    {props.applyFilters(props.landmarks, true, props.landmark?.id)?.map((landmark) => {
+                        return (
+                            <TouchableOpacity key={landmark.id} style={styles.groupListItem}
+                                onPress={() => {
+                                    // props.toggleDetailsPanel(false)
+                                    console.log('changing to landmark' + lmTypes[landmark?.landmark_type]?.label)
+                                    toggleGroupList(false)
+                                    props.setLandmark(landmark.id)
+                                }}>
+                                <Image style={{ height: 52.5, width: 37.5, zIndex: 10, margin: 5 }} source={lmTypes[landmark.landmark_type]?.image} />
+                                <Text style={{marginLeft: 10}}>{lmTypes[landmark?.landmark_type]?.label.toUpperCase()}</Text>
+                            </TouchableOpacity>
+                        )
+                    }
+                    )}
+                    {landmarkOwnedByUser(props.landmark) ? 
+                    <TouchableOpacity style={{marginTop: 30, justifyContent: 'center', alignItems: 'center', opacity: .7}}
+                        onPress={() => {props.promptAddLandmark(props.landmark?.longitude, props.landmark?.latitude, undefined, "groupItem", props.landmark?.id)}}>
+                        <Text style={{fontSize: 20, marginBottom: 10, color: 'white'}}>Add landmark to group</Text>
+                        <FontAwesome name="plus" size={30} color='white' />
+                    </TouchableOpacity> : null }
+                </ScrollView>
+            </View>
         )
     }
 
@@ -195,25 +254,24 @@ export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
         <ScrollView ref={mainScrollRef} nestedScrollEnabled={true} contentContainerStyle={{justifyContent: 'space-between'}} style={{flex: 1, marginHorizontal: 20}}>
             {props.editingEnabled ?
             <>
-                <Text onPress={() => toggleEditLocation(!editLocation)} style={{color: 'white', marginBottom: 10}}>Tap to edit location</Text>
-                {editLocation && <MapView
+                {props.landmark?.groupCount !== -1 &&  // Prevent users from editing the location of landmarks in a group (must edit group itself)
+                <MapView
                     toolbarEnabled={false}
                     onPress={(e) => 
                         props.setUpdatedLandmark({...props.updatedLandmark, longitude: e.nativeEvent.coordinate.longitude, latitude: e.nativeEvent.coordinate.latitude})
                     }
                     testID="mapViewLocationEdit"
-                    style={{ width: '100%', height: Dimensions.get("window").height * .4, marginBottom: 20}}
+                    style={{ width: '100%', height: Dimensions.get("window").height * .4, marginBottom: 20, marginTop: 10}}
                     initialRegion={{latitude: props.landmark?.latitude, longitude: props.landmark?.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01}}
                     showsUserLocation={locationPermissionsGranted}
                     showsMyLocationButton={false}
                     >
-
                     <Marker
                         coordinate={{ latitude: props.updatedLandmark?.latitude as number, longitude: props.updatedLandmark?.longitude as number }} >
                         {props.updatedLandmark?.landmark_type ? <Image style={{ height: 35, width: 25 }} source={lmTypes[props.updatedLandmark?.landmark_type]?.image} /> : null}
                     </Marker>
                 </MapView>}
-                <LandmarkTypePickerContainer />
+                {props.landmark?.landmark_type !== 30 && <LandmarkTypePickerContainer /> /* Prevent editing of landmark type if it is a group */}
                 <Separator style={{marginVertical: 10, opacity: .5}} color="lightgray" />
                 <Text style={{color: 'white', marginBottom: 10}}>Description</Text>
                 <ScrollView nestedScrollEnabled={true} style={{backgroundColor: 'white', marginBottom: 20}}>
@@ -245,14 +303,16 @@ export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
                         fadeToColor="none" />}
                 </View>
             </>: <EditingDisabledUpperView />}
-            {!props.editingEnabled ? 
+            {!props.editingEnabled && groupList ?
+            <LandmarkGroupListView /> : null}
+            {!props.editingEnabled && !groupList ? 
             <TagContainer
                 landmark={props.landmark}
                 tagLandmark={props.tagLandmark}
                 toggleLmDetails={props.toggleLmDetails}
                 authNavigation={props.authNavigation}
                 taggedByUser={props.taggedByUser} /> : null}
-            {!props.editingEnabled ?
+            {!props.editingEnabled && !groupList ?
             <CommentsContainer
                 toggleLmDetails={props.toggleLmDetails}
                 authNavigation={props.authNavigation}
@@ -273,7 +333,7 @@ export const DetailsBody: React.FC<DetailsBodyProps> = (props) => {
                 startEditingComment={props.startEditingComment}
                 deleteComment={props.deleteComment}
                 uri={props.uri} /> : null}
-            {!props.editingEnabled && !props.keyboardOpened ?
+            {!props.editingEnabled && !props.keyboardOpened && !groupList ?
             <LandmarkPhotos 
                 profileId={props.profileId}
                 deletePhotoStatus={props.deletePhotoStatus}
@@ -303,4 +363,29 @@ const styles = StyleSheet.create({
         flex: 5,
         marginBottom: 40,
     },
+    groupListItem: {
+        flex: 1,
+        flexDirection: "row",
+        alignItems: "center",
+        borderColor: "white",
+        borderWidth: 1,
+        borderRadius: 10,
+        backgroundColor: "white",
+        margin: 10,
+    },
+    groupReturnButton: {
+        flex: 1,
+        flexDirection: "row",
+        alignItems: "center",
+        marginBottom: 5,
+        paddingTop: 5,
+        paddingBottom: 5,
+    },
+    toggleLocationEditor: {
+        flex: 1,
+        flexDirection: "row",
+        alignItems: "center",
+        marginTop: 5,
+        marginBottom: 5,
+    }
 })

+ 22 - 5
src/components/Map/Panels/LandmarkDetailsPanel/LandmarkDetails.tsx

@@ -49,6 +49,9 @@ export interface LandmarkDetailsProps {
     markerWindowPosition: {x: number, y: number}
     place: string;
     uri: String
+    applyFilters: (landmarks?: Landmark[], getGroupList?: boolean, parent?: string) => Landmark[]
+    landmarks: Landmark[]
+    promptAddLandmark: (longitude: number, latitude: number, floor?: number, lmCount?: string, parent?: string) => void
 }
 
 /**
@@ -56,7 +59,7 @@ export interface LandmarkDetailsProps {
  * @component
  * @category Map
  */
-const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({markerWindowPosition, authNavigation, landmarkId, setLandmark, toggleDetailsPanel, setEditing, editingEnabled, visible, toggleLmDetails, place, uri}) => {
+const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({markerWindowPosition, authNavigation, landmarkId, setLandmark, toggleDetailsPanel, setEditing, editingEnabled, visible, toggleLmDetails, place, uri, applyFilters, landmarks, promptAddLandmark}) => {
     const {userId, landmarkOwnedByUser} = useAuth()
 
     // /**
@@ -223,10 +226,19 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({markerWindowPosition,
      */
     const editLandmark = async () => {
         if (updatedLandmark) {
-            await editLandmarkMutation.mutateAsync(updatedLandmark) 
+            if (updatedLandmark?.landmark_type == 30) {
+                Alert.alert(
+                    'Are you sure?', 
+                    "This will convert the landmark to an empty group and is irreversible.", 
+                    [{text: "Okay", onPress: async () => {await editLandmarkMutation.mutateAsync(updatedLandmark); setEditing(false);}},
+                    {text: 'Cancel', onPress: () => {return}}]
+                ) 
+            }
+            else {
+                await editLandmarkMutation.mutateAsync(updatedLandmark)
+                setEditing(false);
+            }
         }
-        
-        setEditing(false);
     }
 
     /**
@@ -437,7 +449,12 @@ const LandmarkDetails: React.FC<LandmarkDetailsProps> = ({markerWindowPosition,
                     addPhotoStatus={addLandmarkPhotoMutation.status}
                     uri={uri}
                     tagLandmark={tagLandmark}
-                    taggedByUser={landmarkQuery?.data?.taggedByUser}/>
+                    taggedByUser={landmarkQuery?.data?.taggedByUser}
+                    applyFilters={applyFilters}
+                    landmarks={landmarks}
+                    toggleDetailsPanel={toggleDetailsPanel}
+                    setLandmark={setLandmark}
+                    promptAddLandmark={promptAddLandmark}/>
                 </> :
                 <View style={{height: '100%', justifyContent: "space-evenly", alignItems: "center", marginHorizontal: 20}}>
                     <Text style={{color: 'white', fontSize: 20}}>{

+ 0 - 1
src/components/Map/Panels/LandmarkDetailsPanel/TagContainer.tsx

@@ -125,7 +125,6 @@ const styles = StyleSheet.create({
     },
     tag: {
         margin: 2,
-        // backgroundColor: "#ADADAD",
     },
     tagText: {
         color: "#FF0000",

+ 7 - 0
src/data/landmarks.ts

@@ -55,6 +55,13 @@ export interface Landmark {
     tag_imprecise?: number | null,
     /*** A Date object representing an optional expiry date that will automatically cull the landmark */
     expiry_date?: Date | null,
+    /** A number for grouped landmarks. 0 means not part of a group, -1 means it is part of a group,
+     *  and anything else means that this is the actual group of landmarks, with the number representing
+     *  the number of landmarks in the group.
+     */
+    groupCount?: number | null,
+    /*** The id of the parent landmark if this landmark is part of a group */
+    parent?: string | null,
 }
 
 export interface LMPhoto {

+ 30 - 0
src/data/profiles.ts

@@ -39,6 +39,10 @@ export interface UserProfile {
      * The user's preference for seeing tips.
      */
     show_tips?: boolean
+    /**
+     * The user's preference for seeing a tip when adding a group landmark.
+     */
+    show_group_lm_tip?: boolean
 }
 
 /**
@@ -207,4 +211,30 @@ export const useToggleTips = () => {
         onSuccess: () => { queryClient.invalidateQueries(queryKeys.getOwnedProfile)},  
         onError: () => queryClient.invalidateQueries(queryKeys.getOwnedProfile),  
     })
+}
+
+
+export const useToggleGroupLMTip = () => {
+    const {sendApiRequestAsync, userId} = useAuth()
+    const queryClient = useQueryClient();
+
+    const toggleGroupTip = async () => {
+        if (userId) { 
+            const response = await sendApiRequestAsync({
+                axiosConfig: {
+                    method: 'POST',
+                    url: `/api/user-profile/toggle-group-lm-tip/${userId}/`,
+                },
+                authorized: true,
+                errorMessage: 'Something went wrong when toggling group landmark tip',
+                loggingCategory: 'PROFILE',
+            });   
+            return response.data;
+        }
+    }
+
+    return useMutation(toggleGroupTip, {
+        onSuccess: () => { queryClient.invalidateQueries(queryKeys.getOwnedProfile)},  
+        onError: () => queryClient.invalidateQueries(queryKeys.getOwnedProfile),  
+    })
 }

+ 36 - 17
src/navigation/MapNavigator.tsx

@@ -74,18 +74,23 @@ const MapNavigator: React.FC = ({ }) => {
         refetchLandmarksOnFilterOptionsChange()
     }, [mapState.lmFilteredTypes, mapState.onlyOwned, mapState.minLmRating])
 
-    const applyFilters = (landmarks: Landmark[]): Landmark[] => {
-        if (landmarks?.length > 0) {
-            if (mapState.minLmRating > 0) {
-                landmarks = landmarks?.filter(lm => lm && lm.rating >= mapState.minLmRating);
-            }
-
-            if (mapState.lmFilteredTypes?.length > 0) {
-                landmarks = landmarks?.filter(lm => mapState.lmFilteredTypes.includes(lm?.landmark_type));
-            }
-
-            if (mapState.onlyOwned) {
-                landmarks = landmarks?.filter(lm => landmarkOwnedByUser(lm));
+    const applyFilters = (landmarks?: Landmark[], getGroupList?: boolean, parent?: string): Landmark[] => {
+        if (getGroupList) {
+            landmarks = landmarks?.filter(lm => lm && lm.parent == parent)
+        }
+        else {
+            if (landmarks?.length > 0) {
+                if (mapState.minLmRating > 0) {
+                    landmarks = landmarks?.filter(lm => lm && lm.rating >= mapState.minLmRating);
+                }
+
+                if (mapState.lmFilteredTypes?.length > 0) {
+                    landmarks = landmarks?.filter(lm => mapState.lmFilteredTypes.includes(lm?.landmark_type));
+                }
+
+                if (mapState.onlyOwned) {
+                    landmarks = landmarks?.filter(lm => landmarkOwnedByUser(lm));
+                }
             }
         }
 
@@ -125,11 +130,21 @@ const MapNavigator: React.FC = ({ }) => {
  * Triggers {@link openAddLandmark} via useEffect because the asyncronous nature of useState does not set the coordinates fast enough to toggle the
  * modal directly through this method (this issue shows up in many other parts of the app where a modal is toggled by a boolean, and is solved in the same way).
  */
-    const promptAddLandmark = async (longitude?: number, latitude?: number, floor?: number) => {
+    const promptAddLandmark = async (longitude?: number, latitude?: number, floor?: number, lmCount?: string, parent?: string) => {
         console.log('[Map]: Opening add landmark panel...')
-        mapState.setNewLandmark({ latitude: latitude, longitude: longitude, floor: floor, voice: false });
-        mapState.toggleLmAdd(true)
-        mapState.toggleLmDetails(false)
+        if (lmCount == "groupItem") {  // Landmark is part of a group
+            mapState.setNewLandmark({ latitude: latitude, longitude: longitude, floor: floor, voice: false, parent: parent, groupCount: -1 });
+            mapState.toggleLmAdd(true)
+            mapState.toggleLmDetails(false)
+        } else if (lmCount == "group") {  // Landmark is the group itself
+            mapState.setNewLandmark({ latitude: latitude, longitude: longitude, floor: floor, voice: false, landmark_type: 30 });
+            mapState.toggleLmAdd(true)
+            mapState.toggleLmDetails(false)
+        } else {  // Regular individial landmark
+            mapState.setNewLandmark({ latitude: latitude, longitude: longitude, floor: floor, voice: false });
+            mapState.toggleLmAdd(true)
+            mapState.toggleLmDetails(false)
+        }
     }
 
 
@@ -279,7 +294,8 @@ const MapNavigator: React.FC = ({ }) => {
                 setNewLandmark={mapState.setNewLandmark}
                 setVisible={mapState.toggleLmAdd}
                 newLandmark={mapState.newLandmark}
-                visible={mapState.lmAddVisible} />
+                visible={mapState.lmAddVisible}
+                setLandmark={mapState.setSelectedLandmarkId} />
             <LandmarkDetails
                 markerWindowPosition={markerWindowPosition}
                 authNavigation={authNavigation}
@@ -292,6 +308,9 @@ const MapNavigator: React.FC = ({ }) => {
                 landmarkId={mapState.selectedLandmarkId}
                 place = {mapState.place}
                 uri = {mapState.uri}
+                applyFilters = {applyFilters}
+                landmarks={landmarksQuery?.data}
+                promptAddLandmark={promptAddLandmark}
                 />
             <FilterPanel
                 visible={mapState.filterVisible}

+ 14 - 1
src/utils/GlobalUtils.ts

@@ -24,9 +24,20 @@ export const catTypes: {[key: number]: {cat: string}} = {
     3: {cat: "difficult access"},
     4: {cat: "accessible"},
     5: {cat: "unpleasant"},
-    6: {cat: "not in list"}
+    6: {cat: "not in list"},
+    7: {cat: "group (combine landmarks)"}
 } // works for now, but better solution would be to take categories from lmTypes and remove duplicates
 
+// Set of category types used when adding a landmark inside a group, 'group' option removed to prevent group nesting
+export const catTypesInGroup: {[key: number]: {cat: string}} = {
+    1: {cat: "temporary"},
+    2: {cat: "permanent"},
+    3: {cat: "difficult access"},
+    4: {cat: "accessible"},
+    5: {cat: "unpleasant"},
+    6: {cat: "not in list"},
+}
+
 export const lmTypes: {[key: number]: {image: ImageRequireSource, label: string, cat: string}} = {
     1: {image: require('../../assets/pothole.png'), label: "pothole", cat: "temporary"}, //done
     2: {image: require('../../assets/stairs.png'), label: "stairs", cat: "permanent"}, //done
@@ -57,6 +68,7 @@ export const lmTypes: {[key: number]: {image: ImageRequireSource, label: string,
     27: {image: require('../../assets/water.png'), label: 'water fountain', cat: "accessible"}, //done
     28: {image: require('../../assets/loudnoise.png'), label: 'loud', cat: "unpleasant"}, //done
     29: {image: require('../../assets/garbage.png'), label: 'stinky', cat: "unpleasant"}, //done
+    30: {image: require('../../assets/group.png'), label: 'group', cat: "group (combine landmarks)"}, //done
 }
 
 export const lmTypesIndoor: {[key: number]: {image: ImageRequireSource, label: string, cat: string}} = {
@@ -89,6 +101,7 @@ export const lmTypesIndoor: {[key: number]: {image: ImageRequireSource, label: s
     27: {image: require('../../assets/water.png'), label: 'water fountain', cat: "accessible"}, //done
     28: {image: require('../../assets/loudnoise.png'), label: 'loud', cat: "unpleasant"}, //done
     29: {image: require('../../assets/garbage.png'), label: 'stinky', cat: "unpleasant"}, //done
+    30: {image: require('../../assets/group.png'), label: 'group', cat: "group (combine landmarks)"}, //done
 }
 
 export const GlobalStyles = StyleSheet.create({

+ 2 - 2
src/utils/RequestUtils.ts

@@ -11,11 +11,11 @@
 //export const API_URL = 'http://192.168.3.81:8000'
 // export const API_URL = 'https://staging.clicknpush.ca'
 
-// export const API_URL = 'https://app.clicknpush.ca'
+export const API_URL = 'https://app.clicknpush.ca'
 // export const API_URL = 'http://192.168.1.106:8000' // Nathan
 //export const API_URL = 'http://192.168.1.64:8000'   // Chase
 //export const API_URL = 'http://192.168.0.22:8000'       // Eric
 // export const API_URL = 'http://192.168.1.131:8000'  // Aidan surface
-export const API_URL = 'http://192.168.1.99:8000'  // Aidan home
+// export const API_URL = 'http://192.168.1.87:8000'  // Aidan home
 
 // export const API_URL = Config.API_URL