Mobile Example App
An explanation of how the open source Recce application works
Mobile Example App
This documentation is out of date. Please see https://github.com/eegeo/eegeo-example-app
The example app is intended to convey best practices for interacting with the eeGeo platform in the context of a complex client application. The example app is presented in publicly available source code form for iOS and Android platforms. This page presents key concepts to aid in understanding the code. A glossary of terms is first presented to identify concepts used throughout this document, then the high-level implementation of the various individual aspects of the application are explained.
In this section:
- Glossary of terms
- Structure and architecture
- Tactile menu UX
- Category and free-text search
- Visualising Points of Interest
- Dynamic weather and season control
- Environment flattening
- Animated camera transitioning
- Background preloading of environment resources
- Initial user experience
- Code repository
Glossary of terms
The terms introduced in this section will be used throughout this document, and correspond to the terminology used to describe the implementation of the concepts or features in the source code. Images from the application will be used as illustration.
The default view of the application looks like:
- Primary Menu. The primary menu is located on the left-hand side of the screen, and can be extended right to expose a collection of options. The primary menu contains a button to display an About Page.
- Secondary Menu. The secondary menu is located on the right-hand side of the screen, and can be extended left to expose a collection of options.
- Compass. The compass widget reflects the orientation of the camera in the map.
- Flatten Button. The flatten button is a stateful control which enables and disables the environment flattening.
After performing a search, the application looks like:
- Search Result Menu. The search result menu is located on the bottom of the screen, and can be extended upward to expose a collection of search results. The search result menu is only present when a search is active. The on-screen menu tab displays the search term and category icon, the number of results, an activity indicator to inform if a search refresh is occurring, and a close button. The close button clears the active search, dismissing the search result menu in the process.
- Search Result On Map. Only one search result can be in focus and visually represented with a detailed display, but all search results are visualised. Non-focussed search results on map are represented by instances of World Pins. The world pins can be selected to show detailed search result information. The world pins handle updating their screen position to reflect the appropriate terrain height, and draw in the correct order from back to front from the perspective of the camera. If a search result is in focus, the detail widget is drawn above the search result on map visualisation (such that it also adjusts appropriately to the terrain height).
After extending the primary menu, the application looks like:
- The Primary Menu contains a single Menu Option; the About Page Menu Option. This option interacts with the About Page module, triggering the about page view to open. This menu option also closes the primary menu.
After extending the secondary menu, the application looks like:
- The Secondary Menu contains three Menu Sections; these menu sections allow interaction with the Search, Weather Menu and Place Jumps modules respectively. When selected, each of these sections will expand to show the menu section contents (or contract if already expanded).
After expanding the weather menu, the application looks like:
After extending the search menu, the example_app_search_menu_view looks like:
- Unlike the primary and secondary menus, the Search Result Menu is dynamically populated with content loaded from an external search web service (the deCarta REST API. The menu is only present on screen when a Search Query is active; this is modelled by the Search Query Performer. The search query performer performs a search query in response to a search event generated either explicitly by user interaction, or indirectly by the meeting of a refresh condition in the Search Refresh Service. The search query performer delegates to an instance of ISearchService to perform the search (a deCarta implementation is provided). The search result service is notified when results are received, and it adds them to the Search Results Repository, which is observed by a Menu Model. As search results are added to the repository, corresponding instances of Search Result Item Model are created and added to the menu. These items are removed from the menu and cleaned up when the corresponding search results are removed from the search result repository. When a search result item is selected, a Camera Transition is performed to take the user to the location of the search result. This transition will smoothly animate the camera to the location if it is sufficiently nearby.
After extending the primary menu and selecting the about page button, the application looks like:
- The About Page displays some information about eeGeo along with attributions to our data suppliers. The about page also contains useful information about the version of the eeGeo platform that the application was build against; the platform version number and hash, which are useful when reporting problems or errors to eeGeo.
After selecting a search result on map, the application looks like:
- Upon selecting a Search Result On Map, the SearchResultPoi dialog is displayed. The search result POI contains more detailed information about the point of interest, including its name, category, address, telephone number and web site, collected from the search service. If this information is not available it will be omitted from the display. The dialog scrolls if required. If a phone number is available, it will be presented as a link which if selected will result in the number being called. Similarly, the web site is presented as a link which if selected will open in a web browser.
After loading during the first application run, the application looks like:
- The first time the application is loaded, the Initial Experience ceremony is triggered.
Structure and architecture
The application architecture adheres to a regular, modular structure. The source is divided into shared components, and platform specific components. The cross platform module components reside under the src directory, and includes application domain models, view models, and generic services and controllers. The platform specific module components reside under a dedicated directory for the platform, such as for Android and iOS. Cross platform module components typically contain platform specific view implementations, and may depend on cross platform abstractions.
On Android there are concurrency concerns that developers must be aware of when interacting with eeGeo SDK types. Types in “SdkModel” namespaces are Domain Model types that interact with the eeGeo SDK, or have collaborators that exist with the eeGeo SDK. On Android, these execute on the “Native Thread”. Types in “View” namespaces are View types that, on Android, execute on the “UI” thread. Additional information can be found in the Concurrency Concerns on Android documentation. Some types cross this concurrency boundary, such as SearchResultModel however these types should be immutable. Prefer to pass copies of Data via Data Transfer Objects via Messaging across this concurrency boundary.
Each namespace typically defines a Module for the namespace (e.g., FlattenButtonModule, SearchModule). This module class must be constructed with appropriate dependencies, and will allocate persistent services and models associated with the namespace. These are not singleton types, and any number can be created; however, in the example app, a single instance of each module is stored as a member of either the cross platform core application type, or in a platform specific application host type such that its lifetime can be appropriately managed.
The core application type, MobileExampleApp, is a cross platform class responsible for constructing all cross platform application domain modules. The application also constructs and maintains an EegeoWorld instance for interacting with the eeGeo SDK. Each application module constructor must be provided with its dependencies; typically eeGeo SDK types, types from lower level application modules, or both.
In addition to the core app class, each platform has an AppRunner class (see Android, iOS) and an AppHost class (see Android, iOS). The AppRunner is responsible for generic activities unrelated to the application domain, such as responding to operating system events, constructing a display, etc. The AppRunner constructs an AppHost instance. The AppHost is responsible for constructing the MobileExampleApp instance, as well as platform specific application domain objects, such as native view types. These native view types may depend on services or models extracted from cross platform modules (e.g., see the FlattenButtonViewModule for Android, iOS).
Interaction between components generally follows a consistent pattern; and pure abstract class is used to present an interface to high level components, which can be consumed and stored as a member variable (preferably as a reference if copy semantics are not required, else as a pointer). Lifetime management of high level components is conventionally the responsibility of the module which constructs them; the taking of a dependency does not confer ownership. If appropriate, models or services present observable interfaces such that dependencies may be notified of state changes. An example of an abstract class based interface to a model is the IFlattenButtonModel; the implementation satisfies the subscription methods on the interface using the CallbackCollection object, for tracking subscribers requiring notification of state changes. This interaction pattern is prevalent through the example application.
View components are placed in namespaces which are the same as the cross platform component which they are responsible for the presentation of. For example, the cross platform FlattenButton has a corresponding view namespace for presentation concerns for Android and iOS.
Controllers in an MVC sense are cross-platform in the example app, for example FlattenButtonController. A pure virtual interface for the View (in an MVC sense) also exists in the cross-platform portion, for example IFlattenButtonView. There are then two specialisations of this view, one for iOS that takes the form of the Objective C interop, and one for Android that takes the form of the C++/JNI interop.
Unless explicitly stated, platform services presented by the eeGeo SDK are non thread-safe. Please see the Concurrency Concerns on Android documentation for additional information about how to manage this on Android.
Category and free-text search
The example app presents a location based search feature, which demonstrates several concepts:
- Interaction with an external search web service.
- Parsing results of a search query.
- Observation of a data repository of search results by internal components.
- Dynamically updating a persistent query based on current location.
- Visualising a simplified representation of the results of a query in the 3D environment, and interacting with such a visualisation such that a more detailed representation can be presented.
The core search component code resides in the Search namespace. The Search module requires dependencies to make web requests, and to observe the camera. Like all modules, the search module wires up its various concrete components internally, and presents them externally via abstractions. This means that, while the current implementation uses the deCarta REST API as an external data provider, all other code outside of this module is unaware of this, and remains insulated from dependencies on a particular search vendor. The module also constructs a Search Query Performer to represent the currently active query, and a Search Refresh Service to update the currently active query.
The deCarta search component implementations reside in a deCarta namespace under the main search namespace. This namespace consists of DecartaSearchService and DecartaSearchJsonParser. DecartaSearchService is an implementation of ISearchService which issues search queries to the deCarta REST API. DecartaSearchJsonParser is an implementation of DecartaSearchJsonParser, which parses a serialised representation of search results into a collection of SearchResultModel instances.
There are two forms of search query; a free text based query, and a category based query. An instance of a search query is represented by the SearchQuery type, which encapsulates the query string, the location, the search radius and the search category (if applicable). Search categories are modelled in the CategorySearch namespace. A search category is a first-class entity implemented as CategorySearchModel, which encapsulates the category name, the category search string representation, and an icon name. Category search model objects are constructed in the CategorySearchModule; this is currently inline, but could trivially be data-driven. The categories are based on the deCarta Category Search REST API.
A search query is initially issued by user interaction, either by the user selecting a search category icon from the secondary menu search menu section, or by the user entering a free text query into the search box in the secondary menu. A dependency is taken by the view controller types responsible for these UI elements on the ISearchQueryPerformer interface, such that the controller can issue searches based on user input.
The SearchQueryPerformer implementation takes the raw free text or category string and constructs a SearchQuery based on the current camera location and altitude (as the search radius increases proportionally with camera altitude). The SearchQueryPerformer delegates the work of performing the search to the ISearchService implementation. Once a response has been received (or the query has timed out), the SearchQueryPerformer will receive a callback. The callback arguments are the query instance, and the collection of parsed results. The SearchQueryPerformer performs a stable merge of the new results into the old results, such that updating a search will not disrupt the still-available results of the prior search, if the results overlap.
After a search, the SearchQueryPerformer adds any new results to the ISearchResultRepository, an observable collection of search results. Any old results no longer present are removed from the repository. Other types may observe the state of the search result repository, such as the search result menu.
The SearchQueryPerformer retains the previous search query, and maintains state to indicate if a query is currently active. This is used by the SearchRefreshService, a controller type which is ticked each frame by the application. If the search query service has an active query, the SearchRefreshService will re-issue a new query if the camera becomes sufficiently far away from the active query issue location. This causes new search results to stream in as the user moves around the world, which will be merged into the current result set by the SearchQueryPerformer.
The visualisation of the search results is discussed in the next section.
Visualising Points of Interest
The WorldPins namespace contains the types responsible for the application domain modelling of 3D in-world sprites. The WorldPinsModule consumes the PinRepository, PinController, and EnvironmentFlatteningService types from the eeGeo SDK. These types allow the WorldPinsModule to make use of the pins rendering functionality provided by the eeGeo SDK.
The WorldPinsService is the interface through which the application adds world pins; the WorldPinsService animates the scale of the pins depending on screen location, such that pins near the edges of the screen are scaled down. The WorldPinsService also adjusts the pins to accommodate the environment flattening. Each pin’s state is modelled via the WorldPinItemModel, which can encapsulate data for each pin instance; this could be used if pins were to have a non-uniform scale, based on some importance weighting.
WorldPins are not associated with any particular application feature. The mapping to specific features, such as the search results is implemented by constructing a world pin for each feature pin and mapping them in a domain model for the feature. The SearchResultOnMapModel performs this mapping for the search results. The SearchResultOnMapModel observes the search result repository, adding and removing world pins mapped to SearchResultOnMapItemModel instances. The SearchResultOnMapItemModel implements the IWorldPinSelectionHandler; this is called when the world pin is clicked by the user.
This model abstracts the generic behaviour of pins (such as scaling when near the edges of the screen) from the domain specific behaviour (such as opening the detail dialog for a search result).
Dynamic weather and season control
The weather (and season and time of day) can be modified by selecting an option from the secondary menu weather menu section.
The following scene shows the weather menu:
The following scene shows the same view after selecting Snow:
The types concerned with dynamic weather reside in the WeatherMenu namespace. The WeatherMenuModule takes dependencies on the ICityThemesService and ICityThemesUpdater eeGeo SDK types such that the current environment theme can be modified. The module also takes a dependency on an IFileIO instance such that configuration for the menu section can be loaded. This configuration is a JSON document which maps the theme which a menu option will change to its presentation string and icon; this document is embedded into the Android and iOS applications.
The module instantiates a WeatherController object which can be used to change the environment theme via an interface which allows the setting of the time of day, weather or season. An empty MenuOptionsModel is also constructed to contain the menu options. The WeatherMenuModule then parses the JSON to determine how many menu options to add. For each menu option parsed form the JSON, a corresponding element is added to the aforementioned MenuOptionsModel, with an appropriately configured WeatherMenuStateOption. Each WeatherMenuStateOption is mapped to a particular item from the configuration document, so will have a presentation string, icon and associated WeatherMenuStateModel which is used on selection of the menu option to modify the environment theme via the WeatherController.
The module exposes an interface to the populated MenuOptionsModel, which is added to the secondary menu by the application initialisation
As described in the structure section, when a menu option is selected, the Android view implementation of the WeatherMenu must dispatch its interactions to the platform thread in order to safely interact with the eeGeo SDK.
The FlattenButton module implements environment flattening, which collapses the environment such that roads are easier to see and are not occluded by 3D building or terrain features.
The following scene shows a non-flattened environment:
The following scene shows the same view with the environment flattened:
The FlattenButtonModel is an application domain model; it presents the interface to the rest of the application, such that the application is insulated if the underlying eeGeo SDK EnvironmentFlatteningService type changes in a future SDK update. While we currently only wish to control flattening with the flatten button widget, if other application events triggered flattening then they should interact with the FlattenButtonModel rather than the eeGeo SDK type EnvironmentFlatteningService.
Internally, The FlattenButtonModel subscribes to events published by the EnvironmentFlatteningService. By hooking into these events and publishing them via the FlattenButtonModel observable API, observers can be notified if some event outside of the application domain (such as an eeGeo SDK service) causes the environment to become flattened, and can update their state appropriately.
The flatten button widget has no specific view state; the only state which informs the presentation of the view is the model state (i.e., is the environment currently flat?). The flatten button does however react to other openable controls, so its view model implements the screen control interface.
Like the WeatherMenu, the Android view implementation of the FlattenButton must dispatch its interactions to the platform thread in order to safely interact with the eeGeo SDK.
Animated camera transitioning
Camera transitions are used to move smoothly between search results selected in the search result menu. This is implemented in the CameraTransitions namespace. The CameraTransitionController object is updated each frame by the application update loop, which controls the current transition state if a transition is in progress. The CameraTransitionController interpolates between the current and target locations and headings over a predefined transition duration.
In order to begin, end, or query the state of a transition, classes can take a dependency on the ICameraTransitionController interface. This is demonstrated by the SearchResultItemModel, which starts a transition to its associate search result on selection.
The compass widget reflects the orientation of the camera in the map. By pressing the compass, GPS Follow Mode can be enabled such that the application will go to your location. Pressing the compass when in GPS follow mode will enable GPS Compass Mode, in which the application camera will rotate to face the direction of the device.
The compass code resides in the Compass namespace. As usual, the CompassModule specifies the dependencies necessary for the configuration of its concrete implementations, and exposes them to the rest of the application as interfaces. The CompassModel wraps the eeGeo SDK type NavigationService. The CompassModel presents a readable interface to the current heading angle, and allows the GPS Compass mode to be changed (and such changes to be observed). GPS Compass mode are implemented by mutating the GpsGlobeCameraController eeGeo SDK type, which is used as the camera controller for the application.
Background preloading of environment resources
The WorldAreaLoader namespace contains types concerned with asynchronously loading sections of the environment while the application is running. The WorldAreaLoaderModule takes a dependency on the eeGeo SDK PrecacheService type, which is wrapped and presented via the application domain model interfaceIWorldAreaLoaderModel.
The IWorldAreaLoaderModel is observable, such that subscribers can be informed when a preload completes or is cancelled. The region to preload is specified as an instance of eeGeo SDK type IStreamingVolume. An implementation of this interface is implemented as part of the initial experience; PreloadSphereVolume.
Initial user experience
The first time the application is run, an Initial Experience ceremony is triggered. This is modelled as a state machine, such that states are entered sequentially; these states are referred to as Initial Experience Steps. Only a single step is configured in the example application, the Initial Experience Preload Model. This step uses the World Area Loader to preload an area of the world asynchronously while the application is running. The initial experience module consumes the Persistent Settings module to persist the state for which steps have been completed, such that they will not be entered again. Both the preload initial experience step and the persistent settings interact with native OS components (in order to generate a native pop-up alert, and to access the platform persistent key-value stores respectively). Preload initial experience steps are implemented for Android and iOS. A persistent settings implementation also exists for Android and iOS.