H3 - How to get center lat long of a multi polygon? - leaflet

I have created Multi polygons using h3SetToMultiPolygon from the list of H3 cell ids as shown below:
Now I want to get the center (I know it's not a perfect shape to get the center, but a rough one should be fine) of the Multi Polygon which I am unable to get using any H3 or leaflet methods.
I want to show a marker at the center of the boundary of the multi polygons as shown below roughly:
Any help would be appreciated.
Here is my code:
const clusterBoundaryCoordinates = [];
clusters?.forEach((element) => {
clusterBoundaryCoordinates.push(
h3.h3SetToMultiPolygon(element.cellIdsArr, true)
);
});

This isn't really specific to H3 - in general, you want an algorithm that can give you the centroid of a polygon, e.g. #turf/centroid. There are different centroid algorithms based on the use case - e.g. you may or may not require the centroid to be inside a concave polygon (think a C shape) or within one of many small polygons in a multipolygon (think an archipelago).
There's an interesting H3-based option here as well, though I don't know offhand how to make it performant. You can find the center cell by finding the cell whose mean distance to all other cells is lowest, e.g.
let min = Infinity;
let center = null;
for (const cell of cells) {
let total = 0;
for (const neighbor of cells) {
total += h3.h3Distance(cell, neighbor);
}
if (total < min) {
min = total;
center = cell;
}
}
This will always give you a cell in your set, so it will always be within your area, and should give a reasonable value even for pathological shapes. But, because the algorithm is O(n^2), it's going to be very slow for large sets of cells.

I found the solution. I didn't notice earlier that to find the center, I can utilize the lat longs which I receive when I do h3SetToMultiPolygon (it returns the lat long in reverse order).
const clusterBoundaryCoordinates = [];
clusters?.forEach((element) => {
clusterBoundaryCoordinates.push(
h3.h3SetToMultiPolygon(element.cellIdsArr, true); //it returns the latlong in reverse order, so need to reverse it
);
});
const getCentroid = (arr) => {
return arr.reduce(
(x, y) => {
return [x[0] + y[0] / arr.length, x[1] + y[1] / arr.length];
},
[0, 0]
);
};
const centroid = getCentroid(clusterBoundaryCoordinates);

Related

Moving an object along a path with constant speed

I have an object path composed by a polyline (3D point array) with points VERY unevenly distributed. I need to move an object at constant speed using a timer with interval set at 10 ms.
Unevenly distributed points produce variable speed to the human eye. So now I need to decide how to treat this long array of 3D points.
The first idea I got was to subdivide long segments in smaller parts. It works better but where points are jam-packed the problem persists.
What's the best approach in these cases? Another idea, could be to simplify the original path using Ramer–Douglas–Peucker algorithm, then to subdivide it evenly again but I'm not sure if it will fully resolve my problem.
This should be a fairly common problem in many areas of the 3D graphics, so does a proven approach exist?
I made a JavaScript pen for you https://codepen.io/dawken/pen/eYpxRmN?editors=0010 but it should be very similar in any other language. Click on the rect to add points.
You have to maintain a time dependent distance with constant speed, something like this:
const t = currentTime - startTime;
const distance = (t * speed) % totalLength;
Then you have to find the two points in the path such that the current distance is intermediate between the "distance" on the path; you store the "distance from start of the path" on each point {x, y, distanceFromStart}. The first point points[i] such that distance < points[i].distanceFromStart is your destination; the point before that points[i - 1] is your source. You need to interpolate linearly between them.
Assuming that you have no duplicate points (otherwise you get a division by zero) you could do something like this.
for (let i = 0; i < points.length; i++) {
if (distance < points[i].distanceFromStart) {
const pFrom = points[i - 1];
const pTo = points[i];
const f = (distance - pFrom.distanceFromStart) / (pTo.distanceFromStart- pFrom.distanceFromStart);
const x = pFrom.x + (pTo.x - pFrom.x) * f;
const y = pFrom.y + (pTo.y - pFrom.y) * f;
ctx.fillRect(x - 1, y - 1, 3, 3);
break;
}
}
See this pen. Click on the rectangle to add points: https://codepen.io/dawken/pen/eYpxRmN?editors=0010

Get Bounding Box used By queryRenderedFeatures

I have an application that serves vector tiles. The features in the tiles are clickable. When a user clicks the map, I pass mapbox-gl's queryRenderedFeatures a 5 x 5px bounding box around the clicked point.
Is there a way to ascertain the lat-lon bounding box that mapbox uses to query its cached tiles? I would like to have this bounding box so I can query the database for the features around the clicked point. I can use the ids of the features in the vector tiles, but this becomes cumbersome/untenable when there are 1000s of features.
Here's how I am getting features near point, where:
map is the mapbox map object
mapboxLayers are the names of the layers I want to query
point is point property of the click event
export const getMapFeaturesNearPoint = ({ map, mapboxLayers, point }) => {
const { x, y } = point;
const halfPixels = 5;
// set bbox as 5px rectangle around clicked point
const bbox = [
[x - halfPixels, y - halfPixels],
[x + halfPixels, y + halfPixels],
];
const features = map.queryRenderedFeatures(bbox, { layers: [...mapboxLayers] })
return features;
};
Note: I have tried doing the following with the bbox defined above: bbox.map(pt => map.unproject(pt)) to get the lat lon bounding box. From my examination of the mapboxgl source code, it seems the process to unproject queryRenderedFeatures coordinates is a bit more complex than that.
Not very clear to me what you are trying to get, but conceptually speaking, what any queryRenderedFeatures is doing with no filters, is basically find all the tiles in the cache (the ones cached are in the viewport), then per tile, get all the features on them, and merge them all into the result. So, if you use queryRenderedFeatures with no arguments or with only a options argument is equivalent to passing a bounding box encompassing the entire map viewport.
You can check the source code of query_features.js file in Mapbox github repo
With those features in the viewport, it would be easy to reduce them to a lnglat bounding box through a .reduce function that you can use, for instance, as a bounding box to use map.fitBounds.
var coordinates = points;
var bounds = coordinates.reduce(function(bounds, coord) {
return bounds.extend(coord);
}, new mapboxgl.LngLatBounds(coordinates[0], coordinates[0]));
map.fitBounds(bounds, {
padding: { top: 50, bottom: 50, left: 50, right: 50 },
easing(t) {
return t * (2 - t);
}
});
But as said, not sure if that's what you are looking for.
Other option would be to use the canvas width and height and unproject them... I found this code while ago,
const canvas = map.getCanvas()
const w = canvas.width
const h = canvas.height
const cUL = map.unproject ([0,0]).toArray()
const cUR = map.unproject ([w,0]).toArray()
const cLR = map.unproject ([w,h]).toArray()
const cLL = map.unproject ([0,h]).toArray()
const coordinates = [cUL,cUR,cLR,cLL,cUL]

Leaflet latLngToContainerPoint and containerPointToLatLng not reciprocal?

Anybody know why the following is not reciprocal? latLng and new
var point = dispmap.latLngToContainerPoint(latlng);
var newPoint = L.point([point.x, point.y]);
var newLatLng = dispmap.containerPointToLatLng(newPoint);
When I execute this code I send in latlng=(26.75529,-80.93581)
newLatLng, which by inspection of the code above I would expect to reciprocate gives back...
newLatLng = (26.75542,-80.93628)
I'm wanting to array some markers with identical lat-lons around the shared spot on a map, and bumping each by some screen coordinates looks like the best method based on some blog/issue reading I've done.
I'm, "close" to what I want to achieve, but as I try to validate what these leaflet calls are doing for me I hit the fundamental question above.
They can't be ...
Latitude and longitude are float values while x and y are integer values.
This means that there are an (theoretically) infinite number of latlng's and a rather small number of points on your view (width * height).
Furthermore, I'm not sure how you define identical latlng's; the best you can't to is to speak of proximity.
If I read between the lines, identical may mean that the markers overlap. Then the best way is to have a look how Leaflet.MarkerCluster are tackling with the problem.
I was able to achieve my desired result by altering zoom level to avoid pixel-point quantization effects on my translations. The screenshot below illustrates an orange and two green circle markers that represent an identical lat-lon, but I want the green arrayed around the orange in a circular fashion...in this example there are only 2 green.
I perform simple circular array math with an angular step size of PI/4 in this example. The KEY to getting the visual effect correct is the "dispmap.setZoom(dispmap._layersMaxZoom)" call BEFORE I do the math, and then I invoke "dispmap.setZoom(mats.zoom)" after the math, which will give the user the desired zoom level as specified by variable mats.zoom.
var arrayRad=20;
var dtheta=Math.PI/4;
var theta=0;
dispmap.setZoom(dispmap._layersMaxZoom)
L.geoJson(JSON.parse(mats.intendeds), {
pointToLayer: function (feature, latlng) {
var point = dispmap.latLngToContainerPoint(latlng);
dx = arrayRad*Math.cos(theta);
dy = arrayRad*Math.sin(theta);
theta += dtheta;
var newPoint = L.point([point.x + dx, point.y+ dy]);
var newLatLng = dispmap.containerPointToLatLng(newPoint);
return L.circleMarker(newLatLng, intendedDeliveryLocationMarkerOptions);
}, onEachFeature: onEachIntendedLocFeature }).addTo(dispmap);
dispmap.setZoom(mats.zoom);
Sample screen shot at max zoom level: 2 arrayed markers

Mapbox Overlapping Circles

Does anyone know a way to make overlapping circles in mapbox show the same color and only have the border around the outer edge display?
I have this:
And I made this in photoshop for what I want:
While I don't think there is a way to style all the circles to show their group outline, you can achieve the effect you want by creating a union of all the circle geometries and applying your style to that. Unfortunately, Leaflet's L.circle class offers no way to access a circle's geometry beyond the center point, and to perform a union, you need the path of the circle itself. Fortunately, there is Leaflet Geodesy and its LGeo.circle class, which produces circular polygons with a given radius and number of segments. Once you have these polygon representations of your circles, you can use turf.union to produce the outline you want.
Say you are starting with a layer of points called pointLayer (this can be a L.geoJson, L.mapbox.featureLayer, or any other class that inherits the .eachLayer method). You can then iterate over the features, creating a circular polygon for each of them and adding it to a temporary layer group, like this:
var circleLayer = L.layerGroup();
var radius = 5000
var opts = {
parts: 144
};
pointLayer.eachLayer(function(layer) {
LGeo.circle(layer.getLatLng(), radius, opts).addTo(circleLayer);
});
where radius is in meters and the parts option is the number of segments you want your polygons to have. Next, use the .getLayers method to get an array of all the layers in the temporary group, then iterate over that to create a union of all the features:
var circleUnion = unify(circleLayer.getLayers()).addTo(map);
function unify(polyList) {
for (var i = 0; i < polyList.length; ++i) {
if (i == 0) {
var unionTemp = polyList[i].toGeoJSON();
} else {
unionTemp = turf.union(unionTemp, polyList[i].toGeoJSON());
}
}
return L.geoJson(unionTemp, {style: unionStyle});
}
where unionStyle is whatever style you want to apply to your newly-combined circles. Here is an example fiddle showing all this with some random data:
http://fiddle.jshell.net/nathansnider/L2d626hn/

How to get intersection area coordinates of two polygons on General Polygon Clipper(GPC)?

I'm using nutiteq library to draw polygons and getting the coordinates of the polygons with .getVertexList() command. Then I cast these coordinates to an array list . Then I cast these coordinates to another polygon list. GPC is calculating the intersection, union, XOR and difference areas integer values. Then I need to highlight the process area so I need processed areas coordinates but I can't get these coordinates directly from GPC.
The code I'm using for the area calculation is below. What should I do to get the coordinates of result polygon?. (I can't cast the coordinates directly by the way as you can see here...)
Thanks in advance.
public void IntersectionButton(View view) {
VectorElement selectedElement = mapView.getSelectedElement();
List<?> VisibleElements = selectedElement.getLayer().getVisibleElements();
ArrayList<Poly> polyList = new ArrayList<Poly>();
for (Object obj : VisibleElements) {
if (obj instanceof Polygon) {
Polygon poly = (Polygon) obj;
List<MapPos> geoList = poly.getVertexList();
Poly p = new PolyDefault();
for (MapPos pos : geoList) {
p.add(pos.x, pos.y);
}
polyList.add(p);
}
}
PolyDefault result = (PolyDefault) Clip.intersection(polyList.get(0), polyList.get(1));
int area = (int) (((int) result.getArea()) * (0.57417));
The result polygon seems to have all the methods you need:
getNumPoints() to get number of outer polygon points.
getX(i) to get X of specific outer polygon point, and getY(i) for Y.
getNumInnerPoly() to get number of holes in the polygon
getInnerPoly(i) to get specific hole. You iterate through hole similar way like outer polygon
You can construct new Nutiteq Polygon from this data, create list of MapPos for outer and list of list of MapPos for inner polygons (holes). What are values of X and Y, do they need further processing, is another question what you can investigate.