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)