절대참조 :
https://github.com/mapsforge/mapsforge
여기가서 읽어본뒤
이것을 다운로드 받으면 된다.
s:\>osmosis-latest 에 압축을 풀었다고 가정
많은 파일중에서 지금 현재는 mapsforge-map-writer-master-20180417.073816-214-jar-with-dependencies.jar
이 파일을 받는다. 아마 세월이 흐르면 날짜부분에 변화가 있을듯.
s:\>osmosis-latest\lib\default 폴더에 위의 jar파일을 넣는다.
gpx 파일을 읽어 그중 필요한 것만 뽑아내고 거기에 몇개를 더해서 osm파일을 만든뒤 이 osm파일을 재료로 map파일을 만든다.
오룩스맵이나 산길샘, 가민,트랭글 같은 앱에서 gpx파일을 만들어내는데 그 파일을 편집기에서 열어보면 좀 복잡해보이는 xml형식이다.
아래그림은 처음 부분이고
이건 마지막 부분이다.
알아보기 좋게 구조를 다시 적어보면 다음과 같다.
일정시간간격으로 gps위성의 신호를 받아와서 그 지점의 위치를 표시한게 <trkpt> 이고 적당한 지점을 이름으로 표시한곳이 <wpt>이다. 여기서는 [배드민턴]코트를 <wpt>로 표시했다.
위 그림에서 표현은 하지 않았지만 <trk>도 여러개가 있을 수 있고 <trkseg>도 여러개일 수 있다.
한달동안 산행을 했고 그것을 하나의 gpx파일에 기록했고 하루의 산행을 오전,오후로 구별했다면 다음과 같이 쓸 수 있다.
osm 파일의 예를 보자.
어떤 지역의 지도를 JOSM에서 받아와서 필요한 몇개만 추려냈다.
gpx 자리에 osm이 적혀있고 wpt 가 node 에 , trkpt 가 way속의 nd 에 대응한다.
node와 way의 속성을 살펴보면 way에 위도,경도가 없는것을 빼고보면 둘다 동일한 속성을 가지고 있다.
osm(OpenStreetMap)은 열려있는 지도이기때문에 누구나 사이트에 등록만 하면 지도를 추가하고 편집할 수 있다. 그래서 user와 uid가 필요한것이다.
위의 노드는 이름이 Aleks-Berlin 이고 uid가 85218 을 가진 분이 만든것이며 이 노드자체의 id는 442882724 이다.
처음 이 노드가 만들어질때 딱 한번 숫자가 부여되면 바뀌지 않는다. 이 노드가 삭제되더라도 다른 노드에서 이 노드의 아이디를 가지지 못한다. 그야말로 고유아이디이다.
처음 노드를 만들어서 서버에 올리기 전에 오프라인 저장한뒤 살펴보면 이 아이디가 음수로 나타나는데 이는 이 노드가 서버에 올려지지 않았다는 것을 의미한다. 음수이이디를 가진 노드가 서버에 올려지면 적절한 검사를 받은뒤 양수로 된 고유아이디를 부여받는다. 음수아이디를 가진 노드로는 map파일이 만들어지지 않는다.
gpsbabel 같은 프로그램에서 gpx를 osm으로 변환해보면 이 아이디가 음수로 표시되는것도 이러한 까닭이고 이 파일로는 map파일이 만들어지지 않는것도 이러한 이유에서이다.
timestamp는 이 노드를 마지막 편집한 시간이고 version과 changeset는 노드의 값이 변할 때마다 적절한 값으로 바뀐다.
여기서는 gpx로 만든 지도를 osm서버에 올리지 않을 것이기 때문에 id의 값에 신경을 쓸 필요는 없다. 숫자1에서 시작할 것이다. 노드의 속성값이 너무 많은 듯 하여 하나씩 지워가며 map 파일을 만들어 보았는데 uid, user, changeset는 없어도 되고 시간은 같은 시간으로 적어도 상관없었다.
<node> 와 <way> 안에 <tag>가 있는데 node와 way의 모양과 이름을 나타낸다.
예를들어
<node....>
<tag k='name' v='들머리' />
<tag k='amenity' v='cafe' />
</node>
<way....>
<nd>....</nd>
..
<tag k='highway' v='path'/>
</way>
오룩스맵에 올려본 모습인데 노드의 이름이 보이고 모양은 cafe 니까 커피잔이 나타났고 웨이의 모양은 점선으로 나타났다.
이 태그의 값에 따라 나타나는 모양은 다양하다. 잠시
여기로 가서 보고오자.
이제 gpx 파일에서 osm파일로 바꾸는 과정을 살펴보자.
먼저 파이썬에서 xml을 다루는 방법을
여기와 저기를 들락거리면서 공부했다. 그것을 바탕으로 아래의 코드를 작성했다.
파이썬에서 한글을 다루니까 제일처음에 다음문장을 적는다.
#*-* coding:utf-8 -*-
xml을 다루니까 다음을 추가.
import xml.etree.ElementTree as ET
gpx 파일을 연다.
tree = ET.parse('test.gpx')
최상위 태그를 가져온다.
src = tree.getroot()
src를 찍어보면
{http://www.topografix.com/GPX/1/1}gpx
gpx만 나오는게 아니라 네임스페이스(namespace)가 앞에 붙어있다.
위 그림처럼 xmlns= 의 뒤에 있는 값이다. 모든 gpx파일의 네임스페이스가 이 값일텐데 그래도 장담을 못하니까 프로그램내에서 이값을 구해서 사용한다.
ns = src.tag.split('}')[0].strip('{')
이렇게 하면 ns의 값에 http://www.topografix.com/GPX/1/1 이 들어온다.
중괄호를 넣는다.
nss = '{'+ns+'}'
중괄호가 들어있는 nss는 검색을 하는데 사용하고 ns는 다음과 같이 등록하는데 사용한다.
ET.register_namespace('',ns)
이렇게 하면 최종적으로 osm파일이 만들어질때 네임스페이스가 잘 만들어진다. 위 문장을 넣고 안넣고에 따라 osm파일이 어떻게 달라지는가는 마지막쯤에서 보여주겠다.
osm파일의 루트노드를 만든다. 속성으로 버전만 넣는다.
dst = ET.Element('osm',version='0.6')
gpx태그 바로 밑에 있는 태그중 <wpt>들을 모두 가져온다.
wpts = src.findall(nss+'wpt')
그중에 하나의 자료다. 빨간박스친 부분만 쓰인다.
위 그림과 같은 <wpt>를 아래그램과 같은 <node>로 바꾼다
times = '1999-12-25T01:01:01Z'
count = 1
tagNode1 = ET.Element('tag',k='amenity',v='cafe')
for wpt in wpts:
node = ET.Element('node', id = str(count), version='1', timestamp=times)
count += 1
node.attrib['lat'] = wpt.attrib['lat']
node.attrib['lon'] = wpt.attrib['lon']
node.append(tagNode1)
tagNode2 = ET.Element('tag',k='name')
tagNode2.attrib['v'] = wpt.find(nss+'name').text.strip()
node.append(tagNode2)
dst.append(node)
makeBounds(wpt.attrib['lat'],wpt.attrib['lon'])
count의 값은 첫번째 node에서 1이고 하나씩 증가하는데 이값을 노드의 id로 사용한다. 시간은 고정값을 사용하고 version도 1로 고정값을 사용했고 두개의 tag중 하나는 고정값으로 amenity=cafe를 사용했다. 이 값을 바꾼다든가 tag하나를 더 추가한다든가 하는것은 위의 코드를 참조해서 상상력을 조금만 발휘하면 된다.
더 진행하기전에 gpx파일의 <metadata>의 <bounds>에 대해서 살펴보고가자.
gpx파일속에 사용된 모든 포인트의 경위도값을 비교해서 구한 최소값과 최대값을 저장하고 있다.
이 <bounds>가 osm에서도 사용되는데 이 태그의 값이 gpx파일를 생성하는 앱에 따라서는 없는경우도 있으므로 여기서는 직접 구하는 것으로 한다.
minlati = '90.0'
minlong = '180.0'
maxlati = '0.0'
maxlong = '0.0'
def makeBounds(lat,lon):
global minlati,minlong,maxlati,maxlong
if lat < minlati : minlati = lat
if lat > maxlati : maxlati = lat
if lon < minlong : minlong = lon
if lon > maxlong : maxlong = lon
최소.최대값을 전역변수로 뽑아냈고 적당한 연산을 하는 함수로 만들었다. 실수로 변환하지 않고 문자열비교를 했는데 그것은 비교대상의 정수부값이 같은 자리수를 가지기에 그렇게 했다. 하나의 gpx파일안에 위도가 35.xxx 와 2.xxx가 같이 나오는 경우라든가 경도값으로 127.xxx와 12.xxx와 같이 정수의 자리수가 다른경우가 나올때는 이렇게 문자열비교를 할 수 없다. 완전 범용으로 만들려면 위 함수를 손봐야하는데 여기서는 이대로 사용한다.
gpx에서는 <trkpt>라는 태그로 시간순으로 쭉 늘어놓았는데 osm에서는 그렇게 하지 않고 모든 trkpt를 각각 node로 만든뒤 way라는 태그에서 해당 node의 id값을 쭉 적는다.
<trkpt lat='xx1' lon='xx'></trkpt>
<trkpt lat='xx2' lon='xx'></trkpt>
<trkpt lat='xx3' lon='xx'></trkpt>
<trkpt lat='xx4' lon='xx'></trkpt>
위와 같은 gpx파일속의 문장이 아래과 같이 두개의 부분으로 쪼개진다.
<node id ='1' 'lat='xx1' lon='xx' .../>
<node id ='2' 'lat='xx2' lon='xx' .../>
<node id ='3' 'lat='xx3' lon='xx' .../>
<node id ='4' 'lat='xx4' lon='xx' .../>
<way id='xx' ...>
<nd ref='1' />
<nd ref='2' />
<nd ref='3' />
<nd ref='4' />
</way>
이 부분을 처리해보자
ways = []
for trkseg in src.iter(nss+'trkseg'):
way = ET.Element('way',version='1',timestamp=times)
for trkpt in trkseg.findall(nss+'trkpt'):
node = ET.Element('node',id = str(count),version='1',timestamp=times)
node.attrib['lat'] = trkpt.attrib['lat']
node.attrib['lon'] = trkpt.attrib['lon']
dst.append(node)
nd = ET.Element('nd',ref=str(count))
count += 1
way.append(nd)
makeBounds(trkpt.attrib['lat'],trkpt.attrib['lon'])
tag = ET.Element('tag',k='highway',v='path')
way.append(tag)
ways.append(way)
for way in ways:
way.attrib['id'] = str(count)
count += 1
dst.append(way)
osm 파일에서는 모든 node 를 다 적은뒤 way가 따라온다. 그래서
trkpt를 node와 way로 나누어 node는 dst에 넣고 way는 따로 ways 리스트에 보관한뒤 node 작업이 다 끝난뒤 ways 리스트에서 way를 불러와서 dst에 넣었기 때문에 코드가 약간 뒤틀렸지만 이해못할 바는 아니다.
bounds를 osm파일제일 위에 삽입한다.
bounds = ET.Element('bounds',minlat = minlati, minlon = minlong, maxlat = maxlati, maxlon = maxlong)
dst.insert(0,bounds)
이제 osm파일을 만든다.
ET.ElementTree(dst).write('test.osm', encoding='utf-8',xml_declaration=True)
map파일을 만든다
s:\>osmosis-latest\bin\osmosis --rx s:/test.osm --mw file=s:/test.map
전체 프로그램 소스는 다음과 같다.
#*-* coding:utf-8 -*-
import xml.etree.ElementTree as ET
filename = 's:/test.gpx'
osmfile = filename + '.osm'
mapfile = osmfile + '.map'
times = '1999-12-25T01:01:01Z'
node_k = 'amenity'
node_v = 'cafe'
way_k1 = 'highway'
way_v1 = 'path'
minlati = '90.0'
minlong = '180.0'
maxlati = '0.0'
maxlong = '0.0'
def makeBounds(lat,lon):
global minlati,minlong,maxlati,maxlong
if lat < minlati : minlati = lat
if lat > maxlati : maxlati = lat
if lon < minlong : minlong = lon
if lon > maxlong : maxlong = lon
def gpx2osm(file):
tree = ET.parse(filename)
src = tree.getroot()
ns = src.tag.split('}')[0].strip('{')
# 'http://www.topografix.com/GPX/1/1'
nss = '{'+ns+'}'
# '{http://www.topografix.com/GPX/1/1}'
# gpx파일의 네임스페이스가 없을경우
if src.tag == 'gpx':
ns = nss = ''
ET.register_namespace('',ns)
dst = ET.Element('osm',version='0.6')
wpts = src.findall(nss+'wpt')
tagNode1 = ET.Element('tag',k=node_k,v=node_v)
# id 의 시작숫자
count = 1
for wpt in wpts:
node = ET.Element('node', id = str(count), version='1', timestamp=times)
count += 1
node.attrib['lat'] = wpt.attrib['lat']
node.attrib['lon'] = wpt.attrib['lon']
node.append(tagNode1)
tagNode2 = ET.Element('tag',k='name')
tagNode2.attrib['v'] = wpt.find(nss+'name').text.strip()
node.append(tagNode2)
# 높이를 표현하고 싶을때 사용함
# 소수점 첫째자리만 사용
# txtEle[1][:2] 이렇게 바꾸면 둘째자리까지 사용
# tagNode3 = ET.Element('tag',k='ele')
# txtEle = wpt.find(nss+'ele').text.split('.')
# txtEle = txtEle[0] + '.' + txtEle[1][:1]
# tagNode3.attrib['v'] = txtEle
# node.append(tagNode3)
dst.append(node)
makeBounds(wpt.attrib['lat'],wpt.attrib['lon'])
ways = []
for trkseg in src.iter(nss+'trkseg'):
way = ET.Element('way',version='1',timestamp=times)
for trkpt in trkseg.findall(nss+'trkpt'):
node = ET.Element('node',id = str(count),version='1',timestamp=times)
node.attrib['lat'] = trkpt.attrib['lat']
node.attrib['lon'] = trkpt.attrib['lon']
dst.append(node)
nd = ET.Element('nd',ref=str(count))
count += 1
way.append(nd)
makeBounds(trkpt.attrib['lat'],trkpt.attrib['lon'])
tag = ET.Element('tag',k=way_k1,v=way_v1)
way.append(tag)
ways.append(way)
for way in ways:
way.attrib['id'] = str(count)
count += 1
dst.append(way)
bounds = ET.Element('bounds',minlat=minlati,minlon=minlong,maxlat=maxlati,maxlon=maxlong)
dst.insert(0,bounds)
ET.ElementTree(dst).write(osmfile, encoding='utf-8',xml_declaration=True)
def osmosis():
import os
cmd = 's:/osmosis-latest/bin/osmosis'
cmd += ' --rx ' + osmfile
cmd += ' --mw file=' + mapfile
print(cmd)
os.system(cmd)
gpx2osm(filename)
osmosis()
변환할 gpx파일이름을 test.gpx 로 가정
전체 파이썬소스이름을 gpx2map.py 로 가정.
둘다 s:\ 에 있다고 가정.
cmd창을 열어서 s:\ 폴더로 간다음
s:\>python gpx2map.py 라고 하면 된다.
그러면 s:\ 폴더에
test.gpx.osm
test.gpx.osm.map
이 만들어진다.
map파일을 오룩스맵에 올려서 살펴보면 줌레벨 14 부터 패스가 보이고 줌레벨 17이 되어야 커피잔이 보인다.
node의 tag값이 amenity=cafe 이고
way의 tag값이 highway=path 이기 때문이다.
<osm-tag key="highway" value="path" zoom-appear="14" />
<osm-tag key="amenity" value="cafe" zoom-appear="17" />
기본값이 위와 같이 정해져있다.
커피잔이 16레벨 부터, 패스는 15레벨부터 보이기를 원한다면 그렇게 할 수 있다.
매핑파일을 따로 만들어서 osmosis 실행할때 넣어주면 된다.
<?xml version="1.0" encoding="UTF-8"?>
<tag-mapping xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" default-zoom-appear="16"
profile-name="default-profile" xmlns="http://mapsforge.org/tag-mapping"
xsi:schemaLocation="http://mapsforge.org/tag-mapping https://raw.githubusercontent.com/mapsforge/mapsforge/master/resources/tag-mapping.xsd">
<osm-tag key="highway" value="path" zoom-appear="15" />
<osm-tag key="amenity" value="cafe" zoom-appear="16" />
</tag-mapping>
이런파일을 만들어서 이름을 적당히 준다음(test.xml 이라고 하자) osmosis 함수안에서 다음과 같이 한문장을 추가하면 된다.
cmd = 's:/osmosis-latest/bin/osmosis'
cmd += ' --rx ' + osmfile
cmd += ' --mw file=' + mapfile
cmd += ' tag-conf-file=s:/test.xml'
네임스페이스 등록관련해서 빠트린 설명을 이제 해본다.
osm 파일을 열어서 osm속성을 보면 따로 네임스페이스 항목이 없다.
<osm version="0.6">
파이썬 프로그램에서 gpx파일의 태그를 복사한게 하나도 없기때문에 생긴 요행이다. <bounds>를 구할때 gpx에 있는 <bounds>를 그대로 받아서 쓰는걸로 코드를 수정하면 결과는 달라진다.
...
metadata = src.find(nss + 'metadata')
bounds = metadata.find(nss + 'bounds')
dst.append(bounds)
이렇게 코드를 바꾸어서 실행하면
<osm xmlns="http://www.topografix.com/GPX/1/1" version="0.6"><bounds maxlat="35.2304286" maxlon="129.1069870" minlat="35.2208487" minlon="129.1012863" />
osm의 속성으로 xmlns가 생겨났다.
이제 네이스페이스를 등록하는걸 하지 않고 실행하면 즉 ET.register_namespace('',ns) 을 지우고 실행하면
<osm xmlns:ns0="http://www.topografix.com/GPX/1/1" version="0.6"><ns0:bounds maxlat="35.2304286" maxlon="129.1069870" minlat="35.2208487" minlon="129.1012863" />
ns0가 첨가되어있다. 이 osm파일로는 map파일이 만들어지지 않는다.
map파일을 오룩스맵에서 합성지도(밑에 다음지도를 넣고 위에 방금만든 map파일을 올린경우)로 만들어 띄워보면 이런 모습이다.
등산로를 나타내는 선이 눈에 거슬릴 정도로 굵다.
highway=path 대신에 딴걸 넣어서 돌려봐도 신통치가 않다. 위 그림의 녹색 등산로 정도의 굵기에 점선정도면 좋을 듯하다.
렌더링관련 문서를 읽어보면 어렴풋이 답이 보이나 명확하지는 않다. 일단 간단하게 아래와 같은 파일을 만들어보았다.
파일이름을 mypath.xml 이라고 주고 이 파일을 오룩스맵의 mapstyles 폴더에 넣어줬다. 그리고나서 mapsforge 테마를 mypath로 바꾸어주면 다음과 같이 나타난다.
색깔이 약간 밝은게 흠이나 굵기와 점선은 마음에 든다. 그런데 치명적으로 커피잔이 보이지 않는다. 파일을 이렇게 만들면 기존에 있던것은 그래도 두고 새로 정의한 것만 바뀌기를 기대했었는데 그런게 아닌 모양이다. 기본값으로 가지고 있던 모든 값이 다 사라지고 path하나만 정의된 듯. 커피잔 항목을 만들어서 추가하면 문제는 해결되나 이런 해결은 잠정적으로 위험을 내포하고 있다. 바탕지도로 벡터맵을 깔고 그 위에 방금 만든 map을 올리면 바탕지도가 보이지 않게된다. 기본정의가 하나도 없고 단지 path와 cafe두개의 정의만 있기때문이다.
기본값이 정의되어 있는 xml파일을 찾아서 그곳의 highway=path항목만 바꾸는게 최선일 듯하다.
여기들어가서 오른쪽 위쪽의 녹색버튼을 눌러 다운로드한다.
mapsforge-master.zip\mapsforge-master\mapsforge-themes\src\main\resources\assets\mapsforge\default.xml
위 파일이 기본값을 정의한 파일이고 이 파일에서 사용하는 아이콘그림과 패튼이 들어있는 폴더는 아래와 같다.
mapsforge-master.zip\mapsforge-master\mapsforge-themes\src\main\resources\assets\symbols
mapsforge-master.zip\mapsforge-master\mapsforge-themes\src\main\resources\assets\patterns
default.xml 에서 path 항목을 찾아 적당한 값으로 바꾼뒤 그 파일과 위의 두개의 폴더를 오룩스맵의 mapstyles폴더에 넣으면 된다.
그렇게 한 뒤 mapsforge의 테마로 [default]를 선택한다.
이렇게 진행하기 위해서는 먼저 default.xml에서 path항목을 찾아야한다.
126라인의 path는 아니다. tunnel = true 즉 터널속의 path는 우리가 찾는 패스가 아니다.
227라인의 path도 아니다. area=yes 즉 공간또는 건물을 나타내는데 쓰이는 path이다.
458라인의 path도 아니다. bridge=yes 즉 다리위의 path이다.
571라인의 path가 바로 그 path다. tunnel=no, area=no
stroke-dasharray = "5,5" 로 바꾸었고 stroke-width="0.2"로 바꾸었다.
전반적으로 바꿔져야 할 것이 하나 더 있다.
다음 그림을 보자.
20라인을 보면 jar가 붙어있다. jar대신에 file 로 바꾼다. 현재 이 파일속에는 jar가 128군데 있는데 다 바꾼다.
default.xml의 수정이 정확하게 끝났다면 path가 이렇게 나타난다. 물론 커피잔은 기본값이다.
default.xml 을 손대기 시작한 김에 조금만 더 설명을 해보자.
등산로의 선굵기와 이정표의 그림을 메뉴로 띄워 선택해볼 수도 있다.
아래그림처럼 stylemenu 태그를 삽입한다.
레이어를 다섯개 만들었는데 마지막 레이어가 메뉴역할은 한다. overlay 각각이 하나의 메뉴를 뜻한다.
오룩스맵에서 어떻게 보이는지 먼저 살펴보자.
아래그림처럼 Mapsforge 테마를 열어서 [default]를 선택하면 선택과 동시에 창이 닫히는데 다시 열어서 이제는 톱니바퀴를 누르면 메뉴가 나타난다.
메뉴가 4개 있다. 각각이 하나의 레이어를 나타낸다.
등산로 0.1은 layer id 가 my_path01 인 레이어를 나타내는데 cat id= path01를 품고있다. cat는 카테고리를 뜻한다.
cat id와 layer id는 이름을 같게 둘수도 다르게 둘수도 있다.
등산로 0.1을 선택한다는 이야기는 카테고리 path01을 선택한다는 말과 다르지 않다. 메뉴에 직접 카테고리를 집어넣으면 레이어없이 두단계만으로 충분할 것도 같은데 이렇게 한 이유는 하나의 메뉴로 여러개의 카테고리를 왕창 선택할려면 이런식의 중간단계를 거치는게 자연스럽다. 여기서는 카테고리 하나에 레이어 하나니까 레이어의 장점이 보이지가 않는데 하나의 레이어에 여러개의 카테고리를 집어넣을 수 있다는 것을 알면 이런 구성방법이 어색하지 않다
해당되는 <rule>을 찾아서 위의 그림처럼 메뉴에서 정의한 이름대로 cat항목을 추가한 그림이다. rule자체가 각각 하나씩 늘어났다.
등산로 0.1과 picnic을 선택하면 이렇게 보인다. 등산로의 굵기가 다음지도의 녹색등산로 굵기보다 얇고 이정표로 나무 피크닉테이블이 나타났다. 봐줄 만하다.
등고선을 표시하는 맵스타일을
여기가면 구할 수 있는데 6000라인쯤 되는 xml파일을 열어서 읽어보자!
그러면 테마렌더링에 관해 더 깊게 이해할 수 있다.
최근 덧글