让前端飞Web前端之路GIS

扩展ArcGIS JS API中的MapImageLayer支持

2020-03-07  本文已影响0人  徐磊x
效果示例图

需求

BaseDynmicLayer不支持3D模式

分析

之前通过后台服务模拟Map Service时就知道了MapImageLayer是支持2D/3D地图加载的,而大概知道了JS API 中MapImageLayer的基本原理,所以针对这个问题还要首先想到了从MapImageLayer下手。MapImageLayer的基本实现如下:

实现

源码分析

虽然 ArcGIS JS API的代码是混淆,但格式化后还是能有所发现的。

在MapImageLayer.js文件两个函数引起了我的注意,感觉有戏,一个是fetchImage,一个是_fetchService,很明显fetchImage是在向服务端请求图片,十之八九就是返回给View层渲染的图片,而fetchService很有可能是查询元数据的方法。

c.prototype._fetchService = function (a) {
    return k(this, void 0, void 0, function () {
        var b, c, d;
        return f(this, function (e) {
            switch (e.label) {
                case 0:
                    return this.sourceJSON ? (this.read(this.sourceJSON, {
                                origin: "service",
                                url: this.parsedUrl
                            }), [2]) : 
                    [4, m(this.parsedUrl.path, {query: p({f: "json"}, this.parsedUrl.query), signal: a})];
                case 1:
                    b = e.sent();                            
                    c = b.data;                            
                    if (d = b.ssl) this.url = this.url.replace(/^http:/i, "https:");
                    this.sourceJSON = c;                            
                    this.read(c, {origin: "service", url: this.parsedUrl});               
                    return [2]
                    }
                })
            })
        };

这个函数里大致的意思就是根据一些条件决定是否去后台取数据,通过调试跟踪代码发现它调的接口就是地图服务的元数据查询接口。看到代码里有一段 this.sourceJSON=C ,初步推断 this.sourceJSON就图层的元数据信息,为了验证这个猜测,构建了一个元数据模板,在load方法里直接把元数据赋给了this.sourceJSON,图层在地图上正常加载了,那么实锤this.sourceJSON就是图层元数据对象。

c.prototype.load =function (a) {
    this.sourceJSON = this._setupSourceJSON(this.spatialReference, this.pictureExtent, this.units);
    ……
};
c.prototype.fetchImage =            
    function (a, b, c, d) {
    var e = { responseType: "image" };
    d && d.timestamp && (e.query = { _ts: d.timestamp });
    d && d.signal && (e.signal = d.signal);
    var f, h = this.getImageUrl(a, b, c, d);
    if (h) a = g.when(h).then(function (a) {
        f = a;
        return m(f, e)
                });
    else {
        f = this.parsedUrl.path + "/export";
        a = p({}, this.parsedUrl.query, this.createExportImageParameters(a, b, c, d), {
                        f: "image",
                        _ts: this.alwaysRefetch ? Date.now() : null});
        if (null != a.dynamicLayers &&!this.capabilities.exportMap.supportsDynamicLayers)
            return g.reject(new w("mapimagelayer:dynamiclayer-not-supported",
                        "service " + this.url + " doesn't support dynamic layers, which is required to be able to change the sublayer's order, rendering, labeling or source.", { query: a }));
        e.query = e.query ? p({}, a, e.query) : a;
        a = m(f, e)
    }
    return a.then(function (a) {
        return a.data
    }).catch(function (a) {
        if (g.isAbortError(a)) throw a;
        throw new w("mapimagelayer:image-fetch-error", "Unable to load image: " + f, { error: a });
    })
};

在代码里发现了 /export 字样,果然这个方法是用来获取渲染图片的,调试跟踪代码发现 a是一个Extent类数据,那么它应该就是获取图版时的地图范围,b、c的值都是2048,那么这两个参数很有可参对应width、height两个参数。这个方法最终返回一个Promise对象,并在then里返回了后数据,调试时也确认返回的数据就是一个img。

有了上面这些结果,下面要做事情就是两件:

自动填充图层元数据

ArcGIS Map Service的元数据结构示例:

{
    "currentVersion": "10.7",
    "serviceDescription": "",
    "mapName": "Layers",
    "description": "",
    "copyrightText": "",
    "supportsDynamicLayers": true,
    "singleFusedMapCache": false,
    "minScale": 0,
    "maxScale": "0",
    "units": "esriMeters",
    "supportedImageFormatTypes": "PNG32,PNG24,PNG,JPG,DIB,TIFF,EMF,PS,PDF,GIF,SVG,SVGZ,BMP",
    "capabilities": "Map,Query,Data",
    "supportedQueryFormats": "JSON, AMF, geoJSON",
    "exportTilesAllowed": false,
    "supportsDatumTransformation": true,
    "maxRecordCount": 1000,
    "maxImageHeight": 4096,
    "maxImageWidth": 4096,
    "supportedExtensions": "KmlServer",
    "layers": [
        {
            "id": 0,
            "name": "82f22214-12da-433e-a2eb-f9c1acaa3718",
            "parentLayerId": -1,
            "subLayerIds": [],
            "minScale": 0,
            "maxScale": 0,
            "type": "Raster Layer"
        }
    ],
    "tables": [],
    "spatialReference": {
        "wkid": 102100,
        "latestWkid": 3857
    },
    "initialExtent": {
        "xmin": 7792364.355529149,
        "ymin": -7.081154551613622e-10,
        "xmax": 16697923.618991036,
        "ymax": 4865942.279503176,
        "spatialReference": {
            "wkid": 102100,
            "latestWkid": 3857
        }
    },
    "fullExtent": {
        "xmin": 7792364.355529149,
        "ymin": -7.081154551613622e-10,
        "xmax": 16697923.618991036,
        "ymax": 4865942.279503176,
        "spatialReference": {
            "wkid": 102100,
            "latestWkid": 3857
        }
    },
    "documentInfo": {
        "Title": "",
        "Author": "",
        "Comments": "",
        "Subject": "",
        "Category": "",
        "AntialiasingMode": "None",
        "TextAntialiasingMode": "Force",
        "Keywords": ""
    },
    "datumTransformations": null
}

其实在这些数据中影像图片到地图上加载的参数就是fullExtent、initExtent、spatialReference、units,所以在新定义的PictureLayer中新添加了pictureExtent、units两个属性,spatialReference是图层本身就有的属性。

在图层load的时候就把图层的元数据构建好并赋值给this.sourceJSON。

c.prototype.load =function (a) {
    this.sourceJSON = this._setupSourceJSON(this.spatialReference, this.pictureExtent, this.units);
    var b = this, c = h.isSome(a) ? a.signal : null;
    this.addResolvingPromise(this.loadFromPortal({supportedTypes: ["Map Service"]}, a).then(function () {
        return b._fetchService(c);
    }));
    return this.when();
};

c.prototype._setupSourceJSON = function (spatialReference, extent, units) {
let json = {
    currentVersion: "10.7",
    serviceDescription: "",
    spatialReference: spatialReference,
    initialExtent: extent,
    fullExtent: extent,
    units: units,
    ……
    };
    return json;
};

前端生成图片

首先要计算图片是否在当前地图显示区域

在fetchImage函数里传了一个参数a,它实际上就是当前地图范围,另外在图层属性中定义了图片的显示范围,那么在fetchImage函数中做的第一件事就是判定这两个范围是否有重叠部分,如果有侧计算出重叠的范围。

位置示意图

设定两个变量geo_map_extent{xmin,ymin,xmax,ymax}、geo_picture_extent{xmin,ymin,xmax,ymax},以上面的示意图来看当两个变量相应值的最大值比最小值小或最小值比最大值 大,那么这两个范围间没有重叠区域,而在之外的情况下,两个范围是有重叠的。所以判定是否有重置区域的函数定义如下:

c.prototype.isRectCross = function (a, c) {
    return (a[0] > c[2] || a[2] < c[0] || a[1] > c[3] || a[3] < c[1]) ?
    false :
    true;
};

a、c为两个数组变量,结构为:[xmin,ymin,xmax,yma]

范围重叠示意图

根据图形重置关系,计算重叠区的函数定义如下:

c.prototype.crossRect = function (a, c) {
    let left = Math.max(a[0], c[0]);
    let right = Math.min(a[2], c[2]);
    let top = Math.min(a[3], c[3]);
    let bottom = Math.max(a[1], c[1]);
    return [left, bottom, right, top];
};

根据重叠范围生成图片

图片生成最终是调用了canvas.drawImage方法

context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);
参数 描述
img 规定要使用的图像、画布或视频。
sx 可选。开始剪切的 x 坐标位置。
sy 可选。开始剪切的 y 坐标位置。
swidth 可选。被剪切图像的宽度。
sheight 可选。被剪切图像的高度。
x 在画布上放置图像的 x 坐标位置。
y 在画布上放置图像的 y 坐标位置。
width 可选。要使用的图像的宽度。(伸展或缩小图像)
height 可选。要使用的图像的高度。(伸展或缩小图像)

第一步:计算出原始图片和生成的图片单位像素的地理距离

使用地图、图片的地图范围和地图图版、原始图片的尺寸计算出单位像素的地理距离,代码如下:

/**计算原始图片单位像素的地理距离*/
let  mapBox = [a.xmin, a.ymin, a.xmax, a.ymax];
let imgBox = [pl.pictureExtent.xmin, pl.pictureExtent.ymin, pl.pictureExtent.xmax, pl.pictureExtent.ymax];
let image = new Image();
image.src = pl.url;
let imgWidth = image.width;
let imgHeight = image.height;
let imgDx = imgWidth / (imgBox[2] - imgBox[0]);
let imgDy = imgHeight / (imgBox[3] - imgBox[1]);

/**计算地图图片单位像素的地理距离*/
let mapDx = width / (mapBox[2] - mapBox[0]);
let mapDy = height / (mapBox[3] - mapBox[1]);

第二步:计算出地图图版上的绘制范围与图片上的裁切范围

let crossBox = pl.crossRect(mapBox, imgBox);

/**计算地图图片的绘制区域*/
let imgLeft = Math.ceil(imgDx * (crossBox[0] - imgBox[0]));
let imgRight = Math.ceil(imgDx * (crossBox[2] - imgBox[0]));
let imgTop = Math.ceil(imgDy * (imgBox[3] - crossBox[3]));
let imgBottom = Math.ceil(imgDy * (imgBox[3] - crossBox[1]));

/**计算原始图片的裁剪区域*/
let mapLeft = Math.ceil(mapDx * (crossBox[0] - mapBox[0]));
let mapRight = Math.ceil(mapDx * (crossBox[2] - mapBox[0]));
let mapTop = Math.ceil(mapDy * (mapBox[3] - crossBox[3]));
let mapBottom = Math.ceil(mapDy * (mapBox[3] - crossBox[1]));

第三步:在地图图版上绘制图片

let data = new Image(); //返回的数据
data.crossOrigin = "Anonymous";
data.alt = "map-picture";
canvas.context.drawImage(image, imgLeft, imgTop, imgRight - imgLeft, imgBottom - imgTop, mapLeft, mapTop, mapRight - mapLeft, mapBottom - mapTop);
data.src = overlayCanvas.toDataURL("image/png");
resolve(data);

如果图片与当前地图返回没有重叠,那么返回一张透明的空间图片,

let data = new Image(); //返回的数据
data.crossOrigin = "Anonymous";
data.alt = "map-picture";
canvas.context.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
data.src = overlayCanvas.toDataURL("image/png");
resolve(data);

以上就是实现地图加载图片显示的思路了,完整代码及使用示例到Github上获取吧 ags-picture-layer

上一篇下一篇

猜你喜欢

热点阅读