From 014eab2b7049f0b6eecd75f9bf973d9ee756ad00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Stra=C3=9Fburger?= Date: Tue, 20 Sep 2016 20:52:49 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=B2=20using=20r-tree=20spatial=20index?= =?UTF-8?q?ing=20to=20declutter=20the=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++++- package.json | 1 + src/LabelBuffer.coffee | 27 +++++++++++++++++++++++++++ termap.coffee | 35 ++++++++++++++++++++++++----------- tiles/europe.pbf.gz | Bin 0 -> 14059 bytes 5 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 src/LabelBuffer.coffee create mode 100644 tiles/europe.pbf.gz diff --git a/README.md b/README.md index bd6c141..a0b93d0 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Discover the world in your console! * [`node-mbtiles`](https://github.com/mapbox/node-mbtiles) for MBTiles parsing * [`pbf`](https://github.com/mapbox/pbf) for Protobuf decoding * [`vector-tile-js`](https://github.com/mapbox/vector-tile-js) for [VectorTile](https://github.com/mapbox/vector-tile-spec/tree/master/2.1) parsing +* [`rbush`](https://github.com/mourner/rbush) for 2D spatial indexing * [`sphericalmercator`](https://github.com/mapbox/node-sphericalmercator) for EPSG:3857 <> WGS84 conversions ## Wishlist @@ -41,8 +42,11 @@ Discover the world in your console! * [ ] mapping of view to tiles to show * [ ] label drawing * [x] support for point labels - * [ ] dynamic decluttering of labels + * [x] dynamic decluttering of labels + * [ ] centering text labels * [ ] lat/lng-center + zoom based viewport + * [ ] bbox awareness + * [ ] zoom -> scale calculation * [ ] TileSource class (abstracting URL, mbtiles, single vector tile source) * [ ] tile request system * [ ] from local mbtiles diff --git a/package.json b/package.json index e6b662c..3245a7a 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "drawille-canvas-blessed-contrib": "^0.1.3", "keypress": "^0.2.1", "pbf": "^3.0.0", + "rbush": "^2.0.1", "sphericalmercator": "^1.0.5", "term-mouse": "^0.1.1", "vector-tile": "^1.3.0" diff --git a/src/LabelBuffer.coffee b/src/LabelBuffer.coffee new file mode 100644 index 0000000..2c46f29 --- /dev/null +++ b/src/LabelBuffer.coffee @@ -0,0 +1,27 @@ +rbush = require 'rbush' + +module.exports = class LabelBuffer + tree: null + margin: 1 + + constructor: (@width, @height) -> + @tree = rbush() + + project: (x, y) -> + [Math.floor(x/2), Math.floor(y/4)] + + writeIfPossible: (text, x, y) -> + point = @project x, y + + return false unless @_hasSpace text, point[0], point[1] + @tree.insert @_calculateArea text, point[0], point[1] + true + + _hasSpace: (text, x, y) -> + not @tree.collides @_calculateArea text, x, y + + _calculateArea: (text, x, y) -> + minX: x-@margin + minY: y-@margin + maxX: x+@margin+text.length + maxY: y+@margin diff --git a/termap.coffee b/termap.coffee index 109943e..5b1f25b 100644 --- a/termap.coffee +++ b/termap.coffee @@ -6,6 +6,7 @@ fs = require 'fs' zlib = require 'zlib' TermMouse = require 'term-mouse' mercator = new (require('sphericalmercator'))() +LabelBuffer = require __dirname+'/src/LabelBuffer' utils = deg2rad: (angle) -> @@ -16,12 +17,10 @@ utils = digits: (number, digits) -> Math.floor(number*Math.pow(10, digits))/Math.pow(10, digits) + metersPerPixel: (zoom, lat = 0) -> utils.rad2deg(40075017*Math.cos(utils.deg2rad(lat))/Math.pow(2, zoom+8)) -console.log utils.metersPerPixel(16, 180) -process.exit 0 - class Termap config: @@ -84,7 +83,7 @@ class Termap lat: 49.019855 lng: 12.096956 - zoom: 0 + zoom: 2 view: [-400, -80] scale: 4 @@ -198,6 +197,9 @@ class Termap scale = Math.pow 2, @zoom + drawn = [] + labelBuffer = new LabelBuffer() + for layer in @config.drawOrder continue unless @features?[layer] @@ -212,25 +214,30 @@ class Termap visible = false points = for point in points p = [point.x/scale, point.y/scale] - if not visible and - p[0]+@view[0]>=4 and - p[0]+@view[0]<@width-4 and - p[1]+@view[1]>=0 and - p[1]+@view[1]<@height - visible = true + if not visible and @_isOnScreen p + visible = true p continue unless visible + wasDrawn = false switch feature.type when "polygon", "line" @canvas.beginPath() @canvas.moveTo points.shift()... @canvas.lineTo point... for point in points @canvas.stroke() + wasDrawn = true when "point" text = feature.properties.house_num or @config.icons[feature.properties.maki] or "◉" - @canvas.fillText text, point... for point in points + + for point in points + if labelBuffer.writeIfPossible text, point... + @canvas.fillText text, point... + wasDrawn = true + + if wasDrawn + drawn.push feature @canvas.restore() @@ -239,6 +246,12 @@ class Termap @isDrawing = false + _isOnScreen: (point) -> + point[0]+@view[0]>=4 and + point[0]+@view[0]<@width-4 and + point[1]+@view[1]>=0 and + point[1]+@view[1]<@height + _write: (text) -> process.stdout.write text diff --git a/tiles/europe.pbf.gz b/tiles/europe.pbf.gz new file mode 100644 index 0000000000000000000000000000000000000000..03664c42cb1c271d4268c5c227966dac89042275 GIT binary patch literal 14059 zcmVc8v-OVQOpu!mVm?J7)S_V9}MX z84QHUOc*X8{oVUK_YXflb>36w-G1*@E&p39FGm9Nx$zwn+lrGZL&}tYHl(Rt!-CMv<=Mv67GZs%N7#=470ge1~Qo zor0DE4U4^ULiwxtaES!8kt_qbf_v(hA>Yk8xZvuEPC7myL(!LMHNhBqR#tgn*o3`` z!Q`m_X9&Qb;Uog#_6oASh-?p^6K?-A^f^o=fJAlhJZ1$9`i#>H=AOw}W4{jYP%ZBPb}QywZs97%~9 zOa~9K16WbToM9L=!YckD{4?5vpwCA%PgV0~DMP&7^|kx~T7$s~0* zTtdiyrijYWT7HBs_v^e z>2RLL)Jko`P;!iVK@j2w(lWg0MhqZFAeI!xwycEfnlP@3-dyOiK}2n3b-DuK@XjO($E!P+z|cFfDGW`Il)MavG|?!jzd#w`kp zM2S)a^TmNu4<1{@h>D^y#)#DAv6!U?mfjAjm<*i~i62wuOp+XQnNczz&M1(LDo8Qo zs&raJisuUwX9;{ZPWdv#mW{#K|3(Bh+)ruZs^7quk)hC#SW&PIkD`bT*KZ{YOcp~| z7y^Q7Xtn1;KRyZZB!470KyiLXV2w!0+lbfKJ#eMT5?ecQ0ce`S8BPRrWTg09bkZ{j z_{l2mbt<<&iAjn994csL9Cl>5c&?Jf)VM_R4be@AOk5Ha%*c{__DFt2&sk~C6j(}* z=>*F7tLS6aN4`_wil)YpJDUGO|(*HJaqxSJ$fGH$<}V#7X4DYP?5yKYK6o`~+|uifNJiSR;O-M`eVR?V*moSS)Bc8huB+JVN zKrA+_Lf09Ix93D!F2;&kf%cNIc0H2G;OKcqE!mb96;b8Z5a$|up&=QtDaOYk?0h3q6tU_UDZ1idyKEbWSv}=;%?RDR>iNh*w3aUSCt3 z)v-ioC2|(qU>9~-&jJo7wbNb#`F2z^A{5&tl4#{ushZq{YLz_iM@+7$^K!lrRE0Sj z-6OXrJw>u9*+PaTB40z46_42YGCL@ko}$DB?EHi#09QYuhz&L$Pz*U$OsaV9_)^%@ z-L*R6#kqK-i29WzN2z$h8lwa;!PPnQma>x0a(Rs<;SJa{7FCi+5_CNV{iGC88ODYr zJq(G4Bmh^%1)BFAmt2Xs@&dhCi+6^G|0BdN3LnJ|FjPEe zkD-JK^rX(>#=Do{0KY;6z2&WkgAci9jyu3cZA*N45xqUPhHU!M0tGH}ph21p~d! zWPt69TD(mI|BEM)?iw)Goweg}oli%#jA}+%@Q-kk2Vl3-PAAo3mw96zSwuw=O2y`p90fQM2((&B0?XCXvMg{aqlNj^P#A`!gXho^pLZ-3 zMM%lQK5q#OMGEEInlC91wn^v}eufni_^xW~8N zX0O%^NNM2s)_cr%G$T^6np-~z^Un83-q9wtd(qt(s5T!rd((Z&D!Gu1aEhBVPcg2A zCP;qXPDXmbP&7@sY2?HMj*Vd4?${Xl(Wogv&D7zTb4uB?M^t<}z0qg+(=#ps)`JWwI}$IOO$(Sr4;kM>Pij6%+0p58JgqR%ENM`f6f3gz zW(b#`BRuK7W$tE2>T=MZpC4PriN!c0It?$EH3!mA?r?R^OBm6Zob|Mfl3ZCx_@gOx zsh6*D8B0QK`7~AWA)E9Bpa)T`S9|xOZ$Pkzjc9UApqap;SOuOWOH7qkc-<1fU)#yE zD#_d}uq_v4SK6rv&6T5~lrhsM^wq?u6e+7>jU}xnW(HF@2}Y`hrW$|seLeJS&>?$ z`*ZARO44A2kp%>Q+n+>!wod0%5|$s)e{C>HlC6k=pQng{pP}%dyk95RSd6SGLY&ml z2q(pK5|u>~@~_fAqWADh#+GYrp?gkyH%P}wB8P%yYHiV+VRTi4fYpi~r1&ICrsM3A zINy#dPA$4tWu-IcbwvgSF3+4VomA#yrVo~vwbd#QyN0X=in{dL%sdq=h;b!tb?*g@ z;xdpoxunDNos1ezrnU^;38f`WCEcDql|OATvx8T=C! zhWN+KrsZ*tGCU#Dg;~>86-rA8*}TfyrtbJsVy-6GshR~HKbDRov!~k>=O&&;qE%61F#)-1v>iucCuG4jax)S9 zmcC0393DtzI6@|%udw3;lD?@DfPS0GlDbt!c16W=u^@GL6_m z|FD)Q$y~{j1wn`yJdBDAs|Spf>=J{vws~PbxEDPHOT>O-0}#yD$wq2uz!01Wo6Iw! zY0L9Siv34899T{fN@A3#eNx>pM2c{G4_*V;=&~)yV_Gqx2Tr2X;Mkf@VE(UYM_?(e z40M-RmmeIBITn@8mT)n%U_p}35&t|%;Zdes;oTrso~ssX`hw8r&!BqUq*KEwUeV>j z2(kC~xsDRf(tHUUZj|&-zhaXm*O#O$vf>QM?*BPwO=!qthjJcxG>5N|u;Ynm}71Css ztd8O#4hfA)vQg)7E(YwJBel)p+6M#NJ#FgnW z-F4$X?ve2s|oEoxO= z$!GIfs@PbTNfT`zfq#$oKsmpvY!4-1d~s4!Q?z&bSSfy4T(b3kK27A0c2?TYB1wt; zgB&=%PH_>TIei}t6UnzpA{qXZ|2k7Eiz(jkalvlXsGXdyq)(-7%xOvU3>^e&!eWg9 zGvCI_xr%EJwkIAW*DBEj8nVkGaw@V*jnr&`s^p;UBd>$XWi>(S zOY?5jWYvVhn+mP0T~O1&m!i$o_7CjdxQ{v%3!SXuucJuNl_gk9)(8%cp$iQGgyw&c zU*#tRK_y|1AA-L?OZ9TWp{1g_JXNQu?zT#r;N)&p^Mfu{jgt)dGsx7jL?;x5^!-th zIN&z;FnS;ISrdW9GDKc^aXl7m_}0pZlSo7gBV510Bq*`U2AZE&iUlg_o6F{8_<8+q zikrJ`lZszLPs8=6jE?aguFxh6y1js6;uSI&VuHxuQ&}><@p08=2r4JpUNW>&Bu701 zFZ5+AfKs!kyfti|ZbbNFbanM!25Lr?OF0g&d$e!@j=TtgqcN;S@qXU1MrAU{8LSsY zq?zWTwraGM1uMquy^W24DmhAih_s-rk{Sj;iHjyLTBP6a;8n{5ufhV?8?ytUtou=^ zKXaNL?_C$I(497;t2F7OIp}tD#~ZO)+>qlU&xxyI^WP9!}%IZN(Y?Pd-4y}e3b+rXwD^{gq`(oA$|=XsJ%dwsXZnO z9WZH)LEiEC6p8B;Jz6efGRuRwmvTtPdl@!ZU1S6q6RcCGme$5k<3xLzjO_@f{{i}j zImePT#*R3gWw8tsos)JjrE5aEL2@mVDq`C+xJpXAz>21Zx)*~$>68TFJm~sVB1>UK z^F+bWT$WQgOT||_3I~d`qKq=M8X-X4K$6)HV8!4(hZL5EYcQ6g{_?t6$Plw! z)?r8#@d4)2fdv@B0->=6qlUrhBq!);gXM7Z!&E!bK8OEZ`lPl^wO~o;n}07%JK%po z1u|;P&S+%1PiCb=GaW1>hllYL^jlH6N2B(0OFQO(}-d~(k@q#J#+@+uU0rQ{?9R2o%Lv7^LigE(Fs z1DKPP+zO29B&d743rs=cWF5(6qRM^nNoZ&=>?cIg$kHx}Sj-Smgx|bQogkRN&2i(N z(7RuQ_!D`uvWL!TNJxVq8(~FK2~!sRD$G$DsZg=T`z*;i;UI_Yf8`jKy<;#2S1ZQbv2MYB7N$EUn~e z-mbBvt4EWD^MvockC8!X<99zzeo2GwF=T zMHHL|l;F^+{3-+}s|XYX3Xe4sM;D&r`xG!k7i?%BP^|bQ2nC4N;i#f|h|4A{nDSU!&T{cIO18J3-4OAhp)$6w z!e|CW9wJgqqE0|+E+eUoYWApD&iK&R7({w^_6}G=iXhU8#;MI5@k}Vm0f^u0oE+JM zL>pDa$RfSrh<=GRQi~bv+1S?7AnOTPf|4{;6s^c|tRjH{8BiRHHNb*Ff2sk^V@3+XxGBC+{U!Z(P;3-)?{_Dhh)nAVIsl=Cw=KbMrFWIf($*w<* zEVVfh>970^+B84gR<$9x4Nit%M1Mq`NLb7x%q-nEfN9#zd~q6iaUGuQn;bm=KL;2J z(bqwsag1uIK+iBJ(Ux!l~B!xdV6<;YnP}G*8d2Bbp+xm{hh8CXonlUtgQ8Q zl!TQTgZAcn$`^#9aW*61lC;Jo0|u=W!P|UjnzhyFDW~m({{m6Y!738wg`@KoTFy#S zISpKxaCgCUDtc}*rXp99FH1`45@Y2$L^PL+}!cxtzV?#%|+R1SoO8inN@lj&=^vnGpQtE2Qg)u=yngn+6`5`o!jrH_`aE?Ae4UIm^2>@}>QFr{*;$2!+bfIiYnXu6nbpVyXd3_{YS zX^J=2lO4!g-k7gF69SSu3Gn$u35YBUB?;4-M_PVPP#Mc%)o?mI%=$r|HccY)vTh?G zg@fpZXGye3fW-F59V9Js;m1e1)8Gy53>7DCL52|i#6y$`h%PbwVP@~(8ki+B&~B8I z-P0nFbBmt?oqki{zY7(95#q_>$8#T{GrZ_v2s+lfc85~>Jp{6wQyeue<_ZF4I8k6Y z&@08(&vi%z%q@T?cMjH);5y{MqT>a?_ZZ4ck$fkQ4(HTpV3<+I;ANSUaDrakW-W0y zMcYnZ#d3w3Q3-OC=z&-y#Q-_sX|g5AF^7b!stYd9o#-DGUNI#ZFfk>N@q1P4I*$D{gtgd zGGmrE0i$L}DWt%Be2RT}(!6nbH!Ac~g_*qR)G3F$>bElEs3At5OL`)C7=E0#@zS>ELk! zz28%lG^xyg1K+0%8i!x{1%f3?C%MU}mGV6j#imu2;2kg23|0*LCpgcb6?Rtjm@I!d z&j-qb5E!$*Sx=Vt4H{S!sK0<$b#nL->TeN1c1eo5mJ01mAW|gs3Z}-zq`oIe36f(> zu~WpoMer?X_d_fR&OyplS&p=|B?5~3V27Qm^n?HXJaRzSzlF#B~PEr-5)}o=V zouyP3St*6<4&m2cAwk78o86~EjrK!)sQrQJ9;U4d1P?MsL_pH=Mo9*TxvL~zQM^(s>9neTjQAUkJpucuTTbxgyv*-ijy#r`MSvQKbJY`5yG(e*fgI5in z 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