Ver un mapa en tiempo real las posiciones de conductores

En este ejemplo integro Firebase Realtime y google maps para poder ver en tiempo real las posiciones, de conductores para este ejemplo, y ver algunos datos básicos en un popup al clicar sobre sus pines.

Mejoras pendientes:
– Centrar el mapa por click
– Mostrar el icono del pin en la dirección del movimiento del conductor
– Cambiar el icono del conductor dependiendo de su tipo
– …

Primero mostraremos un mapa.

Ir a la página de google para una guía rápida https://developers.google.com/maps/documentation/javascript/overview

Para agregarlo a mi página copie los scripts

<script
  src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBIwzALxUPNbatRBj3Xi1Uhp0fFzwWNBkE&callback=initMap&libraries=&v=weekly"
  defer
></script>

Agregr el div para el mapa

<div id="map"></div>

Y colocar al final el llamado a la carga del mapa

let map;

function initMap() {
  map = new google.maps.Map(document.getElementById("map"), {
    center: { lat: -34.397, lng: 150.644 },
    zoom: 8,
  });
}

Errores:

Error RefererNotAllowedMapError:
Al colocarlo como está, obtengo el error RefererNotAllowedMapError en consola, parece que se tiene que autorizar la url desde donde se llamara al mapa.

Solución:
Para restringirlo encontré en la documentación https://developers.google.com/maps/documentation/javascript/get-api-key, que configurando la api key se puede restringir desde donde se solicita.
Pueden ir directamente a cada paso desde este link: https://developers.google.com/maps/documentation/javascript/get-api-key#restrict_key

  1. Crear o escoger un proyecto en Google Cloud Console ( crear: https://console.cloud.google.com/projectcreate )
  2. Ir a las credenciales ( https://console.cloud.google.com/apis/credentials ) y crear credenciales, escoger “Clave Api”

Luego click en “Restringir Clave”, luego en el panel seleccionar “URL de referencia” y abajo agregar los sitios desde donde se llamara.

Mas abajo en restricciones api, seleccionar map api, que no aparecerá, la documentación dice que se debe habilitar.
Habilitar Map Api, buscandolo en la biblioteca de apis y habilitarlo

Volviendo al paso anterior ya aparece api maps, y seleccionarlo.

Luego copiar el api key en la llamada al script. ( puede demorar en hacer efecto 5 minutos )

<script
            src="https://maps.googleapis.com/maps/api/js?key=COPIAR_AQUI&callback=initMap&libraries=&v=weekly"
            defer
    ></script>

Error: No muestra el mapa
Es posible que este error solo sea por los estilos de mi sitio, el problema es que al cargar carga por defecto el div map con los estilos:

    position: relative;
    overflow: hidden;

Gracias a https://stackoverflow.com/users/2384642/shiv-singh con los estilos , cambiando el script de inicio y estilos logra mostrarse el mapa.

<div id="map" style="width: 100%;
  height: 400px;
  margin-bottom: 15px;
  border: 2px solid #fff;">
....
<script>
let map;

        function initMap() {
            console.log("cargo");

            var mapOptions = {
                center: new google.maps.LatLng(-12.048905, -76.968220),
                zoom: 15,
                mapTypeId: google.maps.MapTypeId.HYBRID,
                scrollwheel: true,
                draggable: true,
                panControl: true,
                zoomControl: true,
                mapTypeControl: true,
                scaleControl: true,
                streetViewControl: true,
                overviewMapControl: true,
                rotateControl: true,
            };


            map = new google.maps.Map(document.getElementById("map"),mapOptions);


        }
</script>

El texto “For development purpose only” según la documentación se quitara una ves se habilite la facturación.

Listo. Ya vemos un mapa.

Lo siguiente es conectar a firebase realtime para escuchar la posición de cada conductor.

La logica es:

  • Escuchar la ruta principal del nodo de posiciones ( esto variará de cómo hizo el esquema no relacional cada uno).
  • Obtener el nodo raíz , que en nuestro caso es el ID y los dos nodos hijos que son las pociones Latitud y Longitud.
  • Agregar estos datos a un array o matriz, o actualizarlo en caso exista.
  • Según el array también se sabe si ya se agrego un marcador en el mapa, el cual se agrega o actualiza.
  • Listo.

Empezamos.

Escuchar la ruta principal del nodo de posiciones ( esto variará de cómo hizo el esquema no relacional cada uno).

Agregamos los script para firebase realtime database

 
  <!-- Insert these scripts at the bottom of the HTML, but before you use any Firebase services -->

  <!-- Firebase App (the core Firebase SDK) is always required and must be listed first -->
  <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-app.js"></script>

  <!-- Add Firebase products that you want to use -->
  <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-auth.js"></script>
  <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-firestore.js"></script>
  <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-database.js"></script>

Configuramos Firebase

  <script>
    // TODO: Replace the following with your app's Firebase project configuration
    var firebaseConfig = {
      apiKey: "??",
     // authDomain: "??",
      databaseURL: "??",
      projectId: "??",
      storageBucket: "??",
      appID: "??",
    };

    // Initialize Firebase
    firebase.initializeApp(firebaseConfig);
</script>

Para obtener apiKey, ir a configuración y luego General>”Clave api de la web”

Para databaseurl , ir a realtime database>datos>click en clip y copiar la url de la base de datos

projectId, es el ID del projecto, que esta en configuración>general>Id Del Proyecto.

Con la configuración ya podemos escuchar los cambios en la base de datos, colocando el siguiente script

var logref = firebase.database().ref('RUTA_DEL NODO');
           logref.on('value', function(snapshot) {
               console.log("Entro");
               console.log(snapshot.val())
           });

obtenemos una referencia al nodo y despues imprimimos los datos,

Para este caso no se obtuvo errores en la consola.

Siguiente:

Obtener el nodo raíz , que en nuestro caso es el ID y los dos nodos hijos que son las pociones Latitud y Longitud.

Ya obtenemos el valor de firebase, obtener los otros datos depende de como lo esquematizaron, en mi caso, tengo que parsear el nodo raíz y dos hijos:

logref.on('value', function(snapshot) {
            console.log("Entro");
            console.log(snapshot.val());

            $driverIds = Object.getOwnPropertyNames(snapshot.val());

            $driverIds.forEach( function(valor, indice, array) {
                $lat = snapshot.val()[valor].position.lat;
                $lon = snapshot.val()[valor].position.lon;
                $idDriver = valor.replace("driver_", "");

                console.log($lat,$lon,$idDriver);
            });
        });

List, ahora.

Agregar estos datos a un array o matriz, o actualizarlo en caso exista.

Analizando esto va depende como referencia los marcadores, con eso implementado decidiré como guardarlos.

Según el array también se sabe si ya se agrego un marcador en el mapa, el cual se agrega o actualiza.

Para agregar un markador, siguiendo el tutorial de google maps https://developers.google.com/maps/documentation/javascript/markers#add , con los datos que obtuvimos de lat lon y iddriver

$myLatLng = { lat: $lat, lng: $lon };

                new google.maps.Marker({
                    position: $myLatLng,
                    map,
                    title: "Conductor "+$idDriver
                });

Si se deja como esta, cuando se actualiza la posición aparece otro marcador

para eso asociamos el marcador a una hashmap, creamos un hashmap y creamos una función que se encarga de validar si existe o no.

var markers = {};
        function addMarker($id,$lat,$lon){
            $idDriver = 'driver_'+$id;

            if($idDriver in markers){
                console.log("existe");
                //existe
                var latlng = new google.maps.LatLng($lat,$lon);
                markers[$idDriver].setPosition(latlng);
            }
            else{
                console.log("no existe");
                //no existe
                $myLatLng = { lat: $lat, lng: $lon };

                markers[$idDriver] = new google.maps.Marker({
                    position: $myLatLng,
                    map,
                    title: "Conductor "+$idDriver
                });
            }
        }

**Como el mapa carga asincronamente es posible que firebase cargue primero, para eso firebase cargara despues de que inicie el mapa, moviendo un poco el orden quedaría asi.

<!--MAPS-->
    <script src="https://polyfill.io/v3/polyfill.min.js?features=default"></script>
    <script
            src="https://maps.googleapis.com/maps/api/js?key=XXXXXX&callback=initMap&libraries=&v=weekly"
            defer
    ></script>


    <!--FIREBASE-->
    <!-- Insert these scripts at the bottom of the HTML, but before you use any Firebase services -->

    <!-- Firebase App (the core Firebase SDK) is always required and must be listed first -->
    <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-app.js"></script>

    <!-- Add Firebase products that you want to use -->
    <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-auth.js"></script>
    <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-firestore.js"></script>
    <script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-database.js"></script>

    


    <!--INI FIREBASE-->
    <script>
        var markers = {};
        function addMarker($id,$lat,$lon){
            $idDriver = 'driver_'+$id;

            if($idDriver in markers){
                console.log("existe");
                //existe
                var latlng = new google.maps.LatLng($lat,$lon);
                markers[$idDriver].setPosition(latlng);
            }
            else{
                console.log("no existe");
                //no existe
                $myLatLng = { lat: $lat, lng: $lon };

                markers[$idDriver] = new google.maps.Marker({
                    position: $myLatLng,
                    map,
                    title: "Conductor "+$idDriver
                });
            }
        }

        function configurarFirebase() {

            var firebaseConfig = {
                apiKey: "XXXXXX",
                databaseURL: "XXXXXX",
                projectId: "XXXXXX"
            };

            // Initialize Firebase
            firebase.initializeApp(firebaseConfig);


            var logref = firebase.database().ref('xxxxxx/position');
            logref.on('value', function(snapshot) {
                console.log("Entro");
                console.log(snapshot.val());

                $driverIds = Object.getOwnPropertyNames(snapshot.val());

                $driverIds.forEach( function(valor, indice, array) {
                    $lat = snapshot.val()[valor].position.lat;
                    $lon = snapshot.val()[valor].position.lon;
                    $idDriver = valor.replace("driver_", "");

                    console.log($lat,$lon,$idDriver);
                    addMarker($idDriver,$lat,$lon);

                });


            });
        }



    </script>
    <!--FIN FIREBASE-->


    <!--INI MAPA-->
    <script>
        let map;

        function initMap() {
            console.log("cargo");

            var mapOptions = {
                center: new google.maps.LatLng(-2.048905, -6.968220),
                zoom: 15,
                mapTypeId: google.maps.MapTypeId.HYBRID,
                scrollwheel: true,
                draggable: true,
                panControl: true,
                zoomControl: true,
                mapTypeControl: true,
                scaleControl: true,
                streetViewControl: true,
                overviewMapControl: true,
                rotateControl: true,
            };
            
            map = new google.maps.Map(document.getElementById("map"),mapOptions);
            
            configurarFirebase();

        }

    </script>
    <!--FIN MAPA-->

Problemas con repositorio privado de Bitbucket en Laravel

Si al ejecutar para agregar los repositorios configurados en composer.json en un proyecto Laravel

composer update

se obtiene un error de la carga del repositorio si poner obtenerlos desde bitbucket por la url git

Failed to update https://..javier.git, package information from this repository may be outdated

Es posible que falte configurar una clave SSH en las configuraciones personales de bitbucket

Primero generar una clave ssh del entorno local con el comando

ssh-keygen -t rsa

Luego ir a bitbucket > personal settings

bitbucket personal settings

https://bitbucket.org/account/settings/

Click en SSH Keys

bitbucket personal setting en SSH Keys

https://bitbucket.org/account/settings/ssh-keys/

Click en Add Keys

bitbucket Click en Add Keys

Copiar la llave y colocar un nombre del archivo pub

bitbucket Copiar la llave y colocar un nombre

Para obtener la KEY puede seguir las instrucciones que muestra, o desde la terminal lanzar el comando

sudo vi ../.ssh/id_rsa.pub

y copiar el contenido del archivo, click en “Add key”

Luego ejecutar

composer update

y debería cargar las librerías privadas desde bitbucket identificados por ssh al entorno local ya acceder a los repositorios privados.

Figma to Android , Exportar icono desde Figma para proyecto android ( svg )

En Figma, seleccionar el icono y en el menú derecho inferior buscar la opción exportar, escoger el formato y click en exportar.

Si es como un archivo no vector , se puede pegar tal cual o redimencionar para cada formato

Una web que puede servir según para que se use es

https://romannurik.github.io/AndroidAssetStudio/index.html

Luego para importar la imagen como vector, desde android ir a la carpeta drawable ( click derecho )> New > Vector Asset

Seleccionar local, configurar y finalizar

Debe quedar un archivo xml mostrando el icono ,y se puede usar como drawable.

Kotlin Koin inyección con parámetros

koin

Para instalar y configurar un ejemplo básico de ejemplo puede verlo en la documentación https://start.insert-koin.io/#/quickstart/android-java

Luego surge la pregunta de cómo hacerlo si al momento de construir necesita parámetros del lugar donde se instancia , como el caso del Presenter(iView), donde al instancia el presenter se debe pasar la vista que implementa la interfaz de la vista que usa el presenter.

Modificando un poco el ejemplo se puede hacer así.

Para mi caso, la clase Presenter y interfaz en kotlin es:

class PedidosPresenter(var view: IPedidosView) {
    fun sayHello () {
        view.sayHello()
    }
}

interface IPedidosView
{
    fun sayHello()
}

sayHello, solo es un método para comprobar que view es el fragment que debe referenciar.

En mi caso el fragment esta en Java, mas abajo coloco el como es el mismo código en kotlin.

public class PedidosFragment extends Fragment implements IPedidosView {

    private Lazy<PedidosPresenter> pedidosPresenter = KoinJavaComponent.inject(PedidosPresenter.class, null , () -> DefinitionParametersKt.parametersOf(this)); //se coloco THIS porque el fragmento es quien implemento IPedidosView

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
       
        View view = inflater.inflate(R.layout.fragment_home_pedidos, container, false);
        pedidosPresenter.getValue().sayHello();
         /**more code**/
        return view;
    }

    @Override
    public void sayHello() {
        Log.i("KOIN","Oh yes");
    }
 /**more code**/
}

Y la configuración de los módulos:

object KoinModules
{
    @JvmStatic
    fun getModules(): Module {
        return module {
            single { (view : IPedidosView) -> PedidosPresenter(view) }
        }
    }
}

En resumen, en la configuración de módulos se indica que parámetros recibirá

single { (view : IPedidosView) -> PedidosPresenter(view) }

y en la inyección, se debe agregar como tercer parámetro un mapeo de los parámetros que necesita.

private Lazy<PedidosPresenter> pedidosPresenter = KoinJavaComponent.inject(PedidosPresenter.class, null , () -> DefinitionParametersKt.parametersOf(this)); //se coloco THIS porque el fragmento es quien implemento IPedidosView

Las demás configuraciones son igual al de la guía.

Referencias:

Inyección con parámetros en kotlin
https://github.com/InsertKoinIO/koin/blob/master/koin-projects/docs/reference/koin-core/injection-parameters.md

Issue del tema en caso deseen reabrirlo
https://github.com/InsertKoinIO/koin/issues/62

Github de Koin
https://github.com/InsertKoinIO/koin

Kodein opción a Koin en caso no se pudiera
https://www.kotlindevelopment.com/koin-vs-kodein/

Articulo de Clean arquitecture por el cual surgió la duda
https://devexperto.com/clean-architecture-android/

Link de referencia que revise

Usar librería común entre dos proyectos Android

Un problema cuando se tiene dos aplicaciones del mismo proyecto que usan cosas comunes como el : estilo del app, iconos, textos, api rest, pero por ser dirigidas a diferentes roles tienen funcionalidades distintas, algunas soluciones, dirigidas a rehusar código o minimizar repetir trabajo:

  1. Usar flavor para cada tipo de rol y separarlas
  2. Usar proguard para que según una condición no compile algunos módulos
  3. Usar android app bundle para cargar las partes según demanda
  4. Usar librería común por path
  5. Usar aar común
  6. etc..

No son todas las opciones, ni las mejores, solo las que analice, dependiendo del problema alguno puede ser mejor, en mi caso:

  1. Tendría varios flavor y manejar las constantes seria un poco trabajoso, ademas de la demora de compilación por todo el código en una app
  2. Agrega mas tiempo de compilación.
  3. Seria una idea a probar si la app fuera de cero, pero ya esta avanzada y tendría que refactorizarla, lo cual si esta bien estructurada no seria muy complejo pero si trabajoso, pero es también el tiempo de probar nuevamente cada módulo.
  4. Es la que escogí ya que como esta en desarrollo, los cambios que haga en una serán reflejadas en la otra fuente sin tener que reimportar el modulo.
  5. Seria una solución si los recursos comunes ya estaría casi en su versión final o final.

Entonces aplicando la solución 4 se podría hacer así:

Escoger los recursos comunes : rest api, sql, estilos, views, cadenas, traducciones, colores, layouts, constantes de servidor, iconos, raws, librerias comunes, etc.

Mover los recursos comunes en una librería, en mi caso lo llame common

Estructura del proyecto

Luego agregarlo como referencia en build.gradle

apply plugin: 'com.android.application'
.....
dependencies {
    ....
    //LIBRARY COMUN
    implementation project(':common')
}

Luego en los demás proyectos que usen la librería se debe importar por dirección de la carpeta, esto se hace desde el archivo settings.gradle

include ':app',':common'
project(':common').projectDir = new File( '/ruta/a/carpeta/common')

Y en build.gradle, agregarlo igual que en el proyecto original

apply plugin: 'com.android.application'
.....
dependencies {
    ....
    //LIBRARY COMUN
    implementation project(':common')
}

Estilo para la documentación

En cabecera de tema header.php

<head>
	<meta charset="<?php bloginfo( 'charset' ); ?>" />
	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
	<link rel="profile" href="http://gmpg.org/xfn/11" />
	<link rel="pingback" href="<?php bloginfo( 'pingback_url' ); ?>" />
	<!--[if lt IE 9]>
	<script src="<?php echo get_template_directory_uri(); ?>/js/html5.js" type="text/javascript"></script>
	<![endif]-->
	<?php wp_head(); ?>
	
	
	
	
	
	<?php
$host = $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI'];
	if (strpos($host, 'blog.puntoycomalab.com/docs/') === 0 && strlen($host) > strlen('blog.puntoycomalab.com/docs/')) {
		echo '
		
		<style type="text/css">
		/*para que ocupe el doc todo la pantalla*/
		.site {
			background-color: #ffffff;
			/* margin: 80px auto; */
			padding: 0 40px;
			max-width: 100% !important; 
			margin-bottom: 0;
			margin-top: 20px !important; 
		} 
		
		/*Para los que tiene articulos*/
		#post-255 > div > div.article-child.well > h3{
		    margin-top: 0px !important; 
    		margin-bottom: 15px !important; 
		}
		
		
		/*para que muestre las paginas del menu*/
		.site-header {
    text-align: center !important;
    margin-left: 0 !important;
    display: block !important;
    width: 100% !important;
    position: relative !important;
    margin-bottom: 0 !important;
}

.single .site-header-info {
    display: none !important;
}

.main-navigation li {
    display: inline-block !important;
    padding: 5px 10px !important;
    width: auto !important;
}

#masthead .site-navigation {
    border-bottom: 1px solid #eee !important;
    margin-top: 0 !important;
    padding-bottom: 20px !important;
    margin-bottom: 20px !important;
}

#masthead .main-navigation ul {
    margin-left: -10px !important;
}

.main-navigation li {
    display: inline-block !important;
    padding: 5px 10px !important;
    width: auto !important;
}


/*para mostrar el footer centrado*/
.site-content, .site-footer {
    /* float: unset; */
    max-width: 100% !important;
    /* width: 100%; */
}

	</style>';
	}
	
?>
	
	
	
	
</head>