Show nearest reports, handling the area of the report (not only as a

point, but polygon as well)
This commit is contained in:
Lucas Verney 2018-11-25 23:21:48 +01:00
parent aed7497d57
commit 09e5a2d4e2
7 changed files with 515 additions and 75 deletions

View File

@ -17,7 +17,8 @@ import requests
from functools import partial from functools import partial
from shapely.geometry import LineString, MultiPolygon, Point, Polygon from shapely.geometry import (LineString, MultiPolygon, MultiLineString,
MultiPoint, Point)
from shapely.geometry import mapping, shape from shapely.geometry import mapping, shape
from shapely.ops import transform from shapely.ops import transform
@ -596,22 +597,49 @@ def process_opendata(name, data, report_type=REPORT_TYPE):
# Report geographical shape # Report geographical shape
if 'geo_shape' in fields: if 'geo_shape' in fields:
geo_shape = shape(fields['geo_shape']) maybe_multi_geo_shape = shape(fields['geo_shape'])
else: else:
geo_shape = shape(item['geometry']) maybe_multi_geo_shape = shape(item['geometry'])
if isinstance(geo_shape, MultiPolygon): geo_shapes = []
# Split multipolygons into multiple polygons if (
isinstance(maybe_multi_geo_shape, MultiPolygon)
or isinstance(maybe_multi_geo_shape, MultiPoint)
):
# Split MultiPolygon into multiple Polygon
# Same for MultiPoint
positions = [ positions = [
p.centroid p.centroid
for p in geo_shape for p in maybe_multi_geo_shape
] ]
else: geo_shapes = [
p
for p in maybe_multi_geo_shape
]
elif isinstance(maybe_multi_geo_shape, MultiLineString):
# Split MultiLineString into multiple LineString
positions = [ positions = [
geo_shape.centroid p.interpolate(0.5, normalized=True)
for p in maybe_multi_geo_shape
] ]
geo_shapes = [
p
for p in maybe_multi_geo_shape
]
elif isinstance(maybe_multi_geo_shape, LineString):
# LineString, interpolate midpoint
positions = [
maybe_multi_geo_shape.interpolate(0.5, normalized=True)
]
geo_shapes = [maybe_multi_geo_shape]
else:
# Polygon or Point
positions = [
maybe_multi_geo_shape.centroid
]
geo_shapes = [maybe_multi_geo_shape]
for position in positions: for (geo_shape, position) in zip(geo_shapes, positions):
# Check if this precise position is already in the database # Check if this precise position is already in the database
if transform(project, position) in current_reports_points: if transform(project, position) in current_reports_points:
logging.info( logging.info(

View File

@ -28,6 +28,7 @@
<script> <script>
import Feature from 'ol/Feature'; import Feature from 'ol/Feature';
import GeoJSON from 'ol/format/GeoJSON';
import Map from 'ol/Map'; import Map from 'ol/Map';
import LineString from 'ol/geom/LineString'; import LineString from 'ol/geom/LineString';
import Point from 'ol/geom/Point'; import Point from 'ol/geom/Point';
@ -47,7 +48,7 @@ import compassNorthIcon from '@/assets/compassNorth.svg';
import unknownMarkerIcon from '@/assets/unknownMarker.svg'; import unknownMarkerIcon from '@/assets/unknownMarker.svg';
import * as constants from '@/constants'; import * as constants from '@/constants';
import REPORT_TYPES from '@/report-types'; import REPORT_TYPES from '@/report-types';
import { distance } from '@/tools'; import { pointToGeometryDistance } from '@/tools/geometry';
const MAIN_VECTOR_LAYER_NAME = 'MAIN'; const MAIN_VECTOR_LAYER_NAME = 'MAIN';
const REPORTS_MARKERS_VECTOR_LAYER_NAME = 'REPORTS_MARKERS'; const REPORTS_MARKERS_VECTOR_LAYER_NAME = 'REPORTS_MARKERS';
@ -168,22 +169,48 @@ export default {
return; return;
} }
// Create a Feature for the marker, to add it on the map // Read geometry from the marker object
const geometry = (new GeoJSON()).readGeometry(
marker.geometry,
);
geometry.transform('EPSG:4326', 'EPSG:3857');
const reportMarkerFeature = new Feature({ const reportMarkerFeature = new Feature({
geometry: new Point(fromLonLat([marker.latLng[1], marker.latLng[0]])), geometry,
id: marker.id, id: marker.id,
}); });
reportMarkerFeature.setStyle(new Style({ // Create a Feature for the marker, to add it on the map
image: new Icon({ reportMarkerFeature.setStyle([
anchor: constants.ICON_ANCHOR, new Style({
scale: ( stroke: new Stroke({
marker.id === this.reportDetailsID color: (
? constants.LARGE_ICON_SCALE marker.id === this.reportDetailsID
: constants.NORMAL_ICON_SCALE ? `rgb(${constants.MARKER_AREA_HL_COLOR.join(',')})`
), : `rgb(${constants.MARKER_AREA_NORMAL_COLOR.join(',')})`
src: REPORT_TYPES[marker.type].marker, ),
lineDash: [4],
width: 3,
}),
fill: new Fill({
color: (
marker.id === this.reportDetailsID
? `rgb(${constants.MARKER_AREA_HL_COLOR.join(',')}, 0.3)`
: `rgb(${constants.MARKER_AREA_NORMAL_COLOR.join(',')}, 0.3)`
),
}),
}), }),
})); new Style({
image: new Icon({
anchor: constants.ICON_ANCHOR,
scale: (
marker.id === this.reportDetailsID
? constants.LARGE_ICON_SCALE
: constants.NORMAL_ICON_SCALE
),
src: REPORT_TYPES[marker.type].marker,
}),
geometry: new Point(fromLonLat([marker.latLng[1], marker.latLng[0]])),
}),
]);
// Add the marker to the map and keep a reference to it // Add the marker to the map and keep a reference to it
this.reportsMarkersFeatures[marker.id] = reportMarkerFeature; this.reportsMarkersFeatures[marker.id] = reportMarkerFeature;
this.reportsMarkersVectorSource.addFeature(reportMarkerFeature); this.reportsMarkersVectorSource.addFeature(reportMarkerFeature);
@ -507,34 +534,6 @@ export default {
const view = this.map.getView(); const view = this.map.getView();
if (!this.isRecenterButtonShown && newOlCenter.every(item => item !== null)) { if (!this.isRecenterButtonShown && newOlCenter.every(item => item !== null)) {
// Eventually display closest report
const isReportDetailsAlreadyShown = this.$store.state.reportDetails.id;
const isReportDetailsOpenedByUser = this.$store.state.reportDetails.userAsked;
if (!isReportDetailsAlreadyShown || !isReportDetailsOpenedByUser) {
// Compute all markers distance, filter by max distance
const distances = this.markers.map(
marker => ({
id: marker.id,
distance: distance(this.center, marker.latLng),
}),
).filter(item => item.distance < constants.MIN_DISTANCE_REPORT_DETAILS);
const closestReport = distances.reduce( // Get the closest one
(acc, item) => (
item.distance < acc.distance ? item : acc
),
{ distance: Number.MAX_VALUE, id: -1 },
);
// TODO: Take into account the history of positions for the direction
if (closestReport.id !== -1) {
// Only open the details if the box was not just closed
if (this.$store.state.reportDetails.previousId !== closestReport.id) {
this.$store.dispatch('showReportDetails', { id: closestReport.id, userAsked: false });
}
} else {
this.$store.dispatch('hideReportDetails');
}
}
// Update view // Update view
view.setCenter(newOlCenter); view.setCenter(newOlCenter);
if (this.isInAutorotateMap) { if (this.isInAutorotateMap) {
@ -574,6 +573,35 @@ export default {
if (!this.isRecenterButtonShown) { if (!this.isRecenterButtonShown) {
this.map.getView().setCenter(newOlPosition); this.map.getView().setCenter(newOlPosition);
} }
// Eventually display closest report
const isReportDetailsAlreadyShown = this.$store.state.reportDetails.id;
const isReportDetailsOpenedByUser = this.$store.state.reportDetails.userAsked;
if (!isReportDetailsAlreadyShown || !isReportDetailsOpenedByUser) {
// Compute all markers distance, filter by max distance
// TODO: Compute distance to geometry, not to point
const distances = this.markers.map(
marker => ({
id: marker.id,
distance: pointToGeometryDistance(this.positionLatLng, marker.geometry),
}),
).filter(item => item.distance < constants.MIN_DISTANCE_REPORT_DETAILS);
const closestReport = distances.reduce( // Get the closest one
(acc, item) => (
item.distance < acc.distance ? item : acc
),
{ distance: Number.MAX_VALUE, id: -1 },
);
// TODO: Take into account the history of positions for the direction
if (closestReport.id !== -1) {
// Only open the details if the box was not just closed
if (this.$store.state.reportDetails.previousId !== closestReport.id) {
this.$store.dispatch('showReportDetails', { id: closestReport.id, userAsked: false });
}
} else {
this.$store.dispatch('hideReportDetails');
}
}
}, },
reportDetailsID(newID, oldID) { reportDetailsID(newID, oldID) {
[oldID, newID].forEach((id) => { [oldID, newID].forEach((id) => {

View File

@ -4,6 +4,9 @@ export const NORMAL_ICON_SCALE = 0.625;
export const LARGE_ICON_SCALE = 1.0; export const LARGE_ICON_SCALE = 1.0;
export const ICON_ANCHOR = [0.5, 1.0]; export const ICON_ANCHOR = [0.5, 1.0];
export const MARKER_AREA_HL_COLOR = [255, 152, 0];
export const MARKER_AREA_NORMAL_COLOR = [33, 33, 33];
// Display order of the report types // Display order of the report types
export const REPORT_TYPES_ORDER = ['gcum', 'interrupt', 'obstacle', 'pothole', 'accident', 'misc']; export const REPORT_TYPES_ORDER = ['gcum', 'interrupt', 'obstacle', 'pothole', 'accident', 'misc'];

View File

@ -1,6 +1,6 @@
import * as api from '@/api'; import * as api from '@/api';
import * as constants from '@/constants'; import * as constants from '@/constants';
import { distance } from '@/tools'; import { pointToPointDistance } from '@/tools/geometry';
import i18n from '@/i18n'; import i18n from '@/i18n';
import { import {
@ -34,7 +34,7 @@ export function fetchReports({ commit, state }) {
if (report.attributes.downvotes >= constants.REPORT_DOWNVOTES_THRESHOLD) { if (report.attributes.downvotes >= constants.REPORT_DOWNVOTES_THRESHOLD) {
return false; return false;
} }
return distance( return pointToPointDistance(
[report.attributes.lat, report.attributes.lng], [report.attributes.lat, report.attributes.lng],
state.map.center, state.map.center,
) < 10000; ) < 10000;

393
src/tools/geometry.js Normal file
View File

@ -0,0 +1,393 @@
/**
* Check whether a given point is within (in the interior) of a ring.
* Adapted from https://github.com/Turfjs/turf/, licensed under MIT.
*
* @param latLng The [latitude, longitude] coordinates of the point.
* @param ring A list of [latitude, longitude] for each vertex in
* the ring. The ring is always considered to be
* closed (last point being the same as the first
* one), even if that is not explicitly the case.
* @param ignoreBoundary Whether to consider a point on the boundary as
* being within the ring or not.
*
* @note This is used with latitude and longitude in mind,
* hence the names, but is much more generic and can
* be used with any (X, Y) coordinates.
*
* Examples:
* @code
* const ring = [[0, 0], [0, 1], [1, 1], [0.5, 0.5], [1, 0], [0, 0]];
* isInRing([0, 0], ring, false) === true
* isInRing([0, 0], ring, true) === false
* isInRing([0.25, 0.25], ring, false) === true
* isInRing([0.25, 0.25], ring, true) === true
* isInRing([0.8, 0.6], ring, true) === false
* isInRing([0.8, 0.6], ring, false) === false
* isInRing([2, 2], ring, false) === false
* isInRing([2, 2], ring, true) === false
* @endcode
*/
export function isInRing(latLng, ring, ignoreBoundary) {
let isInside = false;
// If the ring is a full loop, ignore the duplicate point
let openRing = Array.concat([], ring);
if (
openRing[0][0] === openRing[openRing.length - 1][0]
&& openRing[0][1] === openRing[openRing.length - 1][1]
) {
openRing = openRing.slice(0, openRing.length - 1);
}
for (let i = 0, j = openRing.length - 1; i < openRing.length; j = i, i += 1) {
// Get the current edge of the ring
const xi = openRing[i][0];
const yi = openRing[i][1];
const xj = openRing[j][0];
const yj = openRing[j][1];
// Check whether the point is on the boundary
const onBoundary = (
(latLng[1] * (xi - xj) + yi * (xj - latLng[0]) + yj * (latLng[0] - xi) === 0)
&& ((xi - latLng[0]) * (xj - latLng[0]) <= 0)
&& ((yi - latLng[1]) * (yj - latLng[1]) <= 0)
);
if (onBoundary) {
return !ignoreBoundary;
}
const intersect = (
((yi > latLng[1]) !== (yj > latLng[1]))
&& (latLng[0] < (xj - xi) * (latLng[1] - yi) / (yj - yi) + xi)
);
if (intersect) {
isInside = !isInside;
}
}
return isInside;
}
/**
* Check whether a point is within a given bbox.
* Adapted from https://github.com/Turfjs/turf/, licensed under MIT.
*
* @param latlng A [latitude, longitude] array for the point.
* @param bbox A [minLatitude, minLongitude, maxLatitude, maxLongitude]
* array representing the bbox.
* @return True if the point is within the bbox, false otherwise.
*
* @note This is used with latitude and longitude in mind, hence the
* names, but is much more generic and can be used with any
* (X, Y) coordinates.
*
* Examples:
* @code
* isInBBox([0.5, 0.5], [0, 0, 1, 1]) === true
* isInBBox([0, 1], [0, 0, 1, 1]) === true
* isInBBox([1, 0], [0, 0, 1, 1]) === true
* isInBBox([2, 0], [0, 0, 1, 1]) === false
* isInBBox([0, 2], [0, 0, 1, 1]) === false
* @endcode
*/
export function isInBBox(latLng, bbox) {
return (
bbox[0] <= latLng[0]
&& bbox[1] <= latLng[1]
&& bbox[2] >= latLng[0]
&& bbox[3] >= latLng[1]
);
}
/**
* Compute the bbox of a Polygon.
*
* @param polygon A list of [latitude, longitude] each vertex in the polygon
* (or polyline).
* @return A [minLatitude, minLongitude, maxLatitude, maxLongitude]
* array representing the bbox.
*
* @note This is used with latitude and longitude in mind, hence the
* names, but is much more generic and can be used with any
* (X, Y) coordinates.
* @note This works with a polygon or polyline.
*
* Examples:
* @code
* computeBBox([[0, 0], [1, 0], [1, 1], [0, 1]]) === [0, 0, 1, 1]
* @endcode
*/
export function computeBBox(polygon) {
const latList = polygon.map(item => item[0]);
const lngList = polygon.map(item => item[1]);
return [
Math.min(...latList),
Math.min(...lngList),
Math.max(...latList),
Math.max(...lngList),
];
}
/**
* Check whether a point is within a Polygon.
* Adapted from https://github.com/Turfjs/turf/, licensed under MIT.
*
* @param latLng A [latitude, longitude] array for the point.
* @param polygon An array of [latitude, longitude] arrays for each
* vertex of the Polygon (polygon ring).
* @param ignoreBoundary Whether a point on the boundary should be considered
* within the Polygon or not. Default to false.
* @return true if the point is within the Polygon, false
* otherwise.
*
* @note This is used with latitude and longitude in mind,
* hence the names, but is much more generic and can
* be used with any (X, Y) coordinates.
*
* Examples:
* @code
* const ring = [[0, 0], [0, 1], [1, 1], [0.5, 0.5], [1, 0], [0, 0]];
* isWithinPolygon([0, 0], ring, false) === true
* isWithinPolygon([0, 0], ring, true) === false
* isWithinPolygon([0.25, 0.25], ring, false) === true
* isWithinPolygon([0.25, 0.25], ring, true) === true
* isWithinPolygon([0.8, 0.6], ring, true) === false
* isWithinPolygon([0.8, 0.6], ring, false) === false
* isWithinPolygon([2, 2], ring, false) === false
* isWithinPolygon([2, 2], ring, true) === false
* @endcode
*/
export function isWithinPolygon(latLng, polygon, ignoreBoundary) {
const shouldIgnoreBoundary = ignoreBoundary || false;
// Quick check: is point inside bbox?
const bbox = computeBBox(polygon);
if (isInBBox(latLng, bbox) === false) {
return false;
}
// Thorough check
if (isInRing(latLng, polygon, shouldIgnoreBoundary)) {
return true;
}
return false;
}
/* eslint-disable max-len */
/**
* Cheap distance computation between two points based on
* https://blog.mapbox.com/fast-geodesic-approximations-with-cheap-ruler-106f229ad016.
* (ISC license)
*
* @param latLng1 A [latitude, longitude] array for the first point.
* @param latLng2 A [latitude, longitude] array for the second point.
* @return The distance in meters.
*
* Examples
* @code
* // Vincenty's formula gives 5177.692 meters
* Math.abs(pointToPointDistance([48.8171, 2.3186], [48.8454, 2.3746]) - 5177.692) / pointToPointDistance([48.8171, 2.3186], [48.8454, 2.3746]) < 1 / 100
* // Vincenty's formula gives 7720.121 meters
* Math.abs(pointToPointDistance([50.6314, 3.0027], [50.6271, 3.1116]) - 7720.121) / pointToPointDistance([50.6314, 3.0027], [50.6271, 3.1116]) < 1 / 100
* // Vincenty's formula gives 10443.762 meters
* Math.abs(pointToPointDistance([42.6722, 2.8508], [42.7093, 2.9679]) - 10443.762) / pointToPointDistance([42.6722, 2.8508], [42.7093, 2.9679]) < 1 / 100
* @endcode
*/
/* eslint-enable max-len */
export function pointToPointDistance(latLng1, latLng2) {
const cos = Math.cos((latLng1[0] + latLng2[0]) / 2 * Math.PI / 180);
const cos2 = 2 * cos * cos - 1;
const cos3 = 2 * cos * cos2 - cos;
const cos4 = 2 * cos * cos3 - cos2;
const cos5 = 2 * cos * cos4 - cos3;
// Multipliers for converting longitude and latitude degrees into distance
// (http://1.usa.gov/1Wb1bv7)
const kx = 1000 * (111.41513 * cos - 0.09455 * cos3 + 0.00012 * cos5);
const ky = 1000 * (111.13209 - 0.56605 * cos2 + 0.0012 * cos4);
const dx = (latLng1[1] - latLng2[1]) * kx;
const dy = (latLng1[0] - latLng2[0]) * ky;
return Math.sqrt(dx * dx + dy * dy);
}
/**
* Compute the dot product of two vectors.
* Adapted from https://github.com/Turfjs/turf/, licensed under MIT.
*
* @param u Array of coordinates of the first vector.
* @param v Array of coordinates of the second vector.
* @return The dot product of the two vectors.
*
* Examples:
* @code
* Math.abs(dot([1, 0], [0, 0]) - 0) < Number.EPSILON
* Math.abs(dot([1, 0], [1, 0]) - 1) < Number.EPSILON
* Math.abs(dot([1, 1], [1, 2]) - 3) < Number.EPSILON
* @endcode
*/
export function dot(u, v) {
return (u[0] * v[0] + u[1] * v[1]);
}
/* eslint-disable max-len */
/**
* Compute the distance between a point and a polyLine.
* Adapted from https://github.com/Turfjs/turf/, licensed under MIT.
*
* @param latLng An array [latitude, longitude] for the point to
* compute distance from.
* @param polyLine A list of [latitude, longitude] arrays for each vertex
* of the polyLine.
* @return The distance between the point and the polyLine.
*
* Examples:
* @code
* const polyLine = [[48.8105, 2.3088], [48.8098, 2.3218]];
*
* // Any point at the end
* Math.abs(pointToLineDistance([48.8105, 2.3088], polyLine) - 0) < Number.EPSILON
* Math.abs(pointToLineDistance([48.8098, 2.3218], polyLine) - 0) < Number.EPSILON
*
* // Points in misc positions
* Math.abs(pointToLineDistance([48.8170, 2.3188], polyLine) - 780) / 780 < 1 / 100
* Math.abs(pointToLineDistance([48.8121, 2.3074], polyLine) - 205) / 205 < 1 / 100
* Math.abs(pointToLineDistance([48.8089, 2.3315], polyLine) - 720) / 720 < 5 / 100
*
* // Longer polyLine
* Math.abs(pointToLineDistance([48.8098, 2.3218], [[48.8105, 2.3088], [48.8098, 2.3218], [48.8089, 2.3315]]) - 0) < Number.EPSILON
* Math.abs(pointToLineDistance([48.82787, 2.32686], [[48.809982, 2.3190774], [48.8176872, 2.3320935], [48.8182127, 2.3323712], [48.8222148, 2.3143633], [48.8222632, 2.314133], [48.8115136, 2.3002323], [48.8113242, 2.3000166], [48.809982, 2.3190774]]) - 900) / 900 < 1 / 100
* @endcode
*/
/* eslint-enable max-len */
export function pointToLineDistance(latLng, polyLine) {
let distance = Number.POSITIVE_INFINITY;
// Iterate over the segments forming the polyLine
for (let i = 0; i < (polyLine.length - 1); i += 1) {
// Distance between point and the current segment
let distanceToSegment = null;
// Origin and end of the segment
const a = polyLine[i];
const b = polyLine[i + 1];
// Segment vector
const v = [b[0] - a[0], b[1] - a[1]];
// Point to origin of the segment vector
const w = [latLng[0] - a[0], latLng[1] - a[1]];
const c1 = dot(w, v);
if (c1 <= 0) {
// Point is closer to origin
distanceToSegment = pointToPointDistance(latLng, a);
} else {
const c2 = dot(v, v);
if (c2 <= c1) {
// Point is closer to end
distanceToSegment = pointToPointDistance(latLng, b);
} else {
const b2 = c1 / c2;
const Pb = [a[0] + (b2 * v[0]), a[1] + (b2 * v[1])];
distanceToSegment = pointToPointDistance(latLng, Pb);
}
}
if (distanceToSegment < distance) {
distance = distanceToSegment;
}
}
return distance;
}
/* eslint-disable max-len */
/**
* Compute the distance between a point and a polygon.
*
* @param latLng A [latitude, longitude] array representing the point.
* @param polygon A list of [latitude, longitude] arrays of the vertices of
* the polygon.
* @return The distance between the point and the polygon.
*
* Examples:
* @code
* const polygon = [[48.809982, 2.3190774], [48.8176872, 2.3320935], [48.8182127, 2.3323712], [48.8222148, 2.3143633], [48.8222632, 2.314133], [48.8115136, 2.3002323], [48.8113242, 2.3000166], [48.809982, 2.3190774]];
*
* // Point on the ring
* Math.abs(pointToPolygonDistance([48.809982, 2.3190774], polygon) - 0) < Number.EPSILON
* // Point in the inside
* Math.abs(pointToPolygonDistance([48.8161, 2.3169], polygon) - 0) < Number.EPSILON
* // Point outside of the ring
* Math.abs(pointToPolygonDistance([48.82787, 2.32686], polygon) - 900) / 900 < 1 / 100
* @endcode
*/
/* eslint-enable max-len */
export function pointToPolygonDistance(latLng, polygon) {
const polygonRing = polygon;
// Ensure the polygon ring is a full loop
if (
polygonRing[0][0] !== polygonRing[polygonRing.length - 1][0]
&& polygonRing[0][1] !== polygonRing[polygonRing.length - 1][1]
) {
polygonRing.push(polygonRing[0]);
}
// First, check whether the point is on or inside the polygon
if (isWithinPolygon(latLng, polygonRing, false)) {
return 0;
}
// Otherwise return the distance from the point to the polygon ring.
return pointToLineDistance(latLng, polygonRing);
}
/* eslint-disable max-len */
/**
* Compute the distance between a point and a GeoJSON geometry.
*
* @param latLng A [latitude, longitude] array representing the point.
* @param geometry A GeoJSON-like geometry (Object with "type" and
* "coordinates" keys). Coordinates are GeoJSON-like,
* longitude first and latitude then.
* @return The distance between the point and the geometry.
*
* Examples:
* @code
* const point = { type: 'Point', coordinates: [2.3746, 48.8454] }
* Math.abs(pointToGeometryDistance([48.8171, 2.3186], point) - 5177.692) / 5177.692 < 1 / 100
*
* const lineString = { type: 'LineString', coordinates: [[2.3088, 48.8105], [2.3218, 48.8098]] }
* Math.abs(pointToGeometryDistance([48.8170, 2.3188], lineString) - 780) / 780 < 1 / 100
*
* const polygon = { type: 'Polygon', coordinates: [[2.3190774, 48.809982], [2.3320935, 48.8176872], [2.3323712, 48.8182127], [2.3143633, 48.8222148], [2.314133, 48.8222632], [2.3002323, 48.8115136], [2.3000166, 48.8113242], [2.3190774, 48.809982]] }
* Math.abs(pointToGeometryDistance([48.82787, 2.32686], polygon) - 900) / 900 < 1 / 100
*
* const unknownGeometry = { type: 'Foobar', coordinates: [48.8454, 2.3746] }
* pointToGeometryDistance([48.82787, 2.32686], unknownGeometry) === null
* @endcode
*/
/* eslint-enable max-len */
export function pointToGeometryDistance(latLng, geometry) {
const lngLatCoordinates = Array.concat([], geometry.coordinates);
if (geometry.type === 'Point') {
return pointToPointDistance(latLng, lngLatCoordinates.reverse());
}
if (geometry.type === 'LineString') {
return pointToLineDistance(latLng, lngLatCoordinates.map(item => item.reverse()));
}
if (geometry.type === 'Polygon') {
return pointToPolygonDistance(latLng, lngLatCoordinates.map(item => item.reverse()));
}
// Unsupported geometry
return null;
}

View File

@ -14,27 +14,6 @@ if (process.env.NODE_ENV !== 'production') {
mockGPX = require('mock_gpx.json'); // eslint-disable-line global-require mockGPX = require('mock_gpx.json'); // eslint-disable-line global-require
} }
/**
* Cheap distance computation based on
* https://blog.mapbox.com/fast-geodesic-approximations-with-cheap-ruler-106f229ad016.
*/
export function distance(latLng1, latLng2) {
const cos = Math.cos((latLng1[0] + latLng2[0]) / 2 * Math.PI / 180);
const cos2 = 2 * cos * cos - 1;
const cos3 = 2 * cos * cos2 - cos;
const cos4 = 2 * cos * cos3 - cos2;
const cos5 = 2 * cos * cos4 - cos3;
// Multipliers for converting longitude and latitude degrees into distance
// (http://1.usa.gov/1Wb1bv7)
const kx = 1000 * (111.41513 * cos - 0.09455 * cos3 + 0.00012 * cos5);
const ky = 1000 * (111.13209 - 0.56605 * cos2 + 0.0012 * cos4);
const dx = (latLng1[1] - latLng2[1]) * kx;
const dy = (latLng1[0] - latLng2[0]) * ky;
return Math.sqrt(dx * dx + dy * dy);
}
export function mockLocationRandom() { export function mockLocationRandom() {
let heading = null; let heading = null;
if (Math.random() > 0.25) { if (Math.random() > 0.25) {

View File

@ -61,7 +61,8 @@ import ReportCard from '@/components/ReportCard.vue';
import ReportDialog from '@/components/ReportDialog/index.vue'; import ReportDialog from '@/components/ReportDialog/index.vue';
import * as constants from '@/constants'; import * as constants from '@/constants';
import { distance, mockLocation } from '@/tools'; import { mockLocation } from '@/tools';
import { pointToPointDistance } from '@/tools/geometry';
import i18n from '@/i18n'; import i18n from '@/i18n';
import store from '@/store'; import store from '@/store';
@ -176,6 +177,14 @@ export default {
id: report.id, id: report.id,
type: report.attributes.type, type: report.attributes.type,
latLng: [report.attributes.lat, report.attributes.lng], latLng: [report.attributes.lat, report.attributes.lng],
geometry: (
report.attributes.shape_geojson
? JSON.parse(report.attributes.shape_geojson)
: {
type: 'Point',
coordinates: [report.attributes.lng, report.attributes.lat],
}
),
})); }));
}, },
}, },
@ -263,7 +272,7 @@ export default {
&& lastFetchingLocation[0] !== null && lastFetchingLocation[0] !== null
&& lastFetchingLocation[1] !== null && lastFetchingLocation[1] !== null
) { ) {
distanceFromPreviousPoint = distance( distanceFromPreviousPoint = pointToPointDistance(
[lastFetchingLocation[0], lastFetchingLocation[1]], [lastFetchingLocation[0], lastFetchingLocation[1]],
[center[0], center[1]], [center[0], center[1]],
); );