HK Spot Height Data: From GML to GeoJSON

Tommy
4 min readMay 4, 2022

--

LandsD provides plenty of information in their digital map, which is not released in Geoinfo Map or explicitly in open data. You may read the resource file to explore these data.

Kowloon City Spot Height Data Visualized in QGIS

Spot height, height/altitude of a point as shown on a map. Spot height would be useful for my driving simulation game, because some of the 3D road model is not updated in LandsD, which requires us to “create”. For example, the above map shows a flyover (white) flys over the main road (yellow), but part of this road (section of spot height 12.7, 8.5, 7.2, 6.5) is newly built. We may project a 3D curve based on existing 2D(xy) line data and spot height, and hence generate 3D road mesh.

Free tools to create path in Unity

Again, most of the geospatial data is not designed for game / web development. GML is the most machine readable format (xml) available but JSON/GeoJSON would be much better for development. In this article I will illustrate how to convert spot height GML to GeoJSON.

GML of one spot height

During coding, I found that conversion of GML to GeoJSON would be lossy, not all information can be kept intact easily. For example, parsing string digit by python float(). Moreover, it seems no official guide to teach how to maintain the nested structure of GML. So I just keep the data type correct and store the unique fields in my conversion.

Part of data dictionary of Spot Height data

Before coding, we have to download the data first. Please note that only iB1000 provides spot data. I will download fullset tile-base zip file (filesize is 1.88GB when I download) and unzip it recursively.

The iB1000 map data is tile-based

After unzip, first of all is to obtain all SpotHeight.gml available, by glob library.

import glob
sp_paths = glob.glob(f"/your/path/iB1000_Fullset_Tile-based_GML/*/Layers/Relief/SpotHeight.gml")

And then prepare lxml and namespaces for GML parsing

from lxml.etree import XMLParser, parse
parser = XMLParser(huge_tree=True)
namespaces = {
"gml": "http://www.opengis.net/gml",
"fme": "http://www.safe.com/gml/fme"}

Then start iterate all GML files:

for sp_path in sp_paths:
index = sp_path.split("/")[-4]
sp_tree = parse(sp_path, parser=parser)

points = []

(index is the tile index obtained from filepath)

Inside each file, iterate all “gml:featureMember”, and save all data in correct type. It will be “properties” part of the geojson. (Sorry the code is quite clumsy.)

for gco in sp_tree.getroot().findall("gml:featureMember", namespaces=namespaces):
properties = {}
sh = gco.find("fme:SpotHeight", namespaces=namespaces)
properties['gml:id'] = sh.attrib[f"{{{namespaces['gml']}}}id"]
fmes = sh.findall(".//fme:*", namespaces=namespaces)
for d in fmes:
k = d.tag.replace(f"{{{namespaces['fme']}}}", "")
v = d.text
properties[k] = v
properties['SPOTHEIGHTID'] = int(properties['SPOTHEIGHTID'])
properties['DISPLAYSTATUS'] = int(properties['DISPLAYSTATUS'])
properties['HEIGHTVALUE'] = float(properties['HEIGHTVALUE'])
properties['HEIGHTVALUEPOSITION'] = int(properties['HEIGHTVALUEPOSITION'])
properties['ZPRECISION'] = int(properties['ZPRECISION'])

Last but not least, the coordinates of the spot height:

pointProperty_tag = sh.find("gml:pointProperty", namespaces=namespaces)        
point_coord_text = pointProperty_tag.find(".//gml:pos", namespaces=namespaces).text
point_coord = [float(v) for v in point_coord_text.split(" ")[:2] ][::-1]

Prepare geojson format and save in points list.

geojson = {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": point_coord
},
"properties": properties
}

points.append(geojson)

After iterating all spot height inside a tile GML, save all points in “FeatureCollection” and export as geojson file.

fc = {
"type": "FeatureCollection",
"crs" : {
"type" : "name",
"properties" : {
"name" : "EPSG:2326"
}
},
"features": points
}
import jsonwith open(f"spot_height/{index}.geojson", "w") as f:
f.write(json.dumps(fc))

According to GeoJSON official specification, coordinates must be stored in WGS84 (latitude and longitude), other CRS is not allowed. But my game use EPSG:2326-like system, so I won’t transform the coordinates to WGS84. Note that the exported geojson will be non-standard GeoJSON form. If you want to know how to transform coordinates to WGS84, you may read my other articles.

GeoJSON Specification about CRS

The overall code:

Done~Let’s compare the GML and exported GeoJSON in QGIS.

Plot the same dataset of GML/GeoJSON in QGIS

They plot on the same point with identical height data label, so I think it is converted correctly.

I had also try combine all spots height data in HK into one GeoJSON file…

376101 Spot Height Recorded in Hong Kong, 126MB size in GeoJSON

Over 376k spot height recorded, and I view it in QGIS… it’s so laggy. Better use tile-based GeoJSON instead.

--

--