Có cách nào dễ dàng để sử dụng Postgis GeoJSON trong Openlayers 3 không


8

Tôi đã sử dụng các lớp vectơ google và các lớp vectơ từ Jason Sanford ( https://github.com/JasonSanford ) để dễ dàng hiển thị, tạo kiểu và thêm cửa sổ bật lên tùy chỉnh cho dữ liệu từ cơ sở dữ liệu Postgis. Điều này hoạt động kết hợp với một phiên bản sửa đổi của PHP-Database-GeoJSON từ Bryan McBride.

Có gì có thể so sánh để sử dụng với Openlayers 3 ngoài kia không? Tôi phải thừa nhận rằng tôi không có kỹ năng lập trình để viết một thư viện như vậy. Có thể là ai đó biết về một mã so sánh. Sau khi thực hiện nhiều thao tác trên Google, tôi đã tìm thấy rất nhiều câu trả lời cho các vấn đề cụ thể và quản lý để thực hiện mọi thứ về cách tải GeoJSON bằng chiến lược Ajax và ràng buộc, tạo kiểu các lớp vectơ và thêm cửa sổ bật lên, nhưng tôi vẫn còn thiếu một cách dễ dàng cách để đặt tất cả những điều này với nhau.

Tôi tự hỏi liệu có thể có một giải pháp hiện có mà tôi không tìm thấy cho đến ngày hôm nay không, vì tôi nghĩ PostGis> GeoJSON> Openlayers 3 (bao gồm cả kiểu dáng và hiển thị các tính năng qua cửa sổ bật lên) có phải là một cách khá chuẩn?


gis.stackexchange.com/questions/134688/ . Bạn thêm url như một phần của src của lớp vectơ (tất nhiên, bạn có thể tạo kiểu).
John Powell

Tôi đã nghiên cứu bài đăng này rồi, nhưng như tôi đã đề cập trước đây, không có giải pháp tất cả trong một như các kịch bản lớp vector.
geom

Bạn có thể muốn thêm một ví dụ cho thấy cách các kịch bản lớp vector hoạt động, vì nó hoàn toàn không rõ ràng từ một liên kết đến trang github và những gì còn thiếu trong các ví dụ OL3 về vấn đề đó.
John Powell

Câu trả lời:


7

Vì không có câu trả lời cho câu hỏi của mình, tôi đã cố gắng lấy cảm hứng từ mã đã có sẵn khác và tiếp tục phát triển một hàm tạo GeoJSON cơ bản trong openlayers 3, đáp ứng đầy đủ nhu cầu của tôi.

Tôi thực sự có thể

  • tải các lớp khác nhau từ PostGIS bằng cách chuyển một tên bảng, trường bảng và mệnh đề WHERE (với tất cả các phép thuật có thể có của PosgreQuery / PostGis)
  • tạo kiểu cho chúng (sử dụng các giá trị đơn, duy nhất hoặc phạm vi)
  • hiển thị / ẩn các lớp ở mức tối thiểu / maxResolution
  • định danh duy nhất cần phải là id để ngăn các tính năng được tải nhiều lần
  • các lớp được tải thông qua chiến lược ol.loadingstrargety.bbox để chỉ có dữ liệu hiển thị được tải
  • xác định các mẫu popup cụ thể của lớp
  • thêm lớp nhãn cụ thể

Vì vậy, nó đã là một gói hài hước, có thể được điều chỉnh (và cải thiện) rất dễ dàng. Vì vậy tôi muốn chia sẻ mã ở đây. Toàn bộ bao gồm ba phần:

  • ol3Vector.js một lớp mở rộng của ol.layer.Vector
  • một tệp map.js trong đó bản đồ và tất cả các lớp được xác định
  • Tệp get_geojson.php được sử dụng bên máy chủ để xây dựng chuỗi sql và trả về GeoJSON hợp lệ.

Để hoạt động chính xác, ol3Vector.js cần được tải trước tiên. Đây là mã:

 // class to load vector layers from Postgis using a modified php-script from
 // Bryan McBride
 // https://github.com/bmcbride/PHP-Database-GeoJSON 
 //
 ol3Vector = function(options) {

 //
 // -------- Defining default style settings ----------
 //
 var fill = new ol.style.Fill({
    color: 'rgba(255,255,255,0.4)'
 });
 var stroke = new ol.style.Stroke({
    color: '#3399CC',
    width: 1.25
 });
 var text = new ol.style.Text({
    text: "",
    font: "16px Calibri,sans-serif",
    fill: new ol.style.Fill({
        color: [255, 255, 255, 1]
    }),
    stroke: new ol.style.Stroke({
        color: [0, 0, 0, 1],
        width: 2.5
    })
});

var image = new ol.style.Circle({  // actually point styling only works with image, and not with icons
    fill: fill,
    stroke: stroke,
    radius: 5
});

var style = new ol.style.Style({
    image: image,
    fill: fill,
    stroke: stroke,
    text: text
});

var styles = [style];

//
// ------------- defining options to build the new ol3Vector-layer -----------
//
var options = {
    title: options.title,
    visible: false,
    geotable: options.geotable,  // table name in PostGis-database
    fields: options.fields,      // field-names
    where: options.where,        // where-string passed to PostGis
    source: new ol.source.Vector({
        projection: "EPSG:4326",
        attributions: [new ol.Attribution({
            html: options.attribution
        })],
        strategy: ol.loadingstrategy.bbox,   //load only data off the visible map
        loader: function(extent, resolution, projection) {
            var extent = ol.proj.transformExtent(extent, projection.getCode(), ol.proj.get('EPSG:4326').getCode());
            $.ajax({
                type: "GET",
                dataType: "json",
                url: "./mapdata/get_geojson.php?" +     // define path to the get_geojson.php script
                    "geotable=" + options.geotable +
                    "&fields=" + options.fields +
                    "&where=" + options.where +
                    "&bbox=" + extent.join(","),
                context: this
            }).done(function(data) {
                var format = new ol.format.GeoJSON();
                this.addFeatures(format.readFeatures(data, {
                    dataProjection: "EPSG:4326",
                    featureProjection: "EPSG:3857"
                }));

            });

        }
    }),
    minResolution: options.minResolution,
    maxResolution: options.maxResolution,
    content: options.content,
    symbology: options.symbology,
    showLabels: options.showLabels,
    label: options.label,
    style: (function label_style() { // style function needed to be wrapped in a function to get it work
        // ------ labeling -----------
        var layerLabel = "";
        if (!options.showLabels) {
            layerLabel = "";
        } else if (options.showLabels) {
            layerLabel = options.label;
        }
        return function(feature, resolution) {
            style.getText().setText(feature.get(layerLabel));

            // ------ styling -------------
            if (!options.symbology) {
                return style;
                //            return false;        
            } else if (options.symbology) {

                var atts = feature.getProperties();

                switch (options.symbology.type) {
                    case "single":
                        // Its a single symbology for all features so just set the values for fill and stroke
                        //
                        switch (feature.getGeometry().getType()) {
                            case "Point":
                            case "MultiPoint":

                                style.image = style.getImage().getFill().setColor(options.symbology.styleOptions.fill);
                                style.image = style.getImage().getStroke().setColor(options.symbology.styleOptions.color);
                                style.image = style.getImage().getStroke().setWidth(options.symbology.styleOptions.width);
                                style.image = style.getImage().setRadius(options.symbology.styleOptions.radius);
                                break;

                            case "LineString":
                            case "MultiLineString":
                            case "Polygon":
                            case "MultiPolygon":

                                style.fill = style.setFill(new ol.style.Fill({
                                    color: options.symbology.styleOptions.fill
                                }));
                                style.stroke = style.setStroke(new ol.style.Stroke({
                                    color: options.symbology.styleOptions.color,
                                    width: options.symbology.styleOptions.width
                                }));
                                break;
                        }
                        break;

                    case "unique":
                        // Its a unique symbology. Check if the features property value matches that in the symbology and style accordingly
                        // 
                        var att = options.symbology.property;
                        for (var i = 0, len = options.symbology.values.length; i < len; i++) { // field with values to define styles
                            if (atts[att] == options.symbology.values[i].value) { // unique value to identify style
                                for (var key in options.symbology.values[i].styleOptions) {

                                    switch (feature.getGeometry().getType()) {
                                        case "Point":
                                        case "MultiPoint":

                                            style.image = style.getImage().getFill().setColor(options.symbology.values[i].styleOptions.fill);
                                            style.image = style.getImage().getStroke().setColor(options.symbology.values[i].styleOptions.color);
                                            style.image = style.getImage().getStroke().setWidth(options.symbology.values[i].styleOptions.width);
                                            style.image = style.getImage().setRadius(options.symbology.values[i].styleOptions.radius);
                                            break;

                                        case "LineString":
                                        case "MultiLineString":
                                        case "Polygon":
                                        case "MultiPolygon":

                                            style.fill = style.setFill(new ol.style.Fill({
                                                color: options.symbology.values[i].styleOptions.fill
                                            }));
                                            style.stroke = style.setStroke(new ol.style.Stroke({
                                                color: options.symbology.values[i].styleOptions.color,
                                                width: options.symbology.values[i].styleOptions.width
                                            }));
                                            break;
                                    }
                                }
                            }
                        }
                        break;

                    case "range":
                        // Its a range symbology. Check if the features property value is in the range set in the symbology and style accordingly
                        //
                        var att = options.symbology.property;
                        for (var i = 0, len = options.symbology.ranges.length; i < len; i++) {
                            if (atts[att] >= options.symbology.ranges[i].range[0] && atts[att] <= options.symbology.ranges[i].range[1]) {
                                for (var key in options.symbology.ranges[i].styleOptions) {

                                    switch (feature.getGeometry().getType()) {
                                        case "Point":
                                        case "MultiPoint":

                                            style.image = style.getImage().getFill().setColor(options.symbology.ranges[i].styleOptions.fill);
                                            style.image = style.getImage().getStroke().setColor(options.symbology.ranges[i].styleOptions.color);
                                            style.image = style.getImage().getStroke().setWidth(options.symbology.ranges[i].styleOptions.width);
                                            style.image = style.getImage().setRadius(options.symbology.ranges[i].styleOptions.radius);
                                            break;

                                        case "LineString":
                                        case "MultiLineString":
                                        case "Polygon":
                                        case "MultiPolygon":

                                            style.fill = style.setFill(new ol.style.Fill({
                                                color: options.symbology.ranges[i].styleOptions.fill
                                            }));
                                            style.stroke = style.setStroke(new ol.style.Stroke({
                                                color: options.symbology.ranges[i].styleOptions.color,
                                                width: options.symbology.ranges[i].styleOptions.width
                                            }));
                                            break;
                                    }
                                }
                            }
                        }

                        break;
                }
            }
            return styles;
        }
    })()
}

ol.layer.Vector.call(this, options);

};

ol.inherits(ol3Vector, ol.layer.Vector);

đây là một ví dụ về map.js

function init() {
document.removeEventListener('DOMContentLoaded', init);

//
// ------- Layers and Map ----------------
//
var baselayers = new ol.layer.Group({  
    title: 'Baselayers',
    layers: [
        new ol.layer.Tile({
            title: 'OSM',
            type: 'base',
            visible: true,
            source: new ol.source.OSM()
        })
    ]
});

var toplayers = new ol.layer.Group({
    title: 'Toplayer',
    layers: []
});


var view = new ol.View({
    center: ol.proj.fromLonLat([6.2, 49.6]),
    zoom: 12
});

var popup_div = document.getElementById('popup');
var popup = new ol.Overlay({
    element: popup_div,
    positioning: 'bottom-center',
    stopEvent: false
});

var map = new ol.Map({
    target: 'map',
    view: view,
    controls: ol.control.defaults().extend([
        new ol.control.Zoom(),
        new ol.control.FullScreen(),
        new ol.control.ZoomSlider(),
        new ol.control.LayerSwitcher({  //layergroups are used with the layerswitcher from https://github.com/walkermatt/ol3-layerswitcher
            tipLabel: 'Layer Switcher'
        }),
        new ol.control.OverviewMap(),
        new ol.control.ScaleLine()
    ]),
    overlays: [popup],
    layers: [baselayers, toplayers],
    interactions: ol.interaction.defaults().extend([
        new ol.interaction.Select({
            layers: [baselayers, toplayers]
        })
    ])
});

//
// ------------ define toplayers -------------------
//
var n2k_dh_l = new ol3Vector({
    title: "Natura 2000 Habitats Directive",   // name of the layer to show up in the layerswitcher
    attribution: "<br />Réseau Natura 2000 Habitats Directive",
    geotable: "n2k_dh",
    fields: "gid as id, sitecode, sitename, surfha",
    where: "sitename ilike '%moselle%'",    // You can use all the PostgreSQL or PostGis features here
    symbology: {
        type: "single",
        styleOptions: {
            fill: "rgba(100,250,0,0.1)",   // define colors as rgba()
            color: "green",                // or simple color-names
            width: 2
        }
    },
    minResolution: 0.01,
    maxResolution: 50,
    content: "<p><strong> BH {sitecode}</strong><hr>{sitename}<br />{surfha} ha </p>",
    showLabels: true,    // show labels on map
    label: "sitename"    // field used for labeling
});

var n2k_do_l = new ol3Vector({
    title: "Natura 2000 Birds Directive",
    attribution: "<br />Réseau Natura 2000 Birds Directive",
    geotable: "n2k_do",
    fields: "gid as id, sitecode, sitename, surfha",
    where: "",
    symbology: {
        type: "single",
        styleOptions: {
            fill: "rgba(100,250,0,0.1)",
            color: "magenta",
            width: 2
        }
    },
    minResolution: 0.01,
    maxResolution: 50,
    content: "<p><strong> BD {sitecode}</strong><hr>{sitename}<br />{surfha} ha </p>",
    showLabels: true,
    label: "sitecode"
});

var communes = new ol3Vector({
    map: map,
    title: "Communes",
    attribution: "<br />Communes",
    geotable: "communes",
    fields: "gid as id, surfha, commune, stats",
    where: "",
    symbology: {
        type: "unique",
        property: "stats",  // field used to style depending on their value
        values: [{
                value: "AB", 
                styleOptions: {
                    fill: "rgba(0,0,0,0.0)",
                    color: "grey",
                    width: 1.25
                }
            },
            {
                value: "CD",
                styleOptions: {
                    fill: "rgba(100,250,0,0.1)",
                    color: "green",
                    width: 2
                }
            },
            {
                value: "",
                styleOptions: {
                    fill: "rgba(250,0,0,0.1)",
                    color: "red",
                    width: 2
                }
            }
        ]
    },
    minResolution: 0.01,
    maxResolution: 50,
    content: "<p><strong>{commune}</strong><hr>{stat}<br />{surfha} ha </p>",
    showLabels: true,   
    label: "commune"    
});

var n2k_dh_r = new ol3Vector({
    map: map,
    title: "Natura 2000 Habitats Directive - site size",
    attribution: "<br />Réseau Natura 2000 Habitats Directive",
    geotable: "n2k_dh",
    fields: "gid as id, sitecode, sitename, surfha",
    where: "",
    symbology: {
        type: "range",
        property: "surfha", // field that holds values that should be displayed as ranges
        ranges: [{
            range: [1, 100], // defining range min and max values of property field
            styleOptions: {
                fill: "rgba(220,20,60,0.3)",
                color: "crimson",
                width: 1
            }
        }, {
            range: [101, 1000],
            styleOptions: {
                fill: "rgba(255,165,0,0.3)",
                color: "orange",
                width: 1
            }
        }, {
            range: [1001, 10000],
            styleOptions: {
                fill: "rgba(255,255,0,0.3)",
                color: "Yellow",
                width: 1
            }
        }]
    },
    minResolution: 0.01,
    maxResolution: 50,
    content: "<p><strong> BH {sitecode}</strong><hr>{sitename}<br />{surfha} ha </p>",
    showLabels: true,
    label: "sitecode"
});

var btk_p = new ol3Vector({
    map: map,
    title: "Habitats points",  // point layers
    attribution: "<br />Cartho",
    geotable: "btk_p",
    fields: "gid as id, btyp1_code, btyp1_name, cs",
    where: "",
    symbology: {
        type: "unique",
        property: "cs",
        values: [{
                value: "A", 
                styleOptions: {
                    fill: "rgba(0,128,0,0.3)",
                    color: "rgba(0,128,0,0.3)",
                    width: 1,
                    radius: 20 // must be set in order to render point features    
                }
            },
            {
                value: "B",
                styleOptions: {
                    fill: "rgba(255,165,0,0.3)",
                    color: "rgba(255,165,0,0.3)",
                    width: 1,
                    radius: 15
                }
            },
            {
                value: "C",
                styleOptions: {
                    fill: "rgba(255,0,0,0.3)",
                    color: "rgba(255,0,0,0.3)",
                    width: 1,
                    radius: 10
                }
            }
        ]
    },
    minResolution: 0.01,
    maxResolution: 50,
    content: "<p><strong>{btyp1_code}</strong><hr>{btyp1_name}<br />{cs}</p>",
    showLabels: false,
    label: "bewertung1"
});

// ------------------ add ol3Vectors to toplayers ----------
// 
toplayers.setLayers(new ol.Collection([n2k_do_l, n2k_dh_l, communes, n2k_dh_r, btk_p]));
//
// ------------------ show popups based on content-template for different layers --------------------
//  
map.on('click', function(evt) {
    var feature = map.forEachFeatureAtPixel(evt.pixel,
        function(feature, layer) {
            return feature;
        });
    var popupContent = map.forEachLayerAtPixel(evt.pixel,
        function(layer) {
            return layer.get('content');
        });
    if (feature) {
        popup.setPosition(evt.coordinate);
        var atts = feature.getProperties();
        for (var prop in atts) {
            var re = new RegExp("{" + prop + "}", "g");
            popupContent = popupContent.replace(re, atts[prop]);
        }
        $(popup_div).attr('data-placement', 'auto');
        $(popup_div).attr('data-content', popupContent);
        $(popup_div).attr('data-html', true);
        $(popup_div).popover();
        $(popup_div).popover('show');
        $('.popover-title').click(function() {
            $(popup_div).popover('destroy');
        });
    } else {
        $(popup_div).popover('destroy');
    }
});
} // End function init()
document.addEventListener('DOMContentLoaded', init);

Cuối cùng, chúng ta cần get_geojson.php để lấy dữ liệu từ cơ sở dữ liệu PostGis.

<?php
/**
 * GET GeoJSON from PostGIS
 * Query a PostGIS table or view and return the results in GeoJSON format, suitable for use in OpenLayers, Leaflet, etc.
 * Author:  Bryan R. McBride, GISP, adapted by G.Moes
 * Contact: bryanmcbride.com
 * GitHub:  https://github.com/bmcbride/PHP-Database-GeoJSON
 * 
 * @param       string      $geotable       The PostGIS layer name *REQUIRED*
 * @param       string      $geomfield      The PostGIS geometry field *REQUIRED*
 * @param       string      $srid           The SRID of the returned GeoJSON *OPTIONAL (If omitted, EPSG: 2169 will be used)*
 * @param       string      $fields         Fields to be returned *OPTIONAL (If omitted, all fields will be returned)* 
 *                              NOTE- Uppercase field names should be wrapped in double quotes
 * @param       string      $parameters     SQL WHERE clause parameters *OPTIONAL*
 * @param       string      $orderby        SQL ORDER BY constraint *OPTIONAL*
 * @param       string      $sort           SQL ORDER BY sort order (ASC or DESC) *OPTIONAL*
 * @param       string      $limit          Limit number of results returned *OPTIONAL*
 * @param       integer     $precision      digits of returned geojson 6 = 0.111 m submeter as DEFAULT *OPTIONAL*
 * @param       real        $simplify       simplify geometry to >5.0m as DEFAULT *OPTIONAL*  
 * @param       string      $offset         Offset used in conjunction with limit *OPTIONAL*
 * @return      string                  resulting geojson string
 */


# Connect to PostgreSQL database You need to pass here the credentials to connect to Your database
require("../database/connect.php");


function escapeJsonString($value) { # list from www.json.org: (\b backspace, \f formfeed)
  $escapers = array("\\", "/", "\"", "\n", "\r", "\t", "\x08", "\x0c");
  $replacements = array("\\\\", "\\/", "\\\"", "\\n", "\\r", "\\t", "\\f", "\\b");
  $result = str_replace($escapers, $replacements, $value);
  return $result;
}


# Retrive URL variables
if (empty($_GET['geotable'])) {
    echo "missing required parameter: <i>geotable</i>";
    exit;
} else
    $geotable = $_GET['geotable'];
if (empty($_GET['geomfield'])) {
    $geomfield='the_geom';
} else
    $geomfield = $_GET['geomfield'];
if (empty($_GET['srid'])) {
    $srid = 2169;    // changethis if You need another standard SRID
} else
    $srid = $_GET['srid'];
if (empty($_GET['fields'])) {
    $fields = '*';
} else
    $fields = $_GET['fields'];
$parameters = $_GET['where'];
$bbox = $_GET['bbox'];
if (empty($_GET['precision'])) {
    $precision = 6;    // change this to Your needs
} else
    $precision = $_GET['precision'];
if (empty($_GET['simplify'])) {
    $simplify = 5.0;   // change this to Your needs
} else
    $simplify = $_GET['simplify'];    
$orderby    = $_GET['orderby'];
if (empty($_GET['sort'])) {
    $sort = 'ASC';
} else
    $sort = $_GET['sort'];
$limit      = $_GET['limit'];
$offset     = $_GET['offset'];



# Build SQL SELECT statement and return the geometry as a GeoJSON element in EPSG: 4326
$sql = "SELECT " . pg_escape_string($fields) . ", st_asgeojson(st_transform(ST_SimplifyPreserveTopology(" . pg_escape_string($geomfield) . ",".$simplify."),4326),".$precision.") AS geojson FROM " . pg_escape_string($geotable);
if (strlen(trim($parameters)) > 0) {

      $sql .= " WHERE " . str_replace("''", "'", pg_escape_string($parameters));
}
if (strlen(trim($parameters)) > 0 AND strlen(trim($bbox)) > 0){
     $sql .= " AND the_geom &&  st_transform(st_makeenvelope(".pg_escape_string ($bbox).",4326),".$srid.")";
}
if (strlen(trim($parameters)) <= 0 AND strlen(trim($bbox)) > 0) {
      $sql .= " WHERE the_geom &&  st_transform(st_makeenvelope(".pg_escape_string ($bbox).",4326),".$srid.")";
}
if (strlen(trim($orderby)) > 0) {
    $sql .= " ORDER BY " . pg_escape_string($orderby) . " " . $sort;
}
if (strlen(trim($limit)) > 0) {
    $sql .= " LIMIT " . pg_escape_string($limit);
}
if (strlen(trim($offset)) > 0) {
    $sql .= " OFFSET " . pg_escape_string($offset);
}
//echo $sql;
# Try query or error
$rs = pg_query($db_handle, $sql);
if (!$rs) {
    echo "An SQL error occured.\n";
    echo $sql;
    exit;
}
# Build GeoJSON
$output    = '';
$rowOutput = '';
while ($row = pg_fetch_assoc($rs)) {
    $rowOutput = (strlen($rowOutput) > 0 ? ',' : '') . '{"type": "Feature", "geometry": ' . $row['geojson'] . ', "properties": {';
    $props = '';
    $id    = '';
    foreach ($row as $key => $val) {
        if ($key != "geojson") {
            $props .= (strlen($props) > 0 ? ',' : '') . '"' . $key . '":"' . escapeJsonString($val) . '"';
        }
        if ($key == "id") {
            $id .= ',"id":"' . escapeJsonString($val) . '"';
        }
    }

    $rowOutput .= $props . '}';
    $rowOutput .= $id;
    $rowOutput .= '}';
    $output .= $rowOutput;
}
$output = '{"type": "FeatureCollection", "features": [ ' . $output . ' ]}';

header('Content-type:application/json;charset=utf-8');
echo json_encode( $output);
?>

Chúc vui vẻ! Điều này có thể giúp ai đó tiến xa hơn hoặc cải thiện mã này và tôi sẽ xuất bản nó trên GitHub.


Có chức năng POSTGIS nào để cải thiện sự hoàn hảo không? Tôi đang nói 50k tính năng ish cùng một lúc
Revo

Có lẽ: ol.source.ImageVector () mới?
Revo

1
Các tập lệnh này (JS và PHP) không nên được sử dụng như được hiển thị ở đây, vì chúng mở ra cánh cửa để tiêm
JGH

Trên thực tế, các tập lệnh JS và PHP này chỉ được gọi bên trong để tải các lớp vào bản đồ. Các tập lệnh php chỉ được thực thi nếu một tập tin được đăng nhập. Nhưng Quyền của bạn. Bất cứ ai, đã đăng nhập, có thể dễ dàng thao tác chuỗi GET để thực thi sql độc hại. Tôi sẽ nghiên cứu liên kết và xem làm thế nào tôi có thể ngăn ngừa tiêm. Cảm ơn đã xem mã.
geom

1
Có cảm giác hơi giống như phát minh lại bánh xe, tại sao không sử dụng WFS?
nmtoken
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.