Visualizing your photos on a map with React Native
Tags: javascript, typescript, mobile, reactnative
Published: 2023-03-01 18:45:32

---
title: Visualizing your photos on a map with React Native
published: true
description: 
tags: javascript, typescript, mobile, reactnative
canonical_url: https://wkalmar.github.io/post/visualising-you-photos-with-react/
---
that time, I had a Nokia Lumia phone with a nice widget that displayed the locations of the photos I've taken on a map. I enjoyed looking at it realizing how big is the world and how much I have to travel in order to discover it.

While I have a similar widget in my Android phone I wasn't satisfied with its look and feel. Another issue for me was that the last time I touched fron-end was [4 years ago](https://wkalmar.github.io/post/cooking-angular-js-with-typescript/) and since then I hadn't much experience with modern SPA-frameworks. The desire to bridge that gap and to have the app I wanted motivated me to create my own.

The ultimate result looks like this.

<img src="https://wkalmar.github.io/14/1.jpg">

In case you want to try out the app you can download it from the [Play Market](https://play.google.com/store/apps/details?id=com.wkalmar.Locate). The source code is available on [Github](https://github.com/Wkalmar/locate) in case you want to follow along with the code in the article.

## Constraints and tradeoffs
The reason behind choosing my tech stack was to get to know modern SPA frameworks better. So naturally, my choice was [React Native](https://reactnative.dev/). As a novice in mobile development, I decided to start with [Expo](https://expo.dev/). It is a nice way to start the project since it contains a couple of templates as well as a nice debugging toolset.

What I've learned is that the convenience of Expo is not free. Here are some disadvantages:
- Implicit integration with [Facebook SDK](https://betterprogramming.pub/how-expo-is-fooling-everyone-adf7f34d7528)
- Depreciation of some crucial libs such as [expo-ads-admob](https://docs.expo.dev/versions/v45.0.0/sdk/admob/)
- Deprecation of [classic build](https://blog.expo.dev/turtle-goes-out-to-sea-d334db2a6b60).

However, these shortcomings can be remediated with EAS build.

## Visualization algorithm

In order to provide the best possible look and feel we need to give a user a feeling that something is happening while the application is enumerating the photos and obtaining their geodata. So rendering sequence looks as below:

1. Display a splash screen
2. Replace it with a screen that resembles a splash screen but additionally provides information about loading progress.
3. Once photos are processed, display a map with the points.

Let's briefly look at these points.

## Displaying progress information

The root component code is provided below
```jsx
SplashScreen.preventAutoHideAsync().catch(() => {
  /* reloading the app might trigger some race conditions, ignore them */
});

const App = () => {
  return (
    <AnimatedAppLoader/>
  );
}
```
Pretty straightforward. We prevent the automatic hiding of a splashscreen and render a single component. Let's dive deeper into the code of this component.
```jsx
  return (
      <View style={{ flex: 1 }}>
        <AnimatedSplashScreen/>
      </View>
    )
  }
```
The point of our interest is `AnimatedSplashScreen` component.
```jsx
const AnimatedSplashScreen = () => {
    const textAnimation = useMemo(() => new Animated.Value(0), []);
    const [isAppReady, setAppReady] = useState(false);
    const [isTextAnimationIsReady, setTextAnimationIsReady] = useState(false);
    const [loadingText, setLoadingText] = useState("");

    useEffect(() => {
      if (!isAppReady && isTextAnimationIsReady) {
        Animated.timing(textAnimation, {
          toValue: 1,
          duration: 200,
          easing: Easing.inOut(Easing.exp),
          useNativeDriver: true,
        }).start();
      }
    }, [isAppReady, isTextAnimationIsReady]);

    const loadLocations = async () => {
      let markersArray : MediaLibrary.Location[] = [];
      let hasMoreData = true;
      try {
        while (hasMoreData) {
          //loading imahe locations is omitted by now
          let now = Date.now();
          let delta = now - timeStart;
          if (delta > 9000) {
            setLoadingText(`We've processed ${cursor.endCursor} items. There's more work though...`)
          } else if (delta > 5000) {
            setLoadingText("Working on it...")
          } else if (delta > 2000) {
            if (!isTextAnimationIsReady) {
              setTextAnimationIsReady(true);
            }
            setLoadingText("Hold on! We're doing some magic just for you...")
          }
        }
      } catch (e) {
        console.log(e)
      } finally {
        setAppReady(!hasMoreData);
      }
    }

    const onImageLoaded = useCallback(async () => {
      await SplashScreen.hideAsync();
      await loadLocations();
    }, []);

    return (
      <View style={{ flex: 1 }}>
        {isAppReady  && (<MainScreen/>)}
        {!isAppReady && (<Animated.View
          pointerEvents="none"
          style={[
            {
              backgroundColor: Constants.manifest?.splash?.backgroundColor
            },
          ]}>
          <Animated.Text>
            {loadingText}
          </Animated.Text>
          <Animated.Image
            source={require("../assets/splash.png")}
            onLoadEnd={onImageLoaded}
            fadeDuration={0}
          />
        </Animated.View>)}
      </View>
    );
  }
```
What happens here is that we replace our splash screen with `Animated.Image` that looks exactly like the splash screen. Once the image is ready we call `onImageLoaded` callback. Now when our image is ready we can hide the splashscreen and call `loadLocations`. This method performs the work needed to enumerate location of our photos and meanwhile takes charge of animation to keep the user engaged.

For now, everything we need to know about `loadLocations` method is that it does image processing batch by batch which is indicated by `while (hasMoreData)`. This means that inside each loop iteration, we can check how much time elapsed and trigger text animation accordingly. Once it's time to display text animation we update the state with `setTextAnimationIsReady(true);`. This in turn triggers `Animated.timing`. Which changes the opacity of `Animated.Text`.

Once we're done we update the state with `setAppReady` and substitute our animation component with `MainScreen`.

## Loading image location
In order to enumerate the contents of our gallery we'll use [Expo Medialibrary](https://docs.expo.dev/versions/latest/sdk/media-library/).

```jsx
let medialibraryRequest : MediaLibrary.AssetsOptions = {}

const loadLocations = async () => {
  let markersArray : MediaLibrary.Location[] = [];
  let hasMoreData = true;
  try {
    let { status } = await MediaLibrary.requestPermissionsAsync();
    let markersSet : Set<MediaLibrary.Location> = new Set();
    const albums = await MediaLibrary.getAlbumsAsync();
    const cameraAlbum = albums.find(p => p.title === "Camera");
    medialibraryRequest.album = cameraAlbum;
    while (hasMoreData) {
      let cursor = await MediaLibrary.getAssetsAsync(medialibraryRequest);
      await populateLocationsIntoSet(cursor, markersSet);
      hasMoreData = cursor.hasNextPage;
      medialibraryRequest.after = cursor.endCursor
    }
    markersArray = [...markersSet]
    setMarkers(markersArray);
  } catch (e) {
    console.log(e)
  } finally {
    setAppReady(!hasMoreData);
  }
}

const populateLocationsIntoSet = async (
  cursor : MediaLibrary.PagedInfo<MediaLibrary.Asset>,
  markersSet : Set<MediaLibrary.Location>) => {
    const allowedTypes : MediaLibrary.MediaTypeValue[] = [
      MediaLibrary.MediaType.photo,
      MediaLibrary.MediaType.video
    ]
    const markersArray = await Promise.all(cursor.assets.map(async element => {
      if (!allowedTypes.includes(element.mediaType)) {
        return;
      }
      let image = await MediaLibrary.getAssetInfoAsync(element);
      return image.location;
    }));
    if (markersArray.length === 0) {
      return;
    }
    let nonNullLocations = markersArray.filter(p => p != undefined) as MediaLibrary.Location[];
    nonNullLocations.forEach(markersSet.add, markersSet);
}
```

We start off with requesting permissions to access the media library using `requestPermissionsAsync`. Then we iterate "Camera" album contents with the method `getAssetsAsync` that acts as a cursor obtaining items batch by batch. Since the resulting items contain only basic information about the item we need to perform additional request to obtain geolocation. We do this inside `populateLocationsIntoSet` using `getAssetInfoAsync` method.

## Displaying locations on a map
To display locations on a map I use [react-native-maps](https://www.npmjs.com/package/react-native-maps).

```jsx
const MainScreen = ({markers}: MainScreenProps) => {
    let map = useRef<MapView>(null);

    const fitAllMarkers = () => {
      const boundingBox = markers
      if (markers.length === 1) {
        markers.push({latitude: markers[0].latitude+0.1, longitude: markers[0].longitude+0.1})
      }
      map.current?.fitToCoordinates(boundingBox, {
        edgePadding: DEFAULT_PADDING,
        animated: true,
    })};

    return (
       <View style={styles.container}>
        <MapView ref={map}
          style={styles.map}
          onMapLoaded={fitAllMarkers}>
          {markers.map((item : MediaLibrary.Location) => (
            <Marker
              key={Math.random()}
              coordinate={{
                latitude: item.latitude,
                longitude: item.longitude,
              }}
              icon={require('../assets/marker.png')}>
            </Marker>
          ))}
        </MapView>
    );
  }
```
In the code above we use `MapView` component and for each item inside `markers` array we place a `Marker` on the map. Once the map is loaded we focus it on the area where the markers are set with `fitToCoordinates` method.

## Sharing your map
I guess you can never imagine the modern world without people sharing stuff on social networks. So the crucial piece of functionality I wanted to embed in my application is "Share" button.

To share the results we'll require the combination of [react-native-view-shot](https://github.com/gre/react-native-view-shot) that captures the state of our screen once the map is rendered and [expo-sharing](https://docs.expo.dev/versions/latest/sdk/sharing/) to share the screenshot we've captured.

```jsx
const captureAndShareScreenshot = async () => {
  const uri = await captureRef(map, {
      format: "png",
      quality: 1
  })
  await Sharing.shareAsync("file://" + uri);
};
```

## Checking for connection status
The downside of react native maps is that it requires internet connection to load the tiles. That's why we would like to gracefully notify user if the internet connection is present instead of displaing broken maps.

In order to probe for internet connection we'll require [NetInfo](https://docs.expo.dev/versions/latest/sdk/netinfo/).

Let's revisit `AnimatedAppLoader` component.
```jsx
const AnimatedAppLoader = () => {
  const [isConnected, setConnected] = useState(false)
  const [isConnectionProbeFinished, setConnectionProbeFinished] = useState(false)

  const fetchConnection = async () => {
    const connectionStaus = await NetInfo.fetch();
    setConnected(connectionStaus.isConnected === true)
    if (!connectionStaus.isConnected) {
      await SplashScreen.hideAsync();
    }
    setConnectionProbeFinished(true)
  }

  useEffect(() => {
     fetchConnection()
  }, [])

  const refresh = useCallback(async () => {
    await fetchConnection()
  },[])

  return (
      <View style={{ flex: 1 }}>
        {isConnected && isConnectionProbeFinished && <AnimatedSplashScreen/>}
        {!isConnected && isConnectionProbeFinished && <OfflineScreen refresh={refresh}/>}
      </View>
    )
  }
```

The point I'd like to highlight is the passing `refresh` inside the `OfflineScreen` component. Doing so allows us to perform another connection probe from inside the child component.
```jsx
const OfflineScreen = ({refresh}: any) => {
    return (
        <View style={styles.container}>
            <Text style={styles.text}>
                Locate! needs an internet connection to function properly. {'\n'}
                Refresh this page once you enable it.
            </Text>
            <Button
                icon="refresh"
                mode="contained"
                buttonColor="#E81E25"
                textColor="#FFF"
                theme={CustomTheme}
                style={styles.button}
                onPress={() => refresh()}>
                    Refresh
            </Button>
        </View>
    )
}
```

## Conclusion
Summarizing my journey I would say that React Native and Expo, in particular, are pretty convenient tools that allow you to develop mobile applications without the need to master Android/IOS specific technologies. With the use of the widely popular React framework, I've managed to develop the application I've wanted for myself for a pretty long time.

As per my personal experience publishing the application is a pretty tedious process and I'm not sure that I would like to try it again but that is the part I'll omit in this article.