扩展ArcGIS JS API中的MapImageLayer支持图片图层加载

效果示例图

需求

  • 有些系统里有添加图片到地图(2D/3D)上显示的需求,这些图片知道地理范围,但也只是普通图片不是严格意义上的栅格地理数据,另外又不想做几何校正等数据处理,然后发布地图服务这些复杂操作。

  • 前段时间实现了后台接口模拟 ArcGIS Map Service 让各端通过 MapImageLayer 加载,但也想尝试有没有纯属前端(JS)的解决方案。

  • 虽然BaseDynmicLayer支持扩展,但官方文档明确指出不支持3D模式。

BaseDynmicLayer不支持3D模式

分析

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

  • 调用地图服务元数据查询接口,获取图层的元数据,这些元数据里包含了坐标系,图层的地图范围等信息,对于图片类型的加载,坐标系与地图范围这两个元数据特别重要,不可缺。
  • 图层显示或地图显示区域变化 时,图层会调取地图服务一个名为 export 的接口,服务端会根据回传的参数生成图片,png、jpe或其它格式,将图片返回给调用端。调用端传递的参数包括了当前地图范围、需要的图片尺寸、像素密度、图片格式等等。
  • 在JS API中,MapImageLayer 获取到服务端返回的图片后,把图片返回给了View层,由View层完成在地图上的绘制。

实现

源码分析

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

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

  • _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);
    ……
};
  • fetchImage
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。

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

  • 一是根据图层配制的一些参数或属性,自动填充图层元数据;
  • 另一件就是根据获取图片的参数,直接在前端生View层渲染需要的图片;

自动填充图层元数据

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容