Ir al contenido principal

[google.maps] Restringir polígono dentro de otro

Hola a todos!!

En mi trabajo me toca desarrollar hartas cosas bonitas usando la API de Google Maps.

La última cosa bonita que estoy haciendo es un Editor Gráfico web, que ocupa Polylines y Rectangles. En el siguiente registro les quiero dejar algunos tips sobre cómo lograr cierta característica que puede serles útil, aun si su desarrollo no apunta a lo mismo. Por lo mismo, este tip requiere de conocimientos previos de manejo de la API para comprenderlo.

Vamos al asunto.

El Editor que estoy realizando requiere de un área base, o como diríamos en términos gráficos, un lienzo donde trabajar. El objetivo es que el usuario sólo trabaje dentro de esta área, y no fuera de ella.

Para ello, me valgo de algunas configuraciones previas, que incluyen un par de variables globales, y algunos manejadores de eventos de Google Maps.

Una variable global me indica en qué "Modo" se encuentra el usuario: para este caso, los valores pueden ser "creando" o "en espera". Otra variable me indicará el "Tipo" de elemento que quiero agregar al Mapa, siendo los valores posibles "lienzo", "rectángulo" o "línea" (no literalmente éstos, claro).

Paralelamente, he agregado un listener para el "click" del Mapa. La acción asociada es precisamente "Crear Lienzo", acción que sólo se ejecuta si el valor de la variable "Modo" es "creando" y de la variable "Tipo" es "lienzo". Lo que hago es capturar dos clicks en el mapa, de los cuales obtengo las dos coordenadas que servirán de vértices para crear mi Rectangle "Lienzo". Una vez creado, la variable "Modo" pasa a "en espera", por lo que cualquier nuevo click sobre el Mapa no realizará acción alguna.

Este rectángulo Lienzo es editable, por lo que el usuario puede modificarlo o moverlo a voluntad. Una vez que el usuario le da click a un botón de "Guardar", bloqueo toda edición de este Lienzo, y ahora ya se puede comenzar a trabajar sobre él.

Previamente, al momento de crear el rectángulo Lienzo, había agregado sobre él un listener para el "click". Este evento ejecuta casi la misma acción que el click sobre el Mapa: si el Modo es "creando", captura los clicks sobre el Lienzo, y dibuja otro Rectangle o un Polyline, según el valor de la variable "Tipo".

De este modo, si el usuario hace click sobre el Lienzo en Modo "creando" de uno de los dos Tipos antes mencionados ("rectángulo" o "línea"), podrá agregar uno de estos elementos dentro del Lienzo. Estos elementos son creados en modo editable, por lo que el usuario puede moverlos y modificar sus dimensiones o forma a voluntad.

Ahora bien, para evitar que estos elementos se salgan del Lienzo, lo primero es detectar los cambios en los elementos, tales como reposicionamiento o redimensionamiento.

En el caso de los Rectangle, nos basta con manejar el evento "bounds_changed": este evento se gatilla cuando las coordenadas Norte, Sur, Este u Oeste del rectángulo cambian, lo cual puede ocurrir tanto si redimensiomos el polígono como si lo movemos de posición.

En el caso de los Polyline, nos toca manejar dos eventos: "drag" y "mouseup".
El evento "drag" se gatilla al mover (arrastrar) el Polyline.
El evento "mouseup" se gatilla al soltar el elemento tras haberlo clickado en cualquiera de sus puntos vértice, o sea, se gatilla cuando modificamos la posición de sus vértices (este último evento fue el que más tiempo me tocó averiguar para poder manejarlo, pues no aparece en la documentación ni hay ejemplos de ello, hasta la fecha; lo obtuve de la experimentación en base a ejemplos de otros casos en Stack Overflow).

Tras haber agregado los listeners respectivos a cada elemento, toca asociarles la acción para "restringir" el movimiento.

Para ello, lo primero que hago es detectar los límites del rectángulo contenedor del elemento. Teniendo esta "caja", se puede determinar fácilmente con comparaciones simples si ella se encuentra dentro o fuera de la "caja" del Lienzo: el Norte del elemento debe ser inferior al Norte del Lienzo; el Sur del elemento debe ser superior al Sur del Lienzo; etc. 

En el caso de los Rectangle, usamos sus coordenadas tal cual, usando el método getBounds() de la API. 

Pero en el caso de los Polyline, no existe este método (en la V.3 de la API), por lo que debemos calcularlas. Afortunadamente, gracias a los usuarios de Stack Overflow, tenemos una solución dada por el usuario Jamie Carl: añadiendo esta orden una vez cargado nuestro Mapa, podremos acceder al método getBounds() en todos los Polyline que añadamos al Mapa.

/*
@author Jamie Carl
@date 2016
*/

google.maps.Polyline.prototype.getBounds = function() {
var bounds = new google.maps.LatLngBounds(); this.getPath().forEach(function(item, index) { bounds.extend(new google.maps.LatLng(item.lat(), item.lng()));
});
return bounds;
};

Retomando el caso de la verificación de los Rectangle, como dije antes, sólo debemos comparar que los puntos cardinales del elemento se encuentren dentro de los puntos cardinales del Lienzo. Si uno o más puntos se encuentran fuera, la idea es reposicionar por completo el elemento dentro del Lienzo. Esto se consigue sumando o restando a cada punto cardinal del elemento la diferencia entre los puntos cardinales paralelos del elemento y el Lienzo que se están desbordando. Haré un ejemplo con un gráfico cartesiando ultra simple:

Gráfico de ejemplo: Elemento fuera del Lienzo

Tomando el gráfico de ejemplo:

  • La caja de nuestro Lienzo tiene los siguientes puntos cardinales o límites (bounds):
    • Norte: 7
    • Sur: 2
    • Oeste: 3
    • Este: 10
  • La caja del rectángulo "A" tiene los siguientes límites:
    • Norte: 6
    • Sur: 4
    • Oeste: 2
    • Este: 5
  • Y tiene las siguientes dimensiones:
    • Alto (N - S): 2
    • Ancho (E - W): 3
  • Al comparar sus límites con los del Lienzo, tenemos que:
    • Hacia el Norte: 6 < 7 = Verdadero => Se encuentra dentro del Lienzo
    • Hacia el Sur: 4 > 2 = Verdadero => Se encuentra dentro del Lienzo
    • Hacia el Oeste: 2 > 3 = Falso => Se encuentra fuera del Lienzo
    • Hacia el Este: 5 < 10 = Verdadero => Se encuentra dentro del Lienzo
    • Conclusión: Por el hecho de tener sólo un punto Fuera del Lienzo, decimos que el Rectangle "A" está fuera del Lienzo, por lo tanto, hay que reposicionarlo dentro.
  • Para reposicionarlo, obtenemos los nuevos límites de "A":
    • Hacemos el cálculo con el/los punto(s) cardinal(es) divergentes. En este caso, sólo ocurrió con el punto "Oeste". Esto significa que deberemos modificar tanto el límite Oeste como el Este, manteniendo intactos los límites Norte y Sur.
    • Nuevo Norte: 6 (sin cambio)
    • Nuevo Sur: 4 (sin cambio)
    • Nuevo Oeste: 2 (usamos el límite Oeste del Lienzo)
    • Nuevo Este: Oeste del Lienzo + Ancho de "A" =  3 + 3 = 6
Gráfico de ejemplo: Elemento reposicionado

  • Cabe mencionar que, si al calcular los nuevos límites de "A", notamos que el elemento no cabe dentro del Lienzo, tocará modificar los límites del elemento para que coincidan con los límites del Lienzo (o sea, que ocupe todo el ancho o alto de éste, según sea el caso).
  • Teniendo los límites definitivos, los aplicamos al elemento usando el método setBounds().
Eso soluciona la verificación en cuanto a Rectangles.

Pero nos queda aún por realizar la verificación a los Polylines.

Gracias al método getBounds() agregado manualmente al prototipo del Polyline, podemos hacer la comparación para constatar si el elemento se encuentra dentro o fuera del Lienzo, pero no nos ayuda necesariamente a determinar la nueva posición del elemento. Pues, en este caso, la verificación se debe hacer en cuanto a los puntos vértice del Polyline, uno por uno.

En este caso, lo que hacemos es obtener la lista de puntos del Polyline mediante el método getPath(), para recorrerla, y realizar la misma comparación simple que hicimos previamente con los límites del Rectangle, pero ahora con cada Punto de la línea.

Importante es recordar que no podemos modificar las coordenadas de un Punto existente, tal cual lo dice la documentación, por lo que es necesario recrear el path completo, a partir de nuevos puntos, que incluirán tanto a los puntos no modificados como a los modificados tras el reposicionamiento.

La lógica de la comparación en este caso, por Punto, sería:
  • Si tenemos las coordenadas del Punto:
    • Latitud (posición en eje N-S)
    • Longitud (posición en eje W-E)
  • Al compararlas con los límites del Lienzo, podremos determinar sus nuevas coordenadas usando la siguiente lógica:
    • Latitud:
      • Si Latitud > Norte del Lienzo => Latitud = Norte del Lienzo
      • Si Latitud < Norte del Lienzo y Latitud > Sur del Lienzo => Latitud sin cambios
      • Si Latitud < Sur del Lienzo => Latitud = Sur del Lienzo
    • Longitud:
      • Si Logintud > Este del Lienzo => Longitud = Este del Lienzo
      • Si Longitud < Este del Lienzo y Longitud > Oeste del Lienzo => Longitud sin cambios
      • Si Longitud < Oeste del Lienzo => Longitud = Oeste del Lienzo
  • Por cada Punto verificado, creamos un nuevo Punto para añadir a un nuevo path, el cual asignamos al Polyline mediante el método setPath() tras salir del ciclo de verificaciones. 
Y ese sería el "tip" del día de hoy. Espero que les sirva ^_^
Hasta pronto!

Referencias:




Comentarios

Entradas populares de este blog

[tsql] Error: La instrucción INSERT EXEC no se puede anidar

Holas a todos. Mientras programaba un procedimiento almacenado, intenté obtener los datos de otro procedimiento, como lo he venido haciendo desde que descubrí tamaña maravilla de la programación sql. Pero hoy me topé con este extraño error: La instrucción INSERT EXEC no se puede anidar . Tras investigar por algunos lados, di con la respuesta: no se puede almacenar en una tabla temporal de procedimiento almacenado, el resultado de otro procedimiento que también esté realizando una inserción de este tipo. Esto es algo como tener: CREATE PROCEDURE miProcedimiento AS  INSERT INTO #tablita EXEC otroProcedimiento;  SELECT * FROM #tablita; END; CREATE PROCEDURE nuevoProcedimiento AS  INSERT INTO #tabla1 EXEC miProcedimiento; END; Esto significará que si ejecuto: EXEC nuevoProcedimiento; ...SQL me arrojará el error antes mencionado. La solución al problema es no llamar a un procedimiento que esté llamando a otro ya en su interior. En algunos lados leí que transf

[mysql] Pasar array a parámetro de procedimiento almacenado (Mysql)

Me tocó hacer una consulta que retornaba una lista de items relacionados con una lista de usuarios que podían o no tener registros en común (vale decir, tabla de quiebre). La lista debía retornar siempre la lista de items, independiente de si había usuarios por los cuales consultar y/o si los usuarios tenían relación con ellos, pero debía mostrarme el status de los usuarios por cada item, de haberlos, esto es, una lista de nombres con una columna que podía estar vacía o no. Para el caso de tener que consultar los items relacionados con usuarios, al hacer la consulta utilizando un LEFT JOIN, me daba resultados si los usuarios tenían relación con los ítems, pero no si los usuarios no tenían items asociados pues, obviamente, al no estar relacionados, la consulta retorna vacío. Por ello, la solución era hacer la consulta de los items primero, y luego por cada item preguntar el status del usuario por cada uno. Para ello, tenía dos alternativas: hacerlo por programación o hacerlo por bas

[php] NuSOAP HTTP Error: socket read of headers timed out

Holas a todos. Este es para comentar un problema que he tenido al trabajar un servicio web montado en PHP con la clase NuSOAP. El problema surgió cuando intenté llamar al servicio web desde el otro servidor, pero se caía a los exactos 30 segundos de ejecución, mostrando el mensaje que titula este registro: HTTP Error: socket read of headers timed out Sabía que el problema era el timeout, pero ¿el timeout de qué? En los servidores y páginas web hay timeouts por todos lados: el de la Conexión a internet o la red, el del Servidor (hardware), el del Servidor Web en sí (Apache, mi caso), el de PHP (mi caso)... Pero nunca se me habría ocurrido que las Aplicaciones o frameworks también pudieran tener :o Por eso, tras buscar por la red la solución a mi problema, la respuesta vino precisamente de alguien que señaló sencillamente que había que modificar el timeout de la clase NuSOAP. Y dicho y hecho, eso solucionó el problema. Si están usando en su servidor y/o cliente la clase NuSOAP, y d