зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1627560 - Implement local geocoding r=Standard8
Differential Revision: https://phabricator.services.mozilla.com/D78611
This commit is contained in:
Родитель
a114756678
Коммит
01b19f11ad
|
@ -264,6 +264,175 @@ class RegionDetector {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the users current region using
|
||||
* request that is used for GeoLocation requests.
|
||||
*
|
||||
* @returns {String}
|
||||
* A 2 character string representing a region.
|
||||
*/
|
||||
async _getRegionLocally() {
|
||||
let { location } = await this._getLocation();
|
||||
return this._geoCode(location);
|
||||
}
|
||||
|
||||
// TODO: Stubs for testing
|
||||
async _getPlainMap() {
|
||||
return null;
|
||||
}
|
||||
async _getBufferedMap() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a location and return the region code for that location
|
||||
* by looking up the coordinates in geojson map files.
|
||||
* Inspired by https://github.com/mozilla/ichnaea/blob/874e8284f0dfa1868e79aae64e14707eed660efe/ichnaea/geocode.py#L114
|
||||
*
|
||||
* @param {Object} location
|
||||
* A location object containing lat + lng coordinates.
|
||||
*
|
||||
* @returns {String}
|
||||
* A 2 character string representing a region.
|
||||
*/
|
||||
async _geoCode(location) {
|
||||
let plainMap = await this._getPlainMap();
|
||||
let polygons = this._getPolygonsContainingPoint(location, plainMap);
|
||||
// The plain map doesnt have overlapping regions so return
|
||||
// region straight away.
|
||||
if (polygons.length) {
|
||||
return polygons[0].region;
|
||||
}
|
||||
let bufferedMap = await this._getBufferedMap();
|
||||
polygons = this._getPolygonsContainingPoint(location, bufferedMap);
|
||||
// Only found one matching region, return.
|
||||
if (polygons.length === 1) {
|
||||
return polygons[0].region;
|
||||
}
|
||||
// Matched more than one region, find the longest distance
|
||||
// from a border and return that region.
|
||||
if (polygons.length > 1) {
|
||||
return this._findLargestDistance(location, polygons);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all the polygons that contain a single point, return
|
||||
* an array of those polygons along with the region that
|
||||
* they define
|
||||
*
|
||||
* @param {Object} point
|
||||
* A lat + lng coordinate.
|
||||
* @param {Object} map
|
||||
* Geojson object that defined seperate regions with a list
|
||||
* of polygons.
|
||||
*
|
||||
* @returns {Array}
|
||||
* An array of polygons that contain the point, along with the
|
||||
* region they define.
|
||||
*/
|
||||
_getPolygonsContainingPoint(point, map) {
|
||||
let polygons = [];
|
||||
for (const feature of map.features) {
|
||||
let coords = feature.geometry.coordinates;
|
||||
if (feature.geometry.type === "Polygon") {
|
||||
if (this._polygonInPoint(point, coords[0])) {
|
||||
polygons.push({
|
||||
coords: coords[0],
|
||||
region: feature.properties.alpha2,
|
||||
});
|
||||
}
|
||||
} else if (feature.geometry.type === "MultiPolygon") {
|
||||
for (const innerCoords of coords) {
|
||||
if (this._polygonInPoint(point, innerCoords[0])) {
|
||||
polygons.push({
|
||||
coords: innerCoords[0],
|
||||
region: feature.properties.alpha2,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return polygons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the largest distance between a point and and a border
|
||||
* that contains it.
|
||||
*
|
||||
* @param {Object} location
|
||||
* A lat + lng coordinate.
|
||||
* @param {Object} polygons
|
||||
* Array of polygons that define a border.
|
||||
*
|
||||
* @returns {String}
|
||||
* A 2 character string representing a region.
|
||||
*/
|
||||
_findLargestDistance(location, polygons) {
|
||||
let maxDistance = { distance: 0, region: null };
|
||||
for (const polygon of polygons) {
|
||||
for (const [lng, lat] of polygon.coords) {
|
||||
let distance = this._distanceBetween(location, { lng, lat });
|
||||
if (distance > maxDistance.distance) {
|
||||
maxDistance = { distance, region: polygon.region };
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxDistance.region;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a point is contained within a polygon using the
|
||||
* point in polygon algorithm:
|
||||
* https://en.wikipedia.org/wiki/Point_in_polygon
|
||||
* This casts a ray from the point and counts how many times
|
||||
* that ray intersects with the polygons borders, if it is
|
||||
* an odd number of times the point is inside the polygon.
|
||||
*
|
||||
* @param {Object} location
|
||||
* A lat + lng coordinate.
|
||||
* @param {Object} polygon
|
||||
* Array of coordinates that define the boundaries of a polygon.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* Whether the point is within the polygon.
|
||||
*/
|
||||
_polygonInPoint({ lng, lat }, poly) {
|
||||
let inside = false;
|
||||
// For each edge of the polygon.
|
||||
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
||||
let xi = poly[i][0];
|
||||
let yi = poly[i][1];
|
||||
let xj = poly[j][0];
|
||||
let yj = poly[j][1];
|
||||
// Does a ray cast from the point intersect with this polygon edge.
|
||||
let intersect =
|
||||
yi > lat != yj > lat && lng < ((xj - xi) * (lat - yi)) / (yj - yi) + xi;
|
||||
// If so toggle result, an odd number of intersections
|
||||
// means the point is inside.
|
||||
if (intersect) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the distance between 2 points.
|
||||
*
|
||||
* @param {Object} p1
|
||||
* A lat + lng coordinate.
|
||||
* @param {Object} p2
|
||||
* A lat + lng coordinate.
|
||||
*
|
||||
* @returns {int}
|
||||
* The distance between the 2 points.
|
||||
*/
|
||||
_distanceBetween(p1, p2) {
|
||||
return Math.hypot(p2.lng - p1.lng, p2.lat - p1.lat);
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around fetch that implements a timeout, will throw
|
||||
* a TIMEOUT error if the request is not completed in time.
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -0,0 +1,52 @@
|
|||
"use strict";
|
||||
|
||||
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
|
||||
const { Region } = ChromeUtils.import("resource://gre/modules/Region.jsm");
|
||||
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
|
||||
|
||||
async function readFile(file) {
|
||||
let decoder = new TextDecoder();
|
||||
let data = await OS.File.read(file.path);
|
||||
return decoder.decode(data);
|
||||
}
|
||||
|
||||
function setLocation(location) {
|
||||
Services.prefs.setCharPref(
|
||||
"geo.provider.network.url",
|
||||
`data:application/json,${JSON.stringify({ location })}`
|
||||
);
|
||||
}
|
||||
|
||||
async function stubMap(path, fun) {
|
||||
let map = await readFile(do_get_file(path));
|
||||
sinon.stub(Region, fun).resolves(JSON.parse(map));
|
||||
}
|
||||
|
||||
add_task(async function test_setup() {
|
||||
await stubMap("regions/world.geojson", "_getPlainMap");
|
||||
await stubMap("regions/world-buffered.geojson", "_getBufferedMap");
|
||||
});
|
||||
|
||||
const LOCATIONS = [
|
||||
{ lat: 55.867005, lng: -4.271078, expectedRegion: "GB" },
|
||||
// Small cove in Italy surrounded by another region.
|
||||
{ lat: 45.6523148, lng: 13.7486427, expectedRegion: "IT" },
|
||||
// In Bosnia and Herzegovina but within a lot of borders.
|
||||
{ lat: 42.557079, lng: 18.4370373, expectedRegion: "HR" },
|
||||
// In the sea bordering Italy and a few other regions.
|
||||
{ lat: 45.608696, lng: 13.4667903, expectedRegion: "IT" },
|
||||
// In the middle of the Atlantic.
|
||||
{ lat: 35.4411368, lng: -41.5372973, expectedRegion: null },
|
||||
];
|
||||
|
||||
add_task(async function test_basic() {
|
||||
for (const { lat, lng, expectedRegion } of LOCATIONS) {
|
||||
setLocation({ lat, lng });
|
||||
let region = await Region._getRegionLocally();
|
||||
Assert.equal(
|
||||
region,
|
||||
expectedRegion,
|
||||
`Got the expected region at ${lat},${lng}`
|
||||
);
|
||||
}
|
||||
});
|
|
@ -7,6 +7,8 @@ support-files =
|
|||
chromeappsstore.sqlite
|
||||
corrupt.sqlite
|
||||
zips/zen.zip
|
||||
regions/world.geojson
|
||||
regions/world-buffered.geojson
|
||||
|
||||
[test_BinarySearch.js]
|
||||
[test_CanonicalJSON.js]
|
||||
|
@ -40,6 +42,7 @@ skip-if = toolkit == 'android'
|
|||
skip-if = os != 'mac'
|
||||
[test_readCertPrefs.js]
|
||||
[test_Region.js]
|
||||
[test_Region_geocoding.js]
|
||||
[test_Services.js]
|
||||
[test_sqlite.js]
|
||||
skip-if = toolkit == 'android' || (verify && !debug && os == 'win')
|
||||
|
|
Загрузка…
Ссылка в новой задаче