public static void smooth(PointList geometry, double maxWindowSize) {
        if (geometry.size() <= 2) {
            // geometry consists only of tower nodes, there are no pillar nodes to be smoothed in between
            return;
        }

        // calculate the distance between all points once here to avoid repeated calculation.
        // for n nodes there are always n-1 edges
        double[] distances = new double[geometry.size() - 1];
        for (int i = 0; i <= geometry.size() - 2; i++) {
            distances[i] = DistancePlaneProjection.DIST_PLANE.calcDist(
                    geometry.getLat(i), geometry.getLon(i),
                    geometry.getLat(i + 1), geometry.getLon(i + 1)
            );
        }


        // map that will collect all smoothed elevation values, size is less by 2
        // because elevation of start and end point (tower nodes) won't be touched
        IntDoubleHashMap averagedElevations = new IntDoubleHashMap((geometry.size() - 1) * 4 / 3);

        // iterate over every pillar node to smooth its elevation
        // first and last points are left out as they are tower nodes
        for (int i = 1; i <= geometry.size() - 2; i++) {
            // first, determine the average window which could be smaller when close to pillar nodes
            double searchDistance = maxWindowSize / 2.0;

            double searchDistanceBack = 0.0;
            for (int j = i - 1; j >= 0; j--) {
                searchDistanceBack += distances[j];
                if (searchDistanceBack > searchDistance) {
                    break;
                }
            }

            // update search distance if pillar node is close to START tower node
            searchDistance = Math.min(searchDistance, searchDistanceBack);

            double searchDistanceForward = 0.0;
            for (int j = i; j < geometry.size() - 1; j++) {
                searchDistanceForward += distances[j];
                if (searchDistanceForward > searchDistance) {
                    break;
                }
            }

            // update search distance if pillar node is close to END tower node
            searchDistance = Math.min(searchDistance, searchDistanceForward);

            if (searchDistance <= 0.0) {
                // there is nothing to smooth. this is an edge case where pillar nodes share exactly the same location
                // as a tower node.
                // by doing so we avoid (at least theoretically) a division by zero later in the function call
                continue;
            }

            // area under elevation curve
            double elevationArea = 0.0;

            // first going again backwards
            double distanceBack = 0.0;
            for (int j = i - 1; j >= 0; j--) {
                double dist = distances[j];
                double searchDistLeft = searchDistance - distanceBack;
                distanceBack += dist;
                if (searchDistLeft < dist) {
                    // node lies outside averaging window
                    double elevationDelta = geometry.getEle(j) - geometry.getEle(j + 1);
                    double elevationAtSearchDistance = geometry.getEle(j + 1) + searchDistLeft / dist * elevationDelta;
                    elevationArea += searchDistLeft * (geometry.getEle(j + 1) + elevationAtSearchDistance) / 2.0;
                    break;
                } else {
                    elevationArea += dist * (geometry.getEle(j + 1) + geometry.getEle(j)) / 2.0;
                }
            }

            // now going forward
            double distanceForward = 0.0;
            for (int j = i; j < geometry.size() - 1; j++) {
                double dist = distances[j];
                double searchDistLeft = searchDistance - distanceForward;
                distanceForward += dist;
                if (searchDistLeft < dist) {
                    double elevationDelta = geometry.getEle(j + 1) - geometry.getEle(j);
                    double elevationAtSearchDistance = geometry.getEle(j) + searchDistLeft / dist * elevationDelta;
                    elevationArea += searchDistLeft * (geometry.getEle(j) + elevationAtSearchDistance) / 2.0;
                    break;
                } else {
                    elevationArea += dist * (geometry.getEle(j + 1) + geometry.getEle(j)) / 2.0;
                }
            }

            double elevationAverage = elevationArea / (searchDistance * 2);
            averagedElevations.put(i, elevationAverage);
        }

        // after all pillar nodes got an averaged elevation, elevations are overwritten
        averagedElevations.forEach((Consumer<IntDoubleCursor>) c -> geometry.setElevation(c.key, c.value));
    }
@Test
    public void testManyPoints() {
        /**
         * used to cross validate this implementation as it was ported from an internal kotlin prototype
         */
        PointList pl = new PointList(27, true);
        pl.add(10.153564, 47.324976, 1209.5);
        pl.add(10.15365, 47.32499, 1209.3);
        pl.add(10.153465, 47.325058, 1213.6);
        pl.add(10.153382, 47.325062, 1213.5);
        pl.add(10.153283, 47.325048, 1213.5);
        pl.add(10.153049, 47.324956, 1213.5);
        pl.add(10.152899, 47.324949, 1213.6);
        pl.add(10.152795, 47.324968, 1213.7);
        pl.add(10.152706, 47.325044, 1213.7);
        pl.add(10.152466, 47.325041, 1215.2);
        pl.add(10.152283, 47.32508, 1215.4);
        pl.add(10.152216, 47.325074, 1215.5);
        pl.add(10.151649, 47.324849, 1216);
        pl.add(10.151502, 47.324824, 1216.9);
        pl.add(10.151212, 47.324708, 1218.1);
        pl.add(10.150862, 47.324493, 1219.7);
        pl.add(10.150729, 47.324491, 1220.4);
        pl.add(10.150714, 47.324514, 1220.6);
        pl.add(10.150767, 47.324605, 1226.5);
        pl.add(10.150989, 47.324943, 1236.4);
        pl.add(10.150996, 47.32502, 1236.3);
        pl.add(10.150964, 47.325038, 1236.3);
        pl.add(10.150528, 47.324928, 1237.2);
        pl.add(10.149945, 47.324733, 1239.3);
        pl.add(10.14989, 47.324736, 1249.9);
        pl.add(10.149504, 47.324455, 1248.6);
        pl.add(10.149392, 47.324333, 1248);

        EdgeElevationSmoothingMovingAverage.smooth(pl, 150.0);

        double[] expectedElevations = {
                1209.5, 1209.8259124400417, 1212.16778770315, 1212.4695940128302,
                1212.7073845131501, 1213.3531253136111, 1213.933051594191,
                1214.1484378838704, 1214.3274054744827, 1214.72713517562,
                1215.0590153809194, 1215.1879106460751, 1216.6557611915186,
                1217.0679077126047, 1218.343674121969, 1223.000531752824,
                1224.8734639760428, 1225.2508699507118, 1226.687883355977,
                1232.0752337564345, 1233.0834528364871, 1233.565160290987,
                1237.6276352913503, 1243.4122145535805, 1243.92486025017,
                1248.5623859735897, 1248.0
        };

        for (int i = 0; i < pl.size(); i++) {
            assertEquals(expectedElevations[i], pl.getEle(i), 0.0000001);
        }
    }