/********************************************************************** * * PostGIS - Spatial Types for PostgreSQL * http://postgis.net * * PostGIS is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * PostGIS is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PostGIS. If not, see . * ********************************************************************** * * Copyright 2015 Daniel Baston * Copyright 2017 Darafei Praliaskouski * **********************************************************************/ #include #include #include "liblwgeom_internal.h" #include "lwgeom_log.h" static double calc_weighted_distances_3d(const POINT3D* curr, const POINT4D* points, uint32_t npoints, double* distances) { uint32_t i; double weight = 0.0; for (i = 0; i < npoints; i++) { double dist = distance3d_pt_pt(curr, (POINT3D*)&points[i]); distances[i] = dist / points[i].m; weight += dist * points[i].m; } return weight; } static uint32_t iterate_4d(POINT3D* curr, const POINT4D* points, const uint32_t npoints, const uint32_t max_iter, const double tol) { uint32_t i, iter; double delta; double sum_curr = 0, sum_next = 0; int hit = LW_FALSE; double *distances = lwalloc(npoints * sizeof(double)); sum_curr = calc_weighted_distances_3d(curr, points, npoints, distances); for (iter = 0; iter < max_iter; iter++) { POINT3D next = { 0, 0, 0 }; double denom = 0; /** Calculate denom to get the next point */ for (i = 0; i < npoints; i++) { /* we need to use lower epsilon than in FP_IS_ZERO in the loop for calculation to converge */ if (distances[i] > DBL_EPSILON) { next.x += points[i].x / distances[i]; next.y += points[i].y / distances[i]; next.z += points[i].z / distances[i]; denom += 1.0 / distances[i]; } else { hit = LW_TRUE; } } if (denom < DBL_EPSILON) { /* No movement - Final point */ break; } /* Calculate the new point */ next.x /= denom; next.y /= denom; next.z /= denom; /* If any of the intermediate points in the calculation is found in the * set of input points, the standard Weiszfeld method gets stuck with a * divide-by-zero. * * To get ourselves out of the hole, we follow an alternate procedure to * get the next iteration, as described in: * * Vardi, Y. and Zhang, C. (2011) "A modified Weiszfeld algorithm for the * Fermat-Weber location problem." Math. Program., Ser. A 90: 559-566. * DOI 10.1007/s101070100222 * * Available online at the time of this writing at * http://www.stat.rutgers.edu/home/cunhui/papers/43.pdf */ if (hit) { double dx = 0, dy = 0, dz = 0; double d_sqr; hit = LW_FALSE; for (i = 0; i < npoints; i++) { if (distances[i] > DBL_EPSILON) { dx += (points[i].x - curr->x) / distances[i]; dy += (points[i].y - curr->y) / distances[i]; dz += (points[i].z - curr->z) / distances[i]; } } d_sqr = sqrt(dx*dx + dy*dy + dz*dz); if (d_sqr > DBL_EPSILON) { double r_inv = FP_MAX(0, 1.0 / d_sqr); next.x = (1.0 - r_inv)*next.x + r_inv*curr->x; next.y = (1.0 - r_inv)*next.y + r_inv*curr->y; next.z = (1.0 - r_inv)*next.z + r_inv*curr->z; } } /* Check movement with next point */ sum_next = calc_weighted_distances_3d(&next, points, npoints, distances); delta = sum_curr - sum_next; if (delta < tol) { break; } else { curr->x = next.x; curr->y = next.y; curr->z = next.z; sum_curr = sum_next; } } lwfree(distances); return iter; } static POINT3D init_guess(const POINT4D* points, uint32_t npoints) { assert(npoints > 0); POINT3D guess = { 0, 0, 0 }; double mass = 0; uint32_t i; for (i = 0; i < npoints; i++) { guess.x += points[i].x * points[i].m; guess.y += points[i].y * points[i].m; guess.z += points[i].z * points[i].m; mass += points[i].m; } guess.x /= mass; guess.y /= mass; guess.z /= mass; return guess; } POINT4D* lwmpoint_extract_points_4d(const LWMPOINT* g, uint32_t* npoints, int* input_empty) { uint32_t i; uint32_t n = 0; POINT4D* points = lwalloc(g->ngeoms * sizeof(POINT4D)); int has_m = lwgeom_has_m((LWGEOM*) g); for (i = 0; i < g->ngeoms; i++) { LWGEOM* subg = lwcollection_getsubgeom((LWCOLLECTION*) g, i); if (!lwgeom_is_empty(subg)) { *input_empty = LW_FALSE; if (!getPoint4d_p(((LWPOINT*) subg)->point, 0, &points[n])) { lwerror("Geometric median: getPoint4d_p reported failure on point (POINT(%g %g %g %g), number %d of %d in input).", points[n].x, points[n].y, points[n].z, points[n].m, i, g->ngeoms); lwfree(points); return NULL; } if (has_m) { /* This limitation on non-negativity can be lifted * by replacing Weiszfeld algorithm with different one. * Possible option described in: * * Drezner, Zvi & O. Wesolowsky, George. (1991). * The Weber Problem On The Plane With Some Negative Weights. * INFOR. Information Systems and Operational Research. * 29. 10.1080/03155986.1991.11732158. */ if (points[n].m < 0) { lwerror("Geometric median input contains points with negative weights (POINT(%g %g %g %g), number %d of %d in input). Implementation can't guarantee global minimum convergence.", points[n].x, points[n].y, points[n].z, points[n].m, i, g->ngeoms); lwfree(points); return NULL; } /* points with zero weight are not going to affect calculation, drop them early */ if (points[n].m > DBL_EPSILON) n++; } else { points[n].m = 1.0; n++; } } } #if PARANOIA_LEVEL > 0 /* check Z=0 for 2D inputs*/ if (!lwgeom_has_z((LWGEOM*) g)) for (i = 0; i < n; i++) assert(points[i].z == 0); #endif *npoints = n; return points; } LWPOINT* lwmpoint_median(const LWMPOINT* g, double tol, uint32_t max_iter, char fail_if_not_converged) { /* m ordinate is considered weight, if defined */ uint32_t npoints = 0; /* we need to count this ourselves so we can exclude empties and weightless points */ uint32_t i; int input_empty = LW_TRUE; POINT3D median; POINT4D* points = lwmpoint_extract_points_4d(g, &npoints, &input_empty); /* input validation failed, error reported already */ if (points == NULL) return NULL; if (npoints == 0) { lwfree(points); if (input_empty) { return lwpoint_construct_empty(g->srid, 0, 0); } else { lwerror("Median failed to find non-empty input points with positive weight."); return NULL; } } median = init_guess(points, npoints); i = iterate_4d(&median, points, npoints, max_iter, tol); lwfree(points); if (fail_if_not_converged && i >= max_iter) { lwerror("Median failed to converge within %g after %d iterations.", tol, max_iter); return NULL; } if (lwgeom_has_z((LWGEOM*) g)) { return lwpoint_make3dz(g->srid, median.x, median.y, median.z); } else { return lwpoint_make2d(g->srid, median.x, median.y); } } LWPOINT* lwgeom_median(const LWGEOM* g, double tol, uint32_t max_iter, char fail_if_not_converged) { switch (g->type) { case POINTTYPE: return lwpoint_clone(lwgeom_as_lwpoint(g)); case MULTIPOINTTYPE: return lwmpoint_median(lwgeom_as_lwmpoint(g), tol, max_iter, fail_if_not_converged); default: lwerror("%s: Unsupported geometry type: %s", __func__, lwtype_name(g->type)); return NULL; } }