├── .gitignore
├── LICENSE
├── README.md
├── README_zh.md
├── examples
├── README.md
├── preview-example.yml
├── preview-style.json
├── preview.html
└── preview.png
├── pom.xml
├── scripts
└── SimpleWebServer.sh
└── src
├── main
└── scala
│ └── org
│ ├── apache
│ └── spark
│ │ └── sql
│ │ ├── GeometricUtils.scala
│ │ └── SQLGeometricExtensions.scala
│ └── ieee
│ └── codemeow
│ └── geometric
│ ├── CRSUtils.scala
│ ├── Feature.scala
│ ├── GeometricUtils.scala
│ ├── MvtBuilder.scala
│ └── spark
│ ├── Configurations.scala
│ ├── VectorTileTask.scala
│ └── data
│ ├── AbstractDataProvider.scala
│ └── SQLDataProvider.scala
└── test
└── scala
└── samples
├── junit.scala
├── scalatest.scala
└── specs.scala
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | dependency-reduced-pom.xml
3 | .idea/
4 | target/
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Alexander.Zhou
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vector Tile Spark Process
2 |
3 | The ***Vector Tile Spark Process*** allows developers and data scientists clip geographic data into Hadoop SequeueFiles on Spark platform.
4 |
5 | The effect comes from [Preview Example](https://github.com/codemeow5/Vector-Tile-Spark-Process/tree/master/examples).
6 | 
7 |
8 | ## Features
9 |
10 | - Parallel processing based on Apache Spark
11 | - Adapting various data sources
12 | - Output standard [Mapbox Vector Tiles](https://github.com/mapbox/vector-tile-spec/tree/master/2.1 "Vector Tile Specification") format
13 | - A configuration file similar to [TileStache.Goodies.VecTiles.Provider](https://github.com/TileStache/TileStache/blob/master/TileStache/Goodies/VecTiles/server.py)
14 |
15 | ## Dependencies
16 |
17 | - [GeoTools](http://www.geotools.org/ "GeoTools") - an open source Java library that provides tools for geospatial data
18 | - [mapbox-vector-tile-java](https://github.com/wdtinc/mapbox-vector-tile-java "mapbox-vector-tile-java") - Java Mapbox Vector Tile Library for Encoding/Decoding
19 |
20 | ## Requirements
21 |
22 | - Hadoop 2.7 and later
23 | - Spark 2.1.1 and above
24 | - Protocol Buffers 3.0.0-beta-2
25 |
26 | ## Getting Started
27 |
28 | ### Build
29 |
30 | $ mvn clean && mvn package
31 |
32 | ### Run
33 |
34 | $SPARK_HOME/bin/spark-submit --class org.ieee.codemeow.geometric.spark.VectorTileTask --master yarn --deploy-mode cluster --jars /path/to/postgresql-42.0.0.jar --driver-class-path /path/to/postgresql-42.0.0.jar /path/to/vectortile-spark-process-1.0-SNAPSHOT.jar hdfs:///path/to/vectortile-spark-process.yml
35 |
36 | ## Configuration File
37 |
38 | ---
39 | # vectortile-spark-process.yml
40 |
41 | appName: "Vector Tile Process"
42 | sequenceFileDir: "hdfs:///path/to"
43 | layers:
44 | - layerName: "layerName"
45 | minZoom: "0"
46 | maxZoom: "22"
47 | dataProvider: "org.ieee.codemeow.geometric.spark.data.SQLDataProvider"
48 | kwargs:
49 | url: "jdbc:postgresql://hostname/dbname"
50 | dbtables:
51 | planet_osm_line: "public.planet_osm_line"
52 | planet_osm_point: "public.planet_osm_point"
53 | planet_osm_polygon: "public.planet_osm_polygon"
54 | planet_osm_roads: "public.planet_osm_roads"
55 | user: "postgres"
56 | password: "postgres"
57 | zooms:
58 | 0: "SELECT osm_id AS __id__, ST_GeomFromWKB(way) AS __geometry__ FROM ..."
59 | 1: "SELECT osm_id AS __id__, ST_GeomFromWKB(way) AS __geometry__ FROM ..."
60 | ...
61 | 22: "SELECT osm_id AS __id__, ST_GeomFromWKB(way) AS __geometry__ FROM ..."
62 |
63 | ## Resources
64 |
65 | - http://tilestache.org/
66 | - https://github.com/locationtech/geomesa
67 | - http://www.geomesa.org/documentation/current/user/spark/sparksql_functions.html
68 | - https://github.com/mapbox/awesome-vector-tiles
69 | - https://github.com/DataSystemsLab/GeoSpark
70 | - https://github.com/Esri/spatial-framework-for-hadoop
71 | - https://github.com/gbif/maps
72 | - https://github.com/mapbox/mercantile
73 | - https://github.com/modestmaps/modestmaps-processing
74 | - http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/
75 |
76 | ## Issues
77 |
78 | Find a bug or want to request a new feature? Please let us know by submitting an issue.
79 |
80 | ## Tips
81 |
82 | 1. Upgrade protobuf package version on your Spark cluster
83 |
84 | >cp protobuf-java-3.0.0-beta-2.jar $SPARK_HOME/jars
85 |
86 | 2. Use SparkSQL in the ***zooms*** section of the configuration file
87 |
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 | # Vector Tile Spark Process
2 |
3 | ***Vector Tile Spark Process*** 允许开发者和数据研究员使用Spark平台将地理空间数据切分到Hadoop SequeueFiles文件中.
4 |
5 | 效果来自于我们用于[预览的例子](https://github.com/codemeow5/Vector-Tile-Spark-Process/tree/master/examples).
6 | 
7 |
8 | ## 特性
9 |
10 | - 基于Apache Spark进行并行处理
11 | - 适配多种数据源
12 | - 输出标准 [Mapbox Vector Tiles](https://github.com/mapbox/vector-tile-spec/tree/master/2.1 "Vector Tile Specification") 格式
13 | - 类似于 [TileStache.Goodies.VecTiles.Provider](https://github.com/TileStache/TileStache/blob/master/TileStache/Goodies/VecTiles/server.py)的配置文件
14 |
15 | ## 依赖项
16 |
17 | - [GeoTools](http://www.geotools.org/ "GeoTools") - 提供空间数据工具的开源Java库
18 | - [mapbox-vector-tile-java](https://github.com/wdtinc/mapbox-vector-tile-java "mapbox-vector-tile-java") - Map Vector Tile 编解码Java库
19 |
20 | ## 要求
21 |
22 | - Hadoop 2.7 及以上
23 | - Spark 2.1.1 及以上
24 | - Protocol Buffers 3.0.0-beta-2
25 |
26 | ## 开始工作
27 |
28 | ### 构建
29 |
30 | $ mvn clean && mvn package
31 |
32 | ### 运行
33 |
34 | $SPARK_HOME/bin/spark-submit --class org.ieee.codemeow.geometric.spark.VectorTileTask --master yarn --deploy-mode cluster --jars /path/to/postgresql-42.0.0.jar --driver-class-path /path/to/postgresql-42.0.0.jar /path/to/vectortile-spark-process-1.0-SNAPSHOT.jar hdfs:///path/to/vectortile-spark-process.yml
35 |
36 | ## 配置文件
37 |
38 | ---
39 | # vectortile-spark-process.yml
40 |
41 | appName: "Vector Tile Process"
42 | sequenceFileDir: "hdfs:///path/to"
43 | layers:
44 | - layerName: "layerName"
45 | minZoom: "0"
46 | maxZoom: "22"
47 | dataProvider: "org.ieee.codemeow.geometric.spark.data.SQLDataProvider"
48 | kwargs:
49 | url: "jdbc:postgresql://hostname/dbname"
50 | dbtables:
51 | planet_osm_line: "public.planet_osm_line"
52 | planet_osm_point: "public.planet_osm_point"
53 | planet_osm_polygon: "public.planet_osm_polygon"
54 | planet_osm_roads: "public.planet_osm_roads"
55 | user: "postgres"
56 | password: "postgres"
57 | zooms:
58 | 0: "SELECT osm_id AS __id__, ST_GeomFromWKB(way) AS __geometry__ FROM ..."
59 | 1: "SELECT osm_id AS __id__, ST_GeomFromWKB(way) AS __geometry__ FROM ..."
60 | ...
61 | 22: "SELECT osm_id AS __id__, ST_GeomFromWKB(way) AS __geometry__ FROM ..."
62 |
63 | ## 资源
64 |
65 | - http://tilestache.org/
66 | - https://github.com/locationtech/geomesa
67 | - http://www.geomesa.org/documentation/current/user/spark/sparksql_functions.html
68 | - https://github.com/mapbox/awesome-vector-tiles
69 | - https://github.com/DataSystemsLab/GeoSpark
70 | - https://github.com/Esri/spatial-framework-for-hadoop
71 | - https://github.com/gbif/maps
72 | - https://github.com/mapbox/mercantile
73 | - https://github.com/modestmaps/modestmaps-processing
74 | - http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/
75 |
76 | ## 问题
77 |
78 | 如果你找到一些问题或者希望我们提供一些新特性,请通过提交Issue的方式通知我们.
79 |
80 | ## 贴士
81 |
82 | 1. 升级你的Spark集群的protobuf版本
83 |
84 | >cp protobuf-java-3.0.0-beta-2.jar $SPARK_HOME/jars
85 |
86 | 2. 在配置文件的***zooms***节,请使用SparkSQL
87 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # Preview Example
2 |
3 | Processing Beijing OpenStreetMap data and Preview it
4 |
5 | 
6 |
7 | ## Download raw data and import to PostGIS database
8 |
9 | 1. Download [beijing_china.osm.pbf](https://s3.amazonaws.com/metro-extracts.mapzen.com/beijing_china.osm.pbf) from [Mapzen](https://mapzen.com/data/metro-extracts/metro/beijing_china/)
10 | 2. Install Osm2pgsql to your enviroment ([http://wiki.openstreetmap.org/wiki/Osm2pgsql#Installation](http://wiki.openstreetmap.org/wiki/Osm2pgsql#Installation))
11 | 3. Import data into PostgreSQL database
12 |
13 | $ osm2pgsql -c -d beijing\_china\_osm -U postgres -W -H [PGHOST] beijing_china.osm.pbf
14 |
15 | 4. Add column ***wkb_way***
16 |
17 | ALTER TABLE public.planet_osm_line ADD wkb_way bytea;
18 | UPDATE public.planet_osm_line SET wkb_way = ST_AsBinary(way);
19 |
20 | ## Update your configuration file
21 |
22 | ---
23 | # preview-example.yml
24 |
25 | appName: "Preview Example Task"
26 | sequenceFileDir: "hdfs:///preview_data"
27 | layers:
28 | - layerName: "lines"
29 | minZoom: "3"
30 | maxZoom: "18"
31 | dataProvider: "org.ieee.codemeow.geometric.spark.data.SQLDataProvider"
32 | kwargs:
33 | url: "jdbc:postgresql://hostname/beijing_china_osm"
34 | dbtables:
35 | planet_osm_line: "public.planet_osm_line"
36 | user: "postgres"
37 | password: "postgres"
38 | zooms:
39 | 3: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
40 | 4: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
41 | 5: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
42 | 6: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
43 | 7: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
44 | 8: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
45 | 9: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
46 | 10: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
47 | 11: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
48 | 12: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
49 | 13: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
50 | 14: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
51 | 15: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
52 | 16: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
53 | 17: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
54 | 18: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
55 |
56 | ## Run Spark Job
57 |
58 | $SPARK_HOME/bin/spark-submit --class org.ieee.codemeow.geometric.spark.VectorTileTask --master yarn --deploy-mode cluster --jars /path/to/postgresql-42.0.0.jar --driver-class-path /path/to/postgresql-42.0.0.jar /path/to/vectortile-spark-process-1.0-SNAPSHOT.jar hdfs:///path/to/preview-example.yml
59 |
60 | ## Run Simple Web Server
61 |
62 | #### Install SBT scripts runtime ([http://www.scala-sbt.org/0.13/docs/Scripts.html](http://www.scala-sbt.org/0.13/docs/Scripts.html))
63 |
64 | // Installing conscript
65 | export CONSCRIPT_HOME="$HOME/.conscript"
66 | export PATH=$CONSCRIPT_HOME/bin:$PATH
67 | wget https://raw.githubusercontent.com/foundweekends/conscript/master/setup.sh -O - | sh
68 |
69 | //Install command screpl and scalas
70 | cs sbt/sbt --branch 0.13
71 |
72 | #### Start server
73 |
74 | ./scripts/SimpleWebServer.sh hdfs://hostname:port/preview_data
75 |
76 | ## Open preview.html
77 |
78 | 1. Modify server address on ***preview-style.json***
79 | 2. Copy ***preview.html*** and ***preview-style.json*** to web root directory
80 | 3. Visit ***http://127.0.0.1/preivew.html***
81 |
82 |
83 |
--------------------------------------------------------------------------------
/examples/preview-example.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # preview-example.yml
3 |
4 | appName: "Preview Example Task"
5 | sequenceFileDir: "hdfs:///preview_data"
6 | layers:
7 | - layerName: "lines"
8 | minZoom: "3"
9 | maxZoom: "18"
10 | dataProvider: "org.ieee.codemeow.geometric.spark.data.SQLDataProvider"
11 | kwargs:
12 | url: "jdbc:postgresql://hostname/beijing_china_osm"
13 | dbtables:
14 | planet_osm_line: "public.planet_osm_line"
15 | user: "postgres"
16 | password: "postgres"
17 | zooms:
18 | 3: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
19 | 4: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
20 | 5: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
21 | 6: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
22 | 7: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
23 | 8: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
24 | 9: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
25 | 10: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
26 | 11: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
27 | 12: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
28 | 13: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
29 | 14: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
30 | 15: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
31 | 16: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
32 | 17: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
33 | 18: "SELECT osm_id AS __id__, ST_GeomFromWKB(wkb_way) AS __geometry__ FROM planet_osm_line"
34 |
--------------------------------------------------------------------------------
/examples/preview-style.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 8,
3 | "name": "Preview Style For Vector Tile Spark Process",
4 | "sources": {
5 | "simple_web_server": {
6 | "type": "vector",
7 | "tileSize": 512,
8 | "tiles": [
9 | "http://127.0.0.1:9000/lines/{z}/{x}/{y}.pbf"
10 | ],
11 | "minzoom": 3,
12 | "maxzoom": 18
13 | }
14 | },
15 | "layers": [
16 | {
17 | "id": "lines",
18 | "type": "line",
19 | "source": "simple_web_server",
20 | "source-layer": "lines",
21 | "paint": {
22 | "line-color": "#333333"
23 | }
24 | }
25 | ]
26 | }
--------------------------------------------------------------------------------
/examples/preview.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Preview - Vector Tile Spark Process
6 |
7 |
8 |
9 |
10 |
11 |
28 |
29 |
--------------------------------------------------------------------------------
/examples/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bluenoah1991/Vector-Tile-Spark-Process/2b75f0947b45c991a83c3ec72311fccc0aa7a548/examples/preview.png
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 | 4.0.0
3 | org.ieee.codemeow
4 | vectortile-spark-process
5 | 1.0-SNAPSHOT
6 | vectortile-spark-process
7 |
8 | 2017
9 |
10 |
11 | MIT License
12 | http://www.opensource.org/licenses/mit-license.php
13 | repo
14 |
15 |
16 |
17 | 1.7
18 | 1.7
19 | UTF-8
20 | 2.10
21 | 2.11.8
22 | 2.1.0
23 | 2.4.4
24 |
25 |
26 |
27 |
28 | osgeo
29 | Open Source Geospatial Foundation Repository
30 | http://download.osgeo.org/webdav/geotools/
31 |
32 |
33 |
34 |
35 |
36 | org.scala-lang
37 | scala-library
38 | ${scala.version}
39 |
40 |
41 | org.apache.spark
42 | spark-core_${scala.tools.version}
43 | ${spark.version}
44 | provided
45 |
46 |
47 | org.apache.spark
48 | spark-sql_${scala.tools.version}
49 | ${spark.version}
50 | provided
51 |
52 |
53 | org.geotools
54 | gt-api
55 | 17.0
56 |
57 |
58 | com.wdtinc
59 | mapbox-vector-tile
60 | 1.1.1
61 |
62 |
63 | com.fasterxml.jackson.dataformat
64 | jackson-dataformat-yaml
65 |
66 | ${jackson.spark.version}
67 |
68 |
69 | com.fasterxml.jackson.core
70 | jackson-annotations
71 |
72 | ${jackson.spark.version}
73 |
74 |
75 | com.fasterxml.jackson.core
76 | jackson-core
77 |
78 | ${jackson.spark.version}
79 |
80 |
81 |
82 |
83 | junit
84 | junit
85 | 4.11
86 | test
87 |
88 |
89 | org.specs2
90 | specs2_${scala.tools.version}
91 | 1.13
92 | test
93 |
94 |
95 | org.scalatest
96 | scalatest_${scala.tools.version}
97 | 2.0.M6-SNAP8
98 | test
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | net.alchim31.maven
111 | scala-maven-plugin
112 | 3.2.0
113 |
114 |
115 |
116 | add-source
117 | compile
118 |
119 |
120 |
121 |
122 | -dependencyfile
123 | ${project.build.directory}/.scala_dependencies
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | org.apache.maven.plugins
132 | maven-surefire-plugin
133 | 2.18.1
134 |
135 | false
136 | true
137 |
138 | false
139 |
140 | **/*Test.*
141 | **/*Suite.*
142 |
143 |
144 |
145 |
146 |
147 | org.apache.maven.plugins
148 | maven-shade-plugin
149 | 2.3
150 |
151 |
152 | package
153 |
154 | shade
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
--------------------------------------------------------------------------------
/scripts/SimpleWebServer.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env scalas
2 |
3 | /***
4 | scalaVersion := "2.11.8"
5 |
6 | libraryDependencies ++= Seq(
7 | "com.tumblr" %% "colossus" % "0.8.3",
8 | "org.apache.hadoop" % "hadoop-common" % "2.2.0",
9 | "org.apache.hadoop" % "hadoop-hdfs" % "2.2.0"
10 | )
11 | */
12 |
13 | import java.net.URI
14 |
15 | import colossus._
16 | import core._
17 | import service._
18 | import protocols.http._
19 | import HttpMethod._
20 | import akka.actor.ActorSystem
21 | import colossus.protocols.http.UrlParsing._
22 | import org.apache.hadoop.conf.Configuration
23 | import org.apache.hadoop.fs.{FileSystem, Path}
24 | import org.apache.hadoop.io._
25 | import org.apache.hadoop.util.ReflectionUtils
26 |
27 |
28 | // load all tiles into memory (so slow)
29 | val cache = scala.collection.mutable.Map[String, Array[Byte]]()
30 |
31 | val config = new Configuration
32 | config.set("fs.hdfs.impl", "org.apache.hadoop.hdfs.DistributedFileSystem");
33 |
34 | // Ref http://alvinalexander.com/scala/scala-shell-script-command-line-arguments-args
35 | val uri = URI.create(args(0))
36 | val fs = FileSystem.get(uri, config)
37 | val statusCollection = fs.listStatus(new Path(uri.getPath))
38 |
39 |
40 | println("Starting load all SequenceFile...")
41 |
42 | statusCollection.filter(status => {
43 | // filter _SUCCESS
44 | status.getPath.getName != "_SUCCESS"
45 | }).foreach(status => {
46 | val path = status.getPath
47 | println(s"load ${path}")
48 |
49 | val reader = new SequenceFile.Reader(config, SequenceFile.Reader.file(path))
50 | val key = ReflectionUtils.newInstance(reader.getKeyClass, config).asInstanceOf[Text]
51 | val value = ReflectionUtils.newInstance(reader.getValueClass, config).asInstanceOf[BytesWritable]
52 |
53 | while(reader.next(key, value)){
54 | val code = key.toString()
55 | // Ref http://stackoverflow.com/questions/23211493/how-to-extract-data-from-hadoop-sequence-file
56 | val bytes = value.copyBytes()
57 | cache(code) = bytes
58 | }
59 | })
60 |
61 | println("Finished loading.")
62 |
63 | // Take samples
64 |
65 | val samples = cache.take(5) ++ cache.takeRight(5)
66 |
67 | println("Take samples of Tiles")
68 | samples.zipWithIndex.foreach(t => {
69 | val tile = t._1
70 | val index = t._2
71 | val (layerName, (row, column, zoom)) = decodeTile(tile._1)
72 | println(s"${index}: Layer is ${layerName} and Row is ${row} and Column is ${column} and Zoom is ${zoom}")
73 | })
74 |
75 | def encodeTile(layerName: String, tile: (Long, Long, Long)): String ={
76 | val (row, column, zoom) = tile
77 | val code = (row % 0x1000000L) << 40 | (column % 0x1000000L) << 16 | (zoom % 0x10000L)
78 | s"${layerName}:${code}"
79 | }
80 |
81 | def decodeTile(key: String): (String, (Long, Long, Long)) ={
82 | val x = key.split(":").toSeq
83 | val layerName = x(0)
84 | val code = x(1).toLong
85 | val row = code >> 40
86 | val column = (code >> 16) % 0x1000000L
87 | val zoom = code % 0x10000L
88 | (layerName, (row, column, zoom))
89 | }
90 |
91 | class ArrayByteEncoder extends HttpBodyEncoder[Array[Byte]]{
92 | def encode(data : Array[Byte]) : HttpBody ={
93 |
94 | val contentType = HttpHeader("Content-Type", "application/vnd.mapbox-vector-tile")
95 | new HttpBody(data, Some(contentType))
96 | }
97 | }
98 |
99 | class WMSService(context: ServerContext) extends HttpService(context) {
100 |
101 | val allowOrigin = HttpHeader("Access-Control-Allow-Origin", "*")
102 | val allowMethod = HttpHeader("Access-Control-Allow-Method", "GET")
103 | val crossDomainHeaders = HttpHeaders(allowOrigin, allowMethod)
104 |
105 | def handle = {
106 | case request @ Get on Root => {
107 | Callback.successful(request.ok("Simple Web Map Server!"))
108 | }
109 |
110 | case request @ Options on Root / layerName / Long(zoom) / Long(row) / fileName => {
111 |
112 | Callback.successful(request.ok("", crossDomainHeaders))
113 | }
114 |
115 | case request @ Get on Root / layerName / Long(zoom) / Long(row) / fileName => {
116 |
117 | val index = fileName.indexOf(".pbf")
118 | if(index == -1){
119 | Callback.successful(request.badRequest[String]("Wrong path format"))
120 | } else {
121 | val column = fileName.substring(0, index).toLong
122 | val code = encodeTile(layerName,(row, column, zoom))
123 |
124 | val bytes = cache.get(code)
125 | if(bytes.isEmpty){
126 | Callback.successful(request.notFound[String](s"The requested tile parameters is (${row}, ${column}, ${zoom})"))
127 | } else {
128 | implicit val encoder = new ArrayByteEncoder
129 | Callback.successful(request.ok(bytes.get, crossDomainHeaders))
130 | }
131 | }
132 |
133 | }
134 | }
135 | }
136 |
137 | class WMSInitializer(worker: WorkerRef) extends Initializer(worker) {
138 |
139 | def onConnect = context => new WMSService(context)
140 |
141 | }
142 |
143 | implicit val actorSystem = ActorSystem()
144 | implicit val io = IOSystem()
145 |
146 | Server.start("wms", 9000){ worker => new WMSInitializer(worker) }
147 |
--------------------------------------------------------------------------------
/src/main/scala/org/apache/spark/sql/GeometricUtils.scala:
--------------------------------------------------------------------------------
1 | package org.apache.spark.sql
2 |
3 | import com.vividsolutions.jts.geom.Geometry
4 | import com.vividsolutions.jts.io.{WKBReader, WKBWriter, WKTReader, WKTWriter}
5 |
6 | /**
7 | * Created by CodeMeow on 2017/5/11.
8 | */
9 |
10 | // Ref https://github.com/locationtech/geomesa/blob/master/geomesa-utils/src/main/scala/org/locationtech/geomesa/utils/text/WKUtils.scala
11 | object WKTUtils {
12 | private val readerPool = new ThreadLocal[WKTReader]{
13 | override def initialValue() = new WKTReader
14 | }
15 |
16 | private val writePool = new ThreadLocal[WKTWriter]{
17 | override def initialValue() = new WKTWriter
18 | }
19 |
20 | def read(s: String): Geometry = readerPool.get.read(s)
21 | def write(g: Geometry): String = writePool.get.write(g)
22 | }
23 |
24 | object WKBUtils {
25 | private val readerPool = new ThreadLocal[WKBReader]{
26 | override def initialValue() = new WKBReader
27 | }
28 |
29 | private val writePool = new ThreadLocal[WKBWriter]{
30 | override def initialValue() = new WKBWriter
31 | }
32 |
33 | def read(b: Array[Byte]): Geometry = readerPool.get.read(b)
34 | def write(g: Geometry): Array[Byte] = writePool.get.write(g)
35 | }
--------------------------------------------------------------------------------
/src/main/scala/org/apache/spark/sql/SQLGeometricExtensions.scala:
--------------------------------------------------------------------------------
1 | package org.apache.spark.sql
2 |
3 | import com.vividsolutions.jts.geom._
4 | import org.apache.spark.sql.catalyst.InternalRow
5 | import org.apache.spark.sql.catalyst.expressions.GenericInternalRow
6 | import org.apache.spark.sql.types._
7 | import org.geotools.geometry.jts.JTS
8 |
9 | import scala.reflect.ClassTag
10 |
11 | /**
12 | * Created by CodeMeow on 2017/5/11.
13 | */
14 |
15 |
16 | object SQLGeometricExtensions {
17 |
18 | UDTRegistration.register(classOf[Point].getCanonicalName, classOf[PointUDT].getCanonicalName)
19 | UDTRegistration.register(classOf[MultiPoint].getCanonicalName, classOf[MultiPointUDT].getCanonicalName)
20 | UDTRegistration.register(classOf[LineString].getCanonicalName, classOf[LineStringUDT].getCanonicalName)
21 | UDTRegistration.register(classOf[MultiLineString].getCanonicalName, classOf[MultiLineStringUDT].getCanonicalName)
22 | UDTRegistration.register(classOf[Polygon].getCanonicalName, classOf[PolygonUDT].getCanonicalName)
23 | UDTRegistration.register(classOf[MultiPolygon].getCanonicalName, classOf[MultiPolygonUDT].getCanonicalName)
24 | UDTRegistration.register(classOf[Geometry].getCanonicalName, classOf[GeometryUDT].getCanonicalName)
25 |
26 | def registerExtensions(spark: SparkSession): Unit ={
27 | SQLGeometricFunctions.registerFunctions(spark)
28 | }
29 | }
30 |
31 | // Ref https://github.com/locationtech/geomesa/blob/master/geomesa-spark/geomesa-spark-sql/src/main/scala/org/apache/spark/sql/SQLGeometricConstructorFunctions.scala
32 | object SQLGeometricFunctions {
33 |
34 | val ST_MakePoint: (Double, Double) => Point = (x: Double, y: Double) => WKTUtils.read(s"POINT($x $y)").asInstanceOf[Point]
35 | val ST_MakeBox2D: (Point, Point) => Geometry = (ll: Point, ur: Point) => JTS.toGeometry(new Envelope(ll.getX, ur.getX, ll.getY, ur.getY))
36 | val ST_GeomFromWKB: Array[Byte] => Geometry = (array: Array[Byte]) => WKBUtils.read(array)
37 | val ST_Intersects: (Geometry, Geometry) => Boolean = (geom1: Geometry, geom2: Geometry) => geom1.intersects(geom2)
38 |
39 | def registerFunctions(spark: SparkSession): Unit = {
40 | spark.udf.register("ST_MakePoint", ST_MakePoint)
41 | spark.udf.register("ST_MakeBox2D", ST_MakeBox2D)
42 | spark.udf.register("ST_GeomFromWKB", ST_GeomFromWKB)
43 | spark.udf.register("ST_Intersects", ST_Intersects)
44 | }
45 |
46 | }
47 |
48 | // Ref https://github.com/locationtech/geomesa/blob/master/geomesa-spark/geomesa-spark-sql/src/main/scala/org/apache/spark/sql/SQLTypes.scala
49 | abstract class AbstractGeometryUDT[T >: Null <: Geometry](override val simpleString: String)(implicit cm: ClassTag[T]) extends UserDefinedType[T]{
50 | override def serialize(obj: T): InternalRow = {
51 | new GenericInternalRow(Array[Any](WKBUtils.write(obj)))
52 | }
53 |
54 | override def sqlType: DataType = StructType(
55 | Seq(
56 | StructField("wkb", DataTypes.BinaryType)
57 | )
58 | )
59 |
60 | override def userClass: Class[T] = cm.runtimeClass.asInstanceOf[Class[T]]
61 |
62 | override def deserialize(datum: Any): T = {
63 | val ir = datum.asInstanceOf[InternalRow]
64 | WKBUtils.read(ir.getBinary(0)).asInstanceOf[T]
65 | }
66 | }
67 |
68 | private[spark] class PointUDT extends AbstractGeometryUDT[Point]("point")
69 | private[spark] class MultiPointUDT extends AbstractGeometryUDT[MultiPoint]("multipoint")
70 | private[spark] class LineStringUDT extends AbstractGeometryUDT[LineString]("linestring")
71 | private[spark] class MultiLineStringUDT extends AbstractGeometryUDT[MultiLineString]("multilinestring")
72 | private[spark] class PolygonUDT extends AbstractGeometryUDT[Polygon]("polygon")
73 | private[spark] class MultiPolygonUDT extends AbstractGeometryUDT[MultiPolygon]("multipolygon")
74 |
75 | private[spark] class GeometryUDT extends AbstractGeometryUDT[Geometry]("geometry"){
76 | private[sql] override def acceptsType(dataType: DataType): Boolean = {
77 | super.acceptsType(dataType) ||
78 | dataType.getClass == classOf[GeometryUDT] ||
79 | dataType.getClass == classOf[PointUDT] ||
80 | dataType.getClass == classOf[MultiPointUDT] ||
81 | dataType.getClass == classOf[LineStringUDT] ||
82 | dataType.getClass == classOf[MultiLineStringUDT] ||
83 | dataType.getClass == classOf[PolygonUDT] ||
84 | dataType.getClass == classOf[MultiPolygonUDT]
85 | }
86 | }
--------------------------------------------------------------------------------
/src/main/scala/org/ieee/codemeow/geometric/CRSUtils.scala:
--------------------------------------------------------------------------------
1 | package org.ieee.codemeow.geometric
2 |
3 | /**
4 | * Created by CodeMeow on 2017/5/12.
5 | */
6 |
7 | // Ref https://github.com/mapbox/mercantile/blob/master/mercantile/__init__.py
8 | object CRSUtils {
9 |
10 | val MAX_ZOOM: Long = 28
11 | val RADIUS: Double = 6378137.0
12 |
13 | // Returns the upper left (lon, lat) of tile
14 | def upperLeft(tile: (Long, Long, Long)): (Double, Double) ={
15 | val (row, column, zoom) = tile
16 | val n = math.pow(2.0, zoom)
17 | val lon_deg = row / n * 360.0 - 180.0
18 | val lat_rad = math.atan(math.sinh(math.Pi * (1 - 2 * column / n)))
19 | val lat_deg = math.toDegrees(lat_rad)
20 | return (lon_deg, lat_deg)
21 | }
22 |
23 | // Returns the upper left Mercator coordinate (x, y) of tile
24 | def upperLeftMercator(tile: (Long, Long, Long)): (Double, Double) ={
25 | val (lng, lat) = upperLeft(tile)
26 | lnglatToMercator(lng, lat)
27 | }
28 |
29 | // Returns the (lon, lat) bounding box of a tile
30 | def bounds(tile: (Long, Long, Long)): (Double, Double, Double, Double) ={
31 | val (row, column, zoom) = tile
32 | val a = upperLeft(tile)
33 | val b = upperLeft((row + 1, column + 1, zoom))
34 | return (a._1, b._2, b._1, a._2)
35 | }
36 |
37 | // Returns the Mercator coordinate (x, y) bounding box of a tile
38 | def boundsMercator(tile: (Long, Long, Long)): (Double, Double, Double, Double) ={
39 | val (row, column, zoom) = tile
40 | val a = upperLeftMercator(row + 1, column, zoom)
41 | val b = upperLeftMercator((row, column + 1, zoom))
42 | return (a._1, b._1, a._2, b._2)
43 | }
44 |
45 | // Returns the Spherical Mercator (x, y) in meters
46 | def lnglatToMercator(lng: Double, lat: Double): (Double, Double) ={
47 | val x = RADIUS * math.toRadians(lng)
48 | val y = RADIUS * math.log(math.tan((math.Pi * 0.25) + (0.5 * math.toRadians(lat))))
49 | return (x, y)
50 | }
51 |
52 | // Returns the Location (lon, lat) from Spherical Mercator (x, y)
53 | def mercatorTolnglat(x: Double, y: Double): (Double, Double) ={
54 | val lng = math.toDegrees(x / RADIUS)
55 | val lat = math.toDegrees(math.atan(math.exp(y / RADIUS)) * 2.0 - math.Pi / 2)
56 | return (lng, lat)
57 | }
58 |
59 | // Returns the (row, column, zoom) tile of a location (lon, lat)
60 | def lnglatToTile(lng: Double, lat: Double, zoom: Long): (Long, Long, Long) ={
61 | val lat_rad = math.toRadians(lat)
62 | val n = math.pow(2.0, zoom)
63 | val row = math.floor((lng + 180.0) / 360.0 * n).toLong
64 | val column = math.floor((1.0 - math.log(math.tan(lat_rad) + (1.0 / math.cos(lat_rad))) / math.Pi) / 2.0 * n).toLong
65 | return (row, column, zoom)
66 | }
67 |
68 | // Returns the (row, column, zoom) tile from Spherical Mercator (x, y) in special zoom level
69 | def mercatorToTile(x: Double, y: Double, zoom: Long): (Long, Long, Long) ={
70 | val (lng, lat) = mercatorTolnglat(x, y)
71 | lnglatToTile(lng, lat, zoom)
72 | }
73 |
74 | // Returns the tiles included in the Mercator bbox (minX, minY, maxX, maxY) in special zoom level
75 | def mercatorToTiles(bbox: (Double, Double, Double, Double), zoom: Long): Seq[(Long, Long, Long)] ={
76 | val ul = mercatorToTile(bbox._1, bbox._4, zoom)
77 | val lr_ = mercatorToTile(bbox._3, bbox._2, zoom)
78 | val lr = (lr_._1 + 1, lr_._2 + 1, zoom)
79 |
80 | (ul._1 to lr._1).flatMap(row => {
81 | (ul._2 to lr._2).map(column => {
82 | (row, column, zoom)
83 | })
84 | })
85 | }
86 |
87 | // Returns the children of an (row, column, zoom) tile
88 | def childrenTiles(tile: (Long, Long, Long)): Seq[(Long, Long, Long)] ={
89 | val (row, column, zoom) = tile
90 | return Seq[(Long, Long, Long)](
91 | (row * 2, column * 2, zoom + 1),
92 | (row * 2 + 1, column * 2, zoom + 1),
93 | (row * 2 + 1, column * 2 + 1, zoom + 1),
94 | (row * 2, column * 2 + 1, zoom + 1)
95 | )
96 | }
97 |
98 | // Returns the smallest tile (row, column, zoom) containing the bbox (west, south, east, north)
99 | def boundingTile(bbox: (Double, Double, Double, Double)): (Long, Long, Long) ={
100 | val (w, s, e, n) = bbox
101 | val tmin = lnglatToTile(w, s, 32)
102 | val tmax = lnglatToTile(e, n, 32)
103 | val cell = (tmin._1, tmin._2, tmax._1, tmax._2)
104 | val zoom = getBboxZoom(cell)
105 | if(zoom == 0) {
106 | return (0, 0, 0)
107 | }
108 | val row = rshift(cell._1, (32 - zoom))
109 | val column = rshift(cell._2, (32 - zoom))
110 | return (row, column, zoom)
111 | }
112 |
113 | def rshift(value: Long, n: Long): Long ={
114 | return (value % 0x100000000L) >> n
115 | }
116 |
117 | def getBboxZoom(bbox: (Long, Long, Long, Long)): Long ={
118 | for(zoom <- 0L to MAX_ZOOM){
119 | val mask = 1 << (32 - (zoom + 1))
120 | if(((bbox._1 & mask) != (bbox._3 & mask)) || ((bbox._2 & mask) != (bbox._4 & mask))){
121 | return zoom
122 | }
123 | }
124 | return MAX_ZOOM
125 | }
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/src/main/scala/org/ieee/codemeow/geometric/Feature.scala:
--------------------------------------------------------------------------------
1 | package org.ieee.codemeow.geometric
2 |
3 | import com.vividsolutions.jts.geom.Geometry
4 |
5 | /**
6 | * Created by CodeMeow on 2017/5/13.
7 | */
8 | case class Feature(id: Long, geom: Geometry, props: scala.collection.Map[String, String])
--------------------------------------------------------------------------------
/src/main/scala/org/ieee/codemeow/geometric/GeometricUtils.scala:
--------------------------------------------------------------------------------
1 | package org.ieee.codemeow.geometric
2 |
3 | import com.vividsolutions.jts.geom.{Envelope, Geometry}
4 | import org.apache.spark.rdd.RDD
5 | import org.geotools.geometry.jts.JTS
6 |
7 | /**
8 | * Created by CodeMeow on 2017/5/12.
9 | */
10 | object GeometricUtils {
11 |
12 | def boundary(rdd: RDD[Geometry]): (Double, Double, Double, Double) ={
13 | implicit val XMinComparator = new Ordering[Geometry]{
14 | override def compare(geom1: Geometry, geom2: Geometry): Int ={
15 | val x1 = geom1.getEnvelopeInternal.getMinX
16 | val x2 = geom2.getEnvelopeInternal.getMinX
17 | return if(x1 > x2) 1 else (if(x1 < x2) -1 else 0)
18 | }
19 | }
20 | implicit val XMaxComparator = new Ordering[Geometry]{
21 | override def compare(geom1: Geometry, geom2: Geometry): Int ={
22 | val x1 = geom1.getEnvelopeInternal.getMaxX
23 | val x2 = geom2.getEnvelopeInternal.getMaxX
24 | return if(x1 > x2) 1 else (if(x1 < x2) -1 else 0)
25 | }
26 | }
27 | implicit val YMinComparator = new Ordering[Geometry]{
28 | override def compare(geom1: Geometry, geom2: Geometry): Int ={
29 | val y1 = geom1.getEnvelopeInternal.getMinY
30 | val y2 = geom2.getEnvelopeInternal.getMinY
31 | return if(y1 > y2) 1 else (if(y1 < y2) -1 else 0)
32 | }
33 | }
34 | implicit val YMaxComparator = new Ordering[Geometry]{
35 | override def compare(geom1: Geometry, geom2: Geometry): Int ={
36 | val y1 = geom1.getEnvelopeInternal.getMaxY
37 | val y2 = geom2.getEnvelopeInternal.getMaxY
38 | return if(y1 > y2) 1 else (if(y1 < y2) -1 else 0)
39 | }
40 | }
41 | val minX: Double = rdd.min()(XMinComparator).getEnvelopeInternal.getMinX
42 | val maxX: Double = rdd.max()(XMaxComparator).getEnvelopeInternal.getMaxX
43 | val minY: Double = rdd.min()(YMinComparator).getEnvelopeInternal.getMinY
44 | val maxY: Double = rdd.max()(YMaxComparator).getEnvelopeInternal.getMaxY
45 |
46 | return (minX, minY, maxX, maxY)
47 | }
48 |
49 | def intersectedTiles(geom: Geometry, zoom: Long): Seq[(Long, Long, Long)] ={
50 | val minX = geom.getEnvelopeInternal.getMinX
51 | val maxX = geom.getEnvelopeInternal.getMaxX
52 | val minY = geom.getEnvelopeInternal.getMinY
53 | val maxY = geom.getEnvelopeInternal.getMaxY
54 | val bbox = (minX, minY, maxX, maxY)
55 |
56 | val tiles = CRSUtils.mercatorToTiles(bbox, zoom)
57 | tiles.filter(tile => {
58 | val ll = CRSUtils.upperLeftMercator((tile._1, tile._2 + 1, tile._3))
59 | val ur = CRSUtils.upperLeftMercator((tile._1 + 1, tile._2, tile._3))
60 | val boundary = JTS.toGeometry(new Envelope(ll._1, ur._1, ll._2, ur._2))
61 | boundary.intersects(geom)
62 | })
63 | }
64 |
65 | def encodeTile(layerName: String, tile: (Long, Long, Long)): String ={
66 | val (row, column, zoom) = tile
67 | val code = (row % 0x1000000L) << 40 | (column % 0x1000000L) << 16 | (zoom % 0x10000L)
68 | s"${layerName}:${code}"
69 | }
70 |
71 | def decodeTile(key: String): (String, (Long, Long, Long)) ={
72 | val x = key.split(":").toSeq
73 | val layerName = x(0)
74 | val code = x(1).toLong
75 | val row = code >> 40
76 | val column = (code >> 16) % 0x1000000L
77 | val zoom = code % 0x10000L
78 | (layerName, (row, column, zoom))
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/scala/org/ieee/codemeow/geometric/MvtBuilder.scala:
--------------------------------------------------------------------------------
1 | package org.ieee.codemeow.geometric
2 |
3 | import com.vividsolutions.jts.geom.{Envelope, Geometry, GeometryFactory}
4 | import com.wdtinc.mapbox_vector_tile.VectorTile
5 | import com.wdtinc.mapbox_vector_tile.VectorTile.Tile
6 | import com.wdtinc.mapbox_vector_tile.VectorTile.Tile.Layer
7 | import com.wdtinc.mapbox_vector_tile.adapt.jts.{IGeometryFilter, JtsAdapter, UserDataIgnoreConverter}
8 | import com.wdtinc.mapbox_vector_tile.build.{MvtLayerBuild, MvtLayerParams, MvtLayerProps}
9 |
10 | /**
11 | * Created by CodeMeow on 2017/5/13.
12 | */
13 | object MvtBuilder {
14 |
15 | def buildLayer(layerName: String, tile: (Long, Long, Long), features: TraversableOnce[Feature]): Layer ={
16 | val bounds = CRSUtils.boundsMercator(tile)
17 | val tileEnvelope = new Envelope(bounds._1, bounds._2, bounds._3, bounds._4)
18 |
19 | val layerParams = new MvtLayerParams
20 | val layerBuilder = MvtLayerBuild.newLayerBuilder(layerName, layerParams)
21 |
22 | for(feature <- features){
23 | val geomFactory = new GeometryFactory()
24 | val tileGeom = JtsAdapter.createTileGeom(feature.geom, tileEnvelope, geomFactory, layerParams, AcceptAllGeomFilter)
25 |
26 | val layerProps = new MvtLayerProps
27 | for((key, value) <- feature.props){
28 | if(key == null || value == null){
29 | } else {
30 | layerProps.addKey(key)
31 | layerProps.addValue(value)
32 | }
33 | }
34 |
35 | val f = JtsAdapter.toFeatures(tileGeom.mvtGeoms, layerProps, new UserDataIgnoreConverter)
36 | layerBuilder.addAllFeatures(f)
37 | }
38 |
39 | return layerBuilder.build()
40 | }
41 |
42 | def buildMvt(layers: Seq[Layer]): Tile ={
43 | val tileBuilder = VectorTile.Tile.newBuilder()
44 | for(layer <- layers){
45 | tileBuilder.addLayers(layer)
46 | }
47 | tileBuilder.build()
48 | }
49 |
50 | def buildMvt(layer: Layer): Tile ={
51 | val tileBuilder = VectorTile.Tile.newBuilder()
52 | tileBuilder.addLayers(layer)
53 | tileBuilder.build()
54 | }
55 |
56 | }
57 |
58 | object AcceptAllGeomFilter extends IGeometryFilter{
59 | override def accept(geometry: Geometry) ={
60 | true
61 | }
62 | }
--------------------------------------------------------------------------------
/src/main/scala/org/ieee/codemeow/geometric/spark/Configurations.scala:
--------------------------------------------------------------------------------
1 | package org.ieee.codemeow.geometric.spark
2 |
3 |
4 | import com.fasterxml.jackson.annotation.JsonProperty
5 | import com.fasterxml.jackson.databind.ObjectMapper
6 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
7 | import com.fasterxml.jackson.module.scala.DefaultScalaModule
8 | import com.google.common.base.Preconditions
9 | import org.apache.hadoop.conf.Configuration
10 | import org.apache.hadoop.fs.{FileSystem, Path}
11 |
12 |
13 |
14 | /**
15 | * Created by CodeMeow on 2017/5/12.
16 | */
17 | object Configurations {
18 | def fromFile(_path: String): MainConfiguration ={
19 | val config = new Configuration
20 | val path = new Path(_path)
21 | val fs = FileSystem.get(path.toUri, config)
22 | val file = fs.open(path)
23 | val mapper = new ObjectMapper(new YAMLFactory())
24 | mapper.registerModule(DefaultScalaModule)
25 | mapper.readValue(file, classOf[MainConfiguration])
26 | }
27 | }
28 |
29 | class MainConfiguration (
30 | @JsonProperty("appName") _appName: String,
31 | @JsonProperty("bbox") _bbox: (Double, Double, Double, Double), // lat1, lon1, lat2, lon2
32 | @JsonProperty("layers") _layers: Array[LayerConfiguration],
33 | @JsonProperty("sequenceFileDir") _sequenceFileDir: String
34 | ) extends Serializable{
35 | val appName = Preconditions.checkNotNull(_appName, "appName cannot be null": Object)
36 | val bbox = _bbox
37 | val layers = Preconditions.checkNotNull(_layers, "layers cannot be null": Object)
38 | val sequenceFileDir = Preconditions.checkNotNull(_sequenceFileDir, "sequenceFileDir cannot be null": Object)
39 | }
40 |
41 | class LayerConfiguration (
42 | @JsonProperty("layerName") _layerName: String,
43 | @JsonProperty("minZoom") _minZoom: Int,
44 | @JsonProperty("maxZoom") _maxZoom: Int,
45 | @JsonProperty("dataProvider") _dataProvider: String,
46 | @JsonProperty("kwargs") _kwargs: Map[String, Any]
47 | ) extends Serializable{
48 | val layerName = Preconditions.checkNotNull(_layerName, "layerName cannot be null": Object)
49 | val minZoom = Preconditions.checkNotNull(_minZoom, "minZoom cannot be null": Object)
50 | val maxZoom = Preconditions.checkNotNull(_maxZoom, "maxZoom cannot be null": Object)
51 | val dataProvider = Preconditions.checkNotNull(_dataProvider, "dataProvider cannot be null": Object)
52 | val kwargs = Preconditions.checkNotNull(_kwargs, "kwargs cannot be null": Object)
53 | }
--------------------------------------------------------------------------------
/src/main/scala/org/ieee/codemeow/geometric/spark/VectorTileTask.scala:
--------------------------------------------------------------------------------
1 | package org.ieee.codemeow.geometric.spark
2 |
3 | import org.apache.hadoop.io.compress.GzipCodec
4 | import org.apache.spark.sql._
5 | import org.ieee.codemeow.geometric.{Feature, GeometricUtils, MvtBuilder}
6 | import org.ieee.codemeow.geometric.spark.data.AbstractDataProvider
7 |
8 | /**
9 | * Created by CodeMeow on 2017/5/13.
10 | */
11 | object VectorTileTask {
12 |
13 | val usage = "Usage: configFile"
14 |
15 | private def checkArgs(args: Array[String]) = {
16 | assert(args != null && args.length >= 1, usage)
17 | }
18 |
19 | def main(args: Array[String]): Unit ={
20 | checkArgs(args)
21 |
22 | // load application config
23 | val config: MainConfiguration = Configurations.fromFile(args(0))
24 |
25 | // Ref http://spark.apache.org/docs/latest/sql-programming-guide.html#starting-point-sparksession
26 | val spark = SparkSession.builder().appName(config.appName).getOrCreate()
27 |
28 | // Ref http://stackoverflow.com/questions/38664972/why-is-unable-to-find-encoder-for-type-stored-in-a-dataset-when-creating-a-dat
29 | import spark.implicits._
30 | // Ref http://stackoverflow.com/questions/36648128/how-to-store-custom-objects-in-dataset
31 | implicit val featureEncoder = Encoders.kryo[Feature]
32 |
33 | SQLGeometricExtensions.registerExtensions(spark)
34 |
35 | val layersCollection = config.layers.map(layer => {
36 | // classloader
37 | val constructor = Class.forName(layer.dataProvider).getConstructor(classOf[SparkSession], classOf[LayerConfiguration])
38 | val dataSource = constructor.newInstance(spark, layer).asInstanceOf[AbstractDataProvider]
39 |
40 | val tileCodeWithBytesCollection = buildLayerRDD(spark, dataSource, layer)
41 | tileCodeWithBytesCollection
42 | }).reduce((ds1, ds2) => {
43 | ds1.union(ds2)
44 | })
45 |
46 | layersCollection.rdd.saveAsSequenceFile(config.sequenceFileDir, Some(classOf[GzipCodec]))
47 | }
48 |
49 | def buildLayerRDD(spark: SparkSession, dataSource: AbstractDataProvider, layer: LayerConfiguration): Dataset[(String, Array[Byte])] ={
50 | // Ref http://stackoverflow.com/questions/38664972/why-is-unable-to-find-encoder-for-type-stored-in-a-dataset-when-creating-a-dat
51 | import spark.implicits._
52 | // Ref http://stackoverflow.com/questions/36648128/how-to-store-custom-objects-in-dataset
53 | //implicit val featureEncoder = Encoders.kryo[Feature]
54 |
55 |
56 | val layerMvtCollection = (layer.minZoom to layer.maxZoom).map(zoom => {
57 | val featureCollection = dataSource.getFeatures(layer.layerName, zoom)
58 |
59 | if(featureCollection.isEmpty){
60 |
61 | spark.emptyDataset[((Long, Long, Long), Seq[Feature])]
62 | } else {
63 | val featureWithTilesCollection = featureCollection.get.map(feature => {
64 | (feature, GeometricUtils.intersectedTiles(feature.geom, zoom))
65 | })
66 |
67 | val tileWithFeatureCollection = featureWithTilesCollection.flatMap(tuple => {
68 | tuple._2.map(tile => {
69 | (tile, tuple._1)
70 | })
71 | })
72 |
73 | val featureGroupByTileCollection_ = tileWithFeatureCollection.groupByKey(tuple => tuple._1)
74 |
75 | val featureGroupByTileCollection = featureGroupByTileCollection_.mapGroups((tile, groups) => {
76 | (tile, groups.map(tuple => tuple._2).toSeq)
77 | })
78 |
79 | featureGroupByTileCollection
80 | }
81 |
82 | }).reduce((ds1, ds2) => {
83 | ds1.union(ds2)
84 | })
85 |
86 | val tileWithBytesCollection = layerMvtCollection.map(tuple => {
87 | val layer_ = MvtBuilder.buildLayer(layer.layerName, tuple._1, tuple._2)
88 | val b = MvtBuilder.buildMvt(layer_).toByteArray
89 | (tuple._1, b)
90 | })
91 |
92 | val tileCodeWithBytesCollection = tileWithBytesCollection.map(tuple => {
93 | (GeometricUtils.encodeTile(layer.layerName, tuple._1), tuple._2)
94 | })
95 |
96 | tileCodeWithBytesCollection
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/main/scala/org/ieee/codemeow/geometric/spark/data/AbstractDataProvider.scala:
--------------------------------------------------------------------------------
1 | package org.ieee.codemeow.geometric.spark.data
2 |
3 | import org.apache.spark.sql.{Dataset, SparkSession}
4 | import org.ieee.codemeow.geometric.Feature
5 | import org.ieee.codemeow.geometric.spark.LayerConfiguration
6 |
7 | /**
8 | * Created by CodeMeow on 2017/5/16.
9 | */
10 | abstract class AbstractDataProvider(_spark: SparkSession, _layer: LayerConfiguration) {
11 |
12 | val spark = _spark
13 | val layer = _layer
14 |
15 | def getFeatures(layerName: String, zoom: Long): Option[Dataset[Feature]]
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/scala/org/ieee/codemeow/geometric/spark/data/SQLDataProvider.scala:
--------------------------------------------------------------------------------
1 | package org.ieee.codemeow.geometric.spark.data
2 |
3 | import com.vividsolutions.jts.geom.Geometry
4 | import org.apache.spark.sql.{Dataset, Encoders, SparkSession}
5 | import org.ieee.codemeow.geometric.Feature
6 | import org.ieee.codemeow.geometric.spark.LayerConfiguration
7 |
8 |
9 | /**
10 | * Created by CodeMeow on 2017/5/13.
11 | */
12 | class SQLDataProvider(_spark: SparkSession, _layer: LayerConfiguration) extends AbstractDataProvider(_spark, _layer){
13 |
14 | val url = layer.kwargs.get("url").get.asInstanceOf[String]
15 | val dbtables = layer.kwargs.get("dbtables").get.asInstanceOf[Map[String, String]]
16 | val user = layer.kwargs.get("user").get.asInstanceOf[String]
17 | val password = layer.kwargs.get("password").get.asInstanceOf[String]
18 | val zoomConfig = layer.kwargs.get("zooms").get.asInstanceOf[Map[String, String]]
19 |
20 | // load all tables
21 | dbtables.foreach(tuple => {
22 | val sparkTableName = tuple._1
23 | val realTableName = tuple._2
24 |
25 | val mapDataFrame = spark.read.format("jdbc")
26 | .option("url", url)
27 | .option("user", user)
28 | .option("password", password)
29 | .option("dbtable", realTableName).load
30 |
31 | mapDataFrame.createOrReplaceTempView(sparkTableName)
32 | })
33 |
34 | override def getFeatures(layerName: String, zoom: Long): Option[Dataset[Feature]] ={
35 | // Ref http://stackoverflow.com/questions/38664972/why-is-unable-to-find-encoder-for-type-stored-in-a-dataset-when-creating-a-dat
36 | import spark.implicits._
37 | // Ref http://stackoverflow.com/questions/36648128/how-to-store-custom-objects-in-dataset
38 | implicit val featureEncoder = Encoders.kryo[Feature]
39 |
40 | val natureSQL = zoomConfig.get(zoom.toString)
41 | if(natureSQL.isEmpty){
42 | return None
43 | }
44 |
45 | val rawDF = spark.sql(natureSQL.get)
46 | val featureCollection = rawDF.map(row => {
47 | val id = row.getAs[Long]("__id__")
48 | val geom = row.getAs[Geometry]("__geometry__")
49 | val fields = row.schema.filter(field => {
50 | !Seq("__id__", "__geometry__").contains(field.name)
51 | }).map(field => field.name)
52 | val props = row.getValuesMap[String](fields)
53 | Feature(id, geom, props)
54 | })
55 | Some(featureCollection)
56 | }
57 | }
--------------------------------------------------------------------------------
/src/test/scala/samples/junit.scala:
--------------------------------------------------------------------------------
1 | package samples
2 |
3 | import org.junit._
4 | import Assert._
5 |
6 | @Test
7 | class AppTest {
8 |
9 | @Test
10 | def testOK() = assertTrue(true)
11 |
12 | // @Test
13 | // def testKO() = assertTrue(false)
14 |
15 | }
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/test/scala/samples/scalatest.scala:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2001-2009 Artima, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package samples
17 |
18 | /*
19 | ScalaTest facilitates different styles of testing by providing traits you can mix
20 | together to get the behavior and syntax you prefer. A few examples are
21 | included here. For more information, visit:
22 |
23 | http://www.scalatest.org/
24 |
25 | One way to use ScalaTest is to help make JUnit or TestNG tests more
26 | clear and concise. Here's an example:
27 | */
28 | import scala.collection.mutable.Stack
29 | import org.scalatest.Assertions
30 | import org.junit.Test
31 |
32 | class StackSuite extends Assertions {
33 |
34 | @Test def stackShouldPopValuesIinLastInFirstOutOrder() {
35 | val stack = new Stack[Int]
36 | stack.push(1)
37 | stack.push(2)
38 | assert(stack.pop() === 2)
39 | assert(stack.pop() === 1)
40 | }
41 |
42 | @Test def stackShouldThrowNoSuchElementExceptionIfAnEmptyStackIsPopped() {
43 | val emptyStack = new Stack[String]
44 | intercept[NoSuchElementException] {
45 | emptyStack.pop()
46 | }
47 | }
48 | }
49 |
50 | /*
51 | Here's an example of a FunSuite with ShouldMatchers mixed in:
52 | */
53 | import org.scalatest.FunSuite
54 | import org.scalatest.matchers.ShouldMatchers
55 |
56 | import org.junit.runner.RunWith
57 | import org.scalatest.junit.JUnitRunner
58 | @RunWith(classOf[JUnitRunner])
59 | class ListSuite extends FunSuite with ShouldMatchers {
60 |
61 | test("An empty list should be empty") {
62 | List() should be ('empty)
63 | Nil should be ('empty)
64 | }
65 |
66 | test("A non-empty list should not be empty") {
67 | List(1, 2, 3) should not be ('empty)
68 | List("fee", "fie", "foe", "fum") should not be ('empty)
69 | }
70 |
71 | test("A list's length should equal the number of elements it contains") {
72 | List() should have length (0)
73 | List(1, 2) should have length (2)
74 | List("fee", "fie", "foe", "fum") should have length (4)
75 | }
76 | }
77 |
78 | /*
79 | ScalaTest also supports the behavior-driven development style, in which you
80 | combine tests with text that specifies the behavior being tested. Here's
81 | an example whose text output when run looks like:
82 |
83 | A Map
84 | - should only contain keys and values that were added to it
85 | - should report its size as the number of key/value pairs it contains
86 | */
87 | import org.scalatest.FunSpec
88 | import scala.collection.mutable.Stack
89 |
90 | class ExampleSpec extends FunSpec {
91 |
92 | describe("A Stack") {
93 |
94 | it("should pop values in last-in-first-out order") {
95 | val stack = new Stack[Int]
96 | stack.push(1)
97 | stack.push(2)
98 | assert(stack.pop() === 2)
99 | assert(stack.pop() === 1)
100 | }
101 |
102 | it("should throw NoSuchElementException if an empty stack is popped") {
103 | val emptyStack = new Stack[Int]
104 | intercept[NoSuchElementException] {
105 | emptyStack.pop()
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/test/scala/samples/specs.scala:
--------------------------------------------------------------------------------
1 | package samples
2 |
3 | import org.junit.runner.RunWith
4 | import org.specs2.mutable._
5 | import org.specs2.runner._
6 |
7 |
8 | /**
9 | * Sample specification.
10 | *
11 | * This specification can be executed with: scala -cp ${package}.SpecsTest
12 | * Or using maven: mvn test
13 | *
14 | * For more information on how to write or run specifications, please visit:
15 | * http://etorreborre.github.com/specs2/guide/org.specs2.guide.Runners.html
16 | *
17 | */
18 | @RunWith(classOf[JUnitRunner])
19 | class MySpecTest extends Specification {
20 | "The 'Hello world' string" should {
21 | "contain 11 characters" in {
22 | "Hello world" must have size(11)
23 | }
24 | "start with 'Hello'" in {
25 | "Hello world" must startWith("Hello")
26 | }
27 | "end with 'world'" in {
28 | "Hello world" must endWith("world")
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------