Collect interesting places, and pin them in your map like a fridge magnet!
This app shows you where all cities, counties, states, and countries your photos were taken. Each time you input a photo into the app, it will search for the city, county, state, and country, and then plot them on a map.
- This project contains a submodule library. Clone this project using the
--recurse-submodules
param in order to clone together with the library:
git clone --recurse-submodules [email protected]:giovanischiar/fridgnet.git
or
git clone --recurse-submodules https://github.com/giovanischiar/fridgnet.git
- If the project was already cloned, run this command:
git submodule update --init
-
Go to Google Maps library and get a map API key. Go here to learn how to create a API maps key.
-
Create an
apikey.properties
file on the root of the project containing:
MAPS_API_KEY=/* Your Google Maps API key. */
- Open the project using Android Studio.
The documentation for the whole project (public artifacts 100% documented) is available here
- The user sends the photos to the application using the Photo Picker.
- The application extracts the GPS coordinates for each photo and sends them to the Geocoder.
- The Geocoder finds the address for each GPS coordinate and returns to the application.
- The application extracts the names of all the different levels of administrative regions (city, county, state, and country) for each address and sends each one to the Nominatim API.
- The Nominatim API returns a JSON containing the list of coordinates that mark the outline of each level of administrative region.
- The application plots the coordinates of each administrative region and connects them, forming polygons, using Google Map Components to show them to the user.
Screenshot | Description |
---|---|
![]() |
Sometimes, there are locations that have exclaves. Take a look at San Francisco. As you can see, the Farallon Islands are part of San Francisco. However, the map becomes too small when including these islands. To hide the islands on the map, go to the MapScreen and click on the San Francisco territory. |
![]() |
This screen is the PolygonsScreen ; it allows you to hide exclaves by clicking on the checkbox in the upper corner of each map. You can uncheck them one by one or click on switch all to check or uncheck all of them at once. |
![]() |
Now the HomeScreen shows the mini-map of San Francisco without taking into account those islands. Therefore, it appears bigger than before. This change will also apply to both the PhotosScreen and MapScreen . |
![]() |
You can do it even with countries, counties, or states, like, for example, the United States. You'll be surprised at how many overseas territories a country has. |
Technology | Purpose |
---|---|
Jetpack Compose |
Design UI |
Geocoder |
Convert coordinates into addresses |
Nominatim |
Retrieve coordinates of the outline of administrative regions |
Room |
Cache Nominatim JSONs data, and persist application data |
GSON |
Convert Nominatim JSON data into Kotlin objects |
IconCreator |
Generate application Icon (my own library) |
-
The challenge was to handle the JSON response format that Nominatim returns when searching for a location. When you search for a location to get its polygon coordinates, it returns them using the GeoJSON format. Among other types, this application recognizes these 4:
-
{ "type": "Point", "coordinates": [1.23, 14.42] }
This type is straightforward; it's only a single coordinate that follows the format
[longitude, latitude]
. All of the following types also use this same coordinate format. -
{ "type": "LineString", "coordinates": [[2, 9], [4, 2], [5, 3], /* ... */] }
This type is an array of
Point
. It is used to return the coordinates of streets. -
{ "type": "Polygon", "coordinates": [ [[0, 0], /*...*/, [0, 0]] // This first array is the polygon that represents the outermost polygon. First and last coordinates must be the same [[0.1, 0.1], /*...*/, [0.1, 0.1]] // any subsequent arrays in this list are handled as holes inside the polygon /* ... */ ] }
This type is one of the main ones used in the application to draw the outline of cities, counties, states, and countries. This type considers that the location is only one closed polygon with possible holes within, where the first list represents the coordinates of the polygon itself while the other ones represent the possible inside holes.
-
This other type is used to draw locations that contain more than one polygon, like the United States, which has Alaska and Hawaii as outlying states. It's an array of
Polygon
.
Although only
Polygon
andMultipolygon
are used to plot locations on the map, there were times when the API returnedPoint
orLineString
, making me have to handle those types as well. The issue was that thecoordinates
field had a variable type, so I had to learn how to create a custom JSON deserializer when converting the JSON into Kotlin objects. -
- The application, especially the 'MapScreen', mainly consists of rendering polygons on a map. These polygons are a list of several coordinates outlining a region of the map when those coordinates are connected to each other. As many polygons are rendered on the map, the heavier the calculations the app would need to do, making the app very slow. That's when the classical Computer Graphics method called clipping comes in handy. It prevents the app from rendering unnecessary polygons that the screen is not presenting at the moment. To make this clipping algorithm happen, I started drawing on paper all the possible cases where a polygon shouldn't be rendered because it is not visible on the screen. Let's take a look at the digitized (and enhanced) version:
As you can see, the big rectangle at the center is the visible area of the map at the moment; I call it bounds
. To simplify, instead of comparing each coordinate of each polygon against the coordinates of bounds
, I compare its southeast
and northeast
coordinates. Let's take a closer look at what those rectangles mean.
@Test // 26
fun `Polygon with southwest and northeast different from bounds south of bounds`() { /* .. */ }
For each polygon, I calculated its boundingbox
, which consists of its southwest
and northeast
coordinates. These coordinates create a box that encloses the polygon. Each boundingbox
around bounds
is numbered, and then for each one, I wrote a test labeled after its relative position to bounds
. Here's a slightly modified excerpt of PolygonsOutsideBoundsTest.kt showing that each unit test corresponds to a numbered boundingbox
drawn on the diagram:
/* ... */
@Test // 1
fun `Polygon with southwest latitude equals bounds southwest latitude west of bounds`() {/* ... */}
@Test // 2
fun `Polygon with southwest latitude equals bounds southwest latitude east of bounds`() {/* ... */}
@Test // 3
fun `Polygon with southwest longitude equals bounds southwest longitude south of bounds`() {/* ... */}
@Test // 4
fun `Polygon with southwest longitude equals bounds southwest longitude north of bounds`() {/* ... */}
@Test // 5
fun `Polygon with southwest latitude equals bounds northeast latitude west of bounds`() {/* ... */}
@Test // 6
fun `Polygon with southwest latitude equals bounds northeast latitude east of bounds`() {/* ... */}
@Test // 7
fun `Polygon with southwest longitude equals bounds northeast longitude south of bounds`() {/* ... */}
@Test // 8
fun `Polygon with southwest longitude equals bounds northeast longitude north of bounds`() {/* ... */}
/* ... */
The tests in this file only cover whether the algorithm correctly returns false
for those polygons outside of bounds
. Another file covers the opposite case. There are also other tests that cover even more scenarios; for example, how the algorithm will behave if the antimeridian is visible? Or what if there is a polygon that crosses that meridian? Every case was thoroughly considered, and its files are inside the boundingbox folder in the tests.
Please check this repository to learn more about the notation I used to create the diagrams in this project.
This diagram shows all the packages the application has, along with their structures. Some packages are simplified, while others are more detailed.
These diagrams illustrate the relationship between screens from view
and viewmodel
classes. The arrows from the View Models represent View Data objects (classes that hold all the necessary data for the view to display), primitives, or collections encapsulated by State Flows, which are classes that encapsulate data streams. Every update in the View Data triggers the State Flow to emit these new values to the view
, and the view updates automatically. Typically, the methods called from screens in view
to classes in viewmodel
trigger these changes, as represented in the diagram below by arrows from the view
screens to viewmodel
classes.
View Datas are classes that hold all the data the view
needs to present. They are created from model
classes and served by View Models to the view
. This diagram represents all the associations among the classes in the view.viewdata
.
View Models serve the view
with objects made from view.viewdata
classes, collections, or primitive objects encapsulated by State Flows. This diagram represents all the associations among the classes in viewmodel
and view.viewdata
.
View Models also serve as a façade, triggering methods in model.repository
classes. This diagram shows that each View Model has its own repository class and illustrates all methods each View Model calls, represented by arrows from View Models to Repositories.
Model classes handle the logic of the application. This diagram represents all the associations among the classes in the model
.
These diagrams represent all the associations among the classes in model.repository
and model
.
Data Sources provide their repositories with all the needed data for the application. They contain modules that make requests to the Nominatim API and consult the database. This diagram represents all the associations among the classes in model.repository
, model.datasource
, and library
.
- Fix Bugs:
- The Geocoder library sometimes doesn't get the address of the locations right, and the Nominatim API sometimes doesn't return the right outline for the location. A solution would be to let the user search and correct the location.
- The
PhotosScreen
displays a map with photos pinned at their respective coordinates, along with a grid of all the photos. It is accessed by clicking on the mini-map at theHomeScreen
. However, it now only functions when clicking on a city map. It does not work when changing the current level of administrative region on the dropdown located at the top of theHomeScreen
to a county, state, or country. - In the hide exclave feature, all the exclaves that belong to a city may also belong to its county, which belongs to the state, which belongs to the country. When you hide the exclave from a city, although on the
HomeScreen
the exclave from the city is hidden, it doesn't get hidden from the other levels of the administrative region. Thus, on theMapScreen
, these exclaves are still showing. This happens because the app is not prepared for an exclave to belong to multiple locations.
- Use the date of each photo to show not only where but also when the photo was taken.
- Create a dark mode.
- Although unit tests were created to test the clipping, there are many other tests I'd like to create for this application.