In my app, I draw a lot of "polyline" objects (French high voltage electric network) . it represents about 1 millions vertices.
In order to increase the perf, I have implemented an adaptative simplification before creating the Shape object to draw.
By adaptative, I mean that the level of simplification depends on the zoom : I specify an error in pixels, (for ex. 1 px max), then I transform this tolerance in real length (it depends on the zoom level) and then I simplify the geometries of the polylines, and at the end, I create Shape objects.
Therefore, the more I zoom, the more details remain : the drawing needs more vertices, but many objets are out of view so the perf are quite constant with the zoom.
In my case, the time spent in the simplification pass (classical Douglas Peucker) is less than the time saved during the drawing.
For example, the full draw with all vertices is about 500 ms, and with a tolerance of 1px, the drawing takes about 150 ms only, including the simplification pass.
I wonder if this kind of simplification could be done directly in Marlin, from a Shape object, before effectively drawing it.
There are faster algorithms than Douglas Peucker.
Very often, simplification algorithms don't respect topology (after simplification, a polygone may intersect himslef) : topology-proof algorithms exist but are very slow : JTS library proposes this kind of algoritm.
But in the case I speak about, the tolerance is very low (in pixels) so that the simplification is almost invisible : even if a polygon intersects himself, it is not visible.
Have you ever considered this kind of optimisation in Marlin Renderer ?
Marlin has already a simplifier for collinear segments.That would be a good idea to experiment an higher simplification but visual quality is important too.Could you provide a code sample or image outputs at least ?
==
GeoServer Professional Services from the experts! Visit http://goo.gl/it488V for more information.
==
Ing. Andrea Aime
@geowolf
Technical Lead
GeoSolutions S.A.S.
Via di Montramito 3/A
55054 Massarosa (LU)
phone: +39 0584 962313
fax: +39 0584 1660272
mob: +39 339 8844549
http://www.geo-solutions.it
http://twitter.com/geosolutions_it
AVVERTENZE AI SENSI DEL D.Lgs. 196/2003
Le informazioni contenute in questo messaggio di posta elettronica e/o nel/i file/s allegato/i sono da considerarsi strettamente riservate. Il loro utilizzo è consentito esclusivamente al destinatario del messaggio, per le finalità indicate nel messaggio stesso. Qualora riceviate questo messaggio senza esserne il destinatario, Vi preghiamo cortesemente di darcene notizia via e-mail e di procedere alla distruzione del messaggio stesso, cancellandolo dal Vostro sistema. Conservare il messaggio stesso, divulgarlo anche in parte, distribuirlo ad altri soggetti, copiarlo, od utilizzarlo per finalità diverse, costituisce comportamento contrario ai principi dettati dal D.Lgs. 196/2003.
The information in this message and/or attachments, is intended solely for the attention and use of the named addressee(s) and may be confidential or proprietary in nature or covered by the provisions of privacy act (Legislative Decree June, 30 2003, no.196 - Italy's New Data Protection Code).Any use not in accord with its purpose, any disclosure, reproduction, copying, distribution, or either dissemination, either whole or partial, is strictly forbidden except previous formal approval of the named addressee(s). If you are not the intended recipient, please contact immediately the sender by telephone, fax or e-mail and delete the information in this message that has been received in error. The sender does not give any warranty or accept liability as the content, accuracy or completeness of sent messages and accepts no responsibility for changes made after they were sent or for other risks which arise as a result of e-mail transmission, viruses, etc.
There are faster algorithms than Douglas Peucker.
public class DouglasPeucker {
static private final double K_DEG2RAD = PI / 180.0;
static private final double K_M_BY_DEG = 40_075_000 / (2*PI);
// TODO PERF : test bounding box de la geom, si sous la tolerance, conserver directement que les points extrémité !
// potentielle utile uniquement pour les fort zoom arriere o ules minuscule géométries ...
// TODO PERF : voir si la prise en compte des Z, via un facteur multiplicateur 2 ou 3 sur l'indice de parcours
// du tableau de coord flat n'est pas optimisable
// TODO PERF : renvoyer un AGeom spécial qui est une vue simplifiée du geom en entrée !
// -> petite indirection pour récupérer les coord, mais compensée par une plus faible
// gestion mémoire
// TODO PERF : plutot que de gérer une liste d'indice de point à conserver, qu'il faut ensuite trier
// essayer de gérer un bit array et le reparcourir à la fin pour constituer la liste d'indices
// sinon, tester avec le tri radix de FastUtil qui peut etre plus rapide
// Simplifie l'objet AGeom fourni
// Peut renvoyer l'objet geom lui-même si la simplification conserve tous les points
// Gère correctement les AGeomMulti : sépare les géométries, les simplifie et reconstitue le tout
// Fonctionne avec des geom 2D ou 3D
// mais ne simplifie que sur le critère de distance 2D (X et Y)
// bLongLatWGS84 indique que les coordonnées sont des longitude/latitude -> calcul de distance particulier
// sinon, l'algo considère que les coordonnées sont projetées et dans un référentiel orthonormé !
// bLongLatFastApprox permet, dans le cas ou bLongLatWGS84=true, d'accélérer le calcul de distance en faisant une approximation
public static AGeom douglasPeuckerReduction_global (
AGeom geom, double tolerance,
boolean bLongLatWGS84, boolean bLongLatFastApprox) {
int nbSousGeom = geom.getNbSousGeom();
if (nbSousGeom==1) {
// géometrie de type Single
I_DoubleArrayListRead vCoord = geom.getSousGeom(0).getCoordsFlat();
TIntArrayList indicesToKeep = douglasPeuckerReduction_global_indices(vCoord, tolerance, geom.hasZ(), bLongLatWGS84, bLongLatFastApprox);
return geom.extract(indicesToKeep);
}
// plusieurs sous-géométries : on les traite une par une, indépendamment
ArrayList<AGeom> listGeom = new ArrayList<>(nbSousGeom);
for (int i=0 ; i<nbSousGeom ; i++) {
AGeomSingle ssGeom = geom.getSousGeom(i);
I_DoubleArrayListRead vCoord = ssGeom.getCoordsFlat();
TIntArrayList indicesToKeep = douglasPeuckerReduction_global_indices(vCoord, tolerance, geom.hasZ(), bLongLatWGS84, bLongLatFastApprox);
listGeom.add(ssGeom.extract(indicesToKeep));
}
AGeom res = AGeom.createGeomMulti(listGeom);
return res;
}
// Peut renvoyer vCoordFlat lui-même si la simplification conserve tous les points
// bHasZ true -> les coordonnées sont des triplets, mais le traitement reste sur les x,y !
public static MyTDoubleArrayList douglasPeuckerReduction_global (
MyTDoubleArrayList vCoordFlat, double tolerance,
boolean bHasZ, boolean bLongLatWGS84, boolean bLongLatFastApprox) {
TIntArrayList indicesToKeep = douglasPeuckerReduction_global_indices(vCoordFlat, tolerance, bHasZ, bLongLatWGS84, bLongLatFastApprox);
if (indicesToKeep==null) {
// on conserve tous les points
return vCoordFlat;
}
// A partir des points conservés par l'algo, on reconstitue une liste de points
MyTDoubleArrayList vCoordResult = new MyTDoubleArrayList(indicesToKeep.size()*2);
int nbPointToKeep = indicesToKeep.size();
if (bHasZ) {
for (int i=0 ; i<nbPointToKeep ; i++) {
int index = 3*indicesToKeep.get(i);
vCoordResult.add(vCoordFlat.getQuick(index));
vCoordResult.add(vCoordFlat.getQuick(index+1));
vCoordResult.add(vCoordFlat.getQuick(index+2));
}
} else {
for (int i=0 ; i<nbPointToKeep ; i++) {
int index = 2*indicesToKeep.get(i);
vCoordResult.add(vCoordFlat.getQuick(index));
vCoordResult.add(vCoordFlat.getQuick(index+1));
}
}
return vCoordResult;
}
// Fonctionne avec des coord flat 2D ou 3D, dans ce cas, mettre bHasZ = TRUE
// Renvoie les indices (0 based) des points à conserver
// renvoie null s'il faut conserver tous les points
// les indices renvoyés sont triés
public static TIntArrayList douglasPeuckerReduction_global_indices (
I_DoubleArrayListRead vCoordFlat, double tolerance,
boolean bHasZ, boolean bLongLatWGS84, boolean bLongLatFastApprox) {
int nbPoint = bHasZ ? vCoordFlat.size()/3 : vCoordFlat.size()/2;
if (nbPoint <= 2) {
return null;
}
// OPTIMISATION
if (bLongLatWGS84 && bLongLatFastApprox) {
// mega approx : on convertit en pseudo metres en suposant que les données sont tres localisées pour permettre cette approx !
// cette approx ne fonctionne en l'état que s'il n'y a pas plus de Pi d'écart entre les min/max des angles
// notamment, si certain angle sont à 1° et d'aurte à 359°, le calcul n'a plus de sens
// -> il faudrait alors convertir le 359° en -1°
// si hasZ :
// comme on crée une list temp, on ne met que les x,y dedans, pas besoin de mettre un z inutile ...
int nbPt = bHasZ ? vCoordFlat.size()/3 : vCoordFlat.size()/2;
MyTDoubleArrayList vCoordLL = new MyTDoubleArrayList(nbPt*2);
double lg_rad_min = Double.POSITIVE_INFINITY;
double lg_rad_max = Double.NEGATIVE_INFINITY;
double lt_rad_min = Double.POSITIVE_INFINITY;
double lt_rad_max = Double.NEGATIVE_INFINITY;
int multIndex = bHasZ ? 3 : 2;
for (int i=0 ; i<nbPt ; i++) {
int index = multIndex*i;
double lg_rad = vCoordFlat.getQuick(index++) * K_DEG2RAD;
double lt_rad = vCoordFlat.getQuick(index) * K_DEG2RAD;
if (lg_rad > lg_rad_max) lg_rad_max = lg_rad;
if (lg_rad < lg_rad_min) lg_rad_min = lg_rad;
if (lt_rad > lt_rad_max) lt_rad_max = lt_rad;
if (lt_rad < lt_rad_min) lt_rad_min = lt_rad;
double x = lg_rad * K_M_BY_DEG * Math.cos(lt_rad);
double y = lt_rad * K_M_BY_DEG;
vCoordLL.add(x);
vCoordLL.add(y);
}
// check ecart des angles
if (Math.abs(lg_rad_max-lg_rad_min) > PI) {
throw new MyException("Le mode LongLatFastApprox n'est pas possible car l'écart entre la longitude min et max est > 180° : min = " + (lg_rad_min/K_DEG2RAD) + "° max = " + (lg_rad_max/K_DEG2RAD) + "°");
}
if (Math.abs(lt_rad_max-lt_rad_min) > PI) {
throw new MyException("Le mode LongLatFastApprox n'est pas possible car l'écart entre la latitude min et max est > 180° : min = " + (lt_rad_min/K_DEG2RAD) + "° max = " + (lt_rad_max/K_DEG2RAD) + "°");
}
// remplacement des variables ...
vCoordFlat = vCoordLL;
bLongLatWGS84 = false;
bHasZ = false;
}
// sinon soit on est en mètre, soit on est en Lg/Lt mais on ne veut pas l'approximation
int firstPoint = 0;
int lastPoint = nbPoint-1;
TIntArrayList vIndexPointToKeep = new TIntArrayList(nbPoint);
//vIndexPointToKeep.clear();
// On ajoute le 1er et le dernier point
vIndexPointToKeep.add (firstPoint);
vIndexPointToKeep.add (lastPoint);
douglasPeuckerReduction_recurs (vCoordFlat, firstPoint, lastPoint, tolerance, bHasZ, bLongLatWGS84, vIndexPointToKeep);
if (vIndexPointToKeep.size() == nbPoint) {
// aucune simplif réalisée !
return null;
}
// Attention l'appel précédent renvoi un vecteur d'index non trié !
vIndexPointToKeep.sort();
return vIndexPointToKeep;
}
public static void douglasPeuckerReduction_recurs (
I_DoubleArrayListRead vCoord, int firstPoint, int lastPoint, double tolerance, boolean bHasZ, boolean bLongLatWGS84,
TIntArrayList vIndexPointToKeep) {
double distMax_sq = 0;
int indexFarthest = 0;
// Fin de la récursion
if (firstPoint>=lastPoint-1) {
return;
}
// Le 1er et le dernier point ne peuvent être identiques
while (arePointEqual(vCoord, bHasZ, firstPoint, lastPoint)) {
lastPoint--;
vIndexPointToKeep.add (lastPoint);
if (firstPoint>=lastPoint-1) {
return;
}
}
int multIndex = bHasZ ? 3 : 2;
// Recherche de l'index du point le plus loin du segment [firstPoint, lastPoint]
double xA, yA, xB, yB, xI, yI;
xA = vCoord.getQuick(firstPoint*multIndex);
yA = vCoord.getQuick(firstPoint*multIndex+1);
xB = vCoord.getQuick(lastPoint*multIndex);
yB = vCoord.getQuick(lastPoint*multIndex+1);
for (int i=firstPoint+1 ; i<lastPoint ; i++) {
xI = vCoord.getQuick(i*multIndex);
yI = vCoord.getQuick(i*multIndex+1);
double dist_sq;
if (bLongLatWGS84) {
dist_sq = UtilsGeom.getDistPointSegWGS84(xI,yI, xA,yA, xB,yB);
dist_sq = dist_sq*dist_sq;
} else {
dist_sq = UtilsGeom.getDistPointSegSq(xI,yI, xA,yA, xB,yB);
}
if (dist_sq>distMax_sq) {
distMax_sq = dist_sq;
indexFarthest = i;
}
}
// Si ce point le plus loin dépasse la tolérence, on le conserve et on appelle l'algo sur les 2 bouts de lignes autour.
if (distMax_sq>(tolerance*tolerance) && indexFarthest!=0) {
vIndexPointToKeep.add(indexFarthest);
douglasPeuckerReduction_recurs (vCoord, firstPoint, indexFarthest, tolerance, bHasZ, bLongLatWGS84, vIndexPointToKeep);
douglasPeuckerReduction_recurs (vCoord, indexFarthest, lastPoint, tolerance, bHasZ, bLongLatWGS84, vIndexPointToKeep);
}
}
private static boolean arePointEqual(I_DoubleArrayListRead vCoord, boolean bHasZ, int index1, int index2) {
int multIndex = bHasZ ? 3 : 2;
double x1 = vCoord.getQuick(index1*multIndex);
double y1 = vCoord.getQuick(index1*multIndex+1);
double x2 = vCoord.getQuick(index2*multIndex);
double y2 = vCoord.getQuick(index2*multIndex+1);
return UtilsData.equals_double_notNull(x1, x2) && UtilsData.equals_double_notNull(y1, y2);
}
}
public static double getDistPointSegSq(double xP, double yP,
double xA, double yA,
double xB, double yB) {
double dx = xB - xA;
double dy = yB - yA;
if (dx != 0 || dy != 0) {
double t = ((xP - xA) * dx + (yP - yA) * dy) / (dx * dx + dy * dy);
if (t > 1) {
xA = xB;
yA = yB;
} else if (t > 0) {
xA += dx * t;
yA += dy * t;
}
}
dx = xP - xA;
dy = yP - yA;
return dx * dx + dy * dy;
}
public static double getDistPointSegWGS84(double xP, double yP,
double xA, double yA,
double xB, double yB) {
double hP = 0;
double lata = Math.toRadians(yA);
double lnga = Math.toRadians(xA);
double latb = Math.toRadians(yB);
double lngb = Math.toRadians(xB);
double latp = Math.toRadians(yP);
double lngp = Math.toRadians(xP);
double sinlata = Math.sin(lata);
double coslata = Math.cos(lata);
double sinlnga = Math.sin(lnga);
double coslnga = Math.cos(lnga);
double sinlatb = Math.sin(latb);
double coslatb = Math.cos(latb);
double sinlngb = Math.sin(lngb);
double coslngb = Math.cos(lngb);
double sinlatp = Math.sin(latp);
double coslatp = Math.cos(latp);
double sinlngp = Math.sin(lngp);
double coslngp = Math.cos(lngp);
double costh = sinlata*sinlatb
+ coslata*coslatb*(coslnga*coslngb+sinlnga*sinlngb);
double sin2th = 1-costh*costh;
if (sin2th < 1.0E-8) {
// a and b are very close; return distance from a to p
double costhp = sinlata*sinlatp
+ coslata*coslatp*(coslnga*coslngp+sinlnga*sinlngp);
return FastMath.acos(costhp)*(R+hP);
}
double num = sinlata*(coslatb*coslatp*coslngb*sinlngp
- coslatb*coslatp*sinlngb*coslngp)
+ coslata*coslnga*(coslatb*sinlatp*sinlngb
- sinlatb*coslatp*sinlngp)
+ coslata*sinlnga*(sinlatb*coslatp*coslngp
- coslatb*sinlatp*coslngb);
double sinr = Math.abs(num)/Math.sqrt(sin2th);
return (R+hP)*FastMath.asin(sinr);
}
--
You received this message because you are subscribed to the Google Groups "marlin-renderer" group.
To unsubscribe from this group and stop receiving emails from it, send an email to marlin-renderer+unsubscribe@googlegroups.com.
To post to this group, send email to marlin-renderer@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Regards,
Andrea Aime
Hi Olivier,I see you are keeping all the data in memory, and it's high resolution... that's not a use case we have in GeoServer,where normally tens (to hundred of thousands) of layers need to be served (and memory comes at a premium,as it needs to be split among many concurrent requests all allocating drawing surfaces in memory).So typically the datasets are resolution dependent, one high resolution as yours would display only when fairlyzoomed in, and have some smaller replacement at lower scales.As a result, the simplification does not manage to remove as many points, topically it's 1/10 or 1/100.That does not mean memory caching is banned from GeoServer, but nobody has come forth with some fundingto implement it as an optional behavior :-)CheersAndrea
On Tue, Nov 14, 2017 at 1:45 PM, Olivier Brault <o.br...@gmail.com> wrote:
I've made a quick comparison : with and without DP simplification
I've captured 2 images, exactly in the same position and zoom, so that it is easy, in the Windows Photo viewer, to swap quickly between 2 pictures and see the differences.
There are indeed small and subtle visible differences when swapping like this, but it is almost impossible, IMHO, to tell which image is simplified when seing it without this "swap comparison".
(for information, these images represent the high voltage french electric network)
The first image is drawn with all the vertices (more than 1,1 million) : it takes 520 ms to draw, on average.
the snd image is drawn with 0.75 pixel tolerance DP simplification : only 13% of the vertices are drawn, it takes 160 ms to draw, on average.
Olivier
--
You received this message because you are subscribed to the Google Groups "marlin-renderer" group.
To unsubscribe from this group and stop receiving emails from it, send an email to marlin-render...@googlegroups.com.
To post to this group, send email to marlin-...@googlegroups.com.
--
Hi,
I'd also like to contribute my two cents to this discussion. Although I am also a big fan of
geometry simplifications in order to increase performance I still want to throw in a few
words of caution.
I once had to display some geometry like this:
https://www.sky-map.de/images/ipgafor.png
The original data was very fine-grained so for efficient rendering some simplification was
necessary. The problem though was that the region outlines were modeled as polygons
and not as connected line strings. So many polygon sections were redundant. When you
now start to automatically simplify these polygons most algorithms end up with different
solutions for previously identical sections and this really looks ugly. This depends of course
on how aggressive you simplify and which algorithm you use.
So my personal conclusion is that the introduction of some automatic simplification will at
least need some mechanism to activate or deactivate it. Maybe something like a rendering hint.
In fact, I use aggressive DP because I know that even with topology error, it will be almost invisible : I don't make any computation or geometric algorithm on simplified data so I don't care of topology errors.
So I agree to let the user to desactivate or to tune finely the tolerance.
I think that even with topology proof alogorithm, there will still be problems with adjacent polygons.
Indeed, these algorithms guarantee you don't have self intersecting shape : but they consider shapes independantly.
In the past, for another project, I implemented an algorithm of simplification that garanteed that so intersection at all where created during simplification.
But even with that, adjacent polygon may still have small holes between them ...
The only real solution is to have a topology description of the géometries, like in TopoJSON format.
So : here the pragmatic solution is to set a very low tolerance to not bother about all this.
Laurent : you solution with simple radial dist threshold could be insufficient in case of many long straight lines.
But as you already have a colinear simplification, the combination of both can be efficient, I think : is there a tunable tolerance of the angle ?
-> for information, can you print the stats of the simplification ratio made by Marlin ? (like total number of vertices before and after, and then the % of reduction) ?
As a matter of fact, when you are fed by a tool (like Geoserver) which already simplify geometries, you may have almost no gain.
--> I will try your algo with my full res data, and I am sure I will see improvements.
for info, I tried 0.125 px tolerance and have still only 16% of vertices to draw (200 ms compared to >500ms without simplification)
I also will make tests on a better computer too : the one I use everyday is a banal laptop with poor graphics capabilities.
Maybe on a powerful computer, the drawing time will be reduce and then the need of simplification less usefull, I don't know
I plan to rewrite the collinear simplifier to be faster and I will add tuning parameters as I would like to use the area error instead.
Let me insist: please give me your Marlin internal statistics (doStats=true/false) to measure the gains offered by your D&P simplifier.