Guía 6 – Indicar dirección a través de una mapa

En este ejemplo se resuelve un problema que se da en los núcleos urbanos poco poblados y es el poder georreferenciar un domicilio a través de su latitud y longitud utilizando OpenStreetMap.

Pero también, y no menos importante, lo que se explica es cómo integrar librerías de JavaScript en los desarrollos realizados con PHPRunner, sin que tengamos que desarrollar un “plugin”, de forma bastante sencilla y creo, accesible a casi todo el mundo.

Requisitos a resolver

Se desea que utilizando OpenStreetMap (no se desea utilizar Google Map por sus costes) un usuario que está utilizando la aplicación en un móvil pueda informar de la ubicación (latitud y longitud) dónde vive o dónde ha ocurrido un hecho o evento. Es para usuarios de zonas rurales, por lo que no existe un domicilio postal normalizado que nos pueda informar de esa ubicación.

Ampliación:

Se desea obtener la dirección postal del punto indicado a través del mapa.

DEMO: https://fhumanes.com/address

Solución Técnica

Como siempre hago, lo primero es ver en internet lo que existe y principalmente, si en GITHUB existe alguna librería JavaScript que facilita la funcionalidad que se requiere.

Después de varios intentos y fracasos (funcionaba en PC, pero no en móvil) observé que en muchas web se utilizaba esta librería https://leafletjs.com/

Comprobé que funcionaba en PC y en móvil y que me daba la funcionalidad requerida. Ahora sólo me quedaba decidir cómo integrarla.

La solución, que os voy a explicar, es a mí entender la forma más sencilla de hacerlo en PHPRunner.

Primero, cree una tabla para hacer un proyecto de prueba de concepto de la funcionalidad.

Create Tabla
CREATE TABLE `pruebas`.`address_user` (
`idaddress_user` int(11) NOT NULL AUTO_INCREMENT,
`Name` varchar(100) NOT NULL,
`Latitude` varchar(50) DEFAULT NULL,
`Longitude` varchar(50) DEFAULT NULL,
`AddressPostal` varchar(400) DEFAULT NULL,
PRIMARY KEY (`idaddress_user`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8

Segundo, inicie un proyecto PHPRunner (versión 10.4, pero la solución sirve para cualquier versión) que tiene estas características.

  • El Query de por defecto lo modifiqué para incluir el campo que representaría al mapa
    SELECT idaddress_user, Name, '' map, Latitude, Longitude FROM address_user

    El campo “map” lo utilizo para presentar el mapa en las pantallas.

  • Para cargar las librerías JavaScript y definición de estilos CSS extras que requiere la solución utilicé el evento “After Application Initialized”. Esto lo hago así para que estos objetos también estén cargados para las páginas “popup”. En este caso la solución tiene un problema, que aún no he resuelto, y no es totalmente funcional en “popup”.
    En este evento creo una variable con el código HTML que hay que añadir a la página y en la parte de definición de la cabecera “head”, lo pongo.

Para no cargarlo en todas las páginas, compruebo qué “Table” o “View” se está ejecutando y si es adecuada, cargo las librerías.

After Application Initialized
customjscript ='';
global $pinfo;
$temp1 = explode('_',$pinfo['filename']);
$tableName = '';
for ($i = 0; $i < count($temp1)-1 ; $i++) {
    $tableName .= $temp1[$i].'_';
}
$tableName = substr($tableName,0,-1); // delete last '_'
if ($tableName == 'address_user') { //  Only load the libraries in the tables that require it
$customjscript = <<<EOT
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin=""/>
<script src="https://unpkg.com/[email protected]/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script>
<style>
      #map {
                  width: 600px;
                  height: 400px;
            }
</style>
EOT;
}
  • El campo “map”, en Designer, indico que es un campo de tipo “custom” y ahí indico el “DIV” del mapa y el JavaScript que debe ejecutarse. El JavaScript tiene 3 variaciones:
    • 1/ Cuando estamos en “Add”, debemos situarnos en el mapa próximos a dónde estamos y por eso se utiliza el GPS para situarnos en el mapa.
    • 2/ Cuando estamos en “Edit” se contempla la posibilidad de que no haya información y entonces actúa como si fuera “Add”.  Si tiene información, entonces se utiliza esta para situarse en el mapa.
      Si se cambia la ubicación, entonces se actualiza el registro y si no se informa de nueva dirección, los campos de Latitud y Longitud, no se cambian.
    • 3/ Cuando estamos en “View” se muestra el mapa con la información de ubicación registrada, sin posibilidad de cambiar esta.
      Custom
      if ($data['Latitude'] == '') { //  Has not fixed address
      $value = <<<EOT
      <div id='map'></div>
      <script>
         var lat = 0; // To share
         var lon = 0; // To share
               
            var map = L.map('map').fitWorld();
               
         L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                  attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
                  }).addTo(map);
            function onLocationFound(e) {
                  var radius = e.accuracy / 2;
                  L.marker(e.latlng).addTo(map)
                        .bindPopup("You are " + radius + " meters from this point").openPopup(); 
                  L.circle(e.latlng, radius).addTo(map);
            }
            function onLocationError(e) {
                  alert(e.message);
            }
            map.on('locationfound', onLocationFound);
            map.on('locationerror', onLocationError);
            map.locate({setView: true, maxZoom: 16});
               
              var popup = L.popup();
            function onMapClick(e) {
                  popup
                        .setLatLng(e.latlng)
                        .setContent("You have set the location to: <br>" + 'Latitude: '+ e.latlng.lat.toString() +'<br>'+'Longitude: '+e.latlng.lng.toString())
                        .openOn(map);
             // console.log('Todo: '+e.latlng.toString());
             // console.log('Separado: '+ e.latlng.lat.toString()+' más: '+ e.latlng.lng.toString());
                        lat = e.latlng.lat.toString();
                        lon = e.latlng.lng.toString();
            }
            map.on('click', onMapClick);
       </script>
      EOT;
      } else {
      $Latitude = $data['Latitude'];
      $Longitude = $data['Longitude'];
      $js_update = '';
      global $params;
      $pageType = $params[pageType];
      if ($pageType == 'edit') { // Is page Edit
      $js_update = <<<EOT
        var popup = L.popup();
            function onMapClick(e) {
                  popup
                        .setLatLng(e.latlng)
                        .setContent("You have set the location to: <br>" + 'Latitude: '+ e.latlng.lat.toString() +'<br>'+'Longitude: '+e.latlng.lng.toString())
                        .openOn(map);
             // console.log('Todo: '+e.latlng.toString());
             // console.log('Separado: '+ e.latlng.lat.toString()+' más: '+ e.latlng.lng.toString());
                        lat = e.latlng.lat.toString();
                        lon = e.latlng.lng.toString();
            }
            map.on('click', onMapClick);
      EOT;
      }
      $value = <<<EOT
      <div id='map'></div>
      <script>
         var lat = 0; // To share
         var lon = 0; // To share
               
            var map = L.map('map').setView([$Latitude, $Longitude], 16);
               
         L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                  attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
                  }).addTo(map);
        L.marker([$Latitude, $Longitude]).addTo(map)
          .bindPopup('This is the address/place reported')
          .openPopup();
      $js_update
       </script>
      EOT;
      };
  • Se utiliza el evento “Javascript OnLoad event” para controlar la interacción realizada en el mapa y cargar los datos guardado en las variables “lat” y “lon”, en los campos del formulario. Un aspecto muy IMPORTANTE es la utilización del evento “on(‘beforeSave’)” que nos da posibilidad de aplicar lógica de control para fijar si es obligatorio o no y para pasar los datos desde variables del JavaScript de los mapas a los campos de PHPRunner.
    Onload Event
    var ctrlLatitude = Runner.getControl(pageid, 'Latitude');
    ctrlLatitude.makeReadonly();
    var ctrlLongintude = Runner.getControl(pageid, 'Longitude');
    ctrlLongintude.makeReadonly();
    this.on('beforeSave', function(formObj, fieldControlsArr, pageObj){
          if ( lat == 0 ) {
                alert("Es necesario informar en el mapa la dirección/lugar");
                Runner.delDisabledClass(pageObj.saveButton ); // Activar nuevamente, botón SAVE
                return false;
          } else {
                ctrlLatitude.setValue(lat);
                ctrlLongintude.setValue(lon);
                return true;
          }
    });
  • Como os he explicado en otros ejemplos, si añadimos “seudo-campos” a las tablas, estos hay que destruirlos “unset(field)” en el evento “Before Record Add/Update” para que no actúen en los insert o updates.

Para obtener la dirección postal de unas coordenadas he utilizado la librería de GITHUB https://github.com/maxhelias/php-nominatim que hace uso del servicio Nomatim de OpenStreetMap.

El código que he usado es:

geo_reverse.php
<?php
require_once __DIR__ . '/php-nominatim_2.2/autoload.php'; 
use maxh\Nominatim\Nominatim;
$url = "http://nominatim.openstreetmap.org/";
$nominatim = new Nominatim($url);
$reverse = $nominatim->newReverse()
            ->latlon($values['Latitude'], $values['Longitude']);
$result = $nominatim->find($reverse);
$values['AddressPostal'] = $result['address']['road']."\n".$result['address']['postcode']." - ".$result['address']['city']."\n".
       $result['address']['state']."\n".$result['address']['country'];

Lo más importante del ejemplo es esta forma de integrar librerías JavaScript, lo que os va a permitir hacer que vuestras aplicaciones dispongan de funcionalidades que de otra forma sería imposible con el PHPRunner estándar.

Como siempre os indico, cualquier duda o problema que tengáis, podéis hacérmela llegar a mi cuenta de email [email protected]

Como siempre, os dejo los fuentes del ejemplo para que podáis instalarlo en vuestros  equipos.

Adjuntos

Archivo Tamaño de archivo Descargas
zip PHPRunner 10.4 - actualizado 16/07/2022 1 MB 707
zip PHPRunner 10.7 - PHP 8.1 296 KB 245

Blog personal para facilitar soporte gratuito a usuarios de React y PHPRunner