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

Implement a feature to remove elements that exceed the max_feature_count limit based on their spatial distribution and density. #993

Open
CrazyBug-11 opened this issue Aug 23, 2024 · 6 comments

Comments

@CrazyBug-11
Copy link

It would be helpful to provide a max_feature_count parameter that can remove elements exceeding the limit based on the density within the tile, ensuring a more uniform appearance across the entire tile.

@msbarry
Copy link
Contributor

msbarry commented Aug 23, 2024

For points the label grid size/limit does this. It divides each tile into a grid of a certain size then limits the number of features that can go in each cell. Maybe if we just add the capability for this to apply to small polygons too that would accomplish what you're trying to do?

@CrazyBug-11
Copy link
Author

My dataset consists of polygon (area) data, not point data. The current issue is that setting min_size=0 results in too many features at lower zoom levels, which increases memory requirements. However, setting min_size=256/4096 causes some areas to be blank, and in such cases, the tile size at lower zoom levels inflates to 20-30MB. Therefore, I hope to have a feature that can evenly sample features within a tile, limit the number of features, and also control the tile size.

If source code modification is necessary, I would appreciate any advice you can provide. Thank you very much.

@msbarry
Copy link
Contributor

msbarry commented Aug 23, 2024

On each feature you can call setPointLabelGridPixelSize and setPointLabelGridLimit (there are a few aliases) and FeatureRenderer.renderPoint calls getPointLabelGridPixelSizeAtZoom and getPointLabelGridLimitAtZoom to get these values when generating vector tile features from the input feature. We'd have to add similar handling to renderLineOrPolygon probably using either the first or center point of the polygon or line so that it passes the groupInfo parameter into encodeAndEmitFeature. The rest of the pipeline should handle size/limit automatically, but we should add an end to end test to PlanetilerTests to be sure.

Once this is implemented it would be good to rename all of the setPointLabelGrid... methods to setLabelGrid... but we'd have to add the new methods, mark old as deprecated then remove later since it changes the external API.

Feel free to submit a PR, this would be useful for others as well. For example the openmaptiles profile should be limiting mountain_peak linestrings (cliffs and ridges) using label grid like this, but because it's not implemented we just end up with very crowded tiles.

@CrazyBug-11

This comment was marked as outdated.

@CrazyBug-11
Copy link
Author

CrazyBug-11 commented Aug 24, 2024

I have added the following code to calculate the centroid of each feature after executing sliceIntoTiles(). However, after testing, I found that the results did not change, and the label grid functionality is still not implemented as expected.
Currently, the pipeline does not automatically handle size/limit constraints. The constraints are set using methods like setPointLabelGridPixelSize and setPointLabelGridLimit.

Could you please advise on which other parts need to be modified besides this?

private void writeTileFeatures_labelGrid(int zoom, long id, FeatureCollector.Feature feature, TiledGeometry sliced,
    Map<String, Object> attrs) {
    int emitted = 0;
    RenderedFeature.Group groupInfo = null;
    for (var entry : sliced.getTileData().entrySet()) {
      TileCoord tile = entry.getKey();
      try {
        List<List<CoordinateSequence>> geoms = entry.getValue();
        Coordinate centroid;
        Geometry geom;
        int scale = 0;
        PrecisionModel precision = null;
        if (feature.isPolygon()) {
          geom = GeometryCoordinateSequences.reassemblePolygons(geoms);

          precision = GeoUtils.getPrecision(zoom, config.maxzoom());
          geom = GeoUtils.snapAndFixPolygon(geom, stats, "render", precision);

          centroid = getGlobalCoord(tile, geom.getCentroid());
          geom = geom.reverse();
        } else {
          geom = GeometryCoordinateSequences.reassembleLineStrings(geoms);
          centroid = getGlobalCoord(tile, geom.getCentroid());

          scale = Math.max(config.maxzoom(), 14) - zoom;

          scale = Math.min(31 - 14, scale);
        }

        if (!geom.isEmpty()) {
          // for "label grid" point density limiting, compute the grid square that this point sits in
          // only valid if not a multipoint
          boolean hasLabelGrid = feature.hasLabelGrid();
          if (hasLabelGrid) {
            int tilesAtZoom = 1 << zoom;
           Coordinate coord = new Coordinate(GeoUtils.getWorldX(centroid.x) * tilesAtZoom,
              GeoUtils.getWorldY(centroid.y) * tilesAtZoom);
            double labelGridTileSize = feature.getPointLabelGridPixelSizeAtZoom(zoom) / 256d;
            groupInfo = labelGridTileSize < 1d / 4096d ? null : new RenderedFeature.Group(
              GeoUtils.labelGridId(tilesAtZoom, labelGridTileSize, coord),
              feature.getPointLabelGridLimitAtZoom(zoom)
            );
          }
          encodeAndEmitFeature(feature, id, attrs, tile, geom, groupInfo, scale);
          emitted++;
        }
      } catch (GeometryException e) {
        e.log(stats, "write_tile_features", "Error writing tile " + tile + " feature " + feature);
      }
    }

    // polygons that span multiple tiles contain detail about the outer edges separate from the filled tiles, so emit
    // filled tiles now
    if (feature.isPolygon()) {
      emitted += emitFilledTiles_labelGrid(id, feature, sliced, groupInfo);
    }

    stats.emittedFeatures(zoom, feature.getLayer(), emitted);
  }

  private Coordinate getGlobalCoord(TileCoord tileCoord, Point coordinate) {
    double worldWidthAtZoom = Math.pow(2, tileCoord.z());
    double minX = tileCoord.x() / worldWidthAtZoom;
    double maxX = (tileCoord.x() + 1) / worldWidthAtZoom;
    double minY = (tileCoord.y() + 1) / worldWidthAtZoom;
    double maxY = tileCoord.y() / worldWidthAtZoom;

    double relativeX = coordinate.getX() / 256.0;
    double relativeY = coordinate.getY() / 256.0;

    double worldX = minX + relativeX * (maxX - minX);
    double worldY = maxY - relativeY * (maxY - minY);

    double lon = normalizeLongitude(GeoUtils.getWorldLon(worldX));
    double lat = GeoUtils.getWorldLat(worldY);

    return new Coordinate(lon, lat);
  }

  private static double normalizeLongitude(double lon) {
    while (lon > 180) {
      lon -= 360;
    }
    while (lon < -180) {
      lon += 360;
    }
    return lon;
  }

I noticed that parameters related to grid limits are set in the encodeValue method. However, I could not find where the logic to delete features that exceed the limit is implemented. Could you please provide guidance on this?

if (group != null) {
        packer.packLong(group.group());
        packer.packInt(group.limit());
      }
      packe

@msbarry
Copy link
Contributor

msbarry commented Aug 24, 2024

Can you post what you have in a PR and I can test from there?

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

2 participants