Show nearest reports, handling the area of the report (not only as a
point, but polygon as well)
This commit is contained in:
parent
aed7497d57
commit
09e5a2d4e2
@ -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(
|
||||||
|
@ -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) => {
|
||||||
|
@ -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'];
|
||||||
|
|
||||||
|
@ -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
393
src/tools/geometry.js
Normal 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;
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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]],
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user