青未了分享 http://blog.sciencenet.cn/u/yanghang

博文

[转载]瓦片地图

已有 13321 次阅读 2019-2-24 10:28 |个人分类:科研笔记|系统分类:科研笔记|关键词:学者| 瓦片, 图像处理 |文章来源:转载

一、定义

瓦片地图金字塔模型是一种多分辨率层次模型,从瓦片金字塔的底层到顶层,分辨率越来越低,但表示的地理范围不变。首先确定地图服务平台所要提供的缩放级别的数量N,把缩放级别最高、地图比例尺最大的地图图片作为金字塔的底层,即第0层,并对其进行分块,从地图图片的左上角开始,从左至右、从上到下进行切割,分割成相同大小(比如256x256像素)的正方形地图瓦片,形成第0层瓦片矩阵;在第0层地图图片的基础上,按每2x2像素合成为一个像素的方法生成第1层地图图片,并对其进行分块,分割成与下一层相同大小的正方形地图瓦片,形成第1层瓦片矩阵;采用同样的方法生成第2层瓦片矩阵;…;如此下去,直到第N一1层,构成整个瓦片金字塔。

二、原理

1.经纬度:

地球是一个椭球,Datum是一组用于描述这个椭球的数据集合。最常用的一个Datum是WGS84(World Geodetic System 1984),它的主要参数有:

坐标系的原点是地球质心(center of mass);

子午线(meridian),即零度经线,位于格林威治子午线Royal Observatory所在纬度往东102.5米所对应的的经线圈;

椭球截面长轴为a=6378137米;

椭圆截面短轴为b=6356752.3142米,可选参数;

扁平比例(flattening)f=(a−b)/a=1/298.257223563;

geoid,海平面,用于定义高度,本文从略。

通过以上参数设定,我们才能对地球上的任意一个位置用经度、纬度、高度三个变量进行描述。所以当我们获取一组经纬度信息时,首先要弄明白这组信息对应的Datum。

2.投影

地图是显示在平面上的,因此需要将球面坐标转换为平面坐标,这个转换过程称为投影。最常见的投影是墨卡托(Mercator)投影,它具有等角性质,即球体上的两点之间的角度方位与平面上的两点之间的角度方位保持不变,因此特别适合用于导航。

Web墨卡托投影(又称球体墨卡托投影)是墨卡托投影的变种,它接收的输入是Datum为WGS84的经纬度,但在投影时不再把地球当做椭球而当做半径为6378137米的标准球体,以简化计算。

Web墨卡托投影有两个相关的投影标准,经常搞混:

  • EPSG4326:Web墨卡托投影后的平面地图,但仍然使用WGS84的经度、纬度表示坐标;

  • EPSG3857:Web墨卡托投影后的平面地图,坐标单位为米。

3.瓦片

经过Web墨卡托投影后,地图就变为平面的一张地图。考虑到有时候我们需要看宏观的地图信息(如世界地图里每个国家的国界),有时候又要看很微观的地图信息(如导航时道路的路况信息)。为此,我们对这张地图进行等级切分。在最高级(zoom=0),需要的信息最少,只需保留最重要的宏观信息,因此用一张256x256像素的图片表示即可;在下一级(zoom=1),信息量变多,用一张512x512像素的图片表示;以此类推,级别越低的像素越高,下一级的像素是当前级的4倍。这样从最高层级往下到最低层级就形成了一个金字塔坐标体系。

对每张图片,我们将其切分为256x256的图片,称为瓦片(Tile)。这样,在最高级(zoom=0)时,只有一个瓦片;在下一级(zoom=1)时有4个瓦片;在下一级(zoom=2)时有16个瓦片,以此类推。

4.瓦片编号

瓦片生成后,就是一堆图片。怎么对这堆图片进行编号,是目前主流互联网地图商分歧最大的地方。总结起来分为四个流派:

  • 谷歌XYZ:Z表示缩放层级,Z=zoom;XY的原点在左上角,X从左向右,Y从上向下。

  • TMS:开源产品的标准,Z的定义与谷歌相同;XY的原点在左下角,X从左向右,Y从下向上。

  • QuadTree:微软Bing地图使用的编码规范,Z的定义与谷歌相同,同一层级的瓦片不用XY两个维度表示,而只用一个整数表示,该整数服从四叉树编码规则

  • 百度XYZ:Z从1开始,在最高级就把地图分为四块瓦片;XY的原点在经度为0纬度位0的位置,X从左向右,Y从下向上。

下表总结了中国主要地图商的瓦片编号流派,点击每个链接就可以获得一个对应编号的瓦片地图:

地图商

瓦片编码

图层

链接

高德地图

谷歌XYZ

道路

http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x=105&y=48&z=7

高德地图

谷歌XYZ

卫星

http://webst04.is.autonavi.com/appmaptile?style=6&x=843&y=388&z=10

谷歌地图

谷歌XYZ

道路

http://mt2.google.cn/vt/lyrs=m&hl=zh-CN&gl=cn&x=105&y=48&z=7

谷歌地图

谷歌XYZ

卫星

http://mt2.google.cn/vt/lyrs=s&hl=zh-CN&gl=cn&x=105&y=48&z=7

谷歌地图

谷歌XYZ

地形

http://mt0.google.cn/vt/lyrs=t&hl=zh-CN&gl=cn&x=420&y=193&z=9

OpenStreetMap

谷歌XYZ

道路

http://a.tile.openstreetmap.org/7/105/48.png

腾讯地图

TMS

道路

http://rt1.map.gtimg.com/realtimerender?z=7&x=105&y=79&type=vector&style=0

Bing地图

QuadTree

道路

http://r1.tiles.ditu.live.com/tiles/r1321001.png?g=100&mkt=zh-cn

百度地图

百度XYZ

道路

http://online4.map.bdimg.com/tile/?qt=tile&x=98&y=36&z=9&;styles=pl&scaler=1&udt=20170406

百度地图

百度XYZ

交通

http://its.map.baidu.com:8002/traffic/TrafficTileService?level=19&x=99052&y=20189&time=1373790856265&label=web2D&;v=017

三、瓦片矢量数据格式

地图经过切割后形成了瓦片,每一个编号对于一个瓦片,瓦片可以是栅格数据也可以是矢量数据,栅格数据就是一张正方形图片,渲染时只需要将这张图片作为纹理贴到指定位置就行可以,栅格瓦片的缺点就是数据量大,缩放时失真,不能自由配置显示样式。这些缺点在矢量瓦片中都得以解决,矢量瓦片中使用矢量数据描述地图元素,通过渲染这些矢量数据形成地图,数据量很小,缩放的时候不会失真,可以在不同视角展示,可以展示更丰富的高度信息,例如可以拔高建筑物。

1,瓦片数据格式设计原则

瓦片数据在地图渲染的流程中包含:下载,解析,生成渲染Mesh数据,在所有,一般对瓦片数据格式的要求是,体积小,解析快,可以配合不同的样式生成不同的效果。

2,瓦片中的Feature

2.1 Feature

Feature是几何图形数据和属性的合集。例如点Feature 包含 x ,y 坐标,和name等属性。下面是Feature类型对应描述的地图元素。

Feature常见地图元素
POI
线路网,边界,水系骨架
区域,海洋,绿地,建筑物
多点点云,热力图
多线多线相同属性的线
多面多个相同属性的面
3D模型地图上的3D元素,例如建筑物模型

2.2 数据模型

      图形数据和特性的属性构成了数据模型,例如建筑物的数据模型包含一个多边形代表俯视轮廓,属性中包含名称,高度,类型(商业还是住宅等)。不同地图使用的数据模型不同,包含的信息丰富程度也不相同,但是基本的信息都很雷同,下面是常见的一些数据模型:

地图元素模型信息
POI点数据,文本,重要度(用于POI碰撞),商标,类型
公路线数据,名称,道路等级,方向性,类型
铁路线数据,名称,类型
水面名称
绿地名称
建筑物名称,高度
边界线类型

2.3 数据与样式

在渲染实现中,通常是先不区分实际地图元素,而是实现对每种类型Feature的渲染,然后通过不同的配置实现不同的现实效果。例如,公路,铁路和边界线都是线元素,只要实现线的渲染,在线的渲染实现中,线的颜色,宽度,实虚线都可以配置,这样我们通过不同的配置就可以实现各种公路,铁路,和边界线的渲染。这些配置就是样式。数据模型中属性需要能关联的一个特性的样式,这样我们就能通过一组样式配置来配置整个地图了。不同的样式组合就渲染出不同的地图风格,例如百度地图中的普通地图和旅游地图。

详细细节请阅读:https://www.cnblogs.com/hsljyyy/p/8496267.html

四、瓦片时空数据库——切片数据读取一(ArcGIS)

在栅格切片方面,目前主要的还是GeoServer和ArcGIS两大阵营。本小节主要讲基于ArcGIS的切片数据格式。

1 概述

 目前ArcGIS的切片格式目前主要可以分为三种形式,松散型切片格式,紧凑型切片格式和紧凑型2.0数据格式。

2 松散型切片格式

 第一个比例尺的文件夹名为L00,第二个比例尺的为L01,如此类推。比例尺文件夹(以下统称L文件夹)目录下还会有R开头的文件夹,R表示的ROW,当前比例尺的瓦片每一行对应一个文件夹。R文件夹的命名方式是瓦片的行序列(用rIndex表示),把rIndex转为8位16进制,不足的在左边补0,用代码公式表示FolderName = “R”+rIndex.ToString(“x”).PadLeft(8, ‘0’) 。R文件夹里面保存的就是瓦片,瓦片的命名方式跟R文件夹的命名方式相似,以字母C开头,后面是瓦片在该行的列序号(用cIndex)表示,后面依然是一个8位16进制FileName = “C”+rIndex.ToString(“x”).PadLeft(8, ‘0’)+ “.”+format.ToString()。

3 紧凑型切片格式

 紧凑型切片格式(暂且把它叫做1.0格式),在切片方案文件conf.xml中,对应节点标识如下:

    <CacheStorageInfo xsi:type="typens:CacheStorageInfo">

    <StorageFormat>esriMapCacheStorageModeCompact</StorageFormat>

    <PacketSize>128</PacketSize>

    </CacheStorageInfo>

包含了两类文件,bundle和bundlex,其中bundle文件中存储的是图片文件,bundlx文件中则存储了各个图片文件在bundle文件中的偏移量。

4 紧凑型切片2.0格式

 紧凑型切片2.0格式,在切片方案文件conf.xml中,对应节点标识如下:


  <CacheStorageInfo xsi:type="typens:CacheStorageInfo">

    <StorageFormat>esriMapCacheStorageModeCompactV2</StorageFormat>

    <PacketSize>128</PacketSize>

</CacheStorageInfo>

相比较与1.0的格式,缓存文件夹下仅包含了bundle文件。

以下代码为读取松散型数据格式和紧凑型1.0数据格式的,2.0格式的读取方式后面更新。


    class GetTileBytesHandle

    {

        /// <summary>

        /// 从本地切片文件读取相应的层行列对应的切片

        /// </summary>

        /// <param name="level"></param>

        /// <param name="row"></param>

        /// <param name="column"></param>

        /// <returns></returns>

        public byte[] GetTileBytesExplores(int level, int row, int column)

        {

            try

            {

                string bundlesDir = input_path + "\\_alllayers";

                string l = "0" + level;

                int lLength = l.Length;

                if (lLength > 2)

                {

                    l = l.Substring(lLength - 2);

                }

                l = "L" + l;

                //int rGroup = size * (row / size);

                string r = "0000000" + row.ToString("X");

                int rLength = r.Length;

                if (rLength > 8)

                {

                    r = r.Substring(rLength - 8);

                }

                r = "R" + r;

                //int cGroup = size * (column / size);

                string c = "0000000" + column.ToString("X");

                int cLength = c.Length;

                if (cLength > 8)

                {

                    c = c.Substring(cLength - 8);

                }

                c = "C" + c;

                string bundleBase = bundlesDir + "\\" + l + "\\" + r + "\\" + c + ".png";

                if (File.Exists(bundleBase))

                {

                    return GetFileStream(bundleBase);

                }

                else

                    return null;

            }

            catch (System.Exception ex)

            {

                return null;

            }

        }

        /// <summary>

        /// 从本地切片文件读取相应的层行列对应的切片

        /// </summary>

        /// <param name="level"></param>

        /// <param name="row"></param>

        /// <param name="column"></param>

        /// <returns></returns>

        public byte[] GetTileBytesDot3(int mLevel, int mRow, int mColumn)

        {

            int size = 128;

            //byte[] result = null;

            byte[] tileBytes = null;

            FileStream isBundle = null;

            string bundlesDir = input_path + "\\_alllayers";

            try

            {

                string level = mLevel.ToString();

                int levelLength = level.Length;

                if (levelLength == 1)

                {

                    level = "0" + level;

                }

                level = "L" + level;

                int rowGroup = 128 * (mRow / 128);

                string row = rowGroup.ToString("X");

                int rowLength = row.Length;

                if (rowLength < 4)

                {

                    for (int i = 0; i < 4 - rowLength; i++)

                    {

                        row = "0" + row;

                    }

                }

                row = "R" + row;

                int columnGroup = 128 * (mColumn / 128);

                string column = columnGroup.ToString("X");

                int columnLength = column.Length;

                if (columnLength < 4)

                {

                    for (int i = 0; i < 4 - columnLength; i++)

                    {

                        column = "0" + column;

                    }

                }

                column = "C" + column;

                //string bundleName = string.Format("%S/%S/%S%S", bundlesDir, level, row, column) + ".bundle";

                string bundleName = bundlesDir + "\\" + level + "\\" + row + column + ".bundle";

                int index = 128 * (mRow - rowGroup) + (mColumn - columnGroup);

                //isBundle = new RandomAccessFile(bundleName, "r");

                //isBundle.skipBytes(64 + 8*index);

                if (!File.Exists(bundleName) )

                    return null;

                isBundle = new FileStream(bundleName, FileMode.Open, FileAccess.Read);

                isBundle.Seek(64 + 8 * index, SeekOrigin.Begin);

                //获取位置索引并计算切片位置偏移量

                byte[] indexBytes = new byte[4];

                isBundle.Read(indexBytes, 0, 4);

                long offset = (long)(indexBytes[0] & 0xff) + (long)(indexBytes[1] & 0xff) * 256 + (long)(indexBytes[2] & 0xff) * 65536

                                + (long)(indexBytes[3] & 0xff) * 16777216;

                //获取切片长度索引并计算切片长度

                long startOffset = offset - 4;

                isBundle.Seek(startOffset, SeekOrigin.Begin);

                byte[] lengthBytes = new byte[4];

                isBundle.Read(lengthBytes, 0, 4);

                int length = (int)(lengthBytes[0] & 0xff) + (int)(lengthBytes[1] & 0xff) * 256 + (int)(lengthBytes[2] & 0xff) * 65536

                            + (int)(lengthBytes[3] & 0xff) * 16777216;

                //根据切片位置和切片长度获取切片

                // ByteArrayOutputStream bos = new ByteArrayOutputStream();

                tileBytes = new byte[length];

                int bytesRead = 0;

                if (length > 4)

                {

                    bytesRead = isBundle.Read(tileBytes, 0, tileBytes.Length);

                }

                else

                {

                    tileBytes = null;

                }

                //tile = bos.toByteArray();

            }

            catch (Exception ex)

            {

                return null;

            }

            finally

            {

                if (isBundle != null)

                {

                    isBundle.Close();

                }

            }

            return tileBytes;

        }

        /// <summary>

        /// 从本地切片文件读取相应的层行列对应的切片

        /// </summary>

        /// <param name="level"></param>

        /// <param name="row"></param>

        /// <param name="column"></param>

        /// <returns></returns>

        public byte[] GetTileBytes(int level, int row, int column)

        {

            int size = 128;

            byte[] result = null;

            FileStream isBundle = null;

            FileStream isBundlx = null;

            try

            {

                string bundlesDir = input_path + "\\_alllayers";

                string l = "0" + level;

                int lLength = l.Length;

                if (lLength > 2)

                {

                    l = l.Substring(lLength - 2);

                }

                l = "L" + l;

                int rGroup = size * (row / size);

                string rGroupString = rGroup.ToString("X");

                string r;

                if (rGroupString.Length <= 4)

                {

                    r = "000" + rGroup.ToString("X");

                    int rLength = r.Length;


                    if (rLength > 4)

                    {

                        r = r.Substring(rLength - 4);

                    }

                }

                else

                {

                    r = rGroupString;

                }

                r = "R" + r;

                int cGroup = size * (column / size);

                string cGroupString = cGroup.ToString("X");

                string c;

                if (cGroupString.Length <= 4)

                {

                    c = "000" + cGroup.ToString("X");

                    int rLength = c.Length;


                    if (rLength > 4)

                    {

                        c = c.Substring(rLength - 4);

                    }

                }

                else

                {

                    c = cGroupString;

                }

                c = "C" + c;

                string bundleBase = bundlesDir + "\\" + l + "\\" + r + c;

                string bundlxFileName = bundleBase + ".bundlx";

                string bundleFileName = bundleBase + ".bundle";

                if (!File.Exists(bundlxFileName) || !File.Exists(bundleFileName))

                    return null;

                int index = size * (column - cGroup) + (row - rGroup);

                //行列号是整个范围内的,在某个文件中需要先减去前面文件所占有的行列号(都是128的整数)这样就得到在文件中的真是行列号

                isBundlx = new FileStream(bundlxFileName, FileMode.Open, FileAccess.Read);

                isBundlx.Seek(16 + 5 * index, SeekOrigin.Begin);

                byte[] buffer = new byte[5];

                isBundlx.Read(buffer, 0, 5);

                long offset = (long)(buffer[0] & 0xff) + (long)(buffer[1] & 0xff) * 256 +

                              (long)(buffer[2] & 0xff) * 65536 + (long)(buffer[3] & 0xff) * 16777216 +

                              (long)(buffer[4] & 0xff) * 4294967296L;

                isBundle = new FileStream(bundleFileName, FileMode.Open, FileAccess.Read);

                isBundle.Seek(offset, SeekOrigin.Begin);

                byte[] lengthBytes = new byte[4];

                isBundle.Read(lengthBytes, 0, 4);

                int length = (int)(lengthBytes[0] & 0xff)

                           + (int)(lengthBytes[1] & 0xff) * 256

                           + (int)(lengthBytes[2] & 0xff) * 65536

                           + (int)(lengthBytes[3] & 0xff) * 16777216;

                result = new byte[length];

                isBundle.Read(result, 0, length);

            }

            catch (Exception ex)

            {

                return null;

            }

            finally

            {

                if (isBundle != null)

                {

                    isBundle.Close();

                    isBundlx.Close();

                }

            }

            return result;

        }

    }

五、瓦片时空数据库——切片数据读取一(GeoWebCache)

1 概述

    在GeoServer阵营中,可以选择UDig和QGIS进行数据的符号化。并将符号化后的sld符号文件导入到GeoServer中,进行数据发布。切片默认路径在“GeoServer<版本号>\data_dir\gwc\”文件夹中。可以在“GeoServer <版本号>\webapps\geoserver\WEB-INF\web.xml”文件中,重新配置缓存地址路径。例如:

<context-param>

   <param-name>GEOWEBCACHE_CACHE_DIR</param-name>

   <param-value>C:\temp</param-value>

</context-param>

    其中,GeoWebCache分别在<data_dir> / gwc-layers /目录中存储了每个GeoServer切片图层的配置。每个切片层都有一个XML文件。这些文件包含与独立版本中的语法不同的语法。可以在“Tile Layers”页面或通过GeoWebCache REST API配置切片图层。

    数据发布完成后,点击首页“Tile Layers”,点击切图的图层组后面“Seed/Truncate”,点击“submit”即可开始切图,点击“Refresh list”可刷新查看切片状态。 数据将存放在配置的切片路径下。


2 数据存储格式

    历史上,GeoWebCache有三种存储方式,负责tile和tile元数据处理:blob存储,Metastore和磁盘配额子系统。

1)blobstore是tile的存储机制,其默认实现基于文件系统。

2)Metastore是一个可选的基于H2的存储机制,用于关于切片的元信息,例如切片创建时间,大小和请求参数的使用。

3)磁盘配额机制使用nosql嵌入式数据库来跟踪切片磁盘的使用情况,并根据用户设置的策略使切片到期。

从GeoWebCache 1.4.0开始,Metastore被基于完整文件系统的解决方案所取代,使得blobstore负责先前由Metastore跟踪的信息。默认情况下,这两个存储的存储位置是servlet容器指定的临时存储目录(将在那里创建名为geowebcache的目录)。如果此目录不可用,GeoWebCache将尝试在TEMP环境变量指定的位置创建新目录。内部将有一个磁盘配额目录(默认情况下称为diskquota_page_store),以及以每个缓存层命名的blobstore目录(例如topp_states用于图层topp:states)。

2.1 松散型切片数据格式

    在GeoServer中,使用GeoWebCache默认生成的数据格式为松散型切片文件

2.2 文件Blob(persistent storage mechanisms)存储方式

    除了松散型切片数据格式以外,GeoWebCache还提供了一种永久性的blob存储机制,即“blobstore”。“blobstore”是一种软件组件,它提供了在给定存储机制中存储和检索切片的操作。

从版本1.8.0开始,tile有两种类型的BlobStore存储机制:

1)文件blob存储:将tile存储在目录结构中,该目录结构由按层和缩放级别组织的各种图像文件组成。

2)S3 blob store:将磁贴存储在Amazon Simple Storage Service存储桶中,作为遵循类似TMS的密钥结构的单个“对象”。

可以在配置文件中配置零个或多个blobstore,以在不同位置和不同存储后端存储切片。 其中一个已配置的blobstores将是默认值。 这意味着它将用于存储其配置未明确指示应使用哪个blobstore的每个层的切片。

2.3 MBTiles Blob存储方式

    这个blob存储允许我们使用MBTiles规范(版本    1.1)存储切片,该规范定义了一个用于在SQLite数据库中存储切片的模式,其中包含有关切片格式和投影的一些限制。MBTiles规范仅支持JPEG和PNG格式,并且假设EPSG:3857。实现的blob存储将读取和写入符合规范的MBTiles文件,但也能够写入和读取使用其他格式和投影的MBTiles文件。使用MBTiles blob存储将带来一些好处,但代价是性能损失。 MBTiles存储使用的文件数量明显减少,从而可以更轻松地处理数据(例如,备份,在环境之间移动切片)。在某些情况下,存储的数据将更紧凑,减少了磁盘上数据的大小。与文件blob存储区相比,此存储有两个限制:

1)此存储未与磁盘配额集成,这是使用数据库文件的结果。

2)无法在多个GeoWebCache实例之间共享此存储。

MBTiles文件对应于SQLite数据库文件。 为了限制每个单个数据库文件的争用量,系统允许用户决定数据库文件的粒度。 当GeoWebCache需要将切片映射到数据库文件时,它只会检索数据库文件路径,而不会考虑MBTiles元数据(这就是为什么这个存储能够处理其他格式和投影)。

    基于默认的文件模板将属于某个图层的所有图块存储在单个文件夹中,该文件夹将包含每个给定格式,投影和参数集的子文件夹,并将在SQLite中对具有相同缩放级别,列范围和行范围的图块进行分组。 列和行范围值通过配置传递,默认情况下为250。提供的文件路径模板将始终被视为相对于作为配置选项提供的根目录。

下面是使用默认路径模板时blob存储根目录结构的示例:


.

|-- nurc_Pk50095

|   `-- EPSG_4326image_pngnull

|       |-- 11_2000_1500.sqlite

|       `-- 12_4250_3000.sqlite

`-- topp_states

    |-- EPSG_900913image_jpeg7510004a12f49fdd49a2ba366e9c4594be7e4358

    |   |-- 6_250_500.sqlite

    |   `-- 7_0_0.sqlite

    `-- EPSG_900913image_jpegnull

        |-- 3_500_0.sqlite

        |-- 4_0_250.sqlite

        `-- 8_750_500.sqlite

如果没有提供参数,将使用空字符串。 最好定义避免冲突的文件路径模板。可以在文件路径模板中使用的变量名称包括:

grid:网格集id

layer:图层的名称

format:图块的图像格式

params:参数唯一哈希值

x:列范围,基于列范围计数配置属性计算

y:行范围,基于行范围计数配置属性计算

z:缩放级别

有效的MBTiles文件同时也需要元数据信息,在创建MBTiles文件时将,系统会自动添加图像格式和图层名称。

元数据模板如下:

<layerName>.metadata








https://m.sciencenet.cn/blog-346157-1163878.html

上一篇:IDL直接图形法
下一篇:[转载]IDL对象图形法

0

该博文允许注册用户评论 请点击登录 评论 (0 个评论)

数据加载中...

Archiver|手机版|科学网 ( 京ICP备07017567号-12 )

GMT+8, 2024-5-30 08:45

Powered by ScienceNet.cn

Copyright © 2007- 中国科学报社

返回顶部