Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Interactive Polygons #385

Closed
ibisoda opened this issue Aug 12, 2019 · 112 comments · Fixed by #1822
Closed

[FEATURE] Interactive Polygons #385

ibisoda opened this issue Aug 12, 2019 · 112 comments · Fixed by #1822
Labels
feature This issue requests a new feature P: 1 (important) S: core Scoped to the core flutter_map functionality
Milestone

Comments

@ibisoda
Copy link

ibisoda commented Aug 12, 2019

Hi,

I'm trying to reimplement an existing Leaflet-based app using flutter. In that app, the developer used Polygons to display the outline of a building, which when clicked/tapped shows additional information. and changes the polygon.

I'm trying to replicate that function, however I noticed there is no onTap Listener, at least for Polygons. Is there a different way to listen to tap events?

@matyhaty
Copy link

We are also looking at a onTap of a polygon / live.

Would be very helpful to us

We have some developers here also, so if someone can point us in the right direction we are happy to contribute

@neokree
Copy link

neokree commented Aug 27, 2019

From what I found it exist a onTap method, settable from the MapOptions. However this returns a LatLng object of the position tapped, so it seems like that it is not "binded" to any Polygon, Line, Marker etc.

@neokree
Copy link

neokree commented Aug 27, 2019

Just for anyone interested, I just translated in dart a function to find if a position is inside a Polygon

/// Translated from PHP
/// Source: https://assemblysys.com/php-point-in-polygon-algorithm/
bool _pointInPolygon(LatLng position, Polygon polygon) {
  // Check if the point sits exactly on a vertex
  var vertexPosition = polygon.points
      .firstWhere((point) => point == position, orElse: () => null);
  if (vertexPosition != null) {
    return true;
  }

  // Check if the point is inside the polygon or on the boundary
  int intersections = 0;
  var verticesCount = polygon.points.length;

  for (int i = 1; i < verticesCount; i++) {
    LatLng vertex1 = polygon.points[i - 1];
    LatLng vertex2 = polygon.points[i];

    // Check if point is on an horizontal polygon boundary
    if (vertex1.latitude == vertex2.latitude &&
        vertex1.latitude == position.latitude &&
        position.longitude > min(vertex1.longitude, vertex2.longitude) &&
        position.longitude < max(vertex1.longitude, vertex2.longitude)) {
      return true;
    }

    if (position.latitude > min(vertex1.latitude, vertex2.latitude) &&
        position.latitude <= max(vertex1.latitude, vertex2.latitude) &&
        position.longitude <= max(vertex1.longitude, vertex2.longitude) &&
        vertex1.latitude != vertex2.latitude) {
      var xinters = (position.latitude - vertex1.latitude) *
              (vertex2.longitude - vertex1.longitude) /
              (vertex2.latitude - vertex1.latitude) +
          vertex1.longitude;
      if (xinters == position.longitude) {
        // Check if point is on the polygon boundary (other than horizontal)
        return true;
      }
      if (vertex1.longitude == vertex2.longitude ||
          position.longitude <= xinters) {
        intersections++;
      }
    }
  }

  // If the number of edges we passed through is odd, then it's in the polygon.
  return intersections % 2 != 0;
}

@synw
Copy link

synw commented Aug 27, 2019

This lib can do it as well. The problem is that this kind of brute force geofencing is really slow so not user friendly. If you have a lot of polygons it is not appropriate. It would be much better to have an on tap callback.

@spvalencia
Copy link

It would be great to have onTap event for polygons and polylines. I also need that feature but I have not be able to find a good way to implemented.

@MichalMisiaszek
Copy link

Interestingly enough the same authors created map_view plugin which uses static Google Maps and provide listeners for polygon layers. @johnpryan how difficult it to add to flutter_map ?

@MichalMisiaszek
Copy link

Just for anyone interested, I just translated in dart a function to find if a position is inside a Polygon

/// Translated from PHP
/// Source: https://assemblysys.com/php-point-in-polygon-algorithm/
bool _pointInPolygon(LatLng position, Polygon polygon) {
  // Check if the point sits exactly on a vertex
  var vertexPosition = polygon.points
      .firstWhere((point) => point == position, orElse: () => null);
  if (vertexPosition != null) {
    return true;
  }

  // Check if the point is inside the polygon or on the boundary
  int intersections = 0;
  var verticesCount = polygon.points.length;

  for (int i = 1; i < verticesCount; i++) {
    LatLng vertex1 = polygon.points[i - 1];
    LatLng vertex2 = polygon.points[i];

    // Check if point is on an horizontal polygon boundary
    if (vertex1.latitude == vertex2.latitude &&
        vertex1.latitude == position.latitude &&
        position.longitude > min(vertex1.longitude, vertex2.longitude) &&
        position.longitude < max(vertex1.longitude, vertex2.longitude)) {
      return true;
    }

    if (position.latitude > min(vertex1.latitude, vertex2.latitude) &&
        position.latitude <= max(vertex1.latitude, vertex2.latitude) &&
        position.longitude <= max(vertex1.longitude, vertex2.longitude) &&
        vertex1.latitude != vertex2.latitude) {
      var xinters = (position.latitude - vertex1.latitude) *
              (vertex2.longitude - vertex1.longitude) /
              (vertex2.latitude - vertex1.latitude) +
          vertex1.longitude;
      if (xinters == position.longitude) {
        // Check if point is on the polygon boundary (other than horizontal)
        return true;
      }
      if (vertex1.longitude == vertex2.longitude ||
          position.longitude <= xinters) {
        intersections++;
      }
    }
  }

  // If the number of edges we passed through is odd, then it's in the polygon.
  return intersections % 2 != 0;
}

I used diffrent trick. Each polygon in lat/lng space I convert into a path in x/y space. Then using 2d geometry functions of path I can check if given point is inside specific path (which I can connect to specific polygon). Works pretty fast.

@synw
Copy link

synw commented Aug 29, 2019

Interesting. Do you have some code that we could see? I'm trying to do more or less the same thing using the geohex encoding format

@MichalMisiaszek
Copy link

MichalMisiaszek commented Aug 29, 2019

Interesting. Do you have some code that we could see? I'm trying to do more or less the same thing using the geohex encoding format

So I am using some classes from actual map plugin to convert points from lat/lng to x/y.
I iterate over all polygons and store paths in List in the same order.

import 'dart:ui' as ui;

 ui.Path convertPolygon(Polygon polygon) {
    CustomPoint _customPoint;
    List<Offset> _offsets = new List<Offset>();
    ui.Path _polygonPath = new ui.Path();
    polygon.points.forEach((point) {
      _customPoint =  _mapOptions.crs.latLngToPoint(point, _mapOptions.zoom);
      _offsets.add(new Offset(_customPoint.x, _customPoint.y));
    });
    _polygonPath.addPolygon(_offsets, true);
    return _polygonPath;
  }

and then:

  Polygon getPolygonWithPosition(LatLng _position)  {
    CustomPoint _customPoint = _mapOptions.crs.latLngToPoint(_position, _mapOptions.zoom);
    Offset _offset = new Offset(_customPoint.x, _customPoint.y);
    int _index = -1;
    _polygonPaths.forEach((path) {
      _index ++;
      if (path.contains(_offset)) {
        return;
      }
    });
    return _polygons[index];
  }

@GregorySech
Copy link
Contributor

PSA getPolygonWithPosition does not compile, you need to convert to the classic for(var path in _polygonPaths) loop.

@MichalMisiaszek
Copy link

MichalMisiaszek commented Aug 29, 2019

PSA getPolygonWithPosition does not compile, you need to convert to the classic for(var path in _polygonPaths) loop.

I just changed code in answer from original. Let me fix it. Fixed.

@GregorySech
Copy link
Contributor

GregorySech commented Aug 29, 2019

I do not enjoy being pedantic but using return does not stop functional forEach, the intent is clear tho.
EDIT:
int _index = _polygonPaths.indexWhere((path) => path.contains(_offset));
Could be an alternative to forEach.

@aleffabricio
Copy link

It would be great to have onTap event for polygons and polylines. I also need that feature but I have not be able to find a good way to implemented.

Hello, @spvalencia, this library has been implemented https://github.com/synw/geodraw, sometimes it helps you in some way.

@spvalencia
Copy link

Hi @aleffabricio. Thank you so much. I'm going to check it out.

@joandervieira
Copy link

I found this useful plugin for flutter_map:
https://pub.dev/packages/map_controller

@matyhaty
Copy link

matyhaty commented Feb 1, 2020

@joandervieira Thank you

@X-SLAYER
Copy link

check this package geodesy it have a method to check if a given geo point is in the a polygon

@Anup2712
Copy link

I have done ontap of polygon and show info window using map controller

FlutterMap(
        mapController: mapController,
        options: MapOptions(
          center: LatLng(20.1754, 84.4053),
          zoom: 8,
          onTap: (latlng) {
            showDialog(
              context: context,
              builder: (context) {
                return Dialog(
                  child: Container(
                    height: MediaQuery.of(context).size.height * 0.3,
                    width: MediaQuery.of(context).size.width * 0.2,
                    decoration: BoxDecoration(
                      border: Border.all(
                        color: Colors.blue,
                        width: 8,
                      ),
                    ),
                    child: Align(
                      alignment: Alignment.topLeft,
                      child: Column(
                        children: <Widget>[ 
                          Text(
                            "Latitude : " + plotlat + "",
                            style: _textStyleinfo,
                          ),
                          Divider(
                            color: Colors.black54,
                          ),
                          Text(
                            "Longitude : " + plotlon + "",
                            style: _textStyleinfo,
                          ),
                        ],
                      ),
                    ),
                  ),
                );
              },
            );
          },
        ),
      ),

@iRELGE
Copy link

iRELGE commented Oct 18, 2020

you can use my code i used to know if point is inside polygon i hope it help you. soon i'll share
i code that show you a pop up on map if point is inside polygon:
inside pubspec.yaml: flutter_map and geodesy.

`class PolylinePage extends StatelessWidget {
static const String route = 'polyline';
Geodesy geodesy = Geodesy();
@OverRide
Widget build(BuildContext context) {
// List of point to draw polygon
var points = [
LatLng(51.5, -0.09),
LatLng(53.3498, -6.2603),
LatLng(48.8566, 2.3522),
];
//List of point to draw second polygon
var pointsGradient = [
LatLng(55.5, -0.09),
LatLng(54.3498, -6.2603),
LatLng(52.8566, 2.3522),
LatLng(55.5, -0.09),
LatLng(55.5, -0.09),
];

var polygones = <Polygon>[
  Polygon(points: points, color: Colors.red),
  Polygon(points: pointsGradient)
];

void onPolygon(LatLng point) {

  polygones.forEach((element) {
    // if point is on the polygon isGeoPointInPolygon iS true
    bool isGeoPointInPolygon =
        geodesy.isGeoPointInPolygon(point, element.points);
    if (isGeoPointInPolygon == true) {
      print(element.points);
    }
  });
}

return Scaffold(
  appBar: AppBar(title: Text('Polylines')),
  drawer: buildDrawer(context, PolylinePage.route),
  body: Padding(
    padding: EdgeInsets.all(8.0),
    child: Column(
      children: [
        Padding(
          padding: EdgeInsets.only(top: 8.0, bottom: 8.0),
          child: Text('Polylines'),
        ),
        Flexible(
          child: FlutterMap(
            options: MapOptions(
            ///use on tap to get point coordinates
              onTap: onPolygon,
              plugins: [
           /// import plugin
                TappablePolylineMapPlugin(),
              ],
              center: LatLng(51.5, -0.09),
              zoom: 5.0,
            ),
            layers: [
              TileLayerOptions(
                  urlTemplate:
                      'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
                  subdomains: ['a', 'b', 'c']),
             // to draw polygon on map
              PolygonLayerOptions(polygons: polygones),

             
            ],
          ),
        ),
      ],
    ),
  ),
);

}
}`

@Anup2712
Copy link

Anup2712 commented Oct 19, 2020 via email

@kotrotko
Copy link

@iRELGE Thank you so much for this! You saved me a lot of time and energy. If you have more publications on this issue, please, share them with me.

@iRELGE
Copy link

iRELGE commented Feb 28, 2021

@kotrotko your welcome ill share it soon ,ill share how to show popup if you clock on polygon if you need it

@iRELGE
Copy link

iRELGE commented Feb 28, 2021

@kotrotko please can you marke this issue as closed

@kotrotko
Copy link

kotrotko commented Feb 28, 2021

I'm so sorry status closed is not available now((

@iRELGE
Copy link

iRELGE commented Feb 28, 2021

@kotrotko its ok bro my its about Agriculture draw field and follow the interventions if you have any question contact me

@kotrotko
Copy link

kotrotko commented Feb 28, 2021 via email

@github-actions
Copy link

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.

@JaffaKetchup
Copy link
Member

JaffaKetchup commented Aug 6, 2023

Ah, thanks @ignatz, I've been meaning to put polybool in the docs somewhere, but couldn't find it again (didn't know it belonged to you). EDIT: Just realised my mistake here, ignore me. Already had it in the docs.

In terms of performance, the solutions I've seen took a while to determine the correct polygon - however, I now realize that may have been to do with something I 'fixed' in #1532, where the double tap gesture delayed receipt of the single tap gesture for 250ms (now, if you specify to disallow the double tap gesture, there is no delay). So maybe we need to double check this.

I wonder, would it help to triangulate the polygons? This would help with handling holes (as @ignatz mentioned is a pain point), and from my basic understanding, this is one step of many algorithms anyway. If so, I just so happened to have recently ported a very performant triangulation library to Dart: https://pub.dev/packages/dart_earcut. Interestingly, the original came from the mapbox org - interesting loop back to mapping there :D. I wonder how that might fit into FMTC?

@ignatz
Copy link
Contributor

ignatz commented Aug 6, 2023

Ah, thanks @ignatz, I've been meaning to put polybool in the docs somewhere, but couldn't find it again (didn't know it belonged to you).

It doesn't. It was someones 1:1 javascript to dart robo conversion, which didn't care much for nullability. Unfortunately, the owner has admitted to not be comfortable touching the implementation and only maintains the project's README. So I just spend some non-trivial amount of time going through the entire implementation. In the process I did some research and credit goes to:

  • polybooljs by velipso@, which is based on
  • polybool actionscript by akavel@, which is based on
  • an implementation by Mahir Iqbal, which is based on
  • F. Martinez' (2008) algorithm (Paper)

If you wanted to go down the polybool route, which I'd be careful with*, I would consider at least forking the code.

(* mostly regarding the state of the project. The algorithm itself is pretty rad and battle tested)

I wonder, would it help to triangulate the polygons? This would help with handling holes (as @ignatz mentioned is a pain point), and from my basic understanding, this is one step of many algorithms anyway. If so, I just so happened to have recently ported a very performant triangulation library to Dart: https://pub.dev/packages/dart_earcut. Interestingly, the original came from the mapbox org - interesting loop back to mapping there :D. I wonder how that might fit into FMTC?

Sorry, I didn't want to make it sound like a huge problem (that's why I put it in parenthesis). I just didn't care for it myself, since airspaces typically don't have holes. With polybool, this can be trivially handled as a set-operation. I'm sure the raycasting can account for it too by running both on the hull and the holes separately.

The long term solution is probably best with an R-tree or something similar like a quadtree. Essentially this is what https://github.com/ibrierley/geojson_vector_slicer does behind the scenes (tilecheck, which is kind of a quadtree). The downside with some of these solutions (and why I haven't suggested including in the core) is that they can be hard to maintain.

I've implemented plygon/marker culling with r-trees before. It felt more worthwhile since culling runs on every frame (unlike on-tap) but in practice it wasn't worth it for me... maybe if you have orders of magnitude, i.e. 1 millions, more objects 🤷. For more common numbers the impact is more likely to be negative.

@JaffaKetchup
Copy link
Member

So I just spend some non-trivial amount of time going through the entire implementation

:D


I honestly am not sure what to do here (ping @TesteurManiak @mootw). Leaving it for a plugin is a good way to allow for a massive, performant implementation. Adding to core means more users more happy more quicker.

We can see how @b-cancel's solution works. @ignatz, would you mind sharing your implementation in some way?

@ignatz
Copy link
Contributor

ignatz commented Aug 6, 2023

I honestly am not sure what to do here (ping @TesteurManiak @mootw). Leaving it for a plugin is a good way to allow for a massive, performant implementation. Adding to core means more users more happy more quicker.

Sorry, if I caused confusion. I was trying to say:

  1. I'd love an onTap handler if it fits my needs
  2. Don't worry about performance
  3. Worry about accuracy and make sure to test with weirdly shaped polygons

Any algorithm/implementation that meets #3 will do. I do like the polybool algorithm because I haven't encountered any false postives/negatives and I'm probably also suffering from a good amount of Stockholm syndrome.

@ignatz, would you mind sharing your implementation in some way?

I'm not very protective, I just haven't gotten around to open sourcing it plus a few quirks. I pulled out and cleaned up the relevant part, which is just as straight forward as you'd expect:

List<Polygon> findPolygonCollisions(
  FlutterMapState map,
  List<Polygon> polygons,
  CustomPoint<double> point,
) {
  final latlng = map.pointToLatLng(point)!;

  final hits = <Polygon>[];
  for (final polygon in polygons) {
    // First do a quick pass on the rough bounding box envelope to find candidate hits.
    final bounds = LatLngBounds.fromPoints(polygon.points);
    if (bounds.contains(latlng)) {
      final coordinates = 
          polygon.points.map((c) => polybool.Coordinate(c.longitude, c.latitude)).toList()
      // NOTE: Polybool requires polygons to be closed loops, i.e.: c.first == c.last.
      if (coordinates.first != coordinates.last) {
        coordinates.add(coordinates.first);
      }
      final p = polybool.Polygon(regions: [coordinates]);

      const eps = 1e-7;
      final touch = polybool.Polygon(regions: [
        [
          polybool.Coordinate(latlng.longitude, latlng.latitude),
          polybool.Coordinate(latlng.longitude + eps, latlng.latitude),
          polybool.Coordinate(latlng.longitude + eps, latlng.latitude + eps),
          polybool.Coordinate(latlng.longitude, latlng.latitude + eps),
          polybool.Coordinate(latlng.longitude, latlng.latitude),
        ]
      ]);

      final match = p.intersect(touch).regions.isNotEmpty;
      if (match) {
        hits.add(polygon);
      }
    }
  }
  return hits;
}

Note that I'm returning a list of hits, since polygons are over-lapping and I want to show a list of stacked airspaces under the tap area. More traditionally, a on-tap handler would probably only handle hits.last, i.e. the top-most of the overlapping polygons.

@JaffaKetchup
Copy link
Member

Absolutely no confusion at all :). "Performance" as in accuracy as well as speed.

Thanks for sharing, very much appriciated :)

Maybe I can piece together all the different implementations from this thread and benchmark them without the complications of flutter_map.

@ignatz
Copy link
Contributor

ignatz commented Aug 6, 2023

As someone who likes to experiment and play around, you have my full support. However, I also want to be mindful of your time so let me point this out once and then get off you lawn and let you have fun :)

  • any optimization here will only pay dividends during on-tap events firing (not when panning or zooming).
  • frame drops will only be visible if at the same time something is moving (e.g. an animation playing since the map is neither panning nor zooming during a tap event). More generally any optimizations we can do on the frame render path will have a bigger impact. And any optimization to rasterization even bigger (see screenshot)
  • optimizations to identify candidate polygons quicker than going over every polygon are sort of mood, since that's what's happening anyways on every single frame where missing the frame deadline would be visible: https://github.com/fleaflet/flutter_map/blob/master/lib/src/layer/polygon_layer.dart#L111, e.g. during pans zooms. (when using an R-Tree for polygon culling during every single frame, it would be interesting to see for how many polygons and a given R-Tree implementation we reach the break-even point It will certainly hurt the bottom line. I did benchmark both r_tree and rbush in case you're interested: Fix removal benchmark and add reference benchmarks for RBush to contextualize results. Workiva/r_tree#49).

You can see how its running at 30fps with plenty of head-room on the UI thread solely being throttled by raster performance:

image

@JaffaKetchup
Copy link
Member

Nah, no "lawns" here! You guys are much more in the know about this stuff than me, so I'm happy for the advice :)

Anyway, I've got some other high priority stuff to experiment with - like getting FM v6 ready & debugging why one algorithm generates 30,000 tiles in one area and another generates 2,000,000 in the same area for no apparent reason. So who knows if I'll actually get round to this any time soon.

Anyways, thanks for the code again :)

@b-cancel
Copy link

b-cancel commented Aug 6, 2023

@ignatz thanks for your function! I can't wait to use it

I only have roughly 100 polygons on the screen at any given point
I was planing doing that pre optimization step but simply using bounding boxes with r trees

given your performances test
assuming my polygons don't change after they are initially rendered
is it worth the extra over head to generate an R tree using
https://pub.dev/packages/r_tree
or
https://pub.dev/packages/rbush

@ignatz
Copy link
Contributor

ignatz commented Aug 7, 2023

My intuition is that for N=100 traversing the tree has likely more overhead than going through the list. More importantly, FlutterMap is traversing the full list on every frame anyways. Whatever ends up being faster in any particular case, you're paying the cost of a full scan... (which I've never seen to cost a frame, since rasterization will always push your frame deadlines first)

@JaffaKetchup JaffaKetchup changed the title Polygon onTap? [FEATURE] Tappable Polygons Oct 20, 2023
@Nico04
Copy link

Nico04 commented Oct 27, 2023

Is there any chance we will see this implemented in the v6 ?
Thanks !

@josxha josxha linked a pull request Nov 26, 2023 that will close this issue
@JaffaKetchup JaffaKetchup reopened this Dec 15, 2023
@JaffaKetchup
Copy link
Member

JaffaKetchup commented Dec 15, 2023

Hit detectable Polylines have now been added 🎉, thanks to #1728!
The same API framework from there will likely be used here as well, when we have the detection algorithm iteself. Check out the new API in the 'master' branch example app and the "v7 (beta)" docs!

PS. Apologies for the notifications!

@V3ntus
Copy link

V3ntus commented Jan 26, 2024

I don't understand, the issue/PR linked is for Polylines not Polygons. Am I just not aware the the Polylines in this library can be closed to form a filled polygon area? Duh forgot Polygon's already exist in this library.

@JaffaKetchup
Copy link
Member

Hi @V3ntus,
No, the comment was just making readers aware that progress is being made in the region, and what the API could look like when implemented. Also, if anyone wants to make a PR, it clearly shows how it should be implemented.

@V3ntus
Copy link

V3ntus commented Jan 26, 2024

Gotcha, much appreciated! Well that is exciting, glad to see development there. I've been using third-party plugins to achieve polygon uses, so if I can do it "natively" here in the future, that would be awesome.

@JaffaKetchup JaffaKetchup added P: 1 (important) S: core Scoped to the core flutter_map functionality and removed P: 2 (soon™?) labels Jan 31, 2024
@JaffaKetchup
Copy link
Member

JaffaKetchup commented Jan 31, 2024

Ok, as we're looking into implementing it in the next version, I'm trying to find an implementation we can use by reading through the history again here.

As far as I'm concerned, the most recent one by @ignatz, see #385 (comment) looks pretty good. (I'm a bit more understanding of how everything works now we've done it with polylines.) So, if no-one else has an issue, I will likely implement this algorithm at some point after the currenly opened polygon PRs are closed.

@JaffaKetchup JaffaKetchup pinned this issue Jan 31, 2024
@JaffaKetchup JaffaKetchup changed the title [FEATURE] Tappable Polygons [FEATURE] Interactive Polygons Jan 31, 2024
@josxha josxha unpinned this issue Feb 14, 2024
@josxha josxha added this to the v7.0 milestone Feb 14, 2024
@JaffaKetchup
Copy link
Member

We've implemented this! Experiment with it and all the other features we've been hard at work implementing on the 'master' branch: we'd love to hear your feedback! Documentation is available at https://docs.fleaflet.dev/v/v7-beta/layers/layer-interactivity.

If you've been waiting for this for a while, please consider donating https://docs.fleaflet.dev/supporters#support-us!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature This issue requests a new feature P: 1 (important) S: core Scoped to the core flutter_map functionality
Projects
Archived in project