作者:爱古德拉
分布式切图格式扩展这个功能已经推着有一段时间了,只知道帮助文档中有这个的说明,但是从来没有机会去试着写一写学一学。碰巧上周有一个大客户要使用自己的瓦片格式切图和发布,就缕了一下这块的东西,也趁着这个机会跟大家分享一下。
首先来说一下什么是分布式切图,针对传统单机切缓存技术的耗时长、无故障恢复机制等缺点,SuperMap iServer 提供了支持多台机器并行切图的分布式切图服务,可添加位于不同机器的多个切图节点,从而实现并行切图,提升切图工作的效率。分布式切图服务支持 FastDFS、MongoDB、MBTiles、SMTiles、SVTiles 等多种瓦片格式,并可以支持扩展的瓦片格式。
我们就以《地理信息公共服务平台——电子地图数据规范》(下载地址:http://files.ngcc.sbsm.gov.cn/www/201206/20120611090020330.pdf)定义的瓦片格式来一步步实现切图和发布。先来看切图,说白了就是我们现在要去扩展一种新的地图瓦片格式,来让iServer把地图瓦片切成我们自己定义的。扩展地图瓦片格式,需要扩展以下三个方面:
1. 实现自定义的切片源信息类
扩展 TileSourceInfo 类,该类定义了切片的存储路径、切片集类型、具体的切片存储类型等,如地图瓦片的类型为“image”。
2. 实现自定义的切片集,即自定义瓦片格式
实现自定义地图瓦片时,需扩展 AbstractImageTileset 类,应根据具体扩展需求,定义瓦片存储与组织方式,包括比例尺、瓦片拆分与目录组织、命名等。
3. 实现自定义的切片源提供者类
扩展 AbstractTileSourceProvider 类,将上述定义的瓦片格式封装为切图可用的切片源提供者,包括新建和加载瓦片集的能力,使切图的时候可以将瓦片存储为该扩展格式。
public class NationalCacheStandardTileSourceInfo extends TileSourceInfo {}
需要实现的方法有:
字段/方法 | 描述 |
---|---|
public NationalCacheStandardTileSourceInfo(NationalCacheStandardTileSourceInfo info) | 实现拷贝构造函数 |
public TileSourceInfo copy() | 实现 copy 方法 |
public TileType[] getSupportedTileTypes() | 指定该存储支持的瓦片类型,如地图瓦片必须指定为“image” |
public int hashCode() \ public boolean equals(Object obj) | 实现 hashCode、equals 方法 |
实现拷贝构造函数和 copy 方法
实现拷贝构造函数,方法如下:
public NationalCacheStandardTileSourceInfo(NationalCacheStandardTileSourceInfo info) {
super(info);
this.setOutputPath(info.getOutputPath());
}
实现 copy 方法:
@Override
public TileSourceInfo copy() {
return new NationalCacheStandardTileSourceInfo(this);
}
指定存储类型
需要为扩展的瓦片格式(tileset)定义类型,所有扩展格式的瓦片类型都应该设置为 UserDefined。
public TileSourceType getType() {
return TileSourceType.UserDefined;
}
//所有扩展格式的瓦片类型都应该设置为 UserDefined。
public void setType(TileSourceType type) {
if (!(TileSourceType.UserDefined.equals(type))) {
throw new IllegalArgumentException();
}
super.setType(type);
}
指定瓦片类型
定义扩展的瓦片的存储格式类型,如地图瓦片(栅格格式)需要设置为“Image”。
@Override
public TileType[] getSupportedTileTypes() {
return new TileType[] { TileType.Image };
}
实现 hashCode、equals 方法
public int hashCode() {
HashCodeBuilder builder = new HashCodeBuilder(120000039, 120000041);
String outputPath = getOutputPath();
if (outputPath != null) {
builder.append(Tool.getUniqueAbsoluteFilePath(outputPath));
}
if (getType() != null) {
builder.append(getType());
}
return builder.toHashCode();
}
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (!(obj instanceof NationalCacheStandardTileSourceInfo)) {
return false;
}
NationalCacheStandardTileSourceInfo rhs = (NationalCacheStandardTileSourceInfo) obj;
return new EqualsBuilder().append(this.getOutputPath(), rhs.getOutputPath()).append(this.getType(), rhs.getType()).isEquals();
}
这块的内容可以直接粘贴,没有需要修改的地方。
public class NationalCacheStandardTileset extends AbstractImageTileset { }
需要实现 AbstractImageTileset 类中未实现的方法,具体包括:
字段/方法 | 描述 |
---|---|
protected boolean doUpdateMetaData(ImageMetaData metaData, TileVersionList tileVersions) | 更新瓦片的元信息 |
public void put(ImageTileInfo tileInfo) | 创建或更新一个瓦片 |
public ImageTileInfo get(Tile tile) | 获取一个瓦片 |
public String getName() | 获取存储的片集的名称 |
切片格式类主要定义了具体的瓦片存储与组织方式,包括比例尺、瓦片拆分与目录组织、命名等,对应《地理信息公共服务平台——电子地图数据规范》中的瓦片结构定义如下:
在扩展类中,按照上述结构,定义瓦片的命名和存储规则并创建存储路径,部分代码如下:
//根据规范,设置支持的比例尺。
private static final double nationalCacheStandardScale[] = new double[] { 295829355.45, 147914677.73, 73957338.86, 36978669.43, 18489334.72, 9244667.36, 4622333.68, 2311166.84, 1155583.42, 577791.71, 288895.85, 144447.93, 72223.96, 36111.98, 18055.99, 9028.00, 4514.00, 2257.00, 1128.50, 564.25 };
//规范中定义的目录结构。
private static final String levelStr = "L";
private static final String rowStr = "R";
private static final String colStr = "C";
//规范中支持的瓦片后缀。
private static final String pngFormat = "png";
private static final String jpgFormat = "jpg";
private static final String dotStr = ".";
//创建存储瓦片的目录。
public NationalCacheStandardTileset(File cacheFile) {
if (!cacheFile.exists()) {
cacheFile.mkdirs();
}
if (!cacheFile.isDirectory()) {
throw new IllegalArgumentException("cacheFile is not a directory!");
}
this.cacheFile = cacheFile;
this.cacheName = cacheFile.getName();
}
更新切片的元信息
doUpdateMetaData 方法用于更新 Tileset 的元信息,由于该规范中并没有这个概念,所以此方法不用实现。
@Override
protected boolean doUpdateMetaData(ImageMetaData metaData, TileVersionList tileVersions) {
return false;
}
追加或更新一个切片
因为分布式切图服务采取了纯色瓦片优化策略来提升切图和存储的效率,所以在存储一张瓦片的时候,需要判断获取的瓦片是否为纯色瓦片,如果是纯色的瓦片需要转化并存储为瓦片文件(即一个 image)。
@Override
public void put(ImageTileInfo tileInfo) throws PutTileFailedException {
checkTile(tileInfo);
byte[] data = tileInfo.tileData;
if (data == null) {
return;
}
//分布式切图中生成的纯色图片都是用一段信息来表示的,这里先判断是否是纯色图片,如果是纯色就使用工具方法获取这段信息代表的图片。
if (MBTilesUtil.isDistributedPureImage(data)) {
data = MBTilesUtil.transformPureImageToCommonImageData(data);
}
storeTileData(tileInfo, data);
}
获取一个切片
通过 get(Tile tile)方法可以获取一张生成的瓦片,并按照指定的规范命名。
@Override
public ImageTileInfo get(Tile tile) {
checkTile(tile);
ImageTileInfo tileInfo = new ImageTileInfo();
tileInfo.resolution = tile.resolution;
tileInfo.scaleDenominator = tile.scaleDenominator;
tileInfo.x = tile.x;
tileInfo.y = tile.y; //获取生成的瓦片。
File imgFile = getPNGImageFile(tile);
String format = null;
if (!imgFile.exists()) { // png 后缀的文件找不到则尝试去找 jpg 后缀的图片
String fileName = imgFile.getName().split("\\.")[0] + dotStr + jpgFormat;
imgFile = new File(imgFile.getParentFile(), fileName);
format = new String(jpgFormat);
} else {
format = new String(pngFormat);
} //找到瓦片后,根据规范命名。
if (imgFile.exists()) {
tileInfo.tileData = getImageData(imgFile, format);
}
return tileInfo;
}
设置瓦片存储目录
//根据规范定义的瓦片存储格式,获取生成的瓦片并给瓦片命名,包括根据比例尺和行列号命名的目录结构、瓦片名称构成,形式如:\L1\R0\C0.png。
private File getPNGImageFile(Tile tileInfo) {
int level = getLevel(tileInfo.scaleDenominator);
File levelFile = new File(cacheFile, levelStr + level);
File rowFile = new File(levelFile, rowStr + tileInfo.y);
File colFile = new File(rowFile, colStr + tileInfo.x + dotStr + pngFormat);
return colFile;
}
获取切片和设置瓦片存储目录是本类的核心,在这里实现了瓦片存储的目录。参照规范定义实现了三级路径。第一级别为L+比例尺level,第二级为R+瓦片行号,第三级为C+瓦片列数,第三级别即是瓦片文件。
如果要扩展自己的瓦片格式,主要修改这两个方法即可。其他方法都可以直接复制使用。
获取存储的切片集的名称
通过 getName()方法可以获取存储的瓦片集的名称,由前缀和瓦片路径创建时的名称缓存文件名组成。
@Override
public String getName() {
if (this.cacheFile != null && this.cacheFile.exists()) {
return namePrefix + cacheFile.getName();
}
return null;
}
@TileSourceProviderAnnotation(infoType com.supermap.services.tilesource.NationalCacheStandardTileSourceInfo.class, name = "NationalMap", isLocal = true)
public class NationalCacheStandardTileSourceProvider extends AbstractTileSourceProvider<NationalCacheStandardTileSourceInfo> { }
@TileSourceProviderAnnotation 注记标识了切片源信息类、名称,是否为本地存储的瓦片。其中,infoType 指向了当前切片源提供者所对应的切片源,本例为前文扩展的 NationalCacheStandardTileSourceInfo;isLocal 为 true 时表示存储类型为本地磁盘存储,false 表示存储类型为分布式存储。
扩展时需要实现 AbstractTileSourceProvider 类中未实现的方法,具体包括:
字段/方法 | 描述 |
---|---|
protected boolean doConnect(NationalCacheStandardTileSourceInfo tilesourceInfo) | 创建并连接存储路径,将瓦片存储到该路径。 |
protected Tileset<ImageMetaData, ImageTileInfo> doCreateTileset(MetaData metaData) |
创建一个切片集。 |
public Tileset<ImageMetaData, ImageTileInfo>[] getTilesets() |
获取 Provider 中所有的 Tileset 的集合。 |
public Tileset<ImageMetaData, ImageTileInfo> getTileset(String name) |
根据切片集名字获取一个切片集。 |
public void refresh() | 刷新操作,会把文件夹下未加载进来的 Tileset 加载。 |
protected boolean doDisConnect() | 断开连接,清理掉所有切片集。 |
1.指定使用自定义扩展的瓦片格式,并创建该类型的切片集,本例中为 NationalCacheStandardTileset:
private ConcurrentHashMap<String, NationalCacheStandardTileset> tilesets = new ConcurrentHashMap<String, NationalCacheStandardTileset>();
@Override
protected Tileset<ImageMetaData, ImageTileInfo> doCreateTileset(MetaData metaData) {
if (!(metaData instanceof ImageMetaData)) {
throw new IllegalArgumentException("NationalCacheStandard TileSource only support ImageMetaData");
}
NationalCacheStandardTileset tileset = new NationalCacheStandardTileset(new File(outputFile, metaData.mapName));
tilesets.put(tileset.getName(), tileset);
return tileset;
}
2.根据扩展的切片源信息类创建瓦片存储目录,并将获取的扩展格式的切片存储到创建的目录中。
@Override
protected boolean doConnect(NationalCacheStandardTileSourceInfo tilesourceInfo) {
String outputPath = tilesourceInfo.getOutputPath();
if (isEmpty(outputPath)) {
return false;
} //创建瓦片存储的目录。
outputFile = new File(outputPath);
if (!outputFile.exists()) {
outputFile.mkdirs();
} //将瓦片集存储到创建的目录。
File[] files = outputFile.listFiles();
for (File file : files) {
if (file.isDirectory()) {
NationalCacheStandardTileset tileset = new NationalCacheStandardTileset(file);
tilesets.put(tileset.getName(), tileset);
}
}
return true;
}
3.获取 Provider 中所有的 Tileset 的集合
@Override
public Tileset<ImageMetaData, ImageTileInfo>[] getTilesets() {
Collection<NationalCacheStandardTileset> tilesets = this.tilesets.values();
int size = tilesets.size();
NationalCacheStandardTileset[] tilesetArray = new NationalCacheStandardTileset[size];
tilesets.toArray(tilesetArray);
return tilesetArray;
}
4.根据切片集名字获取一个切片集
@Override
public Tileset<ImageMetaData, ImageTileInfo> getTileset(String name) {
return tilesets.get(name);
}
5.刷新操作,会把文件夹下未加载进来的 Tileset 加载
@Override
public void refresh() {
if (!connected.get()) {
return;
}
try {
lock.lock();
File[] files = outputFile.listFiles();
for (File file : files) {
if (file.isDirectory()) {
NationalCacheStandardTileset tileset = new NationalCacheStandardTileset(file);
if (!tilesets.containsKey(tileset.getName())) {
tilesets.put(tileset.getName(), tileset);
}
}
}
} finally {
lock.unlock();
}
}
6.断开连接,清理掉所有切片集
@Override
protected boolean doDisConnect() {
this.tilesets.clear();
this.outputFile = null;
return true;
}
这块的内容可以直接粘贴,没有需要修改的地方。至此我们就已经扩展好了一个自定义的分布式切图瓦片格式。下面我们要把它配置到iServer中。编译的需要注意jdk版本不得低于1.6。
- 配置切片格式
配置瓦片存储
1.将示范代码编译后导出为 JAR 包,并把 JAR 文件复制到%SuperMap iServer_HOME%/webapps/iserver/WEB-INF/lib 目录
2.打开系统配置文件 iserver-system.xml(位于%SuperMap iServer_HOME%/webapps/iserver/WEB-INF/config) ,在节点下添加存储 ID(可自行命名,本例为 NationalCacheSample1)和瓦片存储位置的配置,示例:
<storages>
<storage>
<id>NationalCacheSample1</id>
<tileSourceInfo class="com.supermap.services.tilesource.NationalCacheStandardTileSourceInfo">
<outputPath>F:/tempcache>/outputPath>
</tileSourceInfo>
</storage>
</storages>
3.重启 iserver 服务
使用扩展的瓦片格式
访问 http://localhost:8090/iserver/manager/tileservice/jobs 页面,创建切图任务时即可使用自定义的瓦片格式。
除常规操作外,需要注意:
• 存储类型需选择“UserDefined”,所有扩展的瓦片格式,其类型都是“UserDefined”
• 存储 ID 需选择配置信息中定义的 id,本例为“NationalCacheSample1”
• 比例尺方案选择“自定义”,然后输入规范中支持的比例尺。如果输入的比例尺不是规范中的标准比例尺,创建切图任务后系统会报错。规范中支持的比例尺集合:[295829355.45, 147914677.73, 73957338.86, 36978669.43, 18489334.72, 9244667.36, 4622333.68, 2311166.84, 1155583.42, 577791.71, 288895.85, 144447.93, 72223.96, 36111.98, 18055.99, 9028.00, 4514.00, 2257.00, 1128.50, 564.25]
现在我们已经完成了切片格式的扩展而且已经部署到了iServer中,使用iServer就可以生成我们自定义格式的瓦片格式了。有没有觉得有一点小激动,但好戏才刚刚开始,只是能切图并不能满足需求,我们要将切好的瓦片发布成iServer的rest服务,这样才能够在客户端被浏览。
- 扩展TilesetMapProvider发布切片
要想发布服务,就一定离不开服务提供者,根据数据来源和服务类型我们扩展TilesetMapProvider来发布我们切好的自定义瓦片格式。扩展形式如下:
public class NationalCacheStandardMapProvider extends TilesetMapProvider {}
扩展时需要实现 TilesetMapProvider类中未实现的方法,具体包括:
字段/方法 | 描述 |
---|---|
protected List initTilesets() | 获取切片集合 |
@Override
protected List<ImageTileset> initTilesets() {
NationalCacheStandardMapProviderSetting mapSetting = (NationalCacheStandardMapProviderSetting) this.getMapProviderSetting();
NationalCacheStandardTileSourceInfo info = new NationalCacheStandardTileSourceInfo().outputPath(mapSetting.outputPath);
TileSource<NationalCacheStandardTileSourceInfo> tilesource = TileSourceContainer.getInstance().get(info, this);
if (tilesource == null) {
throw new IllegalStateException(" open fail");
}
List<ImageTileset> tilesetList = new ArrayList<ImageTileset>();
Tileset<?, ?>[] tilesetArray = tilesource.getTilesets();
if (tilesetArray == null) {
return tilesetList;
}
for (int i = 0; i < tilesetArray.length; i++) {
ImageTileset temp = (ImageTileset) tilesetArray[i];
tilesetList.add(temp);
}
return tilesetList;
}
代码里面用到了NationalCacheStandardMapProviderSetting类,这个类Provider的配置类,咱们实现的功能参数少,只要一个output即可。
public class NationalCacheStandardMapProviderSetting extends MapProviderSetting {
public String outputPath;
}
然后我们来解释一下,扩展到MapProvider里面都做了写什么:
1.通过NationalCacheStandardMapProviderSetting得到了output地址,也就是瓦片的存储路径。
2.通过瓦片存储路径,我们初始化了NationalCacheStandardTileSourceInfo对象,也就是我们之前扩展的切片格式原信息类,从而得到了TileSource对象。
3.调用getTilesets(),我们就得到了瓦片集合。
代码看起来很简单,简单几句代码就完成了MapProvider的扩展,我当时也觉得大功告成,随即修改的配置文件,发布了rest-map服务,但是结果却失败了,并没有顺利的看到地图。我才意识到我忘记了一个很重要的信息。
我们刚扩展的MapProvider,确实已经顺利的读取到了瓦片,但是仅仅有瓦片规则我们是无法出图的,我们需要比例尺、地图范围、瓦片大小、坐标系、出图原点等信息,才能使iServer计算出现在要显示的瓦片。我们来修改一下NationalCacheStandardTileset切片集,我们要修改它的构造函数,添加上我们需要的地图信息。
public NationalCacheStandardTileset(File cacheFile) {
if (!cacheFile.exists()) {
cacheFile.mkdirs();
}
if (!cacheFile.isDirectory()) {
throw new IllegalArgumentException("cacheFile is not a directory!");
}
this.cacheFile = cacheFile;
this.cacheName = cacheFile.getName();
double[] scaleDenominators = new double[] { 295829355.45, 147914677.73, 73957338.86, 36978669.43, 18489334.72, 9244667.36, 4622333.68, 2311166.84, 1155583.42, 577791.71, 288895.85, 144447.93, 72223.96, 36111.98, 18055.99, 9028.00, 4514.00, 2257.00 };
ImageMetaData metaData = new ImageMetaData().tileWidth(256).tileHeight(256).bounds(new Rectangle2D(-180, -90, 180, 90)).tileFormat(OutputFormat.PNG).scaleDenominators(scaleDenominators).resolutions(toResolutions(scaleDenominators)).mapName(this.cacheName).transparent(false);
metaData.originalPoint = new Point2D(-180, 90);
metaData.prjCoordSys = PrjCoordSysConversionTool.getPrjCoordSys(4326);
his.setMetaData(metaData);
}
代码中我们将比例尺转换成了分辨率,再添加下面的转换方法。DPI为96。
private double[] toResolutions(double[] scaleDenominators) {
double[] reses = new double[scaleDenominators.length];
for (int i = 0; i < scaleDenominators.length; i++) {
double scale = 1.0 / scaleDenominators[i];
reses[i] = TileTool.scaleToResolution(scale, DPI, Unit.DEGREE);
}
return reses;
}
代码就全部完成了,最后一步,发布服务。
- 配置发布地图服务
1. 打开系统配置文件 iserver-services.xml(位于%SuperMap iServer_HOME%/webapps/iserver/WEB-INF/config) ,在< providers>节点下,添加扩展的NationalCacheStandardMapProvider,示例:
<provider class="com.supermap.services.providers.NationalCacheStandardMapProvider" enabled="true" name="nationalcache-provider">
<config class="com.supermap.services.providers.NationalCacheStandardMapProviderSetting">
<outputPath>F:/tempcache></outputPath>
</config>
</provider>
2.打开系统配置文件 iserver-system.xml,在< components>节点下添加地图服务组件,示例:
<component class="com.supermap.services.components.impl.MapImpl" enabled="true" interfaceNames="rest" name="map-nationalcache" providers=" nationalcache-provider">
<config class="com.supermap.services.components.MapConfig">
<useCache>false</useCache>
<useUTFGridCache>false</useUTFGridCache>
<useVectorTileCache>false</useVectorTileCache>
<expired>0</expired>
<cacheReadOnly>false</cacheReadOnly>
</config>
</component>
现在我们终于可以启动iServer打开服务管理页面去查看我们的地图了。希望大家也能成功的实现此功能。扩展切片格式的代码可以在iServer自带的samplecode中找到,名为ExtendTileSourceProvider。