概要

IIIF (International Image Interoperability Framework) Presentation API v3のマニフェストに含まれるアノテーション座標(xywh形式)を、Leaflet-IIIFを使用したマップビューアー上で正確に表示する方法について解説します。

この問題は一見シンプルに見えますが、Leaflet-IIIFの内部動作を理解しないと正確な座標変換ができません。

問題の背景

IIIFマニフェストのアノテーション形式

IIIF Presentation API v3では、アノテーションの対象領域は以下のようなxywh形式で指定されます:

{}""""}"itmb,tdyoo"""a"ptdtvlr:eiyyaag"v"plne":a:eugtht"eu"t"i{:"a:tAo:gpnn"e"sn"T""h::e:ttxt/a"t"petcu"jsxioa,a:aoml"/mnmB/p"eoel,ndxetya.i"mon,prglg"e/,.ioirigf//iciainfv/acsa/n1v/aasn/n1o#txaytwiho=n4/110"1,2,81,115,49"

このxywh=41012,81,115,49は:

  • x: 41012(左端のピクセル位置)
  • y: 81(上端のピクセル位置)
  • w: 115(幅)
  • h: 49(高さ)

を意味します。これらは元画像のピクセル座標 です。

Leaflet-IIIFの座標系

Leaflet-IIIFは、IIIF Image APIで提供される高解像度画像をタイル形式で表示するLeafletプラグインです。内部的には:

  1. CRS.Simple座標参照系を使用
  2. 画像を複数のズームレベルで縮小して管理
  3. ズームレベルごとに異なる座標スケールを使用

この複雑な座標系のため、単純にmap.unproject()pointToLatLng()を使っても正しい位置に配置できません。

試行錯誤の過程

失敗した試み1: map.unproject()の直接使用

ccoonnssttploaitnLtng==L.mpaopi.nutn(pxr,ojye)c;t(point,3);3

問題点 : unproject()は現在のマップの座標系を前提としており、Leaflet-IIIFが内部で使用している座標系とは異なります。

失敗した試み2: マップ境界からの比例計算

cccccooooonnnnnssssstttttbnnllooonaurrgtnmmdXY==s==bb=ooiiuummmnnaggddpXYss...g/ggeeetiittBmmWNoaaeouggsrneettdWH(hsie)((di))tg;hh;t(;n(onromrXmY(b(obuonudnsd.sg.egteEtaNsotr(t)h(-)b-oubnodusn.dgse.tgWeetsSto(u)t)h)(;)));

問題点 : Leaflet-IIIFは画像のアスペクト比を保持するため、マップ境界と実際の画像境界は異なります。

失敗した試み3: アスペクト比を考慮した境界計算

cci}}oofnness(ltticcsmoeimannmagss{apettgAAessavAppceseetrpccutettai調調clct=>Hael=mmiPaagaipphdmWAtdaisigdp=netegWhcmita=d/)ptW(hm{imada/ptpHhHieemi/iaggghihetmtH;aeg-iegAahsctpt;eucatl;Height)/2;

問題点 : この方法でも近い位置には配置できますが、Leaflet-IIIFが内部で使用している縮小された画像サイズを考慮していないため、わずかにずれが生じます。

正解:Leaflet-IIIFの_fitBoundsメソッドを再現

Leaflet-IIIFのソースコードを分析

Leaflet-IIIFの_fitBoundsメソッドを見ると、以下のロジックで画像を配置していることがわかります:

_}fitBvvvvvvv_oaaaaaaaturrrrrrrhnid_ioisnbsstnfmweo.:hifau_itsg==nmfsieedauatS__spn=litt.cZ=zhh=fttoeiiiiho_ssLtoimt=...Bnsh__lo(;=i_mmau)staatn_.hppLd{t_i..nshisoog(im.ppBbsa_ttoo.giiiuu_emoonngSannddeigsssstze..(,IeSccsnsirrwti.zss,rtle..uiesppnean[ooe)lgiii);Ztnnn;ohittotTTm-ioo(aLL_1laatZtth-oLLionns_mgg.t((_h+LLmi..asoppp.foo.ofiigpsnnetetttit((So]0iin;,mzsae.ig(mme)aaS)xgi;NezaSeti.izxve,e.Zy0o))o,,m;iinniittiiaallZZoooomm));;

重要なポイント :

  1. _getInitialZoom()で最適なズームレベルを計算
  2. _imageSizes[initialZoom + offset]で、そのズームレベルでの縮小された画像サイズ を取得
  3. その縮小されたサイズを使ってpointToLatLng()で座標変換

正しい実装

f}unctcccccccrioooooooennnnnnntn1s2s3s4ss5ss6u.t.t.t.tt.tt.rinmLioinnssLaenfmooccemgaifarraaaaeftsgmmllfpTlieeXYeel.oeatSddeoLtli0==XYtpa-Z=z-ttIoe1ii==iLIoimmonImi=ggnnngFiXYoos(=firr.iLi//mmLcmiaiXYergiyfiiasXieLiif.,fraiilpL.yffiieoia_eLLmmtimyiraaaa-ngem.yyggItYra_eeeeIT).girrSSIo_em..iiFL{gSaxyzzaeig;;eettze..LIeSxynnsi;;gi.z(tleLiesii.an[iiplgiiioZtnffiohiLLnotaatm-iyy((aeesm1lrrcaZ..ap-oxyl.oegim==deiXti+,SfiLoszafceyfa(esl)ree).td;o]Yp;)t,ioinnsi.tmiaaxlNZaotoimv)e;Zoom;

完全な実装例

lla}deesottycnumict}}mairepifycn;fuatLn{ccccccm}ics}tc.acooooooa)ioe,coaytnnnnnnp;inthndeissssssIfsIT2sdrotttttt=cczILtIicccccf}a}0(oE;neroIaImoooooun)0elvmrmcpiLnsoFyaFennnnnnn;0reeiaeaaam.t:menossLsssco)r.nnnsnniame:rnuttettttccccrtccccccL}`;oetiipivngarLotaiooooeaoooooo.))rrLtfofatep:.0=t(iifioionnnnttnnnnnnr.;)ri(enesiS(Ca(mmlnfmnssssuisssssseaos)sssne'[RLt)aaeifattttrottttttccwffd<I{rttet=grm0S.iggttsginntoeiidsD(e{UAva,.to=ee-ieemnnssPtx[tbbalillTt:2'nr==mnipSin>WHIatSaooccmaayxooonogllorelanc'0ilPieIligrraaagrw,ptugrhCO(o$raanoe,]mea{diIZ=zemmllpeghLtnl:topmn{(=wwiI,pLgtgFoeTXYee..eyeode:laagi:'aaf=d{laehhoioddoit=,fms('ocp>n'Diieeytmi=L==XYpttRb#3ri)$d,Ottsc=,e==iate=twi=of,:t.{eMtar==fitii==ima,=gufybaxeC/fr.np.ciLiLmmosarhLn0':inrodeeivaiaiiiainggnnn.nghit.d0#nn+rnatstaiiniiiyfgXYoosfne]mls0f0doottcpesnivfiieL(rr.oota=a,0f.P.1reahom.tfaLffrai//mmcr..=gt'f5ob})n/(nsii(saLL.ymXYrEtseiL{,fpo;tmms[tni.yaa_egiisaapxTmn0udLaae0egmaeyyirXmm.crlyoag0pyonn.]mAanreem.,aaphgiwLgB'(.aiij;sngn.rra_ggiio(ethaeo,`vdffs[neox..giieemmi(t(.tTuaeeeo0oSt;y_emmWHaana;'sLonldssn].ea;gSagieggtn#pnLdu'tt(.brteigYdieeTnxlgase,_U)iovitze)tgSSooyi(t(}vr;tdioIeShhiiL,wtxLt<i3leycnnsi{;tzzah(,no/n_)m.esi.z;eeti='gpsic;ssI[tle..Ln',y(Ltto[ed0iesxynd)')xer)m0r]an[;;ge[);fo;p]v+;lgi(x1.+tna;iZtnL)]m,gccohi.;aw>te/otp=p,b<.[im-io>(obj0n(aiNytrs]fm1ln{ut>o.oaZtm+oni.p-o(bm'dj.osehR;;sgimcr)ioeia);gnti+l;h'Sfet)iLod).zafX;aeyf,d(esd)resT).tco;o]a(p;lmteaidpoY)n);s,.mianxiNtaitailvZeoZoomo)m;;

なぜこの方法が正しいのか

ズームレベルごとの画像サイズ

Leaflet-IIIFは、パフォーマンスのために画像を複数のズームレベルで管理します:

_]imagLLLLL4e.....3Sppppp8iooooo9ziiiii0ennnnnstttttx(((((=136143748338[236788,,,297,05136,500124823)))18,,,58)7,5)zzzzzoooooooooommmmm01238(maxNativeZoom)

initialZoomの役割

_getInitialZoom()は、マップのサイズに対して最適なズームレベルを計算します:

_}getIvvf}rnaaoeirrrttuitoii}raofvmfnllfaaZesrg(2oreeiir;oatiSmmemniaat:c==zggueeeerfttSSnu=hh=iiniizzic0ssteet...h..-i8__ixyo;iisonmm.faa_f(ggittsmeemooeaSSalltpiigee;SzzerrieeSaazssinne..zcc)lleeeees{nn[<<ggitt]mmhh;aapp--SSii11zz;ee-..ixyt)h>&i=&{s.0o;ptii-o-n)s.{maxNativeZoom;

このズームレベルで、画像がマップにぴったり収まるように配置されます。

offsetの意味

offsetは、_imageSizes配列のインデックスとズームレベルの対応を調整するためのものです:

constoffset=iiifLayer._imageSizes.length-1-iiifLayer.options.maxNativeZoom;

例えば、maxNativeZoom = 8_imageSizes.length = 9の場合、offset = 0となります。

デバッグのヒント

座標変換がうまくいかない場合、以下の情報をコンソールに出力して確認してください:

ccoonnssttploaitnLtng==L.mpaopi.nutn(pxr,ojye)c;t(point,3);3

0

まとめ

Leaflet-IIIFでIIIFアノテーションを正確に表示するには:

  1. Leaflet-IIIFの内部ロジックを理解する : _fitBoundsメソッドがどのように画像を配置しているか
  2. 縮小された画像サイズを使用する : 元画像サイズではなく、_imageSizes[initialZoom + offset]を使用
  3. 同じズームレベルで変換する : pointToLatLng(point, initialZoom)で変換

この方法により、ピクセル単位で正確にアノテーションを配置できます。

参考資料

プロジェクト情報

  • 使用ライブラリ:
    • Leaflet 1.9.4
    • Leaflet-IIIF 3.0.0
  • 作成日: 2025年10月19日