Есть ли способ уменьшить количество координат в сложном замкнутом SVG-пути? - PullRequest
0 голосов
/ 11 июля 2019

Что я хотел бы сделать, это взять форму SVG, нарисованную закрытым путем (в данном случае, областью карты), и уменьшить количество точек, чтобы создать более простую форму.

IЯ пытался реализовать алгоритм Рамера-Дугласа-Пекера, чтобы уменьшить количество баллов.Например, вот скрипка с использованием библиотеки simpify.js:

https://jsfiddle.net/0t3n8762/

После прочтения этой проблемы, если я правильно понял, кажется, что алгоритм на самом деле не предназначен дляработать над закрытыми формами, но с открытыми контурами.Я попытался разделить каждый путь на две (так что есть две линии, которые вместе составляют всю форму) и запустил алгоритм для каждой из них перед их рекомбинацией, хотя результаты кажутся практически идентичными:

https://jsfiddle.net/caqwL3t7/

Может быть (и это вполне вероятно), что я просто не понимаю, как должен работать алгоритм, и неправильно его реализую.Или, может быть, мне следует попробовать совершенно другой метод.

coords = pathToCoords($("#JP-01").attr("d"));

for (var i = 0; i < coords.length; i++) {
 newCoords[i] = simplify(coords[i], 2);
}

newPath = coordsToPath(newCoords);

$("#JP-01").attr("d", newPath);

Я бы хотел создать более простой путь, который по-прежнему сохраняет общую форму оригинала, нарисованного с меньшим количеством точек.Фактический результат - искаженная форма, которая имеет мало общего с оригиналом.

1 Ответ

0 голосов
/ 12 июля 2019

Как отметил Павел в комментариях, вы не рассматривали команды l и v в пути.

Я сделал фрагмент, чтобы показать, как достичь вашей цели, но он не будет работать во всех случаях (я полагаю), и его все еще нужно улучшить.

Пожалуйста, найдите фрагмент и комментарии ниже.

$("#simplify-1").on("click", function(){
    var coords;
    var newCoords = [];
    var newPath;
    var coordsObject;

    // get the coordinates from a given path
    function pathToCoords(path) {
      // save each path individually (.i.e islands are stored separately)
      var data = path.match(/(?<=m).*?(?=z)/igs);
      var coordsArray = [];
      var objectArray = [];
      var objectArrayA = [];
      var objectArrayB = [];
      var objectContainer = [];

      // split each pair of coordinates into their own arrays
      for (var i = 0; i < data.length; i++) {
        // should remove anything between h or v and l instead?
        data[i] = data[i].split(/[LlHhVv]/);
        coordsArray[i] = [];

        for (var j = 0; j < data[i].length; j++) {
          coordsArray[i].push(data[i][j].split(",").map(Number));
        }
      }

      // convert each pair of coordinates into an object of x and y
      for (var i = 0; i < coordsArray.length; i++) {
        objectArray[i] = [];

        for (var j = 0; j < coordsArray[i].length; j++) {
          objectArray[i].push({
            x: coordsArray[i][j][0],
            y: coordsArray[i][j][1]
          });
        }

        // split each array of coordinates in half
        var halfway = Math.floor(objectArray[i].length / 2);

        objectArrayB[i] = JSON.parse(JSON.stringify(objectArray[i]));;
        objectArrayA[i] = objectArrayB[i].splice(0, halfway);
      }

      objectContainer = [objectArrayA, objectArrayB];

      return objectContainer;
    }

    // convert the coordinates back into a string for the path
    function coordsToPath(objectContainer) {
      var objectArray = [];
      var coordsArray = [];
      var data;

      // recombine the two objectArrays
      for (var i = 0; i < objectContainer[0].length; i++) {
        objectArray[i] = objectContainer[0][i].concat(objectContainer[1][i])
      }

      for (var i = 0; i < objectArray.length; i++) {
        coordsArray[i] = [];

        // take the X and Y values from the objectArray and strip the unwanted information
        for (var j = 0; j < objectArray[i].length; j++) {
          if (j == 0) {
            // add 'M' in front of the first entry
            coordsArray[i].push("M" + Object.values(objectArray[i][j]));
          } else if (j == objectArray[i].length - 1) {
            // add 'z' to the end of the last entry
            coordsArray[i].push("l" + Object.values(objectArray[i][j]) + "z");
          } else {
            // add 'l' in front of each coordinate pair
            coordsArray[i].push("l" + Object.values(objectArray[i][j]));
          }
        }

        coordsArray[i] = coordsArray[i].toString();
      }

      // put everything back into a single valid SVG path string
      data = coordsArray.join("");

      return data;
    }

    // -- simplify.js -- //

    /*
     (c) 2017, Vladimir Agafonkin
     Simplify.js, a high-performance JS polyline simplification library
     mourner.github.io/simplify-js
    */

    (function() {
      'use strict';

      // to suit your point format, run search/replace for '.x' and '.y';
      // for 3D version, see 3d branch (configurability would draw significant performance overhead)

      // square distance between 2 points
      function getSqDist(p1, p2) {

        var dx = p1.x - p2.x,
          dy = p1.y - p2.y;

        return dx * dx + dy * dy;
      }

      // square distance from a point to a segment
      function getSqSegDist(p, p1, p2) {

        var x = p1.x,
          y = p1.y,
          dx = p2.x - x,
          dy = p2.y - y;

        if (dx !== 0 || dy !== 0) {

          var t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy);

          if (t > 1) {
            x = p2.x;
            y = p2.y;

          } else if (t > 0) {
            x += dx * t;
            y += dy * t;
          }
        }

        dx = p.x - x;
        dy = p.y - y;

        return dx * dx + dy * dy;
      }
      // rest of the code doesn't care about point format

      // basic distance-based simplification
      function simplifyRadialDist(points, sqTolerance) {

        var prevPoint = points[0],
          newPoints = [prevPoint],
          point;

        for (var i = 1, len = points.length; i < len; i++) {
          point = points[i];

          if (getSqDist(point, prevPoint) > sqTolerance) {
            newPoints.push(point);
            prevPoint = point;
          }
        }

        if (prevPoint !== point) newPoints.push(point);

        return newPoints;
      }

      function simplifyDPStep(points, first, last, sqTolerance, simplified) {
        var maxSqDist = sqTolerance,
          index;

        for (var i = first + 1; i < last; i++) {
          var sqDist = getSqSegDist(points[i], points[first], points[last]);

          if (sqDist > maxSqDist) {
            index = i;
            maxSqDist = sqDist;
          }
        }

        if (maxSqDist > sqTolerance) {
          if (index - first > 1) simplifyDPStep(points, first, index, sqTolerance, simplified);
          simplified.push(points[index]);
          if (last - index > 1) simplifyDPStep(points, index, last, sqTolerance, simplified);
        }
      }

      // simplification using Ramer-Douglas-Peucker algorithm
      function simplifyDouglasPeucker(points, sqTolerance) {
        var last = points.length - 1;

        var simplified = [points[0]];
        simplifyDPStep(points, 0, last, sqTolerance, simplified);
        simplified.push(points[last]);

        return simplified;
      }

      // both algorithms combined for awesome performance
      function simplify(points, tolerance, highestQuality) {

        if (points.length <= 2) return points;

        var sqTolerance = tolerance !== undefined ? tolerance * tolerance : 1;

        points = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
        points = simplifyDouglasPeucker(points, sqTolerance);

        return points;
      }

      // export as AMD module / Node module / browser or worker variable
      if (typeof define === 'function' && define.amd) define(function() {
        return simplify;
      });
      else if (typeof module !== 'undefined') {
        module.exports = simplify;
        module.exports.default = simplify;
      } else if (typeof self !== 'undefined') self.simplify = simplify;
      else window.simplify = simplify;

    })();

    // -- end simplify.js -- //

    coords = pathToCoords($("#OriginalJP-01").attr("d"));

    for (var i = 0; i < coords.length; i++) {
      newCoords[i] = [];
      for (var j = 0; j < coords[i].length; j++) {
        newCoords[i][j] = simplify(coords[i][j], 1);
      }
    }

    newPath = coordsToPath(newCoords);
    $("#JP-01").attr("d", newPath);
});

$("#simplify-2").on("click", function(){
    let d = $("#OriginalJP-01").attr("d");
    let coordsArray = [];
    let data = d.match(/(?<=m).*?(?=z)/igs);
    // split each pair of coordinates into the array as an object {x, y}
    for (var i = 0; i < data.length; i++) {
        let ca = coordsArray[i] = [];
        // split data[i] into each coordinate text
        let matches = data[i].match(/((\w?-?\d+(\.\d+)?)(,-?\d+(\.\d+)?)?)/g);
        for(let j=0;j<matches.length;j++){
            let x, y,
                text = matches[j],
                // split with comma and convert it to a number
                temp = text.split(",").map(v=>+v.replace(/^[^\-\d]/g,""));
            switch(text[0]){
                default:
                case "L": // absolute
                    x = temp[0];
                    y = temp[1];
                break;

                case "l": // relative
                    x = ca[j-1].x + temp[0];
                    y = ca[j-1].y + temp[1];
                break;

                case "V": // absolute
                    x = ca[j-1].x;
                    y = temp[0];
                break;

                case "v": // relative
                    x = ca[j-1].x;
                    y = ca[j-1].y + temp[0];
                break;

                case "H": // absolute
                    x = temp[0];
                    y = ca[j-1].y;
                break;

                case "h": // relative
                    x = ca[j-1].x + temp[0];
                    y = ca[j-1].y;
                break;
            }
            x = +x.toFixed(2);
            y = +y.toFixed(2);
            ca.push({x, y});
        }
    }

    let mArray = [];
    // calculate the slopes
    for(let i=0;i<coordsArray.length;i++){
        mArray[i] = [];
        for(let j=0;j<coordsArray[i].length-1;j++){
            let {x, y} = coordsArray[i][j], // current point's x and y
                {x: nx, y: ny} = coordsArray[i][j+1], // next point's x and y
                dy = (ny - y);

            if(dy === 0) // to check if the denominator is legal or not
                // in your case, it would not enter here
                mArray[i].push(Infinity);
            else
                mArray[i].push((nx - x) / dy);
        }
    }
    let abandonFactor = +$("#abandonFactor").val();
    let newCoordsArray = [];
    for(let i=0;i<mArray.length;i++){
        let na = newCoordsArray[i] = [];
        // calculate the abandonRate base on the amount of the original points
        let abandonRate = coordsArray[i].length * abandonFactor;
        for(let j=0;j<mArray[i].length-1;j++){
            let m = mArray[i][j], // this slope
                nm = mArray[i][j+1]; // next slope
            let diffRate = Math.abs((m - nm) / m); // calculate the changes of the slope

            // check if the diffRate is greater than abandonRate
            // or the sign of m not equals the sign of nm
            // you can try out removing the "sign check part" and see what would happen ;)
            if(diffRate >= abandonRate || (Math.sign(m) !== Math.sign(nm))){
                na.push(coordsArray[i][j]);
            }
        }
    }
    
    let newPath = [];
    // create new path
    for(let i=0;i<newCoordsArray.length;i++){
        let temp = [];
        for(let j=0;j<newCoordsArray[i].length;j++){
            let {x, y} = newCoordsArray[i][j];
            let p = `${x},${y}`;
            temp.push(p);
        }
        newPath.push("M" + temp.join("L") + "z");
    }

    $("#JP-01").attr("d", newPath.join(""));
}).click();

$("#abandonFactor").on("change", function(){
    $("#abandonFactor_text").text(`Abandon factor: ${this.value}`);
    $("#simplify-2").click();
});
div {
  width: 50%;
}

.original {
  float: left;
}

.simplified {
  float: right;
}

.map {
  box-sizing: border-box;
  width: 100%;
  padding: 0.5rem;
  text-shadow: 1px 1px white;
  cursor: grab;
}

.land {
  fill: lightgreen;
  fill-opacity: 1;
  stroke: white;
  stroke-opacity: 1;
  stroke-width: 0.5;
  cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="original">
  <svg id="OriginalMap" viewBox="324.55999755859375 0 126.44989013671875 111.65999603271484" class="map" xmlns="http://www.w3.org/2000/svg" xmlns:amcharts="http://amcharts.com/ammap">
    <g>
      <path id="OriginalJP-01" title="Hokkaido" class="land" d="
      M344.04,71.29l2.71,-2.13l0.04,-0.59l-1.96,-3.17l-1.38,-0.99l-0.7,-1.16l0.68,-2.54l-0.27,-0.48l1.64,0.11l0.6,-0.42l0.21,-0.54l0.53,-0.21l2.75,2.66l1.93,0.87l1.13,1.28l2.48,-0.47l0.17,-0.42l1.02,-0.32l0.72,0.02l-0.14,1.17l3.04,1.39l2.2,-1.22l1.97,-2.15l0.99,-1.6l0.1,-2.35l-1.38,-2.76l0.54,-1.94L363,51.89l-0.45,-2.27l1.02,-2.1l1.47,-0.89l0.76,-0.05l2.03,-1.32l0.93,-1.89l0.41,-1.95l-0.35,-7.86l0.5,-0.32l1.64,-3.44l0.21,-2.86l0.38,-0.79l0.13,-1.6l-0.3,-3.27l-0.77,-3.84l-3.04,-7.32l-0.31,-1.16l0.07,-0.99l0.86,-1.27l0.79,-1.95l-0.41,-1.4l0.15,-1.28l0.75,0.77l0.13,0.52l0.39,0.16l1.76,-0.28l1.35,-0.94l0.55,-1.99L374.52,0l0.47,0.33l0.56,1.29l0.59,0.34l0.63,1.47l2.32,1.7l2.21,3.12l4.95,5.56l3.91,7.23l2.58,2.55l2.44,3.1l5.58,4.4l1.57,0.89l0.33,0.95l1.26,0.96l3.7,1.94l3.24,1.28l5.88,1.69l2.79,0.53l0.44,-0.31l0.29,0.77l-0.06,1.28l0.87,1.55l0.58,0.48l3.21,1.01l3.41,0.21l2.57,-0.32l2.28,-1.99l1.54,-1.92l2.18,-2.04l1.83,-1.26l0.66,-1.45l1.1,-1.16l0.78,-1.38l0.46,0.03l0.45,2.23l-1.21,2.49l-0.91,0.92l-0.6,2.4l-1.73,2.13l-0.94,2.12l0.1,0.83L436.06,48l0.22,1.06l1.35,2.58l1.49,1.97l1.52,5.23l1.49,1.96l-1.16,-0.53l-0.19,-0.53l-1.14,-0.22l-0.24,0.37l0.21,1.26l0.86,-0.67l0.47,0.4l-0.29,0.55l0.37,0.39l0.4,-0.28l2.55,0.62l2.09,-1.7l0.32,-0.6l1.11,-0.71l0.24,-0.5l3.28,0.2l-0.19,0.5l-1.17,0.91l-1.73,0.31l-1.87,1.03l-0.8,2.93l-0.77,-0.36l-1.24,-0.05l-2.71,0.51l-1.51,0.9l-1.26,-0.21l-0.62,0.6l-0.14,1.15l-1.6,2.15l-1.02,0.49l-1.9,-0.1l-0.9,-0.79l0.03,-0.53l0.33,-0.31l-0.55,-0.37l-0.73,0.1l-1.19,1.73l-0.02,0.41l1.15,1.13l-2.01,-0.12l-1.15,-0.35l-3.58,-0.02l-0.69,-0.15l-1.58,-1.27l-3.06,0.54l-2.43,1.18l-3.4,2.53l-5.27,5.27l-4.08,5.8l-1.53,3.31l-0.28,1.67l0.55,1.44l-0.82,3.88l-0.91,1.53l0,1.61l-3.7,-3.72l-4.85,-2.02l-8.09,-4.45l-2.38,-1.67l-1.84,-2.05l-3.21,-0.94l-2.21,-2.05l-1.82,-1.2l-1.83,-0.53l-2.38,-0.04l-3.17,1.04l-2.08,1.29l-2.68,2.28l-1.59,0.89l-2.12,2.45l-0.34,0.75l-1.07,-0.4l-0.33,-0.55l0.38,-0.29l0.59,0.64l0.12,-0.81l-1.35,-0.41l-0.87,-2.37l-0.79,-0.4l-1.07,-1.12l-0.12,-0.58l-1.27,-1.23l-1.24,-0.09l-1.25,0.53l-0.97,-0.59l-1.21,0.03l-1.68,1.81l-1.44,2.75l-0.71,2.55l0.28,1.94l2.32,1.2l2.74,2.35l0.75,0.14l1.37,-0.53l2.04,0.31l1.23,2.21l1.63,1.22l1.08,1.88l3.15,1.36l0.55,0.89l0.79,0.59l-0.61,0.61l-0.95,0.01l-1.1,1.3l-1.66,0.66l-0.86,-0.79l-2.78,-0.91l-0.93,0.16l-0.28,0.62l-0.41,0.1l-0.18,-0.61l0.68,-0.45l-0.2,-0.67l-0.59,-0.37l-0.86,0.08l-0.41,0.33l-0.43,1.68l-1.44,1.06l-1.29,0.1l-0.35,0.33l-0.31,3.94l-0.4,0.48l-0.94,0.03l-1.96,0.87l-0.9,1.89l-0.39,0.32l-1.02,-0.8l-1.14,0.22l-0.9,-0.71l-1.08,-2.72l-0.09,-0.86l0.41,-1.77l1.35,-3.03v-1.05l0.77,0.08l0.29,-0.59l0.31,-2.7l-0.4,-1.98l-1.9,-2.94l-0.67,-0.36l-1.3,-0.12L334.29,91l-0.85,-0.88l-1.14,-0.41l-0.39,-0.73l-0.19,-1.39l0.31,-1.12l0.94,-1.03l0.33,-0.95l0.03,-2.65l-0.49,-2.23l0.92,-1.45l1.12,-0.62l2.28,-0.06l0.61,-0.91l1.39,-0.73l0.85,-1.92l0.82,0.57l0.56,0.97l0.89,-0.23l0.09,-1.39l0.82,-0.84L344.04,71.29z
      M358.76,9.86l-0.09,-1.23l1.28,-1.25l2.07,1.21l0.51,1.81l-1.81,1.49l-1.37,-1.07L358.76,9.86z
      M326.88,91.03l-0.29,2.22l-0.39,0.63l-0.65,0.23l-0.02,0.36l-0.61,-0.54l0.06,-1.71l-0.42,-1.11l0.74,-1.09l1.91,-0.38l0.46,-0.52l0.03,0.64L326.88,91.03z
      M357.23,4.25l-0.26,2.59l-0.39,0.13L355.7,4l0.22,-1.31l-0.61,-0.34l0.09,-0.73l0.65,0.85l0.66,-0.15l-0.02,-0.59L357.03,2l0.35,0.78L357.23,4.25z"/>
    </g>
  </svg> Original
</div>

<div class="simplified">
  <svg id="Map" viewBox="324.55999755859375 0 126.44989013671875 111.65999603271484" class="map" xmlns="http://www.w3.org/2000/svg" xmlns:amcharts="http://amcharts.com/ammap">
    <g>
      <path id="JP-01" title="Hokkaido" class="land" d=""/>
    </g>
  </svg> Simplified
  <button id="simplify-1">Your method</button>
  <button id="simplify-2">New method</button>
  <input type="range" min="0.0001" max="0.1" value="0.01" step="0.0001" class="slider" id="abandonFactor">
  <div id="abandonFactor_text">Abandon factor: 0.01</div>
</div>
...