2016-04-30

Display KML, KMZ file on Google Maps using geoXML3?


Mình xem lại cái project dùng geoXML3 hiển thị KML, KMZ. Nhưng lần này file nặng hơn nên nó chết hoài. Nói chung dùng geoXML3 để parse và hiển thị KML file lên Google Maps có một số vấn đề "ngu ngu" cần note lại:

1. File KML quá nặng ví dụ load Administrative boundary cho tỉnh/thành phố lấy từ shapefile tại
Global Administrative Areas. Chỉ cần load các tỉnh miền Nam thì Firefox cũng die ra cái dialog Debug script, Stop script.
2. Parse của geoXML3 chết do xử lý quá nặng.

Giải quyết từng vấn đề:

1a. Load từng phần (split a layer by multi documents)

Load theo từng region (miền Nam, Nam Trung Bộ + Tây Nguyên, Bắc Trung Bộ, miền Bắc), ko load toàn bộ (layer) boundary các tỉnh/thành phố. Listen dragend chẳng hạn dùng lat của map center và bounds của các vùng để load region tiếp theo.

1b. Giảm số sau dấu thập phân (reducing precision)

Giảm kích thước KML bằng các giảm precision của lat, lng còn khoảng 6 dp.

Một số QA trên StackOverflow

How accurately should I store latitude and longitude?
Accuracy versus decimal places at the equator
decimal  degrees    distance
places
-------------------------------  
0        1.0        111 km
1        0.1        11.1 km
2        0.01       1.11 km
3        0.001      111 m
4        0.0001     11.1 m
5        0.00001    1.11 m
6        0.000001   0.111 m
7        0.0000001  1.11 cm
8        0.00000001 1.11 mm
There is a difference between "number of digits used to represent a value" and the "number of meaningful digits used to represent a value".
No matter what you use to determine a location of a point on earth, there will be inaccuracies associated with your method. For GPS you are lucky to get 1 m - or a few cm if you use special correction methods.
Since the circumference of the earth is 40,000 km and 360 degrees, one degree (at the equator) corresponds to 111 km; so when you have 0.000 001 degree, you are down to 11 cm. In other words, 3 + 6 digits (in degrees) is about as accurate as you ever ought to care about.
Mình dùng XSLT để format lại file XML cái này không cần sửa 3rd library (xem file format.xslt bên dưới). Có thể chui thẳng vào code của SharpXML hay geXML để sửa.

Giả sử mình dùng C# và SharpKML, file format.xslt nhúng tromg resources Resources.TransformXslt

Lưu ý: namespace của XSLT phải khớp với namespace của file XML mới có thể transform (rất dễ lộn, một khi sai thì khó có thể nhận ra). KML generate bởi geKML (library cũ) thường là 2.1 sẽ không transform (ko match template) với namespace KML 2.2.


xmlns:gis="http://www.opengis.net/kml/2.2"

Preview:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.SqlServer.Types;
using System.Data.SqlTypes;
using System.Text;
using SharpKml.Dom;
using SharpKml.Engine;
using System.IO;
using SharpKml.Base;
using System.Xml.Linq;
using System.Xml.Serialization;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;

public static byte[] SerializeKml(this KmlFile kmlFile)
{
 var serializer = new Serializer();
 serializer.Serialize(kmlFile.Root);

 //var str = serializer.Xml;
 var xmlDoc = XDocument.Parse(serializer.Xml);

 var xsltTrans = new XslCompiledTransform();
 using (var sr = new StringReader(Resources.TransformXslt))
 using (var xmlReader = XmlReader.Create(sr))
 {
  xsltTrans.Load(xmlReader);
 }

 var transDoc = new XDocument();
 using (var xmlWriter = transDoc.CreateWriter())
 {
  xsltTrans.Transform(xmlDoc.CreateReader(), xmlWriter);
 }

 var xmlDeclaration = "";
 var str = xmlDeclaration + transDoc.ToString(SaveOptions.DisableFormatting);
 //var str = xmlDoc.ToString(SaveOptions.DisableFormatting);
 return Encoding.UTF8.GetBytes(str);
}


1c. Đơn giản hóa (giảm bớt số points) polygon, polyline (simplify-reduce number of points)

Dùng Line Simplification xem thêm https://bost.ocks.org/mike/simplify/ có đề cập tới thuật toán Visvalingam’s algorithm và cho rằng hiệu quả hơn Douglas-Peucker.

Simplifying multiple neighboring polygons

Xem thêm trên Stackoverflow How to simplify (reduce number of points) in KML?

Về DP algorithm Douglas-Peucker Generalization Algorithm

Một số implementation trên CodeProject
A C# Implementation of Douglas-Peucker Line Approximation Algorithm
Polyline Simplification

Nếu dùng C# với NTS TopologySuite thì trong library này có implement sẵn một số các thuật toán trong đó có DouglasPeuckerSimplifier. Chọn tolerance bao nhiêu thì OK, test thử chọn 0.001 hoặc lớn hơn .... Tốt nhất là tùy theo zoom level, nên chia thành nhiều layer ứng với zoom level với cùng chức năng.

public class DouglasPeuckerSimplifier
    Member of NetTopologySuite.Simplify
Simplifies a GeoAPI.Geometries.IGeometry using the Douglas-Peucker algorithm.

2a. Optimize của geoXML3

Optimize các option của geoXML3
processStyles: false
zoom: false không fitBounds
singleInfoWindow: true

Sửa function processPlacemarkCoords

function processPlacemarkCoords(node, tag) {
...
}

Function processPlacemarkCoords có nhiều vấn đề khi thực hiện split và parse sang lat, lng.

var path = coords.split(/\s+/g);

Nếu format trả về là chuẩn (ví dụ: không có space đi với comma, chỉ có 1 space giữa các lat,lng)
Comment đoạn bỏ space đi với dấu , (comma)
//coords = coords.replace(/,\s+/g, ',');

Biến đổi coords thành JSON string, và dùng JSON để parse.

coords = coords.replace(/\s/g, '],[');
coords = '[[' + coords + ']]';
coords = JSON.parse(coords);

Bỏ biến tạm khi parse latlng

Function cuối cùng sau khi chỉnh sửa:
Preview:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function processPlacemarkCoords(node, tag) {
 var parent = node.getElementsByTagName(tag);
 var coordListA = [];
 for (var i = 0; i < parent.length; i++) {
  var coordNodes = parent[i].getElementsByTagName('coordinates')
  if (!coordNodes) {
   if (coordListA.length > 0) {
    break;
   } else {
    return [{ coordinates: [] }];
   }
  }

  for (var j = 0; j < coordNodes.length; j++) {
   var coords = geoXML3.nodeValue(coordNodes[j]).trim();

   // Note: have no space then comma ' ,'
   //coords = coords.replace(/,\s+/g, ',');

   /*
   var path = coords.split(/\s+/g);
   var pathLength = path.length;
   var coordList = [];
   for (var k = 0; k < pathLength; k++) {
    coords = path[k].split(',');
    if (!isNaN(coords[0]) && !isNaN(coords[1])) {
     coordList.push({
      lat: parseFloat(coords[1]),
      lng: parseFloat(coords[0]),
      alt: parseFloat(coords[2])
     });
    }
   }
   */

   // Have no double space
   //coords = coords.replace(/\s+/g, '],[');
   coords = coords.replace(/\s/g, '],[');
   coords = '[[' + coords + ']]';
   coords = JSON.parse(coords);

   var lst = {
    coordinates: []
   };

   for (var k = 0; k < coords.length; k++) {
    var c = coords[k];
    lst.coordinates.push({
     lat: parseFloat(c[1]),
     lng: parseFloat(c[0]),
     alt: parseFloat(c[2])
    });
   }

   coordListA.push(lst);
  }
 }

 return coordListA;
}


File format.xslt


Lưu ý: XSLT của .NET là version 1 nên không có function tokenize nên tạo template tokenize với tag . Precision có thể là 5 hoặc 6.
Sử dụng: chọn delimiter phù hợp, delimiter đang sử dụng cho file generated bởi SharpKML, phân cách bởi CRLF.

Có thể giảm kích thước đến 60%, xem Making KML files smaller by reducing precision


<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:ext="http://exslt.org/common"
    xmlns:gis="http://www.opengis.net/kml/2.2"
    version="1.0">

  <!-- This is an identity transform template - it copies all the nodes -->
  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
  </xsl:template>

  <!-- this template has precedence over the identity template for the `coordinates` nodes -->
  <xsl:template match="gis:coordinates">
    <xsl:copy>
      <!-- it copies the element -->
      <xsl:variable name="newline" select="'&#10;'"/>
      <xsl:variable name="coords">
        <xsl:call-template name="tokenize">
          <xsl:with-param name="string" select="." />
          <xsl:with-param name="delimiter" select="$newline" />
        </xsl:call-template>
      </xsl:variable>
      <!-- saves coordinate pairs in variable -->
      <xsl:for-each select="ext:node-set($coords)/token">
        <!-- for each coordinate pair, formats the values before and after the comma -->
        <xsl:value-of select="round(number(substring-before(.,','))*100000) div 100000"/>
        <xsl:text>,</xsl:text>
        <!-- puts the comma back between the coords -->
        <xsl:value-of select="round(number(substring-after(.,','))*100000) div 100000"/>
        <!-- puts the space back if it's not the last coord -->
        <xsl:if test="position() != last()">
          <xsl:text> </xsl:text>          
        </xsl:if>
        <!-- <xsl:value-of select="$newline"/> -->
      </xsl:for-each>
    </xsl:copy>
  </xsl:template>
  <xsl:template name="tokenize">
    <xsl:param name="string" />
    <xsl:param name="delimiter" />
    <xsl:choose>
      <xsl:when test="contains($string, $delimiter)">
        <token>
          <xsl:value-of select="substring-before($string, $delimiter)" />
        </token>
        <xsl:call-template name="tokenize">
          <xsl:with-param name="string" select="substring-after($string, $delimiter)" />
          <xsl:with-param name="delimiter" select="$delimiter" />
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <xsl:choose>
          <xsl:when test="$string = ''">
            <xsl:text/>
          </xsl:when>
          <xsl:otherwise>
            <token>
              <xsl:value-of select="$string"/>
            </token>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

No comments:

Post a Comment