'osmosis'에 해당되는 글 2건

  1. 2009.07.24 OSM 작업노트 #14: cleanup 2
  2. 2009.07.09 OSM 작업노트 #13: 도로 업로드 작업 2

OSM 작업노트 #14: cleanup

GPS 2009. 7. 24. 16:36
OSM 작업을 하면서 가장 불편한 것은 한글과 영문 이름 병기다. POI 마다 한글명과 영문명 태깅을 하는 것보다는 일단 한글명만 적어두고 나중에 한꺼번에 바꾸는 것이 효율적일 것이다.

한글의 romanize는 node 또는 way에 name:en 태그가 없고, name 태그에 괄호 '('가 없을 때만 로마자 변환을 한다. 괄호가 있는 것은 괄호 속에 영문 표기를 했다고 가정하므로 변환 대상이 아니다.

두번째로 작업중에 실수로, 또는, 삭제하다가 프로그램이 다운 되는 등, 부지불식 간에 생긴 orphan node을 삭제한다. orphan node란 아무런 tag가 지정되어 있지 않고 way의 맴버가 아닌 node를 말한다. 이때 tag중 created_by는 별 의미가 없으므로 무시한다. tag가 지정되어 있는 node는 그게 무엇이라도 삭제하지 않는다.

사용자 삽입 이미지
고아 node의 예. 태그를 가지고 있지 않음. way에 소속되어 있지 않음. 위의 경우 way 삭제 중 사고로 way는 지워졌지만 way의 맴버였던 node가 삭제되지 않은 경우다. 가끔 이런 사고가 발생한다. 이걸 손으로 지우는게 많이 귀찮다.

일단 두 가지 작업을 위해서 남한 OSM 파일을 얻어야 하는데, cloudmade.com에서 매주 제공하는 남한 OSM 파일은 완전한 것이 아니라서(남한의 일부 영역이 잘렸다) planet.osm 파일을 직접 다뤄야 한다. cloudmade.com에는 내가 만든 south_korea.poly 파일을 보내줬다. 그들이 그걸 사용해서 남한 OSM과 IMG를 만들어주면 좋겠다. 그렇게되면 한국의 GPS 사용자들이 변환 등의 거추장 스러운 작업 없이 Garmin용 IMG 파일을 거저 다운받을 수 있게 된다.

매일 커지기만 하는 6GB 용량의 planet.osm을 작업할 때마다 매번 다운받을 수도 없는 노릇이다. 2009/7/24일 평균 1Mbps의 전송속도로 2시간 가량 걸렸다. OSM 역시 그것을 염두에 두고 있어서 일주일에 한 번씩 planet.osm을 덤프하고 다음 7일 동안 매일 또는 매 시간 단위로 이전 일시부터의 변경 내용만 덤프한다. planet.osm의 대표적인 mirror site에 이런 변경 내용이 매 시간 단위로 업로드된다.

즉,  planet.osm을 한 번만 다운 받고 매일의 변경 내용을 다운받아 패치하면(일일 약 20MB 가량) 최신 버전 파일을 유지할 수 있다. 전 세계 데이터가 다 필요한 것은 아니므로 한국만 계속 업데이트 하면 될 것이다. 처음에 작업할 때는 planet.osm 원본 파일을 사용하여 한국 지역만 들어내고 changeset을 적용한다. 7월 22일 받은 planet.osm.bz2와 22-23일 변경분, 23-24일 변경분으로 작업하는 과정:

bzcat planet-090722.osm.bz2 | osmosis.bat --rxc file="20090722-20090723.osc" --rx file=- --ac --bp file=skorea.poly.txt --wx file="korea-090723.osm"

그 다음부터는 이렇게 해서 만든 korea-*.osm에 changeset을 적용한다.

osmosis.bat --rxc file="20090723-20090724.osc" --rx file="korea-090723.osm" --ac --bp file=skorea.poly.txt --wx file="korea-090724.osm"

사용자 삽입 이미지
orphan node 제거 및 이름 변경용 어플리케이션으로 삭제할 노드 리스트가 담긴 osm 파일과 이름이 추가된 osm 파일을 생성한다.

생성된 두 파일을 가지고, JOSM을 이용하던가 python으로 짠 delete 프로그램과 modify 프로그램으로 각각 OSM에 적용한다.

주의: JOSM은 way의 이름만 변경하더라도 way가 참조하는 모든 node가 기술된 'complete OSM file'만 적법하다고 인정하므로 modify된 way 태그를 업로드할 수 없다.

아무래도 이 작업을 가끔씩이나마 하는 것이 좋을 것 같다. 이번에 몇몇 산들의 트래킹 코스를 작업하면서 일일이 영문으로 토달기가 귀찮아서 일을 벌인 셈이지만 주기적으로 이런 작업을 해주면 한국 지도가 깔끔해 질 것 같다.

아무래도 주말 중 하루는 종일 컴퓨터를 돌려야 할 것 같다. 뭐, 말로 적어 놓으면 쉽고 간단하지만 컴퓨터로는 CPU 부하율 100%로 하루 종일 뺑이치는 작업이다. 다운로드 2시간, planet.osm 뜯어내기 3시간, 2-3일 변경분 적용 1시간. 고아 노드 분리 10분. 노드 삭제 3시간. 노드 업데이트 1시간 가량 예상.

트래킹 코스는 다음 GPSGIS 카페에서 수집한 산행 트랙로그를 GPX로 변환한 다음 OSM 홈페이지에서 업로드하고 Potlatch로 일일이 라인을 그리고 POI를 손봤다. 상당히 시간이 많이 걸리는 작업이지만 GPS Trackmaker 프로그램에서 Tracklog를 reduce 해서 그대로 업로드하는 것보다 정밀하다. 왜냐하면 동일한 산행 코스 트랙로그를 여러개 겹쳐 그중 가장 그럴듯하고 올바르다 싶은 길을 라인으로 그리면서 따라가기 때문이다.

만일 그것을 프로그램으로 짜서 했다면 그들 트랙의 평균적인 길을 고르게 되는데, 산행 중 GPS의 오차가 거치 방법에 따라 현저하게 차이 나기 때문에 프로그램으로 판단하는 것은 오류가 클 여지가 있다. 뭐 잘 짜여진 룰베이스를 가지고 스스로 백그라운드 야후 위성 사진에 나타난 도로와 GPS 트랙로그의 오차를 보정하면서 좌충우돌 학습하는 인공지능 프로그램이라면 얘기가 다르겠지만 그런 프로그램은 짜기가 무척 어렵다. 그 프로그램 짜서 키울(?) 시간이면 한국의 100대 산 트래킹 코스를 완성하고도 남는다.

GPS 위성은 매우 고속으로 지구 궤도를 움직이고 있다. GPS 위성의 시계는 그래서 특수상대성이론에 따른 시간 지연이 발생한다. GPS 리시버에서 발생하는 트랙로그의 오차는... GPS의 매우 정밀한 클럭에 일정 정도의 노이즈를 넣어 시간 정밀도를 일부러 떨어뜨리는 SA(Selective Availability)에 의해서만 결정되는 것이 아니다. 보정 기술의 발달로 SA의 영향은 최근들어 상당히 작아졌다. 그러나 트랙로그가 기록될 때 GPS의 프로세서는 측정 시점과 기록 시점이 완전히 일치하지 않는다. 그리고 이전 측정 시점과 다음 측정 시점 사이의 시간 간격이 측정 정밀도에 영향을 끼친다. GPS 기기에 오차가 +-4m라고 표시되도 사실상의 오차는 +-20m 까지 감안해야 한다. 언급한 세 가지 이유 때문에 GPS의 트랙로그는 그렇게 믿음직 스럽지 않다.

사용자 삽입 이미지
능선이나 개활지에서 걸을 때, 즉, 저속일 때에는 여러 개의 트랙로그를 겹쳐놓아도 위치 오차가 크지 않지만,

사용자 삽입 이미지
하늘이 잘 보이지 않는 계곡에서는 위성 수신 불량으로 오차 폭이 대단히 커지고,

사용자 삽입 이미지
위성 수신 상태가 양호하더라도 고속으로 운행중인 자동차에서 같은 구간을 운행하더라도 그림에서 보는 것처럼 동위도에서 기록 오차가 발생하며, GPS의 측점 간 오차가 GPS에 표시된 오차보다 크게 나타난다.  

하여튼 데이타가 그다지 크지 않을 경우 사람이 트랙로그를 여러 차례 만들어 겹쳐 놓은 상태로 보고 '궁리'하며 그리는 것이 개발도 힘든 인공지능 프로그램을 짜는 것보다 낫다. 이쪽은 그래서 프로그래밍을 포기했다(누군가 하겠지).

트래킹 자료가 어느 정도 진전되면(한국의 100대 명산) KOTM 새 버전을 만들어볼 생각이다.  지금까지 서울 근교 5-6개의 유명 산행로를 '그렸다'. 북한산 및 도봉산 트래킹 코스




,
OSM의 전 세계 모든 데이터를 다 담고 있는 planet.osm 은 일주일에 한번씩 업데이트된다. planet.osm에서 한반도만 뜯어내려면 원래 그럴 용도로 만든 osmosis 프로그램을 사용한다. 2009.7.2 현재 planet.osm 파일의 크기는 무려 160GB, 압축한 파일의 크기가 6.2GB이다.

win32에서 osmosis를 실행하면 십중팔구 안 된다. 이유는 osmosis.bat 파일에 library path가 잘못 지정되어 있기 때문이다(linux에서는 상관없다). osmosis 0.31 버전 기준으로, osmosis가 설치된 디렉토리의 하위 디렉토리에 있는 ./lib/default/*.jar 를 EXEC 환경 변수에 모두 추가하여 osmosis.bat 파일을 만드는 편이 빠르다.
@ECHO OFF
set JAVACMD=java.exe
set JAVACMD_OPTIONS=-Xmx1024m
set MYAPP_HOME=D:\luke\Documents\private\gps\tool\osmosis-0.31
set LIB=%MYAPP_HOME%\lib\default
set MAINCLASS=org.openstreetmap.osmosis.core.Osmosis
SET EXEC=%JAVACMD% %JAVACMD_OPTIONS%
SET EXEC=%EXEC% -cp %MYAPP_HOME%\osmosis.jar;
SET EXEC=%EXEC%%LIB%\bzip2-20090327.jar;
SET EXEC=%EXEC%%LIB%\commons-logging-1.0.4.jar;
SET EXEC=%EXEC%%LIB%\jpf-1.5.jar;
SET EXEC=%EXEC%%LIB%\mysql-connector-java-5.1.6.jar;
SET EXEC=%EXEC%%LIB%\postgis-1.3.2.jar;
SET EXEC=%EXEC%%LIB%\postgresql-8.3-603.jdbc4.jar;
SET EXEC=%EXEC%%LIB%\stax2-api-3.0.1.jar;
SET EXEC=%EXEC%%LIB%\woodstox-core-lgpl-4.0.3.jar
SET EXEC=%EXEC% %MAINCLASS% %OSMOSIS_OPTIONS% %*
%EXEC%
planet.osm 파일이 워낙 커서 보통 배포되는 형태인 planet-090702.osm.bz2 파일을 압축을 풀지 않고 그대로 작업한다. osmosis의 위키 페이지에는 이런 식으로 명령을 지정하라고 적어 놓았다.
osmosis.bat --read-xml file=planet-090701.osm.bz2 --bounding-polygon file=skorea.poly.txt --write-xml file=skorea.osm
osmosis의 bzip2 라이브러리의 버그 때문에 실행 하면 몇 시간 잘 돌다가 exception이 발생하며 프로그램이 중단된다. 오래 전부터 알려진 문제란다. win32용 bzip을 얻어서, 실행파일인 bzip-xxx.exe 파일을 bzcat.exe로 복사해 놓고 다음처럼 실행한다(리눅스에는 bzcat이 기본적으로 설치되어 있다).
bzcat planet-090701.osm.bz2 | osmosis.bat --read-xml file=- --bounding-polygon file=skorea.poly.txt --write-xml file=skorea.osm
osmosis의 입력 파라메터로 지정하는 bounding-polygon 파일 'skorea.poly.txt'는 osmosis 위키 페이지에서 참고하라는 각 국가별 polygon 파일을 봤는데, 좀 괴상하다. 남북한 국경선도 명확하지 않고 울릉도, 독도 등이 빠져 있다. 해서 확실한 국경선은 아니지만, 하나 만들었다. skorea.poly.txt:
south_korea
1
124.50082 33.88033
126.84860 32.67590
128.86031 34.19477
130.32874 35.89549
132.62271 37.16911
128.90661 38.93043
124.46191 37.55735
124.50082 33.88033
END
END
이걸 어떻게 만들었냐 하면... 궁즉통, Google Earth에서 대충 한국을 포함하는 polygon을 그린 다음 그것을 kml로 저장해서  좌표를 텍스트 에디터로 옮겨 적었다. 남한의 국경선 폴리곤이 혹시 있지 않을까 싶지만 구글질해서 성과(?)를 얻는 것보다 직접 아웃라인을 따내는 시간이 훨씬 적게 걸린다. -_-

osmosis로 160GB짜리 파일을 처리하는데 얼마나 많은 시간이 걸리는지 모르겠다. 밤에 작업을 걸어두고 잤다. 2시간 40분쯤 돌다가 exception이 발생하며 bzip2 라이브러리 문제로 프로그램이 죽어버렸다. 파일이 깨진 것 같아 다른 서버에서 다운로드 받아 다시 돌렸다. 3시간 정도 걸려 skorea.osm 파일을 얻을 수 있었다.

skorea.osm 파일을 얻은 다음, 일단 잘못된 저수지 정보를 모두 삭제했다. 삭제는 bulk_upload.py를 수정해서 만든 조그만 python 프로그램으로 돌렸다. osmChange를 이용해 한꺼번에 여러 개의 node,way에 대한 작업을 수행하는 것인데, changeset중 단 하나라도 entry에 문제가 있으면 업데이트가 되지 않는다. 그 때는 changeset을 새로 만드는 방법 밖에 없다. JOSM도 동일한 문제가 있다. 예를 들어 하나의 changeset에 100개의 노드를 삭제하는 delete element가 있다고 하면,
<osmChange>
     <delete>
          <node id="34132450" ... />
          <node id="34132452" .../>
           ...
    </delete>
</osmChange>
34132452번째 노드를 삭제할 때 에러가 발생하면 100개의 노드에 대한 delete 작업 전체가 수행되지 않는다. OSM에서 프로그래밍을 하는 사람들이라면 반드시 알아야 할 내용인데 OSM development 섹션에서는 언급이 안되어 있어 메모해 둔다:

Error:  409 :  Version mismatch: Provided 0, server had: 1 of Way 36898268
삭제하려는 node의 버전은 1이고 서버의 해당 노드의 버전은 2이면 삭제되지 않는다. 삭제하려는 노드를 누군가 다른 사람이 작업해서 버전 번호가 달라진 것이다.

Error:  410 :  The way with the id 35355795 has already been deleted
이미 삭제된 노드로, 이 경우에는 에러로 간주되지 않았으면 좋겠는데 이런 에러 때문에 전체 changeset이 commit되지 않는다.

Error:  412 :  Precondition failed: Node 414576118 is still used by way 36898266
삭제하려는 node를 사용하는 다른 way가 있을 경우 발생하는 에러. 이게 가장 골치아프다. 왜냐하면 삭제하려는 node가 만약 어떤 way에 소속되어 있는데, 이미 그 way가 삭제되었다 하더라도 다른 way에서 이 node를 사용하고 있다면 이 node를 삭제하면 안 되는 것은 맞다. 그런데 해당 node가 어떤 way에 속해있는지 사전에 알 방법이 애매하다. 전 node, way, relation을 메모리에 갖고 있던가 그 정보를 db에 넣어 해당 node가 다른 way에서 점유하고 있는지 조사해 봐야 하는데, 삭제하려는 node가 300000 이고 한 node 조사에 100msec이 걸린다면 삭제대상 노드 조사에만 30000초(약 500분)이 걸린다.

osmChange를 사용하지 않고, changeset을 만든 다음 http DELETE로 삭제하는 고전적인 방법을 사용할 수도 있다. 이 방법을 사용하면 한 node에서 발생한 에러가 다른 node의 삭제에 영향을 끼치지 않는다. 그런데 그 방법은 시간이 오래 걸려 비효율적이다. 개당 500msec이 걸린다면 300000개를 삭제하는데 41시간이 걸린다.

그래서 python 프로그램은 먼저 한 번에 111개를 한 세트로 하는 changeset을 사용해 11개 element를 삭제하고, 삭제가 정상적으로 이루어진 element의 id를 기록해 둔 다음, 다음번에 python 프로그램을 다시 실행해 이번에는 53개씩 삭제, 다음에는 23개, 그 다음에는 11개, 5개, 3개, 2개, 그리고 마지막으로 http DELETE로 1개씩 삭제한다. 갯수는 100, 50, 25, 13, 6, 3, 2, 1에 근접한 소수를 사용해서 이전 changeset이 다음 changeset의 배수가 되어 같은 에러가 동일한 위치에서 발생하지 않도록 한 것.

손이 많이 가지만 이전에 잘못 올린 저수지및 호수 데이터 160000개를 삭제하는데 4시간 가량 걸렸다. 사실 작업시간 보다는 changeset의 특성을 파악하고 적응하는데, 특히, 412 에러를 회피하려고 갖은 꽁수를 써 보는데 시간이 많이 걸렸다. 처음에는 c++로 작업하다가 python 프로그램이 워낙 간단하고 편해서 python으로 바꿨다.

smoothing이 적용된 저수지및 호수 데이터(ncat=저수지및호수2)를 저번 주 금요일 저녁에 올리기 시작했다. 월요일에 정상적으로 올라간 것을 확인했다.

교통정보센터의 수정된 도로를 올리기 전에, 이전에 내 아이디로 만든 highway, trunk, primary, secondary 도로를 삭제하기로 했다. 진행 전에 한국 OSM 작업자들에게 도로 업로드 작업을 이번 주 중에 진행한다는 메시지를 작성하여 전송했다. 욕도 일찍 먹는게 낫다고, 이번에 도로 데이터를 올리지 못하면 일이 점점 더 커질 것이다. 한 두어 달을 올릴지 말지 망설이며(서울, 부산, 대전, 안동이 그 당시에 작업이 꽤 진척된 상태였다. 그 작업을 다시 해야 하니까 작업자들에게 미안해서 어떻게 하면 작업양을 최소화하면서 도로 업로드의 당위성을 설명할까 망설였다) 한가한 고민이나 하던 사이에 홍의님이 전주시와 인근 도로 작업을 상당히 진척시켰다.

도로는 월, 화요일에 모두 삭제했다. 올리려고 하는 도로 데이터의 검토 역시 대충 마무리 되었다. 고속도로는 양방향 lane을 그대로 유지하기로 했다. 수요일 OSM 서버의 업데이트가 있기 전 고속도로를 먼저 올려 JOSM과 Potlatch로 상태를 확인했다. 고속도로만 올리는데 3시간 가량 걸렸다. XAPI가 맛이 가는 바람에 조금이라도 일이 틀어지면 다음에 그것을 교정할 기회는 1주일 후에나 주어진다. 특히나 대량의 업로드의 경우는 문제가 심각하다. 조심조심 작업해야겠다.

화요일 밤부터 국도를 올리기 시작했고 수요일에는 도심로와 지방도 업로드를 시작했다. 다 합쳐 20시간 가량 걸렸다. 프로그램이 수고가 많다. 이전의 bulk_upload.py를 좀 더 개선한 소스. bu.py:

[code] #!/usr/bin/python # -*- coding: utf-8 -*- import time import socket import xml.etree.cElementTree as ET import sets import optparse import pickle import os import sys import httplib2 import re socket.setdefaulttimeout(300.0) headers = { 'User-Agent' : 'BatchUploader/0.1', } api_host='http://api.openstreetmap.org/api/0.6' userid = "#####" passwd = "#####" cachefn = "" demo = False idMap = {} testid = '1' class UploadFailed(Exception): pass class ImportProcessor: def __init__(self): self.user = userid self.password = passwd self.e = ET.Element("create") createReq = ET.Element('osm',version="0.6") change = ET.Element('changeset') createReq.append(change) if demo: self.cid = '1' print 'Create changeset ', self.cid return resp, content = self.request(api_host + '/changeset/create','PUT', ET.tostring(createReq)) if resp.status != 200: print 'Error Request changeset ', resp.status, ': ', content raise UploadFailed(resp.status, content) self.cid = content print 'Create changeset ', content def request(self, url, cmd, xml): con = httplib2.Http() con.add_credentials(userid, passwd) fc = 0; failed = True while failed: try: resp, content = con.request(url, cmd, xml, headers=headers) return resp, content except socket.error, msg: fc += 1 if fc > 5: raise print 'Request Error:', msg time.sleep(2) def add(self, item): item.attrib['changeset'] = self.cid self.e.append(item) def upload(self): xml = ET.Element('osmChange') xml.append(self.e) if demo: return 1 resp, content = self.request(api_host + '/changeset/' + self.cid + '/upload', 'POST', ET.tostring(xml)) if resp.status == 200: # print "Return:", content er = ET.fromstring(content) for child in er.getchildren(): old_id = child.attrib['old_id'] new_id = child.attrib['new_id'] idMap[old_id] = new_id return 1 # 그외의 에러 print 'Error: ', resp.status, ':', content return 0 def closeSet(self): if demo: print "Closing changeset", self.cid, '\n' return resp, content = self.request(api_host + '/changeset/' + self.cid + '/close', 'PUT', "") if resp.status != 200: print "Error closing changeset", self.cid, ":", resp.status def doJob(osmData, etypes, csize): cnt = 0 ip = None for e in osmData.getiterator(): if e.tag not in ( etypes ): continue # 이미 기록한 것은 다시 하지 않음 eid = e.attrib['id'] if eid in idMap: continue if ip == None: ip = ImportProcessor() # way 또는 relation이면 이전에 기록된 id로 기록함. if e.tag == 'way' or e.tag == 'relation': for child in e.getiterator(): if child.attrib.has_key('ref'): old_id = child.attrib['ref'] if idMap.has_key(old_id): child.attrib['ref'] = idMap[old_id] print 'create [', ip.cid, ':', cnt, '] ', e.tag, eid ip.add(e) cnt += 1 if cnt >= csize: ip.upload() f = open(cachefn, "w") pickle.dump(idMap, f) f.close() ip.closeSet() ip = None cnt = 0 if cnt > 0: ip.upload() f = open(cachefn, "w") pickle.dump(idMap, f) f.close() ip.closeSet() usage = "usage: %prog -i input.osm" parser = optparse.OptionParser(usage) parser.add_option("-i", dest="infile", default="", help="read data from input.osm") (options, args) = parser.parse_args() if options.infile == "": print "specify input osm file" raise cachefn = options.infile + ".db" try: if os.stat(cachefn): print 'Read from cache file:', cachefn f = open(cachefn, "r") idMap = pickle.load(f) f.close() except: print "no cache found" osmData = ET.parse(options.infile) st = time.time() doJob(osmData, ("node", "way", "relation" ), 500) print "\nJob done. %.0f" %(time.time() - st), "secs ellapsed" [/code]

OSM 서버의 느린 응답 때문에 에러가 빈번하게 발생하여 프로그램이 자주 멎는다. 프로그램 탓은 아니다. changeset이 완전히 반영되지 않으면 idMap에 기록하지 않도록 하고, 한 번에 commit하는 element의 갯수는 2000개나 5000개가 아닌 500개 정도로 제한해서, 시간이 더 걸리더라도 안정적으로 작동하는데 촛점을 맞췄다.

아울러, 저 것과 같은 기능을 하는 프로그램을 처음에는 VC++ 2005로 작성하려고 했지만 c++로는 작업량이 상당했다. 무엇보다도 expat SAX xml parser 때문에 로직을 짜기가 어렵다. c++에서도 python처럼 쉽게 작성 하려면 XPP(xml pull parser) + DOM을 사용하는게 마땅하다. python 프로그램은 한 시간도 안 걸리는 것을, 그것과 등가한 VC 프로그램을 작성하려면 1-2일쯤 걸릴 것 같다.

수요일 밤에 작업이 끝났다. 업로드에 15시간 정도 걸렸다.  작업 시간을 잘 맞춘 덕에 업로드된 내용이 그 다음날 바로 반영되었다.

서울 부근의 겹치고, 태그가 불분명하게 설정된 도로를 다시 정리하는 작업을 진행 중.

사용자 삽입 이미지
OSM에서 mapnik renderer로 본 지도. mapnik으로는 그간 무슨 삽질을 했는지 알 수가 없어서,

사용자 삽입 이미지
Osmarender로 렌더러를 바꿨다. 저 촘촘한 도로들이 지난 한 달 동안 작업한 것. 이것으로 한국 지도 작업을 진행하기 위한 기본 뼈대가 어느 정도 갖춰진 셈이다. 갈 길은 여전히 멀다...

,