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

Drawing over tile #941

Closed
antonshkurenko opened this issue Jun 18, 2021 · 29 comments
Closed

Drawing over tile #941

antonshkurenko opened this issue Jun 18, 2021 · 29 comments

Comments

@antonshkurenko
Copy link

Hi, I need to modify a tile based on coords. For this purpose I need an access to canvas before display. Kind of a callback, which would be called right before tile is drawn:

Pseudocode:


final dots = [...];

TileLayerOptions(
  tileInterceptor: (tileImage, tileBounds) {
    final canvas = Canvas(tileImage)
    
    dots.forEach { dot ->
          if (tileBounds contain dot) {
          
            canvas.drawCircle(dot)
          }
    }
  }
)

For some reason, I can't use overlay and I need exactly the access to canvas to do all modifications there. Thanks for your help

@ibrierley
Copy link
Collaborator

Could you do it like the polyline/polygon layer code ? It's not quite clear why you need a tile callback or whatever (those have culling in the code if that's what you are trying to do).

@antonshkurenko
Copy link
Author

I need to modify exactly bitmap of the tile, cutting a hole in it, for example, etc

@antonshkurenko
Copy link
Author

My map layer isn't main layer and under it I have data which should be visible in some points and for this I need to cut map and make it transparent in some places

@ibrierley
Copy link
Collaborator

Well, in the absence of any other replied, I'm not quite sure how to do it easily as is. The current system downloads as a png and displays it as an image widget. I'm not sure of a "simple" solution.

However, you could probably adapt it and display that png with drawImage in a canvas. That may be easier to "edit", but may involve a fair bit of work to get their, not quite sure.

You could also probably hack my vector tile code at https://github.com/ibrierley/flutter_map_vector_tiles (you can set drawImages: true and it will paint multiple tile images to a single canvas) but it's very beta atm and a lot of stuff will probably change (but equally I'm probably not against adding some simple bits like callbacks if helpful). It may depend slightly if you are looking for a single display edit, or want to specifically edit separate tiles for some reason (e.g so you could save that specific tile for reload).

@eveetc eveetc mentioned this issue Jul 3, 2021
@ibrierley
Copy link
Collaborator

Just thinking about this some more as @eveetc mentioned it in the other issue as well..

If one needs to cut a hole in the "image", do you need access to the canvas at all ? Could you essentially be a tileprovider or something that takes a url grabs the png and cuts a hole in it ? Or use a clip or a mask maybe (will probably be performance implications).

I think these issues are a case of the devil is in the detail, as to where the solution likely lies.

@eveetc
Copy link

eveetc commented Jul 4, 2021

Just thinking about this some more as @eveetc mentioned it in the other issue as well..

If one needs to cut a hole in the "image", do you need access to the canvas at all ? Could you essentially be a tileprovider or something that takes a url grabs the png and cuts a hole in it ? Or use a clip or a mask maybe (will probably be performance implications).

I think these issues are a case of the devil is in the detail, as to where the solution likely lies.

Well essentially yes, it 100% works manipulating the tileprovider or tile location, when specific tiles are requested.
The thing I got the only problem here is, to manually trigger a rebuild of the visible bounds without triggering the move/rotate functionality, as the cache is still valid and even if the tiles shouldnt be valid anymore, flutter_map doesnt know it. Removing the whole tilecache would be also unperformant as hell.
And the other annoying thing is. When you want to connect cut holes, you need to calculate the other tilekeys according to the zoomlevel.
That's why I thought cutting the whole tiles on a vector basis would be easier.

@ibrierley
Copy link
Collaborator

Do you have any similar example out there somewhere of a similar type effect you are after ? (still failing to visualise it properly)

@antonshkurenko
Copy link
Author

@ibrierley

There is a way to do that via leaflet:

on event (class appears) "leaflet-tile-loaded" you can get a canvas and do whatever you want

Can't share more, sorry

@antonshkurenko
Copy link
Author

I tried today very naive approach (I'm open to any comment, I'm sure it has its downsides)

For now, it only renders a red square at the centre of the tile

class CustomNetworkTileProvider extends TileProvider {
  const CustomNetworkTileProvider();

  @override
  ImageProvider getImage(Coords<num> coords, TileLayerOptions options) {
    return CustomImageProvider(NetworkImage(getTileUrl(coords, options)));
  }
}

class CustomImageProvider extends ImageProvider {
  final ImageProvider inner;

  CustomImageProvider(this.inner);

  @override
  ImageStreamCompleter load(Object key, DecoderCallback decode) {
    return inner.load(key, (bytes,
        {allowUpscaling = true, cacheHeight, cacheWidth}) async {
      var imageCodec = await ui.instantiateImageCodec(bytes);
      var frame = await imageCodec.getNextFrame();
      var image = frame.image;

      ui.PictureRecorder recorder = ui.PictureRecorder();
      Canvas c = Canvas(recorder);

      var paint = ui.Paint();
      c.drawImage(image, ui.Offset(0.0, 0.0), paint);

      paint.color = Colors.red;

      c.drawRect(
          ui.Rect.fromCenter(
            center: ui.Offset(image.width / 2, image.height / 2),
            width: image.width / 4,
            height: image.height / 4,
          ),
          paint);

      var newImageByteData = await recorder
          .endRecording()
          .toImage(image.width, image.height)
          .then((value) => value.toByteData(format: ui.ImageByteFormat.png));

      var newBytes = newImageByteData?.buffer != null
          ? Uint8List.view(newImageByteData!.buffer)
          : Uint8List(0);

      return decode.call(newBytes,
          cacheWidth: cacheWidth,
          cacheHeight: cacheHeight,
          allowUpscaling: allowUpscaling);
    });
  }

  @override
  Future<Object> obtainKey(ImageConfiguration configuration) {
    return inner.obtainKey(configuration);
  }
}

@antonshkurenko
Copy link
Author

antonshkurenko commented Jul 23, 2021

One more question regarding this, how to convert between coordinates? For example, right now what I have:

  1. LatLng
  2. Crs method to convert to XY coordinates
  3. But XY coordinates which are sent to OpenStreetMap (any other service with tile provider) are completely different.

How to understand bounds of 256x256 (or 512x512) tile in terms of XY (map's real, not that one, sent to tile provider) or LatLng? So I could draw exactly on the image in correct positions?

What I want:

I have LatLng, I want to draw there a thing –> I call some method to convert to coordinates, I understand if they intersect with current tile image or not and draw on it.

What I currently found (data I can manipulate somehow):

  1. Screen size
  2. x, y, zoom (taken from URL)
  3. Map bounds (via map controller)
  4. latlng to (and back) XY converter

@JaffaKetchup
Copy link
Member

JaffaKetchup commented Jul 23, 2021

Not sure if this helps, but https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames (for number 3)

@antonshkurenko
Copy link
Author

Very cool article! Huge thanks, I have a couple of questions (hope you know some answers, ha-ha)

  1. I've heard that the closer you are to North, the bigger the pixel/meter. What I mean, does some unique formula (like that in the article) work the same for something near Kenya and Sweden?
  2. It's kind of an addition to question above, how to translate 256 pixels to coords/latlng so I could determine correct position in tile?

@JaffaKetchup
Copy link
Member

JaffaKetchup commented Jul 23, 2021

No problem!

Well, I'll answer question 2 first:

I use this code to convert a LatLng to a tile using a tile size (256) and a map projection (CRS):

final Crs crs = Epsg3857();
final nwPoint = crs
          .latLngToPoint(bounds.northWest, zoomLevel.toDouble())
          .unscaleBy(tileSize)
          .floor();
final sePoint = crs
              .latLngToPoint(bounds.southEast, zoomLevel.toDouble())
              .unscaleBy(tileSize)
              .ceil() -
          CustomPoint(1, 1);

However this is the opposite of what you need. I'd have a look at the crs.pointToLatLng method which claims:

Converts a map point to the sphere coordinate (at a certain zoom).

I can't guide you on exactly how to do this, because I stole this particular code for my plugin from flutter_map so I have little idea how it works. But if you can't figure that out, you can just do it using the maths on that page.

Now in answer to question 1:

Yes, that is true, because of a projection. Think of it like this: If you paint the Earth onto a balloon using ink that can come off, put the balloon into a tube, then blow it up, the Earth will now be on the tube. However, the top and bottom will be squashed, because you can't make a sphere into a flat surface.

I strongly encourage you to watch this rather silly but informative video: https://www.youtube.com/watch?v=jtBV3GgQLg8, which gives more insight into this and a few other problems you'll likely come across using a mapping library.

Therefore, (and don't quote me on this part, I'm less comfortable here) using a CRS projection above is recommended. Whether this deals with this problem, I don't know.

You'll see at the bottom of the 'Zoom Levels' table on that page a comment saying this:

While the width (longitude) in degrees is constant, given a zoom level, for all tiles, this does not happen for the height. In general, tiles belonging to the same row have equal height in degrees, but it decreases moving from the equator to the poles.

It then advises you to check this page, which you should definitely do: https://wiki.openstreetmap.org/wiki/Zoom_levels, it gives you much more information about zoom levels and m/pixels.

I have no idea if you'll need this, but converting between device pixels (what OSM uses) and logical pixels (what Flutter uses) can be done using this formula: https://stackoverflow.com/a/60901354/11846040


Hope that covers some questions for you, sorry if it didn't help or was wrong (which is a chance)!

@antonshkurenko
Copy link
Author

Yeah, sounds like drawing over an image is much harder, than I thought

@antonshkurenko
Copy link
Author

My dart code:

double clipByRange(double n, double range) {
  return n % range;
}

const TILE_SIZE = 512;

// THERE IS NO SINH METHOD IN DART LIB
// LatLng pixelXYToLatLng(int pixelX, int pixelY, int zoom) {
//   final mapSize = pow(2, zoom) * TILE_SIZE;
//   final tileX = pixelX ~/ TILE_SIZE;
//   final tileY = pixelY ~/ TILE_SIZE;
//
//   final n = pi -
//       2 *
//           pi *
//           (clipByRange(pixelY.toDouble(), mapSize - 1) / TILE_SIZE) /
//           pow(2, zoom);
//
//   final lng =
//       clipByRange(pixelX.toDouble(), mapSize - 1) * 360 / pow(2, zoom) - 180;
//
//   final lat = (180 / pi) * atan(sinh(n));
//
//   return LatLng(lat, lng);
// }

PixelPoint latLngToPixelXY(LatLng latLng, int zoom) {
  final mapSize = pow(2, zoom) * TILE_SIZE;

  final x = (latLng.longitude + 180) / 360 * (1 << zoom);

  final y = (1 -
          log(tan(latLng.latitudeInRad) + 1 / cos(latLng.latitudeInRad)) / pi) /
      2 *
      (1 << zoom);

  final tileX = x.toInt();
  final tileY = y.toInt();

  final pixelXInTile = (x - tileX) * TILE_SIZE;
  final pixelYInTile = (y - tileY) * TILE_SIZE;

  final pixelX = clipByRange(tileX * TILE_SIZE + pixelXInTile, mapSize - 1);
  final pixelY = clipByRange(tileY * TILE_SIZE + pixelYInTile, mapSize - 1);

  return PixelPoint(
    tileX.toInt(),
    tileY.toInt(),
    pixelX,
    pixelY,
    pixelXInTile,
    pixelYInTile,
  );
}

class PixelPoint {
  final int tileX;
  final int tileY;

  final double pixelX;
  final double pixelY;

  final double pixelInTileX;
  final double pixelInTileY;

  PixelPoint(this.tileX, this.tileY, this.pixelX, this.pixelY,
      this.pixelInTileX, this.pixelInTileY);

  @override
  String toString() {
    return 'PixelPoint{tileX: $tileX, tileY: $tileY, pixelX: $pixelX, pixelY: $pixelY, pixelInTileX: $pixelInTileX, pixelInTileY: $pixelInTileY}';
  }
}

@ibrierley
Copy link
Collaborator

Being an idiot here, what's the difference between this and project/unproject ?

@antonshkurenko
Copy link
Author

@ibrierley what do you mean?

@ibrierley
Copy link
Collaborator

Part of the essential code for leaflet/flutter_map is based on projections between screen and points, for example if you look at https://github.com/fleaflet/flutter_map/blob/master/lib/src/geo/crs/crs.dart and the crs project/unproject methods (line 407 & 418).

@antonshkurenko
Copy link
Author

antonshkurenko commented Jul 25, 2021

Actually I've tried this code, but it didn't help me, so I didn't dive into that. Don't know what to say

(Or maybe I didn't recognize something, so if I'm wrong, please point me)

@ibrierley
Copy link
Collaborator

Just to expand a bit, to access those, I think normally you would access them via a plugin and use map.project(latLng). But you may be able to access them from wherever you are trying. (you may be able to access it direct though as well)

@antonshkurenko
Copy link
Author

One more question 😅

Does this library have a method to forcefully update tiles? Since I draw on them directly I need a method to mark tiles as "deprecated" or "dirty" or and invalidate them

@JaffaKetchup
Copy link
Member

Unfortunately this is not yet possible, please see #667 for a PR with this feature added.

@antonshkurenko
Copy link
Author

Can we (as a hack) add a fake param to additionalOptions and update it? As I understand, update of this map forces tiles to redraw

@JaffaKetchup
Copy link
Member

I'm not sure, I guess it might be possible? If you will try it, let me know if it works! :)

@antonshkurenko
Copy link
Author

Wanted to update my comment, but you've answered already, so I'll put it here: it didn't work :)

@JaffaKetchup
Copy link
Member

Oh shame. Guess we'll have to 'protest' to merge that PR then!

@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.

@github-actions github-actions bot added the Stale label Aug 29, 2021
@github-actions
Copy link

github-actions bot commented Sep 4, 2021

This issue was closed because it has been stalled for 5 days with no activity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants