Markers along a route in HERE Maps, RouteBoxer port - here-api

I am looking to get this:
When I used Google Maps API there was a "plugin" called RouteBoxer that created a grid along the route and after I could build a query using the bounds of each rectangle.
I have seen that there is a port of that Google Library for LeafLet but I haven't found a port of RouteBoxer library for HERE Maps.
Do exist another way to do that in HERE Maps?
Extended explanation of routeboxer way: how it works
Thank you,
Regards,
WIP EDIT: I'm porting the Google Library by myself. I almost get it but left calculate box intersections correctly...I am here just right now:

Finally I have ported by myself the original Google Routeboxer library to Here Maps v3 3.0 successfully.
Result:
Tips for classes:
google.maps.LatLngBounds is similar to H.geo.Rect
google.maps.LatLng is similar to H.geo.Point
Tips for methods:
google.maps.LatLngBounds.extend is similar to H.geo.Rect.mergeLatLng
New methods prototyped for H.geo.Rect:
getNorthEast() and getSouthWest()
Usage:
Include here-routeboxer.js after Here Maps JS api call and:
// after recieve route response
var route = result.response.route[0];
var path = route.shape;
var path_= [];
// Transform original path to an array of H.geo.Point
// TODO: create a simplified path for better perfomance
path.forEach(function(point) {
var parts = point.split(',');
path_.push(new H.geo.Point(parts[0], parts[1]));
});
var routeBoxer = new RouteBoxer();
var boxes = routeBoxer.box(path_, 3); // params: path and distance
// now use the boxes as you want :)
here-routeboxer.js:
/**
* #name Here-RouteBoxer
* #version 1.0
*
* based on
*
* #name RouteBoxer
* #version 1.0
* #copyright (c) 2010 Google Inc.
* #author Thor Mitchell
*
* #fileoverview The RouteBoxer class takes a path, such as the Polyline for a
* route generated by a Directions request, and generates a set of LatLngBounds
* objects that are guaranteed to contain every point within a given distance
* of that route. These LatLngBounds objects can then be used to generate
* requests to spatial search services that support bounds filtering (such as
* the Google Maps Data API) in order to implement search along a route.
* <br/><br/>
* RouteBoxer overlays a grid of the specified size on the route, identifies
* every grid cell that the route passes through, and generates a set of bounds
* that cover all of these cells, and their nearest neighbours. Consequently
* the bounds returned will extend up to ~3x the specified distance from the
* route in places.
*/
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Creates a new RouteBoxer
*
* #constructor
*/
function RouteBoxer() {
this.R = 6371; // earth's mean radius in km
}
/**
* Generates boxes for a given route and distance
*
* #param {google.maps.LatLng[] | google.maps.Polyline} path The path along
* which to create boxes. The path object can be either an Array of
* google.maps.LatLng objects or a Maps API v2 or Maps API v3
* google.maps.Polyline object.
* #param {Number} range The distance in kms around the route that the generated
* boxes must cover.
* #return {google.maps.LatLngBounds[]} An array of boxes that covers the whole
* path.
*/
RouteBoxer.prototype.box = function (path, range) {
// Two dimensional array representing the cells in the grid overlaid on the path
this.grid_ = null;
// Array that holds the latitude coordinate of each vertical grid line
this.latGrid_ = [];
// Array that holds the longitude coordinate of each horizontal grid line
this.lngGrid_ = [];
// Array of bounds that cover the whole route formed by merging cells that
// the route intersects first horizontally, and then vertically
this.boxesX_ = [];
// Array of bounds that cover the whole route formed by merging cells that
// the route intersects first vertically, and then horizontally
this.boxesY_ = [];
// The array of LatLngs representing the vertices of the path
var vertices = null;
// If necessary convert the path into an array of LatLng objects
if (path instanceof Array) {
// already an arry of LatLngs (eg. v3 overview_path)
vertices = path;
}
// Build the grid that is overlaid on the route
this.buildGrid_(vertices, range);
// Identify the grid cells that the route intersects
this.findIntersectingCells_(vertices);
// Merge adjacent intersected grid cells (and their neighbours) into two sets
// of bounds, both of which cover them completely
this.mergeIntersectingCells_();
// Return the set of merged bounds that has the fewest elements
return (this.boxesX_.length <= this.boxesY_.length ?
this.boxesX_ :
this.boxesY_);
};
/**
* Generates boxes for a given route and distance
*
* #param {LatLng[]} vertices The vertices of the path over which to lay the grid
* #param {Number} range The spacing of the grid cells.
*/
RouteBoxer.prototype.buildGrid_ = function (vertices, range) {
// Create a LatLngBounds object that contains the whole path
// var routeBounds = new google.maps.LatLngBounds();
var routeBounds = new H.geo.Rect(vertices[0].lat, vertices[0].lng, vertices[0].lat, vertices[0].lng);
// alert(vertices.length);
for (var i = 0; i < vertices.length; i++) {
routeBounds = routeBounds.mergeLatLng(vertices[i].lat, vertices[i].lng);
}
// Find the center of the bounding box of the path
var routeBoundsCenter = routeBounds.getCenter();
// Starting from the center define grid lines outwards vertically until they
// extend beyond the edge of the bounding box by more than one cell
this.latGrid_.push(routeBoundsCenter.lat);
// Add lines from the center out to the north
this.latGrid_.push(routeBoundsCenter.rhumbDestinationPoint(0, range).lat);
for (i = 2; this.latGrid_[i - 2] < routeBounds.getNorthEast().lat; i++) {
this.latGrid_.push(routeBoundsCenter.rhumbDestinationPoint(0, range * i).lat);
}
// Add lines from the center out to the south
for (i = 1; this.latGrid_[1] > routeBounds.getSouthWest().lat; i++) {
this.latGrid_.unshift(routeBoundsCenter.rhumbDestinationPoint(180, range * i).lat);
}
// Starting from the center define grid lines outwards horizontally until they
// extend beyond the edge of the bounding box by more than one cell
this.lngGrid_.push(routeBoundsCenter.lng);
// Add lines from the center out to the east
this.lngGrid_.push(routeBoundsCenter.rhumbDestinationPoint(90, range).lng);
for (i = 2; this.lngGrid_[i - 2] < routeBounds.getNorthEast().lng; i++) {
this.lngGrid_.push(routeBoundsCenter.rhumbDestinationPoint(90, range * i).lng);
}
// Add lines from the center out to the west
for (i = 1; this.lngGrid_[1] > routeBounds.getSouthWest().lng; i++) {
this.lngGrid_.unshift(routeBoundsCenter.rhumbDestinationPoint(270, range * i).lng);
}
// Create a two dimensional array representing this grid
this.grid_ = new Array(this.lngGrid_.length);
for (i = 0; i < this.grid_.length; i++) {
this.grid_[i] = new Array(this.latGrid_.length);
}
};
H.geo.Rect.prototype.getNorthEast = function () {
return new H.geo.Point(this.getTop(), this.getRight());
};
H.geo.Rect.prototype.getSouthWest = function () {
return new H.geo.Point(this.getBottom(), this.getLeft());
};
/**
* Find all of the cells in the overlaid grid that the path intersects
*
* #param {LatLng[]} vertices The vertices of the path
*/
RouteBoxer.prototype.findIntersectingCells_ = function (vertices) {
// Find the cell where the path begins
var hintXY = this.getCellCoords_(vertices[0]);
// Mark that cell and it's neighbours for inclusion in the boxes
this.markCell_(hintXY);
// Work through each vertex on the path identifying which grid cell it is in
for (var i = 1; i < vertices.length; i++) {
// Use the known cell of the previous vertex to help find the cell of this vertex
var gridXY = this.getGridCoordsFromHint_(vertices[i], vertices[i - 1], hintXY);
if (gridXY[0] === hintXY[0] && gridXY[1] === hintXY[1]) {
// This vertex is in the same cell as the previous vertex
// The cell will already have been marked for inclusion in the boxes
continue;
} else if ((Math.abs(hintXY[0] - gridXY[0]) === 1 && hintXY[1] === gridXY[1]) ||
(hintXY[0] === gridXY[0] && Math.abs(hintXY[1] - gridXY[1]) === 1)) {
// This vertex is in a cell that shares an edge with the previous cell
// Mark this cell and it's neighbours for inclusion in the boxes
this.markCell_(gridXY);
} else {
// This vertex is in a cell that does not share an edge with the previous
// cell. This means that the path passes through other cells between
// this vertex and the previous vertex, and we must determine which cells
// it passes through
this.getGridIntersects_(vertices[i - 1], vertices[i], hintXY, gridXY);
}
// Use this cell to find and compare with the next one
hintXY = gridXY;
}
};
/**
* Find the cell a path vertex is in by brute force iteration over the grid
*
* #param {LatLng[]} latlng The latlng of the vertex
* #return {Number[][]} The cell coordinates of this vertex in the grid
*/
RouteBoxer.prototype.getCellCoords_ = function (latlng) {
for (var x = 0; this.lngGrid_[x] < latlng.lng; x++) {}
for (var y = 0; this.latGrid_[y] < latlng.lat; y++) {}
return ([x - 1, y - 1]);
};
/**
* Find the cell a path vertex is in based on the known location of a nearby
* vertex. This saves searching the whole grid when working through vertices
* on the polyline that are likely to be in close proximity to each other.
*
* #param {LatLng[]} latlng The latlng of the vertex to locate in the grid
* #param {LatLng[]} hintlatlng The latlng of the vertex with a known location
* #param {Number[]} hint The cell containing the vertex with a known location
* #return {Number[]} The cell coordinates of the vertex to locate in the grid
*/
RouteBoxer.prototype.getGridCoordsFromHint_ = function (latlng, hintlatlng, hint) {
var x, y;
if (latlng.lng > hintlatlng.lng) {
for (x = hint[0]; this.lngGrid_[x + 1] < latlng.lng; x++) {}
} else {
for (x = hint[0]; this.lngGrid_[x] > latlng.lng; x--) {}
}
if (latlng.lat > hintlatlng.lat) {
for (y = hint[1]; this.latGrid_[y + 1] < latlng.lat; y++) {}
} else {
for (y = hint[1]; this.latGrid_[y] > latlng.lat; y--) {}
}
return ([x, y]);
};
/**
* Identify the grid squares that a path segment between two vertices
* intersects with by:
* 1. Finding the bearing between the start and end of the segment
* 2. Using the delta between the lat of the start and the lat of each
* latGrid boundary to find the distance to each latGrid boundary
* 3. Finding the lng of the intersection of the line with each latGrid
* boundary using the distance to the intersection and bearing of the line
* 4. Determining the x-coord on the grid of the point of intersection
* 5. Filling in all squares between the x-coord of the previous intersection
* (or start) and the current one (or end) at the current y coordinate,
* which is known for the grid line being intersected
*
* #param {LatLng} start The latlng of the vertex at the start of the segment
* #param {LatLng} end The latlng of the vertex at the end of the segment
* #param {Number[]} startXY The cell containing the start vertex
* #param {Number[]} endXY The cell containing the vend vertex
*/
RouteBoxer.prototype.getGridIntersects_ = function (start, end, startXY, endXY) {
var edgePoint, edgeXY, i;
var brng = start.rhumbBearingTo(end); // Step 1.
var hint = start;
var hintXY = startXY;
// Handle a line segment that travels south first
if (end.lat > start.lat) {
// Iterate over the east to west grid lines between the start and end cells
for (i = startXY[1] + 1; i <= endXY[1]; i++) {
// Find the latlng of the point where the path segment intersects with
// this grid line (Step 2 & 3)
edgePoint = this.getGridIntersect_(start, brng, this.latGrid_[i]);
// Find the cell containing this intersect point (Step 4)
edgeXY = this.getGridCoordsFromHint_(edgePoint, hint, hintXY);
// Mark every cell the path has crossed between this grid and the start,
// or the previous east to west grid line it crossed (Step 5)
this.fillInGridSquares_(hintXY[0], edgeXY[0], i - 1);
// Use the point where it crossed this grid line as the reference for the
// next iteration
hint = edgePoint;
hintXY = edgeXY;
}
// Mark every cell the path has crossed between the last east to west grid
// line it crossed and the end (Step 5)
this.fillInGridSquares_(hintXY[0], endXY[0], i - 1);
} else {
// Iterate over the east to west grid lines between the start and end cells
for (i = startXY[1]; i > endXY[1]; i--) {
// Find the latlng of the point where the path segment intersects with
// this grid line (Step 2 & 3)
edgePoint = this.getGridIntersect_(start, brng, this.latGrid_[i]);
// Find the cell containing this intersect point (Step 4)
edgeXY = this.getGridCoordsFromHint_(edgePoint, hint, hintXY);
// Mark every cell the path has crossed between this grid and the start,
// or the previous east to west grid line it crossed (Step 5)
this.fillInGridSquares_(hintXY[0], edgeXY[0], i);
// Use the point where it crossed this grid line as the reference for the
// next iteration
hint = edgePoint;
hintXY = edgeXY;
}
// Mark every cell the path has crossed between the last east to west grid
// line it crossed and the end (Step 5)
this.fillInGridSquares_(hintXY[0], endXY[0], i);
}
};
/**
* Find the latlng at which a path segment intersects with a given
* line of latitude
*
* #param {LatLng} start The vertex at the start of the path segment
* #param {Number} brng The bearing of the line from start to end
* #param {Number} gridLineLat The latitude of the grid line being intersected
* #return {LatLng} The latlng of the point where the path segment intersects
* the grid line
*/
RouteBoxer.prototype.getGridIntersect_ = function (start, brng, gridLineLat) {
var d = this.R * ((gridLineLat.toRad() - start.lat.toRad()) / Math.cos(brng.toRad()));
return start.rhumbDestinationPoint(brng, d);
};
/**
* Mark all cells in a given row of the grid that lie between two columns
* for inclusion in the boxes
*
* #param {Number} startx The first column to include
* #param {Number} endx The last column to include
* #param {Number} y The row of the cells to include
*/
RouteBoxer.prototype.fillInGridSquares_ = function (startx, endx, y) {
var x;
if (startx < endx) {
for (x = startx; x <= endx; x++) {
this.markCell_([x, y]);
}
} else {
for (x = startx; x >= endx; x--) {
this.markCell_([x, y]);
}
}
};
/**
* Mark a cell and the 8 immediate neighbours for inclusion in the boxes
*
* #param {Number[]} square The cell to mark
*/
RouteBoxer.prototype.markCell_ = function (cell) {
var x = cell[0];
var y = cell[1];
this.grid_[x - 1][y - 1] = 1;
this.grid_[x][y - 1] = 1;
this.grid_[x + 1][y - 1] = 1;
this.grid_[x - 1][y] = 1;
this.grid_[x][y] = 1;
this.grid_[x + 1][y] = 1;
this.grid_[x - 1][y + 1] = 1;
this.grid_[x][y + 1] = 1;
this.grid_[x + 1][y + 1] = 1;
};
/**
* Create two sets of bounding boxes, both of which cover all of the cells that
* have been marked for inclusion.
*
* The first set is created by combining adjacent cells in the same column into
* a set of vertical rectangular boxes, and then combining boxes of the same
* height that are adjacent horizontally.
*
* The second set is created by combining adjacent cells in the same row into
* a set of horizontal rectangular boxes, and then combining boxes of the same
* width that are adjacent vertically.
*
*/
RouteBoxer.prototype.mergeIntersectingCells_ = function () {
var x, y, box;
// The box we are currently expanding with new cells
var currentBox = null;
// Traverse the grid a row at a time
for (y = 0; y < this.grid_[0].length; y++) {
for (x = 0; x < this.grid_.length; x++) {
if (this.grid_[x][y]) {
// This cell is marked for inclusion. If the previous cell in this
// row was also marked for inclusion, merge this cell into it's box.
// Otherwise start a new box.
box = this.getCellBounds_([x, y]);
if (currentBox) {
currentBox = currentBox.mergeLatLng(box.getNorthEast().lat, box.getNorthEast().lng);
} else {
currentBox = box;
}
} else {
// This cell is not marked for inclusion. If the previous cell was
// marked for inclusion, merge it's box with a box that spans the same
// columns from the row below if possible.
this.mergeBoxesY_(currentBox);
currentBox = null;
}
}
// If the last cell was marked for inclusion, merge it's box with a matching
// box from the row below if possible.
this.mergeBoxesY_(currentBox);
currentBox = null;
}
// Traverse the grid a column at a time
for (x = 0; x < this.grid_.length; x++) {
for (y = 0; y < this.grid_[0].length; y++) {
if (this.grid_[x][y]) {
// This cell is marked for inclusion. If the previous cell in this
// column was also marked for inclusion, merge this cell into it's box.
// Otherwise start a new box.
if (currentBox) {
box = this.getCellBounds_([x, y]);
currentBox = currentBox.mergeLatLng(box.getNorthEast().lat, box.getNorthEast().lng);
} else {
currentBox = this.getCellBounds_([x, y]);
}
} else {
// This cell is not marked for inclusion. If the previous cell was
// marked for inclusion, merge it's box with a box that spans the same
// rows from the column to the left if possible.
this.mergeBoxesX_(currentBox);
currentBox = null;
}
}
// If the last cell was marked for inclusion, merge it's box with a matching
// box from the column to the left if possible.
this.mergeBoxesX_(currentBox);
currentBox = null;
}
};
/**
* Search for an existing box in an adjacent row to the given box that spans the
* same set of columns and if one is found merge the given box into it. If one
* is not found, append this box to the list of existing boxes.
*
* #param {LatLngBounds} The box to merge
*/
RouteBoxer.prototype.mergeBoxesX_ = function (box) {
if (box !== null) {
for (var i = 0; i < this.boxesX_.length; i++) {
if (this.boxesX_[i].getNorthEast().lng === box.getSouthWest().lng &&
this.boxesX_[i].getSouthWest().lat === box.getSouthWest().lat &&
this.boxesX_[i].getNorthEast().lat === box.getNorthEast().lat) {
this.boxesX_[i] = this.boxesX_[i].mergeLatLng(box.getNorthEast().lat, box.getNorthEast().lng);
return;
}
}
this.boxesX_.push(box);
}
};
/**
* Search for an existing box in an adjacent column to the given box that spans
* the same set of rows and if one is found merge the given box into it. If one
* is not found, append this box to the list of existing boxes.
*
* #param {LatLngBounds} The box to merge
*/
RouteBoxer.prototype.mergeBoxesY_ = function (box) {
if (box !== null) {
for (var i = 0; i < this.boxesY_.length; i++) {
if (this.boxesY_[i].getNorthEast().lat === box.getSouthWest().lat &&
this.boxesY_[i].getSouthWest().lng === box.getSouthWest().lng &&
this.boxesY_[i].getNorthEast().lng === box.getNorthEast().lng) {
this.boxesY_[i] = this.boxesY_[i].mergeLatLng(box.getNorthEast().lat, box.getNorthEast().lng);
return;
}
}
this.boxesY_.push(box);
}
};
/**
* Obtain the LatLng of the origin of a cell on the grid
*
* #param {Number[]} cell The cell to lookup.
* #return {LatLng} The latlng of the origin of the cell.
*/
RouteBoxer.prototype.getCellBounds_ = function (cell) {
return new H.geo.Rect(this.latGrid_[cell[1]+1], this.lngGrid_[cell[0]],this.latGrid_[cell[1]], this.lngGrid_[cell[0] + 1]);
};
/* Based on the Latitude/longitude spherical geodesy formulae & scripts
at http://www.movable-type.co.uk/scripts/latlong.html
(c) Chris Veness 2002-2010
*/
H.geo.Point.prototype.rhumbDestinationPoint = function (brng, dist) {
var R = 6371; // earth's mean radius in km
var d = parseFloat(dist) / R; // d = angular distance covered on earth's surface
var lat1 = this.lat.toRad(), lon1 = this.lng.toRad();
brng = brng.toRad();
var lat2 = lat1 + d * Math.cos(brng);
var dLat = lat2 - lat1;
var dPhi = Math.log(Math.tan(lat2 / 2 + Math.PI / 4) / Math.tan(lat1 / 2 + Math.PI / 4));
var q = (Math.abs(dLat) > 1e-10) ? dLat / dPhi : Math.cos(lat1);
var dLon = d * Math.sin(brng) / q;
// check for going past the pole
if (Math.abs(lat2) > Math.PI / 2) {
lat2 = lat2 > 0 ? Math.PI - lat2 : - (Math.PI - lat2);
}
var lon2 = (lon1 + dLon + Math.PI) % (2 * Math.PI) - Math.PI;
if (isNaN(lat2) || isNaN(lon2)) {
return null;
}
return new H.geo.Point(lat2.toDeg(), lon2.toDeg());
};
H.geo.Point.prototype.rhumbBearingTo = function (dest) {
var dLon = (dest.lng - this.lng).toRad();
var dPhi = Math.log(Math.tan(dest.lat.toRad() / 2 + Math.PI / 4) / Math.tan(this.lat.toRad() / 2 + Math.PI / 4));
if (Math.abs(dLon) > Math.PI) {
dLon = dLon > 0 ? -(2 * Math.PI - dLon) : (2 * Math.PI + dLon);
}
return Math.atan2(dLon, dPhi).toBrng();
};
/**
* Extend the Number object to convert degrees to radians
*
* #return {Number} Bearing in radians
* #ignore
*/
Number.prototype.toRad = function () {
return this * Math.PI / 180;
};
/**
* Extend the Number object to convert radians to degrees
*
* #return {Number} Bearing in degrees
* #ignore
*/
Number.prototype.toDeg = function () {
return this * 180 / Math.PI;
};
/**
* Normalize a heading in degrees to between 0 and +360
*
* #return {Number} Return
* #ignore
*/
Number.prototype.toBrng = function () {
return (this.toDeg() + 360) % 360;
};

Related

How to query Firestore data 'sort by distance' without querying whole data [duplicate]

Does the new firestore database from firebase natively support location based geo queries? i.e. Find posts within 10 miles, or find the 50 nearest posts?
I see that there are some existing projects for the real-time firebase database, projects such as geofire- could those be adapted to firestore as well?
UPDATE: Firestore does not support actual GeoPoint queries at present so while the below query executes successfully, it only filters by latitude, not by longitude and thus will return many results that are not nearby. The best solution would be to use geohashes. To learn how to do something similar yourself, have a look at this video.
This can be done by creating a bounding box less than greater than query. As for the efficiency, I can't speak to it.
Note, the accuracy of the lat/long offset for ~1 mile should be reviewed, but here is a quick way to do this:
SWIFT 3.0 Version
func getDocumentNearBy(latitude: Double, longitude: Double, distance: Double) {
// ~1 mile of lat and lon in degrees
let lat = 0.0144927536231884
let lon = 0.0181818181818182
let lowerLat = latitude - (lat * distance)
let lowerLon = longitude - (lon * distance)
let greaterLat = latitude + (lat * distance)
let greaterLon = longitude + (lon * distance)
let lesserGeopoint = GeoPoint(latitude: lowerLat, longitude: lowerLon)
let greaterGeopoint = GeoPoint(latitude: greaterLat, longitude: greaterLon)
let docRef = Firestore.firestore().collection("locations")
let query = docRef.whereField("location", isGreaterThan: lesserGeopoint).whereField("location", isLessThan: greaterGeopoint)
query.getDocuments { snapshot, error in
if let error = error {
print("Error getting documents: \(error)")
} else {
for document in snapshot!.documents {
print("\(document.documentID) => \(document.data())")
}
}
}
}
func run() {
// Get all locations within 10 miles of Google Headquarters
getDocumentNearBy(latitude: 37.422000, longitude: -122.084057, distance: 10)
}
UPDATE: Firestore does not support actual GeoPoint queries at present so while the below query executes successfully, it only filters by latitude, not by longitude and thus will return many results that are not nearby. The best solution would be to use geohashes. To learn how to do something similar yourself, have a look at this video.
(First let me apologize for all the code in this post, I just wanted anyone reading this answer to have an easy time reproducing the functionality.)
To address the same concern the OP had, at first I adapted the GeoFire library to work with Firestore (you can learn a lot about geo-stuff by looking at that library). Then I realized I didn't really mind if locations were returned in an exact circle. I just wanted some way to get 'nearby' locations.
I can't believe how long it took me to realize this, but you can just perform a double inequality query on a GeoPoint field using a SW corner and NE corner to get locations within a bounding box around a center point.
So I made a JavaScript function like the one below (this is basically a JS version of Ryan Lee's answer).
/**
* Get locations within a bounding box defined by a center point and distance from from the center point to the side of the box;
*
* #param {Object} area an object that represents the bounding box
* around a point in which locations should be retrieved
* #param {Object} area.center an object containing the latitude and
* longitude of the center point of the bounding box
* #param {number} area.center.latitude the latitude of the center point
* #param {number} area.center.longitude the longitude of the center point
* #param {number} area.radius (in kilometers) the radius of a circle
* that is inscribed in the bounding box;
* This could also be described as half of the bounding box's side length.
* #return {Promise} a Promise that fulfills with an array of all the
* retrieved locations
*/
function getLocations(area) {
// calculate the SW and NE corners of the bounding box to query for
const box = utils.boundingBoxCoordinates(area.center, area.radius);
// construct the GeoPoints
const lesserGeopoint = new GeoPoint(box.swCorner.latitude, box.swCorner.longitude);
const greaterGeopoint = new GeoPoint(box.neCorner.latitude, box.neCorner.longitude);
// construct the Firestore query
let query = firebase.firestore().collection('myCollection').where('location', '>', lesserGeopoint).where('location', '<', greaterGeopoint);
// return a Promise that fulfills with the locations
return query.get()
.then((snapshot) => {
const allLocs = []; // used to hold all the loc data
snapshot.forEach((loc) => {
// get the data
const data = loc.data();
// calculate a distance from the center
data.distanceFromCenter = utils.distance(area.center, data.location);
// add to the array
allLocs.push(data);
});
return allLocs;
})
.catch((err) => {
return new Error('Error while retrieving events');
});
}
The function above also adds a .distanceFromCenter property to each piece of location data that's returned so that you could get the circle-like behavior by just checking if that distance is within the range you want.
I use two util functions in the function above so here's the code for those as well. (All of the util functions below are actually adapted from the GeoFire library.)
distance():
/**
* Calculates the distance, in kilometers, between two locations, via the
* Haversine formula. Note that this is approximate due to the fact that
* the Earth's radius varies between 6356.752 km and 6378.137 km.
*
* #param {Object} location1 The first location given as .latitude and .longitude
* #param {Object} location2 The second location given as .latitude and .longitude
* #return {number} The distance, in kilometers, between the inputted locations.
*/
distance(location1, location2) {
const radius = 6371; // Earth's radius in kilometers
const latDelta = degreesToRadians(location2.latitude - location1.latitude);
const lonDelta = degreesToRadians(location2.longitude - location1.longitude);
const a = (Math.sin(latDelta / 2) * Math.sin(latDelta / 2)) +
(Math.cos(degreesToRadians(location1.latitude)) * Math.cos(degreesToRadians(location2.latitude)) *
Math.sin(lonDelta / 2) * Math.sin(lonDelta / 2));
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return radius * c;
}
boundingBoxCoordinates(): (There are more utils used in here as well that I've pasted below.)
/**
* Calculates the SW and NE corners of a bounding box around a center point for a given radius;
*
* #param {Object} center The center given as .latitude and .longitude
* #param {number} radius The radius of the box (in kilometers)
* #return {Object} The SW and NE corners given as .swCorner and .neCorner
*/
boundingBoxCoordinates(center, radius) {
const KM_PER_DEGREE_LATITUDE = 110.574;
const latDegrees = radius / KM_PER_DEGREE_LATITUDE;
const latitudeNorth = Math.min(90, center.latitude + latDegrees);
const latitudeSouth = Math.max(-90, center.latitude - latDegrees);
// calculate longitude based on current latitude
const longDegsNorth = metersToLongitudeDegrees(radius, latitudeNorth);
const longDegsSouth = metersToLongitudeDegrees(radius, latitudeSouth);
const longDegs = Math.max(longDegsNorth, longDegsSouth);
return {
swCorner: { // bottom-left (SW corner)
latitude: latitudeSouth,
longitude: wrapLongitude(center.longitude - longDegs),
},
neCorner: { // top-right (NE corner)
latitude: latitudeNorth,
longitude: wrapLongitude(center.longitude + longDegs),
},
};
}
metersToLongitudeDegrees():
/**
* Calculates the number of degrees a given distance is at a given latitude.
*
* #param {number} distance The distance to convert.
* #param {number} latitude The latitude at which to calculate.
* #return {number} The number of degrees the distance corresponds to.
*/
function metersToLongitudeDegrees(distance, latitude) {
const EARTH_EQ_RADIUS = 6378137.0;
// this is a super, fancy magic number that the GeoFire lib can explain (maybe)
const E2 = 0.00669447819799;
const EPSILON = 1e-12;
const radians = degreesToRadians(latitude);
const num = Math.cos(radians) * EARTH_EQ_RADIUS * Math.PI / 180;
const denom = 1 / Math.sqrt(1 - E2 * Math.sin(radians) * Math.sin(radians));
const deltaDeg = num * denom;
if (deltaDeg < EPSILON) {
return distance > 0 ? 360 : 0;
}
// else
return Math.min(360, distance / deltaDeg);
}
wrapLongitude():
/**
* Wraps the longitude to [-180,180].
*
* #param {number} longitude The longitude to wrap.
* #return {number} longitude The resulting longitude.
*/
function wrapLongitude(longitude) {
if (longitude <= 180 && longitude >= -180) {
return longitude;
}
const adjusted = longitude + 180;
if (adjusted > 0) {
return (adjusted % 360) - 180;
}
// else
return 180 - (-adjusted % 360);
}
A new project has been introduced since #monkeybonkey first ask this question. The project is called GEOFirestore.
With this library you can perform queries like query documents within a circle:
const geoQuery = geoFirestore.query({
center: new firebase.firestore.GeoPoint(10.38, 2.41),
radius: 10.5
});
You can install GeoFirestore via npm. You will have to install Firebase separately (because it is a peer dependency to GeoFirestore):
$ npm install geofirestore firebase --save
As of today, there is no way to do such a query. There are other questions in SO related to it:
Is there a way to use GeoFire with Firestore?
How to query closest GeoPoints in a collection in Firebase Cloud Firestore?
Is there a way to use GeoFire with Firestore?
In my current Android project I may use https://github.com/drfonfon/android-geohash to add a geohash field while Firebase team is developing native support.
Using Firebase Realtime Database like suggested in other questions means that you can't filter your results set by location and other fields simultaneously, the main reason I want to switch to Firestore in the first place.
As of late 2020 there is now also documentation of how to do geoqueries with Firestore.
These solutions for iOS, Android, and Web, build on top of a slimmed down version of the Firebase-created GeoFire libraries, and then show how to:
Generate geohash values and store them in Firestore
Determine geohash ranges of the bounding box for a certain point and radius
Perform queries across these geohash ranges
This a bit more low-level than most of the other libraries presented here, so it may be a better fit for some use-cases and a worse fit for others.
Hijacking this thread to hopefully help anyone still looking. Firestore still does not support geo-based queries, and using the GeoFirestore library isnt ideal either as it will only let you search by location, nothing else.
I've put this together:
https://github.com/mbramwell1/GeoFire-Android
It basically lets you do nearby searches using a location and distance:
QueryLocation queryLocation = QueryLocation.fromDegrees(latitude, longitude);
Distance searchDistance = new Distance(1.0, DistanceUnit.KILOMETERS);
geoFire.query()
.whereNearTo(queryLocation, distance)
.build()
.get();
There are more docs on the repo. Its working for me so give it a try, hopefully it will do what you need.
For Dart
///
/// Checks if these coordinates are valid geo coordinates.
/// [latitude] The latitude must be in the range [-90, 90]
/// [longitude] The longitude must be in the range [-180, 180]
/// returns [true] if these are valid geo coordinates
///
bool coordinatesValid(double latitude, double longitude) {
return (latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180);
}
///
/// Checks if the coordinates of a GeopPoint are valid geo coordinates.
/// [latitude] The latitude must be in the range [-90, 90]
/// [longitude] The longitude must be in the range [-180, 180]
/// returns [true] if these are valid geo coordinates
///
bool geoPointValid(GeoPoint point) {
return (point.latitude >= -90 &&
point.latitude <= 90 &&
point.longitude >= -180 &&
point.longitude <= 180);
}
///
/// Wraps the longitude to [-180,180].
///
/// [longitude] The longitude to wrap.
/// returns The resulting longitude.
///
double wrapLongitude(double longitude) {
if (longitude <= 180 && longitude >= -180) {
return longitude;
}
final adjusted = longitude + 180;
if (adjusted > 0) {
return (adjusted % 360) - 180;
}
// else
return 180 - (-adjusted % 360);
}
double degreesToRadians(double degrees) {
return (degrees * math.pi) / 180;
}
///
///Calculates the number of degrees a given distance is at a given latitude.
/// [distance] The distance to convert.
/// [latitude] The latitude at which to calculate.
/// returns the number of degrees the distance corresponds to.
double kilometersToLongitudeDegrees(double distance, double latitude) {
const EARTH_EQ_RADIUS = 6378137.0;
// this is a super, fancy magic number that the GeoFire lib can explain (maybe)
const E2 = 0.00669447819799;
const EPSILON = 1e-12;
final radians = degreesToRadians(latitude);
final numerator = math.cos(radians) * EARTH_EQ_RADIUS * math.pi / 180;
final denom = 1 / math.sqrt(1 - E2 * math.sin(radians) * math.sin(radians));
final deltaDeg = numerator * denom;
if (deltaDeg < EPSILON) {
return distance > 0 ? 360.0 : 0.0;
}
// else
return math.min(360.0, distance / deltaDeg);
}
///
/// Defines the boundingbox for the query based
/// on its south-west and north-east corners
class GeoBoundingBox {
final GeoPoint swCorner;
final GeoPoint neCorner;
GeoBoundingBox({this.swCorner, this.neCorner});
}
///
/// Defines the search area by a circle [center] / [radiusInKilometers]
/// Based on the limitations of FireStore we can only search in rectangles
/// which means that from this definition a final search square is calculated
/// that contains the circle
class Area {
final GeoPoint center;
final double radiusInKilometers;
Area(this.center, this.radiusInKilometers):
assert(geoPointValid(center)), assert(radiusInKilometers >= 0);
factory Area.inMeters(GeoPoint gp, int radiusInMeters) {
return new Area(gp, radiusInMeters / 1000.0);
}
factory Area.inMiles(GeoPoint gp, int radiusMiles) {
return new Area(gp, radiusMiles * 1.60934);
}
/// returns the distance in km of [point] to center
double distanceToCenter(GeoPoint point) {
return distanceInKilometers(center, point);
}
}
///
///Calculates the SW and NE corners of a bounding box around a center point for a given radius;
/// [area] with the center given as .latitude and .longitude
/// and the radius of the box (in kilometers)
GeoBoundingBox boundingBoxCoordinates(Area area) {
const KM_PER_DEGREE_LATITUDE = 110.574;
final latDegrees = area.radiusInKilometers / KM_PER_DEGREE_LATITUDE;
final latitudeNorth = math.min(90.0, area.center.latitude + latDegrees);
final latitudeSouth = math.max(-90.0, area.center.latitude - latDegrees);
// calculate longitude based on current latitude
final longDegsNorth = kilometersToLongitudeDegrees(area.radiusInKilometers, latitudeNorth);
final longDegsSouth = kilometersToLongitudeDegrees(area.radiusInKilometers, latitudeSouth);
final longDegs = math.max(longDegsNorth, longDegsSouth);
return new GeoBoundingBox(
swCorner: new GeoPoint(latitudeSouth, wrapLongitude(area.center.longitude - longDegs)),
neCorner: new GeoPoint(latitudeNorth, wrapLongitude(area.center.longitude + longDegs)));
}
///
/// Calculates the distance, in kilometers, between two locations, via the
/// Haversine formula. Note that this is approximate due to the fact that
/// the Earth's radius varies between 6356.752 km and 6378.137 km.
/// [location1] The first location given
/// [location2] The second location given
/// sreturn the distance, in kilometers, between the two locations.
///
double distanceInKilometers(GeoPoint location1, GeoPoint location2) {
const radius = 6371; // Earth's radius in kilometers
final latDelta = degreesToRadians(location2.latitude - location1.latitude);
final lonDelta = degreesToRadians(location2.longitude - location1.longitude);
final a = (math.sin(latDelta / 2) * math.sin(latDelta / 2)) +
(math.cos(degreesToRadians(location1.latitude)) *
math.cos(degreesToRadians(location2.latitude)) *
math.sin(lonDelta / 2) *
math.sin(lonDelta / 2));
final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
return radius * c;
}
I just published a Flutter package based on the JS code above
https://pub.dartlang.org/packages/firestore_helpers
Yes, this is an old topic, but I want to help only on Java code. How I solved a problem with longitude? I used a code from Ryan Lee and Michael Teper.
A code:
#Override
public void getUsersForTwentyMiles() {
FirebaseFirestore db = FirebaseFirestore.getInstance();
double latitude = 33.0076665;
double longitude = 35.1011336;
int distance = 20; //20 milles
GeoPoint lg = new GeoPoint(latitude, longitude);
// ~1 mile of lat and lon in degrees
double lat = 0.0144927536231884;
double lon = 0.0181818181818182;
final double lowerLat = latitude - (lat * distance);
final double lowerLon = longitude - (lon * distance);
double greaterLat = latitude + (lat * distance);
final double greaterLon = longitude + (lon * distance);
final GeoPoint lesserGeopoint = new GeoPoint(lowerLat, lowerLon);
final GeoPoint greaterGeopoint = new GeoPoint(greaterLat, greaterLon);
Log.d(LOG_TAG, "local general lovation " + lg);
Log.d(LOG_TAG, "local lesserGeopoint " + lesserGeopoint);
Log.d(LOG_TAG, "local greaterGeopoint " + greaterGeopoint);
//get users for twenty miles by only a latitude
db.collection("users")
.whereGreaterThan("location", lesserGeopoint)
.whereLessThan("location", greaterGeopoint)
.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
UserData user = document.toObject(UserData.class);
//here a longitude condition (myLocation - 20 <= myLocation <= myLocation +20)
if (lowerLon <= user.getUserGeoPoint().getLongitude() && user.getUserGeoPoint().getLongitude() <= greaterLon) {
Log.d(LOG_TAG, "location: " + document.getId());
}
}
} else {
Log.d(LOG_TAG, "Error getting documents: ", task.getException());
}
}
});
}
Just inside after issuing the result set the filter to longitude:
if (lowerLon <= user.getUserGeoPoint().getLongitude() && user.getUserGeoPoint().getLongitude() <= greaterLon) {
Log.d(LOG_TAG, "location: " + document.getId());
}
I hope this will help someone.
Have a nice day!
You should use GeoFire (works with Firestore). With this you can filter documents on server and read less documents from your Firestore db. This will reduce your read count as well.
Check this lib for GroFire: https://github.com/patpatchpatrick/GeoFirestore-iOS
"patpatchpatrick" made this to Swift 5 compatible.
Just do a pod install as follows:
pod 'Geofirestore', :git => 'https://github.com/patpatchpatrick/GeoFirestore-iOS'
I am using this library in one of my projects and it works fine.
To set a location:
let location: CLLocation = CLLocation(latitude: lat, longitude: lng)
yourCollection.setLocation(location: location, forDocumentWithID: "YourDocId") { (error) in }
To remove location:
collection.removeLocation(forDocumentWithID: "YourDocId")
To get docs:
let center = CLLocation(latitude: lat, longitude: lng)
let collection = "Your collection path"
let circleQuery = collection.query(withCenter: center, radius: Double(yourRadiusVal))
let _ = circleQuery.observe(.documentEntered, with: { (key, location) in
//Use info as per your need
})
I have used .documentEntered, you can use other available geo queries like (Document Exited, Document Moved) as per your need.
You can query using GeoPoint as well.
This is not fully tested yet it should be a bit of an improvement on Ryan Lee's answer
My calculation is more accurate and then I filter the answers to remove hits which fall within the bounding box but outside the radius
Swift 4
func getDocumentNearBy(latitude: Double, longitude: Double, meters: Double) {
let myGeopoint = GeoPoint(latitude:latitude, longitude:longitude )
let r_earth : Double = 6378137 // Radius of earth in Meters
// 1 degree lat in m
let kLat = (2 * Double.pi / 360) * r_earth
let kLon = (2 * Double.pi / 360) * r_earth * __cospi(latitude/180.0)
let deltaLat = meters / kLat
let deltaLon = meters / kLon
let swGeopoint = GeoPoint(latitude: latitude - deltaLat, longitude: longitude - deltaLon)
let neGeopoint = GeoPoint(latitude: latitude + deltaLat, longitude: longitude + deltaLon)
let docRef : CollectionReference = appDelegate.db.collection("restos")
let query = docRef.whereField("location", isGreaterThan: swGeopoint).whereField("location", isLessThan: neGeopoint)
query.getDocuments { snapshot, error in
guard let snapshot = snapshot else {
print("Error fetching snapshot results: \(error!)")
return
}
self.documents = snapshot.documents.filter { (document) in
if let location = document.get("location") as? GeoPoint {
let myDistance = self.distanceBetween(geoPoint1:myGeopoint,geoPoint2:location)
print("myDistance:\(myDistance) distance:\(meters)")
return myDistance <= meters
}
return false
}
}
}
Functions which accurately measure the distance in Meters between 2 Geopoints for filtering
func distanceBetween(geoPoint1:GeoPoint, geoPoint2:GeoPoint) -> Double{
return distanceBetween(lat1: geoPoint1.latitude,
lon1: geoPoint1.longitude,
lat2: geoPoint2.latitude,
lon2: geoPoint2.longitude)
}
func distanceBetween(lat1:Double, lon1:Double, lat2:Double, lon2:Double) -> Double{ // generally used geo measurement function
let R : Double = 6378.137; // Radius of earth in KM
let dLat = lat2 * Double.pi / 180 - lat1 * Double.pi / 180;
let dLon = lon2 * Double.pi / 180 - lon1 * Double.pi / 180;
let a = sin(dLat/2) * sin(dLat/2) +
cos(lat1 * Double.pi / 180) * cos(lat2 * Double.pi / 180) *
sin(dLon/2) * sin(dLon/2);
let c = 2 * atan2(sqrt(a), sqrt(1-a));
let d = R * c;
return d * 1000; // meters
}
The easiest way is to calculate a "geo hash" when storing the location in the database.
A geo hash is a string which represents a location down to a certain accuracy. The longer the geo hash, the closer the locations with said geo hash must be. Two location which are e.g. 100m apart may have the same 6-char geo hash but when calculating a 7-char geo hash the last char might be different.
There are plenty libraries allowing you to calculate geo hashes for any language. Just store it alongside the location and use a == query to find locations with the same geo hash.
In javascript you can simply
const db = firebase.firestore();
//Geofire
import { GeoCollectionReference, GeoFirestore, GeoQuery, GeoQuerySnapshot } from 'geofirestore';
// Create a GeoFirestore reference
const geofirestore: GeoFirestore = new GeoFirestore(db);
// Create a GeoCollection reference
const geocollection: GeoCollectionReference = geofirestore.collection('<Your_collection_name>');
const query: GeoQuery = geocollectionDrivers.near({
center: new firebase.firestore.GeoPoint(location.latitude, location.longitude),
radius: 10000
});
query.onSnapshot(gquerySnapshot => {
gquerySnapshot.forEach(res => {
console.log(res.data());
})
});
A workaround for Flutter till we have native query in Firestore to pull ordered documents based on lat/long:
https://pub.dev/packages/geoflutterfire
A plugin to store geo hashes in the Firestore and query the same.
Limitations: limit not supported
There's a GeoFire library for Firestore called Geofirestore: https://github.com/imperiumlabs/GeoFirestore (Disclaimer: I helped develop it). It's super easy to use and offers the same features for Firestore that Geofire does for Firebase Realtime DB)

How to run a geo "nearby" query with firestore?

Does the new firestore database from firebase natively support location based geo queries? i.e. Find posts within 10 miles, or find the 50 nearest posts?
I see that there are some existing projects for the real-time firebase database, projects such as geofire- could those be adapted to firestore as well?
UPDATE: Firestore does not support actual GeoPoint queries at present so while the below query executes successfully, it only filters by latitude, not by longitude and thus will return many results that are not nearby. The best solution would be to use geohashes. To learn how to do something similar yourself, have a look at this video.
This can be done by creating a bounding box less than greater than query. As for the efficiency, I can't speak to it.
Note, the accuracy of the lat/long offset for ~1 mile should be reviewed, but here is a quick way to do this:
SWIFT 3.0 Version
func getDocumentNearBy(latitude: Double, longitude: Double, distance: Double) {
// ~1 mile of lat and lon in degrees
let lat = 0.0144927536231884
let lon = 0.0181818181818182
let lowerLat = latitude - (lat * distance)
let lowerLon = longitude - (lon * distance)
let greaterLat = latitude + (lat * distance)
let greaterLon = longitude + (lon * distance)
let lesserGeopoint = GeoPoint(latitude: lowerLat, longitude: lowerLon)
let greaterGeopoint = GeoPoint(latitude: greaterLat, longitude: greaterLon)
let docRef = Firestore.firestore().collection("locations")
let query = docRef.whereField("location", isGreaterThan: lesserGeopoint).whereField("location", isLessThan: greaterGeopoint)
query.getDocuments { snapshot, error in
if let error = error {
print("Error getting documents: \(error)")
} else {
for document in snapshot!.documents {
print("\(document.documentID) => \(document.data())")
}
}
}
}
func run() {
// Get all locations within 10 miles of Google Headquarters
getDocumentNearBy(latitude: 37.422000, longitude: -122.084057, distance: 10)
}
UPDATE: Firestore does not support actual GeoPoint queries at present so while the below query executes successfully, it only filters by latitude, not by longitude and thus will return many results that are not nearby. The best solution would be to use geohashes. To learn how to do something similar yourself, have a look at this video.
(First let me apologize for all the code in this post, I just wanted anyone reading this answer to have an easy time reproducing the functionality.)
To address the same concern the OP had, at first I adapted the GeoFire library to work with Firestore (you can learn a lot about geo-stuff by looking at that library). Then I realized I didn't really mind if locations were returned in an exact circle. I just wanted some way to get 'nearby' locations.
I can't believe how long it took me to realize this, but you can just perform a double inequality query on a GeoPoint field using a SW corner and NE corner to get locations within a bounding box around a center point.
So I made a JavaScript function like the one below (this is basically a JS version of Ryan Lee's answer).
/**
* Get locations within a bounding box defined by a center point and distance from from the center point to the side of the box;
*
* #param {Object} area an object that represents the bounding box
* around a point in which locations should be retrieved
* #param {Object} area.center an object containing the latitude and
* longitude of the center point of the bounding box
* #param {number} area.center.latitude the latitude of the center point
* #param {number} area.center.longitude the longitude of the center point
* #param {number} area.radius (in kilometers) the radius of a circle
* that is inscribed in the bounding box;
* This could also be described as half of the bounding box's side length.
* #return {Promise} a Promise that fulfills with an array of all the
* retrieved locations
*/
function getLocations(area) {
// calculate the SW and NE corners of the bounding box to query for
const box = utils.boundingBoxCoordinates(area.center, area.radius);
// construct the GeoPoints
const lesserGeopoint = new GeoPoint(box.swCorner.latitude, box.swCorner.longitude);
const greaterGeopoint = new GeoPoint(box.neCorner.latitude, box.neCorner.longitude);
// construct the Firestore query
let query = firebase.firestore().collection('myCollection').where('location', '>', lesserGeopoint).where('location', '<', greaterGeopoint);
// return a Promise that fulfills with the locations
return query.get()
.then((snapshot) => {
const allLocs = []; // used to hold all the loc data
snapshot.forEach((loc) => {
// get the data
const data = loc.data();
// calculate a distance from the center
data.distanceFromCenter = utils.distance(area.center, data.location);
// add to the array
allLocs.push(data);
});
return allLocs;
})
.catch((err) => {
return new Error('Error while retrieving events');
});
}
The function above also adds a .distanceFromCenter property to each piece of location data that's returned so that you could get the circle-like behavior by just checking if that distance is within the range you want.
I use two util functions in the function above so here's the code for those as well. (All of the util functions below are actually adapted from the GeoFire library.)
distance():
/**
* Calculates the distance, in kilometers, between two locations, via the
* Haversine formula. Note that this is approximate due to the fact that
* the Earth's radius varies between 6356.752 km and 6378.137 km.
*
* #param {Object} location1 The first location given as .latitude and .longitude
* #param {Object} location2 The second location given as .latitude and .longitude
* #return {number} The distance, in kilometers, between the inputted locations.
*/
distance(location1, location2) {
const radius = 6371; // Earth's radius in kilometers
const latDelta = degreesToRadians(location2.latitude - location1.latitude);
const lonDelta = degreesToRadians(location2.longitude - location1.longitude);
const a = (Math.sin(latDelta / 2) * Math.sin(latDelta / 2)) +
(Math.cos(degreesToRadians(location1.latitude)) * Math.cos(degreesToRadians(location2.latitude)) *
Math.sin(lonDelta / 2) * Math.sin(lonDelta / 2));
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return radius * c;
}
boundingBoxCoordinates(): (There are more utils used in here as well that I've pasted below.)
/**
* Calculates the SW and NE corners of a bounding box around a center point for a given radius;
*
* #param {Object} center The center given as .latitude and .longitude
* #param {number} radius The radius of the box (in kilometers)
* #return {Object} The SW and NE corners given as .swCorner and .neCorner
*/
boundingBoxCoordinates(center, radius) {
const KM_PER_DEGREE_LATITUDE = 110.574;
const latDegrees = radius / KM_PER_DEGREE_LATITUDE;
const latitudeNorth = Math.min(90, center.latitude + latDegrees);
const latitudeSouth = Math.max(-90, center.latitude - latDegrees);
// calculate longitude based on current latitude
const longDegsNorth = metersToLongitudeDegrees(radius, latitudeNorth);
const longDegsSouth = metersToLongitudeDegrees(radius, latitudeSouth);
const longDegs = Math.max(longDegsNorth, longDegsSouth);
return {
swCorner: { // bottom-left (SW corner)
latitude: latitudeSouth,
longitude: wrapLongitude(center.longitude - longDegs),
},
neCorner: { // top-right (NE corner)
latitude: latitudeNorth,
longitude: wrapLongitude(center.longitude + longDegs),
},
};
}
metersToLongitudeDegrees():
/**
* Calculates the number of degrees a given distance is at a given latitude.
*
* #param {number} distance The distance to convert.
* #param {number} latitude The latitude at which to calculate.
* #return {number} The number of degrees the distance corresponds to.
*/
function metersToLongitudeDegrees(distance, latitude) {
const EARTH_EQ_RADIUS = 6378137.0;
// this is a super, fancy magic number that the GeoFire lib can explain (maybe)
const E2 = 0.00669447819799;
const EPSILON = 1e-12;
const radians = degreesToRadians(latitude);
const num = Math.cos(radians) * EARTH_EQ_RADIUS * Math.PI / 180;
const denom = 1 / Math.sqrt(1 - E2 * Math.sin(radians) * Math.sin(radians));
const deltaDeg = num * denom;
if (deltaDeg < EPSILON) {
return distance > 0 ? 360 : 0;
}
// else
return Math.min(360, distance / deltaDeg);
}
wrapLongitude():
/**
* Wraps the longitude to [-180,180].
*
* #param {number} longitude The longitude to wrap.
* #return {number} longitude The resulting longitude.
*/
function wrapLongitude(longitude) {
if (longitude <= 180 && longitude >= -180) {
return longitude;
}
const adjusted = longitude + 180;
if (adjusted > 0) {
return (adjusted % 360) - 180;
}
// else
return 180 - (-adjusted % 360);
}
A new project has been introduced since #monkeybonkey first ask this question. The project is called GEOFirestore.
With this library you can perform queries like query documents within a circle:
const geoQuery = geoFirestore.query({
center: new firebase.firestore.GeoPoint(10.38, 2.41),
radius: 10.5
});
You can install GeoFirestore via npm. You will have to install Firebase separately (because it is a peer dependency to GeoFirestore):
$ npm install geofirestore firebase --save
As of today, there is no way to do such a query. There are other questions in SO related to it:
Is there a way to use GeoFire with Firestore?
How to query closest GeoPoints in a collection in Firebase Cloud Firestore?
Is there a way to use GeoFire with Firestore?
In my current Android project I may use https://github.com/drfonfon/android-geohash to add a geohash field while Firebase team is developing native support.
Using Firebase Realtime Database like suggested in other questions means that you can't filter your results set by location and other fields simultaneously, the main reason I want to switch to Firestore in the first place.
As of late 2020 there is now also documentation of how to do geoqueries with Firestore.
These solutions for iOS, Android, and Web, build on top of a slimmed down version of the Firebase-created GeoFire libraries, and then show how to:
Generate geohash values and store them in Firestore
Determine geohash ranges of the bounding box for a certain point and radius
Perform queries across these geohash ranges
This a bit more low-level than most of the other libraries presented here, so it may be a better fit for some use-cases and a worse fit for others.
Hijacking this thread to hopefully help anyone still looking. Firestore still does not support geo-based queries, and using the GeoFirestore library isnt ideal either as it will only let you search by location, nothing else.
I've put this together:
https://github.com/mbramwell1/GeoFire-Android
It basically lets you do nearby searches using a location and distance:
QueryLocation queryLocation = QueryLocation.fromDegrees(latitude, longitude);
Distance searchDistance = new Distance(1.0, DistanceUnit.KILOMETERS);
geoFire.query()
.whereNearTo(queryLocation, distance)
.build()
.get();
There are more docs on the repo. Its working for me so give it a try, hopefully it will do what you need.
For Dart
///
/// Checks if these coordinates are valid geo coordinates.
/// [latitude] The latitude must be in the range [-90, 90]
/// [longitude] The longitude must be in the range [-180, 180]
/// returns [true] if these are valid geo coordinates
///
bool coordinatesValid(double latitude, double longitude) {
return (latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180);
}
///
/// Checks if the coordinates of a GeopPoint are valid geo coordinates.
/// [latitude] The latitude must be in the range [-90, 90]
/// [longitude] The longitude must be in the range [-180, 180]
/// returns [true] if these are valid geo coordinates
///
bool geoPointValid(GeoPoint point) {
return (point.latitude >= -90 &&
point.latitude <= 90 &&
point.longitude >= -180 &&
point.longitude <= 180);
}
///
/// Wraps the longitude to [-180,180].
///
/// [longitude] The longitude to wrap.
/// returns The resulting longitude.
///
double wrapLongitude(double longitude) {
if (longitude <= 180 && longitude >= -180) {
return longitude;
}
final adjusted = longitude + 180;
if (adjusted > 0) {
return (adjusted % 360) - 180;
}
// else
return 180 - (-adjusted % 360);
}
double degreesToRadians(double degrees) {
return (degrees * math.pi) / 180;
}
///
///Calculates the number of degrees a given distance is at a given latitude.
/// [distance] The distance to convert.
/// [latitude] The latitude at which to calculate.
/// returns the number of degrees the distance corresponds to.
double kilometersToLongitudeDegrees(double distance, double latitude) {
const EARTH_EQ_RADIUS = 6378137.0;
// this is a super, fancy magic number that the GeoFire lib can explain (maybe)
const E2 = 0.00669447819799;
const EPSILON = 1e-12;
final radians = degreesToRadians(latitude);
final numerator = math.cos(radians) * EARTH_EQ_RADIUS * math.pi / 180;
final denom = 1 / math.sqrt(1 - E2 * math.sin(radians) * math.sin(radians));
final deltaDeg = numerator * denom;
if (deltaDeg < EPSILON) {
return distance > 0 ? 360.0 : 0.0;
}
// else
return math.min(360.0, distance / deltaDeg);
}
///
/// Defines the boundingbox for the query based
/// on its south-west and north-east corners
class GeoBoundingBox {
final GeoPoint swCorner;
final GeoPoint neCorner;
GeoBoundingBox({this.swCorner, this.neCorner});
}
///
/// Defines the search area by a circle [center] / [radiusInKilometers]
/// Based on the limitations of FireStore we can only search in rectangles
/// which means that from this definition a final search square is calculated
/// that contains the circle
class Area {
final GeoPoint center;
final double radiusInKilometers;
Area(this.center, this.radiusInKilometers):
assert(geoPointValid(center)), assert(radiusInKilometers >= 0);
factory Area.inMeters(GeoPoint gp, int radiusInMeters) {
return new Area(gp, radiusInMeters / 1000.0);
}
factory Area.inMiles(GeoPoint gp, int radiusMiles) {
return new Area(gp, radiusMiles * 1.60934);
}
/// returns the distance in km of [point] to center
double distanceToCenter(GeoPoint point) {
return distanceInKilometers(center, point);
}
}
///
///Calculates the SW and NE corners of a bounding box around a center point for a given radius;
/// [area] with the center given as .latitude and .longitude
/// and the radius of the box (in kilometers)
GeoBoundingBox boundingBoxCoordinates(Area area) {
const KM_PER_DEGREE_LATITUDE = 110.574;
final latDegrees = area.radiusInKilometers / KM_PER_DEGREE_LATITUDE;
final latitudeNorth = math.min(90.0, area.center.latitude + latDegrees);
final latitudeSouth = math.max(-90.0, area.center.latitude - latDegrees);
// calculate longitude based on current latitude
final longDegsNorth = kilometersToLongitudeDegrees(area.radiusInKilometers, latitudeNorth);
final longDegsSouth = kilometersToLongitudeDegrees(area.radiusInKilometers, latitudeSouth);
final longDegs = math.max(longDegsNorth, longDegsSouth);
return new GeoBoundingBox(
swCorner: new GeoPoint(latitudeSouth, wrapLongitude(area.center.longitude - longDegs)),
neCorner: new GeoPoint(latitudeNorth, wrapLongitude(area.center.longitude + longDegs)));
}
///
/// Calculates the distance, in kilometers, between two locations, via the
/// Haversine formula. Note that this is approximate due to the fact that
/// the Earth's radius varies between 6356.752 km and 6378.137 km.
/// [location1] The first location given
/// [location2] The second location given
/// sreturn the distance, in kilometers, between the two locations.
///
double distanceInKilometers(GeoPoint location1, GeoPoint location2) {
const radius = 6371; // Earth's radius in kilometers
final latDelta = degreesToRadians(location2.latitude - location1.latitude);
final lonDelta = degreesToRadians(location2.longitude - location1.longitude);
final a = (math.sin(latDelta / 2) * math.sin(latDelta / 2)) +
(math.cos(degreesToRadians(location1.latitude)) *
math.cos(degreesToRadians(location2.latitude)) *
math.sin(lonDelta / 2) *
math.sin(lonDelta / 2));
final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
return radius * c;
}
I just published a Flutter package based on the JS code above
https://pub.dartlang.org/packages/firestore_helpers
Yes, this is an old topic, but I want to help only on Java code. How I solved a problem with longitude? I used a code from Ryan Lee and Michael Teper.
A code:
#Override
public void getUsersForTwentyMiles() {
FirebaseFirestore db = FirebaseFirestore.getInstance();
double latitude = 33.0076665;
double longitude = 35.1011336;
int distance = 20; //20 milles
GeoPoint lg = new GeoPoint(latitude, longitude);
// ~1 mile of lat and lon in degrees
double lat = 0.0144927536231884;
double lon = 0.0181818181818182;
final double lowerLat = latitude - (lat * distance);
final double lowerLon = longitude - (lon * distance);
double greaterLat = latitude + (lat * distance);
final double greaterLon = longitude + (lon * distance);
final GeoPoint lesserGeopoint = new GeoPoint(lowerLat, lowerLon);
final GeoPoint greaterGeopoint = new GeoPoint(greaterLat, greaterLon);
Log.d(LOG_TAG, "local general lovation " + lg);
Log.d(LOG_TAG, "local lesserGeopoint " + lesserGeopoint);
Log.d(LOG_TAG, "local greaterGeopoint " + greaterGeopoint);
//get users for twenty miles by only a latitude
db.collection("users")
.whereGreaterThan("location", lesserGeopoint)
.whereLessThan("location", greaterGeopoint)
.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
UserData user = document.toObject(UserData.class);
//here a longitude condition (myLocation - 20 <= myLocation <= myLocation +20)
if (lowerLon <= user.getUserGeoPoint().getLongitude() && user.getUserGeoPoint().getLongitude() <= greaterLon) {
Log.d(LOG_TAG, "location: " + document.getId());
}
}
} else {
Log.d(LOG_TAG, "Error getting documents: ", task.getException());
}
}
});
}
Just inside after issuing the result set the filter to longitude:
if (lowerLon <= user.getUserGeoPoint().getLongitude() && user.getUserGeoPoint().getLongitude() <= greaterLon) {
Log.d(LOG_TAG, "location: " + document.getId());
}
I hope this will help someone.
Have a nice day!
You should use GeoFire (works with Firestore). With this you can filter documents on server and read less documents from your Firestore db. This will reduce your read count as well.
Check this lib for GroFire: https://github.com/patpatchpatrick/GeoFirestore-iOS
"patpatchpatrick" made this to Swift 5 compatible.
Just do a pod install as follows:
pod 'Geofirestore', :git => 'https://github.com/patpatchpatrick/GeoFirestore-iOS'
I am using this library in one of my projects and it works fine.
To set a location:
let location: CLLocation = CLLocation(latitude: lat, longitude: lng)
yourCollection.setLocation(location: location, forDocumentWithID: "YourDocId") { (error) in }
To remove location:
collection.removeLocation(forDocumentWithID: "YourDocId")
To get docs:
let center = CLLocation(latitude: lat, longitude: lng)
let collection = "Your collection path"
let circleQuery = collection.query(withCenter: center, radius: Double(yourRadiusVal))
let _ = circleQuery.observe(.documentEntered, with: { (key, location) in
//Use info as per your need
})
I have used .documentEntered, you can use other available geo queries like (Document Exited, Document Moved) as per your need.
You can query using GeoPoint as well.
This is not fully tested yet it should be a bit of an improvement on Ryan Lee's answer
My calculation is more accurate and then I filter the answers to remove hits which fall within the bounding box but outside the radius
Swift 4
func getDocumentNearBy(latitude: Double, longitude: Double, meters: Double) {
let myGeopoint = GeoPoint(latitude:latitude, longitude:longitude )
let r_earth : Double = 6378137 // Radius of earth in Meters
// 1 degree lat in m
let kLat = (2 * Double.pi / 360) * r_earth
let kLon = (2 * Double.pi / 360) * r_earth * __cospi(latitude/180.0)
let deltaLat = meters / kLat
let deltaLon = meters / kLon
let swGeopoint = GeoPoint(latitude: latitude - deltaLat, longitude: longitude - deltaLon)
let neGeopoint = GeoPoint(latitude: latitude + deltaLat, longitude: longitude + deltaLon)
let docRef : CollectionReference = appDelegate.db.collection("restos")
let query = docRef.whereField("location", isGreaterThan: swGeopoint).whereField("location", isLessThan: neGeopoint)
query.getDocuments { snapshot, error in
guard let snapshot = snapshot else {
print("Error fetching snapshot results: \(error!)")
return
}
self.documents = snapshot.documents.filter { (document) in
if let location = document.get("location") as? GeoPoint {
let myDistance = self.distanceBetween(geoPoint1:myGeopoint,geoPoint2:location)
print("myDistance:\(myDistance) distance:\(meters)")
return myDistance <= meters
}
return false
}
}
}
Functions which accurately measure the distance in Meters between 2 Geopoints for filtering
func distanceBetween(geoPoint1:GeoPoint, geoPoint2:GeoPoint) -> Double{
return distanceBetween(lat1: geoPoint1.latitude,
lon1: geoPoint1.longitude,
lat2: geoPoint2.latitude,
lon2: geoPoint2.longitude)
}
func distanceBetween(lat1:Double, lon1:Double, lat2:Double, lon2:Double) -> Double{ // generally used geo measurement function
let R : Double = 6378.137; // Radius of earth in KM
let dLat = lat2 * Double.pi / 180 - lat1 * Double.pi / 180;
let dLon = lon2 * Double.pi / 180 - lon1 * Double.pi / 180;
let a = sin(dLat/2) * sin(dLat/2) +
cos(lat1 * Double.pi / 180) * cos(lat2 * Double.pi / 180) *
sin(dLon/2) * sin(dLon/2);
let c = 2 * atan2(sqrt(a), sqrt(1-a));
let d = R * c;
return d * 1000; // meters
}
The easiest way is to calculate a "geo hash" when storing the location in the database.
A geo hash is a string which represents a location down to a certain accuracy. The longer the geo hash, the closer the locations with said geo hash must be. Two location which are e.g. 100m apart may have the same 6-char geo hash but when calculating a 7-char geo hash the last char might be different.
There are plenty libraries allowing you to calculate geo hashes for any language. Just store it alongside the location and use a == query to find locations with the same geo hash.
In javascript you can simply
const db = firebase.firestore();
//Geofire
import { GeoCollectionReference, GeoFirestore, GeoQuery, GeoQuerySnapshot } from 'geofirestore';
// Create a GeoFirestore reference
const geofirestore: GeoFirestore = new GeoFirestore(db);
// Create a GeoCollection reference
const geocollection: GeoCollectionReference = geofirestore.collection('<Your_collection_name>');
const query: GeoQuery = geocollectionDrivers.near({
center: new firebase.firestore.GeoPoint(location.latitude, location.longitude),
radius: 10000
});
query.onSnapshot(gquerySnapshot => {
gquerySnapshot.forEach(res => {
console.log(res.data());
})
});
A workaround for Flutter till we have native query in Firestore to pull ordered documents based on lat/long:
https://pub.dev/packages/geoflutterfire
A plugin to store geo hashes in the Firestore and query the same.
Limitations: limit not supported
There's a GeoFire library for Firestore called Geofirestore: https://github.com/imperiumlabs/GeoFirestore (Disclaimer: I helped develop it). It's super easy to use and offers the same features for Firestore that Geofire does for Firebase Realtime DB)

How to know zoom level to display a marker inside google marker cluster

well, almost everything is in the title :
I have thousands of markers with google marker cluster (not google marker cluster plus : let me know if it would help)
everything works perfectly
BUT
when I fire an event relative to a particular marker, I'd like to display this marker alone (not within the cluster anymore).
since spacial repartition of markers is not homogeneous : at some place a zoom level of 9 will display it alone but at another place, I'll have to set zoom level to 15.
SO
the ultimate question is : giving a particular maker, how to "query" marker cluster to know :
perhaps distance from closest maker (giving that I should be able to calculate corresponding zoom level)
any other helpful information I should still miss at the time ...
I have used this solution. It checks if selected marker is in cluster. If it is then increment the zoom level.
var zoom = 15;
this.map.setCenter(marker.getPosition());
var zoomInterval = setInterval($.proxy(function() {
if(!marker.map) {
this.map.setZoom(zoom++);
} else {
clearInterval(zoomInterval);
}
}, this), 400);
Note that if marker is in cluster it's 'map' property is 'null'.
Some code ;)
/**
* Computes scale in meters per pixel for given zoom and latitute.
*
* #param {Object} opt optional parameters
* - zoom
* - lat
* - precision
*
* #returns {Number} scale in meters per pixel
*/
google.maps.Map.prototype.getMapScale = function (opt){
var circumference = 40075040,
zoom, lat, scale;
if (typeof(opt['zoom']) == 'number' && typeof(opt['lat']) == 'number') {
zoom = opt['zoom'];
lat = opt['lat'];
} else {
zoom = this.getZoom();
lat = this.getCenter().lat();
}
scale = (circumference * Math.cos(lat) / Math.pow(2, zoom + 8));
if (typeof(opt['precision']) == 'number') {
scale = Number(scale.toFixed(opt['precision']));
}
return scale;
}
function calculateZoomLevelMarkerAloneInCluster(VigneronID){
var distance = 1000000000;
var found_marker_in_cluster = false;
/* GET ALL CLUSTERS FROM GMAP MARKER CLUSTER */
var clustersArray = mc.getClusters();
/* FOR ALL CLUSTERS */
$.each(clustersArray, function(key, cluster){
var markersClusterArray = cluster.getMarkers();
/* FOR ALL MARKERS IN THIS CLUSTER */
$.each(markersClusterArray, function(key, marker){
/* IF I FIND MY MARKER */
if(marker.VigneronID === VigneronID){
/* MARKER IS IN CLUSTER : no need to process all markers */
found_marker_in_cluster = true;
/* THEN AGAIN : FOR ALL MARKERS IN THIS CLUSTER */
var distance_tmp = 0;
/* FOR ALL MARKERS IN THIS CLUSTER */
$.each(markersClusterArray, function(key, marker2){
/* ALL MARKERS EXCEPT MINE (otherwise distance will always be 0 :-) ) */
if(marker.VigneronID !== marker2.VigneronID){
/* CALCULATE DISTANCE BETWEEN MY MARKER : http://stackoverflow.com/questions/1502590/calculate-distance-between-two-points-in-google-maps-v3 */
distance_tmp = google.maps.geometry.spherical.computeDistanceBetween(marker.getPosition(), marker2.getPosition());
/* KEEP MIN DISTANCE */
if(distance_tmp < distance && distance_tmp!==0){
distance = distance_tmp;
}
}
});
/* NO REASON TO CONTINUE PROCESS : BREAK */
return false;
}
});
});
if(found_marker_in_cluster === false){
var markersClusterArray = mc.getMarkers()
$.each(markersClusterArray, function(key, marker){
if(marker.VigneronID === VigneronID){
var distance_tmp = 0;
/* FOR ALL MARKERS IN THIS CLUSTER */
$.each(markersClusterArray, function(key, marker2){
/* ALL MARKERS EXCEPT MINE (otherwise distance will always be 0 :-) ) */
if(marker.VigneronID !== marker2.VigneronID){
/* CALCULATE DISTANCE BETWEEN MY MARKER : http://stackoverflow.com/questions/1502590/calculate-distance-between-two-points-in-google-maps-v3 */
distance_tmp = google.maps.geometry.spherical.computeDistanceBetween(marker.getPosition(), marker2.getPosition());
/* KEEP MIN DISTANCE */
if(distance_tmp < distance && distance_tmp!==0){
distance = distance_tmp;
}
}
});
}
});
}
var mapScale = map.getMapScale({}); /* meters / pixels */
var gridSizePixels = mc.getGridSize(); /* in pixels */
var gridSizeMeters = gridSizePixels * mapScale;
if(distance!==1000000000 && distance!==0){
if(distance < gridSizeMeters){
var factor = get2Factor(gridSizeMeters, distance);
map.setZoom(map.getZoom() + factor);
} else{
var factor = get2Factor(distance, gridSizeMeters);
map.setZoom(map.getZoom() - factor + 1);
}
}
/*alert(distance);*/
}
function get2Factor(grid, distance){
var count = 0;
while(distance < grid){
count++;
grid = grid / 2;
}
return count;
}
Some explanations :
firstly the code is highly commeted so read it
secondly :
the first function getMapScale has been found over the internet
before I use those functions I need to set my marker an identifier (here : marker.VigneronID) before doing a push in marker cluster
I do a fisrt iteration in cluster to find my marker and distance to the closest marker (in meter)
If I don't find my marker, it means it's not in a cluster so I do a second iteration over all markers managed by marker cluster and get the distance to the closest marker (in meter)
then I calculate gridSize in meter (thanks to the function getMapScale which transforms pixels in meters)
knowing that each zoom level is a factor 2 from previous zoom level, I compute the distance between my closest marker and my cluster grid size to know how many zooms levels I can add or substract to the current zoom level
YEAH !

Items drawing relative to player when near edge of map

I have a tile engine and that's all working swell, my player walks around all good, I'm working on adding items, the player is always in the centre of the screen, until he gets close to the edges of the world then he starts going close to the edges.
When I draw items in the world, they draw fine, except when the player leaves the centre (at the edge of the world). I just can't wrap my head around how to fix this.
public static void Draw(SpriteBatch spriteBatch, World w, Item i, Player p, Point screenDimensions)
{
bool IsOnScreen = true;
float leftX = p.X - ((screenDimensions.X / 32) / 2);
float rightX = leftX + (screenDimensions.X / 32);
float topY = p.Y - ((screenDimensions.Y / 32) / 2);
float bottomY = topY + (screenDimensions.Y / 32);
if (i.x < leftX || i.x > rightX || i.y < topY || i.y > bottomY)
IsOnScreen = false;
if (IsOnScreen)
i.animation.Draw(spriteBatch, (int)Math.Floor((i.x - leftX) * 32), (int)Math.Floor((i.y - topY) * 32));
}
Its pretty self explainatory, the world is passed in to get the dimensions (w.worldDimensions.x for width, and .y for height), the item is used to get the i.x and i.y (location in game world, not on screen), the player for drawing it relative (contains .x and .y for location) and then the screenDimensions.
Well it does not look very clear to me. Are you using a camera class? If you use a camera class and use that to navigate your world this should never happen.
Here is a basic one i currently use for my project.
class Camera
{
float zoom;
public float Rotation { get; private set; }
public Vector2 Position { get; private set; }
Matrix transform;
int velocity = 60;
UserInput input;
public float Zoom
{
get { return zoom; }
set { zoom = value; if (zoom < 0.1f) zoom = 0.1f; } // Negative zoom will flip image
}
public Camera(UserInput input)
{
zoom = 1.0f;
Rotation = 0f;
Position = new Vector2(0, 0);
this.input = input;
}
public void MoveCam()
{
if (input.IsKeyHold(Keys.Up))
{
Position += new Vector2(0, -velocity);
}
if (input.IsKeyHold(Keys.Left))
{
Position += new Vector2(-velocity, 0);
}
if (input.IsKeyHold(Keys.Down))
{
Position += new Vector2(0, velocity);
}
if (input.IsKeyHold(Keys.Right))
{
Position += new Vector2(velocity, 0);
}
if (input.IsKeyHold(Keys.OemMinus))
{
Zoom -= .01f * Zoom;
}
else if (input.IsKeyHold(Keys.OemPlus))
{
Zoom += .01f * Zoom;
}
}
public void FollowCam(int xPos, int yPos)
{
Position = new Vector2(xPos * TileData.Width, yPos * TileData.Height);
}
public Matrix TransformMatrix(GraphicsDevice device)
{
transform = Matrix.CreateTranslation(new Vector3(-Position.X, -Position.Y, 0)) *
Matrix.CreateRotationX(MathHelper.ToRadians(Rotation)) *
Matrix.CreateRotationY(MathHelper.ToRadians(Rotation)) *
Matrix.CreateRotationZ(MathHelper.ToRadians(Rotation)) *
Matrix.CreateScale(new Vector3(zoom, zoom, 0)) *
Matrix.CreateTranslation(new Vector3(device.Viewport.Width * 0.5f, device.Viewport.Height * 0.5f, 0));
return transform;
}
}
Just instantiate the class like in main and use this in your draw method.
batch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, null, null, null, null, camera.TransformMatrix(graphicsDevice));
batch.End()
Draw everything in your world within this spritebatch and use a new basic to draw to screen cooridinates, like a gui/hud. You can use the camera move method to move it manually and the lock to lock it on any location (it follows if updated).
If you have large maps you might want to render only necessary tiles. I do it like this in my map class:
public void Draw(SpriteBatch batch, Vector2 camPosition, float camZoom, GraphicsDevice device)
{
float top = (camPosition.Y / TileData.Height) - ((device.Viewport.Height / 2) / TileData.Height + 1) / camZoom;
float bottom = (camPosition.Y / TileData.Height) + ((device.Viewport.Height / 2) / TileData.Height + 2) / camZoom;
float left = (camPosition.X / TileData.Width) - ((device.Viewport.Width / 2) / TileData.Width + 1) / camZoom;
float right = (camPosition.X / TileData.Width) + ((device.Viewport.Width / 2) / TileData.Width + 2) / camZoom;
for (int y = (int)top; y < (int)bottom; y++)
{
for (int x = (int)left; x < (int)right; x++)
{
if (y >= 0 && y < map.GetLength(1) && x >= 0 && x < map.GetLength(0))
{
batch.Draw(map[x, y].texture, new Rectangle(x * TileData.Width, y * TileData.Height, TileData.Width, TileData.Height), Color.White);
}
}
}
}
Here first i figure out which tiles to draw from each direction. Note the camZoom, you want more tiles to be drawn when zooming out. Then i use these "bounderies" in my for loops, the if statement makes sure i am not accessing tiles that dont exist (out of bounds).

Get bounds of filters applied to Flash Sprite within Sprite

I have a Flash library with Sprite symbols composed of other sprites with design-time applied filters. I'm embedding those symbols into a Flex application like so:
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
[Bindable]
[Embed(source="Resources.swf", symbol="SquareContainer")]
private var squareContainer_class:Class;
private function log(msg:String):void {
output.text = output.text + "\n" + msg;
}
]]>
</mx:Script>
<mx:VBox horizontalAlign="center" width="100%" height="100%" >
<mx:Image id="squareContainer" source="{squareContainer_class}"/>
<mx:Button click="log(squareContainer.width + ', ' + squareContainer.height);"/>
<mx:TextArea id="output" width="100%" height="100%" />
</mx:VBox>
</mx:Application>
In this example, the SquareContainer symbol is 100px wide by 100px height; however it contains a child sprite with a glow and blur filter, that cause the sprite to appear to be significantly larger than 100x100. Since I cannot know for certain the composition of the container, I cannot use BitmapData.generateFilterRect() to get at the filters applied to nested sprites.
How can I get the size of the sprite plus its filters?
Oh sweet success! (and thanks for the tips) A friend helped solve the problem with a nice recursive function to handle the filters which may exist on nested sprites:
private function getDisplayObjectRectangle(container:DisplayObjectContainer, processFilters:Boolean):Rectangle {
var final_rectangle:Rectangle = processDisplayObjectContainer(container, processFilters);
// translate to local
var local_point:Point = container.globalToLocal(new Point(final_rectangle.x, final_rectangle.y));
final_rectangle = new Rectangle(local_point.x, local_point.y, final_rectangle.width, final_rectangle.height);
return final_rectangle;
}
private function processDisplayObjectContainer(container:DisplayObjectContainer, processFilters:Boolean):Rectangle {
var result_rectangle:Rectangle = null;
// Process if container exists
if (container != null) {
var index:int = 0;
var displayObject:DisplayObject;
// Process each child DisplayObject
for(var childIndex:int = 0; childIndex < container.numChildren; childIndex++){
displayObject = container.getChildAt(childIndex);
//If we are recursing all children, we also get the rectangle of children within these children.
if (displayObject is DisplayObjectContainer) {
// Let's drill into the structure till we find the deepest DisplayObject
var displayObject_rectangle:Rectangle = processDisplayObjectContainer(displayObject as DisplayObjectContainer, processFilters);
// Now, stepping out, uniting the result creates a rectangle that surrounds siblings
if (result_rectangle == null) {
result_rectangle = displayObject_rectangle.clone();
} else {
result_rectangle = result_rectangle.union(displayObject_rectangle);
}
}
}
// Get bounds of current container, at this point we're stepping out of the nested DisplayObjects
var container_rectangle:Rectangle = container.getBounds(container.stage);
if (result_rectangle == null) {
result_rectangle = container_rectangle.clone();
} else {
result_rectangle = result_rectangle.union(container_rectangle);
}
// Include all filters if requested and they exist
if ((processFilters == true) && (container.filters.length > 0)) {
var filterGenerater_rectangle:Rectangle = new Rectangle(0,0,result_rectangle.width, result_rectangle.height);
var bmd:BitmapData = new BitmapData(result_rectangle.width, result_rectangle.height, true, 0x00000000);
var filter_minimumX:Number = 0;
var filter_minimumY:Number = 0;
var filtersLength:int = container.filters.length;
for (var filtersIndex:int = 0; filtersIndex < filtersLength; filtersIndex++) {
var filter:BitmapFilter = container.filters[filtersIndex];
var filter_rectangle:Rectangle = bmd.generateFilterRect(filterGenerater_rectangle, filter);
filter_minimumX = filter_minimumX + filter_rectangle.x;
filter_minimumY = filter_minimumY + filter_rectangle.y;
filterGenerater_rectangle = filter_rectangle.clone();
filterGenerater_rectangle.x = 0;
filterGenerater_rectangle.y = 0;
bmd = new BitmapData(filterGenerater_rectangle.width, filterGenerater_rectangle.height, true, 0x00000000);
}
// Reposition filter_rectangle back to global coordinates
filter_rectangle.x = result_rectangle.x + filter_minimumX;
filter_rectangle.y = result_rectangle.y + filter_minimumY;
result_rectangle = filter_rectangle.clone();
}
} else {
throw new Error("No displayobject was passed as an argument");
}
return result_rectangle;
}
Here is a slightly different approach: just draw the entire object into BitmapData and then calculate the bounds of the non-transparent area of the bitmap. This approach might be more performant, especially on complex objects.
package lup.utils
{
import flash.display.BitmapData;
import flash.display.DisplayObject;
import flash.geom.Matrix;
import flash.geom.Point;
import flash.geom.Rectangle;
public class BoundsHelper
{
private var _hBmd:BitmapData;
private var _hBmdRect:Rectangle;
private var _hMtr:Matrix;
private var _hPoint:Point;
private var _xMin:Number;
private var _xMax:Number;
private var _yMin:Number;
private var _yMax:Number;
/**
* Specify maxFilteredObjWidth and maxFilteredObjHeight to match the maximum possible size
* of a filtered object. Performance of the helper is inversely proportional to the product
* of these values.
*
* #param maxFilteredObjWidth Maximum width of a filtered object.
* #param maxFilteredObjHeight Maximum height of a filtered object.
*/
public function BoundsHelper(maxFilteredObjWidth:Number = 500, maxFilteredObjHeight:Number = 500) {
_hMtr = new Matrix();
_hPoint = new Point();
_hBmd = new BitmapData(maxFilteredObjWidth, maxFilteredObjHeight, true, 0);
_hBmdRect = new Rectangle(0, 0, maxFilteredObjWidth, maxFilteredObjHeight);
}
/**
* Calculates the boundary rectangle of an object relative to the given coordinate space.
*
* #param obj The object which bounds are to be determined.
*
* #param space The coordinate space relative to which the bounds should be represented.
* If you pass null or the object itself, then the bounds will be represented
* relative to the (untransformed) object.
*
* #param dst Destination rectangle to store the result in. If you pass null,
* new rectangle will be created and returned. Otherwise, the passed
* rectangle will be updated and returned.
*/
public function getRealBounds(obj:DisplayObject, space:DisplayObject = null, dst:Rectangle = null):Rectangle {
var tx:Number = (_hBmdRect.width - obj.width ) / 2,
ty:Number = (_hBmdRect.height - obj.height) / 2;
// get transformation matrix that translates the object to the center of the bitmap
_hMtr.identity();
_hMtr.translate(tx, ty);
// clear the bitmap so it will contain only pixels with zero alpha channel
_hBmd.fillRect(_hBmdRect, 0);
// draw the object; it will be drawn untransformed, except for translation
// caused by _hMtr matrix
_hBmd.draw(obj, _hMtr);
// get the area which encloses all pixels with nonzero alpha channel (i.e. our object)
var bnd:Rectangle = dst ? dst : new Rectangle(),
selfBnd:Rectangle = _hBmd.getColorBoundsRect(0xFF000000, 0x00000000, false);
// transform the area to eliminate the effect of _hMtr transformation; now we've obtained
// the bounds of the object in its own coord. system (self bounds)
selfBnd.offset(-tx, -ty);
if (space && space !== obj) { // the dst coord space is different from the object's one
// so we need to obtain transformation matrix from the object's coord space to the dst one
var mObjToSpace:Matrix;
if (space === obj.parent) {
// optimization
mObjToSpace = obj.transform.matrix;
} else if (space == obj.stage) {
// optimization
mObjToSpace = obj.transform.concatenatedMatrix;
} else {
// general case
var mStageToSpace:Matrix = space.transform.concatenatedMatrix; // space -> stage
mStageToSpace.invert(); // stage -> space
mObjToSpace = obj.transform.concatenatedMatrix; // obj -> stage
mObjToSpace.concat(mStageToSpace); // obj -> space
}
// now we transform all four vertices of the boundary rectangle to the target space
// and determine the bounds of this transformed shape
_xMin = Number.MAX_VALUE;
_xMax = -Number.MAX_VALUE;
_yMin = Number.MAX_VALUE;
_yMax = -Number.MAX_VALUE;
expandBounds(mObjToSpace.transformPoint(getPoint(selfBnd.x, selfBnd.y)));
expandBounds(mObjToSpace.transformPoint(getPoint(selfBnd.right, selfBnd.y)));
expandBounds(mObjToSpace.transformPoint(getPoint(selfBnd.x, selfBnd.bottom)));
expandBounds(mObjToSpace.transformPoint(getPoint(selfBnd.right, selfBnd.bottom)));
bnd.x = _xMin;
bnd.y = _yMin;
bnd.width = _xMax - _xMin;
bnd.height = _yMax - _yMin;
} else {
// the dst coord space is the object's one, so we simply return the self bounds
bnd.x = selfBnd.x;
bnd.y = selfBnd.y;
bnd.width = selfBnd.width;
bnd.height = selfBnd.height;
}
return bnd;
}
private function getPoint(x:Number, y:Number):Point {
_hPoint.x = x;
_hPoint.y = y;
return _hPoint;
}
private function expandBounds(p:Point):void {
if (p.x < _xMin) {
_xMin = p.x;
}
if (p.x > _xMax) {
_xMax = p.x;
}
if (p.y < _yMin) {
_yMin = p.y;
}
if (p.y > _yMax) {
_yMax = p.y;
}
}
}
}
I'm not sure this is possible using the normal methods of getBounds or getRect as these methods will just return the normal 100x100 square.
I have a few of suggestions for you.
Firstly you could apply the filters dynamically. That way you will have the numbers for the filters and you could do the maths programatically to work out what the actual size is.
You could add a second layer to the movieclip in the fla, which has the dimensions of your original square plus all the filters. Then set the alpha for this square to zero
Thirdly you could just have a png in the fla that contained the square plus, say, the glow within it.
Personally i'd go with the middle option as it would require the least amount of maintenance if the filters or the original square were to change.
Well, there is good news and bad news. The bad news is that there really isn't an effective way to do this "the right way". The good news is that there is an adequate way to approximate it.The general rule is that the size of the width is that it is approximately ( filter.blurX * 1.5 ) + sprite.width where "filter" is the Filter in the sprite.filters array. The same is true regarding blurY and height. The other general rule is that the minimum x becomes sprite.x - ( filter.blurX * 1.5 ) / 2;None of these numbers are "Adobe official", but the work within a reasonable enough margin of error to allow you to create a BitmapData based on that.

Categories

Resources