Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
FikriMilano committed Jan 29, 2024
1 parent e353865 commit d395a2b
Show file tree
Hide file tree
Showing 10 changed files with 543 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2021-2024 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.geowidget.model

import java.math.BigDecimal

data class GeoWidgetLocation(
val id: String = "",
val name: String = "",
val position: Position? = null,
val context: Context? = null,
)

data class Position(
val latitude: BigDecimal = BigDecimal(0),
val longitude: BigDecimal = BigDecimal(0),
)

data class Context(
val type: String = "", // Group, Patient, Healthcare Service
val reference: String = "", // the reference of 'type'
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
* Copyright 2021-2024 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.geowidget.screens

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.MultiPoint
import com.mapbox.geojson.Point
import com.mapbox.mapboxsdk.Mapbox
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
import com.mapbox.mapboxsdk.geometry.LatLngBounds
import com.mapbox.mapboxsdk.maps.MapView
import com.mapbox.mapboxsdk.maps.Style
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource
import com.mapbox.turf.TurfMeasurement
import io.ona.kujaku.utils.CoordinateUtils
import java.util.LinkedList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.smartregister.fhircore.geowidget.BuildConfig
import org.smartregister.fhircore.geowidget.R
import org.smartregister.fhircore.geowidget.model.GeoWidgetLocation
import timber.log.Timber

private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"

class NewGeoWidgetFragment : Fragment() {
private var param1: String? = null
private var param2: String? = null

val geoWidgetViewModel by activityViewModels<NewGeoWidgetViewModel>()

private lateinit var mapView: MapView
private var geoJsonSource: GeoJsonSource? = null
private var featureCollection: FeatureCollection? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
Mapbox.getInstance(requireContext(), BuildConfig.MAPBOX_SDK_TOKEN)
return setupViews()
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
geoWidgetViewModel.featureCollectionFlow.collect { featureList ->
val featureCollection = FeatureCollection.fromFeatures(featureList)
this@NewGeoWidgetFragment.featureCollection = featureCollection
zoomToPointsOnMap(featureCollection)
}
}
}

private fun setupViews(): LinearLayout {
val toolbar = setUpToolbar()
mapView = setUpMapView()

return LinearLayout(requireContext()).apply {
orientation = LinearLayout.VERTICAL
addView(toolbar)
addView(mapView)
}
}

private fun setUpMapView(): MapView {
return MapView(requireActivity()).apply {
id = R.id.kujaku_widget
val builder = Style.Builder().fromUri("asset://fhircore_style.json")
getMapAsync { mapboxMap ->
Timber.i("Get Map async finished")
mapboxMap.setStyle(builder) { style ->
Timber.i("Finished setting the style")
geoJsonSource = style.getSourceAs("quest-data-set")
if (geoJsonSource != null && featureCollection != null) {
Timber.i("Setting the feature collection")
geoJsonSource!!.setGeoJson(featureCollection)
}
}
}
}
}

private fun setUpToolbar(): View {
return Toolbar(requireContext()).apply {
popupTheme = R.style.AppTheme
visibility = View.VISIBLE
navigationIcon =
ContextCompat.getDrawable(context, androidx.appcompat.R.drawable.abc_ic_ab_back_material)
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 168)
setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.colorPrimary))
setNavigationOnClickListener { findNavController().popBackStack() }
}
}

private fun zoomToPointsOnMap(featureCollection: FeatureCollection?) {
featureCollection ?: return

val points = LinkedList<Point>()
featureCollection.features()?.forEach { feature ->
val geometry = feature.geometry()
if (geometry is Point) {
points.add(geometry)
}
}

if ((featureCollection.features()?.size ?: 0) == 0) return

val bbox = TurfMeasurement.bbox(MultiPoint.fromLngLats(points))
val paddedBbox = CoordinateUtils.getPaddedBbox(bbox, 1000.0)
val bounds = LatLngBounds.from(paddedBbox[3], paddedBbox[2], paddedBbox[1], paddedBbox[0])
val finalCameraPosition = CameraUpdateFactory.newLatLngBounds(bounds, 50)

mapView.getMapAsync { mapboxMap -> mapboxMap.easeCamera(finalCameraPosition) }
}

override fun onStart() {
super.onStart()
mapView.onStart()
}

override fun onResume() {
super.onResume()
mapView.onResume()
}

override fun onPause() {
super.onPause()
mapView.onPause()
}

override fun onStop() {
super.onStop()
mapView.onStop()
}

override fun onDestroy() {
super.onDestroy()
mapView.onDestroy()
}

override fun onLowMemory() {
super.onLowMemory()
mapView.onLowMemory()
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
}

fun addLocation(location: GeoWidgetLocation) {
geoWidgetViewModel.addLocation(location)
}

companion object {
@JvmStatic
fun newInstance(param1: String, param2: String) =
NewGeoWidgetFragment().apply {
arguments =
Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}

fun builder() = Builder()
}
}

class Builder {

// todo: might be a good place to add callbacks

fun build(): NewGeoWidgetFragment {
return NewGeoWidgetFragment()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2021-2024 Ona Systems, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.smartregister.fhircore.geowidget.screens

import androidx.lifecycle.ViewModel
import com.mapbox.geojson.Feature
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import org.json.JSONObject
import org.smartregister.fhircore.engine.util.DispatcherProvider
import org.smartregister.fhircore.geowidget.model.GeoWidgetLocation
import org.smartregister.fhircore.geowidget.util.extensions.getGeoJsonGeometry

@HiltViewModel
class NewGeoWidgetViewModel @Inject constructor(val dispatcherProvider: DispatcherProvider) :
ViewModel() {

private val _featureCollectionFlow: MutableStateFlow<MutableList<Feature>> =
MutableStateFlow(mutableListOf())
val featureCollectionFlow: StateFlow<List<Feature>> = _featureCollectionFlow

fun addLocation(location: GeoWidgetLocation) {
val jsonFeature =
JSONObject().apply {
put("type", "Feature")
put("properties", JSONObject().apply { put("name", location.name) })
put("geometry", location.getGeoJsonGeometry())
}
val feature = Feature.fromJson(jsonFeature.toString())

_featureCollectionFlow.update {
it.add(feature)
it
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import org.hl7.fhir.r4.model.Location
import org.json.JSONArray
import org.json.JSONObject
import org.smartregister.fhircore.geowidget.KujakuFhirCoreConverter
import org.smartregister.fhircore.geowidget.model.GeoWidgetLocation

typealias Coordinate = Pair<Double, Double>

Expand Down Expand Up @@ -86,6 +87,24 @@ fun Location.getGeoJsonGeometry(): JSONObject {
return geometry
}

fun GeoWidgetLocation.getGeoJsonGeometry(): JSONObject {
position ?: return JSONObject()

val geometry = JSONObject()

geometry.put("type", "Point")
geometry.put("coordinates", JSONArray(arrayOf(position.longitude, position.latitude)))

// Boundary GeoJson Extension geometry overrides any lat, long declared in the Location.lat
// & Location.long
// if (hasBoundaryGeoJsonExt) {
// val featureFromExt =
// Base64.decodeBase64(boundaryGeoJsonExtAttachment!!.data).run { JSONObject(String(this)) }
// return featureFromExt.getJSONObject("geometry")
// }
return geometry
}

fun generateLocation(featureJSONObject: JSONObject, coordinates: Coordinate): Location {
val (longitude, latitude) = coordinates

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ sealed class MainNavigationScreen(
object Profile :
MainNavigationScreen(titleResource = R.string.profile, route = R.id.profileFragment)

object GeoWidget : MainNavigationScreen(route = R.id.geoWidgetFragment)
object GeoWidgetLauncher : MainNavigationScreen(route = R.id.geoWidgetLauncherFragment)

object Insight : MainNavigationScreen(route = R.id.userInsightScreenFragment)

Expand Down
Loading

0 comments on commit d395a2b

Please sign in to comment.