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-->

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>

Obtener la query de las migraciones

La opción adicional es “–pretend” ( doble guíon medio), ojo no ejecutará la migración, es para mostrar la query que lanzará

php artisan migrate --pretend

Se obtiene una salida así

vagrant@homestead:~/CheckListLegal$ php artisan migrate --pretend
AddColumnJavierformidToTableChecklist: alter table `CheckList` add `JavierForm_id` int unsigned null
AddColumnJavierformidToTableChecklist: alter table `CheckList` add constraint `checklist_javierform_id_foreign` foreign key (`JavierForm_id`) references `JavierForm` (`id`) on delete cascade

Capturar error VUE

Capturar error en vue, insertar este script al final del body.

<script>
    
    Vue.config.errorHandler = (err, vm, info) => {
        // err: error trace
        // vm: component in which error occured
        // info: Vue specific error information such as lifecycle hooks, events etc.

        console.log("data+");
        console.log(vm.$data);
        console.log("error");
        console.error(err);
        console.error("info:"+info);

        //bugsnagClient.notify(err);
    };
</script>

Añadir nuevo sitio en homestead

Primero agregamos la nueva dirección en host

nano /etc/hosts

Segundo Después en el archivo agregamos una linea para el nuevo dominio en local

192.168.10.10  nuevositio.test

Al terminar de editar ctrl+x , tecla “Y” y yes para salvar los cambios

Tercero editamos el archivo de homestead con el comando

homestead edit

En el archivo, agregamos: la ruta a su carpeta en loca, la ruta en la maquina virtual, a que dominio apunta y la nueva BD en caso necesitemos, en folders,sites y databases, las rutas pueden variar dependiendo del proyecto

---
ip: "192.168.10.10"
memory: 2048
cpus: 1
provider: virtualbox
ssl: true
authorize: ~/id_rsa.pub

keys:
    - ~/id_rsa

folders:
    - map: ~/Documents/Proyectos/nuevositio/web
      to: /home/vagrant/nuevositio/web
    
sites:
    - map: nuevositio.test
      to: /home/vagrant/nuevositio/web/nuevositio/public
   
databases:
    - nuevositio_sb

variables:
    - key: APP_ENV
      value: local

Cuarto, actualizamos la configuración, con el comando

homestead halt && homestead up --provision

Con eso el sitio debió quedar agregado, y si colocamos nuevositio.test en el navegador cargara la web.

Laravel clear

Comandos para resetear rutas, variables de entorno, configuración, etc

composer dump-autoload
php artisan optimize
php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear

php artisan route:cache
php artisan config:cache

php artisan optimize

Cambiar valores de app a los componentes y viceversa

bidirectional reactive vue app to component
En este apunte intento poder comunicar desde la app al componente y viceversa.

Creamos el componente my-component.vue

<template>
    <div class="div_component">
        <input 
        v-model="message" 
        @keyup="changeMessage">
        <p>Message en componente es: {{message}}</p>
    <div>
</template>

<script>
module.exports = {
    data: function () {
        return {
          message: this.level
        }
      },
    ready:function(){},
    methods: {
        changeMessage: function(){
            console.log(this.message);
            this.$emit('change-message',this.message);
        }
    },
    props: 
    {
        level: {
            required: true
        }
    }
}
</script>

<style>
.div_component {
    background-color: rgb(204, 24, 114);
    padding: 10px;
    margin: 10px;
}
</style>

Donde level, es la propiedad que asignará el padre al componente, y con ese valor iniciamos el valor de message, que será la variable reactiva, después en <template> relacionamos v-model y escuchamos cuando cambie con @keyup.

La parte importante para transmitir los cambios al padre es la función $emit, donde el primer parámetro ‘change-message’ debe coincidir con el nombre que indiquemos al insertar el componente.

Definimos index.html

<!doctype html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue"></script>
    <script src="https://unpkg.com/http-vue-loader"></script>
    <style>
        .div_padre{
            background: blueviolet;
            padding: 10px;
        }
    </style>
  </head>

  <body>
    <div id="my-app">
      <div class="div_padre">
        <!--
        -->
        <input
        @keyup="updateMessageChild"  
        v-model="message"></input> 
        <my-component 
        ref="componentChild" 
        :level="message" 
        @change-message="message = $event"></my-component>
      </div>
        
    </div>

    <script src="app.js" ></script>
  </body>
</html>

En index.html agregamos el componente my-component, indicamos ref, para tener una referencia desde app y poder actualizarlo, :level, es para indicar el dato inicial y @change-message, es para escuchar cuando el componente llame a $emit.

Ahora definimos app.js

new Vue({
    el: '#my-app',
    components: {
         'my-component': httpVueLoader('./my-component.vue')
    },
    data () {
        return {
             message:"ejemplo"
            }
        },
    methods: {
        
        updateMessageChild:function(){
            this.$refs.componentChild.message = this.message;
        }
        
    }
});

En app.js, el método updateMessageChild sirve para escuchar los cambios de message y poder actualizar el componente con this.$ref.componentChild donde componentChild es el nombre de la referencia al componente que indicamos por medio de ref.

Se debería comportar así

Código:
*Lo modifique un poco porque codesandbox mostraba errores al código original.

link: https://27rog.csb.app/

Notas:

Revisando básicamente, vue recomienda que cualquier paso de valor entre componentes sea definido y no directo, es posible pero altamente no recomendable.

No he profundizado mucho con este ejemplo, pero para otros casos mas complejos, considerar las limitantes descritas en la documentación https://es.vuejs.org/v2/guide/reactivity.html

Otro articulo interesante que estoy revisando:
https://alligator.io/vuejs/component-communication/