cuhacking / atlas Goto Github PK
View Code? Open in Web Editor NEWThe map project - codename Atlas
The map project - codename Atlas
Add a Mapbox MapView
to the Android app's main activity centered around the Carleton University campus. This should be implemented in the android
module as a plain native Android view.
Add a layer with a random feature visible on the map.
This map will be used to test implementations of Mapbox classes and functions in future tasks.
Version needs to be updated in Versions.kt and in Podfile (possibly elsewhere, need to double check when this gets worked on).
Also need to check for and resolve any build/compatibility issues... Hopefully these are minimal, if there are any at all ๐ค.
Note: As of this issue's creation date, current target is 13.7.
Upgrade is important since the project is currently broken for anyone developing on Windows because of KT-40834 which was fixed in 1.4.10
Blocked by:
The map and search bar are currently fed separate test data. This should be removed and replaced with the actual source of data: FeatureApi
. The API will need to be made use of to download test data from the development server, and then stored in the DataCache
and Database
.
The application (on all platforms) must check and/or download data at launch. This will involve launching a coroutine that calls the appropriate API methods and ensures that any downloading or other I/O operations happens in the "background". Some care may need to be paid attention to in order to make sure that the correct CoroutineScope
is used on each platform to avoid memory leaks.
Once the launching function^ is written, it should be as simple as calling it in the correct place on each platform. On Android this can be initiated in the onCreate()
method of the AtlasApplication
class. On iOS this needs to be handled in the UIApplicationDelegate
protocol. In JS, this needs to be called when the application is created (possibly in App.tsx
?).
Extend the remote api class from #21 to include another function that makes a HEAD
request to the same endpoint, then return the parsed value (using a date/time library) of the Last-Modified
header of the response.
Set up integration with the Mapbox SDK for both the Android app (android
module) as well as for the Kotlin Multiplatform Mapbox bindings (mapbox
module).
For the Android app module, a Mapbox API Key should be read in from the local.properties
file and included as a build config field in the common
module so that it can be accessed from each platform frontend. (You can use BuildKonfig for this). The mapbox
module does not require an API key or any extra setup.
Create the project, it will be a gradle project with the following modules:
Automate the building and deployment of the web app for internal testing.
Add the Ktor client library to the project. Be sure to follow the instructions for adding the library to a Multiplatform project.
Look under the buildSrc
folder to see how dependencies are being managed in the project.
Along with #21 which downloads map data and inserts it into the database to be indexed for searching, we also want to cache the full set of data so that it can be loaded directly onto the map. To do this, on Android and iOS it should be saved as a file on-device, and in JS it can just be kept in memory (it only needs to exist as long as the web page is open).
Create a new class using the expect/actual mechanism that contains a function that will handle reading and writing JSON data.
actual
implementations can simply call the native methods to read and write files.This class should also track the last time the cached data was updated. This will be used to determine whether or not the cache should be updated by comparing the cache's time with the Last-Modified
header of the remote API response. This will require adding a dependency to a date/time library, such as this one.
Finally, update the data repository class to write data to this cache when the data is updated.
We'll need some way to create instances of the Database to use. expect
and actual
won't actually be the best for accomplishing most of this because of a key difference required for the Android Driver to work.
My recommendation is to implement an extension function similar to this one which will help create a database instance given an instance of a driver. Then, we'll focus on creating the driver instances separately.
Create an expect
ed function called provideDbDriver
that returns a SqlDriver
and then provide actual
implementations in each platform sourceset. Then add a global lateinit var
called database
that will store the result of provideDbDriver
later on.
Essentially, at some point during the application's initialization on each platform, an instance of the database will need to be created. The entire application will have **only one instance ** of the database to share throughout the entire codebase, i.e. the database will be a singleton. There are much nicer ways of handling this including many fancy libraries and frameworks, but we won't touch those for now because a) everyone on the team needs more experience first and b) there are no perfect solutions for Kotlin Multiplatform yet, but it's mostly a.
The key difference for the Android Driver that I mentioned above is that it needs a Context. For the time being, create a global lateinit var
called appContext
that will store a Context
and use that to create the driver. This solution is far from pretty, but it's the best we can do for the time being.
In Android apps, there is an Application
class that can handle initialization of the app process. Create a subclass of the Application
class called AtlasApp
and override the onCreate
to initialize the previously created database
variable. I'll leave the magic spell to initialize appContext
as a learning exercise for you.
This driver doesn't require anything special, but the database still needs to be initialized when the app process starts similar to the Android app.
Since SQLDelight isn't publishing artifacts for the JS driver yet, it's hard for us to test. I recommend trying to build the SQLDelight driver locally as described here, but if that doesn't work just create an implementation of the expected functions with a TODO
The json
column in the feature
table will hold GeoJson strings, specifically for Features. In our code, we would like to handle these json strings as objects instead, and using SQLDelight's custom column types we can have these strings automatically convert themselves to and from their string representations so that we can use them as objects in our code.
In this case, the json
column should be updated to store values as Feature
objects from Spatial K.
json
colum to the Feature
class from Spatial K.ColumnAdapter
in common code.Finalize the UI designs for the Android, iOS, and Web apps. Mockups should be included and should cover the colours, shapes, typography, and icons to be included in the final design.
The scope of this task is to design the UI components themselves. The design of the map itself will be covered by another task.
Use a JSON schema to validate map data when loading rather than trying to parse it. Using a schema can provide better error messages to indicate why the data wasn't parsed.
In addition, the command line component of the server can include a mode to test the data without actually starting the server.
e.g. ./server.jar --test data.json
could validate the data and then terminate.
Requires #85 to be complete first.
Finalize an initial schema for the map data for use in development and testing.
The schema should be represented as a JSON schema that can be used to validate the data set.
Create the base of a class hierarchy for representing Layers in Mapbox. Attributes that are common to all types of layers include an id
, source
, filter
(an expression), layout
(expressions), paint
(expressions), minzoom
and maxzoom
, and others listed in the Mapbox docs.
This should be an abstract, expected class in the mapbox
module's commonMain
sourceset.
Provide actual
implementations for Android and JS, as with the GeoJsonSource.
Now that Kotlin 1.4 is stable, upgrade to it and update all required dependencies.
This is currently blocked by:
Once these dependencies are upgraded appropriately:
Write a Dockerfile to generate a docker image for the web server.
Create actions to deploy the image to the dev server for testing.
Automate the building of the Android App and publish the app internally for testing.
Add the SQLDelight library and plugin to the project.
The plugin will need to be applied to the root project (in the project folder's build.gradle.kts
file).
The library will need to be added to the common
module (in that folder's build.gradle.kts
).
Look under the buildSrc
folder to see how dependencies are being managed in the project.
Build the UI for the search feature, and then following the MVVM pattern, connect it with the existing search database.
For this task, there is the option between using Android's layout system, or making use of Jetpack Compose (which is very new).
The end result should look something like this:
where the empty space in the background will be filled by the map.
Using a bottom sheet, display some info a room that has been selected either on the map, or through the search results in the search bar.
Implement a GeoJson data source in the mapbox
module's common code and then provide implementations for Android and JS. The implementations can be based off of the implementations in mmapp.
In common Kotlin code, we can declare a GeoJsonSource
like so: val source = GeoJsonSource(data)
.
When this common declaration is imported to platform-specific view (i.e. to use in the view to add to the Map), we would like to be able to treat the source
object as though it were already a platform-specific instance of the data source. In other words, we would like to be able to say that our source
is a com.mapbox.mapboxsdk.style.sources.GeoJsonSource
when referenced in Android code, and that our source
matches the contract for a data source for use in the mapbox-gl-js library. (i.e. has type: "geojson"
and data
properties). Examples of how this works can be found in the mmapp project.
Add a map to the iOS app's main screen centered around the Carleton University campus.
This map should be added using SwiftUI. Mapbox has a good tutorial on integrating the Mapbox MapView with Swift UI. The Annotations section can be omitted, however you should add a layer to the map with a random feature visible.
The Mapbox access token should be provided programmatically by importing it from the Common
framework.
This is blocked by #3
Using FTS4, implement full-text search in the database. FTS requires a virtual table that should mirror the existing "Features" table created in an earlier task. Any item that is inserted into the main features table should also be inserted into the virtual table.
Not all values from the original table need to be mirrored. For instance, the json
values from the original table are basically useless for FTS.
Create a "search" query that returns all rows that match a given query string, as well as another query to clear the virtual table.
Test the FTS implementation on the JVM.
common_name
of "River Building", the "search" query with "river" as an argument should return that row.We may choose to use FTS5 in the future, but this will require more development work for a later time.
Since we want to treat this module as a library (that will eventually be split off), enable strict mode which is a new feature of Kotlin 1.4 to ensure the module follows library writing best-practices.
Blocked by #25
Now that data is being downloaded and stored in the database, display it on the map. This will replace the test data currently declared in-code.
#57 implements a class that handles caching map data, however, no testing have been done to confirm that it functions properly on iOS targets.
The tests must ensure that the JSON data can be both written to and read from the cache.
Add the Spatial K snapshots for geojson
and turf
to the common
module.
Mapbox's layers use style expressions to control how data appears on the map. These expressions allow the appearance of features on the map to be driven by data from the features themselves, or from the map itself.
In order to effectively make use of this capability in a Kotlin Multiplatform project, we want to create bindings for these expressions so that they can be created in Kotlin but then be used in the SDK for each respective platform.
Our goal is to create a Kotlin DSL for building these expressions that might look something along the lines of this:
paint {
fillColor("#00ffff")
fillOpacity(0.5f)
}
The result (return value) of this building would be something that could then be used in other Multiplatform bindings for the layers themselves that would then be translated/adapted to the platform-specific SDK calls to construct those expressions at run-time.
The DSL is an abstraction. Each platform implements the Mapbox style spec in a very different way and our job is to unify them into a single interface (the DSL) for building them.
On Android, the style expressions are built using static factory methods that return an Expression
object. When adding them to a layer, they are a passed to a variable argument function. This is the most similar to what we are looking to create, however we can make use of more Kotlin syntactic sugar to make the interface more appealing while also providing better IDE autocomplete (those static factory methods are a pain for the IDE to find because there's no scoping on them, unlike with Kotlin DSLs).
The Kotlin abstraction will need to map to those factory methods in the platform-specific implementation details.
In JS, the mapbox style spec is represented using a object where property names are mapped to values.
iOS is once again different in implementing the style specification. It uses a "standard" NSExpression
class for modelling style expressions which makes use of many strings instead of array-based operators like in Android and on JS.
The Kotlin DSL will need to map to these expressions.
The goal isn't to create a 1-to-1 mapping of the Kotlin DSL to each platform's implementation since they are all too different for this to be possible. Instead, we are trying to create an intermediate representation of the expressions that can then be converted to the platform-specific structures at runtime.
The mmapp project is a good reference for this since the data source for GeoJSON data was implemented on all three platforms. Each one was implemented in a very different way.
Set up integration with the Mapbox SDK for both the iOS app project (through XCode) as well as for the Kotlin Multiplatform bindings (mapbox
module). You can use the mmapp project as a reference on how to set up the Mapbox SDK with the Kotlin Multiplatform module.
When setting up the Mapbox SDK in the iOS app, do not include a Mapbox API key in the Info.plist
file. The API key should instead be set programatically and will come from a build config field that will be configured as part of #2.
This database will be built using SQLDelight and will be a SQLite database.
The map data table will require the following columns:
Name | Type | Description |
---|---|---|
fid |
integer (not null) | Unique ID for each feature |
common_name |
text (not null) | A common name for a feature |
secondary_name |
text | An (optional) secondary name for a feature |
type |
text (not null) | A type classifier for each feature |
building |
text | Two letter code for the building this feature is in |
floor |
text | A code that indicates what floor of a building |
search_tags |
text | Other text that can be indexed for searching |
json |
text (not null) | The JSON object for this feature |
The following queries will need to be added:
fid
valueUsing a Ktor client, implement a GET request that downloads some JSON data.
This function should be encapsulated in a class. The class should have two constructor properties: one for the Database
, and one for an HttpClient
. These properties will be supplied by other parts of the code later on.
The server endpoint that the request will be made to should be configured via the local.properties
file similar to how our mapbox keys are currently set up. Add a string field to the buildkonfig
setup to read in some url. For the scope of this task, it doesn't matter what this url is since we won't be testing with an actual server.
The returned JSON data will be a FeatureCollection
following the GeoJson standard. You can use the Spatial K library to convert this JSON text into an object.
Using the queries and table created in #13, insert each Feature
into the table by mapping each Spatial K feature to a database Feature. Here's how the features from the FeatureCollection map to the database for now:
Name | Mapping |
---|---|
fid |
Get from Feature properties |
common_name |
Use name property from props |
secondary_name |
Set to null for now |
type |
Use type property from props |
building |
Use building property from props, or null if not available |
floor |
Use floor property from props, or null if not available |
search_tags |
Use the name property from props for now |
json |
Convert the Feature back to a string. (This will be done by an adapter once #18 is complete.) |
I recommend creating a separate function for this. An extension function would be good for this purpose.
Using Ktor's mock client(s), write tests to ensure that data is correctly downloaded and inserted into the database. I recommend waiting until #18 is finished so that you can reuse some of the database test code.
Write a simple server that will serve a single static JSON file.
The endpoint must also specify a Last-Modified
header containing the last time the file was updated.
The server should be written with Ktor.
This task covers the design/aesthetic of the map itself. This includes the colours, typography, and icons of various features that would be situated on a map including both outdoor and indoor features.
Current entire words need to be matched for results to show up in the results list. The search should autocomplete as the user is typing.
Note that this task is almost identical to #19, but specifically for iOS. This task will make use of the same common expect
declarations as #19.
Using the common GeoJson data source in the mapbox
module's common code, provide the implementation for iOS. The implementations can be based off of the implementations in mmapp.
When the common declaration of a GeoJsonSource
is imported to platform-specific view (i.e. to use in the view to add to the Map), we would like to be able to treat the source
object as though it were already a platform-specific instance of the data source. In other words, we would like to be able to say that our source
is a MGLShapeSource
when referenced in iOS code. This is the same as in #19. However, there are currently limitations in how Kotlin is allowed to subclass Objective-C classes, so this isn't possible.
Instead, in the actual
implementation of the GeoJsonSource
, you have to keep an instance of a MGLMapSource
and then update that instead. This instance will then be exposed to the iOS code instead.
So instead of:
let source = GeoJsonSource(...)
mapView.style?.addSource(source)
we'll have:
let source = GeoJsonSource(...)
mapView.style?.addSource(source.internalSource)
although the name "internalSource
" can be changed.
actual
implementations of this class for iOS.GeoJsonSource
with some random feature and then add them to the map that was added in #16.Add a map that fills the entire screen centered around the Carleton University campus. This should be implemented through React and in TypeScript.
Add a layer with a random feature visible on the map.
This map will be used to test implementations of Mapbox classes and functions in future tasks.
This task is very similar to cuhacking/atlas-editor#1, but the difference will be where the Mapbox access token comes from. In that task, it came from a .env
file, but here it will come from the compiled Kotlin/JS code.
Before starting, make sure to follow the build instructions in the README. You will need to make sure you run gradlew build
otherwise the compiled JavaScript won't be visible and yarn will complain.
Because we are using TypeScript, we need TypeScript definitions for the compiled Kotlin/JS code. In Kotlin 1.4 the compiler will automatically generate the definitions for us, but we're stuck on 1.3.70 for the time being... so we have to write them manually. This was already done in 4016cb4, but nonetheless the way you import these definitions is a little strange since Kotlin namespaces all of the compiled code.
You can see this in index.tsx
under the web directory, like this:
import { com } from "Atlas-common";
import BuildKonfig = com.cuhacking.atlas.common.BuildKonfig;
// Use it here
BuildKonfig.MAPBOX_KEY
This constant was compiled from Kotlin code, how cool is that??
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.