Save two float numbers in one blueprint field

Hi,

I want two form fields in the panel (with float numbers for latitude and longitude) be saved in one blueprint field (name “location” comma seperated), how can I do that? I need it for the kirby geo-plugin: https://github.com/getkirby-plugins/geo-plugin

Why do you need them in one field? That is not necessary, you can pull both floats from two separate fields.

I need it for the radius filter. The readme says the location field must be in the following format:

location: {lat},{lng}

or with a real life example:

location: 49.4883333,8.4647222

:-/

I see, you have two options:

  1. Use a panel.page.update hook to create a new field from the two separate fields
  2. Use the map() method to create a virtual field

I’d go for the latter, but it’s up to you.

Unfortunately both is beyond of my possibilities :frowning: So I think I must go with a simple textfield named “location” (where in panel I type in both floats with a comma). :frowning:

Here is an example using map():

$addresses = page('addresses')
    ->children()
    ->map(function($child) {
      $child->location = $child->lat().','.$child->lon();
        return $child;
      })
    ->filterBy('location', 'radius', [
       'lat'    => 49.4883333,
       'lng'    => 8.4647222,
       'radius' => 10
      ]);

The map function adds a virtual location field with a value that is constructed from the lon and lat fields.

1 Like

Thank you very much. $child is only a placeholder?

You can use any variable instead of child, it is just a placeholder for a single page within the collection.

Thanks. It works wonderful. :slight_smile:

Do you know about the Map Field by @AugustMiller. It’s a very nice way to handle geo coordinate user input in the panel. I use it in combination with the geo plugin and it works really well.

@flokosiol Thanks for pointing this out.

I agree that this field is very nice if you need a way to find location data from addresses or pin locations on a map. However, the plugin stores the data in yaml format, which means you end up with an array to work with. You would still have to transform the data to use it with the radius method of the geo plugin.

Oh thanks for this note. If I would use the map field how I would have to modify the map function? Is it simply the following way?

$addresses = page('addresses')
    ->children()
    ->map(function($child) {
      $child->location = $child->location()->yaml()['lat'].','.$child->location()->yaml()['lng'];
        return $child;
      })
    ->filterBy('location', 'radius', [
       'lat'    => 49.4883333,
       'lng'    => 8.4647222,
       'radius' => 10
      ]);

The radius filter is opinionated, in that it relies on the specific formatting you’ve determined—but you can implement a custom filter by falling back to the geo::distance function:

$addresses = page('address')->children()->filter(function ($a) {
  # Parse + Save your Map field data:
  $myAddress = $a->location()->yaml();

  # Set your center (where we'll base the distance calculation from)
  $center = geo::point(49.4883333, 8.4647222);

  # Create a `target` point from the parsed address data to compare with the center:
  $target = geo::point($myAddress['lat'], $myAddress['lng']);

  # Get the distance (in a specified unit), and compare against a threshold:
  return geo::distance($center, $target, 'km') < 10;
});

If the distance between the point and your defined $center is less than 10km, the address will remain part of the collection—otherwise, it gets tossed. As an added benefit, the field data will remain untouched, so you can use it as expected, after the filtering happens!

Edit: this is untested, but I trust you can adapt it!

Using the original idea, I’d use a different virtual field name to filter by:

$addresses = page('addresses')
  ->children()
  ->map(function($child) {
    $address = $child->location()->yaml();
    $child->latLng = $address['lat'].','.$address['lng'];
      return $child;
    })
  ->filterBy('latLng', 'radius', [
     'lat'    => 49.4883333,
     'lng'    => 8.4647222,
     'radius' => 10
    ]);

Thanks, texnixe, it works.

Is there a solution to sort $adresses by the radius (ASC)? Maybe with creating a new virtual field (with the radius as value)?

You could try this (not tested and there might be some typos, so use debugging):

$options = [
'radius' => '10',
'lat'    => 49.4883333,
'lng'    => 8.4647222
];
$addresses = page('addresses')
  ->children()
  ->map(function($child) use ($options) {
    $address = $child->location()->yaml();
    $point = geo::point($address['lat'], $address['lng']);
    
    $origin = geo::point(a::get($options, 'lat'), a::get($options, 'lng'));
    $radius = a::get($options, 'radius');
    $unit = a::get($options, 'unit');

    if($origin && $radius > 0) {
      $child->distance = geo::distance($origin, $point, $unit);
    }
      return $child;
    })
  ->filterBy('distance', '>=', $options['radius']);

Edit: Edited missing comma in last line