Journey into Odyssey
Introduction
Inspired by map posters which people have used to pin and mark places to travel, I sought out to build a modern version of such a thing, seperate from a typical maps app to just fulfill this singular purpose. There was more that could have been done as well.
With the Journal feature, you can keep tabs of your overall pins in one convienient view. Pins can be captioned and colored to keep note of certain locations and destinations.
UI Management
Odyssey (Web):
While designing the UI was easy enough thanks to tinkering with the HTML and CSS.
For the actual Pin itself, it’s based on a vectorized version of the pin image i previously made (SVG), we do this because not only does it provide a better resolution that tailors the device but that also means that we can avoid having to store another asset in the app and it allows us to provide different colors for the pins
Every UI element was to be replaced with Widgets instead of DIVs Because the current User Interface already resembled Google’s Material Design, I also used the design language in the new code as well. Doing so made it much easier to implement the UI within a matter of fewer lines of code.
From Web to App:
In the beginning Odyssey was mean't to run on the web, while being submitted as a PWA to the Microsoft Store. But of course that couldn’t just be the end of it. The idea was then to expand it to iOS, Android and macOS. Since the app was built using HTML, CSS and JavaScript, in theory it could have been really easy to port it to the other platforms and it was. But the problem was that it didn’t feel like a native app and that wasn’t okay. If I was going to continue on this path, the app had to be written natively from scratch. The question from there was the following: what set of tools and what language was I going to use to do the conversion. Naturally, an Electron/React Native app was the step in the right direction because it was widely adopted and that it used JavaScript, it was going to be easy to reuse my old code. But ultimately I ended up switching later to Flutter. This meant that the app needed to be retooled to use Dart instead of JavaScript.
The result was a such a drastic difference, buttons from before now live in a submenu and animations and transitions are smooth and responsive, this means that certain controls weren't needed anymore.
The HTML/JavaScript version will still exist as a Web App for those who want to use Odyssey on an unsupported platform.
Memory Management
Odyssey (Web):
The biggest challenge was storing the data long term in the case of application close/restart. Service Workers could have done the trick but since the data was being saved locally instead of through a database, I turned to using cookies to saving data. Since cookies are stored as a single string, there had to be methods called to parse the string and send the values into the appropriate variables that then the functions can call
document.cookie = "PinCoor_" + MarkerCounter +"=" + e.latLng + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=None; Secure;";
document.cookie = "PinCapt_" + MarkerCounter +"=" + caption + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=None; Secure;";
document.cookie = "PinColor_" + MarkerCounter +"=" + PinColor + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=None; Secure;";
document.cookie = "Counter=" + MarkerCounter + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=None; Secure;";
for (i = 0; i
< document.cookie.split("Coor").length-1; i++){ LoadState["Coor_" + i]=decodeURIComponent(document.cookie).split("PinCoor_" + i +"=").pop();
LoadState[" Coor_" + i]=LoadState["Coor_" + i].split(";").shift();
LoadState["Capt_" + i] = decodeURIComponent(document.cookie).split("PinCapt_" + i +"=").pop();
LoadState["Capt_" + i] = LoadState["Capt_" + i].split(";").shift();
LoadState["Color_" + i] = decodeURIComponent(document.cookie).split("PinColor_" + i +"=").pop();
LoadState["Color_" + i] = LoadState["Color_" + i].split(";").shift();
LoadState["Coor_" + i] = LoadState["Coor_" + i].replace("(", "");
LoadState["Coor_" + i] = LoadState["Coor_" + i].replace(")", "");
CurrCorrX = LoadState["Coor_" + i].split(",").shift();
CurrCorrY = LoadState["Coor_" + i].split(", ").pop();
placeMarker(map, { lat: parseFloat(CurrCorrX, 10), lng: parseFloat(CurrCorrY, 10) }, LoadState["Capt_" + i], LoadState["Color_" + i]);
Later version of the Web App switched to a localStorage model instead of Cookies for better dependability.
Odyssey (App):
Because Flutter and Dart didn't support these HTML/JavaScript properties for the native app, all storage and data was stored as a SQLite database that lived in the OS's App Documents.
The memory model of Odyssey works as the following:
When a session is restored the contents of OdysseyDB.db are loaded into Pins[] to be used for the session memory, after it’s loaded, a minimized version of appendMarker() runs because we already have the contents from reversegeocoder() already loaded, we don’t want to waste time and resources trying to reverse geocode for something we already have.
Pins are stored in a separate list that ideally only the Google Maps Controller should be using. In the case we need to “Delete Last Pin” or “Clear all Pins” we touch that list then.
In Odyssey 1.3, the ability to edit and delete certain pins were added, this is done through three new functions:
remunerateState(), initDBfromState() and editPinDB()
remunerateState() works exactly how it sounds, the arrays containing the state and Google Maps Controller is set to be cleared and initStatefromDB() is ran again to refresh the state
Instead of pulling the contents from the DB into the state, the contents of the state are put into the DB, this means that whenever a pin is deleted, the IDs can be reassigned through reinputting that data back into the DB
editPinDB() simply edits the content of the PinData and DB according to given ID
Location Management
When running “Pin from Address” we actually only need to do a standard geocode to get the LatLng and just do an appendMarker() we also have to do another reverse geocode because sometimes a user only give a partial address and we rely on full address to be stored in OdysseyDB.db
“Pin My Location” and zooming into location first ask the OS for it’s physical location, we rely on full accurate location because we don’t want to zoom or pin somewhere the user isn’t. If we don’t see that location is available right there, we might have to ask for permission to get that information
_permissionGranted = await location.hasPermission();
if (_permissionGranted == prefix.PermissionStatus.denied) {
_permissionGranted = await location.requestPermission();
if (_permissionGranted != prefix.PermissionStatus.granted) {
simpleDialog(context, "No Location", "Unable to Determine Location",
"Check your Location or Privacy Settings", "error");
return;
}
if (_permissionGranted == prefix.PermissionStatus.deniedForever) {
simpleDialog(context, "No Location", "Unable to Determine Location",
"Check your Location or Privacy Settings", "error");
return;
}
}
Near By
Near By is a new feature introduced with Version 1.4 that allowed local places to be found organized by category.
The user’s current location is sent through the Google Places API and presented through a bottomSheetDialog
Certain Journal Entry attributes are pre-populated with the results for the places selected.
Journal Management
I think that this was the toughest aspect of Odyssey to work on aside from DB appending. The logical decision around this was to combine this with appendMarker() so that once the pin was created, reverse geocoding was done then after but for some reason appending on the Journal’s entry list attempts before the session’s PinData list has a chance to populate.
Pin Shapes
In the original release of Odyssey there was only one pin shape: circle.
Starting with Version 1.1, there are four additional shapes, diamond, star, heart and square.
Odyssey uses SVG based vectors to reduce the reliance on file-based assets and to have the pin's resolution scale up.
Using the new shapeHandler() function that contains all the SVG codes for all five shapes, we can have a simple variable and a set of switch cases to toggle between which SVG code is being called in the bitmapDescriptorFromSvg() function.
This function is called whenever a new pin is being appended whether by tapping the map or on restoring the state from OdysseyDB.db
SQLite DB Update
The sqlite Flutter library features the ability to do version updates to an existing database. Taking advantage of this feature allowed Version 1.1 updates and future foundational updates to OdysseyDB.db.
On detection that a current database's version number does not match the one specified, a custom function is called, and the version number is incremented.
When the function is called, a set of SQL queries to insert the missing columns and append the missing values is done to catch the current database up to date.
void _upgradeDB(Database db, int oldVersion, int newVersion) async {
//This is for upgrading from 1.0 to 1.1 and future updates
if (oldVersion
< newVersion) {
...
}
}
Odyssey is available in the App Store, Google Play Store, Microsoft Store and on the Web, the app is free for all users.
With the Journal feature, you can keep tabs of your overall pins in one convienient view. Pins can be captioned and colored to keep note of certain locations and destinations.
UI Management
Odyssey (Web):
While designing the UI was easy enough thanks to tinkering with the HTML and CSS.
For the actual Pin itself, it’s based on a vectorized version of the pin image i previously made (SVG), we do this because not only does it provide a better resolution that tailors the device but that also means that we can avoid having to store another asset in the app and it allows us to provide different colors for the pins
Every UI element was to be replaced with Widgets instead of DIVs Because the current User Interface already resembled Google’s Material Design, I also used the design language in the new code as well. Doing so made it much easier to implement the UI within a matter of fewer lines of code.
From Web to App:
In the beginning Odyssey was mean't to run on the web, while being submitted as a PWA to the Microsoft Store. But of course that couldn’t just be the end of it. The idea was then to expand it to iOS, Android and macOS. Since the app was built using HTML, CSS and JavaScript, in theory it could have been really easy to port it to the other platforms and it was. But the problem was that it didn’t feel like a native app and that wasn’t okay. If I was going to continue on this path, the app had to be written natively from scratch. The question from there was the following: what set of tools and what language was I going to use to do the conversion. Naturally, an Electron/React Native app was the step in the right direction because it was widely adopted and that it used JavaScript, it was going to be easy to reuse my old code. But ultimately I ended up switching later to Flutter. This meant that the app needed to be retooled to use Dart instead of JavaScript.
The result was a such a drastic difference, buttons from before now live in a submenu and animations and transitions are smooth and responsive, this means that certain controls weren't needed anymore.
The HTML/JavaScript version will still exist as a Web App for those who want to use Odyssey on an unsupported platform.
Memory Management
Odyssey (Web):
The biggest challenge was storing the data long term in the case of application close/restart. Service Workers could have done the trick but since the data was being saved locally instead of through a database, I turned to using cookies to saving data. Since cookies are stored as a single string, there had to be methods called to parse the string and send the values into the appropriate variables that then the functions can call
document.cookie = "PinCoor_" + MarkerCounter +"=" + e.latLng + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=None; Secure;";
document.cookie = "PinCapt_" + MarkerCounter +"=" + caption + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=None; Secure;";
document.cookie = "PinColor_" + MarkerCounter +"=" + PinColor + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=None; Secure;";
document.cookie = "Counter=" + MarkerCounter + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=None; Secure;";
for (i = 0; i
< document.cookie.split("Coor").length-1; i++){ LoadState["Coor_" + i]=decodeURIComponent(document.cookie).split("PinCoor_" + i +"=").pop();
LoadState[" Coor_" + i]=LoadState["Coor_" + i].split(";").shift();
LoadState["Capt_" + i] = decodeURIComponent(document.cookie).split("PinCapt_" + i +"=").pop();
LoadState["Capt_" + i] = LoadState["Capt_" + i].split(";").shift();
LoadState["Color_" + i] = decodeURIComponent(document.cookie).split("PinColor_" + i +"=").pop();
LoadState["Color_" + i] = LoadState["Color_" + i].split(";").shift();
LoadState["Coor_" + i] = LoadState["Coor_" + i].replace("(", "");
LoadState["Coor_" + i] = LoadState["Coor_" + i].replace(")", "");
CurrCorrX = LoadState["Coor_" + i].split(",").shift();
CurrCorrY = LoadState["Coor_" + i].split(", ").pop();
placeMarker(map, { lat: parseFloat(CurrCorrX, 10), lng: parseFloat(CurrCorrY, 10) }, LoadState["Capt_" + i], LoadState["Color_" + i]);
Later version of the Web App switched to a localStorage model instead of Cookies for better dependability.
Odyssey (App):
Because Flutter and Dart didn't support these HTML/JavaScript properties for the native app, all storage and data was stored as a SQLite database that lived in the OS's App Documents.
The memory model of Odyssey works as the following:
When a session is restored the contents of OdysseyDB.db are loaded into Pins[] to be used for the session memory, after it’s loaded, a minimized version of appendMarker() runs because we already have the contents from reversegeocoder() already loaded, we don’t want to waste time and resources trying to reverse geocode for something we already have.
Pins are stored in a separate list that ideally only the Google Maps Controller should be using. In the case we need to “Delete Last Pin” or “Clear all Pins” we touch that list then.
In Odyssey 1.3, the ability to edit and delete certain pins were added, this is done through three new functions:
remunerateState(), initDBfromState() and editPinDB()
remunerateState() works exactly how it sounds, the arrays containing the state and Google Maps Controller is set to be cleared and initStatefromDB() is ran again to refresh the state
Instead of pulling the contents from the DB into the state, the contents of the state are put into the DB, this means that whenever a pin is deleted, the IDs can be reassigned through reinputting that data back into the DB
editPinDB() simply edits the content of the PinData and DB according to given ID
Location Management
When running “Pin from Address” we actually only need to do a standard geocode to get the LatLng and just do an appendMarker() we also have to do another reverse geocode because sometimes a user only give a partial address and we rely on full address to be stored in OdysseyDB.db
“Pin My Location” and zooming into location first ask the OS for it’s physical location, we rely on full accurate location because we don’t want to zoom or pin somewhere the user isn’t. If we don’t see that location is available right there, we might have to ask for permission to get that information
_permissionGranted = await location.hasPermission();
if (_permissionGranted == prefix.PermissionStatus.denied) {
_permissionGranted = await location.requestPermission();
if (_permissionGranted != prefix.PermissionStatus.granted) {
simpleDialog(context, "No Location", "Unable to Determine Location",
"Check your Location or Privacy Settings", "error");
return;
}
if (_permissionGranted == prefix.PermissionStatus.deniedForever) {
simpleDialog(context, "No Location", "Unable to Determine Location",
"Check your Location or Privacy Settings", "error");
return;
}
}
Near By
Near By is a new feature introduced with Version 1.4 that allowed local places to be found organized by category.
The user’s current location is sent through the Google Places API and presented through a bottomSheetDialog
Certain Journal Entry attributes are pre-populated with the results for the places selected.
Journal Management
I think that this was the toughest aspect of Odyssey to work on aside from DB appending. The logical decision around this was to combine this with appendMarker() so that once the pin was created, reverse geocoding was done then after but for some reason appending on the Journal’s entry list attempts before the session’s PinData list has a chance to populate.
Pin Shapes
In the original release of Odyssey there was only one pin shape: circle.
Starting with Version 1.1, there are four additional shapes, diamond, star, heart and square.
Odyssey uses SVG based vectors to reduce the reliance on file-based assets and to have the pin's resolution scale up.
Using the new shapeHandler() function that contains all the SVG codes for all five shapes, we can have a simple variable and a set of switch cases to toggle between which SVG code is being called in the bitmapDescriptorFromSvg() function.
This function is called whenever a new pin is being appended whether by tapping the map or on restoring the state from OdysseyDB.db
SQLite DB Update
The sqlite Flutter library features the ability to do version updates to an existing database. Taking advantage of this feature allowed Version 1.1 updates and future foundational updates to OdysseyDB.db.
On detection that a current database's version number does not match the one specified, a custom function is called, and the version number is incremented.
When the function is called, a set of SQL queries to insert the missing columns and append the missing values is done to catch the current database up to date.
void _upgradeDB(Database db, int oldVersion, int newVersion) async {
//This is for upgrading from 1.0 to 1.1 and future updates
if (oldVersion
< newVersion) {
...
}
}
Odyssey is available in the App Store, Google Play Store, Microsoft Store and on the Web, the app is free for all users.
While designing the UI was easy enough thanks to tinkering with the HTML and CSS.
For the actual Pin itself, it’s based on a vectorized version of the pin image i previously made (SVG), we do this because not only does it provide a better resolution that tailors the device but that also means that we can avoid having to store another asset in the app and it allows us to provide different colors for the pins
Every UI element was to be replaced with Widgets instead of DIVs Because the current User Interface already resembled Google’s Material Design, I also used the design language in the new code as well. Doing so made it much easier to implement the UI within a matter of fewer lines of code.
From Web to App:
In the beginning Odyssey was mean't to run on the web, while being submitted as a PWA to the Microsoft Store. But of course that couldn’t just be the end of it. The idea was then to expand it to iOS, Android and macOS. Since the app was built using HTML, CSS and JavaScript, in theory it could have been really easy to port it to the other platforms and it was. But the problem was that it didn’t feel like a native app and that wasn’t okay. If I was going to continue on this path, the app had to be written natively from scratch. The question from there was the following: what set of tools and what language was I going to use to do the conversion. Naturally, an Electron/React Native app was the step in the right direction because it was widely adopted and that it used JavaScript, it was going to be easy to reuse my old code. But ultimately I ended up switching later to Flutter. This meant that the app needed to be retooled to use Dart instead of JavaScript.
The result was a such a drastic difference, buttons from before now live in a submenu and animations and transitions are smooth and responsive, this means that certain controls weren't needed anymore.
The HTML/JavaScript version will still exist as a Web App for those who want to use Odyssey on an unsupported platform.
Memory Management
Odyssey (Web):The biggest challenge was storing the data long term in the case of application close/restart. Service Workers could have done the trick but since the data was being saved locally instead of through a database, I turned to using cookies to saving data. Since cookies are stored as a single string, there had to be methods called to parse the string and send the values into the appropriate variables that then the functions can call
document.cookie = "PinCoor_" + MarkerCounter +"=" + e.latLng + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=None; Secure;";
document.cookie = "PinCapt_" + MarkerCounter +"=" + caption + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=None; Secure;";
document.cookie = "PinColor_" + MarkerCounter +"=" + PinColor + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=None; Secure;";
document.cookie = "Counter=" + MarkerCounter + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=None; Secure;";
for (i = 0; i
< document.cookie.split("Coor").length-1; i++){ LoadState["Coor_" + i]=decodeURIComponent(document.cookie).split("PinCoor_" + i +"=").pop();
LoadState[" Coor_" + i]=LoadState["Coor_" + i].split(";").shift();
LoadState["Capt_" + i] = decodeURIComponent(document.cookie).split("PinCapt_" + i +"=").pop();
LoadState["Capt_" + i] = LoadState["Capt_" + i].split(";").shift();
LoadState["Color_" + i] = decodeURIComponent(document.cookie).split("PinColor_" + i +"=").pop();
LoadState["Color_" + i] = LoadState["Color_" + i].split(";").shift();
LoadState["Coor_" + i] = LoadState["Coor_" + i].replace("(", "");
LoadState["Coor_" + i] = LoadState["Coor_" + i].replace(")", "");
CurrCorrX = LoadState["Coor_" + i].split(",").shift();
CurrCorrY = LoadState["Coor_" + i].split(", ").pop();
placeMarker(map, { lat: parseFloat(CurrCorrX, 10), lng: parseFloat(CurrCorrY, 10) }, LoadState["Capt_" + i], LoadState["Color_" + i]);
Later version of the Web App switched to a localStorage model instead of Cookies for better dependability.
Odyssey (App):
Because Flutter and Dart didn't support these HTML/JavaScript properties for the native app, all storage and data was stored as a SQLite database that lived in the OS's App Documents.
The memory model of Odyssey works as the following:
When a session is restored the contents of OdysseyDB.db are loaded into Pins[] to be used for the session memory, after it’s loaded, a minimized version of appendMarker() runs because we already have the contents from reversegeocoder() already loaded, we don’t want to waste time and resources trying to reverse geocode for something we already have.
Pins are stored in a separate list that ideally only the Google Maps Controller should be using. In the case we need to “Delete Last Pin” or “Clear all Pins” we touch that list then.
In Odyssey 1.3, the ability to edit and delete certain pins were added, this is done through three new functions:
remunerateState(), initDBfromState() and editPinDB()
remunerateState() works exactly how it sounds, the arrays containing the state and Google Maps Controller is set to be cleared and initStatefromDB() is ran again to refresh the state
Instead of pulling the contents from the DB into the state, the contents of the state are put into the DB, this means that whenever a pin is deleted, the IDs can be reassigned through reinputting that data back into the DB
editPinDB() simply edits the content of the PinData and DB according to given ID
Location Management
When running “Pin from Address” we actually only need to do a standard geocode to get the LatLng and just do an appendMarker() we also have to do another reverse geocode because sometimes a user only give a partial address and we rely on full address to be stored in OdysseyDB.db“Pin My Location” and zooming into location first ask the OS for it’s physical location, we rely on full accurate location because we don’t want to zoom or pin somewhere the user isn’t. If we don’t see that location is available right there, we might have to ask for permission to get that information
_permissionGranted = await location.hasPermission();
if (_permissionGranted == prefix.PermissionStatus.denied) {
_permissionGranted = await location.requestPermission();
if (_permissionGranted != prefix.PermissionStatus.granted) {
simpleDialog(context, "No Location", "Unable to Determine Location",
"Check your Location or Privacy Settings", "error");
return;
}
if (_permissionGranted == prefix.PermissionStatus.deniedForever) {
simpleDialog(context, "No Location", "Unable to Determine Location",
"Check your Location or Privacy Settings", "error");
return;
}
}
Near By
Near By is a new feature introduced with Version 1.4 that allowed local places to be found organized by category.
The user’s current location is sent through the Google Places API and presented through a bottomSheetDialog
Certain Journal Entry attributes are pre-populated with the results for the places selected.
Journal Management
I think that this was the toughest aspect of Odyssey to work on aside from DB appending. The logical decision around this was to combine this with appendMarker() so that once the pin was created, reverse geocoding was done then after but for some reason appending on the Journal’s entry list attempts before the session’s PinData list has a chance to populate.Pin Shapes
In the original release of Odyssey there was only one pin shape: circle.
Starting with Version 1.1, there are four additional shapes, diamond, star, heart and square.
Odyssey uses SVG based vectors to reduce the reliance on file-based assets and to have the pin's resolution scale up.
Using the new shapeHandler() function that contains all the SVG codes for all five shapes, we can have a simple variable and a set of switch cases to toggle between which SVG code is being called in the bitmapDescriptorFromSvg() function.
This function is called whenever a new pin is being appended whether by tapping the map or on restoring the state from OdysseyDB.db
SQLite DB Update
The sqlite Flutter library features the ability to do version updates to an existing database. Taking advantage of this feature allowed Version 1.1 updates and future foundational updates to OdysseyDB.db.
On detection that a current database's version number does not match the one specified, a custom function is called, and the version number is incremented.
When the function is called, a set of SQL queries to insert the missing columns and append the missing values is done to catch the current database up to date.
void _upgradeDB(Database db, int oldVersion, int newVersion) async {
//This is for upgrading from 1.0 to 1.1 and future updates
if (oldVersion
< newVersion) {
...
}
}
Odyssey is available in the App Store, Google Play Store, Microsoft Store and on the Web, the app is free for all users.