Mixing Image and Map coordinates

Mixing Image and Map coordinates

Written by Tina Hammar on Feb 2nd, 2021 Views Report Post

I use Leafletjs for both, map (OSM) and image based positioning. I also need to store both types of coordinates in the same database columns.

This article is not about how to use Leafletjs, it's about the problems that arise when you have to support both geographical coordinates as well as image based coordinates and how to differentiate between them in your code.

We are going to create

  • A helper class.
  • Two Model traits.
  • Two static helper methods.

Preparations

This tutorial requires you to install Joseph Estefane, Laravel mySQL Spatial package. It will help you with all kinds of mySQL spatial data conversions. Follow the package installation instructions in the GitHub repository.

How to store spatial data in db

A common way to store latitude and longitude are in decimal columns with these constraints.

$table->decimal('latitude', 10, 8)->nullable();
$table->decimal('longitude', 11, 8)->nullable();

But with that setup, you'll run into problems if you allow images instead of map layers for positioning.

Depending on the size of the image and the zoom level, it will not take long before you're breaking the constraints and the db will throw out of range errors.

The values for an image based location might look like this:

lat: 6800.51701407496,
long: 10492.589379187975,

The solution is to increase the constraints in db. You have to decide a suitable range based on how large images and zoom levels you allow in your application.

$table->decimal('latitude', 22, 15)->nullable();
$table->decimal('longitude', 22, 15)->nullable();

If you want to benefit from mySQL spatial column types you should add a point column.

$table->point('position')->nullable();

Some may argue that duplication of data is undesirable, but you will be happier in the long run when you can pick the best suited format for different use cases. Or, you might have old values that you wish to convert to spatial points, while keeping the old data for legacy reasons.

Avoid out of range errors

The next problem you'll face is when the database starts throwing errors when you try to save the image based locations to the position column.

The solution is to create a method that checks if the coordinates are valid before trying to save it to the database.

Example syntax:

if(validateGeoCoords($model->latitude, $model->longitude)) {
    //do something, values valid map coordinates
}

Create a Helper

It's time to create a helper that you can use throughout your application in different scenarios.

App\Helpers\GeoHelper.php

<?php


namespace App\Helpers;


class GeoHelper
{
    /**
     * If the geo coordinates are based on an image,
     * <br> the lat/long are invalid to save as a spatial point.
     * <br> mySQL will throw an [COLUMN NAME]_OUT_OF_RANGE error.
     *
     * @param float $lat
     * @param float $long
     * @return bool
     */
    public static function validateGeoCoords(float $lat, float $long): bool
    {
        return ($lat >= -90 && $lat <= 90 && $long >= -180 && $long <= 180);
    }
    
    public static function generateLatLong($model): void
    {
        // "position" must be declared in the Models $spatialFields property
        if (filled($model->position)) {
            $model->latitude = $model->position->getLat();
            $model->longitude = $model->position->getLng();
        }
    }

    public static function generateSpatialPoint($model): void
    {
        if (
            filled($model->latitude) &&
            filled($model->longitude) &&
            self::validateGeoCoords((float)$model->latitude, (float)$model->longitude)
        ) {
            $model->position = new Point($model->latitude, $model->longitude); // (lat, lng);
        }
    }

}

Auto generate values


Case 1

You have lat/long in decimal values and you want to auto-populate the mySQL point column.

Solution: Create a Trait that auto generates a spatial point when a Model is created or updated.

App\Traits\AutoGeneratesPosition.php

<?php


namespace App\Traits;

use App\Helpers\GeoHelper;
use Grimzy\LaravelMysqlSpatial\Types\Point;

trait AutoGeneratesPosition
{
    protected static function bootAutoGeneratesPosition()
    {
        static::creating(function ($model) {
            GeoHelper::generateSpatialPoint($model);
        });

        static::updated(function ($model) {
            GeoHelper::generateSpatialPoint($model);
        });
    }
}

Case 2

You have a column with a spatial point value and you want to autogenerate lat/long values for the decimal columns.

Solution: Create a Trait that auto generates lat/long when the Model is created/updated

<?php


namespace App\Traits;

trait AutoGeneratesLatLong
{
    protected static function bootAutoGeneratesLatLong()
    {
        static::creating(function ($model) {
            GeoHelper::generateLatLong($model);
        });

        static::updated(function ($model) {
            GeoHelper::generateLatLong($model);
        });
    }

}

Add the Trait to the Model

Never use both traits :) Pick the one that suits your case and add it to the Model.

use AutoGeneratesLatLong;
//or
use AutoGeneratesPosition;

Or, use Model Observers

If you don't want to add Traits to your models you can easily move the methods to a Model Observer instead.

Case 3

You have a mixed environment where you sometimes convert from spatial data to decimals, or vice versa.

Solution: Don't add any Trait to your Model. Instead, use the static methods from the GeoHelper where needed.

Example when you have a spatial point:

$model = Model::first();
//assumes $model->position contains a spatial point value
GeoHelper::generateLatLong($model);
$model->save();

Example when you have lat/long as decimal values:

$model = Model::first();
//assumes $model->latitude && $model->longitude contains decimal values
GeoHelper::generateSpatialPoint($model);
$model->save();

For both case 1, 2 and 3

Now that you have the GeoHelper you can always differentiate between "real" map coordinates and image based coordinates.

if(GeoHelper::validateGeoCoords($model->latitude, $model->longitude)) {
    //do something, the values are valid map coordinates
}

There's more ...

Don't forget to check out all the other helpers provided by the Laravel mySQL Spatial package package, like;

  • Scopes: Spatial analysis functions
  • Working with polygons, GeoJSON and geometry collections
  • Conversion helpers

Comments (0)