photo-sphere-viewer.js 223 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855
  1. /*!
  2. * Photo Sphere Viewer 3.3.3
  3. * Copyright (c) 2014-2015 Jérémy Heleine
  4. * Copyright (c) 2015-2018 Damien "Mistic" Sorel
  5. * Licensed under MIT (https://opensource.org/licenses/MIT)
  6. */
  7. (function(root, factory) {
  8. if (typeof define === 'function' && define.amd) {
  9. define(['three', 'd.js', 'uevent', 'dot/doT'], factory);
  10. } else if (typeof module === 'object' && module.exports) {
  11. module.exports = factory(require('three'), require('d.js'), require('uevent'), require('dot/doT'));
  12. } else {
  13. root.PhotoSphereViewer = factory(root.THREE, root.D, root.uEvent, root.doT);
  14. }
  15. }(this, function(THREE, D, uEvent, doT) {
  16. "use strict";
  17. /**
  18. * @typedef {Object} PhotoSphereViewer.Point
  19. * @summary Object defining a point
  20. * @property {int} x
  21. * @property {int} y
  22. */
  23. /**
  24. * @typedef {Object} PhotoSphereViewer.Size
  25. * @summary Object defining a size
  26. * @property {int} width
  27. * @property {int} height
  28. */
  29. /**
  30. * @typedef {Object} PhotoSphereViewer.CssSize
  31. * @summary Object defining a size in CSS (px, % or auto)
  32. * @property {string} [width]
  33. * @property {string} [height]
  34. */
  35. /**
  36. * @typedef {Object} PhotoSphereViewer.Position
  37. * @summary Object defining a spherical position
  38. * @property {float} longitude
  39. * @property {float} latitude
  40. */
  41. /**
  42. * @typedef {Object} PhotoSphereViewer.ExtendedPosition
  43. * @summary Object defining a spherical or texture position
  44. * @description A position that can be expressed either in spherical coordinates (radians or degrees) or in texture coordinates (pixels)
  45. * @property {float} longitude
  46. * @property {float} latitude
  47. * @property {int} x
  48. * @property {int} y
  49. */
  50. /**
  51. * @typedef {Object} PhotoSphereViewer.CacheItem
  52. * @summary An entry in the memory cache
  53. * @property {string} panorama
  54. * @property {THREE.Texture} image
  55. * @property {PhotoSphereViewer.PanoData} pano_data
  56. */
  57. /**
  58. * @typedef {Object} PhotoSphereViewer.PanoData
  59. * @summary Crop information of the panorama
  60. * @property {int} full_width
  61. * @property {int} full_height
  62. * @property {int} cropped_width
  63. * @property {int} cropped_height
  64. * @property {int} cropped_x
  65. * @property {int} cropped_y
  66. */
  67. /**
  68. * @typedef {Object} PhotoSphereViewer.ClickData
  69. * @summary Data of the `click` event
  70. * @property {int} client_x - position in the browser window
  71. * @property {int} client_y - position in the browser window
  72. * @property {int} viewer_x - position in the viewer
  73. * @property {int} viewer_y - position in the viewer
  74. * @property {float} longitude - position in spherical coordinates
  75. * @property {float} latitude - position in spherical coordinates
  76. * @property {int} texture_x - position on the texture
  77. * @property {int} texture_y - position on the texture
  78. * @property {PSVMarker} [marker] - clicked marker
  79. */
  80. /**
  81. * Viewer class
  82. * @param {Object} options - see {@link http://photo-sphere-viewer.js.org/#options}
  83. * @constructor
  84. * @fires PhotoSphereViewer.ready
  85. * @throws {PSVError} when the configuration is incorrect
  86. */
  87. function PhotoSphereViewer(options) {
  88. // return instance if called as a function
  89. if (!(this instanceof PhotoSphereViewer)) {
  90. return new PhotoSphereViewer(options);
  91. }
  92. // init global system variables
  93. if (!PhotoSphereViewer.SYSTEM.loaded) {
  94. PhotoSphereViewer._loadSystem();
  95. }
  96. /**
  97. * @summary Configuration object
  98. * @member {Object}
  99. * @readonly
  100. */
  101. this.config = PSVUtils.clone(PhotoSphereViewer.DEFAULTS);
  102. PSVUtils.deepmerge(this.config, options);
  103. // check container
  104. if (!options.container) {
  105. throw new PSVError('No value given for container.');
  106. }
  107. // must support canvas
  108. if (!PhotoSphereViewer.SYSTEM.isCanvasSupported) {
  109. throw new PSVError('Canvas is not supported.');
  110. }
  111. // additional scripts if webgl not supported/disabled
  112. if ((!PhotoSphereViewer.SYSTEM.isWebGLSupported || !this.config.webgl) && !PSVUtils.checkTHREE('CanvasRenderer', 'Projector')) {
  113. throw new PSVError('Missing Three.js components: CanvasRenderer, Projector. Get them from three.js-examples package.');
  114. }
  115. // longitude range must have two values
  116. if (this.config.longitude_range && this.config.longitude_range.length !== 2) {
  117. this.config.longitude_range = null;
  118. console.warn('PhotoSphereViewer: longitude_range must have exactly two elements.');
  119. }
  120. if (this.config.latitude_range) {
  121. // latitude range must have two values
  122. if (this.config.latitude_range.length !== 2) {
  123. this.config.latitude_range = null;
  124. console.warn('PhotoSphereViewer: latitude_range must have exactly two elements.');
  125. }
  126. // latitude range must be ordered
  127. else if (this.config.latitude_range[0] > this.config.latitude_range[1]) {
  128. this.config.latitude_range = [this.config.latitude_range[1], this.config.latitude_range[0]];
  129. console.warn('PhotoSphereViewer: latitude_range values must be ordered.');
  130. }
  131. }
  132. // migrate legacy tilt_up_max and tilt_down_max
  133. else if (this.config.tilt_up_max !== undefined || this.config.tilt_down_max !== undefined) {
  134. this.config.latitude_range = [
  135. this.config.tilt_down_max !== undefined ? this.config.tilt_down_max - Math.PI / 4 : -PSVUtils.HalfPI,
  136. this.config.tilt_up_max !== undefined ? this.config.tilt_up_max + Math.PI / 4 : PSVUtils.HalfPI
  137. ];
  138. console.warn('PhotoSphereViewer: tilt_up_max and tilt_down_max are deprecated, use latitude_range instead.');
  139. }
  140. // min_fov and max_fov must be ordered
  141. if (this.config.max_fov < this.config.min_fov) {
  142. var temp_fov = this.config.max_fov;
  143. this.config.max_fov = this.config.min_fov;
  144. this.config.min_fov = temp_fov;
  145. console.warn('PhotoSphereViewer: max_fov cannot be lower than min_fov.');
  146. }
  147. if (this.config.cache_texture && (!PSVUtils.isInteger(this.config.cache_texture) || this.config.cache_texture < 0)) {
  148. this.config.cache_texture = PhotoSphereViewer.DEFAULTS.cache_texture;
  149. console.warn('PhotoSphereViewer: invalid value for cache_texture');
  150. }
  151. if ('panorama_roll' in this.config) {
  152. this.config.sphere_correction.roll = this.config.panorama_roll;
  153. console.warn('PhotoSphereViewer: panorama_roll is deprecated, use sphere_correction.roll instead');
  154. }
  155. // min_fov/max_fov between 1 and 179
  156. this.config.min_fov = PSVUtils.bound(this.config.min_fov, 1, 179);
  157. this.config.max_fov = PSVUtils.bound(this.config.max_fov, 1, 179);
  158. // default default_fov is middle point between min_fov and max_fov
  159. if (this.config.default_fov === null) {
  160. this.config.default_fov = this.config.max_fov / 2 + this.config.min_fov / 2;
  161. }
  162. // default_fov between min_fov and max_fov
  163. else {
  164. this.config.default_fov = PSVUtils.bound(this.config.default_fov, this.config.min_fov, this.config.max_fov);
  165. }
  166. // parse default_long, is between 0 and 2*PI
  167. this.config.default_long = PSVUtils.parseAngle(this.config.default_long);
  168. // parse default_lat, is between -PI/2 and PI/2
  169. this.config.default_lat = PSVUtils.parseAngle(this.config.default_lat, true);
  170. // parse camera_correction, is between -PI/2 and PI/2
  171. this.config.sphere_correction.pan = PSVUtils.parseAngle(this.config.sphere_correction.pan, true);
  172. this.config.sphere_correction.tilt = PSVUtils.parseAngle(this.config.sphere_correction.tilt, true);
  173. this.config.sphere_correction.roll = PSVUtils.parseAngle(this.config.sphere_correction.roll, true);
  174. // default anim_lat is default_lat
  175. if (this.config.anim_lat === null) {
  176. this.config.anim_lat = this.config.default_lat;
  177. }
  178. // parse anim_lat, is between -PI/2 and PI/2
  179. else {
  180. this.config.anim_lat = PSVUtils.parseAngle(this.config.anim_lat, true);
  181. }
  182. // parse longitude_range, between 0 and 2*PI
  183. if (this.config.longitude_range) {
  184. this.config.longitude_range = this.config.longitude_range.map(function(angle) {
  185. return PSVUtils.parseAngle(angle);
  186. });
  187. }
  188. // parse latitude_range, between -PI/2 and PI/2
  189. if (this.config.latitude_range) {
  190. this.config.latitude_range = this.config.latitude_range.map(function(angle) {
  191. return PSVUtils.parseAngle(angle, true);
  192. });
  193. }
  194. // parse anim_speed
  195. this.config.anim_speed = PSVUtils.parseSpeed(this.config.anim_speed);
  196. // reactivate the navbar if the caption is provided
  197. if (this.config.caption && !this.config.navbar) {
  198. this.config.navbar = ['caption'];
  199. }
  200. // translate boolean fisheye to amount
  201. if (this.config.fisheye === true) {
  202. this.config.fisheye = 1;
  203. } else if (this.config.fisheye === false) {
  204. this.config.fisheye = 0;
  205. }
  206. /**
  207. * @summary Top most parent
  208. * @member {HTMLElement}
  209. * @readonly
  210. */
  211. this.parent = (typeof options.container === 'string') ? document.getElementById(options.container) : options.container;
  212. /**
  213. * @summary Main container
  214. * @member {HTMLElement}
  215. * @readonly
  216. */
  217. this.container = null;
  218. /**
  219. * @member {module:components.PSVLoader}
  220. * @readonly
  221. */
  222. this.loader = null;
  223. /**
  224. * @member {module:components.PSVNavBar}
  225. * @readonly
  226. */
  227. this.navbar = null;
  228. /**
  229. * @member {module:components.PSVHUD}
  230. * @readonly
  231. */
  232. this.hud = null;
  233. /**
  234. * @member {module:components.PSVPanel}
  235. * @readonly
  236. */
  237. this.panel = null;
  238. /**
  239. * @member {module:components.PSVTooltip}
  240. * @readonly
  241. */
  242. this.tooltip = null;
  243. /**
  244. * @member {HTMLElement}
  245. * @readonly
  246. * @private
  247. */
  248. this.canvas_container = null;
  249. /**
  250. * @member {THREE.WebGLRenderer | THREE.CanvasRenderer}
  251. * @readonly
  252. * @private
  253. */
  254. this.renderer = null;
  255. /**
  256. * @member {THREE.Scene}
  257. * @readonly
  258. * @private
  259. */
  260. this.scene = null;
  261. /**
  262. * @member {THREE.PerspectiveCamera}
  263. * @readonly
  264. * @private
  265. */
  266. this.camera = null;
  267. /**
  268. * @member {THREE.Mesh}
  269. * @readonly
  270. * @private
  271. */
  272. this.mesh = null;
  273. /**
  274. * @member {THREE.Raycaster}
  275. * @readonly
  276. * @private
  277. */
  278. this.raycaster = null;
  279. /**
  280. * @member {THREE.DeviceOrientationControls}
  281. * @readonly
  282. * @private
  283. */
  284. this.doControls = null;
  285. /**
  286. * @summary Internal properties
  287. * @member {Object}
  288. * @readonly
  289. * @property {boolean} isCubemap - if the panorama is a cubemap
  290. * @property {float} longitude - current longitude of the center
  291. * @property {float} longitude - current latitude of the center
  292. * @property {THREE.Vector3} direction - direction of the camera
  293. * @property {float} anim_speed - parsed animation speed (rad/sec)
  294. * @property {int} zoom_lvl - current zoom level
  295. * @property {float} vFov - vertical FOV
  296. * @property {float} hFov - horizontal FOV
  297. * @property {float} aspect - viewer aspect ratio
  298. * @property {float} move_speed - move speed (computed with pixel ratio and configuration move_speed)
  299. * @property {boolean} moving - is the user moving
  300. * @property {boolean} zooming - is the user zooming
  301. * @property {int} start_mouse_x - start x position of the click/touch
  302. * @property {int} start_mouse_y - start y position of the click/touch
  303. * @property {int} mouse_x - current x position of the cursor
  304. * @property {int} mouse_y - current y position of the cursor
  305. * @property {Array[]} mouse_history - list of latest positions of the cursor, [time, x, y]
  306. * @property {int} gyro_alpha_offset - current alpha offset for gyroscope controls
  307. * @property {int} pinch_dist - distance between fingers when zooming
  308. * @property orientation_reqid - animationRequest id of the device orientation
  309. * @property autorotate_reqid - animationRequest id of the automatic rotation
  310. * @property {Promise} animation_promise - promise of the current animation (either go to position or image transition)
  311. * @property {Promise} loading_promise - promise of the setPanorama method
  312. * @property start_timeout - timeout id of the automatic rotation delay
  313. * @property {PhotoSphereViewer.ClickData} dblclick_data - temporary storage of click data between two clicks
  314. * @property dblclick_timeout - timeout id for double click
  315. * @property {PhotoSphereViewer.CacheItem[]} cache - cached panoramas
  316. * @property {Size} size - size of the container
  317. * @property {PhotoSphereViewer.PanoData} pano_data - panorama metadata
  318. */
  319. this.prop = {
  320. isCubemap: undefined,
  321. longitude: 0,
  322. latitude: 0,
  323. direction: null,
  324. anim_speed: 0,
  325. zoom_lvl: 0,
  326. vFov: 0,
  327. hFov: 0,
  328. aspect: 0,
  329. move_speed: 0.1,
  330. moving: false,
  331. zooming: false,
  332. start_mouse_x: 0,
  333. start_mouse_y: 0,
  334. mouse_x: 0,
  335. mouse_y: 0,
  336. mouse_history: [],
  337. gyro_alpha_offset: 0,
  338. pinch_dist: 0,
  339. orientation_reqid: null,
  340. autorotate_reqid: null,
  341. animation_promise: null,
  342. loading_promise: null,
  343. start_timeout: null,
  344. dblclick_data: null,
  345. dblclick_timeout: null,
  346. cache: [],
  347. size: {
  348. width: 0,
  349. height: 0
  350. },
  351. pano_data: {
  352. full_width: 0,
  353. full_height: 0,
  354. cropped_width: 0,
  355. cropped_height: 0,
  356. cropped_x: 0,
  357. cropped_y: 0
  358. }
  359. };
  360. // init templates
  361. Object.keys(PhotoSphereViewer.TEMPLATES).forEach(function(tpl) {
  362. if (!this.config.templates[tpl]) {
  363. this.config.templates[tpl] = PhotoSphereViewer.TEMPLATES[tpl];
  364. }
  365. if (typeof this.config.templates[tpl] === 'string') {
  366. this.config.templates[tpl] = doT.template(this.config.templates[tpl]);
  367. }
  368. }, this);
  369. // init
  370. this.parent.photoSphereViewer = this;
  371. // create actual container
  372. this.container = document.createElement('div');
  373. this.container.classList.add('psv-container');
  374. this.parent.appendChild(this.container);
  375. // apply container size
  376. if (this.config.size !== null) {
  377. this._setViewerSize(this.config.size);
  378. }
  379. this._onResize();
  380. // apply default zoom level
  381. var tempZoom = Math.round((this.config.default_fov - this.config.min_fov) / (this.config.max_fov - this.config.min_fov) * 100);
  382. this.zoom(tempZoom - 2 * (tempZoom - 50), false);
  383. // actual move speed depends on pixel-ratio
  384. this.prop.move_speed = THREE.Math.degToRad(this.config.move_speed / PhotoSphereViewer.SYSTEM.pixelRatio);
  385. // set default position
  386. this.rotate({
  387. longitude: this.config.default_long,
  388. latitude: this.config.default_lat
  389. }, false);
  390. // load loader (!!)
  391. this.loader = new PSVLoader(this);
  392. this.loader.hide();
  393. // load navbar
  394. this.navbar = new PSVNavBar(this);
  395. this.navbar.hide();
  396. // load hud
  397. this.hud = new PSVHUD(this);
  398. this.hud.hide();
  399. // load side panel
  400. this.panel = new PSVPanel(this);
  401. // load hud tooltip
  402. this.tooltip = new PSVTooltip(this.hud);
  403. // attach event handlers
  404. this._bindEvents();
  405. // load panorama
  406. if (this.config.panorama) {
  407. this.load();
  408. }
  409. // enable GUI after first render
  410. this.once('render', function() {
  411. if (this.config.navbar) {
  412. this.container.classList.add('psv-container--has-navbar');
  413. this.navbar.show();
  414. }
  415. this.hud.show();
  416. if (this.config.markers) {
  417. this.config.markers.forEach(function(marker) {
  418. this.hud.addMarker(marker, false);
  419. }, this);
  420. this.hud.renderMarkers();
  421. }
  422. // Queue animation
  423. if (this.config.time_anim !== false) {
  424. this.prop.start_timeout = window.setTimeout(this.startAutorotate.bind(this), this.config.time_anim);
  425. }
  426. /**
  427. * @event ready
  428. * @memberof PhotoSphereViewer
  429. * @summary Triggered when the panorama image has been loaded and the viewer is ready to perform the first render
  430. */
  431. setTimeout(this.trigger.bind(this, 'ready'), 0);
  432. }.bind(this));
  433. }
  434. /**
  435. * @summary Triggers an event on the viewer
  436. * @function trigger
  437. * @memberof PhotoSphereViewer
  438. * @instance
  439. * @param {string} name
  440. * @param {...*} [arguments]
  441. * @returns {uEvent.Event}
  442. */
  443. /**
  444. * @summary Triggers an event on the viewer and returns the modified value
  445. * @function change
  446. * @memberof PhotoSphereViewer
  447. * @instance
  448. * @param {string} name
  449. * @param {*} value
  450. * @param {...*} [arguments]
  451. * @returns {*}
  452. */
  453. /**
  454. * @summary Attaches an event listener on the viewer
  455. * @function on
  456. * @memberof PhotoSphereViewer
  457. * @instance
  458. * @param {string|Object.<string, function>} name - event name or events map
  459. * @param {function} [callback]
  460. * @returns {PhotoSphereViewer}
  461. */
  462. /**
  463. * @summary Removes an event listener from the viewer
  464. * @function off
  465. * @memberof PhotoSphereViewer
  466. * @instance
  467. * @param {string|Object.<string, function>} name - event name or events map
  468. * @param {function} [callback]
  469. * @returns {PhotoSphereViewer}
  470. */
  471. /**
  472. * @summary Attaches an event listener called once on the viewer
  473. * @function once
  474. * @memberof PhotoSphereViewer
  475. * @instance
  476. * @param {string|Object.<string, function>} name - event name or events map
  477. * @param {function} [callback]
  478. * @returns {PhotoSphereViewer}
  479. */
  480. uEvent.mixin(PhotoSphereViewer);
  481. /**
  482. * @summary Loads the XMP data with AJAX
  483. * @param {string} panorama
  484. * @returns {Promise.<PhotoSphereViewer.PanoData>}
  485. * @throws {PSVError} when the image cannot be loaded
  486. * @private
  487. */
  488. PhotoSphereViewer.prototype._loadXMP = function(panorama) {
  489. if (!this.config.usexmpdata) {
  490. return D.resolved(null);
  491. }
  492. var defer = D();
  493. var xhr = new XMLHttpRequest();
  494. var progress = 0;
  495. xhr.onreadystatechange = function() {
  496. if (xhr.readyState === 4) {
  497. if (xhr.status === 200 || xhr.status === 201 || xhr.status === 202 || xhr.status === 0) {
  498. this.loader.setProgress(100);
  499. var binary = xhr.responseText;
  500. var a = binary.indexOf('<x:xmpmeta'),
  501. b = binary.indexOf('</x:xmpmeta>');
  502. var data = binary.substring(a, b);
  503. // No data retrieved
  504. if (a === -1 || b === -1 || data.indexOf('GPano:') === -1) {
  505. defer.resolve(null);
  506. } else {
  507. var pano_data = {
  508. full_width: parseInt(PSVUtils.getXMPValue(data, 'FullPanoWidthPixels')),
  509. full_height: parseInt(PSVUtils.getXMPValue(data, 'FullPanoHeightPixels')),
  510. cropped_width: parseInt(PSVUtils.getXMPValue(data, 'CroppedAreaImageWidthPixels')),
  511. cropped_height: parseInt(PSVUtils.getXMPValue(data, 'CroppedAreaImageHeightPixels')),
  512. cropped_x: parseInt(PSVUtils.getXMPValue(data, 'CroppedAreaLeftPixels')),
  513. cropped_y: parseInt(PSVUtils.getXMPValue(data, 'CroppedAreaTopPixels'))
  514. };
  515. if (!pano_data.full_width || !pano_data.full_height || !pano_data.cropped_width || !pano_data.cropped_height) {
  516. console.warn('PhotoSphereViewer: invalid XMP data');
  517. defer.resolve(null);
  518. } else {
  519. defer.resolve(pano_data);
  520. }
  521. }
  522. } else {
  523. this.container.textContent = 'Cannot load image';
  524. throw new PSVError('Cannot load image');
  525. }
  526. } else if (xhr.readyState === 3) {
  527. this.loader.setProgress(progress += 10);
  528. }
  529. }.bind(this);
  530. xhr.onprogress = function(e) {
  531. if (e.lengthComputable) {
  532. var new_progress = parseInt(e.loaded / e.total * 100);
  533. if (new_progress > progress) {
  534. progress = new_progress;
  535. this.loader.setProgress(progress);
  536. }
  537. }
  538. }.bind(this);
  539. xhr.onerror = function() {
  540. this.container.textContent = 'Cannot load image';
  541. throw new PSVError('Cannot load image');
  542. }.bind(this);
  543. xhr.open('GET', panorama, true);
  544. xhr.send(null);
  545. return defer.promise;
  546. };
  547. /**
  548. * @summary Loads the panorama texture(s)
  549. * @param {string|string[]} panorama
  550. * @returns {Promise.<THREE.Texture|THREE.Texture[]>}
  551. * @fires PhotoSphereViewer.panorama-load-progress
  552. * @throws {PSVError} when the image cannot be loaded
  553. * @private
  554. */
  555. PhotoSphereViewer.prototype._loadTexture = function(panorama) {
  556. var tempPanorama = [];
  557. if (Array.isArray(panorama)) {
  558. if (panorama.length !== 6) {
  559. throw new PSVError('Must provide exactly 6 image paths when using cubemap.');
  560. }
  561. // reorder images
  562. for (var i = 0; i < 6; i++) {
  563. tempPanorama[i] = panorama[PhotoSphereViewer.CUBE_MAP[i]];
  564. }
  565. panorama = tempPanorama;
  566. } else if (typeof panorama === 'object') {
  567. if (!PhotoSphereViewer.CUBE_HASHMAP.every(function(side) {
  568. return !!panorama[side];
  569. })) {
  570. throw new PSVError('Must provide exactly left, front, right, back, top, bottom when using cubemap.');
  571. }
  572. // transform into array
  573. PhotoSphereViewer.CUBE_HASHMAP.forEach(function(side, i) {
  574. tempPanorama[i] = panorama[side];
  575. });
  576. panorama = tempPanorama;
  577. }
  578. if (Array.isArray(panorama)) {
  579. if (this.prop.isCubemap === false) {
  580. throw new PSVError('The viewer was initialized with an equirectangular panorama, cannot switch to cubemap.');
  581. }
  582. if (this.config.fisheye) {
  583. console.warn('PhotoSphereViewer: fisheye effect with cubemap texture can generate distorsions.');
  584. }
  585. if (this.config.cache_texture === PhotoSphereViewer.DEFAULTS.cache_texture) {
  586. this.config.cache_texture *= 6;
  587. }
  588. this.prop.isCubemap = true;
  589. return this._loadCubemapTexture(panorama);
  590. } else {
  591. if (this.prop.isCubemap === true) {
  592. throw new PSVError('The viewer was initialized with an cubemap, cannot switch to equirectangular panorama.');
  593. }
  594. this.prop.isCubemap = false;
  595. return this._loadEquirectangularTexture(panorama);
  596. }
  597. };
  598. /**
  599. * @summary Loads the sphere texture
  600. * @param {string} panorama
  601. * @returns {Promise.<THREE.Texture>}
  602. * @fires PhotoSphereViewer.panorama-load-progress
  603. * @throws {PSVError} when the image cannot be loaded
  604. * @private
  605. */
  606. PhotoSphereViewer.prototype._loadEquirectangularTexture = function(panorama) {
  607. if (this.config.cache_texture) {
  608. var cache = this.getPanoramaCache(panorama);
  609. if (cache) {
  610. this.prop.pano_data = cache.pano_data;
  611. return D.resolved(cache.image);
  612. }
  613. }
  614. return this._loadXMP(panorama).then(function(pano_data) {
  615. var defer = D();
  616. var loader = new THREE.ImageLoader();
  617. var progress = pano_data ? 100 : 0;
  618. loader.setCrossOrigin('anonymous');
  619. var onload = function(img) {
  620. progress = 100;
  621. this.loader.setProgress(progress);
  622. /**
  623. * @event panorama-load-progress
  624. * @memberof PhotoSphereViewer
  625. * @summary Triggered while a panorama image is loading
  626. * @param {string} panorama
  627. * @param {int} progress
  628. */
  629. this.trigger('panorama-load-progress', panorama, progress);
  630. // Config XMP data
  631. if (!pano_data && this.config.pano_data) {
  632. pano_data = PSVUtils.clone(this.config.pano_data);
  633. }
  634. // Default XMP data
  635. if (!pano_data) {
  636. pano_data = {
  637. full_width: img.width,
  638. full_height: img.height,
  639. cropped_width: img.width,
  640. cropped_height: img.height,
  641. cropped_x: 0,
  642. cropped_y: 0
  643. };
  644. }
  645. this.prop.pano_data = pano_data;
  646. var texture;
  647. var ratio = Math.min(pano_data.full_width, PhotoSphereViewer.SYSTEM.maxTextureWidth) / pano_data.full_width;
  648. // resize image / fill cropped parts with black
  649. if (ratio !== 1 || pano_data.cropped_width !== pano_data.full_width || pano_data.cropped_height !== pano_data.full_height) {
  650. var resized_pano_data = PSVUtils.clone(pano_data);
  651. resized_pano_data.full_width *= ratio;
  652. resized_pano_data.full_height *= ratio;
  653. resized_pano_data.cropped_width *= ratio;
  654. resized_pano_data.cropped_height *= ratio;
  655. resized_pano_data.cropped_x *= ratio;
  656. resized_pano_data.cropped_y *= ratio;
  657. img.width = resized_pano_data.cropped_width;
  658. img.height = resized_pano_data.cropped_height;
  659. var buffer = document.createElement('canvas');
  660. buffer.width = resized_pano_data.full_width;
  661. buffer.height = resized_pano_data.full_height;
  662. var ctx = buffer.getContext('2d');
  663. ctx.drawImage(img, resized_pano_data.cropped_x, resized_pano_data.cropped_y, resized_pano_data.cropped_width, resized_pano_data.cropped_height);
  664. texture = new THREE.Texture(buffer);
  665. } else {
  666. texture = new THREE.Texture(img);
  667. }
  668. texture.needsUpdate = true;
  669. texture.minFilter = THREE.LinearFilter;
  670. texture.generateMipmaps = false;
  671. if (this.config.cache_texture) {
  672. this._putPanoramaCache({
  673. panorama: panorama,
  674. image: texture,
  675. pano_data: pano_data
  676. });
  677. }
  678. defer.resolve(texture);
  679. };
  680. var onprogress = function(e) {
  681. if (e.lengthComputable) {
  682. var new_progress = parseInt(e.loaded / e.total * 100);
  683. if (new_progress > progress) {
  684. progress = new_progress;
  685. this.loader.setProgress(progress);
  686. this.trigger('panorama-load-progress', panorama, progress);
  687. }
  688. }
  689. };
  690. var onerror = function(e) {
  691. this.container.textContent = 'Cannot load image';
  692. defer.reject(e);
  693. throw new PSVError('Cannot load image');
  694. };
  695. loader.load(panorama, onload.bind(this), onprogress.bind(this), onerror.bind(this));
  696. return defer.promise;
  697. }.bind(this));
  698. };
  699. /**
  700. * @summary Load the six textures of the cube
  701. * @param {string[]} panorama
  702. * @returns {Promise.<THREE.Texture[]>}
  703. * @fires PhotoSphereViewer.panorama-load-progress
  704. * @throws {PSVError} when the image cannot be loaded
  705. * @private
  706. */
  707. PhotoSphereViewer.prototype._loadCubemapTexture = function(panorama) {
  708. var defer = D();
  709. var loader = new THREE.ImageLoader();
  710. var progress = [0, 0, 0, 0, 0, 0];
  711. var loaded = [];
  712. var done = 0;
  713. loader.setCrossOrigin('anonymous');
  714. var onend = function() {
  715. loaded.forEach(function(img) {
  716. img.needsUpdate = true;
  717. img.minFilter = THREE.LinearFilter;
  718. img.generateMipmaps = false;
  719. });
  720. defer.resolve(loaded);
  721. };
  722. var onload = function(i, img) {
  723. done++;
  724. progress[i] = 100;
  725. this.loader.setProgress(PSVUtils.sum(progress) / 6);
  726. this.trigger('panorama-load-progress', panorama[i], progress[i]);
  727. var ratio = Math.min(img.width, PhotoSphereViewer.SYSTEM.maxTextureWidth / 2) / img.width;
  728. // resize image
  729. if (ratio !== 1) {
  730. var buffer = document.createElement('canvas');
  731. buffer.width = img.width * ratio;
  732. buffer.height = img.height * ratio;
  733. var ctx = buffer.getContext('2d');
  734. ctx.drawImage(img, 0, 0, buffer.width, buffer.height);
  735. loaded[i] = new THREE.Texture(buffer);
  736. } else {
  737. loaded[i] = new THREE.Texture(img);
  738. }
  739. if (this.config.cache_texture) {
  740. this._putPanoramaCache({
  741. panorama: panorama[i],
  742. image: loaded[i]
  743. });
  744. }
  745. if (done === 6) {
  746. onend();
  747. }
  748. };
  749. var onprogress = function(i, e) {
  750. if (e.lengthComputable) {
  751. var new_progress = parseInt(e.loaded / e.total * 100);
  752. if (new_progress > progress[i]) {
  753. progress[i] = new_progress;
  754. this.loader.setProgress(PSVUtils.sum(progress) / 6);
  755. this.trigger('panorama-load-progress', panorama[i], progress[i]);
  756. }
  757. }
  758. };
  759. var onerror = function(i, e) {
  760. this.container.textContent = 'Cannot load image';
  761. defer.reject(e);
  762. throw new PSVError('Cannot load image ' + i);
  763. };
  764. for (var i = 0; i < 6; i++) {
  765. if (this.config.cache_texture) {
  766. var cache = this.getPanoramaCache(panorama[i]);
  767. if (cache) {
  768. done++;
  769. progress[i] = 100;
  770. loaded[i] = cache.image;
  771. continue;
  772. }
  773. }
  774. loader.load(panorama[i], onload.bind(this, i), onprogress.bind(this, i), onerror.bind(this, i));
  775. }
  776. if (done === 6) {
  777. defer.resolve(loaded);
  778. }
  779. return defer.promise;
  780. };
  781. /**
  782. * @summary Applies the texture to the scene, creates the scene if needed
  783. * @param {THREE.Texture|THREE.Texture[]} texture
  784. * @fires PhotoSphereViewer.panorama-loaded
  785. * @private
  786. */
  787. PhotoSphereViewer.prototype._setTexture = function(texture) {
  788. if (!this.scene) {
  789. this._createScene();
  790. }
  791. if (this.prop.isCubemap) {
  792. for (var i = 0; i < 6; i++) {
  793. if (this.mesh.material[i].map) {
  794. this.mesh.material[i].map.dispose();
  795. }
  796. this.mesh.material[i].map = texture[i];
  797. }
  798. } else {
  799. if (this.mesh.material.map) {
  800. this.mesh.material.map.dispose();
  801. }
  802. this.mesh.material.map = texture;
  803. }
  804. /**
  805. * @event panorama-loaded
  806. * @memberof PhotoSphereViewer
  807. * @summary Triggered when a panorama image has been loaded
  808. */
  809. this.trigger('panorama-loaded');
  810. this.render();
  811. };
  812. /**
  813. * @summary Creates the 3D scene and GUI components
  814. * @private
  815. */
  816. PhotoSphereViewer.prototype._createScene = function() {
  817. this.raycaster = new THREE.Raycaster();
  818. this.renderer = PhotoSphereViewer.SYSTEM.isWebGLSupported && this.config.webgl ? new THREE.WebGLRenderer() : new THREE.CanvasRenderer();
  819. this.renderer.setSize(this.prop.size.width, this.prop.size.height);
  820. this.renderer.setPixelRatio(PhotoSphereViewer.SYSTEM.pixelRatio);
  821. var cameraDistance = PhotoSphereViewer.SPHERE_RADIUS;
  822. if (this.prop.isCubemap) {
  823. cameraDistance *= Math.sqrt(3);
  824. }
  825. if (this.config.fisheye) {
  826. cameraDistance += PhotoSphereViewer.SPHERE_RADIUS;
  827. }
  828. this.camera = new THREE.PerspectiveCamera(this.config.default_fov, this.prop.size.width / this.prop.size.height, 1, cameraDistance);
  829. this.camera.position.set(0, 0, 0);
  830. if (this.config.gyroscope && PSVUtils.checkTHREE('DeviceOrientationControls')) {
  831. this.doControls = new THREE.DeviceOrientationControls(this.camera);
  832. }
  833. this.scene = new THREE.Scene();
  834. this.scene.add(this.camera);
  835. if (this.prop.isCubemap) {
  836. this._createCubemap();
  837. } else {
  838. this._createSphere();
  839. }
  840. // create canvas container
  841. this.canvas_container = document.createElement('div');
  842. this.canvas_container.className = 'psv-canvas-container';
  843. this.renderer.domElement.className = 'psv-canvas';
  844. this.container.appendChild(this.canvas_container);
  845. this.canvas_container.appendChild(this.renderer.domElement);
  846. };
  847. /**
  848. * @summary Creates the sphere mesh
  849. * @private
  850. */
  851. PhotoSphereViewer.prototype._createSphere = function() {
  852. // The middle of the panorama is placed at longitude=0
  853. var geometry = new THREE.SphereGeometry(
  854. PhotoSphereViewer.SPHERE_RADIUS,
  855. PhotoSphereViewer.SPHERE_VERTICES,
  856. PhotoSphereViewer.SPHERE_VERTICES, -PSVUtils.HalfPI
  857. );
  858. var material = new THREE.MeshBasicMaterial({
  859. side: THREE.DoubleSide, // needs to be DoubleSide for CanvasRenderer
  860. overdraw: PhotoSphereViewer.SYSTEM.isWebGLSupported && this.config.webgl ? 0 : 1
  861. });
  862. this.mesh = new THREE.Mesh(geometry, material);
  863. this.mesh.scale.x = -1;
  864. this.mesh.rotation.x = this.config.sphere_correction.tilt;
  865. this.mesh.rotation.y = this.config.sphere_correction.pan;
  866. this.mesh.rotation.z = this.config.sphere_correction.roll;
  867. this.scene.add(this.mesh);
  868. };
  869. /**
  870. * @summary Creates the cube mesh
  871. * @private
  872. */
  873. PhotoSphereViewer.prototype._createCubemap = function() {
  874. var geometry = new THREE.BoxGeometry(
  875. PhotoSphereViewer.SPHERE_RADIUS * 2, PhotoSphereViewer.SPHERE_RADIUS * 2, PhotoSphereViewer.SPHERE_RADIUS * 2,
  876. PhotoSphereViewer.CUBE_VERTICES, PhotoSphereViewer.CUBE_VERTICES, PhotoSphereViewer.CUBE_VERTICES
  877. );
  878. var materials = [];
  879. for (var i = 0; i < 6; i++) {
  880. materials.push(new THREE.MeshBasicMaterial({
  881. side: THREE.BackSide,
  882. overdraw: PhotoSphereViewer.SYSTEM.isWebGLSupported && this.config.webgl ? 0 : 1
  883. }));
  884. }
  885. this.mesh = new THREE.Mesh(geometry, materials);
  886. this.mesh.position.x -= PhotoSphereViewer.SPHERE_RADIUS;
  887. this.mesh.position.y -= PhotoSphereViewer.SPHERE_RADIUS;
  888. this.mesh.position.z -= PhotoSphereViewer.SPHERE_RADIUS;
  889. this.mesh.applyMatrix(new THREE.Matrix4().makeScale(1, 1, -1));
  890. this.scene.add(this.mesh);
  891. };
  892. /**
  893. * @summary Performs transition between the current and a new texture
  894. * @param {THREE.Texture} texture
  895. * @param {PhotoSphereViewer.Position} [position]
  896. * @returns {Promise}
  897. * @private
  898. * @throws {PSVError} if the panorama is a cubemap
  899. */
  900. PhotoSphereViewer.prototype._transition = function(texture, position) {
  901. if (this.prop.isCubemap) {
  902. throw new PSVError('Transition is not available with cubemap.');
  903. }
  904. // create a new sphere with the new texture
  905. var geometry = new THREE.SphereGeometry(
  906. PhotoSphereViewer.SPHERE_RADIUS * 0.9,
  907. PhotoSphereViewer.SPHERE_VERTICES,
  908. PhotoSphereViewer.SPHERE_VERTICES, -PSVUtils.HalfPI
  909. );
  910. var material = new THREE.MeshBasicMaterial({
  911. side: THREE.DoubleSide,
  912. overdraw: PhotoSphereViewer.SYSTEM.isWebGLSupported && this.config.webgl ? 0 : 1,
  913. map: texture,
  914. transparent: true,
  915. opacity: 0
  916. });
  917. var mesh = new THREE.Mesh(geometry, material);
  918. mesh.scale.x = -1;
  919. // rotate the new sphere to make the target position face the camera
  920. if (position) {
  921. // Longitude rotation along the vertical axis
  922. mesh.rotateY(position.longitude - this.prop.longitude);
  923. // Latitude rotation along the camera horizontal axis
  924. var axis = new THREE.Vector3(0, 1, 0).cross(this.camera.getWorldDirection()).normalize();
  925. var q = new THREE.Quaternion().setFromAxisAngle(axis, position.latitude - this.prop.latitude);
  926. mesh.quaternion.multiplyQuaternions(q, mesh.quaternion);
  927. }
  928. this.scene.add(mesh);
  929. this.render();
  930. return PSVUtils.animation({
  931. properties: {
  932. opacity: { start: 0.0, end: 1.0 }
  933. },
  934. duration: this.config.transition.duration,
  935. easing: 'outCubic',
  936. onTick: function(properties) {
  937. material.opacity = properties.opacity;
  938. this.render();
  939. }.bind(this)
  940. })
  941. .then(function() {
  942. // remove temp sphere and transfer the texture to the main sphere
  943. this.mesh.material.map.dispose();
  944. this.mesh.material.map = texture;
  945. this.scene.remove(mesh);
  946. mesh.geometry.dispose();
  947. mesh.geometry = null;
  948. mesh.material.dispose();
  949. mesh.material = null;
  950. // actually rotate the camera
  951. if (position) {
  952. // FIXME: find a better way to handle ranges
  953. if (this.config.latitude_range || this.config.longitude_range) {
  954. this.config.longitude_range = this.config.latitude_range = null;
  955. console.warn('PhotoSphereViewer: trying to perform transition with longitude_range and/or latitude_range, ranges cleared.');
  956. }
  957. this.rotate(position);
  958. } else {
  959. this.render();
  960. }
  961. }.bind(this));
  962. };
  963. /**
  964. * @summary Reverses autorotate direction with smooth transition
  965. * @private
  966. */
  967. PhotoSphereViewer.prototype._reverseAutorotate = function() {
  968. var self = this;
  969. var newSpeed = -this.config.anim_speed;
  970. var range = this.config.longitude_range;
  971. this.config.longitude_range = null;
  972. PSVUtils.animation({
  973. properties: {
  974. speed: { start: this.config.anim_speed, end: 0 }
  975. },
  976. duration: 300,
  977. easing: 'inSine',
  978. onTick: function(properties) {
  979. self.config.anim_speed = properties.speed;
  980. }
  981. })
  982. .then(function() {
  983. return PSVUtils.animation({
  984. properties: {
  985. speed: { start: 0, end: newSpeed }
  986. },
  987. duration: 300,
  988. easing: 'outSine',
  989. onTick: function(properties) {
  990. self.config.anim_speed = properties.speed;
  991. }
  992. });
  993. })
  994. .then(function() {
  995. self.config.longitude_range = range;
  996. self.config.anim_speed = newSpeed;
  997. });
  998. };
  999. /**
  1000. * @summary Adds a panorama to the cache
  1001. * @param {PhotoSphereViewer.CacheItem} cache
  1002. * @fires PhotoSphereViewer.panorama-cached
  1003. * @throws {PSVError} when the cache is disabled
  1004. * @private
  1005. */
  1006. PhotoSphereViewer.prototype._putPanoramaCache = function(cache) {
  1007. if (!this.config.cache_texture) {
  1008. throw new PSVError('Cannot add panorama to cache, cache_texture is disabled');
  1009. }
  1010. var existingCache = this.getPanoramaCache(cache.panorama);
  1011. if (existingCache) {
  1012. existingCache.image = cache.image;
  1013. existingCache.pano_data = cache.pano_data;
  1014. } else {
  1015. this.prop.cache = this.prop.cache.slice(0, this.config.cache_texture - 1); // remove most ancient elements
  1016. this.prop.cache.unshift(cache);
  1017. }
  1018. /**
  1019. * @event panorama-cached
  1020. * @memberof PhotoSphereViewer
  1021. * @summary Triggered when a panorama is stored in the cache
  1022. * @param {string} panorama
  1023. */
  1024. this.trigger('panorama-cached', cache.panorama);
  1025. };
  1026. /**
  1027. * @summary Stops all current animations
  1028. * @private
  1029. */
  1030. PhotoSphereViewer.prototype._stopAll = function() {
  1031. this.stopAutorotate();
  1032. this.stopAnimation();
  1033. this.stopGyroscopeControl();
  1034. };
  1035. /**
  1036. * @summary Number of pixels bellow which a mouse move will be considered as a click
  1037. * @type {int}
  1038. * @readonly
  1039. * @private
  1040. */
  1041. PhotoSphereViewer.MOVE_THRESHOLD = 4;
  1042. /**
  1043. * @summary Angle in radians bellow which two angles are considered identical
  1044. * @type {float}
  1045. * @readonly
  1046. * @private
  1047. */
  1048. PhotoSphereViewer.ANGLE_THRESHOLD = 0.003;
  1049. /**
  1050. * @summary Delay in milliseconds between two clicks to consider a double click
  1051. * @type {int}
  1052. * @readonly
  1053. * @private
  1054. */
  1055. PhotoSphereViewer.DBLCLICK_DELAY = 300;
  1056. /**
  1057. * @summary Time size of the mouse position history used to compute inertia
  1058. * @type {int}
  1059. * @readonly
  1060. * @private
  1061. */
  1062. PhotoSphereViewer.INERTIA_WINDOW = 300;
  1063. /**
  1064. * @summary Radius of the THREE.SphereGeometry
  1065. * Half-length of the THREE.BoxGeometry
  1066. * @type {int}
  1067. * @readonly
  1068. * @private
  1069. */
  1070. PhotoSphereViewer.SPHERE_RADIUS = 100;
  1071. /**
  1072. * @summary Number of vertice of the THREE.SphereGeometry
  1073. * @type {int}
  1074. * @readonly
  1075. * @private
  1076. */
  1077. PhotoSphereViewer.SPHERE_VERTICES = 64;
  1078. /**
  1079. * @summary Number of vertices of each side of the THREE.BoxGeometry
  1080. * @type {int}
  1081. * @readonly
  1082. * @private
  1083. */
  1084. PhotoSphereViewer.CUBE_VERTICES = 8;
  1085. /**
  1086. * @summary Order of cube textures for arrays
  1087. * @type {int[]}
  1088. * @readonly
  1089. * @private
  1090. */
  1091. PhotoSphereViewer.CUBE_MAP = [0, 2, 4, 5, 3, 1];
  1092. /**
  1093. * @summary Order of cube textures for maps
  1094. * @type {string[]}
  1095. * @readonly
  1096. * @private
  1097. */
  1098. PhotoSphereViewer.CUBE_HASHMAP = ['left', 'right', 'top', 'bottom', 'back', 'front'];
  1099. /**
  1100. * @summary Map between keyboard events `keyCode|which` and `key`
  1101. * @type {Object.<int, string>}
  1102. * @readonly
  1103. * @private
  1104. */
  1105. PhotoSphereViewer.KEYMAP = {
  1106. 33: 'PageUp',
  1107. 34: 'PageDown',
  1108. 37: 'ArrowLeft',
  1109. 38: 'ArrowUp',
  1110. 39: 'ArrowRight',
  1111. 40: 'ArrowDown',
  1112. 107: '+',
  1113. 109: '-'
  1114. };
  1115. /**
  1116. * @summary System properties
  1117. * @type {Object}
  1118. * @readonly
  1119. * @private
  1120. */
  1121. PhotoSphereViewer.SYSTEM = {
  1122. loaded: false,
  1123. pixelRatio: 1,
  1124. isWebGLSupported: false,
  1125. isCanvasSupported: false,
  1126. deviceOrientationSupported: null,
  1127. maxTextureWidth: 0,
  1128. mouseWheelEvent: null,
  1129. fullscreenEvent: null
  1130. };
  1131. /**
  1132. * @summary SVG icons sources
  1133. * @type {Object.<string, string>}
  1134. * @readonly
  1135. */
  1136. PhotoSphereViewer.ICONS = {};
  1137. /**
  1138. * @summary Default options, see {@link http://photo-sphere-viewer.js.org/#options}
  1139. * @type {Object}
  1140. * @readonly
  1141. */
  1142. PhotoSphereViewer.DEFAULTS = {
  1143. panorama: null,
  1144. container: null,
  1145. caption: null,
  1146. usexmpdata: true,
  1147. pano_data: null,
  1148. webgl: true,
  1149. min_fov: 30,
  1150. max_fov: 90,
  1151. default_fov: null,
  1152. default_long: 0,
  1153. default_lat: 0,
  1154. sphere_correction: {
  1155. pan: 0,
  1156. tilt: 0,
  1157. roll: 0
  1158. },
  1159. longitude_range: null,
  1160. latitude_range: null,
  1161. move_speed: 1,
  1162. time_anim: 2000,
  1163. anim_speed: '2rpm',
  1164. anim_lat: null,
  1165. fisheye: false,
  1166. navbar: [
  1167. 'autorotate',
  1168. 'zoom',
  1169. 'download',
  1170. 'markers',
  1171. 'caption',
  1172. 'gyroscope',
  1173. 'fullscreen'
  1174. ],
  1175. tooltip: {
  1176. offset: 5,
  1177. arrow_size: 7,
  1178. delay: 100
  1179. },
  1180. lang: {
  1181. autorotate: 'Automatic rotation',
  1182. zoom: 'Zoom',
  1183. zoomOut: 'Zoom out',
  1184. zoomIn: 'Zoom in',
  1185. download: 'Download',
  1186. fullscreen: 'Fullscreen',
  1187. markers: 'Markers',
  1188. gyroscope: 'Gyroscope'
  1189. },
  1190. mousewheel: true,
  1191. mousewheel_factor: 1,
  1192. mousemove: true,
  1193. mousemove_hover: false,
  1194. keyboard: true,
  1195. gyroscope: false,
  1196. move_inertia: true,
  1197. click_event_on_marker: false,
  1198. transition: {
  1199. duration: 1500,
  1200. loader: true
  1201. },
  1202. loading_img: null,
  1203. loading_txt: 'Loading...',
  1204. size: null,
  1205. cache_texture: 0,
  1206. templates: {},
  1207. markers: []
  1208. };
  1209. /**
  1210. * @summary doT.js templates
  1211. * @type {Object.<string, string>}
  1212. * @readonly
  1213. */
  1214. PhotoSphereViewer.TEMPLATES = {
  1215. markersList: '\
  1216. <div class="psv-markers-list-container"> \
  1217. <h1 class="psv-markers-list-title">{{= it.config.lang.markers }}</h1> \
  1218. <ul class="psv-markers-list"> \
  1219. {{~ it.markers: marker }} \
  1220. <li data-psv-marker="{{= marker.id }}" class="psv-markers-list-item {{? marker.className }}{{= marker.className }}{{?}}"> \
  1221. {{? marker.image }}<img class="psv-markers-list-image" src="{{= marker.image }}"/>{{?}} \
  1222. <p class="psv-markers-list-name">{{? marker.tooltip }}{{= marker.tooltip.content }}{{?? marker.html }}{{= marker.html }}{{??}}{{= marker.id }}{{?}}</p> \
  1223. </li> \
  1224. {{~}} \
  1225. </ul> \
  1226. </div>'
  1227. };
  1228. /**
  1229. * @summary Adds all needed event listeners
  1230. * @private
  1231. */
  1232. PhotoSphereViewer.prototype._bindEvents = function() {
  1233. window.addEventListener('resize', this);
  1234. // all interation events are binded to the HUD only
  1235. if (this.config.mousemove) {
  1236. this.hud.container.style.cursor = 'move';
  1237. if (this.config.mousemove_hover) {
  1238. this.hud.container.addEventListener('mouseenter', this);
  1239. this.hud.container.addEventListener('mouseleave', this);
  1240. } else {
  1241. this.hud.container.addEventListener('mousedown', this);
  1242. window.addEventListener('mouseup', this);
  1243. }
  1244. this.hud.container.addEventListener('touchstart', this);
  1245. window.addEventListener('touchend', this);
  1246. this.hud.container.addEventListener('mousemove', this);
  1247. this.hud.container.addEventListener('touchmove', this);
  1248. }
  1249. if (PhotoSphereViewer.SYSTEM.fullscreenEvent) {
  1250. document.addEventListener(PhotoSphereViewer.SYSTEM.fullscreenEvent, this);
  1251. }
  1252. if (this.config.mousewheel) {
  1253. this.hud.container.addEventListener(PhotoSphereViewer.SYSTEM.mouseWheelEvent, this);
  1254. }
  1255. this.on('_side-reached', function(side) {
  1256. if (this.isAutorotateEnabled()) {
  1257. if (side === 'left' || side === 'right') {
  1258. this._reverseAutorotate();
  1259. }
  1260. }
  1261. });
  1262. };
  1263. /**
  1264. * @summary Removes all event listeners
  1265. * @private
  1266. */
  1267. PhotoSphereViewer.prototype._unbindEvents = function() {
  1268. window.removeEventListener('resize', this);
  1269. if (this.config.mousemove) {
  1270. this.hud.container.removeEventListener('mousedown', this);
  1271. this.hud.container.removeEventListener('mouseenter', this);
  1272. this.hud.container.removeEventListener('touchstart', this);
  1273. window.removeEventListener('mouseup', this);
  1274. window.removeEventListener('touchend', this);
  1275. this.hud.container.removeEventListener('mouseleave', this);
  1276. this.hud.container.removeEventListener('mousemove', this);
  1277. this.hud.container.removeEventListener('touchmove', this);
  1278. }
  1279. if (PhotoSphereViewer.SYSTEM.fullscreenEvent) {
  1280. document.removeEventListener(PhotoSphereViewer.SYSTEM.fullscreenEvent, this);
  1281. }
  1282. if (this.config.mousewheel) {
  1283. this.hud.container.removeEventListener(PhotoSphereViewer.SYSTEM.mouseWheelEvent, this);
  1284. }
  1285. this.off('_side-reached');
  1286. };
  1287. /**
  1288. * @summary Handles events
  1289. * @param {Event} evt
  1290. * @private
  1291. */
  1292. PhotoSphereViewer.prototype.handleEvent = function(evt) {
  1293. switch (evt.type) {
  1294. // @formatter:off
  1295. case 'resize':
  1296. PSVUtils.throttle(this._onResize(), 50);
  1297. break;
  1298. case 'keydown':
  1299. this._onKeyDown(evt);
  1300. break;
  1301. case 'mousedown':
  1302. this._onMouseDown(evt);
  1303. break;
  1304. case 'mouseenter':
  1305. this._onMouseDown(evt);
  1306. break;
  1307. case 'touchstart':
  1308. this._onTouchStart(evt);
  1309. break;
  1310. case 'mouseup':
  1311. this._onMouseUp(evt);
  1312. break;
  1313. case 'mouseleave':
  1314. this._onMouseUp(evt);
  1315. break;
  1316. case 'touchend':
  1317. this._onTouchEnd(evt);
  1318. break;
  1319. case 'mousemove':
  1320. this._onMouseMove(evt);
  1321. break;
  1322. case 'touchmove':
  1323. this._onTouchMove(evt);
  1324. break;
  1325. case PhotoSphereViewer.SYSTEM.fullscreenEvent:
  1326. this._fullscreenToggled();
  1327. break;
  1328. case PhotoSphereViewer.SYSTEM.mouseWheelEvent:
  1329. this._onMouseWheel(evt);
  1330. break;
  1331. // @formatter:on
  1332. }
  1333. };
  1334. /**
  1335. * @summary Resizes the canvas when the window is resized
  1336. * @fires PhotoSphereViewer.size-updated
  1337. * @private
  1338. */
  1339. PhotoSphereViewer.prototype._onResize = function() {
  1340. if (this.container.clientWidth !== this.prop.size.width || this.container.clientHeight !== this.prop.size.height) {
  1341. this.prop.size.width = parseInt(this.container.clientWidth);
  1342. this.prop.size.height = parseInt(this.container.clientHeight);
  1343. this.prop.aspect = this.prop.size.width / this.prop.size.height;
  1344. if (this.renderer) {
  1345. this.renderer.setSize(this.prop.size.width, this.prop.size.height);
  1346. this.render();
  1347. }
  1348. /**
  1349. * @event size-updated
  1350. * @memberof PhotoSphereViewer
  1351. * @summary Triggered when the viewer size changes
  1352. * @param {PhotoSphereViewer.Size} size
  1353. */
  1354. this.trigger('size-updated', this.getSize());
  1355. }
  1356. };
  1357. /**
  1358. * @summary Handles keyboard events
  1359. * @param {KeyboardEvent} evt
  1360. * @private
  1361. */
  1362. PhotoSphereViewer.prototype._onKeyDown = function(evt) {
  1363. var dLong = 0;
  1364. var dLat = 0;
  1365. var dZoom = 0;
  1366. var key = evt.key || PhotoSphereViewer.KEYMAP[evt.keyCode || evt.which];
  1367. switch (key) {
  1368. // @formatter:off
  1369. case 'ArrowUp':
  1370. dLat = 0.01;
  1371. break;
  1372. case 'ArrowDown':
  1373. dLat = -0.01;
  1374. break;
  1375. case 'ArrowRight':
  1376. dLong = 0.01;
  1377. break;
  1378. case 'ArrowLeft':
  1379. dLong = -0.01;
  1380. break;
  1381. case 'PageUp':
  1382. case '+':
  1383. dZoom = 1;
  1384. break;
  1385. case 'PageDown':
  1386. case '-':
  1387. dZoom = -1;
  1388. break;
  1389. // @formatter:on
  1390. }
  1391. if (dZoom !== 0) {
  1392. this.zoom(this.prop.zoom_lvl + dZoom);
  1393. } else if (dLat !== 0 || dLong !== 0) {
  1394. this.rotate({
  1395. longitude: this.prop.longitude + dLong * this.prop.move_speed * this.prop.hFov,
  1396. latitude: this.prop.latitude + dLat * this.prop.move_speed * this.prop.vFov
  1397. });
  1398. }
  1399. };
  1400. /**
  1401. * @summary Handles mouse button events
  1402. * @param {MouseEvent} evt
  1403. * @private
  1404. */
  1405. PhotoSphereViewer.prototype._onMouseDown = function(evt) {
  1406. this._startMove(evt);
  1407. };
  1408. /**
  1409. * @summary Handles mouse buttons events
  1410. * @param {MouseEvent} evt
  1411. * @private
  1412. */
  1413. PhotoSphereViewer.prototype._onMouseUp = function(evt) {
  1414. this._stopMove(evt);
  1415. };
  1416. /**
  1417. * @summary Handles mouse move events
  1418. * @param {MouseEvent} evt
  1419. * @private
  1420. */
  1421. PhotoSphereViewer.prototype._onMouseMove = function(evt) {
  1422. if (evt.buttons !== 0) {
  1423. evt.preventDefault();
  1424. this._move(evt);
  1425. } else if (this.config.mousemove_hover) {
  1426. this._moveAbsolute(evt);
  1427. }
  1428. };
  1429. /**
  1430. * @summary Handles touch events
  1431. * @param {TouchEvent} evt
  1432. * @private
  1433. */
  1434. PhotoSphereViewer.prototype._onTouchStart = function(evt) {
  1435. if (evt.touches.length === 1) {
  1436. this._startMove(evt.touches[0]);
  1437. } else if (evt.touches.length === 2) {
  1438. this._startZoom(evt);
  1439. }
  1440. };
  1441. /**
  1442. * @summary Handles touch events
  1443. * @param {TouchEvent} evt
  1444. * @private
  1445. */
  1446. PhotoSphereViewer.prototype._onTouchEnd = function(evt) {
  1447. this._stopMove(evt.changedTouches[0]);
  1448. };
  1449. /**
  1450. * @summary Handles touch move events
  1451. * @param {TouchEvent} evt
  1452. * @private
  1453. */
  1454. PhotoSphereViewer.prototype._onTouchMove = function(evt) {
  1455. if (evt.touches.length === 1) {
  1456. evt.preventDefault();
  1457. this._move(evt.touches[0]);
  1458. } else if (evt.touches.length === 2) {
  1459. evt.preventDefault();
  1460. this._zoom(evt);
  1461. }
  1462. };
  1463. /**
  1464. * @summary Initializes the movement
  1465. * @param {MouseEvent|Touch} evt
  1466. * @private
  1467. */
  1468. PhotoSphereViewer.prototype._startMove = function(evt) {
  1469. this.stopAutorotate();
  1470. this.stopAnimation();
  1471. this.prop.mouse_x = this.prop.start_mouse_x = parseInt(evt.clientX);
  1472. this.prop.mouse_y = this.prop.start_mouse_y = parseInt(evt.clientY);
  1473. this.prop.moving = true;
  1474. this.prop.zooming = false;
  1475. this.prop.mouse_history.length = 0;
  1476. this._logMouseMove(evt);
  1477. };
  1478. /**
  1479. * @summary Initializes the zoom
  1480. * @param {TouchEvent} evt
  1481. * @private
  1482. */
  1483. PhotoSphereViewer.prototype._startZoom = function(evt) {
  1484. var t = [
  1485. { x: parseInt(evt.touches[0].clientX), y: parseInt(evt.touches[0].clientY) },
  1486. { x: parseInt(evt.touches[1].clientX), y: parseInt(evt.touches[1].clientY) }
  1487. ];
  1488. this.prop.pinch_dist = Math.sqrt(Math.pow(t[0].x - t[1].x, 2) + Math.pow(t[0].y - t[1].y, 2));
  1489. this.prop.moving = false;
  1490. this.prop.zooming = true;
  1491. };
  1492. /**
  1493. * @summary Stops the movement
  1494. * @description If the move threshold was not reached a click event is triggered, otherwise an animation is launched to simulate inertia
  1495. * @param {MouseEvent|Touch} evt
  1496. * @private
  1497. */
  1498. PhotoSphereViewer.prototype._stopMove = function(evt) {
  1499. if (!PSVUtils.getClosest(evt.target, '.psv-hud')) {
  1500. return;
  1501. }
  1502. if (this.prop.moving) {
  1503. // move threshold to trigger a click
  1504. if (Math.abs(evt.clientX - this.prop.start_mouse_x) < PhotoSphereViewer.MOVE_THRESHOLD && Math.abs(evt.clientY - this.prop.start_mouse_y) < PhotoSphereViewer.MOVE_THRESHOLD) {
  1505. this._click(evt);
  1506. this.prop.moving = false;
  1507. }
  1508. // inertia animation
  1509. else if (this.config.move_inertia && !this.isGyroscopeEnabled()) {
  1510. this._logMouseMove(evt);
  1511. this._stopMoveInertia(evt);
  1512. } else {
  1513. this.prop.moving = false;
  1514. }
  1515. this.prop.mouse_history.length = 0;
  1516. }
  1517. this.prop.zooming = false;
  1518. };
  1519. /**
  1520. * @summary Performs an animation to simulate inertia when the movement stops
  1521. * @param {MouseEvent|Touch} evt
  1522. * @private
  1523. */
  1524. PhotoSphereViewer.prototype._stopMoveInertia = function(evt) {
  1525. var direction = {
  1526. x: evt.clientX - this.prop.mouse_history[0][1],
  1527. y: evt.clientY - this.prop.mouse_history[0][2]
  1528. };
  1529. var norm = Math.sqrt(direction.x * direction.x + direction.y * direction.y);
  1530. this.prop.animation_promise = PSVUtils.animation({
  1531. properties: {
  1532. clientX: { start: evt.clientX, end: evt.clientX + direction.x },
  1533. clientY: { start: evt.clientY, end: evt.clientY + direction.y }
  1534. },
  1535. duration: norm * PhotoSphereViewer.INERTIA_WINDOW / 100,
  1536. easing: 'outCirc',
  1537. onTick: function(properties) {
  1538. this._move(properties, false);
  1539. }.bind(this)
  1540. })
  1541. .ensure(function() {
  1542. this.prop.moving = false;
  1543. }.bind(this));
  1544. };
  1545. /**
  1546. * @summary Triggers an event with all coordinates when a simple click is performed
  1547. * @param {MouseEvent|Touch} evt
  1548. * @fires PhotoSphereViewer.click
  1549. * @fires PhotoSphereViewer.dblclick
  1550. * @private
  1551. */
  1552. PhotoSphereViewer.prototype._click = function(evt) {
  1553. var boundingRect = this.container.getBoundingClientRect();
  1554. var data = {
  1555. target: evt.target,
  1556. client_x: evt.clientX,
  1557. client_y: evt.clientY,
  1558. viewer_x: parseInt(evt.clientX - boundingRect.left),
  1559. viewer_y: parseInt(evt.clientY - boundingRect.top)
  1560. };
  1561. var intersect = this.viewerCoordsToVector3({ x: data.viewer_x, y: data.viewer_y });
  1562. if (intersect) {
  1563. var sphericalCoords = this.vector3ToSphericalCoords(intersect);
  1564. data.longitude = sphericalCoords.longitude;
  1565. data.latitude = sphericalCoords.latitude;
  1566. // TODO: for cubemap, computes texture's index and coordinates
  1567. if (!this.prop.isCubemap) {
  1568. var textureCoords = this.sphericalCoordsToTextureCoords({ longitude: data.longitude, latitude: data.latitude });
  1569. data.texture_x = textureCoords.x;
  1570. data.texture_y = textureCoords.y;
  1571. }
  1572. if (!this.prop.dblclick_timeout) {
  1573. /**
  1574. * @event click
  1575. * @memberof PhotoSphereViewer
  1576. * @summary Triggered when the user clicks on the viewer (everywhere excluding the navbar and the side panel)
  1577. * @param {PhotoSphereViewer.ClickData} data
  1578. */
  1579. this.trigger('click', data);
  1580. this.prop.dblclick_data = PSVUtils.clone(data);
  1581. this.prop.dblclick_timeout = setTimeout(function() {
  1582. this.prop.dblclick_timeout = null;
  1583. this.prop.dblclick_data = null;
  1584. }.bind(this), PhotoSphereViewer.DBLCLICK_DELAY);
  1585. } else {
  1586. if (Math.abs(this.prop.dblclick_data.client_x - data.client_x) < PhotoSphereViewer.MOVE_THRESHOLD &&
  1587. Math.abs(this.prop.dblclick_data.client_y - data.client_y) < PhotoSphereViewer.MOVE_THRESHOLD) {
  1588. /**
  1589. * @event dblclick
  1590. * @memberof PhotoSphereViewer
  1591. * @summary Triggered when the user double clicks on the viewer. The simple `click` event is always fired before `dblclick`
  1592. * @param {PhotoSphereViewer.ClickData} data
  1593. */
  1594. this.trigger('dblclick', this.prop.dblclick_data);
  1595. }
  1596. clearTimeout(this.prop.dblclick_timeout);
  1597. this.prop.dblclick_timeout = null;
  1598. this.prop.dblclick_data = null;
  1599. }
  1600. }
  1601. };
  1602. /**
  1603. * @summary Performs movement
  1604. * @param {MouseEvent|Touch} evt
  1605. * @param {boolean} [log=true]
  1606. * @private
  1607. */
  1608. PhotoSphereViewer.prototype._move = function(evt, log) {
  1609. if (this.prop.moving) {
  1610. var x = parseInt(evt.clientX);
  1611. var y = parseInt(evt.clientY);
  1612. var rotation = {
  1613. longitude: (x - this.prop.mouse_x) / this.prop.size.width * this.prop.move_speed * this.prop.hFov * PhotoSphereViewer.SYSTEM.pixelRatio,
  1614. latitude: (y - this.prop.mouse_y) / this.prop.size.height * this.prop.move_speed * this.prop.vFov * PhotoSphereViewer.SYSTEM.pixelRatio
  1615. };
  1616. if (this.isGyroscopeEnabled()) {
  1617. this.prop.gyro_alpha_offset += rotation.longitude;
  1618. } else {
  1619. this.rotate({
  1620. longitude: this.prop.longitude - rotation.longitude,
  1621. latitude: this.prop.latitude + rotation.latitude
  1622. });
  1623. }
  1624. this.prop.mouse_x = x;
  1625. this.prop.mouse_y = y;
  1626. if (log !== false) {
  1627. this._logMouseMove(evt);
  1628. }
  1629. }
  1630. };
  1631. /**
  1632. * @summary Performs movement absolute to cursor position in viewer
  1633. * @param {MouseEvent} evt
  1634. * @private
  1635. */
  1636. PhotoSphereViewer.prototype._moveAbsolute = function(evt) {
  1637. if (this.prop.moving) {
  1638. this.rotate({
  1639. longitude: ((evt.clientX - this.container.offsetLeft) / this.container.offsetWidth - 0.5) * PSVUtils.TwoPI,
  1640. latitude: -((evt.clientY - this.container.offsetTop) / this.container.offsetHeight - 0.5) * Math.PI
  1641. });
  1642. }
  1643. };
  1644. /**
  1645. * @summary Perfoms zoom
  1646. * @param {TouchEvent} evt
  1647. * @private
  1648. */
  1649. PhotoSphereViewer.prototype._zoom = function(evt) {
  1650. if (this.prop.zooming) {
  1651. var t = [
  1652. { x: parseInt(evt.touches[0].clientX), y: parseInt(evt.touches[0].clientY) },
  1653. { x: parseInt(evt.touches[1].clientX), y: parseInt(evt.touches[1].clientY) }
  1654. ];
  1655. var p = Math.sqrt(Math.pow(t[0].x - t[1].x, 2) + Math.pow(t[0].y - t[1].y, 2));
  1656. var delta = 80 * (p - this.prop.pinch_dist) / this.prop.size.width;
  1657. this.zoom(this.prop.zoom_lvl + delta);
  1658. this.prop.pinch_dist = p;
  1659. }
  1660. };
  1661. /**
  1662. * @summary Handles mouse wheel events
  1663. * @param {MouseWheelEvent} evt
  1664. * @private
  1665. */
  1666. PhotoSphereViewer.prototype._onMouseWheel = function(evt) {
  1667. evt.preventDefault();
  1668. evt.stopPropagation();
  1669. var delta = PSVUtils.normalizeWheel(evt).spinY * 5;
  1670. if (delta !== 0) {
  1671. this.zoom(this.prop.zoom_lvl - delta * this.config.mousewheel_factor);
  1672. }
  1673. };
  1674. /**
  1675. * @summary Handles fullscreen events
  1676. * @fires PhotoSphereViewer.fullscreen-updated
  1677. * @private
  1678. */
  1679. PhotoSphereViewer.prototype._fullscreenToggled = function() {
  1680. var enabled = this.isFullscreenEnabled();
  1681. if (this.config.keyboard) {
  1682. if (enabled) {
  1683. this.startKeyboardControl();
  1684. } else {
  1685. this.stopKeyboardControl();
  1686. }
  1687. }
  1688. /**
  1689. * @event fullscreen-updated
  1690. * @memberof PhotoSphereViewer
  1691. * @summary Triggered when the fullscreen mode is enabled/disabled
  1692. * @param {boolean} enabled
  1693. */
  1694. this.trigger('fullscreen-updated', enabled);
  1695. };
  1696. /**
  1697. * @summary Stores each mouse position during a mouse move
  1698. * @description Positions older than "INERTIA_WINDOW" are removed<br>
  1699. * Positions before a pause of "INERTIA_WINDOW" / 10 are removed
  1700. * @param {MouseEvent|Touch} evt
  1701. * @private
  1702. */
  1703. PhotoSphereViewer.prototype._logMouseMove = function(evt) {
  1704. var now = Date.now();
  1705. this.prop.mouse_history.push([now, evt.clientX, evt.clientY]);
  1706. var previous = null;
  1707. for (var i = 0; i < this.prop.mouse_history.length;) {
  1708. if (this.prop.mouse_history[0][i] < now - PhotoSphereViewer.INERTIA_WINDOW) {
  1709. this.prop.mouse_history.splice(i, 1);
  1710. } else if (previous && this.prop.mouse_history[0][i] - previous > PhotoSphereViewer.INERTIA_WINDOW / 10) {
  1711. this.prop.mouse_history.splice(0, i);
  1712. i = 0;
  1713. previous = this.prop.mouse_history[0][i];
  1714. } else {
  1715. i++;
  1716. previous = this.prop.mouse_history[0][i];
  1717. }
  1718. }
  1719. };
  1720. /**
  1721. * @summary Starts to load the panorama
  1722. * @returns {Promise}
  1723. * @throws {PSVError} when the panorama is not configured
  1724. */
  1725. PhotoSphereViewer.prototype.load = function() {
  1726. if (!this.config.panorama) {
  1727. throw new PSVError('No value given for panorama.');
  1728. }
  1729. return this.setPanorama(this.config.panorama, false);
  1730. };
  1731. /**
  1732. * @summary Returns the current position of the camera
  1733. * @returns {PhotoSphereViewer.Position}
  1734. */
  1735. PhotoSphereViewer.prototype.getPosition = function() {
  1736. return {
  1737. longitude: this.prop.longitude,
  1738. latitude: this.prop.latitude
  1739. };
  1740. };
  1741. /**
  1742. * @summary Returns the current zoom level
  1743. * @returns {int}
  1744. */
  1745. PhotoSphereViewer.prototype.getZoomLevel = function() {
  1746. return this.prop.zoom_lvl;
  1747. };
  1748. /**
  1749. * @summary Returns the current viewer size
  1750. * @returns {PhotoSphereViewer.Size}
  1751. */
  1752. PhotoSphereViewer.prototype.getSize = function() {
  1753. return {
  1754. width: this.prop.size.width,
  1755. height: this.prop.size.height
  1756. };
  1757. };
  1758. /**
  1759. * @summary Checks if the automatic rotation is enabled
  1760. * @returns {boolean}
  1761. */
  1762. PhotoSphereViewer.prototype.isAutorotateEnabled = function() {
  1763. return !!this.prop.autorotate_reqid;
  1764. };
  1765. /**
  1766. * @summary Checks if the gyroscope is enabled
  1767. * @returns {boolean}
  1768. */
  1769. PhotoSphereViewer.prototype.isGyroscopeEnabled = function() {
  1770. return !!this.prop.orientation_reqid;
  1771. };
  1772. /**
  1773. * @summary Checks if the viewer is in fullscreen
  1774. * @returns {boolean}
  1775. */
  1776. PhotoSphereViewer.prototype.isFullscreenEnabled = function() {
  1777. return PSVUtils.isFullscreenEnabled(this.container);
  1778. };
  1779. /**
  1780. * @summary Performs a render
  1781. * @param {boolean} [updateDirection=true] - should update camera direction
  1782. * @fires PhotoSphereViewer.render
  1783. */
  1784. PhotoSphereViewer.prototype.render = function(updateDirection) {
  1785. if (updateDirection !== false) {
  1786. this.prop.direction = this.sphericalCoordsToVector3(this.prop);
  1787. }
  1788. this.camera.position.set(0, 0, 0);
  1789. this.camera.lookAt(this.prop.direction);
  1790. if (this.config.fisheye) {
  1791. this.camera.position.copy(this.prop.direction).multiplyScalar(this.config.fisheye / 2).negate();
  1792. }
  1793. this.camera.aspect = this.prop.aspect;
  1794. this.camera.fov = this.prop.vFov;
  1795. this.camera.updateProjectionMatrix();
  1796. this.renderer.render(this.scene, this.camera);
  1797. /**
  1798. * @event render
  1799. * @memberof PhotoSphereViewer
  1800. * @summary Triggered on each viewer render, **this event is triggered very often**
  1801. */
  1802. this.trigger('render');
  1803. };
  1804. /**
  1805. * @summary Destroys the viewer
  1806. * @description The memory used by the ThreeJS context is not totally cleared. This will be fixed as soon as possible.
  1807. */
  1808. PhotoSphereViewer.prototype.destroy = function() {
  1809. this._stopAll();
  1810. this.stopKeyboardControl();
  1811. if (this.isFullscreenEnabled()) {
  1812. PSVUtils.exitFullscreen();
  1813. }
  1814. // remove listeners
  1815. this._unbindEvents();
  1816. // destroy components
  1817. if (this.tooltip) {
  1818. this.tooltip.destroy();
  1819. }
  1820. if (this.hud) {
  1821. this.hud.destroy();
  1822. }
  1823. if (this.loader) {
  1824. this.loader.destroy();
  1825. }
  1826. if (this.navbar) {
  1827. this.navbar.destroy();
  1828. }
  1829. if (this.panel) {
  1830. this.panel.destroy();
  1831. }
  1832. if (this.doControls) {
  1833. this.doControls.disconnect();
  1834. }
  1835. // destroy ThreeJS view
  1836. if (this.scene) {
  1837. PSVUtils.cleanTHREEScene(this.scene);
  1838. }
  1839. // remove container
  1840. if (this.canvas_container) {
  1841. this.container.removeChild(this.canvas_container);
  1842. }
  1843. this.parent.removeChild(this.container);
  1844. delete this.parent.photoSphereViewer;
  1845. // clean references
  1846. delete this.parent;
  1847. delete this.container;
  1848. delete this.loader;
  1849. delete this.navbar;
  1850. delete this.hud;
  1851. delete this.panel;
  1852. delete this.tooltip;
  1853. delete this.canvas_container;
  1854. delete this.renderer;
  1855. delete this.scene;
  1856. delete this.camera;
  1857. delete this.mesh;
  1858. delete this.doControls;
  1859. delete this.raycaster;
  1860. delete this.passes;
  1861. delete this.config;
  1862. this.prop.cache.length = 0;
  1863. };
  1864. /**
  1865. * @summary Loads a new panorama file
  1866. * @description Loads a new panorama file, optionally changing the camera position and activating the transition animation.<br>
  1867. * If the "position" is not defined, the camera will not move and the ongoing animation will continue<br>
  1868. * "config.transition" must be configured for "transition" to be taken in account
  1869. * @param {string|string[]} path - URL of the new panorama file
  1870. * @param {PhotoSphereViewer.ExtendedPosition} [position]
  1871. * @param {boolean} [transition=false]
  1872. * @returns {Promise}
  1873. * @throws {PSVError} when another panorama is already loading
  1874. */
  1875. PhotoSphereViewer.prototype.setPanorama = function(path, position, transition) {
  1876. if (this.prop.loading_promise !== null) {
  1877. throw new PSVError('Loading already in progress');
  1878. }
  1879. if (typeof position === 'boolean') {
  1880. transition = position;
  1881. position = undefined;
  1882. }
  1883. if (transition && this.prop.isCubemap) {
  1884. throw new PSVError('Transition is not available with cubemap.');
  1885. }
  1886. if (position) {
  1887. this.cleanPosition(position);
  1888. this._stopAll();
  1889. }
  1890. this.config.panorama = path;
  1891. if (!transition || !this.config.transition || !this.scene) {
  1892. this.loader.show();
  1893. if (this.canvas_container) {
  1894. this.canvas_container.style.opacity = 0;
  1895. }
  1896. this.prop.loading_promise = this._loadTexture(this.config.panorama)
  1897. .then(function(texture) {
  1898. this._setTexture(texture);
  1899. if (position) {
  1900. this.rotate(position);
  1901. }
  1902. }.bind(this))
  1903. .ensure(function() {
  1904. this.loader.hide();
  1905. this.canvas_container.style.opacity = 1;
  1906. this.prop.loading_promise = null;
  1907. }.bind(this))
  1908. .rethrow();
  1909. } else {
  1910. if (this.config.transition.loader) {
  1911. this.loader.show();
  1912. }
  1913. this.prop.loading_promise = this._loadTexture(this.config.panorama)
  1914. .then(function(texture) {
  1915. this.loader.hide();
  1916. return this._transition(texture, position);
  1917. }.bind(this))
  1918. .ensure(function() {
  1919. this.loader.hide();
  1920. this.prop.loading_promise = null;
  1921. }.bind(this))
  1922. .rethrow();
  1923. }
  1924. return this.prop.loading_promise;
  1925. };
  1926. /**
  1927. * @summary Starts the automatic rotation
  1928. * @fires PhotoSphereViewer.autorotate
  1929. */
  1930. PhotoSphereViewer.prototype.startAutorotate = function() {
  1931. this._stopAll();
  1932. var last;
  1933. var elapsed;
  1934. var run = function(timestamp) {
  1935. if (timestamp) {
  1936. elapsed = last === undefined ? 0 : timestamp - last;
  1937. last = timestamp;
  1938. this.rotate({
  1939. longitude: this.prop.longitude + this.config.anim_speed * elapsed / 1000,
  1940. latitude: this.prop.latitude - (this.prop.latitude - this.config.anim_lat) / 200
  1941. });
  1942. }
  1943. this.prop.autorotate_reqid = window.requestAnimationFrame(run);
  1944. }.bind(this);
  1945. run();
  1946. /**
  1947. * @event autorotate
  1948. * @memberof PhotoSphereViewer
  1949. * @summary Triggered when the automatic rotation is enabled/disabled
  1950. * @param {boolean} enabled
  1951. */
  1952. this.trigger('autorotate', true);
  1953. };
  1954. /**
  1955. * @summary Stops the automatic rotation
  1956. * @fires PhotoSphereViewer.autorotate
  1957. */
  1958. PhotoSphereViewer.prototype.stopAutorotate = function() {
  1959. if (this.prop.start_timeout) {
  1960. window.clearTimeout(this.prop.start_timeout);
  1961. this.prop.start_timeout = null;
  1962. }
  1963. if (this.prop.autorotate_reqid) {
  1964. window.cancelAnimationFrame(this.prop.autorotate_reqid);
  1965. this.prop.autorotate_reqid = null;
  1966. this.trigger('autorotate', false);
  1967. }
  1968. };
  1969. /**
  1970. * @summary Starts or stops the automatic rotation
  1971. */
  1972. PhotoSphereViewer.prototype.toggleAutorotate = function() {
  1973. if (this.isAutorotateEnabled()) {
  1974. this.stopAutorotate();
  1975. } else {
  1976. this.startAutorotate();
  1977. }
  1978. };
  1979. /**
  1980. * @summary Enables the gyroscope navigation if available
  1981. * @fires PhotoSphereViewer.gyroscope-updated
  1982. */
  1983. PhotoSphereViewer.prototype.startGyroscopeControl = function() {
  1984. if (!this.doControls || !this.doControls.enabled) {
  1985. console.warn('PhotoSphereViewer: gyroscope disabled');
  1986. return;
  1987. }
  1988. PhotoSphereViewer.SYSTEM.deviceOrientationSupported.then(
  1989. PhotoSphereViewer.prototype._startGyroscopeControl.bind(this),
  1990. function() {
  1991. console.warn('PhotoSphereViewer: gyroscope not available');
  1992. }
  1993. );
  1994. };
  1995. /**
  1996. * @summary Immediately enables the gyroscope navigation
  1997. * @description Do not call this method directly, call `startGyroscopeControl` instead
  1998. * @fires PhotoSphereViewer.gyroscope-updated
  1999. * @private
  2000. */
  2001. PhotoSphereViewer.prototype._startGyroscopeControl = function() {
  2002. this._stopAll();
  2003. // compute the alpha offset to keep the current orientation
  2004. this.doControls.alphaOffset = this.prop.longitude;
  2005. this.doControls.update();
  2006. var direction = this.camera.getWorldDirection(new THREE.Vector3());
  2007. var sphericalCoords = this.vector3ToSphericalCoords(direction);
  2008. this.prop.gyro_alpha_offset = sphericalCoords.longitude;
  2009. var run = function() {
  2010. this.doControls.alphaOffset = this.prop.gyro_alpha_offset;
  2011. this.doControls.update();
  2012. this.camera.getWorldDirection(this.prop.direction);
  2013. this.prop.direction.multiplyScalar(PhotoSphereViewer.SPHERE_RADIUS);
  2014. var sphericalCoords = this.vector3ToSphericalCoords(this.prop.direction);
  2015. this.prop.longitude = sphericalCoords.longitude;
  2016. this.prop.latitude = sphericalCoords.latitude;
  2017. this.render(false);
  2018. this.prop.orientation_reqid = window.requestAnimationFrame(run);
  2019. }.bind(this);
  2020. run();
  2021. /**
  2022. * @event gyroscope-updated
  2023. * @memberof PhotoSphereViewer
  2024. * @summary Triggered when the gyroscope mode is enabled/disabled
  2025. * @param {boolean} enabled
  2026. */
  2027. this.trigger('gyroscope-updated', true);
  2028. };
  2029. /**
  2030. * @summary Disables the gyroscope navigation
  2031. * @fires PhotoSphereViewer.gyroscope-updated
  2032. */
  2033. PhotoSphereViewer.prototype.stopGyroscopeControl = function() {
  2034. if (this.prop.orientation_reqid) {
  2035. window.cancelAnimationFrame(this.prop.orientation_reqid);
  2036. this.prop.orientation_reqid = null;
  2037. this.trigger('gyroscope-updated', false);
  2038. this.render();
  2039. }
  2040. };
  2041. /**
  2042. * @summary Enables or disables the gyroscope navigation
  2043. */
  2044. PhotoSphereViewer.prototype.toggleGyroscopeControl = function() {
  2045. if (this.isGyroscopeEnabled()) {
  2046. this.stopGyroscopeControl();
  2047. } else {
  2048. this.startGyroscopeControl();
  2049. }
  2050. };
  2051. /**
  2052. * @summary Rotates the view to specific longitude and latitude
  2053. * @param {PhotoSphereViewer.ExtendedPosition} position
  2054. * @param {boolean} [render=true]
  2055. * @fires PhotoSphereViewer._side-reached
  2056. * @fires PhotoSphereViewer.position-updated
  2057. */
  2058. PhotoSphereViewer.prototype.rotate = function(position, render) {
  2059. this.cleanPosition(position);
  2060. /**
  2061. * @event _side-reached
  2062. * @memberof PhotoSphereViewer
  2063. * @param {string} side
  2064. * @private
  2065. */
  2066. this.applyRanges(position).forEach(
  2067. this.trigger.bind(this, '_side-reached')
  2068. );
  2069. this.prop.longitude = position.longitude;
  2070. this.prop.latitude = position.latitude;
  2071. if (render !== false && this.renderer) {
  2072. this.render();
  2073. /**
  2074. * @event position-updated
  2075. * @memberof PhotoSphereViewer
  2076. * @summary Triggered when the view longitude and/or latitude changes
  2077. * @param {PhotoSphereViewer.Position} position
  2078. */
  2079. this.trigger('position-updated', this.getPosition());
  2080. }
  2081. };
  2082. /**
  2083. * @summary Rotates the view to specific longitude and latitude with a smooth animation
  2084. * @param {PhotoSphereViewer.ExtendedPosition} position
  2085. * @param {string|int} duration - animation speed or duration (in milliseconds)
  2086. * @returns {Promise}
  2087. */
  2088. PhotoSphereViewer.prototype.animate = function(position, duration) {
  2089. this._stopAll();
  2090. this.cleanPosition(position);
  2091. if (!duration || Math.abs(position.longitude - this.prop.longitude) < PhotoSphereViewer.ANGLE_THRESHOLD && Math.abs(position.latitude - this.prop.latitude) < PhotoSphereViewer.ANGLE_THRESHOLD) {
  2092. this.rotate(position);
  2093. return D.resolved();
  2094. }
  2095. this.applyRanges(position).forEach(
  2096. this.trigger.bind(this, '_side-reached')
  2097. );
  2098. if (!duration && typeof duration !== 'number') {
  2099. // desired radial speed
  2100. duration = duration ? PSVUtils.parseSpeed(duration) : this.config.anim_speed;
  2101. // get the angle between current position and target
  2102. var angle = Math.acos(
  2103. Math.cos(this.prop.latitude) * Math.cos(position.latitude) * Math.cos(this.prop.longitude - position.longitude) +
  2104. Math.sin(this.prop.latitude) * Math.sin(position.latitude)
  2105. );
  2106. // compute duration
  2107. duration = angle / duration * 1000;
  2108. }
  2109. // longitude offset for shortest arc
  2110. var tOffset = PSVUtils.getShortestArc(this.prop.longitude, position.longitude);
  2111. this.prop.animation_promise = PSVUtils.animation({
  2112. properties: {
  2113. longitude: { start: this.prop.longitude, end: this.prop.longitude + tOffset },
  2114. latitude: { start: this.prop.latitude, end: position.latitude }
  2115. },
  2116. duration: duration,
  2117. easing: 'inOutSine',
  2118. onTick: this.rotate.bind(this)
  2119. });
  2120. return this.prop.animation_promise;
  2121. };
  2122. /**
  2123. * @summary Stops the ongoing animation
  2124. */
  2125. PhotoSphereViewer.prototype.stopAnimation = function() {
  2126. if (this.prop.animation_promise) {
  2127. this.prop.animation_promise.cancel();
  2128. this.prop.animation_promise = null;
  2129. }
  2130. };
  2131. /**
  2132. * @summary Zooms to a specific level between `max_fov` and `min_fov`
  2133. * @param {int} level - new zoom level from 0 to 100
  2134. * @param {boolean} [render=true]
  2135. * @fires PhotoSphereViewer.zoom-updated
  2136. */
  2137. PhotoSphereViewer.prototype.zoom = function(level, render) {
  2138. this.prop.zoom_lvl = PSVUtils.bound(Math.round(level), 0, 100);
  2139. this.prop.vFov = this.config.max_fov + (this.prop.zoom_lvl / 100) * (this.config.min_fov - this.config.max_fov);
  2140. this.prop.hFov = THREE.Math.radToDeg(2 * Math.atan(Math.tan(THREE.Math.degToRad(this.prop.vFov) / 2) * this.prop.aspect));
  2141. if (render !== false && this.renderer) {
  2142. this.render();
  2143. /**
  2144. * @event zoom-updated
  2145. * @memberof PhotoSphereViewer
  2146. * @summary Triggered when the zoom level changes
  2147. * @param {int} zoomLevel
  2148. */
  2149. this.trigger('zoom-updated', this.getZoomLevel());
  2150. }
  2151. };
  2152. /**
  2153. * @summary Increases the zoom level by 1
  2154. */
  2155. PhotoSphereViewer.prototype.zoomIn = function() {
  2156. if (this.prop.zoom_lvl < 100) {
  2157. this.zoom(this.prop.zoom_lvl + 1);
  2158. }
  2159. };
  2160. /**
  2161. * @summary Decreases the zoom level by 1
  2162. */
  2163. PhotoSphereViewer.prototype.zoomOut = function() {
  2164. if (this.prop.zoom_lvl > 0) {
  2165. this.zoom(this.prop.zoom_lvl - 1);
  2166. }
  2167. };
  2168. /**
  2169. * @summary Resizes the viewer
  2170. * @param {PhotoSphereViewer.CssSize} size
  2171. */
  2172. PhotoSphereViewer.prototype.resize = function(size) {
  2173. if (size.width) {
  2174. this.container.style.width = size.width;
  2175. }
  2176. if (size.height) {
  2177. this.container.style.height = size.height;
  2178. }
  2179. this._onResize();
  2180. };
  2181. /**
  2182. * @summary Enters or exits the fullscreen mode
  2183. */
  2184. PhotoSphereViewer.prototype.toggleFullscreen = function() {
  2185. if (!this.isFullscreenEnabled()) {
  2186. PSVUtils.requestFullscreen(this.container);
  2187. } else {
  2188. PSVUtils.exitFullscreen();
  2189. }
  2190. };
  2191. /**
  2192. * @summary Enables the keyboard controls (done automatically when entering fullscreen)
  2193. */
  2194. PhotoSphereViewer.prototype.startKeyboardControl = function() {
  2195. window.addEventListener('keydown', this);
  2196. };
  2197. /**
  2198. * @summary Disables the keyboard controls (done automatically when exiting fullscreen)
  2199. */
  2200. PhotoSphereViewer.prototype.stopKeyboardControl = function() {
  2201. window.removeEventListener('keydown', this);
  2202. };
  2203. /**
  2204. * @summary Preload a panorama file without displaying it
  2205. * @param {string} panorama
  2206. * @returns {Promise}
  2207. * @throws {PSVError} when the cache is disabled
  2208. */
  2209. PhotoSphereViewer.prototype.preloadPanorama = function(panorama) {
  2210. if (!this.config.cache_texture) {
  2211. throw new PSVError('Cannot preload panorama, cache_texture is disabled');
  2212. }
  2213. return this._loadTexture(panorama);
  2214. };
  2215. /**
  2216. * @summary Removes a panorama from the cache or clears the entire cache
  2217. * @param {string} [panorama]
  2218. * @throws {PSVError} when the cache is disabled
  2219. */
  2220. PhotoSphereViewer.prototype.clearPanoramaCache = function(panorama) {
  2221. if (!this.config.cache_texture) {
  2222. throw new PSVError('Cannot clear cache, cache_texture is disabled');
  2223. }
  2224. if (panorama) {
  2225. for (var i = 0, l = this.prop.cache.length; i < l; i++) {
  2226. if (this.prop.cache[i].panorama === panorama) {
  2227. this.prop.cache.splice(i, 1);
  2228. break;
  2229. }
  2230. }
  2231. } else {
  2232. this.prop.cache.length = 0;
  2233. }
  2234. };
  2235. /**
  2236. * @summary Retrieves the cache for a panorama
  2237. * @param {string} panorama
  2238. * @returns {PhotoSphereViewer.CacheItem}
  2239. * @throws {PSVError} when the cache is disabled
  2240. */
  2241. PhotoSphereViewer.prototype.getPanoramaCache = function(panorama) {
  2242. if (!this.config.cache_texture) {
  2243. throw new PSVError('Cannot query cache, cache_texture is disabled');
  2244. }
  2245. return this.prop.cache.filter(function(cache) {
  2246. return cache.panorama === panorama;
  2247. }).shift();
  2248. };
  2249. /**
  2250. * @summary Inits the global SYSTEM var with generic support information
  2251. * @private
  2252. */
  2253. PhotoSphereViewer._loadSystem = function() {
  2254. var S = PhotoSphereViewer.SYSTEM;
  2255. S.loaded = true;
  2256. S.pixelRatio = window.devicePixelRatio || 1;
  2257. S.isWebGLSupported = PSVUtils.isWebGLSupported();
  2258. S.isCanvasSupported = PSVUtils.isCanvasSupported();
  2259. S.maxTextureWidth = S.isWebGLSupported ? PSVUtils.getMaxTextureWidth() : 4096;
  2260. S.mouseWheelEvent = PSVUtils.mouseWheelEvent();
  2261. S.fullscreenEvent = PSVUtils.fullscreenEvent();
  2262. S.deviceOrientationSupported = PSVUtils.isDeviceOrientationSupported();
  2263. };
  2264. /**
  2265. * @summary Sets the viewer size
  2266. * @param {PhotoSphereViewer.Size} size
  2267. * @private
  2268. */
  2269. PhotoSphereViewer.prototype._setViewerSize = function(size) {
  2270. ['width', 'height'].forEach(function(dim) {
  2271. if (size[dim]) {
  2272. if (/^[0-9.]+$/.test(size[dim])) {
  2273. size[dim] += 'px';
  2274. }
  2275. this.parent.style[dim] = size[dim];
  2276. }
  2277. }, this);
  2278. };
  2279. /**
  2280. * @summary Converts pixel texture coordinates to spherical radians coordinates
  2281. * @param {PhotoSphereViewer.Point} point
  2282. * @returns {PhotoSphereViewer.Position}
  2283. */
  2284. PhotoSphereViewer.prototype.textureCoordsToSphericalCoords = function(point) {
  2285. if (this.prop.isCubemap) {
  2286. throw new PSVError('Unable to use texture coords with cubemap.');
  2287. }
  2288. var relativeX = (point.x + this.prop.pano_data.cropped_x) / this.prop.pano_data.full_width * PSVUtils.TwoPI;
  2289. var relativeY = (point.y + this.prop.pano_data.cropped_y) / this.prop.pano_data.full_height * Math.PI;
  2290. return {
  2291. longitude: relativeX >= Math.PI ? relativeX - Math.PI : relativeX + Math.PI,
  2292. latitude: PSVUtils.HalfPI - relativeY
  2293. };
  2294. };
  2295. /**
  2296. * @summary Converts spherical radians coordinates to pixel texture coordinates
  2297. * @param {PhotoSphereViewer.Position} position
  2298. * @returns {PhotoSphereViewer.Point}
  2299. */
  2300. PhotoSphereViewer.prototype.sphericalCoordsToTextureCoords = function(position) {
  2301. if (this.prop.isCubemap) {
  2302. throw new PSVError('Unable to use texture coords with cubemap.');
  2303. }
  2304. var relativeLong = position.longitude / PSVUtils.TwoPI * this.prop.pano_data.full_width;
  2305. var relativeLat = position.latitude / Math.PI * this.prop.pano_data.full_height;
  2306. return {
  2307. x: parseInt(position.longitude < Math.PI ? relativeLong + this.prop.pano_data.full_width / 2 : relativeLong - this.prop.pano_data.full_width / 2) - this.prop.pano_data.cropped_x,
  2308. y: parseInt(this.prop.pano_data.full_height / 2 - relativeLat) - this.prop.pano_data.cropped_y
  2309. };
  2310. };
  2311. /**
  2312. * @summary Converts spherical radians coordinates to a THREE.Vector3
  2313. * @param {PhotoSphereViewer.Position} position
  2314. * @returns {THREE.Vector3}
  2315. */
  2316. PhotoSphereViewer.prototype.sphericalCoordsToVector3 = function(position) {
  2317. return new THREE.Vector3(
  2318. PhotoSphereViewer.SPHERE_RADIUS * -Math.cos(position.latitude) * Math.sin(position.longitude),
  2319. PhotoSphereViewer.SPHERE_RADIUS * Math.sin(position.latitude),
  2320. PhotoSphereViewer.SPHERE_RADIUS * Math.cos(position.latitude) * Math.cos(position.longitude)
  2321. );
  2322. };
  2323. /**
  2324. * @summary Converts a THREE.Vector3 to spherical radians coordinates
  2325. * @param {THREE.Vector3} vector
  2326. * @returns {PhotoSphereViewer.Position}
  2327. */
  2328. PhotoSphereViewer.prototype.vector3ToSphericalCoords = function(vector) {
  2329. var phi = Math.acos(vector.y / Math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z));
  2330. var theta = Math.atan2(vector.x, vector.z);
  2331. return {
  2332. longitude: theta < 0 ? -theta : PSVUtils.TwoPI - theta,
  2333. latitude: PSVUtils.HalfPI - phi
  2334. };
  2335. };
  2336. /**
  2337. * @summary Converts position on the viewer to a THREE.Vector3
  2338. * @param {PhotoSphereViewer.Point} viewerPoint
  2339. * @returns {THREE.Vector3}
  2340. */
  2341. PhotoSphereViewer.prototype.viewerCoordsToVector3 = function(viewerPoint) {
  2342. var screen = new THREE.Vector2(
  2343. 2 * viewerPoint.x / this.prop.size.width - 1, -2 * viewerPoint.y / this.prop.size.height + 1
  2344. );
  2345. this.raycaster.setFromCamera(screen, this.camera);
  2346. var intersects = this.raycaster.intersectObjects(this.scene.children);
  2347. if (intersects.length === 1) {
  2348. return intersects[0].point;
  2349. } else {
  2350. return null;
  2351. }
  2352. };
  2353. /**
  2354. * @summary Converts a THREE.Vector3 to position on the viewer
  2355. * @param {THREE.Vector3} vector
  2356. * @returns {PhotoSphereViewer.Point}
  2357. */
  2358. PhotoSphereViewer.prototype.vector3ToViewerCoords = function(vector) {
  2359. vector = vector.clone();
  2360. vector.project(this.camera);
  2361. return {
  2362. x: parseInt((vector.x + 1) / 2 * this.prop.size.width),
  2363. y: parseInt((1 - vector.y) / 2 * this.prop.size.height)
  2364. };
  2365. };
  2366. /**
  2367. * @summary Converts x/y to latitude/longitude if present and ensure boundaries
  2368. * @param {PhotoSphereViewer.ExtendedPosition} position - mutated
  2369. * @private
  2370. */
  2371. PhotoSphereViewer.prototype.cleanPosition = function(position) {
  2372. if (position.hasOwnProperty('x') && position.hasOwnProperty('y')) {
  2373. PSVUtils.deepmerge(position, this.textureCoordsToSphericalCoords(position));
  2374. }
  2375. position.longitude = PSVUtils.parseAngle(position.longitude);
  2376. position.latitude = PSVUtils.parseAngle(position.latitude, true);
  2377. };
  2378. /**
  2379. * @summary Apply "longitude_range" and "latitude_range"
  2380. * @param {PhotoSphereViewer.Position} position - mutated
  2381. * @returns {string[]} list of sides that were reached
  2382. * @private
  2383. */
  2384. PhotoSphereViewer.prototype.applyRanges = function(position) {
  2385. var range, offset, sidesReached = [];
  2386. if (this.config.longitude_range) {
  2387. range = PSVUtils.clone(this.config.longitude_range);
  2388. offset = THREE.Math.degToRad(this.prop.hFov) / 2;
  2389. range[0] = PSVUtils.parseAngle(range[0] + offset);
  2390. range[1] = PSVUtils.parseAngle(range[1] - offset);
  2391. if (range[0] > range[1]) { // when the range cross longitude 0
  2392. if (position.longitude > range[1] && position.longitude < range[0]) {
  2393. if (position.longitude > (range[0] / 2 + range[1] / 2)) { // detect which side we are closer too
  2394. position.longitude = range[0];
  2395. sidesReached.push('left');
  2396. } else {
  2397. position.longitude = range[1];
  2398. sidesReached.push('right');
  2399. }
  2400. }
  2401. } else {
  2402. if (position.longitude < range[0]) {
  2403. position.longitude = range[0];
  2404. sidesReached.push('left');
  2405. } else if (position.longitude > range[1]) {
  2406. position.longitude = range[1];
  2407. sidesReached.push('right');
  2408. }
  2409. }
  2410. }
  2411. if (this.config.latitude_range) {
  2412. range = PSVUtils.clone(this.config.latitude_range);
  2413. offset = THREE.Math.degToRad(this.prop.vFov) / 2;
  2414. range[0] = PSVUtils.parseAngle(Math.min(range[0] + offset, range[1]), true);
  2415. range[1] = PSVUtils.parseAngle(Math.max(range[1] - offset, range[0]), true);
  2416. if (position.latitude < range[0]) {
  2417. position.latitude = range[0];
  2418. sidesReached.push('bottom');
  2419. } else if (position.latitude > range[1]) {
  2420. position.latitude = range[1];
  2421. sidesReached.push('top');
  2422. }
  2423. }
  2424. return sidesReached;
  2425. };
  2426. /**
  2427. * @module components
  2428. */
  2429. /**
  2430. * Base sub-component class
  2431. * @param {PhotoSphereViewer | module:components.PSVComponent} parent
  2432. * @constructor
  2433. * @memberof module:components
  2434. */
  2435. function PSVComponent(parent) {
  2436. /**
  2437. * @member {PhotoSphereViewer}
  2438. * @readonly
  2439. */
  2440. this.psv = parent instanceof PhotoSphereViewer ? parent : parent.psv;
  2441. /**
  2442. * @member {PhotoSphereViewer|module:components.PSVComponent}
  2443. * @readonly
  2444. */
  2445. this.parent = parent;
  2446. /**
  2447. * @member {HTMLElement}
  2448. * @readonly
  2449. */
  2450. this.container = null;
  2451. // expose some methods to the viewer
  2452. if (this.constructor.publicMethods) {
  2453. this.constructor.publicMethods.forEach(function(method) {
  2454. this.psv[method] = this[method].bind(this);
  2455. }, this);
  2456. }
  2457. }
  2458. /**
  2459. * @summary CSS class added to the component's container
  2460. * @member {string}
  2461. * @readonly
  2462. */
  2463. PSVComponent.className = null;
  2464. /**
  2465. * @summary List of component's methods which are bound the the main viewer
  2466. * @member {string[]}
  2467. * @readonly
  2468. */
  2469. PSVComponent.publicMethods = [];
  2470. /**
  2471. * @summary Creates the component
  2472. * @protected
  2473. */
  2474. PSVComponent.prototype.create = function() {
  2475. this.container = document.createElement('div');
  2476. if (this.constructor.className) {
  2477. this.container.className = this.constructor.className;
  2478. }
  2479. this.parent.container.appendChild(this.container);
  2480. };
  2481. /**
  2482. * @summary Destroys the component
  2483. * @protected
  2484. */
  2485. PSVComponent.prototype.destroy = function() {
  2486. this.parent.container.removeChild(this.container);
  2487. if (this.constructor.publicMethods) {
  2488. this.constructor.publicMethods.forEach(function(method) {
  2489. delete this.psv[method];
  2490. }, this);
  2491. }
  2492. delete this.container;
  2493. delete this.psv;
  2494. delete this.parent;
  2495. };
  2496. /**
  2497. * @summary Hides the component
  2498. * @protected
  2499. */
  2500. PSVComponent.prototype.hide = function() {
  2501. this.container.style.display = 'none';
  2502. };
  2503. /**
  2504. * @summary Displays the component
  2505. * @protected
  2506. */
  2507. PSVComponent.prototype.show = function() {
  2508. this.container.style.display = '';
  2509. };
  2510. /**
  2511. * HUD class
  2512. * @param {PhotoSphereViewer} psv
  2513. * @constructor
  2514. * @extends module:components.PSVComponent
  2515. * @memberof module:components
  2516. */
  2517. function PSVHUD(psv) {
  2518. PSVComponent.call(this, psv);
  2519. /**
  2520. * @member {SVGElement}
  2521. * @readonly
  2522. */
  2523. this.svgContainer = null;
  2524. /**
  2525. * @summary All registered markers
  2526. * @member {Object.<string, PSVMarker>}
  2527. */
  2528. this.markers = {};
  2529. /**
  2530. * @summary Last selected marker
  2531. * @member {PSVMarker}
  2532. * @readonly
  2533. */
  2534. this.currentMarker = null;
  2535. /**
  2536. * @summary Marker under the cursor
  2537. * @member {PSVMarker}
  2538. * @readonly
  2539. */
  2540. this.hoveringMarker = null;
  2541. /**
  2542. * @member {Object}
  2543. * @private
  2544. */
  2545. this.prop = {
  2546. panelOpened: false,
  2547. panelOpening: false,
  2548. markersButton: this.psv.navbar.getNavbarButton('markers', true)
  2549. };
  2550. this.create();
  2551. }
  2552. PSVHUD.prototype = Object.create(PSVComponent.prototype);
  2553. PSVHUD.prototype.constructor = PSVHUD;
  2554. PSVHUD.className = 'psv-hud';
  2555. PSVHUD.publicMethods = [
  2556. 'addMarker',
  2557. 'removeMarker',
  2558. 'updateMarker',
  2559. 'clearMarkers',
  2560. 'getMarker',
  2561. 'getCurrentMarker',
  2562. 'gotoMarker',
  2563. 'hideMarker',
  2564. 'showMarker',
  2565. 'toggleMarker',
  2566. 'toggleMarkersList',
  2567. 'showMarkersList',
  2568. 'hideMarkersList'
  2569. ];
  2570. /**
  2571. * @override
  2572. */
  2573. PSVHUD.prototype.create = function() {
  2574. PSVComponent.prototype.create.call(this);
  2575. this.svgContainer = document.createElementNS(PSVUtils.svgNS, 'svg');
  2576. this.svgContainer.setAttribute('class', 'psv-hud-svg-container');
  2577. this.container.appendChild(this.svgContainer);
  2578. // Markers events via delegation
  2579. this.container.addEventListener('mouseenter', this, true);
  2580. this.container.addEventListener('mouseleave', this, true);
  2581. this.container.addEventListener('mousemove', this, true);
  2582. // Viewer events
  2583. this.psv.on('click', this);
  2584. this.psv.on('dblclick', this);
  2585. this.psv.on('render', this);
  2586. this.psv.on('open-panel', this);
  2587. this.psv.on('close-panel', this);
  2588. };
  2589. /**
  2590. * @override
  2591. */
  2592. PSVHUD.prototype.destroy = function() {
  2593. this.clearMarkers(false);
  2594. this.container.removeEventListener('mouseenter', this);
  2595. this.container.removeEventListener('mouseleave', this);
  2596. this.container.removeEventListener('mousemove', this);
  2597. this.psv.off('click', this);
  2598. this.psv.off('dblclick', this);
  2599. this.psv.off('render', this);
  2600. this.psv.off('open-panel', this);
  2601. this.psv.off('close-panel', this);
  2602. delete this.svgContainer;
  2603. PSVComponent.prototype.destroy.call(this);
  2604. };
  2605. /**
  2606. * @summary Handles events
  2607. * @param {Event} e
  2608. * @private
  2609. */
  2610. PSVHUD.prototype.handleEvent = function(e) {
  2611. switch (e.type) {
  2612. // @formatter:off
  2613. case 'mouseenter':
  2614. this._onMouseEnter(e);
  2615. break;
  2616. case 'mouseleave':
  2617. this._onMouseLeave(e);
  2618. break;
  2619. case 'mousemove':
  2620. this._onMouseMove(e);
  2621. break;
  2622. case 'click':
  2623. this._onClick(e.args[0], e, false);
  2624. break;
  2625. case 'dblclick':
  2626. this._onClick(e.args[0], e, true);
  2627. break;
  2628. case 'render':
  2629. this.renderMarkers();
  2630. break;
  2631. case 'open-panel':
  2632. this._onPanelOpened();
  2633. break;
  2634. case 'close-panel':
  2635. this._onPanelClosed();
  2636. break;
  2637. // @formatter:on
  2638. }
  2639. };
  2640. /**
  2641. * @summary Adds a new marker to viewer
  2642. * @param {Object} properties - see {@link http://photo-sphere-viewer.js.org/markers.html#config}
  2643. * @param {boolean} [render=true] - renders the marker immediately
  2644. * @returns {PSVMarker}
  2645. * @throws {PSVError} when the marker's id is missing or already exists
  2646. */
  2647. PSVHUD.prototype.addMarker = function(properties, render) {
  2648. if (!properties.id) {
  2649. throw new PSVError('missing marker id');
  2650. }
  2651. if (this.markers[properties.id]) {
  2652. throw new PSVError('marker "' + properties.id + '" already exists');
  2653. }
  2654. var marker = new PSVMarker(properties, this.psv);
  2655. if (marker.isNormal()) {
  2656. this.container.appendChild(marker.$el);
  2657. } else {
  2658. this.svgContainer.appendChild(marker.$el);
  2659. }
  2660. this.markers[marker.id] = marker;
  2661. if (render !== false) {
  2662. this.renderMarkers();
  2663. }
  2664. return marker;
  2665. };
  2666. /**
  2667. * @summary Returns the internal marker object for a marker id
  2668. * @param {*} markerId
  2669. * @returns {PSVMarker}
  2670. * @throws {PSVError} when the marker cannot be found
  2671. */
  2672. PSVHUD.prototype.getMarker = function(markerId) {
  2673. var id = typeof markerId === 'object' ? markerId.id : markerId;
  2674. if (!this.markers[id]) {
  2675. throw new PSVError('cannot find marker "' + id + '"');
  2676. }
  2677. return this.markers[id];
  2678. };
  2679. /**
  2680. * @summary Returns the last marker selected by the user
  2681. * @returns {PSVMarker}
  2682. */
  2683. PSVHUD.prototype.getCurrentMarker = function() {
  2684. return this.currentMarker;
  2685. };
  2686. /**
  2687. * @summary Updates the existing marker with the same id
  2688. * @description Every property can be changed but you can't change its type (Eg: `image` to `html`).
  2689. * @param {Object|PSVMarker} properties
  2690. * @param {boolean} [render=true] - renders the marker immediately
  2691. * @returns {PSVMarker}
  2692. */
  2693. PSVHUD.prototype.updateMarker = function(properties, render) {
  2694. var marker = this.getMarker(properties);
  2695. marker.update(properties);
  2696. if (render !== false) {
  2697. this.renderMarkers();
  2698. }
  2699. return marker;
  2700. };
  2701. /**
  2702. * @summary Removes a marker from the viewer
  2703. * @param {*} marker
  2704. * @param {boolean} [render=true] - renders the marker immediately
  2705. */
  2706. PSVHUD.prototype.removeMarker = function(marker, render) {
  2707. marker = this.getMarker(marker);
  2708. if (marker.isNormal()) {
  2709. this.container.removeChild(marker.$el);
  2710. } else {
  2711. this.svgContainer.removeChild(marker.$el);
  2712. }
  2713. if (this.hoveringMarker === marker) {
  2714. this.psv.tooltip.hideTooltip();
  2715. }
  2716. marker.destroy();
  2717. delete this.markers[marker.id];
  2718. if (render !== false) {
  2719. this.renderMarkers();
  2720. }
  2721. };
  2722. /**
  2723. * @summary Removes all markers
  2724. * @param {boolean} [render=true] - renders the markers immediately
  2725. */
  2726. PSVHUD.prototype.clearMarkers = function(render) {
  2727. Object.keys(this.markers).forEach(function(marker) {
  2728. this.removeMarker(marker, false);
  2729. }, this);
  2730. if (render !== false) {
  2731. this.renderMarkers();
  2732. }
  2733. };
  2734. /**
  2735. * @summary Rotate the view to face the marker
  2736. * @param {*} marker
  2737. * @param {string|int} [duration] - rotates smoothy, see {@link PhotoSphereViewer#animate}
  2738. * @fires module:components.PSVHUD.goto-marker-done
  2739. * @return {Promise} A promise that will be resolved when the animation finishes
  2740. */
  2741. PSVHUD.prototype.gotoMarker = function(marker, duration) {
  2742. marker = this.getMarker(marker);
  2743. return this.psv.animate(marker, duration)
  2744. .then(function() {
  2745. /**
  2746. * @event goto-marker-done
  2747. * @memberof module:components.PSVHUD
  2748. * @summary Triggered when the animation to a marker is done
  2749. * @param {PSVMarker} marker
  2750. */
  2751. this.psv.trigger('goto-marker-done', marker);
  2752. }.bind(this));
  2753. };
  2754. /**
  2755. * @summary Hides a marker
  2756. * @param {*} marker
  2757. */
  2758. PSVHUD.prototype.hideMarker = function(marker) {
  2759. this.getMarker(marker).visible = false;
  2760. this.renderMarkers();
  2761. };
  2762. /**
  2763. * @summary Shows a marker
  2764. * @param {*} marker
  2765. */
  2766. PSVHUD.prototype.showMarker = function(marker) {
  2767. this.getMarker(marker).visible = true;
  2768. this.renderMarkers();
  2769. };
  2770. /**
  2771. * @summary Toggles a marker
  2772. * @param {*} marker
  2773. */
  2774. PSVHUD.prototype.toggleMarker = function(marker) {
  2775. this.getMarker(marker).visible ^= true;
  2776. this.renderMarkers();
  2777. };
  2778. /**
  2779. * @summary Toggles the visibility of markers list
  2780. */
  2781. PSVHUD.prototype.toggleMarkersList = function() {
  2782. if (this.prop.panelOpened) {
  2783. this.hideMarkersList();
  2784. } else {
  2785. this.showMarkersList();
  2786. }
  2787. };
  2788. /**
  2789. * @summary Opens side panel with list of markers
  2790. * @fires module:components.PSVHUD.filter:render-markers-list
  2791. */
  2792. PSVHUD.prototype.showMarkersList = function() {
  2793. var markers = [];
  2794. PSVUtils.forEach(this.markers, function(marker) {
  2795. markers.push(marker);
  2796. });
  2797. /**
  2798. * @event filter:render-markers-list
  2799. * @memberof module:components.PSVHUD
  2800. * @summary Used to alter the list of markers displayed on the side-panel
  2801. * @param {PSVMarker[]} markers
  2802. * @returns {PSVMarker[]}
  2803. */
  2804. var html = this.psv.config.templates.markersList({
  2805. markers: this.psv.change('render-markers-list', markers),
  2806. config: this.psv.config
  2807. });
  2808. this.prop.panelOpening = true;
  2809. this.psv.panel.showPanel(html, true);
  2810. this.psv.panel.container.querySelector('.psv-markers-list').addEventListener('click', this._onClickItem.bind(this));
  2811. };
  2812. /**
  2813. * @summary Closes side panel if it contains the list of markers
  2814. */
  2815. PSVHUD.prototype.hideMarkersList = function() {
  2816. if (this.prop.panelOpened) {
  2817. this.psv.panel.hidePanel();
  2818. }
  2819. };
  2820. /**
  2821. * @summary Updates the visibility and the position of all markers
  2822. */
  2823. PSVHUD.prototype.renderMarkers = function() {
  2824. var rotation = !this.psv.isGyroscopeEnabled() ? 0 : THREE.Math.radToDeg(this.psv.camera.rotation.z);
  2825. PSVUtils.forEach(this.markers, function(marker) {
  2826. var isVisible = marker.visible;
  2827. if (isVisible && marker.isPoly()) {
  2828. var positions = this._getPolyPositions(marker);
  2829. isVisible = positions.length > (marker.isPolygon() ? 2 : 1);
  2830. if (isVisible) {
  2831. marker.position2D = this._getPolyDimensions(marker, positions);
  2832. var points = positions.map(function(pos) {
  2833. return pos.x + ',' + pos.y;
  2834. }).join(' ');
  2835. marker.$el.setAttributeNS(null, 'points', points);
  2836. }
  2837. } else if (isVisible) {
  2838. var position = this._getMarkerPosition(marker);
  2839. isVisible = this._isMarkerVisible(marker, position);
  2840. if (isVisible) {
  2841. marker.position2D = position;
  2842. var scale = marker.getScale(this.psv.getZoomLevel());
  2843. if (marker.isSvg()) {
  2844. marker.$el.setAttributeNS(null, 'transform',
  2845. 'translate(' + position.x + ', ' + position.y + ')' +
  2846. (scale !== 1 ? ' scale(' + scale + ', ' + scale + ')' : '') +
  2847. (!marker.lockRotation && rotation ? ' rotate(' + rotation + ')' : '')
  2848. );
  2849. } else {
  2850. marker.$el.style.transform = 'translate3D(' + position.x + 'px, ' + position.y + 'px, 0px)' +
  2851. (scale !== 1 ? ' scale(' + scale + ', ' + scale + ')' : '') +
  2852. (!marker.lockRotation && rotation ? ' rotateZ(' + rotation + 'deg)' : '');
  2853. }
  2854. }
  2855. }
  2856. PSVUtils.toggleClass(marker.$el, 'psv-marker--visible', isVisible);
  2857. }.bind(this));
  2858. };
  2859. /**
  2860. * @summary Determines if a point marker is visible<br>
  2861. * It tests if the point is in the general direction of the camera, then check if it's in the viewport
  2862. * @param {PSVMarker} marker
  2863. * @param {PhotoSphereViewer.Point} position
  2864. * @returns {boolean}
  2865. * @private
  2866. */
  2867. PSVHUD.prototype._isMarkerVisible = function(marker, position) {
  2868. return marker.position3D.dot(this.psv.prop.direction) > 0 &&
  2869. position.x + marker.width >= 0 &&
  2870. position.x - marker.width <= this.psv.prop.size.width &&
  2871. position.y + marker.height >= 0 &&
  2872. position.y - marker.height <= this.psv.prop.size.height;
  2873. };
  2874. /**
  2875. * @summary Computes HUD coordinates of a marker
  2876. * @param {PSVMarker} marker
  2877. * @returns {PhotoSphereViewer.Point}
  2878. * @private
  2879. */
  2880. PSVHUD.prototype._getMarkerPosition = function(marker) {
  2881. if (marker._dynamicSize) {
  2882. // make the marker visible to get it's size
  2883. PSVUtils.toggleClass(marker.$el, 'psv-marker--transparent', true);
  2884. var transform = marker.$el.style.transform;
  2885. marker.$el.style.transform = null;
  2886. var rect = marker.$el.getBoundingClientRect();
  2887. marker.$el.style.transform = transform;
  2888. PSVUtils.toggleClass(marker.$el, 'psv-marker--transparent', false);
  2889. marker.width = rect.right - rect.left;
  2890. marker.height = rect.bottom - rect.top;
  2891. }
  2892. var position = this.psv.vector3ToViewerCoords(marker.position3D);
  2893. position.x -= marker.width * marker.anchor.left;
  2894. position.y -= marker.height * marker.anchor.top;
  2895. return position;
  2896. };
  2897. /**
  2898. * @summary Computes HUD coordinates of each point of a polygon/polyline<br>
  2899. * It handles points behind the camera by creating intermediary points suitable for the projector
  2900. * @param {PSVMarker} marker
  2901. * @returns {PhotoSphereViewer.Point[]}
  2902. * @private
  2903. */
  2904. PSVHUD.prototype._getPolyPositions = function(marker) {
  2905. var nbVectors = marker.positions3D.length;
  2906. // compute if each vector is visible
  2907. var positions3D = marker.positions3D.map(function(vector) {
  2908. return {
  2909. vector: vector,
  2910. visible: vector.dot(this.psv.prop.direction) > 0
  2911. };
  2912. }, this);
  2913. // get pairs of visible/invisible vectors for each invisible vector connected to a visible vector
  2914. var toBeComputed = [];
  2915. positions3D.forEach(function(pos, i) {
  2916. if (!pos.visible) {
  2917. var neighbours = [
  2918. i === 0 ? positions3D[nbVectors - 1] : positions3D[i - 1],
  2919. i === nbVectors - 1 ? positions3D[0] : positions3D[i + 1]
  2920. ];
  2921. neighbours.forEach(function(neighbour) {
  2922. if (neighbour.visible) {
  2923. toBeComputed.push({
  2924. visible: neighbour,
  2925. invisible: pos,
  2926. index: i
  2927. });
  2928. }
  2929. });
  2930. }
  2931. });
  2932. // compute intermediary vector for each pair (the loop is reversed for splice to insert at the right place)
  2933. toBeComputed.reverse().forEach(function(pair) {
  2934. positions3D.splice(pair.index, 0, {
  2935. vector: this._getPolyIntermediaryPoint(pair.visible.vector, pair.invisible.vector),
  2936. visible: true
  2937. });
  2938. }, this);
  2939. // translate vectors to screen pos
  2940. return positions3D
  2941. .filter(function(pos) {
  2942. return pos.visible;
  2943. })
  2944. .map(function(pos) {
  2945. return this.psv.vector3ToViewerCoords(pos.vector);
  2946. }, this);
  2947. };
  2948. /**
  2949. * Given one point in the same direction of the camera and one point behind the camera,
  2950. * computes an intermediary point on the great circle delimiting the half sphere visible by the camera.
  2951. * The point is shifted by .01 rad because the projector cannot handle points exactly on this circle.
  2952. * {@link http://math.stackexchange.com/a/1730410/327208}
  2953. * @param P1 {THREE.Vector3}
  2954. * @param P2 {THREE.Vector3}
  2955. * @returns {THREE.Vector3}
  2956. * @private
  2957. */
  2958. PSVHUD.prototype._getPolyIntermediaryPoint = function(P1, P2) {
  2959. var C = this.psv.prop.direction.clone().normalize();
  2960. var N = new THREE.Vector3().crossVectors(P1, P2).normalize();
  2961. var V = new THREE.Vector3().crossVectors(N, P1).normalize();
  2962. var H = new THREE.Vector3().addVectors(P1.clone().multiplyScalar(-C.dot(V)), V.clone().multiplyScalar(C.dot(P1))).normalize();
  2963. var a = new THREE.Vector3().crossVectors(H, C);
  2964. return H.applyAxisAngle(a, 0.01).multiplyScalar(PhotoSphereViewer.SPHERE_RADIUS);
  2965. };
  2966. /**
  2967. * @summary Computes the boundaries positions of a polygon/polyline marker
  2968. * @param {PSVMarker} marker - alters width and height
  2969. * @param {PhotoSphereViewer.Point[]} positions
  2970. * @returns {PhotoSphereViewer.Point}
  2971. * @private
  2972. */
  2973. PSVHUD.prototype._getPolyDimensions = function(marker, positions) {
  2974. var minX = +Infinity;
  2975. var minY = +Infinity;
  2976. var maxX = -Infinity;
  2977. var maxY = -Infinity;
  2978. positions.forEach(function(pos) {
  2979. minX = Math.min(minX, pos.x);
  2980. minY = Math.min(minY, pos.y);
  2981. maxX = Math.max(maxX, pos.x);
  2982. maxY = Math.max(maxY, pos.y);
  2983. });
  2984. marker.width = maxX - minX;
  2985. marker.height = maxY - minY;
  2986. return {
  2987. x: minX,
  2988. y: minY
  2989. };
  2990. };
  2991. /**
  2992. * @summary Handles mouse enter events, show the tooltip for non polygon markers
  2993. * @param {MouseEvent} e
  2994. * @fires module:components.PSVHUD.over-marker
  2995. * @private
  2996. */
  2997. PSVHUD.prototype._onMouseEnter = function(e) {
  2998. var marker;
  2999. if (e.target && (marker = e.target.psvMarker) && !marker.isPoly()) {
  3000. this.hoveringMarker = marker;
  3001. /**
  3002. * @event over-marker
  3003. * @memberof module:components.PSVHUD
  3004. * @summary Triggered when the user puts the cursor hover a marker
  3005. * @param {PSVMarker} marker
  3006. */
  3007. this.psv.trigger('over-marker', marker);
  3008. if (marker.tooltip) {
  3009. this.psv.tooltip.showTooltip({
  3010. content: marker.tooltip.content,
  3011. position: marker.tooltip.position,
  3012. left: marker.position2D.x,
  3013. top: marker.position2D.y,
  3014. box: {
  3015. width: marker.width,
  3016. height: marker.height
  3017. }
  3018. });
  3019. }
  3020. }
  3021. };
  3022. /**
  3023. * @summary Handles mouse leave events, hide the tooltip
  3024. * @param {MouseEvent} e
  3025. * @fires module:components.PSVHUD.leave-marker
  3026. * @private
  3027. */
  3028. PSVHUD.prototype._onMouseLeave = function(e) {
  3029. var marker;
  3030. if (e.target && (marker = e.target.psvMarker)) {
  3031. // do not hide if we enter the tooltip itself while hovering a polygon
  3032. if (marker.isPoly() && e.relatedTarget && PSVUtils.hasParent(e.relatedTarget, this.psv.tooltip.container)) {
  3033. return;
  3034. }
  3035. /**
  3036. * @event leave-marker
  3037. * @memberof module:components.PSVHUD
  3038. * @summary Triggered when the user puts the cursor away from a marker
  3039. * @param {PSVMarker} marker
  3040. */
  3041. this.psv.trigger('leave-marker', marker);
  3042. this.hoveringMarker = null;
  3043. this.psv.tooltip.hideTooltip();
  3044. }
  3045. };
  3046. /**
  3047. * @summary Handles mouse move events, refresh the tooltip for polygon markers
  3048. * @param {MouseEvent} e
  3049. * @fires module:components.PSVHUD.leave-marker
  3050. * @fires module:components.PSVHUD.over-marker
  3051. * @private
  3052. */
  3053. PSVHUD.prototype._onMouseMove = function(e) {
  3054. if (!this.psv.prop.moving) {
  3055. var marker;
  3056. // do not hide if we enter the tooltip itself while hovering a polygon
  3057. if (e.target && (marker = e.target.psvMarker) && marker.isPoly() ||
  3058. e.target && PSVUtils.hasParent(e.target, this.psv.tooltip.container) && (marker = this.hoveringMarker)) {
  3059. if (!this.hoveringMarker) {
  3060. this.psv.trigger('over-marker', marker);
  3061. this.hoveringMarker = marker;
  3062. }
  3063. var boundingRect = this.psv.container.getBoundingClientRect();
  3064. if (marker.tooltip) {
  3065. this.psv.tooltip.showTooltip({
  3066. content: marker.tooltip.content,
  3067. position: marker.tooltip.position,
  3068. top: e.clientY - boundingRect.top - this.psv.config.tooltip.arrow_size / 2,
  3069. left: e.clientX - boundingRect.left - this.psv.config.tooltip.arrow_size,
  3070. box: { // separate the tooltip from the cursor
  3071. width: this.psv.config.tooltip.arrow_size * 2,
  3072. height: this.psv.config.tooltip.arrow_size * 2
  3073. }
  3074. });
  3075. }
  3076. } else if (this.hoveringMarker && this.hoveringMarker.isPoly()) {
  3077. this.psv.trigger('leave-marker', this.hoveringMarker);
  3078. this.hoveringMarker = null;
  3079. this.psv.tooltip.hideTooltip();
  3080. }
  3081. }
  3082. };
  3083. /**
  3084. * @summary Handles mouse click events, select the marker and open the panel if necessary
  3085. * @param {Object} data
  3086. * @param {Event} e
  3087. * @param {boolean} dblclick
  3088. * @fires module:components.PSVHUD.select-marker
  3089. * @fires module:components.PSVHUD.unselect-marker
  3090. * @private
  3091. */
  3092. PSVHUD.prototype._onClick = function(data, e, dblclick) {
  3093. var marker;
  3094. if (data.target && (marker = PSVUtils.getClosest(data.target, '.psv-marker')) && marker.psvMarker) {
  3095. this.currentMarker = marker.psvMarker;
  3096. /**
  3097. * @event select-marker
  3098. * @memberof module:components.PSVHUD
  3099. * @summary Triggered when the user clicks on a marker. The marker can be retrieved from outside the event handler
  3100. * with {@link module:components.PSVHUD.getCurrentMarker}
  3101. * @param {PSVMarker} marker
  3102. * @param {boolean} dblclick - the simple click is always fired before the double click
  3103. */
  3104. this.psv.trigger('select-marker', this.currentMarker, dblclick);
  3105. if (this.psv.config.click_event_on_marker) {
  3106. // add the marker to event data
  3107. data.marker = marker.psvMarker;
  3108. } else {
  3109. e.stopPropagation();
  3110. }
  3111. } else if (this.currentMarker) {
  3112. /**
  3113. * @event unselect-marker
  3114. * @memberof module:components.PSVHUD
  3115. * @summary Triggered when a marker was selected and the user clicks elsewhere
  3116. * @param {PSVMarker} marker
  3117. */
  3118. this.psv.trigger('unselect-marker', this.currentMarker);
  3119. this.currentMarker = null;
  3120. }
  3121. if (marker && marker.psvMarker && marker.psvMarker.content) {
  3122. this.psv.panel.showPanel(marker.psvMarker.content);
  3123. } else if (this.psv.panel.prop.opened) {
  3124. e.stopPropagation();
  3125. this.psv.panel.hidePanel();
  3126. }
  3127. };
  3128. /**
  3129. * @summary Clicks on an item
  3130. * @param {MouseEvent} e
  3131. * @fires module:components.PSVHUD.select-marker-list
  3132. * @private
  3133. */
  3134. PSVHUD.prototype._onClickItem = function(e) {
  3135. var li;
  3136. if (e.target && (li = PSVUtils.getClosest(e.target, 'li')) && li.dataset.psvMarker) {
  3137. var marker = this.getMarker(li.dataset.psvMarker);
  3138. /**
  3139. * @event select-marker-list
  3140. * @memberof module:components.PSVHUD
  3141. * @summary Triggered when a marker is selected from the side panel
  3142. * @param {PSVMarker} marker
  3143. */
  3144. this.psv.trigger('select-marker-list', marker);
  3145. this.gotoMarker(marker, 1000);
  3146. this.psv.panel.hidePanel();
  3147. }
  3148. };
  3149. /**
  3150. * @summary Updates status when the panel is updated
  3151. * @private
  3152. */
  3153. PSVHUD.prototype._onPanelOpened = function() {
  3154. if (this.prop.panelOpening) {
  3155. this.prop.panelOpening = false;
  3156. this.prop.panelOpened = true;
  3157. } else {
  3158. this.prop.panelOpened = false;
  3159. }
  3160. if (this.prop.markersButton) {
  3161. this.prop.markersButton.toggleActive(this.prop.panelOpened);
  3162. }
  3163. };
  3164. /**
  3165. * @summary Updates status when the panel is updated
  3166. * @private
  3167. */
  3168. PSVHUD.prototype._onPanelClosed = function() {
  3169. this.prop.panelOpened = false;
  3170. this.prop.panelOpening = false;
  3171. if (this.prop.markersButton) {
  3172. this.prop.markersButton.toggleActive(false);
  3173. }
  3174. };
  3175. /**
  3176. * Loader class
  3177. * @param {PhotoSphereViewer} psv
  3178. * @constructor
  3179. * @extends module:components.PSVComponent
  3180. * @memberof module:components
  3181. */
  3182. function PSVLoader(psv) {
  3183. PSVComponent.call(this, psv);
  3184. /**
  3185. * @summary Animation canvas
  3186. * @member {HTMLCanvasElement}
  3187. * @readonly
  3188. * @private
  3189. */
  3190. this.canvas = null;
  3191. /**
  3192. * @summary Inner container for vertical center
  3193. * @member {HTMLElement}
  3194. * @readonly
  3195. * @private
  3196. */
  3197. this.loader = null;
  3198. this.create();
  3199. }
  3200. PSVLoader.prototype = Object.create(PSVComponent.prototype);
  3201. PSVLoader.prototype.constructor = PSVLoader;
  3202. PSVLoader.className = 'psv-loader-container';
  3203. /**
  3204. * @override
  3205. */
  3206. PSVLoader.prototype.create = function() {
  3207. PSVComponent.prototype.create.call(this);
  3208. var pixelRatio = PhotoSphereViewer.SYSTEM.pixelRatio;
  3209. this.loader = document.createElement('div');
  3210. this.loader.className = 'psv-loader';
  3211. this.container.appendChild(this.loader);
  3212. this.canvas = document.createElement('canvas');
  3213. this.canvas.className = 'psv-loader-canvas';
  3214. this.canvas.width = this.loader.clientWidth * pixelRatio;
  3215. this.canvas.height = this.loader.clientWidth * pixelRatio;
  3216. this.loader.appendChild(this.canvas);
  3217. this.tickness = (this.loader.offsetWidth - this.loader.clientWidth) / 2 * pixelRatio;
  3218. var inner;
  3219. if (this.psv.config.loading_img) {
  3220. inner = document.createElement('img');
  3221. inner.className = 'psv-loader-image';
  3222. inner.src = this.psv.config.loading_img;
  3223. } else if (this.psv.config.loading_txt) {
  3224. inner = document.createElement('div');
  3225. inner.className = 'psv-loader-text';
  3226. inner.innerHTML = this.psv.config.loading_txt;
  3227. }
  3228. if (inner) {
  3229. var a = Math.round(Math.sqrt(2 * Math.pow((this.canvas.width / 2 - this.tickness / 2) / pixelRatio, 2)));
  3230. inner.style.maxWidth = a + 'px';
  3231. inner.style.maxHeight = a + 'px';
  3232. this.loader.appendChild(inner);
  3233. }
  3234. };
  3235. /**
  3236. * @override
  3237. */
  3238. PSVLoader.prototype.destroy = function() {
  3239. delete this.loader;
  3240. delete this.canvas;
  3241. PSVComponent.prototype.destroy.call(this);
  3242. };
  3243. /**
  3244. * @summary Sets the loader progression
  3245. * @param {int} value - from 0 to 100
  3246. */
  3247. PSVLoader.prototype.setProgress = function(value) {
  3248. var context = this.canvas.getContext('2d');
  3249. context.clearRect(0, 0, this.canvas.width, this.canvas.height);
  3250. context.lineWidth = this.tickness;
  3251. context.strokeStyle = PSVUtils.getStyle(this.loader, 'color');
  3252. context.beginPath();
  3253. context.arc(
  3254. this.canvas.width / 2, this.canvas.height / 2,
  3255. this.canvas.width / 2 - this.tickness / 2, -Math.PI / 2, value / 100 * 2 * Math.PI - Math.PI / 2
  3256. );
  3257. context.stroke();
  3258. };
  3259. /**
  3260. * Navigation bar class
  3261. * @param {PhotoSphereViewer} psv
  3262. * @constructor
  3263. * @extends module:components.PSVComponent
  3264. * @memberof module:components
  3265. */
  3266. function PSVNavBar(psv) {
  3267. PSVComponent.call(this, psv);
  3268. /**
  3269. * @member {Object}
  3270. * @readonly
  3271. * @private
  3272. */
  3273. this.config = this.psv.config.navbar;
  3274. /**
  3275. * @summary List of buttons of the navbar
  3276. * @member {Array.<module:components/buttons.PSVNavBarButton>}
  3277. * @readonly
  3278. */
  3279. this.items = [];
  3280. // all buttons
  3281. if (this.config === true) {
  3282. this.config = PSVUtils.clone(PhotoSphereViewer.DEFAULTS.navbar);
  3283. }
  3284. // space separated list
  3285. else if (typeof this.config === 'string') {
  3286. this.config = this.config.split(' ');
  3287. }
  3288. // migration from object
  3289. else if (!Array.isArray(this.config)) {
  3290. console.warn('PhotoSphereViewer: hashmap form of "navbar" is deprecated, use an array instead.');
  3291. var config = this.config;
  3292. this.config = [];
  3293. PSVUtils.forEach(config, function(enabled, key) {
  3294. if (enabled) {
  3295. this.config.push(key);
  3296. }
  3297. }.bind(this));
  3298. this.config.sort(function(a, b) {
  3299. return PhotoSphereViewer.DEFAULTS.navbar.indexOf(a) - PhotoSphereViewer.DEFAULTS.navbar.indexOf(b);
  3300. });
  3301. }
  3302. this.create();
  3303. }
  3304. PSVNavBar.prototype = Object.create(PSVComponent.prototype);
  3305. PSVNavBar.prototype.constructor = PSVNavBar;
  3306. PSVNavBar.className = 'psv-navbar psv-navbar--open';
  3307. PSVNavBar.publicMethods = ['showNavbar', 'hideNavbar', 'toggleNavbar', 'getNavbarButton'];
  3308. /**
  3309. * @override
  3310. * @throws {PSVError} when the configuration is incorrect
  3311. */
  3312. PSVNavBar.prototype.create = function() {
  3313. PSVComponent.prototype.create.call(this);
  3314. this.config.forEach(function(button) {
  3315. if (typeof button === 'object') {
  3316. this.items.push(new PSVNavBarCustomButton(this, button));
  3317. } else {
  3318. switch (button) {
  3319. case PSVNavBarAutorotateButton.id:
  3320. this.items.push(new PSVNavBarAutorotateButton(this));
  3321. break;
  3322. case PSVNavBarZoomButton.id:
  3323. this.items.push(new PSVNavBarZoomButton(this));
  3324. break;
  3325. case PSVNavBarDownloadButton.id:
  3326. this.items.push(new PSVNavBarDownloadButton(this));
  3327. break;
  3328. case PSVNavBarMarkersButton.id:
  3329. this.items.push(new PSVNavBarMarkersButton(this));
  3330. break;
  3331. case PSVNavBarFullscreenButton.id:
  3332. this.items.push(new PSVNavBarFullscreenButton(this));
  3333. break;
  3334. case PSVNavBarGyroscopeButton.id:
  3335. if (this.psv.config.gyroscope) {
  3336. this.items.push(new PSVNavBarGyroscopeButton(this));
  3337. }
  3338. break;
  3339. case 'caption':
  3340. this.items.push(new PSVNavBarCaption(this, this.psv.config.caption));
  3341. break;
  3342. case 'spacer':
  3343. button = 'spacer-5';
  3344. /* falls through */
  3345. default:
  3346. var matches = button.match(/^spacer\-([0-9]+)$/);
  3347. if (matches !== null) {
  3348. this.items.push(new PSVNavBarSpacer(this, matches[1]));
  3349. } else {
  3350. throw new PSVError('Unknown button ' + button);
  3351. }
  3352. break;
  3353. }
  3354. }
  3355. }, this);
  3356. };
  3357. /**
  3358. * @override
  3359. */
  3360. PSVNavBar.prototype.destroy = function() {
  3361. this.items.forEach(function(item) {
  3362. item.destroy();
  3363. });
  3364. delete this.items;
  3365. delete this.config;
  3366. PSVComponent.prototype.destroy.call(this);
  3367. };
  3368. /**
  3369. * @summary Returns a button by its identifier
  3370. * @param {string} id
  3371. * @param {boolean} [silent=false]
  3372. * @returns {module:components/buttons.PSVNavBarButton}
  3373. */
  3374. PSVNavBar.prototype.getNavbarButton = function(id, silent) {
  3375. var button = null;
  3376. this.items.some(function(item) {
  3377. if (item.id === id) {
  3378. button = item;
  3379. return true;
  3380. } else {
  3381. return false;
  3382. }
  3383. });
  3384. if (!button && !silent) {
  3385. console.warn('PhotoSphereViewer: button "' + id + '" not found in the navbar.');
  3386. }
  3387. return button;
  3388. };
  3389. /**
  3390. * @summary Shows the navbar
  3391. */
  3392. PSVNavBar.prototype.showNavbar = function() {
  3393. this.toggleNavbar(true);
  3394. };
  3395. /**
  3396. * @summary Hides the navbar
  3397. */
  3398. PSVNavBar.prototype.hideNavbar = function() {
  3399. this.toggleNavbar(false);
  3400. };
  3401. /**
  3402. * @summary Toggles the navbar
  3403. * @param {boolean} active
  3404. */
  3405. PSVNavBar.prototype.toggleNavbar = function(active) {
  3406. PSVUtils.toggleClass(this.container, 'psv-navbar--open', active);
  3407. };
  3408. /**
  3409. * Navbar caption class
  3410. * @param {PSVNavBar} navbar
  3411. * @param {string} caption
  3412. * @constructor
  3413. * @extends module:components.PSVComponent
  3414. * @memberof module:components
  3415. */
  3416. function PSVNavBarCaption(navbar, caption) {
  3417. PSVComponent.call(this, navbar);
  3418. this.create();
  3419. this.setCaption(caption);
  3420. }
  3421. PSVNavBarCaption.prototype = Object.create(PSVComponent.prototype);
  3422. PSVNavBarCaption.prototype.constructor = PSVNavBarCaption;
  3423. PSVNavBarCaption.className = 'psv-caption';
  3424. PSVNavBarCaption.publicMethods = ['setCaption'];
  3425. /**
  3426. * @summary Sets the bar caption
  3427. * @param {string} html
  3428. */
  3429. PSVNavBarCaption.prototype.setCaption = function(html) {
  3430. if (!html) {
  3431. this.container.innerHTML = '';
  3432. } else {
  3433. this.container.innerHTML = html;
  3434. }
  3435. };
  3436. /**
  3437. * Navbar spacer class
  3438. * @param {PSVNavBar} navbar
  3439. * @param {int} [weight=5]
  3440. * @constructor
  3441. * @extends module:components.PSVComponent
  3442. * @memberof module:components
  3443. */
  3444. function PSVNavBarSpacer(navbar, weight) {
  3445. PSVComponent.call(this, navbar);
  3446. /**
  3447. * @member {int}
  3448. * @readonly
  3449. */
  3450. this.weight = weight || 5;
  3451. this.create();
  3452. this.container.classList.add('psv-spacer--weight-' + this.weight);
  3453. }
  3454. PSVNavBarSpacer.prototype = Object.create(PSVComponent.prototype);
  3455. PSVNavBarSpacer.prototype.constructor = PSVNavBarSpacer;
  3456. PSVNavBarSpacer.className = 'psv-spacer';
  3457. /**
  3458. * Panel class
  3459. * @param {PhotoSphereViewer} psv
  3460. * @constructor
  3461. * @extends module:components.PSVComponent
  3462. * @memberof module:components
  3463. */
  3464. function PSVPanel(psv) {
  3465. PSVComponent.call(this, psv);
  3466. /**
  3467. * @summary Content container
  3468. * @member {HTMLElement}
  3469. * @readonly
  3470. * @private
  3471. */
  3472. this.content = null;
  3473. /**
  3474. * @member {Object}
  3475. * @private
  3476. */
  3477. this.prop = {
  3478. mouse_x: 0,
  3479. mouse_y: 0,
  3480. mousedown: false,
  3481. opened: false
  3482. };
  3483. this.create();
  3484. }
  3485. PSVPanel.prototype = Object.create(PSVComponent.prototype);
  3486. PSVPanel.prototype.constructor = PSVPanel;
  3487. PSVPanel.className = 'psv-panel';
  3488. PSVPanel.publicMethods = ['showPanel', 'hidePanel'];
  3489. /**
  3490. * @override
  3491. */
  3492. PSVPanel.prototype.create = function() {
  3493. PSVComponent.prototype.create.call(this);
  3494. this.container.innerHTML =
  3495. '<div class="psv-panel-resizer"></div>' +
  3496. '<div class="psv-panel-close-button"></div>' +
  3497. '<div class="psv-panel-content"></div>';
  3498. this.content = this.container.querySelector('.psv-panel-content');
  3499. var closeBtn = this.container.querySelector('.psv-panel-close-button');
  3500. closeBtn.addEventListener('click', this.hidePanel.bind(this));
  3501. // Stop event bubling from panel
  3502. if (this.psv.config.mousewheel) {
  3503. this.container.addEventListener(PhotoSphereViewer.SYSTEM.mouseWheelEvent, function(e) {
  3504. e.stopPropagation();
  3505. });
  3506. }
  3507. // Event for panel resizing + stop bubling
  3508. var resizer = this.container.querySelector('.psv-panel-resizer');
  3509. resizer.addEventListener('mousedown', this);
  3510. resizer.addEventListener('touchstart', this);
  3511. this.psv.container.addEventListener('mouseup', this);
  3512. this.psv.container.addEventListener('touchend', this);
  3513. this.psv.container.addEventListener('mousemove', this);
  3514. this.psv.container.addEventListener('touchmove', this);
  3515. };
  3516. /**
  3517. * @override
  3518. */
  3519. PSVPanel.prototype.destroy = function() {
  3520. this.psv.container.removeEventListener('mousemove', this);
  3521. this.psv.container.removeEventListener('touchmove', this);
  3522. this.psv.container.removeEventListener('mouseup', this);
  3523. this.psv.container.removeEventListener('touchend', this);
  3524. delete this.prop;
  3525. delete this.content;
  3526. PSVComponent.prototype.destroy.call(this);
  3527. };
  3528. /**
  3529. * @summary Handles events
  3530. * @param {Event} e
  3531. * @private
  3532. */
  3533. PSVPanel.prototype.handleEvent = function(e) {
  3534. switch (e.type) {
  3535. // @formatter:off
  3536. case 'mousedown':
  3537. this._onMouseDown(e);
  3538. break;
  3539. case 'touchstart':
  3540. this._onTouchStart(e);
  3541. break;
  3542. case 'mousemove':
  3543. this._onMouseMove(e);
  3544. break;
  3545. case 'touchmove':
  3546. this._onTouchMove(e);
  3547. break;
  3548. case 'mouseup':
  3549. this._onMouseUp(e);
  3550. break;
  3551. case 'touchend':
  3552. this._onMouseUp(e);
  3553. break;
  3554. // @formatter:on
  3555. }
  3556. };
  3557. /**
  3558. * @summary Shows the panel
  3559. * @param {string} content
  3560. * @param {boolean} [noMargin=false]
  3561. * @fires module:components.PSVPanel.open-panel
  3562. */
  3563. PSVPanel.prototype.showPanel = function(content, noMargin) {
  3564. this.content.innerHTML = content;
  3565. this.content.scrollTop = 0;
  3566. this.container.classList.add('psv-panel--open');
  3567. PSVUtils.toggleClass(this.content, 'psv-panel-content--no-margin', noMargin === true);
  3568. this.prop.opened = true;
  3569. /**
  3570. * @event open-panel
  3571. * @memberof module:components.PSVPanel
  3572. * @summary Triggered when the panel is opened
  3573. */
  3574. this.psv.trigger('open-panel');
  3575. };
  3576. /**
  3577. * @summary Hides the panel
  3578. * @fires module:components.PSVPanel.close-panel
  3579. */
  3580. PSVPanel.prototype.hidePanel = function() {
  3581. this.content.innerHTML = null;
  3582. this.prop.opened = false;
  3583. this.container.classList.remove('psv-panel--open');
  3584. /**
  3585. * @event close-panel
  3586. * @memberof module:components.PSVPanel
  3587. * @summary Trigered when the panel is closed
  3588. */
  3589. this.psv.trigger('close-panel');
  3590. };
  3591. /**
  3592. * @summary Handles mouse down events
  3593. * @param {MouseEvent} evt
  3594. * @private
  3595. */
  3596. PSVPanel.prototype._onMouseDown = function(evt) {
  3597. evt.stopPropagation();
  3598. this._startResize(evt);
  3599. };
  3600. /**
  3601. * @summary Handles touch events
  3602. * @param {TouchEvent} evt
  3603. * @private
  3604. */
  3605. PSVPanel.prototype._onTouchStart = function(evt) {
  3606. evt.stopPropagation();
  3607. this._startResize(evt.changedTouches[0]);
  3608. };
  3609. /**
  3610. * @summary Handles mouse up events
  3611. * @param {MouseEvent} evt
  3612. * @private
  3613. */
  3614. PSVPanel.prototype._onMouseUp = function(evt) {
  3615. if (this.prop.mousedown) {
  3616. evt.stopPropagation();
  3617. this.prop.mousedown = false;
  3618. this.content.classList.remove('psv-panel-content--no-interaction');
  3619. }
  3620. };
  3621. /**
  3622. * @summary Handles mouse move events
  3623. * @param {MouseEvent} evt
  3624. * @private
  3625. */
  3626. PSVPanel.prototype._onMouseMove = function(evt) {
  3627. if (this.prop.mousedown) {
  3628. evt.stopPropagation();
  3629. this._resize(evt);
  3630. }
  3631. };
  3632. /**
  3633. * @summary Handles touch move events
  3634. * @param {TouchEvent} evt
  3635. * @private
  3636. */
  3637. PSVPanel.prototype._onTouchMove = function(evt) {
  3638. if (this.prop.mousedown) {
  3639. this._resize(evt.touches[0]);
  3640. }
  3641. };
  3642. /**
  3643. * @summary Initializes the panel resize
  3644. * @param {MouseEvent|Touch} evt
  3645. * @private
  3646. */
  3647. PSVPanel.prototype._startResize = function(evt) {
  3648. this.prop.mouse_x = parseInt(evt.clientX);
  3649. this.prop.mouse_y = parseInt(evt.clientY);
  3650. this.prop.mousedown = true;
  3651. this.content.classList.add('psv-panel-content--no-interaction');
  3652. };
  3653. /**
  3654. * @summary Resizes the panel
  3655. * @param {MouseEvent|Touch} evt
  3656. * @private
  3657. */
  3658. PSVPanel.prototype._resize = function(evt) {
  3659. var x = parseInt(evt.clientX);
  3660. var y = parseInt(evt.clientY);
  3661. this.container.style.width = (this.container.offsetWidth - (x - this.prop.mouse_x)) + 'px';
  3662. this.prop.mouse_x = x;
  3663. this.prop.mouse_y = y;
  3664. };
  3665. /**
  3666. * Tooltip class
  3667. * @param {module:components.PSVHUD} hud
  3668. * @constructor
  3669. * @extends module:components.PSVComponent
  3670. * @memberof module:components
  3671. */
  3672. function PSVTooltip(hud) {
  3673. PSVComponent.call(this, hud);
  3674. /**
  3675. * @member {Object}
  3676. * @readonly
  3677. * @private
  3678. */
  3679. this.config = this.psv.config.tooltip;
  3680. /**
  3681. * @member {Object}
  3682. * @private
  3683. */
  3684. this.prop = {
  3685. timeout: null
  3686. };
  3687. this.create();
  3688. }
  3689. PSVTooltip.prototype = Object.create(PSVComponent.prototype);
  3690. PSVTooltip.prototype.constructor = PSVTooltip;
  3691. PSVTooltip.className = 'psv-tooltip';
  3692. PSVTooltip.publicMethods = ['showTooltip', 'hideTooltip', 'isTooltipVisible'];
  3693. PSVTooltip.leftMap = { 0: 'left', 0.5: 'center', 1: 'right' };
  3694. PSVTooltip.topMap = { 0: 'top', 0.5: 'center', 1: 'bottom' };
  3695. /**
  3696. * @override
  3697. */
  3698. PSVTooltip.prototype.create = function() {
  3699. PSVComponent.prototype.create.call(this);
  3700. this.container.innerHTML = '<div class="psv-tooltip-arrow"></div><div class="psv-tooltip-content"></div>';
  3701. this.container.style.top = '-1000px';
  3702. this.container.style.left = '-1000px';
  3703. this.content = this.container.querySelector('.psv-tooltip-content');
  3704. this.arrow = this.container.querySelector('.psv-tooltip-arrow');
  3705. this.psv.on('render', this);
  3706. };
  3707. /**
  3708. * @override
  3709. */
  3710. PSVTooltip.prototype.destroy = function() {
  3711. this.psv.off('render', this);
  3712. delete this.config;
  3713. delete this.prop;
  3714. PSVComponent.prototype.destroy.call(this);
  3715. };
  3716. /**
  3717. * @summary Handles events
  3718. * @param {Event} e
  3719. * @private
  3720. */
  3721. PSVTooltip.prototype.handleEvent = function(e) {
  3722. switch (e.type) {
  3723. // @formatter:off
  3724. case 'render':
  3725. this.hideTooltip();
  3726. break;
  3727. // @formatter:on
  3728. }
  3729. };
  3730. /**
  3731. * @summary Checks if the tooltip is visible
  3732. * @returns {boolean}
  3733. */
  3734. PSVTooltip.prototype.isTooltipVisible = function() {
  3735. return this.container.classList.contains('psv-tooltip--visible');
  3736. };
  3737. /**
  3738. * @summary Displays a tooltip on the viewer
  3739. * @param {Object} config
  3740. * @param {string} config.content - HTML content of the tootlip
  3741. * @param {int} config.top - Position of the tip of the arrow of the tooltip, in pixels
  3742. * @param {int} config.left - Position of the tip of the arrow of the tooltip, in pixels
  3743. * @param {string} [config.position='top center'] - Tooltip position toward it's arrow tip.
  3744. * Accepted values are combinations of `top`, `center`, `bottom`
  3745. * and `left`, `center`, `right`
  3746. * @param {string} [config.className] - Additional CSS class added to the tooltip
  3747. * @param {Object} [config.box] - Used when displaying a tooltip on a marker
  3748. * @param {int} [config.box.width=0]
  3749. * @param {int} [config.box.height=0]
  3750. * @fires module:components.PSVTooltip.show-tooltip
  3751. * @throws {PSVError} when the configuration is incorrect
  3752. *
  3753. * @example
  3754. * viewer.showTooltip({ content: 'Hello world', top: 200, left: 450, position: 'center bottom'})
  3755. */
  3756. PSVTooltip.prototype.showTooltip = function(config) {
  3757. if (this.prop.timeout) {
  3758. window.clearTimeout(this.prop.timeout);
  3759. this.prop.timeout = null;
  3760. }
  3761. var isUpdate = this.isTooltipVisible();
  3762. var t = this.container;
  3763. var c = this.content;
  3764. var a = this.arrow;
  3765. if (!config.position) {
  3766. config.position = ['top', 'center'];
  3767. }
  3768. if (!config.box) {
  3769. config.box = {
  3770. width: 0,
  3771. height: 0
  3772. };
  3773. }
  3774. // parse position
  3775. if (typeof config.position === 'string') {
  3776. var tempPos = PSVUtils.parsePosition(config.position);
  3777. if (!(tempPos.left in PSVTooltip.leftMap) || !(tempPos.top in PSVTooltip.topMap)) {
  3778. throw new PSVError('unable to parse tooltip position "' + config.position + '"');
  3779. }
  3780. config.position = [PSVTooltip.topMap[tempPos.top], PSVTooltip.leftMap[tempPos.left]];
  3781. }
  3782. if (config.position[0] === 'center' && config.position[1] === 'center') {
  3783. throw new PSVError('unable to parse tooltip position "center center"');
  3784. }
  3785. if (isUpdate) {
  3786. // Remove every other classes (Firefox does not implements forEach)
  3787. for (var i = t.classList.length - 1; i >= 0; i--) {
  3788. var item = t.classList.item(i);
  3789. if (item !== 'psv-tooltip' && item !== 'psv-tooltip--visible') {
  3790. t.classList.remove(item);
  3791. }
  3792. }
  3793. } else {
  3794. t.className = 'psv-tooltip'; // reset the class
  3795. }
  3796. if (config.className) {
  3797. PSVUtils.addClasses(t, config.className);
  3798. }
  3799. c.innerHTML = config.content;
  3800. t.style.top = '0px';
  3801. t.style.left = '0px';
  3802. // compute size
  3803. var rect = t.getBoundingClientRect();
  3804. var style = {
  3805. posClass: config.position.slice(),
  3806. width: rect.right - rect.left,
  3807. height: rect.bottom - rect.top,
  3808. top: 0,
  3809. left: 0,
  3810. arrow_top: 0,
  3811. arrow_left: 0
  3812. };
  3813. // set initial position
  3814. this._computeTooltipPosition(style, config);
  3815. // correct position if overflow
  3816. var refresh = false;
  3817. if (style.top < this.config.offset) {
  3818. style.posClass[0] = 'bottom';
  3819. refresh = true;
  3820. } else if (style.top + style.height > this.psv.prop.size.height - this.config.offset) {
  3821. style.posClass[0] = 'top';
  3822. refresh = true;
  3823. }
  3824. if (style.left < this.config.offset) {
  3825. style.posClass[1] = 'right';
  3826. refresh = true;
  3827. } else if (style.left + style.width > this.psv.prop.size.width - this.config.offset) {
  3828. style.posClass[1] = 'left';
  3829. refresh = true;
  3830. }
  3831. if (refresh) {
  3832. this._computeTooltipPosition(style, config);
  3833. }
  3834. // apply position
  3835. t.style.top = style.top + 'px';
  3836. t.style.left = style.left + 'px';
  3837. a.style.top = style.arrow_top + 'px';
  3838. a.style.left = style.arrow_left + 'px';
  3839. t.classList.add('psv-tooltip--' + style.posClass.join('-'));
  3840. // delay for correct transition between the two classes
  3841. if (!isUpdate) {
  3842. this.prop.timeout = window.setTimeout(function() {
  3843. t.classList.add('psv-tooltip--visible');
  3844. this.prop.timeout = null;
  3845. /**
  3846. * @event show-tooltip
  3847. * @memberof module:components.PSVTooltip
  3848. * @summary Trigered when the tooltip is shown
  3849. */
  3850. this.psv.trigger('show-tooltip');
  3851. }.bind(this), this.config.delay);
  3852. }
  3853. };
  3854. /**
  3855. * @summary Hides the tooltip
  3856. * @fires module:components.PSVTooltip.hide-tooltip
  3857. */
  3858. PSVTooltip.prototype.hideTooltip = function() {
  3859. if (this.prop.timeout) {
  3860. window.clearTimeout(this.prop.timeout);
  3861. this.prop.timeout = null;
  3862. }
  3863. if (this.isTooltipVisible()) {
  3864. this.container.classList.remove('psv-tooltip--visible');
  3865. this.prop.timeout = window.setTimeout(function() {
  3866. this.content.innerHTML = null;
  3867. this.container.style.top = '-1000px';
  3868. this.container.style.left = '-1000px';
  3869. this.prop.timeout = null;
  3870. }.bind(this), this.config.delay);
  3871. /**
  3872. * @event hide-tooltip
  3873. * @memberof module:components.PSVTooltip
  3874. * @summary Trigered when the tooltip is hidden
  3875. */
  3876. this.psv.trigger('hide-tooltip');
  3877. }
  3878. };
  3879. /**
  3880. * @summary Computes the position of the tooltip and its arrow
  3881. * @param {Object} style
  3882. * @param {Object} config
  3883. * @private
  3884. */
  3885. PSVTooltip.prototype._computeTooltipPosition = function(style, config) {
  3886. var topBottom = false;
  3887. switch (style.posClass[0]) {
  3888. case 'bottom':
  3889. style.top = config.top + config.box.height + this.config.offset + this.config.arrow_size;
  3890. style.arrow_top = -this.config.arrow_size * 2;
  3891. topBottom = true;
  3892. break;
  3893. case 'center':
  3894. style.top = config.top + config.box.height / 2 - style.height / 2;
  3895. style.arrow_top = style.height / 2 - this.config.arrow_size;
  3896. break;
  3897. case 'top':
  3898. style.top = config.top - style.height - this.config.offset - this.config.arrow_size;
  3899. style.arrow_top = style.height;
  3900. topBottom = true;
  3901. break;
  3902. }
  3903. switch (style.posClass[1]) {
  3904. case 'right':
  3905. if (topBottom) {
  3906. style.left = config.left + config.box.width / 2 - this.config.offset - this.config.arrow_size;
  3907. style.arrow_left = this.config.offset;
  3908. } else {
  3909. style.left = config.left + config.box.width + this.config.offset + this.config.arrow_size;
  3910. style.arrow_left = -this.config.arrow_size * 2;
  3911. }
  3912. break;
  3913. case 'center':
  3914. style.left = config.left + config.box.width / 2 - style.width / 2;
  3915. style.arrow_left = style.width / 2 - this.config.arrow_size;
  3916. break;
  3917. case 'left':
  3918. if (topBottom) {
  3919. style.left = config.left - style.width + config.box.width / 2 + this.config.offset + this.config.arrow_size;
  3920. style.arrow_left = style.width - this.config.offset - this.config.arrow_size * 2;
  3921. } else {
  3922. style.left = config.left - style.width - this.config.offset - this.config.arrow_size;
  3923. style.arrow_left = style.width;
  3924. }
  3925. break;
  3926. }
  3927. };
  3928. /**
  3929. * @module components/buttons
  3930. */
  3931. /**
  3932. * Navigation bar button class
  3933. * @param {module:components.PSVNavBar} navbar
  3934. * @constructor
  3935. * @extends module:components.PSVComponent
  3936. * @memberof module:components/buttons
  3937. */
  3938. function PSVNavBarButton(navbar) {
  3939. PSVComponent.call(this, navbar);
  3940. /**
  3941. * @summary Unique identifier of the button
  3942. * @member {string}
  3943. * @readonly
  3944. */
  3945. this.id = undefined;
  3946. if (this.constructor.id) {
  3947. this.id = this.constructor.id;
  3948. }
  3949. /**
  3950. * @summary State of the button
  3951. * @member {boolean}
  3952. * @readonly
  3953. */
  3954. this.enabled = true;
  3955. }
  3956. PSVNavBarButton.prototype = Object.create(PSVComponent.prototype);
  3957. PSVNavBarButton.prototype.constructor = PSVNavBarButton;
  3958. /**
  3959. * @summary Unique identifier of the button
  3960. * @member {string}
  3961. * @readonly
  3962. */
  3963. PSVNavBarButton.id = null;
  3964. /**
  3965. * @summary SVG icon name injected in the button
  3966. * @member {string}
  3967. * @readonly
  3968. */
  3969. PSVNavBarButton.icon = null;
  3970. /**
  3971. * @summary SVG icon name injected in the button when it is active
  3972. * @member {string}
  3973. * @readonly
  3974. */
  3975. PSVNavBarButton.iconActive = null;
  3976. /**
  3977. * @summary Creates the button
  3978. * @protected
  3979. */
  3980. PSVNavBarButton.prototype.create = function() {
  3981. PSVComponent.prototype.create.call(this);
  3982. if (this.constructor.icon) {
  3983. this._setIcon(this.constructor.icon);
  3984. }
  3985. if (this.id && this.psv.config.lang[this.id]) {
  3986. this.container.title = this.psv.config.lang[this.id];
  3987. }
  3988. this.container.addEventListener('click', function(e) {
  3989. if (this.enabled) {
  3990. this._onClick();
  3991. }
  3992. e.stopPropagation();
  3993. }.bind(this));
  3994. };
  3995. /**
  3996. * @summary Destroys the button
  3997. * @protected
  3998. */
  3999. PSVNavBarButton.prototype.destroy = function() {
  4000. PSVComponent.prototype.destroy.call(this);
  4001. };
  4002. /**
  4003. * @summary Changes the active state of the button
  4004. * @param {boolean} [active] - forced state
  4005. */
  4006. PSVNavBarButton.prototype.toggleActive = function(active) {
  4007. PSVUtils.toggleClass(this.container, 'psv-button--active', active);
  4008. if (this.constructor.iconActive) {
  4009. this._setIcon(active ? this.constructor.iconActive : this.constructor.icon);
  4010. }
  4011. };
  4012. /**
  4013. * @summary Disables the button
  4014. */
  4015. PSVNavBarButton.prototype.disable = function() {
  4016. this.container.classList.add('psv-button--disabled');
  4017. this.enabled = false;
  4018. };
  4019. /**
  4020. * @summary Enables the button
  4021. */
  4022. PSVNavBarButton.prototype.enable = function() {
  4023. this.container.classList.remove('psv-button--disabled');
  4024. this.enabled = true;
  4025. };
  4026. /**
  4027. * @summary Set the button icon from {@link PhotoSphereViewer.ICONS}
  4028. * @param {string} icon
  4029. * @param {HTMLElement} [container] - default is the main button container
  4030. * @private
  4031. */
  4032. PSVNavBarButton.prototype._setIcon = function(icon, container) {
  4033. if (!container) {
  4034. container = this.container;
  4035. }
  4036. if (icon) {
  4037. container.innerHTML = PhotoSphereViewer.ICONS[icon];
  4038. // classList not supported on IE11, className is read-only !!!!
  4039. container.querySelector('svg').setAttribute('class', 'psv-button-svg');
  4040. } else {
  4041. container.innerHTML = '';
  4042. }
  4043. };
  4044. /**
  4045. * @summary Action when the button is clicked
  4046. * @private
  4047. * @abstract
  4048. */
  4049. PSVNavBarButton.prototype._onClick = function() {
  4050. };
  4051. /**
  4052. * Navigation bar autorotate button class
  4053. * @param {module:components.PSVNavBar} navbar
  4054. * @constructor
  4055. * @extends module:components/buttons.PSVNavBarButton
  4056. * @memberof module:components/buttons
  4057. */
  4058. function PSVNavBarAutorotateButton(navbar) {
  4059. PSVNavBarButton.call(this, navbar);
  4060. this.create();
  4061. }
  4062. PSVNavBarAutorotateButton.prototype = Object.create(PSVNavBarButton.prototype);
  4063. PSVNavBarAutorotateButton.prototype.constructor = PSVNavBarAutorotateButton;
  4064. PSVNavBarAutorotateButton.id = 'autorotate';
  4065. PSVNavBarAutorotateButton.className = 'psv-button psv-button--hover-scale psv-autorotate-button';
  4066. PSVNavBarAutorotateButton.icon = 'play.svg';
  4067. PSVNavBarAutorotateButton.iconActive = 'play-active.svg';
  4068. /**
  4069. * @override
  4070. */
  4071. PSVNavBarAutorotateButton.prototype.create = function() {
  4072. PSVNavBarButton.prototype.create.call(this);
  4073. this.psv.on('autorotate', this);
  4074. };
  4075. /**
  4076. * @override
  4077. */
  4078. PSVNavBarAutorotateButton.prototype.destroy = function() {
  4079. this.psv.off('autorotate', this);
  4080. PSVNavBarButton.prototype.destroy.call(this);
  4081. };
  4082. /**
  4083. * @summary Handles events
  4084. * @param {Event} e
  4085. * @private
  4086. */
  4087. PSVNavBarAutorotateButton.prototype.handleEvent = function(e) {
  4088. switch (e.type) {
  4089. // @formatter:off
  4090. case 'autorotate':
  4091. this.toggleActive(e.args[0]);
  4092. break;
  4093. // @formatter:on
  4094. }
  4095. };
  4096. /**
  4097. * @override
  4098. * @description Toggles autorotate
  4099. */
  4100. PSVNavBarAutorotateButton.prototype._onClick = function() {
  4101. this.psv.toggleAutorotate();
  4102. };
  4103. /**
  4104. * Navigation bar custom button class
  4105. * @param {module:components.PSVNavBar} navbar
  4106. * @param {Object} config
  4107. * @param {string} [config.id]
  4108. * @param {string} [config.className]
  4109. * @param {string} [config.title]
  4110. * @param {string} [config.content]
  4111. * @param {function} [config.onClick]
  4112. * @param {boolean} [config.enabled=true]
  4113. * @param {boolean} [config.visible=true]
  4114. * @constructor
  4115. * @extends module:components/buttons.PSVNavBarButton
  4116. * @memberof module:components/buttons
  4117. */
  4118. function PSVNavBarCustomButton(navbar, config) {
  4119. PSVNavBarButton.call(this, navbar);
  4120. /**
  4121. * @member {Object}
  4122. * @readonly
  4123. * @private
  4124. */
  4125. this.config = config;
  4126. if (this.config.id) {
  4127. this.id = this.config.id;
  4128. }
  4129. this.create();
  4130. }
  4131. PSVNavBarCustomButton.prototype = Object.create(PSVNavBarButton.prototype);
  4132. PSVNavBarCustomButton.prototype.constructor = PSVNavBarCustomButton;
  4133. PSVNavBarCustomButton.className = 'psv-button psv-custom-button';
  4134. /**
  4135. * @override
  4136. */
  4137. PSVNavBarCustomButton.prototype.create = function() {
  4138. PSVNavBarButton.prototype.create.call(this);
  4139. if (this.config.className) {
  4140. PSVUtils.addClasses(this.container, this.config.className);
  4141. }
  4142. if (this.config.title) {
  4143. this.container.title = this.config.title;
  4144. }
  4145. if (this.config.content) {
  4146. this.container.innerHTML = this.config.content;
  4147. }
  4148. if (this.config.enabled === false || this.config.disabled === true) {
  4149. this.disable();
  4150. }
  4151. if (this.config.visible === false || this.config.hidden === true) {
  4152. this.hide();
  4153. }
  4154. };
  4155. /**
  4156. * @override
  4157. */
  4158. PSVNavBarCustomButton.prototype.destroy = function() {
  4159. delete this.config;
  4160. PSVNavBarButton.prototype.destroy.call(this);
  4161. };
  4162. /**
  4163. * @override
  4164. * @description Calls user method
  4165. */
  4166. PSVNavBarCustomButton.prototype._onClick = function() {
  4167. if (this.config.onClick) {
  4168. this.config.onClick.apply(this.psv);
  4169. }
  4170. };
  4171. /**
  4172. * Navigation bar download button class
  4173. * @param {module:components.PSVNavBar} navbar
  4174. * @constructor
  4175. * @extends module:components/buttons.PSVNavBarButton
  4176. * @memberof module:components/buttons
  4177. */
  4178. function PSVNavBarDownloadButton(navbar) {
  4179. PSVNavBarButton.call(this, navbar);
  4180. this.create();
  4181. }
  4182. PSVNavBarDownloadButton.prototype = Object.create(PSVNavBarButton.prototype);
  4183. PSVNavBarDownloadButton.prototype.constructor = PSVNavBarDownloadButton;
  4184. PSVNavBarDownloadButton.id = 'download';
  4185. PSVNavBarDownloadButton.className = 'psv-button psv-button--hover-scale psv-download-button';
  4186. PSVNavBarDownloadButton.icon = 'download.svg';
  4187. /**
  4188. * @override
  4189. * @description Asks the browser to download the panorama source file
  4190. */
  4191. PSVNavBarDownloadButton.prototype._onClick = function() {
  4192. var link = document.createElement('a');
  4193. link.href = this.psv.config.panorama;
  4194. link.download = this.psv.config.panorama;
  4195. this.psv.container.appendChild(link);
  4196. link.click();
  4197. };
  4198. /**
  4199. * Navigation bar fullscreen button class
  4200. * @param {module:components.PSVNavBar} navbar
  4201. * @constructor
  4202. * @extends module:components/buttons.PSVNavBarButton
  4203. * @memberof module:components/buttons
  4204. */
  4205. function PSVNavBarFullscreenButton(navbar) {
  4206. PSVNavBarButton.call(this, navbar);
  4207. this.create();
  4208. }
  4209. PSVNavBarFullscreenButton.prototype = Object.create(PSVNavBarButton.prototype);
  4210. PSVNavBarFullscreenButton.prototype.constructor = PSVNavBarFullscreenButton;
  4211. PSVNavBarFullscreenButton.id = 'fullscreen';
  4212. PSVNavBarFullscreenButton.className = 'psv-button psv-button--hover-scale psv-fullscreen-button';
  4213. PSVNavBarFullscreenButton.icon = 'fullscreen-in.svg';
  4214. PSVNavBarFullscreenButton.iconActive = 'fullscreen-out.svg';
  4215. /**
  4216. * @override
  4217. */
  4218. PSVNavBarFullscreenButton.prototype.create = function() {
  4219. PSVNavBarButton.prototype.create.call(this);
  4220. if (!PhotoSphereViewer.SYSTEM.fullscreenEvent) {
  4221. this.hide();
  4222. console.warn('PhotoSphereViewer: fullscreen not supported.');
  4223. }
  4224. this.psv.on('fullscreen-updated', this);
  4225. };
  4226. /**
  4227. * @override
  4228. */
  4229. PSVNavBarFullscreenButton.prototype.destroy = function() {
  4230. this.psv.off('fullscreen-updated', this);
  4231. PSVNavBarButton.prototype.destroy.call(this);
  4232. };
  4233. /**
  4234. * Handle events
  4235. * @param {Event} e
  4236. * @private
  4237. */
  4238. PSVNavBarFullscreenButton.prototype.handleEvent = function(e) {
  4239. switch (e.type) {
  4240. // @formatter:off
  4241. case 'fullscreen-updated':
  4242. this.toggleActive(e.args[0]);
  4243. break;
  4244. // @formatter:on
  4245. }
  4246. };
  4247. /**
  4248. * @override
  4249. * @description Toggles fullscreen
  4250. */
  4251. PSVNavBarFullscreenButton.prototype._onClick = function() {
  4252. this.psv.toggleFullscreen();
  4253. };
  4254. /**
  4255. * Navigation bar gyroscope button class
  4256. * @param {module:components.PSVNavBar} navbar
  4257. * @constructor
  4258. * @extends module:components/buttons.PSVNavBarButton
  4259. * @memberof module:components/buttons
  4260. */
  4261. function PSVNavBarGyroscopeButton(navbar) {
  4262. PSVNavBarButton.call(this, navbar);
  4263. this.create();
  4264. }
  4265. PSVNavBarGyroscopeButton.prototype = Object.create(PSVNavBarButton.prototype);
  4266. PSVNavBarGyroscopeButton.prototype.constructor = PSVNavBarGyroscopeButton;
  4267. PSVNavBarGyroscopeButton.id = 'gyroscope';
  4268. PSVNavBarGyroscopeButton.className = 'psv-button psv-button--hover-scale psv-gyroscope-button';
  4269. PSVNavBarGyroscopeButton.icon = 'compass.svg';
  4270. /**
  4271. * @override
  4272. * @description The button gets visible once the gyroscope API is ready
  4273. */
  4274. PSVNavBarGyroscopeButton.prototype.create = function() {
  4275. PSVNavBarButton.prototype.create.call(this);
  4276. PhotoSphereViewer.SYSTEM.deviceOrientationSupported.then(
  4277. this._onAvailabilityChange.bind(this, true),
  4278. this._onAvailabilityChange.bind(this, false)
  4279. );
  4280. this.hide();
  4281. this.psv.on('gyroscope-updated', this);
  4282. };
  4283. /**
  4284. * @override
  4285. */
  4286. PSVNavBarGyroscopeButton.prototype.destroy = function() {
  4287. this.psv.off('gyroscope-updated', this);
  4288. PSVNavBarButton.prototype.destroy.call(this);
  4289. };
  4290. /**
  4291. * @summary Handles events
  4292. * @param {Event} e
  4293. * @private
  4294. */
  4295. PSVNavBarGyroscopeButton.prototype.handleEvent = function(e) {
  4296. switch (e.type) {
  4297. // @formatter:off
  4298. case 'gyroscope-updated':
  4299. this.toggleActive(e.args[0]);
  4300. break;
  4301. // @formatter:on
  4302. }
  4303. };
  4304. /**
  4305. * @override
  4306. * @description Toggles gyroscope control
  4307. */
  4308. PSVNavBarGyroscopeButton.prototype._onClick = function() {
  4309. this.psv.toggleGyroscopeControl();
  4310. };
  4311. /**
  4312. * @summary Updates button display when API is ready
  4313. * @param {boolean} available
  4314. * @private
  4315. * @throws {PSVError} when {@link THREE.DeviceOrientationControls} is not loaded
  4316. */
  4317. PSVNavBarGyroscopeButton.prototype._onAvailabilityChange = function(available) {
  4318. if (available) {
  4319. if (PSVUtils.checkTHREE('DeviceOrientationControls')) {
  4320. this.show();
  4321. } else {
  4322. throw new PSVError('Missing Three.js components: DeviceOrientationControls. Get them from three.js-examples package.');
  4323. }
  4324. }
  4325. };
  4326. /**
  4327. * Navigation bar markers button class
  4328. * @param {module:components.PSVNavBar} navbar
  4329. * @constructor
  4330. * @extends module:components/buttons.PSVNavBarButton
  4331. * @memberof module:components/buttons
  4332. */
  4333. function PSVNavBarMarkersButton(navbar) {
  4334. PSVNavBarButton.call(this, navbar);
  4335. this.create();
  4336. }
  4337. PSVNavBarMarkersButton.prototype = Object.create(PSVNavBarButton.prototype);
  4338. PSVNavBarMarkersButton.prototype.constructor = PSVNavBarMarkersButton;
  4339. PSVNavBarMarkersButton.id = 'markers';
  4340. PSVNavBarMarkersButton.className = 'psv-button psv-button--hover-scale psv-markers-button';
  4341. PSVNavBarMarkersButton.icon = 'pin.svg';
  4342. /**
  4343. * @override
  4344. * @description Toggles markers list
  4345. */
  4346. PSVNavBarMarkersButton.prototype._onClick = function() {
  4347. this.psv.hud.toggleMarkersList();
  4348. };
  4349. /**
  4350. * Navigation bar zoom button class
  4351. * @param {module:components.PSVNavBar} navbar
  4352. * @constructor
  4353. * @extends module:components/buttons.PSVNavBarButton
  4354. * @memberof module:components/buttons
  4355. */
  4356. function PSVNavBarZoomButton(navbar) {
  4357. PSVNavBarButton.call(this, navbar);
  4358. /**
  4359. * @member {HTMLElement}
  4360. * @readonly
  4361. * @private
  4362. */
  4363. this.zoom_range = null;
  4364. /**
  4365. * @member {HTMLElement}
  4366. * @readonly
  4367. * @private
  4368. */
  4369. this.zoom_value = null;
  4370. /**
  4371. * @member {Object}
  4372. * @private
  4373. */
  4374. this.prop = {
  4375. mousedown: false,
  4376. buttondown: false,
  4377. longPressInterval: null,
  4378. longPressTimeout: null
  4379. };
  4380. this.create();
  4381. }
  4382. PSVNavBarZoomButton.prototype = Object.create(PSVNavBarButton.prototype);
  4383. PSVNavBarZoomButton.prototype.constructor = PSVNavBarZoomButton;
  4384. PSVNavBarZoomButton.id = 'zoom';
  4385. PSVNavBarZoomButton.className = 'psv-button psv-zoom-button';
  4386. /**
  4387. * @override
  4388. */
  4389. PSVNavBarZoomButton.prototype.create = function() {
  4390. PSVNavBarButton.prototype.create.call(this);
  4391. var zoom_minus = document.createElement('div');
  4392. zoom_minus.className = 'psv-zoom-button-minus';
  4393. zoom_minus.title = this.psv.config.lang.zoomOut;
  4394. this._setIcon('zoom-out.svg', zoom_minus);
  4395. this.container.appendChild(zoom_minus);
  4396. var zoom_range_bg = document.createElement('div');
  4397. zoom_range_bg.className = 'psv-zoom-button-range';
  4398. this.container.appendChild(zoom_range_bg);
  4399. this.zoom_range = document.createElement('div');
  4400. this.zoom_range.className = 'psv-zoom-button-line';
  4401. zoom_range_bg.appendChild(this.zoom_range);
  4402. this.zoom_value = document.createElement('div');
  4403. this.zoom_value.className = 'psv-zoom-button-handle';
  4404. this.zoom_range.appendChild(this.zoom_value);
  4405. var zoom_plus = document.createElement('div');
  4406. zoom_plus.className = 'psv-zoom-button-plus';
  4407. zoom_plus.title = this.psv.config.lang.zoomIn;
  4408. this._setIcon('zoom-in.svg', zoom_plus);
  4409. this.container.appendChild(zoom_plus);
  4410. this.zoom_range.addEventListener('mousedown', this);
  4411. this.zoom_range.addEventListener('touchstart', this);
  4412. this.psv.container.addEventListener('mousemove', this);
  4413. this.psv.container.addEventListener('touchmove', this);
  4414. this.psv.container.addEventListener('mouseup', this);
  4415. this.psv.container.addEventListener('touchend', this);
  4416. zoom_minus.addEventListener('mousedown', this._zoomOut.bind(this));
  4417. zoom_plus.addEventListener('mousedown', this._zoomIn.bind(this));
  4418. this.psv.on('zoom-updated', this);
  4419. this.psv.once('ready', function() {
  4420. this._moveZoomValue(this.psv.prop.zoom_lvl);
  4421. }.bind(this));
  4422. };
  4423. /**
  4424. * @override
  4425. */
  4426. PSVNavBarZoomButton.prototype.destroy = function() {
  4427. this._stopZoomChange();
  4428. this.psv.container.removeEventListener('mousemove', this);
  4429. this.psv.container.removeEventListener('touchmove', this);
  4430. this.psv.container.removeEventListener('mouseup', this);
  4431. this.psv.container.removeEventListener('touchend', this);
  4432. delete this.zoom_range;
  4433. delete this.zoom_value;
  4434. this.psv.off('zoom-updated', this);
  4435. PSVNavBarButton.prototype.destroy.call(this);
  4436. };
  4437. /**
  4438. * @summary Handles events
  4439. * @param {Event} e
  4440. * @private
  4441. */
  4442. PSVNavBarZoomButton.prototype.handleEvent = function(e) {
  4443. switch (e.type) {
  4444. // @formatter:off
  4445. case 'mousedown':
  4446. this._initZoomChangeWithMouse(e);
  4447. break;
  4448. case 'touchstart':
  4449. this._initZoomChangeByTouch(e);
  4450. break;
  4451. case 'mousemove':
  4452. this._changeZoomWithMouse(e);
  4453. break;
  4454. case 'touchmove':
  4455. this._changeZoomByTouch(e);
  4456. break;
  4457. case 'mouseup':
  4458. this._stopZoomChange(e);
  4459. break;
  4460. case 'touchend':
  4461. this._stopZoomChange(e);
  4462. break;
  4463. case 'zoom-updated':
  4464. this._moveZoomValue(e.args[0]);
  4465. break;
  4466. // @formatter:on
  4467. }
  4468. };
  4469. /**
  4470. * @summary Moves the zoom cursor
  4471. * @param {int} level
  4472. * @private
  4473. */
  4474. PSVNavBarZoomButton.prototype._moveZoomValue = function(level) {
  4475. this.zoom_value.style.left = (level / 100 * this.zoom_range.offsetWidth - this.zoom_value.offsetWidth / 2) + 'px';
  4476. };
  4477. /**
  4478. * @summary Handles mouse down events
  4479. * @param {MouseEvent} evt
  4480. * @private
  4481. */
  4482. PSVNavBarZoomButton.prototype._initZoomChangeWithMouse = function(evt) {
  4483. if (!this.enabled) {
  4484. return;
  4485. }
  4486. this.prop.mousedown = true;
  4487. this._changeZoom(evt.clientX);
  4488. };
  4489. /**
  4490. * @summary Handles touch events
  4491. * @param {TouchEvent} evt
  4492. * @private
  4493. */
  4494. PSVNavBarZoomButton.prototype._initZoomChangeByTouch = function(evt) {
  4495. if (!this.enabled) {
  4496. return;
  4497. }
  4498. this.prop.mousedown = true;
  4499. this._changeZoom(evt.changedTouches[0].clientX);
  4500. };
  4501. /**
  4502. * @summary Handles click events
  4503. * @description Zooms in and register long press timer
  4504. * @private
  4505. */
  4506. PSVNavBarZoomButton.prototype._zoomIn = function() {
  4507. if (!this.enabled) {
  4508. return;
  4509. }
  4510. this.prop.buttondown = true;
  4511. this.psv.zoomIn();
  4512. this.prop.longPressTimeout = window.setTimeout(this._startLongPressInterval.bind(this, 1), 200);
  4513. };
  4514. /**
  4515. * @summary Handles click events
  4516. * @description Zooms out and register long press timer
  4517. * @private
  4518. */
  4519. PSVNavBarZoomButton.prototype._zoomOut = function() {
  4520. if (!this.enabled) {
  4521. return;
  4522. }
  4523. this.prop.buttondown = true;
  4524. this.psv.zoomOut();
  4525. this.prop.longPressTimeout = window.setTimeout(this._startLongPressInterval.bind(this, -1), 200);
  4526. };
  4527. /**
  4528. * @summary Continues zooming as long as the user presses the button
  4529. * @param value
  4530. * @private
  4531. */
  4532. PSVNavBarZoomButton.prototype._startLongPressInterval = function(value) {
  4533. if (this.prop.buttondown) {
  4534. this.prop.longPressInterval = window.setInterval(function() {
  4535. this.psv.zoom(this.psv.prop.zoom_lvl + value);
  4536. }.bind(this), 50);
  4537. }
  4538. };
  4539. /**
  4540. * @summary Handles mouse up events
  4541. * @private
  4542. */
  4543. PSVNavBarZoomButton.prototype._stopZoomChange = function() {
  4544. if (!this.enabled) {
  4545. return;
  4546. }
  4547. window.clearInterval(this.prop.longPressInterval);
  4548. window.clearTimeout(this.prop.longPressTimeout);
  4549. this.prop.longPressInterval = null;
  4550. this.prop.mousedown = false;
  4551. this.prop.buttondown = false;
  4552. };
  4553. /**
  4554. * @summary Handles mouse move events
  4555. * @param {MouseEvent} evt
  4556. * @private
  4557. */
  4558. PSVNavBarZoomButton.prototype._changeZoomWithMouse = function(evt) {
  4559. if (!this.enabled) {
  4560. return;
  4561. }
  4562. evt.preventDefault();
  4563. this._changeZoom(evt.clientX);
  4564. };
  4565. /**
  4566. * @summary Handles touch move events
  4567. * @param {TouchEvent} evt
  4568. * @private
  4569. */
  4570. PSVNavBarZoomButton.prototype._changeZoomByTouch = function(evt) {
  4571. if (!this.enabled) {
  4572. return;
  4573. }
  4574. this._changeZoom(evt.changedTouches[0].clientX);
  4575. };
  4576. /**
  4577. * @summary Zoom change
  4578. * @param {int} x - mouse/touch position
  4579. * @private
  4580. */
  4581. PSVNavBarZoomButton.prototype._changeZoom = function(x) {
  4582. if (this.prop.mousedown) {
  4583. var user_input = parseInt(x) - this.zoom_range.getBoundingClientRect().left;
  4584. var zoom_level = user_input / this.zoom_range.offsetWidth * 100;
  4585. this.psv.zoom(zoom_level);
  4586. }
  4587. };
  4588. /**
  4589. * Custom error used in the lib
  4590. * @param {string} message
  4591. * @constructor
  4592. */
  4593. function PSVError(message) {
  4594. this.message = message;
  4595. // Use V8's native method if available, otherwise fallback
  4596. if ('captureStackTrace' in Error) {
  4597. Error.captureStackTrace(this, PSVError);
  4598. } else {
  4599. this.stack = (new Error()).stack;
  4600. }
  4601. }
  4602. PSVError.prototype = Object.create(Error.prototype);
  4603. PSVError.prototype.name = 'PSVError';
  4604. PSVError.prototype.constructor = PSVError;
  4605. /**
  4606. * @summary exposes {@link PSVError}
  4607. * @memberof PhotoSphereViewer
  4608. * @readonly
  4609. */
  4610. PhotoSphereViewer.Error = PSVError;
  4611. /**
  4612. * Object representing a marker
  4613. * @param {Object} properties - see {@link http://photo-sphere-viewer.js.org/markers.html#config} (merged with the object itself)
  4614. * @param {PhotoSphereViewer} psv
  4615. * @constructor
  4616. * @throws {PSVError} when the configuration is incorrect
  4617. */
  4618. function PSVMarker(properties, psv) {
  4619. if (!properties.id) {
  4620. throw new PSVError('missing marker id');
  4621. }
  4622. if (properties.image && (!properties.width || !properties.height)) {
  4623. throw new PSVError('missing marker width/height');
  4624. }
  4625. if (properties.image || properties.html) {
  4626. if ((!properties.hasOwnProperty('x') || !properties.hasOwnProperty('y')) && (!properties.hasOwnProperty('latitude') || !properties.hasOwnProperty('longitude'))) {
  4627. throw new PSVError('missing marker position, latitude/longitude or x/y');
  4628. }
  4629. }
  4630. /**
  4631. * @member {PhotoSphereViewer}
  4632. * @readonly
  4633. * @protected
  4634. */
  4635. this.psv = psv;
  4636. /**
  4637. * @member {boolean}
  4638. */
  4639. this.visible = true;
  4640. /**
  4641. * @member {boolean}
  4642. * @readonly
  4643. * @private
  4644. */
  4645. this._dynamicSize = false;
  4646. // private properties
  4647. var _id = properties.id;
  4648. var _type = PSVMarker.getType(properties, false);
  4649. var $el;
  4650. // readonly properties
  4651. Object.defineProperties(this, {
  4652. /**
  4653. * @memberof PSVMarker
  4654. * @type {string}
  4655. * @readonly
  4656. */
  4657. id: {
  4658. configurable: false,
  4659. enumerable: true,
  4660. get: function() {
  4661. return _id;
  4662. },
  4663. set: function() {}
  4664. },
  4665. /**
  4666. * @memberof PSVMarker
  4667. * @type {string}
  4668. * @see PSVMarker.types
  4669. * @readonly
  4670. */
  4671. type: {
  4672. configurable: false,
  4673. enumerable: true,
  4674. get: function() {
  4675. return _type;
  4676. },
  4677. set: function() {}
  4678. },
  4679. /**
  4680. * @memberof PSVMarker
  4681. * @type {HTMLDivElement|SVGElement}
  4682. * @readonly
  4683. */
  4684. $el: {
  4685. configurable: false,
  4686. enumerable: true,
  4687. get: function() {
  4688. return $el;
  4689. },
  4690. set: function() {}
  4691. },
  4692. /**
  4693. * @summary Quick access to self value of key `type`
  4694. * @memberof PSVMarker
  4695. * @type {*}
  4696. * @private
  4697. */
  4698. _def: {
  4699. configurable: false,
  4700. enumerable: true,
  4701. get: function() {
  4702. return this[_type];
  4703. },
  4704. set: function(value) {
  4705. this[_type] = value;
  4706. }
  4707. }
  4708. });
  4709. // create element
  4710. if (this.isNormal()) {
  4711. $el = document.createElement('div');
  4712. } else if (this.isPolygon()) {
  4713. $el = document.createElementNS(PSVUtils.svgNS, 'polygon');
  4714. } else if (this.isPolyline()) {
  4715. $el = document.createElementNS(PSVUtils.svgNS, 'polyline');
  4716. } else {
  4717. $el = document.createElementNS(PSVUtils.svgNS, this.type);
  4718. }
  4719. $el.id = 'psv-marker-' + this.id;
  4720. $el.psvMarker = this;
  4721. this.update(properties);
  4722. }
  4723. /**
  4724. * @summary Types of markers
  4725. * @type {string[]}
  4726. * @readonly
  4727. */
  4728. PSVMarker.types = ['image', 'html', 'polygon_px', 'polygon_rad', 'polyline_px', 'polyline_rad', 'rect', 'circle', 'ellipse', 'path'];
  4729. /**
  4730. * @summary Determines the type of a marker by the available properties
  4731. * @param {object} properties
  4732. * @param {boolean} [allowNone=false]
  4733. * @returns {string}
  4734. * @throws {PSVError} when the marker's type cannot be found
  4735. */
  4736. PSVMarker.getType = function(properties, allowNone) {
  4737. var found = [];
  4738. PSVMarker.types.forEach(function(type) {
  4739. if (properties[type]) {
  4740. found.push(type);
  4741. }
  4742. });
  4743. if (found.length === 0 && !allowNone) {
  4744. throw new PSVError('missing marker content, either ' + PSVMarker.types.join(', '));
  4745. } else if (found.length > 1) {
  4746. throw new PSVError('multiple marker content, either ' + PSVMarker.types.join(', '));
  4747. }
  4748. return found[0];
  4749. };
  4750. /**
  4751. * @summary Destroys the marker
  4752. */
  4753. PSVMarker.prototype.destroy = function() {
  4754. delete this.$el.psvMarker;
  4755. };
  4756. /**
  4757. * @summary Checks if it is a normal marker (image or html)
  4758. * @returns {boolean}
  4759. */
  4760. PSVMarker.prototype.isNormal = function() {
  4761. return this.type === 'image' || this.type === 'html';
  4762. };
  4763. /**
  4764. * @summary Checks if it is a polygon/polyline marker
  4765. * @returns {boolean}
  4766. */
  4767. PSVMarker.prototype.isPoly = function() {
  4768. return this.isPolygon() || this.isPolyline();
  4769. };
  4770. /**
  4771. * @summary Checks if it is a polygon marker
  4772. * @returns {boolean}
  4773. */
  4774. PSVMarker.prototype.isPolygon = function() {
  4775. return this.type === 'polygon_px' || this.type === 'polygon_rad';
  4776. };
  4777. /**
  4778. * @summary Checks if it is a polyline marker
  4779. * @returns {boolean}
  4780. */
  4781. PSVMarker.prototype.isPolyline = function() {
  4782. return this.type === 'polyline_px' || this.type === 'polyline_rad';
  4783. };
  4784. /**
  4785. * @summary Checks if it is an SVG marker
  4786. * @returns {boolean}
  4787. */
  4788. PSVMarker.prototype.isSvg = function() {
  4789. return this.type === 'rect' || this.type === 'circle' || this.type === 'ellipse' || this.type === 'path';
  4790. };
  4791. /**
  4792. * @summary Computes marker scale from zoom level
  4793. * @param {float} zoomLevel
  4794. * @returns {float}
  4795. */
  4796. PSVMarker.prototype.getScale = function(zoomLevel) {
  4797. if (Array.isArray(this.scale)) {
  4798. return this.scale[0] + (this.scale[1] - this.scale[0]) * PSVUtils.animation.easings.inQuad(zoomLevel / 100);
  4799. } else if (typeof this.scale === 'function') {
  4800. return this.scale(zoomLevel);
  4801. } else if (typeof this.scale === 'number') {
  4802. return this.scale * PSVUtils.animation.easings.inQuad(zoomLevel / 100);
  4803. } else {
  4804. return 1;
  4805. }
  4806. };
  4807. /**
  4808. * @summary Updates the marker with new properties
  4809. * @param {object} [properties]
  4810. * @throws {PSVError} when trying to change the marker's type
  4811. */
  4812. PSVMarker.prototype.update = function(properties) {
  4813. // merge objects
  4814. if (properties && properties !== this) {
  4815. var newType = PSVMarker.getType(properties, true);
  4816. if (newType !== undefined && newType !== this.type) {
  4817. throw new PSVError('cannot change marker type');
  4818. }
  4819. PSVUtils.deepmerge(this, properties);
  4820. }
  4821. // reset CSS class
  4822. if (this.isNormal()) {
  4823. this.$el.setAttribute('class', 'psv-marker psv-marker--normal');
  4824. } else {
  4825. this.$el.setAttribute('class', 'psv-marker psv-marker--svg');
  4826. }
  4827. // add CSS classes
  4828. if (this.className) {
  4829. PSVUtils.addClasses(this.$el, this.className);
  4830. }
  4831. if (this.tooltip) {
  4832. PSVUtils.addClasses(this.$el, 'has-tooltip');
  4833. if (typeof this.tooltip === 'string') {
  4834. this.tooltip = { content: this.tooltip };
  4835. }
  4836. }
  4837. // apply style
  4838. if (this.style) {
  4839. PSVUtils.deepmerge(this.$el.style, this.style);
  4840. }
  4841. // parse anchor
  4842. this.anchor = PSVUtils.parsePosition(this.anchor);
  4843. if (this.isNormal()) {
  4844. this._updateNormal();
  4845. } else if (this.isPolygon()) {
  4846. this._updatePoly('polygon_rad', 'polygon_px');
  4847. } else if (this.isPolyline()) {
  4848. this._updatePoly('polyline_rad', 'polyline_px');
  4849. } else {
  4850. this._updateSvg();
  4851. }
  4852. };
  4853. /**
  4854. * @summary Updates a normal marker
  4855. * @private
  4856. */
  4857. PSVMarker.prototype._updateNormal = function() {
  4858. if (this.width && this.height) {
  4859. this.$el.style.width = this.width + 'px';
  4860. this.$el.style.height = this.height + 'px';
  4861. this._dynamicSize = false;
  4862. } else {
  4863. this._dynamicSize = true;
  4864. }
  4865. if (this.image) {
  4866. this.$el.style.backgroundImage = 'url(' + this.image + ')';
  4867. } else {
  4868. this.$el.innerHTML = this.html;
  4869. }
  4870. // set anchor
  4871. this.$el.style.transformOrigin = this.anchor.left * 100 + '% ' + this.anchor.top * 100 + '%';
  4872. // convert texture coordinates to spherical coordinates
  4873. this.psv.cleanPosition(this);
  4874. // compute x/y/z position
  4875. this.position3D = this.psv.sphericalCoordsToVector3(this);
  4876. };
  4877. /**
  4878. * @summary Updates an SVG marker
  4879. * @private
  4880. */
  4881. PSVMarker.prototype._updateSvg = function() {
  4882. this._dynamicSize = true;
  4883. // set content
  4884. switch (this.type) {
  4885. case 'rect':
  4886. if (typeof this._def === 'number') {
  4887. this._def = {
  4888. x: 0,
  4889. y: 0,
  4890. width: this._def,
  4891. height: this._def
  4892. };
  4893. } else if (Array.isArray(this._def)) {
  4894. this._def = {
  4895. x: 0,
  4896. y: 0,
  4897. width: this._def[0],
  4898. height: this._def[1]
  4899. };
  4900. } else {
  4901. this._def.x = this._def.y = 0;
  4902. }
  4903. break;
  4904. case 'circle':
  4905. if (typeof this._def === 'number') {
  4906. this._def = {
  4907. cx: this._def,
  4908. cy: this._def,
  4909. r: this._def
  4910. };
  4911. } else if (Array.isArray(this._def)) {
  4912. this._def = {
  4913. cx: this._def[0],
  4914. cy: this._def[0],
  4915. r: this._def[0]
  4916. };
  4917. } else {
  4918. this._def.cx = this._def.cy = this._def.r;
  4919. }
  4920. break;
  4921. case 'ellipse':
  4922. if (typeof this._def === 'number') {
  4923. this._def = {
  4924. cx: this._def,
  4925. cy: this._def,
  4926. rx: this._def,
  4927. ry: this._def
  4928. };
  4929. } else if (Array.isArray(this._def)) {
  4930. this._def = {
  4931. cx: this._def[0],
  4932. cy: this._def[1],
  4933. rx: this._def[0],
  4934. ry: this._def[1]
  4935. };
  4936. } else {
  4937. this._def.cx = this._def.rx;
  4938. this._def.cy = this._def.ry;
  4939. }
  4940. break;
  4941. case 'path':
  4942. if (typeof this._def === 'string') {
  4943. this._def = {
  4944. d: this._def
  4945. };
  4946. }
  4947. break;
  4948. }
  4949. Object.getOwnPropertyNames(this._def).forEach(function(prop) {
  4950. this.$el.setAttributeNS(null, prop, this._def[prop]);
  4951. }, this);
  4952. // set style
  4953. if (this.svgStyle) {
  4954. Object.getOwnPropertyNames(this.svgStyle).forEach(function(prop) {
  4955. this.$el.setAttributeNS(null, PSVUtils.dasherize(prop), this.svgStyle[prop]);
  4956. }, this);
  4957. } else {
  4958. this.$el.setAttributeNS(null, 'fill', 'rgba(0,0,0,0.5)');
  4959. }
  4960. // convert texture coordinates to spherical coordinates
  4961. this.psv.cleanPosition(this);
  4962. // compute x/y/z position
  4963. this.position3D = this.psv.sphericalCoordsToVector3(this);
  4964. };
  4965. /**
  4966. * @summary Updates a polygon marker
  4967. * @param {'polygon_rad'|'polyline_rad'} key_rad
  4968. * @param {'polygon_px'|'polyline_px'} key_px
  4969. * @private
  4970. */
  4971. PSVMarker.prototype._updatePoly = function(key_rad, key_px) {
  4972. this._dynamicSize = true;
  4973. // set style
  4974. if (this.svgStyle) {
  4975. Object.getOwnPropertyNames(this.svgStyle).forEach(function(prop) {
  4976. this.$el.setAttributeNS(null, PSVUtils.dasherize(prop), this.svgStyle[prop]);
  4977. }, this);
  4978. if (this.isPolyline() && !this.svgStyle.fill) {
  4979. this.$el.setAttributeNS(null, 'fill', 'none');
  4980. }
  4981. } else if (this.isPolygon()) {
  4982. this.$el.setAttributeNS(null, 'fill', 'rgba(0,0,0,0.5)');
  4983. } else if (this.isPolyline()) {
  4984. this.$el.setAttributeNS(null, 'fill', 'none');
  4985. this.$el.setAttributeNS(null, 'stroke', 'rgb(0,0,0)');
  4986. }
  4987. // fold arrays: [1,2,3,4] => [[1,2],[3,4]]
  4988. [this[key_rad], this[key_px]].forEach(function(polygon) {
  4989. if (polygon && typeof polygon[0] !== 'object') {
  4990. for (var i = 0; i < polygon.length; i++) {
  4991. polygon.splice(i, 2, [polygon[i], polygon[i + 1]]);
  4992. }
  4993. }
  4994. });
  4995. // convert texture coordinates to spherical coordinates
  4996. if (this[key_px]) {
  4997. this[key_rad] = this[key_px].map(function(coord) {
  4998. var sphericalCoords = this.psv.textureCoordsToSphericalCoords({ x: coord[0], y: coord[1] });
  4999. return [sphericalCoords.longitude, sphericalCoords.latitude];
  5000. }, this);
  5001. }
  5002. // clean angles
  5003. else {
  5004. this[key_rad] = this[key_rad].map(function(coord) {
  5005. return [
  5006. PSVUtils.parseAngle(coord[0]),
  5007. PSVUtils.parseAngle(coord[1], true)
  5008. ];
  5009. });
  5010. }
  5011. // TODO : compute the center of the polygon
  5012. this.longitude = this[key_rad][0][0];
  5013. this.latitude = this[key_rad][0][1];
  5014. // compute x/y/z positions
  5015. this.positions3D = this[key_rad].map(function(coord) {
  5016. return this.psv.sphericalCoordsToVector3({ longitude: coord[0], latitude: coord[1] });
  5017. }, this);
  5018. };
  5019. /**
  5020. * Static utilities for PSV
  5021. * @namespace
  5022. */
  5023. var PSVUtils = {};
  5024. /**
  5025. * @summary exposes {@link PSVUtils}
  5026. * @member {object}
  5027. * @memberof PhotoSphereViewer
  5028. * @readonly
  5029. */
  5030. PhotoSphereViewer.Utils = PSVUtils;
  5031. /**
  5032. * @summary Short-Hand for PI*2
  5033. * @type {float}
  5034. * @readonly
  5035. */
  5036. PSVUtils.TwoPI = Math.PI * 2.0;
  5037. /**
  5038. * @summary Short-Hand for PI/2
  5039. * @type {float}
  5040. * @readonly
  5041. */
  5042. PSVUtils.HalfPI = Math.PI / 2.0;
  5043. /**
  5044. * @summary Namespace for SVG creation
  5045. * @type {string}
  5046. * @readonly
  5047. */
  5048. PSVUtils.svgNS = 'http://www.w3.org/2000/svg';
  5049. /**
  5050. * @summary Checks if some three.js components are loaded
  5051. * @param {...string} components
  5052. * @returns {boolean}
  5053. */
  5054. PSVUtils.checkTHREE = function(components) {
  5055. for (var i = 0, l = arguments.length; i < l; i++) {
  5056. if (!(arguments[i] in THREE)) {
  5057. return false;
  5058. }
  5059. }
  5060. return true;
  5061. };
  5062. /**
  5063. * @summary Detects if canvas is supported
  5064. * @returns {boolean}
  5065. */
  5066. PSVUtils.isCanvasSupported = function() {
  5067. var canvas = document.createElement('canvas');
  5068. return !!(canvas.getContext && canvas.getContext('2d'));
  5069. };
  5070. /**
  5071. * @summary Tries to return a canvas webgl context
  5072. * @returns {WebGLRenderingContext}
  5073. */
  5074. PSVUtils.getWebGLCtx = function() {
  5075. var canvas = document.createElement('canvas');
  5076. var names = ['webgl', 'experimental-webgl', 'moz-webgl', 'webkit-3d'];
  5077. var context = null;
  5078. if (!canvas.getContext) {
  5079. return null;
  5080. }
  5081. if (names.some(function(name) {
  5082. try {
  5083. context = canvas.getContext(name);
  5084. return (context && typeof context.getParameter === 'function');
  5085. } catch (e) {
  5086. return false;
  5087. }
  5088. })) {
  5089. return context;
  5090. } else {
  5091. return null;
  5092. }
  5093. };
  5094. /**
  5095. * @summary Detects if WebGL is supported
  5096. * @returns {boolean}
  5097. */
  5098. PSVUtils.isWebGLSupported = function() {
  5099. return !!window.WebGLRenderingContext && PSVUtils.getWebGLCtx() !== null;
  5100. };
  5101. /**
  5102. * @summary Detects if device orientation is supported
  5103. * @description We can only be sure device orientation is supported once received an event with coherent data
  5104. * @returns {Promise}
  5105. */
  5106. PSVUtils.isDeviceOrientationSupported = function() {
  5107. var defer = D();
  5108. if ('DeviceOrientationEvent' in window) {
  5109. var listener = function(event) {
  5110. if (event && event.alpha !== null && !isNaN(event.alpha)) {
  5111. defer.resolve();
  5112. } else {
  5113. defer.reject();
  5114. }
  5115. window.removeEventListener('deviceorientation', listener);
  5116. };
  5117. window.addEventListener('deviceorientation', listener, false);
  5118. setTimeout(function() {
  5119. if (defer.promise.isPending()) {
  5120. listener(null);
  5121. }
  5122. }, 2000);
  5123. } else {
  5124. defer.reject();
  5125. }
  5126. return defer.promise;
  5127. };
  5128. /**
  5129. * @summary Gets max texture width in WebGL context
  5130. * @returns {int}
  5131. */
  5132. PSVUtils.getMaxTextureWidth = function() {
  5133. var ctx = PSVUtils.getWebGLCtx();
  5134. if (ctx !== null) {
  5135. return ctx.getParameter(ctx.MAX_TEXTURE_SIZE);
  5136. } else {
  5137. return 0;
  5138. }
  5139. };
  5140. /**
  5141. * @summary Toggles a CSS class
  5142. * @param {HTMLElement|SVGElement} element
  5143. * @param {string} className
  5144. * @param {boolean} [active] - forced state
  5145. */
  5146. PSVUtils.toggleClass = function(element, className, active) {
  5147. // manual implementation for IE11 and SVGElement
  5148. if (!element.classList) {
  5149. var currentClassName = element.getAttribute('class') || '';
  5150. var currentActive = currentClassName.indexOf(className) !== -1;
  5151. var regex = new RegExp('(?:^|\\s)' + className + '(?:\\s|$)');
  5152. if ((active === undefined || active) && !currentActive) {
  5153. currentClassName += currentClassName.length > 0 ? ' ' + className : className;
  5154. } else if (!active) {
  5155. currentClassName = currentClassName.replace(regex, ' ');
  5156. }
  5157. element.setAttribute('class', currentClassName);
  5158. } else {
  5159. if (active === undefined) {
  5160. element.classList.toggle(className);
  5161. } else if (active && !element.classList.contains(className)) {
  5162. element.classList.add(className);
  5163. } else if (!active) {
  5164. element.classList.remove(className);
  5165. }
  5166. }
  5167. };
  5168. /**
  5169. * @summary Adds one or several CSS classes to an element
  5170. * @param {HTMLElement} element
  5171. * @param {string} className
  5172. */
  5173. PSVUtils.addClasses = function(element, className) {
  5174. if (!className) {
  5175. return;
  5176. }
  5177. className.split(' ').forEach(function(name) {
  5178. PSVUtils.toggleClass(element, name, true);
  5179. });
  5180. };
  5181. /**
  5182. * @summary Removes one or several CSS classes to an element
  5183. * @param {HTMLElement} element
  5184. * @param {string} className
  5185. */
  5186. PSVUtils.removeClasses = function(element, className) {
  5187. if (!className) {
  5188. return;
  5189. }
  5190. className.split(' ').forEach(function(name) {
  5191. PSVUtils.toggleClass(element, name, false);
  5192. });
  5193. };
  5194. /**
  5195. * @summary Searches if an element has a particular parent at any level including itself
  5196. * @param {HTMLElement} el
  5197. * @param {HTMLElement} parent
  5198. * @returns {boolean}
  5199. */
  5200. PSVUtils.hasParent = function(el, parent) {
  5201. do {
  5202. if (el === parent) {
  5203. return true;
  5204. }
  5205. } while (!!(el = el.parentNode));
  5206. return false;
  5207. };
  5208. /**
  5209. * @summary Gets the closest parent (can by itself)
  5210. * @param {HTMLElement} el (HTMLElement)
  5211. * @param {string} selector
  5212. * @returns {HTMLElement}
  5213. */
  5214. PSVUtils.getClosest = function(el, selector) {
  5215. var matches = el.matches || el.msMatchesSelector;
  5216. do {
  5217. if (matches.bind(el)(selector)) {
  5218. return el;
  5219. }
  5220. } while (!!(el = el.parentElement));
  5221. return null;
  5222. };
  5223. /**
  5224. * @summary Gets the event name for mouse wheel
  5225. * @returns {string}
  5226. */
  5227. PSVUtils.mouseWheelEvent = function() {
  5228. return 'onwheel' in document.createElement('div') ? 'wheel' : // Modern browsers support "wheel"
  5229. document.onmousewheel !== undefined ? 'mousewheel' : // Webkit and IE support at least "mousewheel"
  5230. 'DOMMouseScroll'; // let's assume that remaining browsers are older Firefox
  5231. };
  5232. /**
  5233. * @summary Gets the event name for fullscreen
  5234. * @returns {string}
  5235. */
  5236. PSVUtils.fullscreenEvent = function() {
  5237. var map = {
  5238. 'exitFullscreen': 'fullscreenchange',
  5239. 'webkitExitFullscreen': 'webkitfullscreenchange',
  5240. 'mozCancelFullScreen': 'mozfullscreenchange',
  5241. 'msExitFullscreen': 'MSFullscreenChange'
  5242. };
  5243. for (var exit in map) {
  5244. if (map.hasOwnProperty(exit) && exit in document) {
  5245. return map[exit];
  5246. }
  5247. }
  5248. return null;
  5249. };
  5250. /**
  5251. * @summary Ensures that a number is in a given interval
  5252. * @param {number} x
  5253. * @param {number} min
  5254. * @param {number} max
  5255. * @returns {number}
  5256. */
  5257. PSVUtils.bound = function(x, min, max) {
  5258. return Math.max(min, Math.min(max, x));
  5259. };
  5260. /**
  5261. * @summary Checks if a value is an integer
  5262. * @function
  5263. * @param {*} value
  5264. * @returns {boolean}
  5265. */
  5266. PSVUtils.isInteger = Number.isInteger || function(value) {
  5267. return typeof value === 'number' && isFinite(value) && Math.floor(value) === value;
  5268. };
  5269. /**
  5270. * @summary Computes the sum of an array
  5271. * @param {number[]} array
  5272. * @returns {number}
  5273. */
  5274. PSVUtils.sum = function(array) {
  5275. return array.reduce(function(a, b) {
  5276. return a + b;
  5277. }, 0);
  5278. };
  5279. /**
  5280. * @summary Transforms a string to dash-case
  5281. * {@link https://github.com/shahata/dasherize}
  5282. * @param {string} str
  5283. * @returns {string}
  5284. */
  5285. PSVUtils.dasherize = function(str) {
  5286. return str.replace(/[A-Z](?:(?=[^A-Z])|[A-Z]*(?=[A-Z][^A-Z]|$))/g, function(s, i) {
  5287. return (i > 0 ? '-' : '') + s.toLowerCase();
  5288. });
  5289. };
  5290. /**
  5291. * @summary Returns the value of a given attribute in the panorama metadata
  5292. * @param {string} data
  5293. * @param {string} attr
  5294. * @returns (string)
  5295. */
  5296. PSVUtils.getXMPValue = function(data, attr) {
  5297. var result;
  5298. // XMP data are stored in children
  5299. if ((result = data.match('<GPano:' + attr + '>(.*)</GPano:' + attr + '>')) !== null) {
  5300. return result[1];
  5301. }
  5302. // XMP data are stored in attributes
  5303. else if ((result = data.match('GPano:' + attr + '="(.*?)"')) !== null) {
  5304. return result[1];
  5305. } else {
  5306. return null;
  5307. }
  5308. };
  5309. /**
  5310. * @summary Detects if fullscreen is enabled
  5311. * @param {HTMLElement} elt
  5312. * @returns {boolean}
  5313. */
  5314. PSVUtils.isFullscreenEnabled = function(elt) {
  5315. return (document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement) === elt;
  5316. };
  5317. /**
  5318. * @summary Enters fullscreen mode
  5319. * @param {HTMLElement} elt
  5320. */
  5321. PSVUtils.requestFullscreen = function(elt) {
  5322. (elt.requestFullscreen || elt.mozRequestFullScreen || elt.webkitRequestFullscreen || elt.msRequestFullscreen).call(elt);
  5323. };
  5324. /**
  5325. * @summary Exits fullscreen mode
  5326. */
  5327. PSVUtils.exitFullscreen = function() {
  5328. (document.exitFullscreen || document.mozCancelFullScreen || document.webkitExitFullscreen || document.msExitFullscreen).call(document);
  5329. };
  5330. /**
  5331. * @summary Gets an element style
  5332. * @param {HTMLElement} elt
  5333. * @param {string} prop
  5334. * @returns {*}
  5335. */
  5336. PSVUtils.getStyle = function(elt, prop) {
  5337. return window.getComputedStyle(elt, null)[prop];
  5338. };
  5339. /**
  5340. * @summary Compute the shortest offset between two longitudes
  5341. * @param {float} from
  5342. * @param {float} to
  5343. * @returns {float}
  5344. */
  5345. PSVUtils.getShortestArc = function(from, to) {
  5346. var tCandidates = [
  5347. 0, // direct
  5348. PSVUtils.TwoPI, // clock-wise cross zero
  5349. -PSVUtils.TwoPI // counter-clock-wise cross zero
  5350. ];
  5351. return tCandidates.reduce(function(value, candidate) {
  5352. candidate = to - from + candidate;
  5353. return Math.abs(candidate) < Math.abs(value) ? candidate : value;
  5354. }, Infinity);
  5355. };
  5356. /**
  5357. * @summary Translate CSS values like "top center" or "10% 50%" as top and left positions
  5358. * @description The implementation is as close as possible to the "background-position" specification
  5359. * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-position}
  5360. * @param {string} value
  5361. * @returns {{top: float, left: float}}
  5362. */
  5363. PSVUtils.parsePosition = function(value) {
  5364. if (!value) {
  5365. return { top: 0.5, left: 0.5 };
  5366. }
  5367. if (typeof value === 'object') {
  5368. return value;
  5369. }
  5370. var tokens = value.toLocaleLowerCase().split(' ').slice(0, 2);
  5371. if (tokens.length === 1) {
  5372. if (PSVUtils.parsePosition.positions[tokens[0]] !== undefined) {
  5373. tokens = [tokens[0], 'center'];
  5374. } else {
  5375. tokens = [tokens[0], tokens[0]];
  5376. }
  5377. }
  5378. var xFirst = tokens[1] !== 'left' && tokens[1] !== 'right' && tokens[0] !== 'top' && tokens[0] !== 'bottom';
  5379. tokens = tokens.map(function(token) {
  5380. return PSVUtils.parsePosition.positions[token] || token;
  5381. });
  5382. if (!xFirst) {
  5383. tokens.reverse();
  5384. }
  5385. var parsed = tokens.join(' ').match(/^([0-9.]+)% ([0-9.]+)%$/);
  5386. if (parsed) {
  5387. return {
  5388. left: parsed[1] / 100,
  5389. top: parsed[2] / 100
  5390. };
  5391. } else {
  5392. return { top: 0.5, left: 0.5 };
  5393. }
  5394. };
  5395. PSVUtils.parsePosition.positions = { 'top': '0%', 'bottom': '100%', 'left': '0%', 'right': '100%', 'center': '50%' };
  5396. /**
  5397. * @summary Parses an speed
  5398. * @param {string} speed - The speed, in radians/degrees/revolutions per second/minute
  5399. * @returns {float} radians per second
  5400. * @throws {PSVError} when the speed cannot be parsed
  5401. */
  5402. PSVUtils.parseSpeed = function(speed) {
  5403. if (typeof speed === 'string') {
  5404. speed = speed.toString().trim();
  5405. // Speed extraction
  5406. var speed_value = parseFloat(speed.replace(/^(-?[0-9]+(?:\.[0-9]*)?).*$/, '$1'));
  5407. var speed_unit = speed.replace(/^-?[0-9]+(?:\.[0-9]*)?(.*)$/, '$1').trim();
  5408. // "per minute" -> "per second"
  5409. if (speed_unit.match(/(pm|per minute)$/)) {
  5410. speed_value /= 60;
  5411. }
  5412. // Which unit?
  5413. switch (speed_unit) {
  5414. // Degrees per minute / second
  5415. case 'dpm':
  5416. case 'degrees per minute':
  5417. case 'dps':
  5418. case 'degrees per second':
  5419. speed = THREE.Math.degToRad(speed_value);
  5420. break;
  5421. // Radians per minute / second
  5422. case 'radians per minute':
  5423. case 'radians per second':
  5424. speed = speed_value;
  5425. break;
  5426. // Revolutions per minute / second
  5427. case 'rpm':
  5428. case 'revolutions per minute':
  5429. case 'rps':
  5430. case 'revolutions per second':
  5431. speed = speed_value * PSVUtils.TwoPI;
  5432. break;
  5433. // Unknown unit
  5434. default:
  5435. throw new PSVError('unknown speed unit "' + speed_unit + '"');
  5436. }
  5437. }
  5438. return speed;
  5439. };
  5440. /**
  5441. * @summary Parses an angle value in radians or degrees and returns a normalized value in radians
  5442. * @param {string|number} angle - eg: 3.14, 3.14rad, 180deg
  5443. * @param {boolean} [zeroCenter=false] - normalize between -Pi/2 - Pi/2 instead of 0 - 2*Pi
  5444. * @returns {float}
  5445. * @throws {PSVError} when the angle cannot be parsed
  5446. */
  5447. PSVUtils.parseAngle = function(angle, zeroCenter) {
  5448. if (typeof angle === 'string') {
  5449. var match = angle.toLowerCase().trim().match(/^(-?[0-9]+(?:\.[0-9]*)?)(.*)$/);
  5450. if (!match) {
  5451. throw new PSVError('unknown angle "' + angle + '"');
  5452. }
  5453. var value = parseFloat(match[1]);
  5454. var unit = match[2];
  5455. if (unit) {
  5456. switch (unit) {
  5457. case 'deg':
  5458. case 'degs':
  5459. angle = THREE.Math.degToRad(value);
  5460. break;
  5461. case 'rad':
  5462. case 'rads':
  5463. angle = value;
  5464. break;
  5465. default:
  5466. throw new PSVError('unknown angle unit "' + unit + '"');
  5467. }
  5468. } else {
  5469. angle = value;
  5470. }
  5471. }
  5472. angle = (zeroCenter ? angle + Math.PI : angle) % PSVUtils.TwoPI;
  5473. if (angle < 0) {
  5474. angle = PSVUtils.TwoPI + angle;
  5475. }
  5476. return zeroCenter ? PSVUtils.bound(angle - Math.PI, -PSVUtils.HalfPI, PSVUtils.HalfPI) : angle;
  5477. };
  5478. /**
  5479. * @summary Removes all children of a three.js scene and dispose all textures
  5480. * @param {THREE.Scene} scene
  5481. */
  5482. PSVUtils.cleanTHREEScene = function(scene) {
  5483. scene.children.forEach(function(item) {
  5484. if (item instanceof THREE.Mesh) {
  5485. if (item.geometry) {
  5486. item.geometry.dispose();
  5487. item.geometry = null;
  5488. }
  5489. if (item.material) {
  5490. if (item.material.materials) {
  5491. item.material.materials.forEach(function(material) {
  5492. if (material.map) {
  5493. material.map.dispose();
  5494. material.map = null;
  5495. }
  5496. material.dispose();
  5497. });
  5498. item.material.materials.length = 0;
  5499. } else {
  5500. if (item.material.map) {
  5501. item.material.map.dispose();
  5502. item.material.map = null;
  5503. }
  5504. item.material.dispose();
  5505. }
  5506. item.material = null;
  5507. }
  5508. }
  5509. });
  5510. scene.children.length = 0;
  5511. };
  5512. /**
  5513. * @callback AnimationOnTick
  5514. * @param {Object} properties - current values
  5515. * @param {float} progress - 0 to 1
  5516. */
  5517. /**
  5518. * @summary Interpolates each property with an easing and optional delay
  5519. * @param {Object} options
  5520. * @param {Object[]} options.properties
  5521. * @param {number} options.properties[].start
  5522. * @param {number} options.properties[].end
  5523. * @param {int} options.duration
  5524. * @param {int} [options.delay=0]
  5525. * @param {string} [options.easing='linear']
  5526. * @param {AnimationOnTick} options.onTick - called on each frame
  5527. * @returns {Promise} Promise with an additional "cancel" method
  5528. */
  5529. PSVUtils.animation = function(options) {
  5530. var defer = D(false); // alwaysAsync = false to allow immediate resolution of "cancel"
  5531. var start = null;
  5532. if (!options.easing || typeof options.easing === 'string') {
  5533. options.easing = PSVUtils.animation.easings[options.easing || 'linear'];
  5534. }
  5535. function run(timestamp) {
  5536. // the animation has been cancelled
  5537. if (defer.promise.getStatus() === -1) {
  5538. return;
  5539. }
  5540. // first iteration
  5541. if (start === null) {
  5542. start = timestamp;
  5543. }
  5544. // compute progress
  5545. var progress = (timestamp - start) / options.duration;
  5546. var current = {};
  5547. var name;
  5548. if (progress < 1.0) {
  5549. // interpolate properties
  5550. for (name in options.properties) {
  5551. current[name] = options.properties[name].start + (options.properties[name].end - options.properties[name].start) * options.easing(progress);
  5552. }
  5553. options.onTick(current, progress);
  5554. window.requestAnimationFrame(run);
  5555. } else {
  5556. // call onTick one last time with final values
  5557. for (name in options.properties) {
  5558. current[name] = options.properties[name].end;
  5559. }
  5560. options.onTick(current, 1.0);
  5561. window.requestAnimationFrame(function() {
  5562. defer.resolve();
  5563. });
  5564. }
  5565. }
  5566. if (options.delay !== undefined) {
  5567. window.setTimeout(function() {
  5568. window.requestAnimationFrame(run);
  5569. }, options.delay);
  5570. } else {
  5571. window.requestAnimationFrame(run);
  5572. }
  5573. // add a "cancel" to the promise
  5574. var promise = defer.promise;
  5575. promise.cancel = function() {
  5576. defer.reject();
  5577. };
  5578. return promise;
  5579. };
  5580. /**
  5581. * @summary Collection of easing functions
  5582. * {@link https://gist.github.com/frederickk/6165768}
  5583. * @type {Object.<string, Function>}
  5584. */
  5585. // @formatter:off
  5586. // jscs:disable
  5587. /* jshint ignore:start */
  5588. PSVUtils.animation.easings = {
  5589. linear: function(t) { return t; },
  5590. inQuad: function(t) { return t * t; },
  5591. outQuad: function(t) { return t * (2 - t); },
  5592. inOutQuad: function(t) { return t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t; },
  5593. inCubic: function(t) { return t * t * t; },
  5594. outCubic: function(t) { return (--t) * t * t + 1; },
  5595. inOutCubic: function(t) { return t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; },
  5596. inQuart: function(t) { return t * t * t * t; },
  5597. outQuart: function(t) { return 1 - (--t) * t * t * t; },
  5598. inOutQuart: function(t) { return t < .5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t; },
  5599. inQuint: function(t) { return t * t * t * t * t; },
  5600. outQuint: function(t) { return 1 + (--t) * t * t * t * t; },
  5601. inOutQuint: function(t) { return t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t; },
  5602. inSine: function(t) { return 1 - Math.cos(t * (Math.PI / 2)); },
  5603. outSine: function(t) { return Math.sin(t * (Math.PI / 2)); },
  5604. inOutSine: function(t) { return .5 - .5 * Math.cos(Math.PI * t); },
  5605. inExpo: function(t) { return Math.pow(2, 10 * (t - 1)); },
  5606. outExpo: function(t) { return 1 - Math.pow(2, -10 * t); },
  5607. inOutExpo: function(t) { t = t * 2 - 1; return t < 0 ? .5 * Math.pow(2, 10 * t) : 1 - .5 * Math.pow(2, -10 * t); },
  5608. inCirc: function(t) { return 1 - Math.sqrt(1 - t * t); },
  5609. outCirc: function(t) { t--; return Math.sqrt(1 - t * t); },
  5610. inOutCirc: function(t) { t *= 2; return t < 1 ? .5 - .5 * Math.sqrt(1 - t * t) : .5 + .5 * Math.sqrt(1 - (t -= 2) * t); }
  5611. };
  5612. /* jshint ignore:end */
  5613. // jscs:enable
  5614. // @formatter:off
  5615. /**
  5616. * @summary Returns a function, that, when invoked, will only be triggered at most once during a given window of time.
  5617. * @copyright underscore.js - modified by Clément Prévost {@link http://stackoverflow.com/a/27078401}
  5618. * @param {Function} func
  5619. * @param {int} wait
  5620. * @returns {Function}
  5621. */
  5622. PSVUtils.throttle = function(func, wait) {
  5623. var self, args, result;
  5624. var timeout = null;
  5625. var previous = 0;
  5626. var later = function() {
  5627. previous = Date.now();
  5628. timeout = null;
  5629. result = func.apply(self, args);
  5630. if (!timeout) {
  5631. self = args = null;
  5632. }
  5633. };
  5634. return function() {
  5635. var now = Date.now();
  5636. if (!previous) {
  5637. previous = now;
  5638. }
  5639. var remaining = wait - (now - previous);
  5640. self = this;
  5641. args = arguments;
  5642. if (remaining <= 0 || remaining > wait) {
  5643. if (timeout) {
  5644. clearTimeout(timeout);
  5645. timeout = null;
  5646. }
  5647. previous = now;
  5648. result = func.apply(self, args);
  5649. if (!timeout) {
  5650. self = args = null;
  5651. }
  5652. } else if (!timeout) {
  5653. timeout = setTimeout(later, remaining);
  5654. }
  5655. return result;
  5656. };
  5657. };
  5658. /**
  5659. * @summary Test if an object is a plain object
  5660. * @description Test if an object is a plain object, i.e. is constructed
  5661. * by the built-in Object constructor and inherits directly from Object.prototype
  5662. * or null. Some built-in objects pass the test, e.g. Math which is a plain object
  5663. * and some host or exotic objects may pass also.
  5664. * {@link http://stackoverflow.com/a/5878101/1207670}
  5665. * @param {*} obj
  5666. * @returns {boolean}
  5667. */
  5668. PSVUtils.isPlainObject = function(obj) {
  5669. // Basic check for Type object that's not null
  5670. if (typeof obj === 'object' && obj !== null) {
  5671. // If Object.getPrototypeOf supported, use it
  5672. if (typeof Object.getPrototypeOf === 'function') {
  5673. var proto = Object.getPrototypeOf(obj);
  5674. return proto === Object.prototype || proto === null;
  5675. }
  5676. // Otherwise, use internal class
  5677. // This should be reliable as if getPrototypeOf not supported, is pre-ES5
  5678. return Object.prototype.toString.call(obj) === '[object Object]';
  5679. }
  5680. // Not an object
  5681. return false;
  5682. };
  5683. /**
  5684. * @summary Merges the enumerable attributes of two objects
  5685. * @description Replaces arrays and alters the target object.
  5686. * @copyright Nicholas Fisher <nfisher110@gmail.com>
  5687. * @param {Object} target
  5688. * @param {Object} src
  5689. * @returns {Object} target
  5690. */
  5691. PSVUtils.deepmerge = function(target, src) {
  5692. var first = src;
  5693. return (function merge(target, src) {
  5694. if (Array.isArray(src)) {
  5695. if (!target || !Array.isArray(target)) {
  5696. target = [];
  5697. } else {
  5698. target.length = 0;
  5699. }
  5700. src.forEach(function(e, i) {
  5701. target[i] = merge(null, e);
  5702. });
  5703. } else if (typeof src === 'object') {
  5704. if (!target || Array.isArray(target)) {
  5705. target = {};
  5706. }
  5707. Object.keys(src).forEach(function(key) {
  5708. if (typeof src[key] !== 'object' || !src[key] || !PSVUtils.isPlainObject(src[key])) {
  5709. target[key] = src[key];
  5710. } else if (src[key] != first) {
  5711. if (!target[key]) {
  5712. target[key] = merge(null, src[key]);
  5713. } else {
  5714. merge(target[key], src[key]);
  5715. }
  5716. }
  5717. });
  5718. } else {
  5719. target = src;
  5720. }
  5721. return target;
  5722. }(target, src));
  5723. };
  5724. /**
  5725. * @summary Clones an object
  5726. * @param {Object} src
  5727. * @returns {Object}
  5728. */
  5729. PSVUtils.clone = function(src) {
  5730. return PSVUtils.deepmerge(null, src);
  5731. };
  5732. /**
  5733. * @summary Normalize mousewheel values accross browsers
  5734. * @description From Facebook's Fixed Data Table
  5735. * {@link https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js}
  5736. * @copyright Facebook
  5737. * @param {MouseWheelEvent} event
  5738. * @returns {{spinX: number, spinY: number, pixelX: number, pixelY: number}}
  5739. */
  5740. PSVUtils.normalizeWheel = function(event) {
  5741. var PIXEL_STEP = 10;
  5742. var LINE_HEIGHT = 40;
  5743. var PAGE_HEIGHT = 800;
  5744. var sX = 0,
  5745. sY = 0; // spinX, spinY
  5746. var pX = 0,
  5747. pY = 0; // pixelX, pixelY
  5748. // Legacy
  5749. if ('detail' in event) { sY = event.detail; }
  5750. if ('wheelDelta' in event) { sY = -event.wheelDelta / 120; }
  5751. if ('wheelDeltaY' in event) { sY = -event.wheelDeltaY / 120; }
  5752. if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; }
  5753. // side scrolling on FF with DOMMouseScroll
  5754. if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) {
  5755. sX = sY;
  5756. sY = 0;
  5757. }
  5758. pX = sX * PIXEL_STEP;
  5759. pY = sY * PIXEL_STEP;
  5760. if ('deltaY' in event) { pY = event.deltaY; }
  5761. if ('deltaX' in event) { pX = event.deltaX; }
  5762. if ((pX || pY) && event.deltaMode) {
  5763. if (event.deltaMode === 1) { // delta in LINE units
  5764. pX *= LINE_HEIGHT;
  5765. pY *= LINE_HEIGHT;
  5766. } else { // delta in PAGE units
  5767. pX *= PAGE_HEIGHT;
  5768. pY *= PAGE_HEIGHT;
  5769. }
  5770. }
  5771. // Fall-back if spin cannot be determined
  5772. if (pX && !sX) { sX = (pX < 1) ? -1 : 1; }
  5773. if (pY && !sY) { sY = (pY < 1) ? -1 : 1; }
  5774. return {
  5775. spinX: sX,
  5776. spinY: sY,
  5777. pixelX: pX,
  5778. pixelY: pY
  5779. };
  5780. };
  5781. /**
  5782. * @callback ForEach
  5783. * @param {*} value
  5784. * @param {string} key
  5785. */
  5786. /**
  5787. * Loops over enumerable properties of an object
  5788. * @param {object} object
  5789. * @param {ForEach} callback
  5790. */
  5791. PSVUtils.forEach = function(object, callback) {
  5792. for (var key in object) {
  5793. if (object.hasOwnProperty(key)) {
  5794. callback(object[key], key);
  5795. }
  5796. }
  5797. };
  5798. /**
  5799. * requestAnimationFrame polyfill
  5800. * {@link http://mattsnider.com/cross-browser-and-legacy-supported-requestframeanimation}
  5801. * @license MIT
  5802. */
  5803. (function(w) {
  5804. "use strict";
  5805. // most browsers have an implementation
  5806. w.requestAnimationFrame = w.requestAnimationFrame ||
  5807. w.mozRequestAnimationFrame || w.webkitRequestAnimationFrame ||
  5808. w.msRequestAnimationFrame;
  5809. w.cancelAnimationFrame = w.cancelAnimationFrame ||
  5810. w.mozCancelAnimationFrame || w.webkitCancelAnimationFrame ||
  5811. w.msCancelAnimationFrame;
  5812. // polyfill, when necessary
  5813. if (!w.requestAnimationFrame) {
  5814. var aAnimQueue = [],
  5815. aProcessing = [],
  5816. iRequestId = 0,
  5817. iIntervalId;
  5818. // create a mock requestAnimationFrame function
  5819. w.requestAnimationFrame = function(callback) {
  5820. aAnimQueue.push([++iRequestId, callback]);
  5821. if (!iIntervalId) {
  5822. iIntervalId = setInterval(function() {
  5823. if (aAnimQueue.length) {
  5824. var time = +new Date();
  5825. // Process all of the currently outstanding frame
  5826. // requests, but none that get added during the
  5827. // processing.
  5828. // Swap the arrays so we don't have to create a new
  5829. // array every frame.
  5830. var temp = aProcessing;
  5831. aProcessing = aAnimQueue;
  5832. aAnimQueue = temp;
  5833. while (aProcessing.length) {
  5834. aProcessing.shift()[1](time);
  5835. }
  5836. } else {
  5837. // don't continue the interval, if unnecessary
  5838. clearInterval(iIntervalId);
  5839. iIntervalId = undefined;
  5840. }
  5841. }, 1000 / 50); // estimating support for 50 frames per second
  5842. }
  5843. return iRequestId;
  5844. };
  5845. // create a mock cancelAnimationFrame function
  5846. w.cancelAnimationFrame = function(requestId) {
  5847. // find the request ID and remove it
  5848. var i, j;
  5849. for (i = 0, j = aAnimQueue.length; i < j; i += 1) {
  5850. if (aAnimQueue[i][0] === requestId) {
  5851. aAnimQueue.splice(i, 1);
  5852. return;
  5853. }
  5854. }
  5855. // If it's not in the queue, it may be in the set we're currently
  5856. // processing (if cancelAnimationFrame is called from within a
  5857. // requestAnimationFrame callback).
  5858. for (i = 0, j = aProcessing.length; i < j; i += 1) {
  5859. if (aProcessing[i][0] === requestId) {
  5860. aProcessing.splice(i, 1);
  5861. return;
  5862. }
  5863. }
  5864. };
  5865. }
  5866. })(window);
  5867. PhotoSphereViewer.ICONS['compass.svg'] = '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M49.997,0C22.38,0.004,0.005,22.383,0,50.002C0.005,77.614,22.38,99.995,49.997,100C77.613,99.995,99.996,77.614,100,50.002C99.996,22.383,77.613,0.004,49.997,0z M49.997,88.81c-21.429-0.04-38.772-17.378-38.809-38.807c0.037-21.437,17.381-38.775,38.809-38.812C71.43,11.227,88.769,28.567,88.81,50.002C88.769,71.432,71.43,88.77,49.997,88.81z"/><path d="M72.073,25.891L40.25,41.071l-0.003-0.004l-0.003,0.009L27.925,74.109l31.82-15.182l0.004,0.004l0.002-0.007l-0.002-0.004L72.073,25.891z M57.837,54.411L44.912,42.579l21.092-10.062L57.837,54.411z"/><!--Created by iconoci from the Noun Project--></svg>';
  5868. PhotoSphereViewer.ICONS['download.svg'] = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M83.285,35.575H66.271L66.277,3H32.151v32.575H16.561l33.648,32.701L83.285,35.575z"/><path d="M83.316,64.199v16.32H16.592v-16.32H-0.094v32.639H100V64.199H83.316z"/><!--Created by Michael Zenaty from the Noun Project--></svg>';
  5869. PhotoSphereViewer.ICONS['fullscreen-in.svg'] = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><polygon points="100,39.925 87.105,39.925 87.105,18.895 66.075,18.895 66.075,6 100,6"/><polygon points="100,93.221 66.075,93.221 66.075,80.326 87.105,80.326 87.105,59.295 100,59.295"/><polygon points="33.925,93.221 0,93.221 0,59.295 12.895,59.295 12.895,80.326 33.925,80.326"/><polygon points="12.895,39.925 0,39.925 0,6 33.925,6 33.925,18.895 12.895,18.895"/><!--Created by Garrett Knoll from the Noun Project--></svg>';
  5870. PhotoSphereViewer.ICONS['fullscreen-out.svg'] = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><polygon points="66.075,7 78.969,7 78.969,28.031 100,28.031 100,40.925 66.075,40.925"/><polygon points="66.075,60.295 100,60.295 100,73.19 78.969,73.19 78.969,94.221 66.075,94.221"/><polygon points="0,60.295 33.925,60.295 33.925,94.221 21.031,94.221 21.031,73.19 0,73.19"/><polygon points="21.031,7 33.925,7 33.925,40.925 0,40.925 0,28.031 21.031,28.031"/><!--Created by Garrett Knoll from the Noun Project--></svg>';
  5871. PhotoSphereViewer.ICONS['pin.svg'] = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve"><path d="M24,0C13.798,0,5.499,8.3,5.499,18.501c0,10.065,17.57,28.635,18.318,29.421C23.865,47.972,23.931,48,24,48s0.135-0.028,0.183-0.078c0.748-0.786,18.318-19.355,18.318-29.421C42.501,8.3,34.202,0,24,0z M24,7.139c5.703,0,10.342,4.64,10.342,10.343c0,5.702-4.639,10.342-10.342,10.342c-5.702,0-10.34-4.64-10.34-10.342C13.66,11.778,18.298,7.139,24,7.139z"/><!--Created by Daniele Marucci from the Noun Project--></svg>';
  5872. PhotoSphereViewer.ICONS['play-active.svg'] = '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 41 41" enable-background="new 0 0 41 41" xml:space="preserve"><path d="M40.5,14.1c-0.1-0.1-1.2-0.5-2.898-1C37.5,13.1,37.4,13,37.4,12.9C34.5,6.5,28,2,20.5,2S6.6,6.5,3.7,12.9c0,0.1-0.1,0.1-0.2,0.2c-1.7,0.6-2.8,1-2.9,1L0,14.4v12.1l0.6,0.2c0.1,0,1.1,0.399,2.7,0.899c0.1,0,0.2,0.101,0.2,0.199C6.3,34.4,12.9,39,20.5,39c7.602,0,14.102-4.6,16.9-11.1c0-0.102,0.1-0.102,0.199-0.2c1.699-0.601,2.699-1,2.801-1l0.6-0.3V14.3L40.5,14.1z M6.701,11.5C9.7,7,14.8,4,20.5,4c5.8,0,10.9,3,13.8,7.5c0.2,0.3-0.1,0.6-0.399,0.5c-3.799-1-8.799-2-13.6-2c-4.7,0-9.5,1-13.2,2C6.801,12.1,6.601,11.8,6.701,11.5z M25.1,20.3L18.7,24c-0.3,0.2-0.7,0-0.7-0.5v-7.4c0-0.4,0.4-0.6,0.7-0.4 l6.399,3.8C25.4,19.6,25.4,20.1,25.1,20.3z M34.5,29.201C31.602,33.9,26.4,37,20.5,37c-5.9,0-11.1-3.1-14-7.898c-0.2-0.302,0.1-0.602,0.4-0.5c3.9,1,8.9,2.1,13.6,2.1c5,0,9.9-1,13.602-2C34.4,28.602,34.602,28.9,34.5,29.201z"/><!--Created by Nick Bluth from the Noun Project--></svg>';
  5873. PhotoSphereViewer.ICONS['play.svg'] = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 41 41" enable-background="new 0 0 41 41" xml:space="preserve"><path d="M40.5,14.1c-0.1-0.1-1.2-0.5-2.899-1c-0.101,0-0.2-0.1-0.2-0.2C34.5,6.5,28,2,20.5,2S6.6,6.5,3.7,12.9c0,0.1-0.1,0.1-0.2,0.2c-1.7,0.6-2.8,1-2.9,1L0,14.4v12.1l0.6,0.2c0.1,0,1.1,0.4,2.7,0.9c0.1,0,0.2,0.1,0.2,0.199C6.3,34.4,12.9,39,20.5,39c7.601,0,14.101-4.6,16.9-11.1c0-0.101,0.1-0.101,0.2-0.2c1.699-0.6,2.699-1,2.8-1l0.6-0.3V14.3L40.5,14.1zM20.5,4c5.8,0,10.9,3,13.8,7.5c0.2,0.3-0.1,0.6-0.399,0.5c-3.8-1-8.8-2-13.6-2c-4.7,0-9.5,1-13.2,2c-0.3,0.1-0.5-0.2-0.4-0.5C9.7,7,14.8,4,20.5,4z M20.5,37c-5.9,0-11.1-3.1-14-7.899c-0.2-0.301,0.1-0.601,0.4-0.5c3.9,1,8.9,2.1,13.6,2.1c5,0,9.9-1,13.601-2c0.3-0.1,0.5,0.2,0.399,0.5C31.601,33.9,26.4,37,20.5,37z M39.101,24.9c0,0.1-0.101,0.3-0.2,0.3c-2.5,0.9-10.4,3.6-18.4,3.6c-7.1,0-15.6-2.699-18.3-3.6C2.1,25.2,2,25,2,24.9V16c0-0.1,0.1-0.3,0.2-0.3c2.6-0.9,10.6-3.6,18.2-3.6c7.5,0,15.899,2.7,18.5,3.6c0.1,0,0.2,0.2,0.2,0.3V24.9z"/><path d="M18.7,24l6.4-3.7c0.3-0.2,0.3-0.7,0-0.8l-6.4-3.8c-0.3-0.2-0.7,0-0.7,0.4v7.4C18,24,18.4,24.2,18.7,24z"/><!--Created by Nick Bluth from the Noun Project--></svg>';
  5874. PhotoSphereViewer.ICONS['zoom-in.svg'] = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 20 20" enable-background="new 0 0 20 20" xml:space="preserve"><path d="M14.043,12.22c2.476-3.483,1.659-8.313-1.823-10.789C8.736-1.044,3.907-0.228,1.431,3.255c-2.475,3.482-1.66,8.312,1.824,10.787c2.684,1.908,6.281,1.908,8.965,0l4.985,4.985c0.503,0.504,1.32,0.504,1.822,0c0.505-0.503,0.505-1.319,0-1.822L14.043,12.22z M7.738,13.263c-3.053,0-5.527-2.475-5.527-5.525c0-3.053,2.475-5.527,5.527-5.527c3.05,0,5.524,2.474,5.524,5.527C13.262,10.789,10.788,13.263,7.738,13.263z"/><polygon points="8.728,4.009 6.744,4.009 6.744,6.746 4.006,6.746 4.006,8.73 6.744,8.73 6.744,11.466 8.728,11.466 8.728,8.73 11.465,8.73 11.465,6.746 8.728,6.746"/><!--Created by Ryan Canning from the Noun Project--></svg>';
  5875. PhotoSphereViewer.ICONS['zoom-out.svg'] = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 20 20" enable-background="new 0 0 20 20" xml:space="preserve"><path d="M14.043,12.22c2.476-3.483,1.659-8.313-1.823-10.789C8.736-1.044,3.907-0.228,1.431,3.255c-2.475,3.482-1.66,8.312,1.824,10.787c2.684,1.908,6.281,1.908,8.965,0l4.985,4.985c0.503,0.504,1.32,0.504,1.822,0c0.505-0.503,0.505-1.319,0-1.822L14.043,12.22z M7.738,13.263c-3.053,0-5.527-2.475-5.527-5.525c0-3.053,2.475-5.527,5.527-5.527c3.05,0,5.524,2.474,5.524,5.527C13.262,10.789,10.788,13.263,7.738,13.263z"/><rect x="4.006" y="6.746" width="7.459" height="1.984"/><!--Created by Ryan Canning from the Noun Project--></svg>';
  5876. return PhotoSphereViewer;
  5877. }));