From a8f507a19308a16244ce7bcd37da204c42b7c458 Mon Sep 17 00:00:00 2001 From: Joshua Bell Date: Thu, 25 Jul 2013 16:52:35 -0700 Subject: [PATCH] Initial snapshot --- .gitignore | 28 + Admin.aspx | 1 + Admin.aspx.cs | 113 + AssemblyInfo.cs | 65 + Astrometrics.cs | 126 + Codes.aspx | 1 + Codes.aspx.cs | 160 + Coordinates.aspx | 2 + Coordinates.aspx.cs | 101 + Credits.aspx | 2 + Credits.aspx.cs | 193 + DataPage.cs | 209 + Dump.aspx | 1 + Dump.aspx.cs | 104 + Errors.aspx | 1 + Errors.aspx.cs | 90 + Global.asax | 1 + Global.asax.cs | 93 + ImageGeneratorPage.cs | 320 + Json.cs | 271 + JumpMap.aspx | 2 + JumpMap.aspx.cs | 199 + JumpWorlds.aspx | 2 + JumpWorlds.aspx.cs | 99 + LICENSE.md | 27 + Location.cs | 203 + MSEC.aspx | 2 + MSEC.aspx.cs | 91 + Maps.csproj | 849 + Maps.sln | 29 + Metadata.cs | 86 + Mobile.aspx | 87 + Mobile.aspx.cs | 463 + Overview.aspx | 2 + Overview.aspx.cs | 89 + Poster.aspx | 2 + Poster.aspx.cs | 200 + Render.cs | 1445 ++ RenderUtil.cs | 689 + ResourceManager.cs | 178 + SEC.aspx | 2 + SEC.aspx.cs | 114 + Search.aspx | 2 + Search.aspx.cs | 247 + SearchEngine.cs | 315 + Sector.cs | 971 ++ SectorDataCollection.cs | 74 + SectorMetaData.aspx | 2 + SectorMetaData.aspx.cs | 82 + Selector.cs | 355 + ServiceConfiguration.cs | 43 + StellarData.cs | 430 + Stylesheet.cs | 773 + Tile.aspx | 2 + Tile.aspx.cs | 80 + Universe.aspx | 2 + Universe.aspx.cs | 82 + Util.cs | 162 + VectorObject.cs | 356 + World.cs | 387 + api.htm | 1266 ++ borders/bd_images/image001.gif | Bin 0 -> 3955 bytes borders/bd_images/image003.jpg | Bin 0 -> 24829 bytes borders/bd_images/image004.gif | Bin 0 -> 2969 bytes borders/bd_images/image005.gif | Bin 0 -> 3129 bytes borders/bd_images/image006.gif | Bin 0 -> 3192 bytes borders/bd_images/image007.gif | Bin 0 -> 3231 bytes borders/bd_images/image008.gif | Bin 0 -> 3253 bytes borders/bd_images/image009.gif | Bin 0 -> 3292 bytes borders/bd_images/image010.gif | Bin 0 -> 3312 bytes borders/bd_images/image011.gif | Bin 0 -> 3327 bytes borders/bd_images/image012.gif | Bin 0 -> 3318 bytes borders/bd_images/image013.gif | Bin 0 -> 3318 bytes borders/bd_images/image014.gif | Bin 0 -> 3267 bytes borders/bd_images/image016.jpg | Bin 0 -> 35976 bytes borders/bd_images/image018.jpg | Bin 0 -> 21433 bytes borders/bd_images/image019.gif | Bin 0 -> 3315 bytes borders/bd_images/image020.gif | Bin 0 -> 3253 bytes borders/bd_images/image021.gif | Bin 0 -> 3358 bytes borders/bd_images/image022.gif | Bin 0 -> 3285 bytes borders/bd_images/image023.gif | Bin 0 -> 1094 bytes borders/borders.js | 467 + borders/default.htm | 1 + borders/demo.htm | 255 + borders/doc.htm | 231 + borders/regen.htm | 350 + credits.htm | 609 + favicon.ico | Bin 0 -> 1078 bytes formats.htm | 1605 ++ handlebars.js | 2201 +++ iframe.htm | 64 + index.html | 389 + index.js | 443 + jpost.htm | 203 + map.css | 235 + map.js | 1390 ++ polyfill/harmony.js | 1569 ++ polyfill/polyfill.js | 1188 ++ post.htm | 202 + post.js | 46 + res/AprilFools/Starburst.png | Bin 0 -> 5654 bytes res/AprilFools/Starburst_Gray.png | Bin 0 -> 4325 bytes res/Candy/Belt.png | Bin 0 -> 11851 bytes res/Candy/Galaxy.pdn | 11008 +++++++++++++ res/Candy/Galaxy.png | Bin 0 -> 238487 bytes res/Candy/Hyd0.png | Bin 0 -> 17859 bytes res/Candy/Hyd1.png | Bin 0 -> 21704 bytes res/Candy/Hyd2.png | Bin 0 -> 20835 bytes res/Candy/Hyd3.png | Bin 0 -> 21904 bytes res/Candy/Hyd4.png | Bin 0 -> 21435 bytes res/Candy/Hyd5.png | Bin 0 -> 23306 bytes res/Candy/Hyd6.png | Bin 0 -> 22640 bytes res/Candy/Hyd7.png | Bin 0 -> 19958 bytes res/Candy/Hyd8.png | Bin 0 -> 18663 bytes res/Candy/Hyd9.png | Bin 0 -> 14991 bytes res/Candy/HydA.png | Bin 0 -> 18068 bytes res/Candy/Nebula.jpg | Bin 0 -> 106739 bytes res/Candy/Nebula.png | Bin 0 -> 822321 bytes res/Candy/Rifts.png | Bin 0 -> 11703 bytes res/Candy/tiled_nebula_256.jpg | Bin 0 -> 65419 bytes res/Candy/tiled_nebula_512.jpg | Bin 0 -> 335504 bytes res/Sectors/Aerenfors.sec | 490 + res/Sectors/Ahkiweahi.sec | 483 + res/Sectors/Aktifao.sec | 539 + res/Sectors/Alpha Crucis.tab | 487 + res/Sectors/Amderstun.sec | 492 + res/Sectors/Angfutsag.sec | 330 + res/Sectors/Antares.tab | 555 + res/Sectors/Avereguar.sec | 429 + res/Sectors/Avereguar.xml | 43 + res/Sectors/Centrax.xml | 180 + res/Sectors/Core.tab | 547 + res/Sectors/Corridor.tab | 269 + res/Sectors/CrucisMargin.sec | 429 + res/Sectors/DARK1120.SEC | 556 + res/Sectors/Dagudashaag.tab | 560 + res/Sectors/Daibei.tab | 454 + res/Sectors/Darret.sec | 1109 ++ res/Sectors/Datsatl.sec | 327 + res/Sectors/Delphi.tab | 347 + res/Sectors/Deneb.tab | 387 + res/Sectors/Dfotseth.sec | 238 + res/Sectors/Dhuerorrg.sec | 364 + res/Sectors/Diaspora.tab | 477 + res/Sectors/Drakken.sec | 792 + res/Sectors/Ealiyasiyw.sec | 464 + res/Sectors/Empty Quarter.tab | 314 + res/Sectors/Etakhasoa.sec | 447 + res/Sectors/Extolian.sec | 481 + res/Sectors/Fa Dzaets.sec | 266 + res/Sectors/Fahreahluis.sec | 386 + res/Sectors/FarFrontiers.xml | 270 + res/Sectors/Finggvakhou.sec | 314 + res/Sectors/Folgore.sec | 444 + res/Sectors/Folgore.xml | 151 + res/Sectors/Fornast.tab | 519 + res/Sectors/Gakghang.sec | 282 + res/Sectors/Gateway.sec | 290 + res/Sectors/Gelath.sec | 360 + res/Sectors/Gh1hken.sec | 528 + res/Sectors/Ghoekhnael.sec | 473 + res/Sectors/Glimmerdrift.tab | 351 + res/Sectors/Grikrng.sec | 149 + res/Sectors/Gushemege.tab | 536 + res/Sectors/Gvurrdon.tab | 359 + res/Sectors/Gzaekfueg.sec | 380 + res/Sectors/Gzirr!k'l.sec | 528 + res/Sectors/Gzirr!k'l.xml | 218 + res/Sectors/Harea.sec | 74 + res/Sectors/Heakhafaw.sec | 519 + res/Sectors/Hfiywitir.sec | 375 + res/Sectors/Hinterworlds.tab | 437 + res/Sectors/Ilelish.tab | 452 + res/Sectors/Irlaftalea.sec | 468 + res/Sectors/Irugangong.sec | 269 + res/Sectors/K'trekreer.sec | 477 + res/Sectors/K'trekreer.xml | 63 + res/Sectors/Kaa G!kul.sec | 682 + res/Sectors/Katoonah.sec | 467 + res/Sectors/Kefiykhta.sec | 508 + res/Sectors/KfazzGhik.sec | 269 + res/Sectors/Kharrthon.sec | 414 + res/Sectors/Knaeleng.sec | 383 + res/Sectors/Knoellighz.sec | 508 + res/Sectors/Ksinanirz.sec | 346 + res/Sectors/Ktiingzat.sec | 473 + res/Sectors/Leonidae.sec | 828 + res/Sectors/Ley.tab | 388 + res/Sectors/Lishun.tab | 602 + res/Sectors/Listanaya.sec | 288 + res/Sectors/Lorspane.sec | 706 + res/Sectors/Magyar.tab | 517 + res/Sectors/Massilia.tab | 517 + res/Sectors/Mendan.sec | 518 + res/Sectors/Ngathksirz.sec | 408 + res/Sectors/Nooq.sec | 424 + res/Sectors/Nooq.xml | 63 + res/Sectors/OeghzVaerrghr.sec | 310 + res/Sectors/Ogadogorz.sec | 466 + res/Sectors/Ohieraoi.sec | 305 + res/Sectors/Old Expanses.tab | 427 + res/Sectors/Phlask.sec | 784 + res/Sectors/Pliabriebl.sec | 18 + res/Sectors/Porlock.sec | 418 + res/Sectors/Porlock.xml | 129 + res/Sectors/Reavers Deep.tab | 367 + res/Sectors/Reft.tab | 124 + res/Sectors/Rfigh.sec | 456 + res/Sectors/Ricenden.sec | 438 + res/Sectors/Ricenden.xml | 188 + res/Sectors/Riftspan Reaches.sec | 170 + res/Sectors/Rzakki.sec | 279 + res/Sectors/Solomani Rim.tab | 401 + res/Sectors/Spinward Marches.tab | 440 + res/Sectors/TarGkellp.sec | 96 + res/Sectors/Teahloarifu.sec | 588 + res/Sectors/ThakuFung.sec | 195 + res/Sectors/Theron.sec | 433 + res/Sectors/Theta Borealis.sec | 409 + res/Sectors/Theta Borealis.xml | 126 + res/Sectors/Tlyasea.sec | 21 + res/Sectors/Trojan Reach.tab | 328 + res/Sectors/Ugoede.sec | 404 + res/Sectors/Uistilrao.sec | 378 + res/Sectors/Ukaarriitb.sec | 133 + res/Sectors/VegVergakh.sec | 352 + res/Sectors/Verge.tab | 233 + res/Sectors/Vland.tab | 533 + res/Sectors/Wrenton.sec | 451 + res/Sectors/Wrenton.xml | 172 + res/Sectors/Yahehwe.sec | 425 + res/Sectors/ZaoKfengIgGrilokh.sec | 289 + res/Sectors/Zarushagar.tab | 497 + res/Sectors/Zhodani Core Route/ablaz.sec | 336 + res/Sectors/Zhodani Core Route/abresh.sec | 769 + res/Sectors/Zhodani Core Route/aed.sec | 758 + res/Sectors/Zhodani Core Route/afdranz.sec | 418 + res/Sectors/Zhodani Core Route/anshnzhi.sec | 355 + res/Sectors/Zhodani Core Route/antsrish.sec | 358 + res/Sectors/Zhodani Core Route/aplie.sec | 320 + res/Sectors/Zhodani Core Route/aviel.sec | 795 + res/Sectors/Zhodani Core Route/azaz.sec | 530 + res/Sectors/Zhodani Core Route/azhpliab.sec | 806 + res/Sectors/Zhodani Core Route/biaqrpra.sec | 335 + res/Sectors/Zhodani Core Route/bliaazh.sec | 342 + res/Sectors/Zhodani Core Route/bliatsie.sec | 850 + res/Sectors/Zhodani Core Route/brelia.sec | 807 + res/Sectors/Zhodani Core Route/brianch.sec | 259 + res/Sectors/Zhodani Core Route/chetlias.sec | 791 + res/Sectors/Zhodani Core Route/chiajavl.sec | 1027 ++ res/Sectors/Zhodani Core Route/chtatiat.sec | 352 + res/Sectors/Zhodani Core Route/chtetara.sec | 804 + res/Sectors/Zhodani Core Route/chtijash.sec | 760 + res/Sectors/Zhodani Core Route/difralfl.sec | 437 + res/Sectors/Zhodani Core Route/dlavvrep.sec | 780 + res/Sectors/Zhodani Core Route/dliaprqr.sec | 1061 ++ res/Sectors/Zhodani Core Route/dliea.sec | 435 + res/Sectors/Zhodani Core Route/dliebria.sec | 324 + res/Sectors/Zhodani Core Route/doklzots.sec | 810 + res/Sectors/Zhodani Core Route/drachabr.sec | 771 + res/Sectors/Zhodani Core Route/driaflfi.sec | 292 + res/Sectors/Zhodani Core Route/drieblie.sec | 351 + res/Sectors/Zhodani Core Route/drierii.sec | 793 + res/Sectors/Zhodani Core Route/driklfet.sec | 780 + res/Sectors/Zhodani Core Route/eble.sec | 326 + res/Sectors/Zhodani Core Route/eblia.sec | 343 + res/Sectors/Zhodani Core Route/ee.sec | 302 + res/Sectors/Zhodani Core Route/eie.sec | 752 + res/Sectors/Zhodani Core Route/ekrbrian.sec | 314 + res/Sectors/Zhodani Core Route/enchakl.sec | 404 + res/Sectors/Zhodani Core Route/etlnchof.sec | 803 + res/Sectors/Zhodani Core Route/evred.sec | 331 + res/Sectors/Zhodani Core Route/ezhebl.sec | 810 + res/Sectors/Zhodani Core Route/favkrani.sec | 829 + res/Sectors/Zhodani Core Route/febiaiad.sec | 355 + res/Sectors/Zhodani Core Route/feflolen.sec | 740 + res/Sectors/Zhodani Core Route/fiaftash.sec | 818 + res/Sectors/Zhodani Core Route/flablent.sec | 259 + res/Sectors/Zhodani Core Route/flaflzhe.sec | 344 + res/Sectors/Zhodani Core Route/frajbrie.sec | 344 + res/Sectors/Zhodani Core Route/fraklbiz.sec | 1022 ++ res/Sectors/Zhodani Core Route/fretvlob.sec | 420 + res/Sectors/Zhodani Core Route/frizhkla.sec | 823 + res/Sectors/Zhodani Core Route/ia.sec | 842 + res/Sectors/Zhodani Core Route/ianse.sec | 788 + res/Sectors/Zhodani Core Route/iate.sec | 817 + res/Sectors/Zhodani Core Route/ichnzhip.sec | 334 + res/Sectors/Zhodani Core Route/idekl.sec | 356 + res/Sectors/Zhodani Core Route/iebtladl.sec | 348 + res/Sectors/Zhodani Core Route/iekrqre.sec | 351 + res/Sectors/Zhodani Core Route/ienjaz.sec | 288 + res/Sectors/Zhodani Core Route/ienza.sec | 320 + res/Sectors/Zhodani Core Route/ieviash.sec | 812 + res/Sectors/Zhodani Core Route/ijar.sec | 335 + res/Sectors/Zhodani Core Route/ijidl.sec | 360 + res/Sectors/Zhodani Core Route/inshzish.sec | 807 + res/Sectors/Zhodani Core Route/itsfiebl.sec | 391 + res/Sectors/Zhodani Core Route/jdeblins.sec | 811 + res/Sectors/Zhodani Core Route/jdenchep.sec | 802 + res/Sectors/Zhodani Core Route/jdibrirv.sec | 331 + res/Sectors/Zhodani Core Route/jdodiafl.sec | 793 + res/Sectors/Zhodani Core Route/jiechnzh.sec | 336 + res/Sectors/Zhodani Core Route/kazhekli.sec | 1058 ++ res/Sectors/Zhodani Core Route/kepridln.sec | 346 + res/Sectors/Zhodani Core Route/kernzhez.sec | 773 + res/Sectors/Zhodani Core Route/kiafldli.sec | 277 + res/Sectors/Zhodani Core Route/klajipl.sec | 1033 ++ res/Sectors/Zhodani Core Route/kliadli.sec | 348 + res/Sectors/Zhodani Core Route/kozhdanz.sec | 339 + res/Sectors/Zhodani Core Route/loplia.sec | 317 + res/Sectors/Zhodani Core Route/lotsar.sec | 353 + res/Sectors/Zhodani Core Route/nchadla.sec | 1024 ++ res/Sectors/Zhodani Core Route/nchefrij.sec | 1047 ++ res/Sectors/Zhodani Core Route/nevlieri.sec | 834 + res/Sectors/Zhodani Core Route/nezhatli.sec | 1044 ++ res/Sectors/Zhodani Core Route/niabrefv.sec | 333 + res/Sectors/Zhodani Core Route/nialkrid.sec | 392 + res/Sectors/Zhodani Core Route/niechnch.sec | 320 + res/Sectors/Zhodani Core Route/nili.sec | 796 + res/Sectors/Zhodani Core Route/nseianzh.sec | 776 + res/Sectors/Zhodani Core Route/nshofref.sec | 778 + res/Sectors/Zhodani Core Route/ntetszif.sec | 329 + res/Sectors/Zhodani Core Route/nzhajdib.sec | 775 + res/Sectors/Zhodani Core Route/nzhenzie.sec | 784 + res/Sectors/Zhodani Core Route/nzhernzh.sec | 789 + res/Sectors/Zhodani Core Route/nzhiaapl.sec | 794 + res/Sectors/Zhodani Core Route/nzievi.sec | 1063 ++ res/Sectors/Zhodani Core Route/out.xml | 1002 ++ res/Sectors/Zhodani Core Route/peklproz.sec | 351 + res/Sectors/Zhodani Core Route/pivchavi.sec | 809 + res/Sectors/Zhodani Core Route/plablsha.sec | 395 + res/Sectors/Zhodani Core Route/pliajenz.sec | 800 + res/Sectors/Zhodani Core Route/pliantib.sec | 385 + res/Sectors/Zhodani Core Route/ploblvla.sec | 733 + res/Sectors/Zhodani Core Route/plontarb.sec | 819 + res/Sectors/Zhodani Core Route/pofdribl.sec | 816 + res/Sectors/Zhodani Core Route/praklfri.sec | 813 + res/Sectors/Zhodani Core Route/pranzakl.sec | 1026 ++ res/Sectors/Zhodani Core Route/prashtsi.sec | 773 + res/Sectors/Zhodani Core Route/preiel.sec | 1031 ++ res/Sectors/Zhodani Core Route/prinjobl.sec | 833 + res/Sectors/Zhodani Core Route/proanz.sec | 815 + res/Sectors/Zhodani Core Route/qlivinch.sec | 358 + res/Sectors/Zhodani Core Route/qrecha.sec | 767 + res/Sectors/Zhodani Core Route/qrefrnzh.sec | 769 + res/Sectors/Zhodani Core Route/ravrazhe.sec | 331 + res/Sectors/Zhodani Core Route/riadrazh.sec | 336 + res/Sectors/Zhodani Core Route/riafrtaf.sec | 773 + res/Sectors/Zhodani Core Route/riekllez.sec | 409 + res/Sectors/Zhodani Core Route/setjefrs.sec | 268 + res/Sectors/Zhodani Core Route/shenzhts.sec | 436 + res/Sectors/Zhodani Core Route/sheqrafl.sec | 771 + res/Sectors/Zhodani Core Route/shiadldr.sec | 772 + res/Sectors/Zhodani Core Route/shiazhan.sec | 783 + res/Sectors/Zhodani Core Route/shiblbro.sec | 344 + res/Sectors/Zhodani Core Route/siantsai.sec | 1022 ++ res/Sectors/Zhodani Core Route/sieriats.sec | 329 + res/Sectors/Zhodani Core Route/stiaklen.sec | 392 + res/Sectors/Zhodani Core Route/stienzar.sec | 1040 ++ res/Sectors/Zhodani Core Route/stotsshi.sec | 743 + res/Sectors/Zhodani Core Route/tatlnjai.sec | 810 + res/Sectors/Zhodani Core Route/tavrbovl.sec | 506 + res/Sectors/Zhodani Core Route/tenshlat.sec | 320 + res/Sectors/Zhodani Core Route/tlabrapl.sec | 386 + res/Sectors/Zhodani Core Route/tlapltsi.sec | 772 + res/Sectors/Zhodani Core Route/tledrbre.sec | 335 + res/Sectors/Zhodani Core Route/tlensbla.sec | 772 + res/Sectors/Zhodani Core Route/tlialied.sec | 1014 ++ res/Sectors/Zhodani Core Route/tliblnzh.sec | 1028 ++ res/Sectors/Zhodani Core Route/tlinchji.sec | 817 + res/Sectors/Zhodani Core Route/tsafrnch.sec | 271 + res/Sectors/Zhodani Core Route/tsebntsi.sec | 887 ++ res/Sectors/Zhodani Core Route/tselidl.sec | 784 + res/Sectors/Zhodani Core Route/vedrabr.sec | 267 + res/Sectors/Zhodani Core Route/viashchi.sec | 424 + res/Sectors/Zhodani Core Route/vietrien.sec | 681 + res/Sectors/Zhodani Core Route/vikrvidr.sec | 806 + res/Sectors/Zhodani Core Route/vlofrkla.sec | 322 + res/Sectors/Zhodani Core Route/voblezit.sec | 807 + res/Sectors/Zhodani Core Route/vravrzha.sec | 376 + res/Sectors/Zhodani Core Route/yejiarie.sec | 1163 ++ res/Sectors/Zhodani Core Route/yikvrior.sec | 395 + res/Sectors/Zhodani Core Route/zabrzikl.sec | 779 + res/Sectors/Zhodani Core Route/zdafeflz.sec | 797 + res/Sectors/Zhodani Core Route/zdechreb.sec | 1026 ++ res/Sectors/Zhodani Core Route/zdielbep.sec | 1020 ++ res/Sectors/Zhodani Core Route/zdienzfi.sec | 340 + res/Sectors/Zhodani Core Route/zdietabl.sec | 808 + res/Sectors/Zhodani Core Route/zentse.sec | 326 + res/Sectors/Zhodani Core Route/zenzhej.sec | 777 + res/Sectors/Zhodani Core Route/zeshavra.sec | 335 + res/Sectors/Zhodani Core Route/zhechinj.sec | 789 + res/Sectors/Zhodani Core Route/zhejnzhi.sec | 791 + res/Sectors/Zhodani Core Route/zhenzhbr.sec | 816 + res/Sectors/Zhodani Core Route/zhetsefr.sec | 792 + res/Sectors/Zhodani Core Route/zhevrekl.sec | 402 + res/Sectors/Zhodani Core Route/zhiatien.sec | 802 + res/Sectors/Zhodani Core Route/zhiejqri.sec | 1007 ++ res/Sectors/Zhodani Core Route/zietsebl.sec | 795 + res/Sectors/Zhodani Core Route/zietssha.sec | 837 + res/Sectors/Zhodani Core Route/zii.sec | 805 + res/Sectors/Ziafrplians.tab | 485 + res/Sectors/Zortakh.sec | 362 + res/Sectors/afawahisa.sec | 198 + res/Sectors/aldebaran.sec | 333 + res/Sectors/amdukan.sec | 449 + res/Sectors/arzul.sec | 349 + res/Sectors/astron.sec | 301 + res/Sectors/ataurre.sec | 1027 ++ res/Sectors/banners.sec | 297 + res/Sectors/beyond.sec | 450 + res/Sectors/blaskon.sec | 447 + res/Sectors/blaskon.xml | 58 + res/Sectors/canopus.sec | 424 + res/Sectors/centrax.sec | 443 + res/Sectors/chtierab.sec | 962 ++ res/Sectors/darknebula.sec | 499 + res/Sectors/dupefreq.py | 37 + res/Sectors/esai-yo.sec | 202 + res/Sectors/extolian.xml | 90 + res/Sectors/faoheiroi-iyhao.sec | 52 + res/Sectors/farfrontiers.sec | 635 + res/Sectors/foreven.sec | 646 + res/Sectors/ftaoiyekyu.sec | 87 + res/Sectors/fulani.sec | 407 + res/Sectors/gashikan.sec | 603 + res/Sectors/gushmege.sec | 543 + res/Sectors/hadji.sec | 568 + res/Sectors/hanstone.sec | 357 + res/Sectors/hkakhaeaw.sec | 81 + res/Sectors/hlakhoi.sec | 401 + res/Sectors/iphigenaia.sec | 296 + res/Sectors/iwahfuah.sec | 418 + res/Sectors/karleaya.sec | 604 + res/Sectors/khaeaw.sec | 51 + res/Sectors/kidunal.sec | 432 + res/Sectors/kidunal.xml | 78 + res/Sectors/lancask.sec | 894 ++ res/Sectors/langere.sec | 242 + res/Sectors/legend.sec | 57 + res/Sectors/malorn.sec | 674 + res/Sectors/meshan.sec | 492 + res/Sectors/mikhail.sec | 371 + res/Sectors/mikhail.xml | 91 + res/Sectors/muarne.sec | 908 ++ res/Sectors/neworld.sec | 323 + res/Sectors/provence.sec | 438 + res/Sectors/spica.sec | 472 + res/Sectors/staihaia.sec | 480 + res/Sectors/star-s_end.sec | 183 + res/Sectors/storr.sec | 556 + res/Sectors/tienspevnekr.sec | 656 + res/Sectors/touchstone.sec | 190 + res/Sectors/trenchans.sec | 413 + res/Sectors/tuglikki.sec | 377 + res/Sectors/ustral_quadrant.sec | 405 + res/Sectors/vanguard_reaches.sec | 306 + res/Sectors/waroatah.sec | 607 + res/Sectors/windhorn.sec | 225 + res/Sectors/yiklerzdanzh.sec | 518 + res/Sectors/zdiedeia.sec | 861 ++ res/Sectors/zhdant.sec | 568 + res/Vectors/Aslan.xml | 849 + res/Vectors/CoreRoute.xml | 26 + res/Vectors/DelphiRift.xml | 97 + res/Vectors/Galaxy_Positive.xvf | 12892 ++++++++++++++++ res/Vectors/GreatRift.xml | 561 + res/Vectors/Hive.xml | 445 + res/Vectors/Imperium.xml | 2191 +++ res/Vectors/J4Route.xml | 34 + res/Vectors/J5Route.xml | 34 + res/Vectors/Kkree.xml | 785 + res/Vectors/LesserRift.xml | 481 + res/Vectors/RimwardClient.xml | 1752 +++ res/Vectors/Solomani.xml | 313 + res/Vectors/SpinwardClient.xml | 3905 +++++ res/Vectors/TrailingClient.xml | 3508 +++++ res/Vectors/Vargr.xml | 5453 +++++++ res/Vectors/WindhornRift.xml | 57 + res/Vectors/ZhdantRift.xml | 32 + res/Vectors/Zhodani.xml | 999 ++ res/Worlds.xml | 72 + res/ZhodaniCoreRoute.xml | 7917 ++++++++++ res/credits.js | 48 + res/credits.xslt | 112 + res/search/ArrivalVengeance.json | 21 + res/search/ArrivalVengeance.xml | 24 + res/search/Default.json | 13 + res/search/Default.xml | 14 + res/search/FarFrontiers.json | 13 + res/search/FarFrontiers.xml | 15 + res/search/GrandTour.json | 24 + res/search/GrandTour.xml | 47 + res/sectors.xml | 9441 +++++++++++ res/sectors.xsd | 180 + res/ui/CT_Logo_Color.png | Bin 0 -> 2349 bytes res/ui/CT_Logo_Gray.png | Bin 0 -> 2486 bytes res/ui/Legend_1003_atlas.png | Bin 0 -> 7601 bytes res/ui/Legend_1003_candy.png | Bin 0 -> 16674 bytes res/ui/Legend_1003_poster.png | Bin 0 -> 7644 bytes res/ui/Legend_1003_print.png | Bin 0 -> 7802 bytes res/ui/Legend_1006_atlas.png | Bin 0 -> 7267 bytes res/ui/Legend_1006_candy.png | Bin 0 -> 16931 bytes res/ui/Legend_1006_poster.png | Bin 0 -> 7262 bytes res/ui/Legend_1006_print.png | Bin 0 -> 7327 bytes res/ui/ajax-loader.gif | Bin 0 -> 8787 bytes res/ui/fb.png | Bin 0 -> 60094 bytes res/ui/grab.cur | Bin 0 -> 326 bytes res/ui/grabbing.cur | Bin 0 -> 326 bytes res/ui/legend.html | 25 + res/ui/sector-spinner.gif | Bin 0 -> 673 bytes res/ui/youarehere.gif | Bin 0 -> 3042 bytes res/ui/youarehere.pdn | Bin 0 -> 57801 bytes res/ui/youarehere_gray.gif | Bin 0 -> 1473 bytes sector.htm | 840 + sector.js | 282 + serialization/ColumnUtils.cs | 214 + serialization/MSECParser.cs | 226 + serialization/MSECWriter.cs | 270 + serialization/SectorMetadataParser.cs | 71 + serialization/SectorMetadataSerializer.cs | 33 + serialization/SectorParser.cs | 340 + serialization/SectorWriter.cs | 168 + site.css | 38 + subsector.htm | 170 + test/APITest.html | 218 + test/APITest.js | 142 + test/ImageTest.html | 193 + test/ref1.png | Bin 0 -> 38388 bytes test/ref10.png | Bin 0 -> 21008 bytes test/ref11.png | Bin 0 -> 11538 bytes test/ref12.png | Bin 0 -> 16851 bytes test/ref13.png | Bin 0 -> 18765 bytes test/ref14.png | Bin 0 -> 19153 bytes test/ref15.png | Bin 0 -> 20198 bytes test/ref16.png | Bin 0 -> 18950 bytes test/ref17.png | Bin 0 -> 11981 bytes test/ref18.png | Bin 0 -> 7029 bytes test/ref19.png | Bin 0 -> 26119 bytes test/ref2.png | Bin 0 -> 1043942 bytes test/ref20.png | Bin 0 -> 23776 bytes test/ref21.png | Bin 0 -> 26072 bytes test/ref22.png | Bin 0 -> 23716 bytes test/ref23.png | Bin 0 -> 66059 bytes test/ref24.png | Bin 0 -> 71268 bytes test/ref25.png | Bin 0 -> 21713 bytes test/ref26.png | Bin 0 -> 68382 bytes test/ref27.png | Bin 0 -> 70950 bytes test/ref28.jpg | Bin 0 -> 98117 bytes test/ref29.png | Bin 0 -> 80109 bytes test/ref3.png | Bin 0 -> 71309 bytes test/ref30.png | Bin 0 -> 80577 bytes test/ref31.png | Bin 0 -> 381183 bytes test/ref32.png | Bin 0 -> 4813 bytes test/ref4.png | Bin 0 -> 104000 bytes test/ref5.png | Bin 0 -> 112195 bytes test/ref6.png | Bin 0 -> 100785 bytes test/ref7.png | Bin 0 -> 66431 bytes test/ref8.png | Bin 0 -> 32562 bytes test/ref9.png | Bin 0 -> 23912 bytes test/ref_bad_example.png | Bin 0 -> 24777 bytes touch/add-to-home-screen/add2home.css | 167 + touch/add-to-home-screen/add2home.js | 342 + touch/background.png | Bin 0 -> 100 bytes touch/background_light.png | Bin 0 -> 257 bytes touch/box.jpg | Bin 0 -> 78917 bytes touch/icon.png | Bin 0 -> 1890 bytes touch/index.html | 108 + touch/info_button.png | Bin 0 -> 1572 bytes touch/logo.png | Bin 0 -> 4697 bytes touch/logo_gray.png | Bin 0 -> 6296 bytes touch/regina114.png | Bin 0 -> 7749 bytes touch/regina144.png | Bin 0 -> 5520 bytes touch/regina57.png | Bin 0 -> 3079 bytes touch/regina72.png | Bin 0 -> 3391 bytes touch/startup-1496x2048.jpg | Bin 0 -> 445873 bytes touch/startup-1536x2008.jpg | Bin 0 -> 447495 bytes touch/startup-320x460.jpg | Bin 0 -> 63494 bytes touch/startup-640x1096.jpg | Bin 0 -> 166574 bytes touch/startup-640x920.jpg | Bin 0 -> 158195 bytes touch/startup-748x1024.jpg | Bin 0 -> 181586 bytes touch/startup-768x1004.jpg | Bin 0 -> 182318 bytes unittests/UnitTests/JsonTest.cs | 24 + .../UnitTests/Properties/AssemblyInfo.cs | 36 + unittests/UnitTests/SerializationTest.cs | 169 + unittests/UnitTests/UnitTests.csproj | 98 + unittests/UnitTests/UtilTest.cs | 57 + 587 files changed, 265934 insertions(+) create mode 100644 .gitignore create mode 100644 Admin.aspx create mode 100644 Admin.aspx.cs create mode 100644 AssemblyInfo.cs create mode 100644 Astrometrics.cs create mode 100644 Codes.aspx create mode 100644 Codes.aspx.cs create mode 100644 Coordinates.aspx create mode 100644 Coordinates.aspx.cs create mode 100644 Credits.aspx create mode 100644 Credits.aspx.cs create mode 100644 DataPage.cs create mode 100644 Dump.aspx create mode 100644 Dump.aspx.cs create mode 100644 Errors.aspx create mode 100644 Errors.aspx.cs create mode 100644 Global.asax create mode 100644 Global.asax.cs create mode 100644 ImageGeneratorPage.cs create mode 100644 Json.cs create mode 100644 JumpMap.aspx create mode 100644 JumpMap.aspx.cs create mode 100644 JumpWorlds.aspx create mode 100644 JumpWorlds.aspx.cs create mode 100644 LICENSE.md create mode 100644 Location.cs create mode 100644 MSEC.aspx create mode 100644 MSEC.aspx.cs create mode 100644 Maps.csproj create mode 100644 Maps.sln create mode 100644 Metadata.cs create mode 100644 Mobile.aspx create mode 100644 Mobile.aspx.cs create mode 100644 Overview.aspx create mode 100644 Overview.aspx.cs create mode 100644 Poster.aspx create mode 100644 Poster.aspx.cs create mode 100644 Render.cs create mode 100644 RenderUtil.cs create mode 100644 ResourceManager.cs create mode 100644 SEC.aspx create mode 100644 SEC.aspx.cs create mode 100644 Search.aspx create mode 100644 Search.aspx.cs create mode 100644 SearchEngine.cs create mode 100644 Sector.cs create mode 100644 SectorDataCollection.cs create mode 100644 SectorMetaData.aspx create mode 100644 SectorMetaData.aspx.cs create mode 100644 Selector.cs create mode 100644 ServiceConfiguration.cs create mode 100644 StellarData.cs create mode 100644 Stylesheet.cs create mode 100644 Tile.aspx create mode 100644 Tile.aspx.cs create mode 100644 Universe.aspx create mode 100644 Universe.aspx.cs create mode 100644 Util.cs create mode 100644 VectorObject.cs create mode 100644 World.cs create mode 100644 api.htm create mode 100644 borders/bd_images/image001.gif create mode 100644 borders/bd_images/image003.jpg create mode 100644 borders/bd_images/image004.gif create mode 100644 borders/bd_images/image005.gif create mode 100644 borders/bd_images/image006.gif create mode 100644 borders/bd_images/image007.gif create mode 100644 borders/bd_images/image008.gif create mode 100644 borders/bd_images/image009.gif create mode 100644 borders/bd_images/image010.gif create mode 100644 borders/bd_images/image011.gif create mode 100644 borders/bd_images/image012.gif create mode 100644 borders/bd_images/image013.gif create mode 100644 borders/bd_images/image014.gif create mode 100644 borders/bd_images/image016.jpg create mode 100644 borders/bd_images/image018.jpg create mode 100644 borders/bd_images/image019.gif create mode 100644 borders/bd_images/image020.gif create mode 100644 borders/bd_images/image021.gif create mode 100644 borders/bd_images/image022.gif create mode 100644 borders/bd_images/image023.gif create mode 100644 borders/borders.js create mode 100644 borders/default.htm create mode 100644 borders/demo.htm create mode 100644 borders/doc.htm create mode 100644 borders/regen.htm create mode 100644 credits.htm create mode 100644 favicon.ico create mode 100644 formats.htm create mode 100644 handlebars.js create mode 100644 iframe.htm create mode 100644 index.html create mode 100644 index.js create mode 100644 jpost.htm create mode 100644 map.css create mode 100644 map.js create mode 100644 polyfill/harmony.js create mode 100644 polyfill/polyfill.js create mode 100644 post.htm create mode 100644 post.js create mode 100644 res/AprilFools/Starburst.png create mode 100644 res/AprilFools/Starburst_Gray.png create mode 100644 res/Candy/Belt.png create mode 100644 res/Candy/Galaxy.pdn create mode 100644 res/Candy/Galaxy.png create mode 100644 res/Candy/Hyd0.png create mode 100644 res/Candy/Hyd1.png create mode 100644 res/Candy/Hyd2.png create mode 100644 res/Candy/Hyd3.png create mode 100644 res/Candy/Hyd4.png create mode 100644 res/Candy/Hyd5.png create mode 100644 res/Candy/Hyd6.png create mode 100644 res/Candy/Hyd7.png create mode 100644 res/Candy/Hyd8.png create mode 100644 res/Candy/Hyd9.png create mode 100644 res/Candy/HydA.png create mode 100644 res/Candy/Nebula.jpg create mode 100644 res/Candy/Nebula.png create mode 100644 res/Candy/Rifts.png create mode 100644 res/Candy/tiled_nebula_256.jpg create mode 100644 res/Candy/tiled_nebula_512.jpg create mode 100644 res/Sectors/Aerenfors.sec create mode 100644 res/Sectors/Ahkiweahi.sec create mode 100644 res/Sectors/Aktifao.sec create mode 100644 res/Sectors/Alpha Crucis.tab create mode 100644 res/Sectors/Amderstun.sec create mode 100644 res/Sectors/Angfutsag.sec create mode 100644 res/Sectors/Antares.tab create mode 100644 res/Sectors/Avereguar.sec create mode 100644 res/Sectors/Avereguar.xml create mode 100644 res/Sectors/Centrax.xml create mode 100644 res/Sectors/Core.tab create mode 100644 res/Sectors/Corridor.tab create mode 100644 res/Sectors/CrucisMargin.sec create mode 100644 res/Sectors/DARK1120.SEC create mode 100644 res/Sectors/Dagudashaag.tab create mode 100644 res/Sectors/Daibei.tab create mode 100644 res/Sectors/Darret.sec create mode 100644 res/Sectors/Datsatl.sec create mode 100644 res/Sectors/Delphi.tab create mode 100644 res/Sectors/Deneb.tab create mode 100644 res/Sectors/Dfotseth.sec create mode 100644 res/Sectors/Dhuerorrg.sec create mode 100644 res/Sectors/Diaspora.tab create mode 100644 res/Sectors/Drakken.sec create mode 100644 res/Sectors/Ealiyasiyw.sec create mode 100644 res/Sectors/Empty Quarter.tab create mode 100644 res/Sectors/Etakhasoa.sec create mode 100644 res/Sectors/Extolian.sec create mode 100644 res/Sectors/Fa Dzaets.sec create mode 100644 res/Sectors/Fahreahluis.sec create mode 100644 res/Sectors/FarFrontiers.xml create mode 100644 res/Sectors/Finggvakhou.sec create mode 100644 res/Sectors/Folgore.sec create mode 100644 res/Sectors/Folgore.xml create mode 100644 res/Sectors/Fornast.tab create mode 100644 res/Sectors/Gakghang.sec create mode 100644 res/Sectors/Gateway.sec create mode 100644 res/Sectors/Gelath.sec create mode 100644 res/Sectors/Gh1hken.sec create mode 100644 res/Sectors/Ghoekhnael.sec create mode 100644 res/Sectors/Glimmerdrift.tab create mode 100644 res/Sectors/Grikrng.sec create mode 100644 res/Sectors/Gushemege.tab create mode 100644 res/Sectors/Gvurrdon.tab create mode 100644 res/Sectors/Gzaekfueg.sec create mode 100644 res/Sectors/Gzirr!k'l.sec create mode 100644 res/Sectors/Gzirr!k'l.xml create mode 100644 res/Sectors/Harea.sec create mode 100644 res/Sectors/Heakhafaw.sec create mode 100644 res/Sectors/Hfiywitir.sec create mode 100644 res/Sectors/Hinterworlds.tab create mode 100644 res/Sectors/Ilelish.tab create mode 100644 res/Sectors/Irlaftalea.sec create mode 100644 res/Sectors/Irugangong.sec create mode 100644 res/Sectors/K'trekreer.sec create mode 100644 res/Sectors/K'trekreer.xml create mode 100644 res/Sectors/Kaa G!kul.sec create mode 100644 res/Sectors/Katoonah.sec create mode 100644 res/Sectors/Kefiykhta.sec create mode 100644 res/Sectors/KfazzGhik.sec create mode 100644 res/Sectors/Kharrthon.sec create mode 100644 res/Sectors/Knaeleng.sec create mode 100644 res/Sectors/Knoellighz.sec create mode 100644 res/Sectors/Ksinanirz.sec create mode 100644 res/Sectors/Ktiingzat.sec create mode 100644 res/Sectors/Leonidae.sec create mode 100644 res/Sectors/Ley.tab create mode 100644 res/Sectors/Lishun.tab create mode 100644 res/Sectors/Listanaya.sec create mode 100644 res/Sectors/Lorspane.sec create mode 100644 res/Sectors/Magyar.tab create mode 100644 res/Sectors/Massilia.tab create mode 100644 res/Sectors/Mendan.sec create mode 100644 res/Sectors/Ngathksirz.sec create mode 100644 res/Sectors/Nooq.sec create mode 100644 res/Sectors/Nooq.xml create mode 100644 res/Sectors/OeghzVaerrghr.sec create mode 100644 res/Sectors/Ogadogorz.sec create mode 100644 res/Sectors/Ohieraoi.sec create mode 100644 res/Sectors/Old Expanses.tab create mode 100644 res/Sectors/Phlask.sec create mode 100644 res/Sectors/Pliabriebl.sec create mode 100644 res/Sectors/Porlock.sec create mode 100644 res/Sectors/Porlock.xml create mode 100644 res/Sectors/Reavers Deep.tab create mode 100644 res/Sectors/Reft.tab create mode 100644 res/Sectors/Rfigh.sec create mode 100644 res/Sectors/Ricenden.sec create mode 100644 res/Sectors/Ricenden.xml create mode 100644 res/Sectors/Riftspan Reaches.sec create mode 100644 res/Sectors/Rzakki.sec create mode 100644 res/Sectors/Solomani Rim.tab create mode 100644 res/Sectors/Spinward Marches.tab create mode 100644 res/Sectors/TarGkellp.sec create mode 100644 res/Sectors/Teahloarifu.sec create mode 100644 res/Sectors/ThakuFung.sec create mode 100644 res/Sectors/Theron.sec create mode 100644 res/Sectors/Theta Borealis.sec create mode 100644 res/Sectors/Theta Borealis.xml create mode 100644 res/Sectors/Tlyasea.sec create mode 100644 res/Sectors/Trojan Reach.tab create mode 100644 res/Sectors/Ugoede.sec create mode 100644 res/Sectors/Uistilrao.sec create mode 100644 res/Sectors/Ukaarriitb.sec create mode 100644 res/Sectors/VegVergakh.sec create mode 100644 res/Sectors/Verge.tab create mode 100644 res/Sectors/Vland.tab create mode 100644 res/Sectors/Wrenton.sec create mode 100644 res/Sectors/Wrenton.xml create mode 100644 res/Sectors/Yahehwe.sec create mode 100644 res/Sectors/ZaoKfengIgGrilokh.sec create mode 100644 res/Sectors/Zarushagar.tab create mode 100644 res/Sectors/Zhodani Core Route/ablaz.sec create mode 100644 res/Sectors/Zhodani Core Route/abresh.sec create mode 100644 res/Sectors/Zhodani Core Route/aed.sec create mode 100644 res/Sectors/Zhodani Core Route/afdranz.sec create mode 100644 res/Sectors/Zhodani Core Route/anshnzhi.sec create mode 100644 res/Sectors/Zhodani Core Route/antsrish.sec create mode 100644 res/Sectors/Zhodani Core Route/aplie.sec create mode 100644 res/Sectors/Zhodani Core Route/aviel.sec create mode 100644 res/Sectors/Zhodani Core Route/azaz.sec create mode 100644 res/Sectors/Zhodani Core Route/azhpliab.sec create mode 100644 res/Sectors/Zhodani Core Route/biaqrpra.sec create mode 100644 res/Sectors/Zhodani Core Route/bliaazh.sec create mode 100644 res/Sectors/Zhodani Core Route/bliatsie.sec create mode 100644 res/Sectors/Zhodani Core Route/brelia.sec create mode 100644 res/Sectors/Zhodani Core Route/brianch.sec create mode 100644 res/Sectors/Zhodani Core Route/chetlias.sec create mode 100644 res/Sectors/Zhodani Core Route/chiajavl.sec create mode 100644 res/Sectors/Zhodani Core Route/chtatiat.sec create mode 100644 res/Sectors/Zhodani Core Route/chtetara.sec create mode 100644 res/Sectors/Zhodani Core Route/chtijash.sec create mode 100644 res/Sectors/Zhodani Core Route/difralfl.sec create mode 100644 res/Sectors/Zhodani Core Route/dlavvrep.sec create mode 100644 res/Sectors/Zhodani Core Route/dliaprqr.sec create mode 100644 res/Sectors/Zhodani Core Route/dliea.sec create mode 100644 res/Sectors/Zhodani Core Route/dliebria.sec create mode 100644 res/Sectors/Zhodani Core Route/doklzots.sec create mode 100644 res/Sectors/Zhodani Core Route/drachabr.sec create mode 100644 res/Sectors/Zhodani Core Route/driaflfi.sec create mode 100644 res/Sectors/Zhodani Core Route/drieblie.sec create mode 100644 res/Sectors/Zhodani Core Route/drierii.sec create mode 100644 res/Sectors/Zhodani Core Route/driklfet.sec create mode 100644 res/Sectors/Zhodani Core Route/eble.sec create mode 100644 res/Sectors/Zhodani Core Route/eblia.sec create mode 100644 res/Sectors/Zhodani Core Route/ee.sec create mode 100644 res/Sectors/Zhodani Core Route/eie.sec create mode 100644 res/Sectors/Zhodani Core Route/ekrbrian.sec create mode 100644 res/Sectors/Zhodani Core Route/enchakl.sec create mode 100644 res/Sectors/Zhodani Core Route/etlnchof.sec create mode 100644 res/Sectors/Zhodani Core Route/evred.sec create mode 100644 res/Sectors/Zhodani Core Route/ezhebl.sec create mode 100644 res/Sectors/Zhodani Core Route/favkrani.sec create mode 100644 res/Sectors/Zhodani Core Route/febiaiad.sec create mode 100644 res/Sectors/Zhodani Core Route/feflolen.sec create mode 100644 res/Sectors/Zhodani Core Route/fiaftash.sec create mode 100644 res/Sectors/Zhodani Core Route/flablent.sec create mode 100644 res/Sectors/Zhodani Core Route/flaflzhe.sec create mode 100644 res/Sectors/Zhodani Core Route/frajbrie.sec create mode 100644 res/Sectors/Zhodani Core Route/fraklbiz.sec create mode 100644 res/Sectors/Zhodani Core Route/fretvlob.sec create mode 100644 res/Sectors/Zhodani Core Route/frizhkla.sec create mode 100644 res/Sectors/Zhodani Core Route/ia.sec create mode 100644 res/Sectors/Zhodani Core Route/ianse.sec create mode 100644 res/Sectors/Zhodani Core Route/iate.sec create mode 100644 res/Sectors/Zhodani Core Route/ichnzhip.sec create mode 100644 res/Sectors/Zhodani Core Route/idekl.sec create mode 100644 res/Sectors/Zhodani Core Route/iebtladl.sec create mode 100644 res/Sectors/Zhodani Core Route/iekrqre.sec create mode 100644 res/Sectors/Zhodani Core Route/ienjaz.sec create mode 100644 res/Sectors/Zhodani Core Route/ienza.sec create mode 100644 res/Sectors/Zhodani Core Route/ieviash.sec create mode 100644 res/Sectors/Zhodani Core Route/ijar.sec create mode 100644 res/Sectors/Zhodani Core Route/ijidl.sec create mode 100644 res/Sectors/Zhodani Core Route/inshzish.sec create mode 100644 res/Sectors/Zhodani Core Route/itsfiebl.sec create mode 100644 res/Sectors/Zhodani Core Route/jdeblins.sec create mode 100644 res/Sectors/Zhodani Core Route/jdenchep.sec create mode 100644 res/Sectors/Zhodani Core Route/jdibrirv.sec create mode 100644 res/Sectors/Zhodani Core Route/jdodiafl.sec create mode 100644 res/Sectors/Zhodani Core Route/jiechnzh.sec create mode 100644 res/Sectors/Zhodani Core Route/kazhekli.sec create mode 100644 res/Sectors/Zhodani Core Route/kepridln.sec create mode 100644 res/Sectors/Zhodani Core Route/kernzhez.sec create mode 100644 res/Sectors/Zhodani Core Route/kiafldli.sec create mode 100644 res/Sectors/Zhodani Core Route/klajipl.sec create mode 100644 res/Sectors/Zhodani Core Route/kliadli.sec create mode 100644 res/Sectors/Zhodani Core Route/kozhdanz.sec create mode 100644 res/Sectors/Zhodani Core Route/loplia.sec create mode 100644 res/Sectors/Zhodani Core Route/lotsar.sec create mode 100644 res/Sectors/Zhodani Core Route/nchadla.sec create mode 100644 res/Sectors/Zhodani Core Route/nchefrij.sec create mode 100644 res/Sectors/Zhodani Core Route/nevlieri.sec create mode 100644 res/Sectors/Zhodani Core Route/nezhatli.sec create mode 100644 res/Sectors/Zhodani Core Route/niabrefv.sec create mode 100644 res/Sectors/Zhodani Core Route/nialkrid.sec create mode 100644 res/Sectors/Zhodani Core Route/niechnch.sec create mode 100644 res/Sectors/Zhodani Core Route/nili.sec create mode 100644 res/Sectors/Zhodani Core Route/nseianzh.sec create mode 100644 res/Sectors/Zhodani Core Route/nshofref.sec create mode 100644 res/Sectors/Zhodani Core Route/ntetszif.sec create mode 100644 res/Sectors/Zhodani Core Route/nzhajdib.sec create mode 100644 res/Sectors/Zhodani Core Route/nzhenzie.sec create mode 100644 res/Sectors/Zhodani Core Route/nzhernzh.sec create mode 100644 res/Sectors/Zhodani Core Route/nzhiaapl.sec create mode 100644 res/Sectors/Zhodani Core Route/nzievi.sec create mode 100644 res/Sectors/Zhodani Core Route/out.xml create mode 100644 res/Sectors/Zhodani Core Route/peklproz.sec create mode 100644 res/Sectors/Zhodani Core Route/pivchavi.sec create mode 100644 res/Sectors/Zhodani Core Route/plablsha.sec create mode 100644 res/Sectors/Zhodani Core Route/pliajenz.sec create mode 100644 res/Sectors/Zhodani Core Route/pliantib.sec create mode 100644 res/Sectors/Zhodani Core Route/ploblvla.sec create mode 100644 res/Sectors/Zhodani Core Route/plontarb.sec create mode 100644 res/Sectors/Zhodani Core Route/pofdribl.sec create mode 100644 res/Sectors/Zhodani Core Route/praklfri.sec create mode 100644 res/Sectors/Zhodani Core Route/pranzakl.sec create mode 100644 res/Sectors/Zhodani Core Route/prashtsi.sec create mode 100644 res/Sectors/Zhodani Core Route/preiel.sec create mode 100644 res/Sectors/Zhodani Core Route/prinjobl.sec create mode 100644 res/Sectors/Zhodani Core Route/proanz.sec create mode 100644 res/Sectors/Zhodani Core Route/qlivinch.sec create mode 100644 res/Sectors/Zhodani Core Route/qrecha.sec create mode 100644 res/Sectors/Zhodani Core Route/qrefrnzh.sec create mode 100644 res/Sectors/Zhodani Core Route/ravrazhe.sec create mode 100644 res/Sectors/Zhodani Core Route/riadrazh.sec create mode 100644 res/Sectors/Zhodani Core Route/riafrtaf.sec create mode 100644 res/Sectors/Zhodani Core Route/riekllez.sec create mode 100644 res/Sectors/Zhodani Core Route/setjefrs.sec create mode 100644 res/Sectors/Zhodani Core Route/shenzhts.sec create mode 100644 res/Sectors/Zhodani Core Route/sheqrafl.sec create mode 100644 res/Sectors/Zhodani Core Route/shiadldr.sec create mode 100644 res/Sectors/Zhodani Core Route/shiazhan.sec create mode 100644 res/Sectors/Zhodani Core Route/shiblbro.sec create mode 100644 res/Sectors/Zhodani Core Route/siantsai.sec create mode 100644 res/Sectors/Zhodani Core Route/sieriats.sec create mode 100644 res/Sectors/Zhodani Core Route/stiaklen.sec create mode 100644 res/Sectors/Zhodani Core Route/stienzar.sec create mode 100644 res/Sectors/Zhodani Core Route/stotsshi.sec create mode 100644 res/Sectors/Zhodani Core Route/tatlnjai.sec create mode 100644 res/Sectors/Zhodani Core Route/tavrbovl.sec create mode 100644 res/Sectors/Zhodani Core Route/tenshlat.sec create mode 100644 res/Sectors/Zhodani Core Route/tlabrapl.sec create mode 100644 res/Sectors/Zhodani Core Route/tlapltsi.sec create mode 100644 res/Sectors/Zhodani Core Route/tledrbre.sec create mode 100644 res/Sectors/Zhodani Core Route/tlensbla.sec create mode 100644 res/Sectors/Zhodani Core Route/tlialied.sec create mode 100644 res/Sectors/Zhodani Core Route/tliblnzh.sec create mode 100644 res/Sectors/Zhodani Core Route/tlinchji.sec create mode 100644 res/Sectors/Zhodani Core Route/tsafrnch.sec create mode 100644 res/Sectors/Zhodani Core Route/tsebntsi.sec create mode 100644 res/Sectors/Zhodani Core Route/tselidl.sec create mode 100644 res/Sectors/Zhodani Core Route/vedrabr.sec create mode 100644 res/Sectors/Zhodani Core Route/viashchi.sec create mode 100644 res/Sectors/Zhodani Core Route/vietrien.sec create mode 100644 res/Sectors/Zhodani Core Route/vikrvidr.sec create mode 100644 res/Sectors/Zhodani Core Route/vlofrkla.sec create mode 100644 res/Sectors/Zhodani Core Route/voblezit.sec create mode 100644 res/Sectors/Zhodani Core Route/vravrzha.sec create mode 100644 res/Sectors/Zhodani Core Route/yejiarie.sec create mode 100644 res/Sectors/Zhodani Core Route/yikvrior.sec create mode 100644 res/Sectors/Zhodani Core Route/zabrzikl.sec create mode 100644 res/Sectors/Zhodani Core Route/zdafeflz.sec create mode 100644 res/Sectors/Zhodani Core Route/zdechreb.sec create mode 100644 res/Sectors/Zhodani Core Route/zdielbep.sec create mode 100644 res/Sectors/Zhodani Core Route/zdienzfi.sec create mode 100644 res/Sectors/Zhodani Core Route/zdietabl.sec create mode 100644 res/Sectors/Zhodani Core Route/zentse.sec create mode 100644 res/Sectors/Zhodani Core Route/zenzhej.sec create mode 100644 res/Sectors/Zhodani Core Route/zeshavra.sec create mode 100644 res/Sectors/Zhodani Core Route/zhechinj.sec create mode 100644 res/Sectors/Zhodani Core Route/zhejnzhi.sec create mode 100644 res/Sectors/Zhodani Core Route/zhenzhbr.sec create mode 100644 res/Sectors/Zhodani Core Route/zhetsefr.sec create mode 100644 res/Sectors/Zhodani Core Route/zhevrekl.sec create mode 100644 res/Sectors/Zhodani Core Route/zhiatien.sec create mode 100644 res/Sectors/Zhodani Core Route/zhiejqri.sec create mode 100644 res/Sectors/Zhodani Core Route/zietsebl.sec create mode 100644 res/Sectors/Zhodani Core Route/zietssha.sec create mode 100644 res/Sectors/Zhodani Core Route/zii.sec create mode 100644 res/Sectors/Ziafrplians.tab create mode 100644 res/Sectors/Zortakh.sec create mode 100644 res/Sectors/afawahisa.sec create mode 100644 res/Sectors/aldebaran.sec create mode 100644 res/Sectors/amdukan.sec create mode 100644 res/Sectors/arzul.sec create mode 100644 res/Sectors/astron.sec create mode 100644 res/Sectors/ataurre.sec create mode 100644 res/Sectors/banners.sec create mode 100644 res/Sectors/beyond.sec create mode 100644 res/Sectors/blaskon.sec create mode 100644 res/Sectors/blaskon.xml create mode 100644 res/Sectors/canopus.sec create mode 100644 res/Sectors/centrax.sec create mode 100644 res/Sectors/chtierab.sec create mode 100644 res/Sectors/darknebula.sec create mode 100644 res/Sectors/dupefreq.py create mode 100644 res/Sectors/esai-yo.sec create mode 100644 res/Sectors/extolian.xml create mode 100644 res/Sectors/faoheiroi-iyhao.sec create mode 100644 res/Sectors/farfrontiers.sec create mode 100644 res/Sectors/foreven.sec create mode 100644 res/Sectors/ftaoiyekyu.sec create mode 100644 res/Sectors/fulani.sec create mode 100644 res/Sectors/gashikan.sec create mode 100644 res/Sectors/gushmege.sec create mode 100644 res/Sectors/hadji.sec create mode 100644 res/Sectors/hanstone.sec create mode 100644 res/Sectors/hkakhaeaw.sec create mode 100644 res/Sectors/hlakhoi.sec create mode 100644 res/Sectors/iphigenaia.sec create mode 100644 res/Sectors/iwahfuah.sec create mode 100644 res/Sectors/karleaya.sec create mode 100644 res/Sectors/khaeaw.sec create mode 100644 res/Sectors/kidunal.sec create mode 100644 res/Sectors/kidunal.xml create mode 100644 res/Sectors/lancask.sec create mode 100644 res/Sectors/langere.sec create mode 100644 res/Sectors/legend.sec create mode 100644 res/Sectors/malorn.sec create mode 100644 res/Sectors/meshan.sec create mode 100644 res/Sectors/mikhail.sec create mode 100644 res/Sectors/mikhail.xml create mode 100644 res/Sectors/muarne.sec create mode 100644 res/Sectors/neworld.sec create mode 100644 res/Sectors/provence.sec create mode 100644 res/Sectors/spica.sec create mode 100644 res/Sectors/staihaia.sec create mode 100644 res/Sectors/star-s_end.sec create mode 100644 res/Sectors/storr.sec create mode 100644 res/Sectors/tienspevnekr.sec create mode 100644 res/Sectors/touchstone.sec create mode 100644 res/Sectors/trenchans.sec create mode 100644 res/Sectors/tuglikki.sec create mode 100644 res/Sectors/ustral_quadrant.sec create mode 100644 res/Sectors/vanguard_reaches.sec create mode 100644 res/Sectors/waroatah.sec create mode 100644 res/Sectors/windhorn.sec create mode 100644 res/Sectors/yiklerzdanzh.sec create mode 100644 res/Sectors/zdiedeia.sec create mode 100644 res/Sectors/zhdant.sec create mode 100644 res/Vectors/Aslan.xml create mode 100644 res/Vectors/CoreRoute.xml create mode 100644 res/Vectors/DelphiRift.xml create mode 100644 res/Vectors/Galaxy_Positive.xvf create mode 100644 res/Vectors/GreatRift.xml create mode 100644 res/Vectors/Hive.xml create mode 100644 res/Vectors/Imperium.xml create mode 100644 res/Vectors/J4Route.xml create mode 100644 res/Vectors/J5Route.xml create mode 100644 res/Vectors/Kkree.xml create mode 100644 res/Vectors/LesserRift.xml create mode 100644 res/Vectors/RimwardClient.xml create mode 100644 res/Vectors/Solomani.xml create mode 100644 res/Vectors/SpinwardClient.xml create mode 100644 res/Vectors/TrailingClient.xml create mode 100644 res/Vectors/Vargr.xml create mode 100644 res/Vectors/WindhornRift.xml create mode 100644 res/Vectors/ZhdantRift.xml create mode 100644 res/Vectors/Zhodani.xml create mode 100644 res/Worlds.xml create mode 100644 res/ZhodaniCoreRoute.xml create mode 100644 res/credits.js create mode 100644 res/credits.xslt create mode 100644 res/search/ArrivalVengeance.json create mode 100644 res/search/ArrivalVengeance.xml create mode 100644 res/search/Default.json create mode 100644 res/search/Default.xml create mode 100644 res/search/FarFrontiers.json create mode 100644 res/search/FarFrontiers.xml create mode 100644 res/search/GrandTour.json create mode 100644 res/search/GrandTour.xml create mode 100644 res/sectors.xml create mode 100644 res/sectors.xsd create mode 100644 res/ui/CT_Logo_Color.png create mode 100644 res/ui/CT_Logo_Gray.png create mode 100644 res/ui/Legend_1003_atlas.png create mode 100644 res/ui/Legend_1003_candy.png create mode 100644 res/ui/Legend_1003_poster.png create mode 100644 res/ui/Legend_1003_print.png create mode 100644 res/ui/Legend_1006_atlas.png create mode 100644 res/ui/Legend_1006_candy.png create mode 100644 res/ui/Legend_1006_poster.png create mode 100644 res/ui/Legend_1006_print.png create mode 100644 res/ui/ajax-loader.gif create mode 100644 res/ui/fb.png create mode 100644 res/ui/grab.cur create mode 100644 res/ui/grabbing.cur create mode 100644 res/ui/legend.html create mode 100644 res/ui/sector-spinner.gif create mode 100644 res/ui/youarehere.gif create mode 100644 res/ui/youarehere.pdn create mode 100644 res/ui/youarehere_gray.gif create mode 100644 sector.htm create mode 100644 sector.js create mode 100644 serialization/ColumnUtils.cs create mode 100644 serialization/MSECParser.cs create mode 100644 serialization/MSECWriter.cs create mode 100644 serialization/SectorMetadataParser.cs create mode 100644 serialization/SectorMetadataSerializer.cs create mode 100644 serialization/SectorParser.cs create mode 100644 serialization/SectorWriter.cs create mode 100644 site.css create mode 100644 subsector.htm create mode 100644 test/APITest.html create mode 100644 test/APITest.js create mode 100644 test/ImageTest.html create mode 100644 test/ref1.png create mode 100644 test/ref10.png create mode 100644 test/ref11.png create mode 100644 test/ref12.png create mode 100644 test/ref13.png create mode 100644 test/ref14.png create mode 100644 test/ref15.png create mode 100644 test/ref16.png create mode 100644 test/ref17.png create mode 100644 test/ref18.png create mode 100644 test/ref19.png create mode 100644 test/ref2.png create mode 100644 test/ref20.png create mode 100644 test/ref21.png create mode 100644 test/ref22.png create mode 100644 test/ref23.png create mode 100644 test/ref24.png create mode 100644 test/ref25.png create mode 100644 test/ref26.png create mode 100644 test/ref27.png create mode 100644 test/ref28.jpg create mode 100644 test/ref29.png create mode 100644 test/ref3.png create mode 100644 test/ref30.png create mode 100644 test/ref31.png create mode 100644 test/ref32.png create mode 100644 test/ref4.png create mode 100644 test/ref5.png create mode 100644 test/ref6.png create mode 100644 test/ref7.png create mode 100644 test/ref8.png create mode 100644 test/ref9.png create mode 100644 test/ref_bad_example.png create mode 100644 touch/add-to-home-screen/add2home.css create mode 100644 touch/add-to-home-screen/add2home.js create mode 100644 touch/background.png create mode 100644 touch/background_light.png create mode 100644 touch/box.jpg create mode 100644 touch/icon.png create mode 100644 touch/index.html create mode 100644 touch/info_button.png create mode 100644 touch/logo.png create mode 100644 touch/logo_gray.png create mode 100644 touch/regina114.png create mode 100644 touch/regina144.png create mode 100644 touch/regina57.png create mode 100644 touch/regina72.png create mode 100644 touch/startup-1496x2048.jpg create mode 100644 touch/startup-1536x2008.jpg create mode 100644 touch/startup-320x460.jpg create mode 100644 touch/startup-640x1096.jpg create mode 100644 touch/startup-640x920.jpg create mode 100644 touch/startup-748x1024.jpg create mode 100644 touch/startup-768x1004.jpg create mode 100644 unittests/UnitTests/JsonTest.cs create mode 100644 unittests/UnitTests/Properties/AssemblyInfo.cs create mode 100644 unittests/UnitTests/SerializationTest.cs create mode 100644 unittests/UnitTests/UnitTests.csproj create mode 100644 unittests/UnitTests/UtilTest.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..16c4801ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Other version control systems +.svn/ + +# Visual Studio cruft +*.csproj.user +*.suo +App_Data/ +obj/ +bin/ + +# Web site stuff +web.config +robots.txt +sitemap.xml + +# Publishing stuff +*.bat +*.scr + +# Unrelated stuff on site +info.htm +model2/ +pictures/ +tmp/ +res/Sectors/Legacy/ + +# Windows cruft +Thumbs.db diff --git a/Admin.aspx b/Admin.aspx new file mode 100644 index 000000000..2323ead15 --- /dev/null +++ b/Admin.aspx @@ -0,0 +1 @@ +<%@ Page language="c#" Codebehind="Admin.aspx.cs" AutoEventWireup="false" Inherits="Maps.Pages.Admin" %> diff --git a/Admin.aspx.cs b/Admin.aspx.cs new file mode 100644 index 000000000..99d721479 --- /dev/null +++ b/Admin.aspx.cs @@ -0,0 +1,113 @@ +using System; +using System.Data.SqlClient; +using System.IO; +using System.Text; + +namespace Maps.Pages +{ + public class Admin : BasePage + { + public override string DefaultContentType { get { return System.Net.Mime.MediaTypeNames.Text.Html; } } + + private void Page_Load(object sender, System.EventArgs e) + { + if (!AdminAuthorized()) + return; + + Server.ScriptTimeout = 3600; // An hour should be plenty + Response.ContentType = System.Net.Mime.MediaTypeNames.Text.Html; + Response.BufferOutput = false; + + Response.Write(""); + Response.Write("Admin Page"); + Response.Write(""); + Response.Write("

Traveller Map - Administration

"); + Response.Flush(); + + string action = GetStringOption("action"); + switch (action) + { + case "reindex": Reindex(); return; + case "flush": Flush(); return; + } + Write("Unknown action:
" + action + "
"); + Write("Ω"); + } + + private void Write(string line) + { + Response.Write("
"); + Response.Write(line); + Response.Write("
"); + Response.Flush(); + } + + private void Flush() + { + SectorMap.Flush(); + Write("Sector map flushed."); + Write("Ω"); + } + + private void Reindex() + { + + Write("Initializing resource manager..."); + ResourceManager resourceManager = new ResourceManager(Server, Cache); + + SearchEngine.PopulateDatabase(resourceManager, Write); + + Write(" "); + Write("Summary:"); + using (var connection = DBUtil.MakeConnection()) + { + foreach (string table in new string[] { "sectors", "subsectors", "worlds" }) + { + string sql = String.Format("SELECT COUNT(*) FROM {0}", table); + using (var command = new SqlCommand(sql, connection)) + { + using (var reader = command.ExecuteReader()) + { + while (reader.Read()) + { + Write(String.Format("{0}: {1}", table, reader.GetInt32(0))); + } + } + } + } + } + + Write("Ω"); + + } + + #region Web Form Designer generated code + override protected void OnInit(EventArgs e) + { + // + // CODEGEN: This call is required by the ASP.NET Web Form Designer. + // + InitializeComponent(); + base.OnInit(e); + } + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.Load += new System.EventHandler(this.Page_Load); + } + #endregion + } + +} diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs new file mode 100644 index 000000000..ac73b2a8e --- /dev/null +++ b/AssemblyInfo.cs @@ -0,0 +1,65 @@ +using System; +using System.Reflection; + +// +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +// +[assembly: AssemblyTitle("")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: + +[assembly: AssemblyVersion("1.0.*")] + +// +// In order to sign your assembly you must specify a key to use. Refer to the +// Microsoft .NET Framework documentation for more information on assembly signing. +// +// Use the attributes below to control which key is used for signing. +// +// Notes: +// (*) If no key is specified, the assembly is not signed. +// (*) KeyName refers to a key that has been installed in the Crypto Service +// Provider (CSP) on your machine. KeyFile refers to a file which contains +// a key. +// (*) If the KeyFile and the KeyName values are both specified, the +// following processing occurs: +// (1) If the KeyName can be found in the CSP, that key is used. +// (2) If the KeyName does not exist and the KeyFile does exist, the key +// in the KeyFile is installed into the CSP and used. +// (*) In order to create a KeyFile, you can use the sn.exe (Strong Name) utility. +// When specifying the KeyFile, the location of the KeyFile should be +// relative to the "project output directory". The location of the project output +// directory is dependent on whether you are working with a local or web project. +// For local projects, the project output directory is defined as +// \obj\. For example, if your KeyFile is +// located in the project directory, you would specify the AssemblyKeyFile +// attribute as [assembly: AssemblyKeyFile("..\\..\\mykey.snk")] +// For web projects, the project output directory is defined as +// %HOMEPATH%\VSWebCache\\\obj\. +// (*) Delay Signing is an advanced option - see the Microsoft .NET Framework +// documentation for more information on this. +// +[assembly: AssemblyDelaySign(false)] +[assembly: AssemblyKeyFile("")] +[assembly: AssemblyKeyName("")] + +[assembly: CLSCompliant(false)] +[assembly: System.Runtime.InteropServices.ComVisible(false)] diff --git a/Astrometrics.cs b/Astrometrics.cs new file mode 100644 index 000000000..3b38773dc --- /dev/null +++ b/Astrometrics.cs @@ -0,0 +1,126 @@ +using System; +using System.Drawing; + +namespace Maps +{ + + public static class Astrometrics + { + public const int SectorWidth = 32; // parsecs + public const int SectorHeight = 40; // parsecs + + public const int SectorCentralHex = (SectorWidth / 2) * 100 + (SectorHeight / 2); + + public const int SubsectorWidth = 8; // parsecs + public const int SubsectorHeight = 10; // parsecs + + // Parsecs are not square - there is horizontal overlap in a hex grid + // width:height ratio for parsecs is cos(30) + // (a subsector is 8:10 parsecs but 0.69:1 aspect ratio) + public static readonly float ParsecScaleX = (float)Math.Cos(Math.PI / 6); + public static readonly float ParsecScaleY = 1.0f; + + // Reference (Core 0140) + // Origin of the coordinate system, relative to the containing sector + // ("Reference, Center of the Imperium" The Travellers' Digest 10) + public static readonly Point ReferenceSector = new Point(0, 0); + public static readonly Point ReferenceHex = new Point(01, 40); + + public static Point LocationToCoordinates(Location location) + { + return LocationToCoordinates(location.SectorLocation, location.HexLocation); + } + public static Point LocationToCoordinates(Point sector, Point hex) + { + int x = (sector.X - ReferenceSector.X) * SectorWidth + (hex.X - ReferenceHex.X); + int y = (sector.Y - ReferenceSector.Y) * SectorHeight + (hex.Y - ReferenceHex.Y); + + return new Point(x, y); + } + public static Location CoordinatesToLocation(Point coordinates) + { + return CoordinatesToLocation(coordinates.X, coordinates.Y); + } + public static Location CoordinatesToLocation(int x, int y) + { + x += Astrometrics.ReferenceHex.X - 1; + y += Astrometrics.ReferenceHex.Y - 1; + + Point sector = Point.Empty; + Point hex = Point.Empty; + + sector.X = (x - (x < 0 ? Astrometrics.SectorWidth - 1 : 0)) / Astrometrics.SectorWidth; + sector.Y = (y - (y < 0 ? Astrometrics.SectorHeight - 1 : 0)) / Astrometrics.SectorHeight; + + hex.X = x - (sector.X * Astrometrics.SectorWidth) + 1; + hex.Y = y - (sector.Y * Astrometrics.SectorHeight) + 1; + + return new Location(sector, hex); + } + + public static int HexDistance(Point hex1, Point hex2) + { + int dx = hex2.X - hex1.X; + int dy = hex2.Y - hex1.Y; + + int adx = Math.Abs(dx); + int ody = dy + (adx / 2); + + if ((hex1.X % 2 == 0) && (hex2.X % 2 != 0)) { ody += 1; } + + return Math.Max(adx - ody, Math.Max(ody, adx)); + + } + + public static PointF HexToCenter(Point point) + { + PointF pf = PointF.Empty; + + pf.X = point.X - 0.5f; + pf.Y = point.Y - ((point.X % 2) != 0 ? 0.0f : 0.5f); + + return pf; + } + + public static int HexNeighbor(int hex, int direction) + { + // return col,row of a neighboring hex (0..5, starting LL and going clockwise) + + int c = hex / 100; + int r = hex % 100; + + switch (direction) + { + case 0: r += 1 - (c-- % 2); break; + case 1: r -= (c-- % 2); break; + case 2: r--; break; + case 3: r -= (c++ % 2); break; + case 4: r += 1 - (c++ % 2); break; + case 5: r++; break; + } + + return c * 100 + r; + } + public static Point HexNeighbor(Point coord, int direction) + { + int c = coord.X; + int r = coord.Y; + + // NOTE: semantics of even/odd column handing are opposite of numbered hexes since this + // is Reference-centric (and reference is in an "odd number" hex 0140) + switch (direction) + { + case 0: r += 1 - (c-- % 2 != 0 ? 0 : 1); break; + case 1: r -= (c-- % 2 != 0 ? 0 : 1); break; + case 2: r--; break; + case 3: r -= (c++ % 2 != 0 ? 0 : 1); break; + case 4: r += 1 - (c++ % 2 != 0 ? 0 : 1); break; + case 5: r++; break; + } + + return new Point(c, r); + } + } + + +} \ No newline at end of file diff --git a/Codes.aspx b/Codes.aspx new file mode 100644 index 000000000..dc252f897 --- /dev/null +++ b/Codes.aspx @@ -0,0 +1 @@ +<%@ Page language="c#" Codebehind="Codes.aspx.cs" AutoEventWireup="false" Inherits="Maps.Pages.Codes" %> diff --git a/Codes.aspx.cs b/Codes.aspx.cs new file mode 100644 index 000000000..2381862ef --- /dev/null +++ b/Codes.aspx.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Mime; +using System.Text.RegularExpressions; + +namespace Maps.Pages +{ + /// + /// Summary description for Search. + /// + public class Codes : BasePage + { + public override string DefaultContentType { get { return System.Net.Mime.MediaTypeNames.Text.Plain; } } + + // TODO: Add T5 codes + static RegexDictionary s_knownCodes = new RegexDictionary + { + // General + { "^Rs[ABGDEZH]$", "Rs" }, + { "^O:[0-9]{4}$", "O:nnnn" }, + + // Legacy + "Ag", "As", "Ba", "De", + "Fa", "Fl", "Hi", "Ic", + "In", "Lo", "Na", "Nh", + "Ni", "Nk", "Po", "Ri", + "St", "Va", "Wa", + "An", "Cf", "Cm", "Cp", + "Cs", "Cx", "Ex", "Mr", + "Pr", "Rs", + + "Aw", "Cw", "Dw", "Vw", // Aslan/Chirper/Droyne/Vargr world + + // Leviathan + "Tp", "Tn", // Terra-prime, Terra-norm + + // Mongoose + "Lt", "Ht", // Low-tech, High-tech + + // Other Common + "Xb", // X-boat stop + + { "^Rw:[0-9]$", "Rw#" }, // TNE: Refugee World + { "^A:?[0-9]$", "A#" }, // Aslan population + { "^C:?[0-9]$", "C#" }, // Chirper population + { "^D:?[0-9]$", "D#" }, // Droyne population + { "^M:?[0-9]$", "M#" }, // Human population (Provence/Tuglikki) + { "^V:?[0-9]$", "V#" }, // Vargr population + { "^X:?[0-9]$", "X#" }, // Addaxur population (Zhodani) + { "^Z:?[0-9]$", "Z#" }, // Zhodani population + + { "^H:?[0-9]$", "H#" }, // Hiver population (LWLG) + { "^Hn$", "Hn" }, // Hiver-norm (LWLG) + { "^Hp$", "Hp" }, // Hiver-prime (LWLG) + { "^F:?[0-9]$", "F#" }, // Federation member (non-Hiver) population (LWLG) + + { "^S[0-9A-F]{1,2}$", "S##" }, // Companion star orbits (James Kundert) + + { "^Rn$", "Rn" }, // Yiklerzdanzh + { "^Rv$", "Rv" }, // Yiklerzdanzh + + + // Traveller5 + "As", "De", "Fl", "Ga", "He", "Ic", "Oc", "Va", + "Wa", "Di", "Ba", "Lo", "Ni", "Ph", "Hi", "Pa", + "Ag", "Na", "Pi", "In", "Po", "Pr", "Ri", "Fr", + "Ho", "Co", "Lk", "Tr", "Tu", "Tz", "Fa", "Mi", + "Co", "Mr", "Px", "Px", "Re", "Cp", "Cs", "Cx", + "An", "Ab", "Sa", "Fo", "Pz", "Da" + }; + + private void Page_Load(object sender, System.EventArgs e) + { + if (!AdminAuthorized()) + return; + + Response.ContentType = MediaTypeNames.Text.Plain; + + ResourceManager resourceManager = new ResourceManager(Server, Cache); + + string sectorName = GetStringOption("sector"); + string type = GetStringOption("type"); + string regex = GetStringOption("regex"); + + // NOTE: This (re)initializes a static data structure used for + // resolving names into sector locations, so needs to be run + // before any other objects (e.g. Worlds) are loaded. + SectorMap.Flush(); + SectorMap map = SectorMap.FromName(SectorMap.DefaultSetting, resourceManager); + + var sectorQuery = from sector in map.Sectors + where (sectorName == null || sector.Names[0].Text.StartsWith(sectorName, ignoreCase: true, culture: CultureInfo.InvariantCulture)) + && (sector.DataFile != null) + && (type == null || sector.DataFile.Type == type) + && (!sector.DataFile.FileName.Contains(System.IO.Path.PathSeparator)) // Skip ZCR sectors + orderby sector.Names[0].Text + select sector; + + Dictionary> codes = new Dictionary>(); + + Regex filter = (regex == null) ? new Regex(".*") : new Regex(regex); + + foreach (var sector in sectorQuery) + { + WorldCollection worlds = sector.GetWorlds(resourceManager, cacheResults: false); + if (worlds == null) + continue; + + foreach (var code in worlds + .SelectMany(world => world.Remarks.Split((char[])null, StringSplitOptions.RemoveEmptyEntries)) + .Where(code => filter.IsMatch(code) && !s_knownCodes.IsMatch(code))) + { + if (!codes.ContainsKey(code)) + { + HashSet hash = new HashSet(); + hash.Add(sector.Names[0].Text); + codes.Add(code, hash); + } + else + { + codes[code].Add(sector.Names[0].Text); + } + } + } + + foreach (var code in codes.Keys.OrderBy(s => s)) + { + Response.Output.Write(code + " - "); + foreach (var sector in codes[code].OrderBy(s => s)) + { + Response.Output.Write(sector + " "); + } + Response.Output.WriteLine(""); + } + } + + + #region Web Form Designer generated code + override protected void OnInit(EventArgs e) + { + // + // CODEGEN: This call is required by the ASP.NET Web Form Designer. + // + InitializeComponent(); + base.OnInit(e); + } + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.Load += new System.EventHandler(this.Page_Load); + } + #endregion + } +} diff --git a/Coordinates.aspx b/Coordinates.aspx new file mode 100644 index 000000000..0dd9d0c22 --- /dev/null +++ b/Coordinates.aspx @@ -0,0 +1,2 @@ +<%@ Page language="c#" Codebehind="Coordinates.aspx.cs" AutoEventWireup="false" Inherits="Maps.Pages.Coordinates" %> +<%@ OutputCache Duration="3600" VaryByParam="*" VaryByHeader="Accept"%> diff --git a/Coordinates.aspx.cs b/Coordinates.aspx.cs new file mode 100644 index 000000000..b7ecc661b --- /dev/null +++ b/Coordinates.aspx.cs @@ -0,0 +1,101 @@ +using System; +using System.Drawing; +using System.Xml.Serialization; + +namespace Maps.Pages +{ + /// + /// Summary description for Search. + /// + /// + public class Coordinates : DataPage + { + public override string DefaultContentType { get { return System.Net.Mime.MediaTypeNames.Text.Xml; } } + + private void Page_Load(object sender, System.EventArgs e) + { + // NOTE: This (re)initializes a static data structure used for + // resolving names into sector locations, so needs to be run + // before any other objects (e.g. Worlds) are loaded. + ResourceManager resourceManager = new ResourceManager(Server, Cache); + SectorMap map = SectorMap.FromName(SectorMap.DefaultSetting, resourceManager); + + Location loc = new Location(map.FromName("Spinward Marches").Location, 1910); + + // Accept either sector [& hex] or sx,sy [& hx,hy] + if (HasOption("sector")) + { + string sectorName = GetStringOption("sector"); + Sector sector = map.FromName(sectorName); + if (sector == null) + { + SendError(404, "Not Found", "Sector not found."); + return; + } + + int hex = GetIntOption("hex", 0); + loc = new Location(sector.Location, hex); + } + else if (HasOption("sx") && HasOption("sy")) + { + int sx = GetIntOption("sx", 0); + int sy = GetIntOption("sy", 0); + int hx = GetIntOption("hx", 0); + int hy = GetIntOption("hy", 0); + loc = new Location(map.FromLocation(sx, sy).Location, hx * 100 + hy); + } + else + { + SendError(404, "Not Found", "Must specify either sector name (and optional hex) or sx, sy (and optional hx, hy)."); + return; + } + + Point coords = Astrometrics.LocationToCoordinates(loc); + + Result result = new Result(); + result.sx = loc.SectorLocation.X; + result.sy = loc.SectorLocation.Y; + result.hx = loc.HexLocation.X; + result.hy = loc.HexLocation.Y; + result.x = coords.X; + result.y = coords.Y; + SendResult(result); + } + + [XmlRoot(ElementName = "Coordinates")] + // public for XML serialization + public class Result + { + // Sector/Hex + public int sx { get; set; } + public int sy { get; set; } + public int hx { get; set; } + public int hy { get; set; } + + // World-space X/Y + public int x { get; set; } + public int y { get; set; } + } + + #region Web Form Designer generated code + override protected void OnInit(EventArgs e) + { + // + // CODEGEN: This call is required by the ASP.NET Web Form Designer. + // + InitializeComponent(); + base.OnInit(e); + } + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.Load += new System.EventHandler(this.Page_Load); + } + #endregion + } + +} diff --git a/Credits.aspx b/Credits.aspx new file mode 100644 index 000000000..9006626fb --- /dev/null +++ b/Credits.aspx @@ -0,0 +1,2 @@ +<%@ Page language="c#" Codebehind="Credits.aspx.cs" AutoEventWireup="false" Inherits="Maps.Pages.Credits" %> +<%@ OutputCache Duration="3600" VaryByParam="*" VaryByHeader="Accept"%> diff --git a/Credits.aspx.cs b/Credits.aspx.cs new file mode 100644 index 000000000..96d6c84a9 --- /dev/null +++ b/Credits.aspx.cs @@ -0,0 +1,193 @@ +using System; +using System.Drawing; +using System.Globalization; +using System.Linq; +using System.Xml.Serialization; + +namespace Maps.Pages +{ + /// + /// Summary description for Search. + /// + public class Credits : DataPage + { + public override string DefaultContentType { get { return System.Net.Mime.MediaTypeNames.Text.Xml; } } + + private void Page_Load(object sender, System.EventArgs e) + { + if (!ServiceConfiguration.CheckEnabled("credits", Response)) + { + return; + } + + ResourceManager resourceManager = new ResourceManager(Server, Cache); + + // NOTE: This (re)initializes a static data structure used for + // resolving names into sector locations, so needs to be run + // before any other objects (e.g. Worlds) are loaded. + SectorMap map = SectorMap.FromName(SectorMap.DefaultSetting, resourceManager); + Location loc = new Location(map.FromName("Spinward Marches").Location, 1910); + + if (HasOption("sector")) + { + string sectorName = GetStringOption("sector"); + Sector sec = map.FromName(sectorName); + if (sec == null) + { + SendError(404, "Not Found", "Sector not found."); + return; + } + + int hex = GetIntOption("hex", Astrometrics.SectorCentralHex); + loc = new Location(sec.Location, hex); + } + else if (HasOption("sx") && HasOption("sy")) + { + int sx = GetIntOption("sx", 0); + int sy = GetIntOption("sy", 0); + int hx = GetIntOption("hx", 0); + int hy = GetIntOption("hy", 0); + loc = new Location(map.FromLocation(sx, sy).Location, hx * 100 + hy); + } + else if (HasOption("x") && HasOption("y")) + { + loc = Astrometrics.CoordinatesToLocation(GetIntOption("x", 0), GetIntOption("y", 0)); + } + + if (loc.HexLocation.IsEmpty) + { + loc.HexLocation = new Point(Astrometrics.SectorWidth / 2, Astrometrics.SectorHeight / 2); + } + + Sector sector = map.FromLocation(loc.SectorLocation.X, loc.SectorLocation.Y); + + Result data = new Result(); + + if (sector != null) + { + // TODO: Multiple names + foreach (var name in sector.Names.Take(1)) + { + data.SectorName = name.Text; + } + + // Raw HTML credits + data.Credits = sector.Credits == null ? null : sector.Credits.Trim(); + + // Product info + if (sector.Products.Count > 0) + { + data.ProductPublisher = sector.Products[0].Publisher; + data.ProductTitle = sector.Products[0].Title; + data.ProductAuthor = sector.Products[0].Author; + data.ProductRef = sector.Products[0].Ref; + } + + // + // Sector Credits + // + if (sector.DataFile != null) + { + data.SectorAuthor = sector.DataFile.Author; + data.SectorSource = sector.DataFile.Source; + data.SectorPublisher = sector.DataFile.Publisher; + data.SectorCopyright = sector.DataFile.Copyright; + data.SectorRef = sector.DataFile.Ref; + data.SectorEra = sector.DataFile.Era; + } + + // + // Subsector Credits + // + int ssx = (loc.HexLocation.X - 1) / Astrometrics.SubsectorWidth; + int ssy = (loc.HexLocation.Y - 1) / Astrometrics.SubsectorHeight; + int ssi = ssx + ssy * 4; + Subsector ss = sector[ssi]; + if (ss != null) + { + data.SubsectorIndex = ss.Index; + data.SubsectorName = ss.Name; + } + + // + // Routes Credits + // + + + // + // World Data + // + WorldCollection worlds = sector.GetWorlds(resourceManager); + if (worlds != null) + { + World world = worlds[loc.HexLocation.X, loc.HexLocation.Y]; + if (world != null) + { + data.WorldName = world.Name; + data.WorldHex = world.Hex.ToString("0000", CultureInfo.InvariantCulture); + data.WorldUwp = world.UWP; + } + } + } + + SendResult(data); + } + + [XmlRoot(ElementName = "Data")] + // public for XML serialization + public class Result + { + public string Credits { get; set; } + + public string SectorName { get; set; } + public string SectorAuthor { get; set; } + public string SectorSource { get; set; } + public string SectorPublisher { get; set; } + public string SectorCopyright { get; set; } + public string SectorRef { get; set; } + public string SectorEra { get; set; } + + public string RouteCredits { get; set; } + + public string SubsectorName { get; set; } + public string SubsectorIndex { get; set; } + public string SubsectorCredits { get; set; } + + public string WorldName { get; set; } + public string WorldHex { get; set; } + public string WorldUwp { get; set; } + public string WorldCredits { get; set; } + public string LandGrabTitle { get; set; } + public string LandGrabURL { get; set; } + + public string ProductPublisher { get; set; } + public string ProductTitle { get; set; } + public string ProductAuthor { get; set; } + public string ProductRef { get; set; } + } + + + #region Web Form Designer generated code + override protected void OnInit(EventArgs e) + { + // + // CODEGEN: This call is required by the ASP.NET Web Form Designer. + // + InitializeComponent(); + base.OnInit(e); + } + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.Load += new System.EventHandler(this.Page_Load); + } + #endregion + } + + + +} diff --git a/DataPage.cs b/DataPage.cs new file mode 100644 index 000000000..f0a92404a --- /dev/null +++ b/DataPage.cs @@ -0,0 +1,209 @@ +using Json; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net.Mime; +using System.Text; +using System.Web.Routing; +using System.Xml.Serialization; + +namespace Maps.Pages +{ + public abstract class BasePage : System.Web.UI.Page + { + protected bool AdminAuthorized() + { + if (Request["key"] != System.Configuration.ConfigurationManager.AppSettings["AdminKey"]) + { + SendError(403, "Access Denied", "Access Denied"); + return false; + } + return true; + } + + public abstract string DefaultContentType { get; } + protected IEnumerable AcceptTypes + { + get { + if (Request["accept"] != null) { + yield return Request["accept"]; + } + if (RouteData.Values["accept"] != null) { + yield return RouteData.Values["accept"].ToString(); + } + foreach (var type in Request.AcceptTypes) + { + yield return type; + } + + yield return DefaultContentType; + } + } + + protected bool Accepts(string mediaType) + { + return AcceptTypes.Contains(mediaType); + } + + protected bool HasOption(string name) + { + return Request[name] != null || RouteData.Values[name] != null; + } + + protected string GetStringOption(string name) + { + if (Request[name] != null) + return Request[name]; + if (RouteData.Values[name] != null) + return RouteData.Values[name].ToString(); + return null; + } + + protected int GetIntOption(string name, int defaultValue) + { + int temp; + if (Int32.TryParse(GetStringOption(name), NumberStyles.Integer, CultureInfo.InvariantCulture, out temp)) + { + return temp; + } + return defaultValue; + } + + protected double GetDoubleOption(string name, double defaultValue) + { + double temp; + if (Double.TryParse(GetStringOption(name), NumberStyles.Float, CultureInfo.InvariantCulture, out temp)) + { + return temp; + } + return defaultValue; + } + + protected bool GetBoolOption(string name, bool defaultValue) + { + int temp; + if (Int32.TryParse(GetStringOption(name), NumberStyles.Integer, CultureInfo.InvariantCulture, out temp)) + { + return temp != 0; + } + return defaultValue; + } + + protected void SendError(int code, string description, string message) + { + Response.StatusCode = code; + Response.StatusDescription = description; + Response.ContentType = MediaTypeNames.Text.Plain; + Response.Output.WriteLine(message); + } + } + + public abstract class DataPage : BasePage + { + /// + /// Send as the specified content type if no Accept: type (or query param) + /// requests differently. + /// + /// + /// + protected void SendResult(object o, Encoding encoding = null) + { + // CORS - allow from any origin + Response.AddHeader("Access-Control-Allow-Origin", "*"); + + // Vary: * is basically ignored by browsers + Response.Cache.SetOmitVaryStar(true); + + if (Request.QueryString["jsonp"] != null) + { + SendJson(o); + return; + } + + foreach (var type in AcceptTypes) + { + if (type == JsonConstants.MediaType) + { + SendJson(o); + return; + } + if (type == MediaTypeNames.Text.Xml) + { + SendXml(o); + return; + } + } + + SendText(o, encoding); + } + + private void SendXml(object o) + { + Response.ContentType = MediaTypeNames.Text.Xml; + XmlSerializer xs = new XmlSerializer(o.GetType()); + xs.Serialize(Response.OutputStream, o); + } + + private void SendText(object o, Encoding encoding) + { + Response.ContentType = MediaTypeNames.Text.Plain; + if (encoding == null) + { + Response.Output.Write(o.ToString()); + } + else + { + Response.ContentEncoding = encoding; + Response.Output.Write(o.ToString()); + } + } + + private void SendJson(object o) + { + Response.ContentType = JsonConstants.MediaType; + JsonSerializer js = new JsonSerializer(); + + // TODO: Subclass this from a DataResponsePage + SendPreamble(JsonConstants.MediaType); + js.Serialize(Response.OutputStream, o); + SendPostamble(JsonConstants.MediaType); + } + + private void SendPreamble(string contentType) + { + if (contentType == JsonConstants.MediaType && Request.QueryString["jsonp"] != null) + { + using (var w = new StreamWriter(Response.OutputStream)) + { + w.Write(Request.QueryString["jsonp"]); + w.Write("("); + } + } + } + + private void SendPostamble(string contentType) + { + if (contentType == JsonConstants.MediaType && Request.QueryString["jsonp"] != null) + { + using (var w = new StreamWriter(Response.OutputStream)) + { + w.Write(");"); + } + } + } + + public void SendFile(string contentType, string filename) + { + // CORS - allow from any origin + Response.AddHeader("Access-Control-Allow-Origin", "*"); + + Response.ContentType = contentType; + SendPreamble(contentType); + Response.TransmitFile(filename); + SendPostamble(contentType); + } + } + +} \ No newline at end of file diff --git a/Dump.aspx b/Dump.aspx new file mode 100644 index 000000000..64753fc53 --- /dev/null +++ b/Dump.aspx @@ -0,0 +1 @@ +<%@ Page language="c#" Codebehind="Dump.aspx.cs" AutoEventWireup="false" Inherits="Maps.Pages.Dump" %> diff --git a/Dump.aspx.cs b/Dump.aspx.cs new file mode 100644 index 000000000..f0a494137 --- /dev/null +++ b/Dump.aspx.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Net.Mime; + +namespace Maps.Pages +{ + /// + /// Fetch data about the universe. + /// + public class Dump : BasePage + { + public override string DefaultContentType { get { return System.Net.Mime.MediaTypeNames.Text.Plain; } } + + private void Page_Load(object sender, System.EventArgs e) + { + if (!AdminAuthorized()) + return; + + ResourceManager resourceManager = new ResourceManager(Server, Cache); + + // NOTE: This (re)initializes a static data structure used for + // resolving names into sector locations, so needs to be run + // before any other objects (e.g. Worlds) are loaded. + SectorMap map = SectorMap.FromName(SectorMap.DefaultSetting, resourceManager); + + Response.ContentType = MediaTypeNames.Text.Plain; + + foreach (Sector sector in map.Sectors) + { + WorldCollection worlds = sector.GetWorlds(resourceManager); + if (worlds == null) + continue; + foreach (World world in worlds) + { + List list = new List { + sector.Names[0].Text, + sector.X.ToString(), + sector.Y.ToString(), + world.X.ToString(), + world.Y.ToString(), + world.Name, + world.UWP, + world.Bases, + world.Remarks, + world.PBG, + world.Allegiance, + world.Stellar + }; + WriteCSV(list); + } + } + } + + private void WriteCSV(List values) + { + bool first = true; + foreach (string value in values) + { + if (!first) + { + Response.Output.Write(','); + } + else + { + first = false; + } + + if (value.IndexOf(',') == -1 && value.IndexOf('"') == -1) + { + // plain + Response.Output.Write(value); + } + else + { + // quoted + Response.Output.Write('"'); + Response.Output.Write(value.Replace("\"", "\"\"")); + Response.Output.Write('"'); + } + } + Response.Output.Write("\n"); + } + + #region Web Form Designer generated code + override protected void OnInit(EventArgs e) + { + // + // CODEGEN: This call is required by the ASP.NET Web Form Designer. + // + InitializeComponent(); + base.OnInit(e); + } + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.Load += new System.EventHandler(this.Page_Load); + } + #endregion + } +} diff --git a/Errors.aspx b/Errors.aspx new file mode 100644 index 000000000..97bf7da70 --- /dev/null +++ b/Errors.aspx @@ -0,0 +1 @@ +<%@ Page language="c#" Codebehind="Errors.aspx.cs" AutoEventWireup="false" Inherits="Maps.Pages.Errors" %> diff --git a/Errors.aspx.cs b/Errors.aspx.cs new file mode 100644 index 000000000..a8654af79 --- /dev/null +++ b/Errors.aspx.cs @@ -0,0 +1,90 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Net.Mime; +using System.Text.RegularExpressions; + +namespace Maps.Pages +{ + /// + /// Summary description for Search. + /// + public class Errors : BasePage + { + public override string DefaultContentType { get { return System.Net.Mime.MediaTypeNames.Text.Plain; } } + + private static readonly Regex candidate = new Regex(@"(\w\w\w\w\w\w\w-\w|Error) \b", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Singleline | RegexOptions.ExplicitCapture | RegexOptions.IgnorePatternWhitespace); + + private void Page_Load(object sender, System.EventArgs e) + { + if (!AdminAuthorized()) + return; + + Response.ContentType = MediaTypeNames.Text.Plain; + + ResourceManager resourceManager = new ResourceManager(Server, Cache); + + string sectorName = GetStringOption("sector"); + string type = GetStringOption("type"); + + // NOTE: This (re)initializes a static data structure used for + // resolving names into sector locations, so needs to be run + // before any other objects (e.g. Worlds) are loaded. + SectorMap.Flush(); + SectorMap map = SectorMap.FromName(SectorMap.DefaultSetting, resourceManager); + + var sectorQuery = from sector in map.Sectors + where (sectorName == null || sector.Names[0].Text.StartsWith(sectorName, ignoreCase: true, culture: CultureInfo.InvariantCulture)) + && (sector.DataFile != null) + && (type == null || sector.DataFile.Type == type) + && (!sector.DataFile.FileName.Contains(System.IO.Path.DirectorySeparatorChar)) // Skip ZCR sectors + orderby sector.Names[0].Text + select sector; + + foreach (var sector in sectorQuery) + { + Response.Output.WriteLine(sector.Names[0].Text); +#if DEBUG + WorldCollection worlds = sector.GetWorlds( resourceManager, cacheResults: false ); + + if( worlds != null ) + { + Response.Output.WriteLine("{0} world(s)", worlds.Count()); + foreach (string s in worlds.ErrorList.Where(s => candidate.IsMatch(s))) + { + Response.Output.WriteLine(s); + } + } + else + { + Response.Output.WriteLine("{0} world(s)", 0); + } +#endif + Response.Output.WriteLine(); + } + return; + } + + + #region Web Form Designer generated code + override protected void OnInit(EventArgs e) + { + // + // CODEGEN: This call is required by the ASP.NET Web Form Designer. + // + InitializeComponent(); + base.OnInit(e); + } + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.Load += new System.EventHandler(this.Page_Load); + } + #endregion + } +} diff --git a/Global.asax b/Global.asax new file mode 100644 index 000000000..fa1a7b34c --- /dev/null +++ b/Global.asax @@ -0,0 +1 @@ +<%@ Application Language="C#" Inherits="Maps.GlobalAsax" Codebehind="Global.asax.cs"%> diff --git a/Global.asax.cs b/Global.asax.cs new file mode 100644 index 000000000..cbffb7dcc --- /dev/null +++ b/Global.asax.cs @@ -0,0 +1,93 @@ +using Json; +using System; +using System.Web.Routing; + +namespace Maps +{ + public class GlobalAsax : System.Web.HttpApplication + { + protected void Application_Start(object sender, EventArgs e) + { + RegisterRoutes(RouteTable.Routes); + } + + private static void RegisterRoutes(RouteCollection routes) + { + var DEFAULT_JSON = new RouteValueDictionary { { "accept", JsonConstants.MediaType } }; + + // Helpers, to avoid having to mint names for each route + int routeNum = 0; + Func routeName = () => "route " + (routeNum++).ToString(); + + Action mpr0 = + (url, file) => routes.MapPageRoute(routeName(), url, file); + Action mpr1 = + (url, file, defaults) => routes.MapPageRoute(routeName(), url, file, checkPhysicalUrlAccess: false, defaults: defaults); + + // Rendering + mpr0("api/poster", "~/Poster.aspx"); + mpr0("api/tile", "~/Tile.aspx"); + mpr0("api/jumpmap", "~/JumpMap.aspx"); + mpr0("api/overview", "~/Overview.aspx"); + + // Search Queries + mpr1("api/search", "~/Search.aspx", DEFAULT_JSON); + + // Location Queries + mpr1("api/coordinates", "~/Coordinates.aspx", DEFAULT_JSON); + mpr1("api/credits", "~/Credits.aspx", DEFAULT_JSON); + mpr1("api/jumpworlds", "~/JumpWorlds.aspx", DEFAULT_JSON); + + // Data Retrieval - API-centric + mpr1("api/sec", "~/SEC.aspx", new RouteValueDictionary { { "type", "SecondSurvey" } }); + mpr1("api/sec/{sector}", "~/SEC.aspx", new RouteValueDictionary { { "type", "SecondSurvey" } }); + mpr0("api/msec", "~/MSEC.aspx"); + mpr0("api/msec/{sector}", "~/MSEC.aspx"); + mpr1("api/metadata", "~/SectorMetaData.aspx", DEFAULT_JSON); + mpr1("api/metadata/{sector}", "~/SectorMetaData.aspx", DEFAULT_JSON); + mpr1("api/universe", "~/Universe.aspx", DEFAULT_JSON); + + // Admin + mpr0("admin/admin", "~/Admin.aspx"); + mpr0("admin/codes", "~/Codes.aspx"); + mpr0("admin/dump", "~/Dump.aspx"); + mpr0("admin/errors", "~/Errors.aspx"); + + // TODO: DEFAULT should be FORCE instead, a type override + + // RESTful + mpr1("data", "~/Universe.aspx", DEFAULT_JSON); + mpr0("data/image", "~/Overview.aspx"); + + mpr1("data/{sector}", "~/SEC.aspx", new RouteValueDictionary { { "type", "SecondSurvey" } }); + mpr0("data/{sector}/sec", "~/SEC.aspx"); + mpr1("data/{sector}/tab", "~/SEC.aspx", new RouteValueDictionary { { "type", "TabDelimited" } }); + mpr0("data/{sector}/metadata", "~/SectorMetaData.aspx"); // TODO: JSON? + mpr0("data/{sector}/msec", "~/MSEC.aspx"); + mpr0("data/{sector}/image", "~/Poster.aspx"); + mpr1("data/{sector}/coordinates", "~/Coordinates.aspx", DEFAULT_JSON); + mpr1("data/{sector}/credits", "~/Credits.aspx", DEFAULT_JSON); + + // data/{sector}/{subsector}/foo conflicts with data/{sector}/{hex}/foo + // so register data/{sector}/A ... data/{sector}/P instead + // TODO: Support subsectors by name - will require manual delegation + for (char s = 'A'; s <= 'P'; ++s) + { + string ss = s.ToString(); + Func r = (pattern) => pattern.Replace("{subsector}", ss); + + mpr1(r("data/{sector}/{subsector}"), "~/SEC.aspx", new RouteValueDictionary { { "subsector", ss }, { "type", "SecondSurvey" }, {"metadata", "0"} }); + mpr1(r("data/{sector}/{subsector}/sec"), "~/SEC.aspx", new RouteValueDictionary { { "subsector", ss }, { "metadata", "0" } }); + mpr1(r("data/{sector}/{subsector}/tab"), "~/SEC.aspx", new RouteValueDictionary { { "subsector", ss }, { "type", "TabDelimited" }, { "metadata", "0" } }); + mpr1(r("data/{sector}/{subsector}/image"), "~/Poster.aspx", new RouteValueDictionary { { "subsector", ss } }); + } + + mpr1("data/{sector}/{hex}", "~/JumpWorlds.aspx", new RouteValueDictionary { { "accept", JsonConstants.MediaType }, { "jump", "0" } }); + mpr1("data/{sector}/{hex}/image", "~/JumpMap.aspx", new RouteValueDictionary { { "jump", "0" } }); + mpr1("data/{sector}/{hex}/coordinates", "~/Coordinates.aspx", DEFAULT_JSON); + mpr1("data/{sector}/{hex}/credits", "~/Credits.aspx", DEFAULT_JSON); + mpr1("data/{sector}/{hex}/jump/{jump}", "~/JumpWorlds.aspx", DEFAULT_JSON); + mpr0("data/{sector}/{hex}/jump/{jump}/image", "~/JumpMap.aspx"); + } + } +} diff --git a/ImageGeneratorPage.cs b/ImageGeneratorPage.cs new file mode 100644 index 000000000..232e2db59 --- /dev/null +++ b/ImageGeneratorPage.cs @@ -0,0 +1,320 @@ +#define LEGACY_STYLES + +using Maps.Rendering; +using Maps.Serialization; +using PdfSharp.Drawing; +using PdfSharp.Pdf; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.Globalization; +using System.IO; +using System.Net.Mime; +using System.Web; +using System.Xml.Serialization; + +namespace Maps.Pages +{ + public abstract class ImageGeneratorPage : BasePage + { + public override string DefaultContentType { get { return Util.MediaTypeName_Image_Png; } } + + public const double MinScale = 0.0078125; // Math.Pow(2, -7); + public const double MaxScale = 512; // Math.Pow(2, 9); + + protected void ParseOptions(ref MapOptions options, ref Stylesheet.Style style) + { + options = (MapOptions)GetIntOption("options", (int)options); + +#if LEGACY_STYLES + // Handle deprecated/legacy options bits for selecting style + style = + (options & MapOptions.StyleMaskDeprecated) == MapOptions.PrintStyleDeprecated ? Stylesheet.Style.Atlas : + (options & MapOptions.StyleMaskDeprecated) == MapOptions.CandyStyleDeprecated ? Stylesheet.Style.Candy : + Stylesheet.Style.Poster; +#endif // LEGACY_STYLES + + if (HasOption("style")) + { + switch (GetStringOption("style").ToLowerInvariant()) + { + case "poster": style = Stylesheet.Style.Poster; break; + case "atlas": style = Stylesheet.Style.Atlas; break; + case "print": style = Stylesheet.Style.Print; break; + case "candy": style = Stylesheet.Style.Candy; break; + } + } + } + + protected void SetCommonResponseHeaders() + { + // CORS - allow from any origin + Response.AddHeader("Access-Control-Allow-Origin", "*"); +#if !DEBUG + Response.Cache.SetCacheability(HttpCacheability.Public); + Response.Cache.SetExpires(DateTime.Now.AddHours(12)); + Response.Cache.SetValidUntilExpires(true); +#endif + if (Request.HttpMethod == "POST") + { + Response.Cache.SetCacheability(HttpCacheability.NoCache); + } + } + + protected void ProduceResponse(string title, Render.RenderContext ctx, Size tileSize, + int rot = 0, float translateX = 0, float translateY = 0, + bool transparent = false) + { + // New-style Options + // TODO: move to ParseOptions (maybe - requires options to be parsed after stylesheet creation?) + if (GetBoolOption("sscoords", defaultValue: false)) + { + ctx.styles.hexCoordinateStyle = Stylesheet.HexCoordinateStyle.Subsector; + } + + if (!GetBoolOption("routes", defaultValue: true)) + { + ctx.styles.macroRoutes.visible = false; + ctx.styles.microRoutes.visible = false; + } + + double devicePixelRatio = GetDoubleOption("dpr", 1); + + if (Accepts(MediaTypeNames.Application.Pdf)) + { + using (var document = new PdfDocument()) + { + document.Version = 14; // 1.4 for opacity + document.Info.Title = title; + document.Info.Author = "Joshua Bell"; + document.Info.Creator = "TravellerMap.com"; + document.Info.Subject = DateTime.Now.ToString("F", CultureInfo.InvariantCulture); + document.Info.Keywords = "The Traveller game in all forms is owned by Far Future Enterprises. Copyright (C) 1977 - 2013 Far Future Enterprises. Traveller is a registered trademark of Far Future Enterprises."; + + // TODO: Credits/Copyright + // This is close, but doesn't define the namespace correctly: + // document.Info.Elements.Add( new KeyValuePair( "/photoshop/Copyright", new PdfString( "HelloWorld" ) ) ); + + PdfPage page = document.AddPage(); + + // NOTE: only PageUnit currently supported in XGraphics is Points + page.Width = XUnit.FromPoint(tileSize.Width); + page.Height = XUnit.FromPoint(tileSize.Height); + + PdfSharp.Drawing.XGraphics gfx = PdfSharp.Drawing.XGraphics.FromPdfPage(page); + + RenderToGraphics(ctx, rot, translateX, translateY, gfx); + + using (var stream = new MemoryStream()) + { + document.Save(stream, closeStream: false); + + SetCommonResponseHeaders(); + + Response.ContentType = MediaTypeNames.Application.Pdf; + Response.AddHeader("content-length", stream.Length.ToString()); + Response.AddHeader("content-disposition", "inline;filename=\"map.pdf\""); + Response.BinaryWrite(stream.ToArray()); + Response.Flush(); + stream.Close(); + } + + return; + } + } + + using (var bitmap = new Bitmap((int)Math.Floor(tileSize.Width * devicePixelRatio), (int)Math.Floor(tileSize.Height * devicePixelRatio), PixelFormat.Format32bppArgb)) + { + if (transparent) + { + bitmap.MakeTransparent(); + } + + using (var g = Graphics.FromImage(bitmap)) + { + g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; + + using (var graphics = XGraphics.FromGraphics(g, new XSize(tileSize.Width * devicePixelRatio, tileSize.Height * devicePixelRatio))) + { + if (devicePixelRatio != 0) + { + graphics.ScaleTransform(devicePixelRatio); + } + + RenderToGraphics(ctx, rot, translateX, translateY, graphics); + } + } + + SetCommonResponseHeaders(); + BitmapResponse(ctx.styles, bitmap, transparent ? Util.MediaTypeName_Image_Png : null); + } + } + + private static void RenderToGraphics(Render.RenderContext ctx, int rot, float translateX, float translateY, XGraphics graphics) + { + graphics.TranslateTransform(translateX, translateY); + graphics.RotateTransform(rot * 90); + + using (Maps.Rendering.RenderUtil.SaveState(graphics)) + { + + if (ctx.clipPath != null) + { + XMatrix m = ctx.ImageSpaceToWorldSpace; + graphics.MultiplyTransform(m); + graphics.IntersectClip(ctx.clipPath); + m.Invert(); + graphics.MultiplyTransform(m); + } + + ctx.graphics = graphics; + Maps.Rendering.Render.RenderTile(ctx); + } + + + if (ctx.border && ctx.clipPath != null) + { + using (Maps.Rendering.RenderUtil.SaveState(graphics)) + { + // Render border in world space + XMatrix m = ctx.ImageSpaceToWorldSpace; + graphics.MultiplyTransform(m); + XPen pen = new XPen(ctx.styles.imageBorderColor, 0.2f); + + // PdfSharp can't ExcludeClip so we take advantage of the fact that we know + // the path starts on the left edge and proceeds clockwise. We extend the + // path with a counterclockwise border around it, then use that to exclude + // the original path's region for rendering the border. + ctx.clipPath.Flatten(); + RectangleF bounds = PathUtil.Bounds(ctx.clipPath); + bounds.Inflate(2 * (float)pen.Width, 2 * (float)pen.Width); + List types = new List(ctx.clipPath.Internals.GdiPath.PathTypes); + List points = new List(ctx.clipPath.Internals.GdiPath.PathPoints); + + PointF key = points[0]; + points.Add(new PointF(bounds.Left, key.Y)); types.Add(1); + points.Add(new PointF(bounds.Left, bounds.Bottom)); types.Add(1); + points.Add(new PointF(bounds.Right, bounds.Bottom)); types.Add(1); + points.Add(new PointF(bounds.Right, bounds.Top)); types.Add(1); + points.Add(new PointF(bounds.Left, bounds.Top)); types.Add(1); + points.Add(new PointF(bounds.Left, key.Y)); types.Add(1); + points.Add(new PointF(key.X, key.Y)); types.Add(1); + + XGraphicsPath path = new XGraphicsPath(points.ToArray(), types.ToArray(), XFillMode.Winding); + graphics.IntersectClip(path); + graphics.DrawPath(pen, ctx.clipPath); + } + } + } + + + protected void BitmapResponse(Stylesheet styles, Bitmap bitmap, string mimeType) + { + // JPEG or PNG if not specified, based on style + if (mimeType == null) + { + mimeType = styles.preferredMimeType; + } + + Response.ContentType = mimeType; + + // Searching for a matching encoder + ImageCodecInfo encoder = null; + ImageCodecInfo[] encoders = ImageCodecInfo.GetImageEncoders(); + for (int i = 0; i < encoders.Length; ++i) + { + if (encoders[i].MimeType == Response.ContentType) + { + encoder = encoders[i]; + break; + } + } + + if (encoder != null) + { + EncoderParameters encoderParams; + if (mimeType == MediaTypeNames.Image.Jpeg) + { + encoderParams = new EncoderParameters(1); + encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, (long)95); + } + else if (mimeType == Util.MediaTypeName_Image_Png) + { + encoderParams = new EncoderParameters(1); + encoderParams.Param[0] = new EncoderParameter(Encoder.ColorDepth, 8); + } + else + { + encoderParams = new EncoderParameters(0); + } + + if (mimeType == Util.MediaTypeName_Image_Png) + { + // PNG encoder is picky about streams - need to do an indirection + // http://www.west-wind.com/WebLog/posts/8230.aspx + using (var ms = new MemoryStream()) + { + bitmap.Save(ms, encoder, encoderParams); + ms.WriteTo(Context.Response.OutputStream); + } + } + else + { + bitmap.Save(Context.Response.OutputStream, encoder, encoderParams); + } + + encoderParams.Dispose(); + } + else + { + // Default to GIF if we can't find anything + Response.ContentType = MediaTypeNames.Image.Gif; + bitmap.Save(Context.Response.OutputStream, ImageFormat.Gif); + } + } + + protected Sector GetPostedSector() + { + Sector sector = null; + if (Request.ContentType.StartsWith("multipart/form-data") + && Request.Files["file"] != null && Request.Files["file"].ContentLength > 0) + { + HttpPostedFile hpf = Request.Files["file"]; + sector = new Sector(hpf.InputStream, hpf.ContentType); + + if (Request.Files["metadata"] != null && Request.Files["metadata"].ContentLength > 0) + { + hpf = Request.Files["metadata"]; + + string type = SectorMetadataFileParser.SniffType(hpf.InputStream); + Sector meta = SectorMetadataFileParser.ForType(type).Parse(hpf.InputStream); + sector.Merge(meta); + } + } + else if (Request.ContentType == "application/x-www-form-urlencoded" + && Request.Form["data"] != null) + { + string data = Request.Form["data"]; + sector = new Sector(data.ToStream(), MediaTypeNames.Text.Plain); + + if (!String.IsNullOrEmpty(Request.Form["metadata"])) + { + string metadata = Request.Form["metadata"]; + string type = SectorMetadataFileParser.SniffType(metadata.ToStream()); + var parser = SectorMetadataFileParser.ForType(type); + using (var reader = new StringReader(metadata)) + { + Sector meta = parser.Parse(reader); + sector.Merge(meta); + } + } + } + else if (Request.ContentType == MediaTypeNames.Text.Plain) + { + sector = new Sector(Request.InputStream, MediaTypeNames.Text.Plain); + } + return sector; + } + } +} diff --git a/Json.cs b/Json.cs new file mode 100644 index 000000000..79f3191f3 --- /dev/null +++ b/Json.cs @@ -0,0 +1,271 @@ +using System; +using System.Data; +using System.Configuration; +using System.Collections; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Reflection; +using System.IO; +using System.Xml; +namespace Json +{ + + [AttributeUsage( AttributeTargets.All )] + public sealed class JsonNameAttribute : Attribute + { + string name; + + public string Name + { + get { return name; } + } + + public JsonNameAttribute( string name ) + { + this.name = name; + } + } + + [AttributeUsage( AttributeTargets.All )] + public sealed class JsonIgnoreAttribute : Attribute + { + } + + public static class JsonConstants + { + public const string MediaType = "application/json"; + public const string StartObject = "{"; + public const string EndObject = "}"; + public const string StartArray = "["; + public const string EndArray = "]"; + public const string NameSeparator = ":"; + public const string FieldDelimiter = ","; + } + + public class JsonSerializer + { + public bool SerializeCollectionsAsArrays { get; set; } + + public void Serialize( Stream stream, object item ) + { + Encoding utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + using (var sw = new StreamWriter(stream, utf8)) + { + Serialize(sw, item); + sw.Flush(); + } + } + + public void Serialize( TextWriter writer, object item ) + { + if( this.SerializeCollectionsAsArrays && item is IEnumerable ) + { + SerializeArray( writer, (IEnumerable)item ); + } + else + { + SerializeValue( writer, item ); + } + } + + private static string GetName( object item ) + { + return item.GetType().GetCustomAttributes(typeof(JsonNameAttribute), inherit: true).OfType() + .Select(jn => jn.Name).FirstOrDefault(); + } + + private static string GetName(PropertyInfo pi) + { + JsonNameAttribute jn = pi.GetCustomAttributes(typeof(JsonNameAttribute), inherit: true).OfType().FirstOrDefault(); + return (jn != null) ? jn.Name : pi.Name; + } + + private static object GetDefaultValue(PropertyInfo pi) + { + return pi.GetCustomAttributes(typeof(DefaultValueAttribute), inherit: true).OfType() + .Select(dva => dva.Value).FirstOrDefault(); + } + + private static bool Ignore(PropertyInfo info) + { + return info.GetCustomAttributes(typeof(JsonIgnoreAttribute), inherit: true).Any(); + } + + private void SerializeObject( TextWriter writer, object item ) + { + string name = GetName( item ); + if( name != null ) + { + writer.Write( JsonConstants.StartObject ); + writer.Write( Enquote( name ) ); + writer.Write( JsonConstants.NameSeparator ); + } + + writer.Write( JsonConstants.StartObject ); + bool first = true; + + foreach (PropertyInfo pi in item.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(pi => pi.CanRead && pi.GetIndexParameters().Length == 0 && !Ignore(pi))) + { + object value = pi.GetValue(item, null); + object default_value = GetDefaultValue(pi); + + // TODO: allow null? Currently being automagically suppressed + if (value != null && !value.Equals(default_value)) + { + if (!first) + { + writer.Write(JsonConstants.FieldDelimiter); + } + else + { + first = false; + } + + writer.Write(Enquote(GetName(pi))); + writer.Write(JsonConstants.NameSeparator); + SerializeValue(writer, value); + } + + } + writer.Write( JsonConstants.EndObject ); + + if( name != null ) + { + writer.Write( JsonConstants.EndObject ); + } + } + + private void SerializeArray( TextWriter writer, IEnumerable enumerable ) + { + string name = GetName( enumerable ); + if( name != null ) + { + writer.Write( JsonConstants.StartObject ); + writer.Write( Enquote( name ) ); + writer.Write( JsonConstants.NameSeparator ); + } + + writer.Write( JsonConstants.StartArray ); + bool first = true; + foreach( object o in enumerable ) + { + if( !first ) + { + writer.Write( JsonConstants.FieldDelimiter ); + } + else + { + first = false; + } + + SerializeValue( writer, o ); + } + writer.Write( JsonConstants.EndArray ); + + if( name != null ) + { + writer.Write( JsonConstants.EndObject ); + } + } + + private void SerializeValue( TextWriter writer, object o ) + { + if( o == null ) + { + writer.Write( "null" ); + } + else if( o is bool ) + { + writer.Write( ( (bool)o ) ? "true" : "false" ); + } + else if( o is byte ) + { + writer.Write( ( (double)(byte)o ).ToString( CultureInfo.InvariantCulture ) ); + } + else if( o is short ) + { + writer.Write( ( (double)(short)o ).ToString( CultureInfo.InvariantCulture ) ); + } + else if( o is int ) + { + writer.Write( ( (double)(int)o ).ToString( CultureInfo.InvariantCulture ) ); + } + else if( o is long ) + { + writer.Write( ( (double)(long)o ).ToString( CultureInfo.InvariantCulture ) ); + } + else if( o is float ) + { + writer.Write( ( (double)(float)o ).ToString( CultureInfo.InvariantCulture ) ); + } + else if( o is double ) + { + writer.Write( ( (double)o ).ToString( CultureInfo.InvariantCulture ) ); + } + else if( o is string ) + { + writer.Write( Enquote( (string)o ) ); + } + else if( o is IEnumerable ) + { + SerializeArray( writer, (IEnumerable)o ); + } + else + { + SerializeObject( writer, o ); + } + } + + private static string Enquote( string s ) + { + if (s == null || s.Length == 0) + return "\"\""; + + StringBuilder sb = new StringBuilder(s.Length); + sb.Append( '"' ); + for (int i = 0; i < s.Length; ++i) + { + char c = s[i]; + if ((c == '\\') || (c == '"')) + { + sb.Append('\\'); + sb.Append(c); + } + else if (c == '\b') + { + sb.Append("\\b"); + } + else if (c == '\t') + { + sb.Append("\\t"); + } + else if (c == '\n') + { + sb.Append("\\n"); + } + else if (c == '\f') + { + sb.Append("\\f"); + } + else if (c == '\r') + { + sb.Append("\\r"); + } + else if (c < ' ') + { + sb.Append("\\u"); + sb.Append(Convert.ToByte(c).ToString("x2").PadLeft(4, '0')); + } + else + { + sb.Append(c); + } + } + sb.Append( '"' ); + return sb.ToString(); + } + } +} diff --git a/JumpMap.aspx b/JumpMap.aspx new file mode 100644 index 000000000..eaa5c979c --- /dev/null +++ b/JumpMap.aspx @@ -0,0 +1,2 @@ +<%@ Page language="c#" Codebehind="JumpMap.aspx.cs" AutoEventWireup="false" Inherits="Maps.Pages.JumpMap" ValidateRequest="false" %> +<%@ OutputCache Duration="3600" VaryByParam="*" VaryByHeader="Accept"%> diff --git a/JumpMap.aspx.cs b/JumpMap.aspx.cs new file mode 100644 index 000000000..c858a70d9 --- /dev/null +++ b/JumpMap.aspx.cs @@ -0,0 +1,199 @@ +using Maps.Rendering; +using PdfSharp.Drawing; +using System; +using System.Collections.Generic; +using System.Drawing; + +namespace Maps.Pages +{ + /// + /// Summary description for WebForm1. + /// + + public class JumpMap : ImageGeneratorPage + { + private void Page_Load(object sender, System.EventArgs e) + { + if (!ServiceConfiguration.CheckEnabled("jumpmap", Response)) + { + return; + } + + // NOTE: This (re)initializes a static data structure used for + // resolving names into sector locations, so needs to be run + // before any other objects (e.g. Worlds) are loaded. + ResourceManager resourceManager = new ResourceManager(Server, Cache); + + // + // Jump + // + int jump = Util.Clamp(GetIntOption("jump", 6), 0, 12); + + // + // Content & Coordinates + // + Selector selector; + Location loc; + if (Request.HttpMethod == "POST") + { + Sector sector; + try + { + sector = GetPostedSector(); + } + catch (Exception ex) + { + SendError(400, "Invalid request", ex.Message); + return; + } + + if (sector == null) + { + SendError(400, "Invalid request", "Either file or data must be supplied in the POST data."); + return; + } + + int hex = GetIntOption("hex", Astrometrics.SectorCentralHex); + loc = new Location(new Point(0, 0), hex); + selector = new HexSectorSelector(resourceManager, sector, loc.HexLocation, jump); + } + else + { + SectorMap map = SectorMap.FromName(SectorMap.DefaultSetting, resourceManager); + + if (HasOption("sector") && HasOption("hex")) + { + string sectorName = GetStringOption("sector"); + int hex = GetIntOption("hex", 0); + loc = new Location(map.FromName(sectorName).Location, hex); + } + else if (HasOption("sx") && HasOption("sy") && HasOption("hx") && HasOption("hy")) + { + int sx = GetIntOption("sx", 0); + int sy = GetIntOption("sy", 0); + int hx = GetIntOption("hx", 0); + int hy = GetIntOption("hy", 0); + loc = new Location(map.FromLocation(sx, sy).Location, hx * 100 + hy); + } + else if (HasOption("x") && HasOption("y")) + { + loc = Astrometrics.CoordinatesToLocation(GetIntOption("x", 0), GetIntOption("y", 0)); + } + else + { + loc = new Location(map.FromName("Spinward Marches").Location, 1910); + } + selector = new HexSelector(map, resourceManager, loc, jump); + } + + + // + // Scale + // + double scale = Util.Clamp(GetDoubleOption("scale", 64), MinScale, MaxScale); + + // + // Options & Style + // + MapOptions options = MapOptions.BordersMajor | MapOptions.BordersMinor | MapOptions.ForceHexes; + Stylesheet.Style style = Stylesheet.Style.Poster; + ParseOptions(ref options, ref style); + + // + // Border + // + bool border = GetBoolOption("border", defaultValue: true); + + // + // Clip + // + bool clip = GetBoolOption("clip", defaultValue: true); + + // + // What to render + // + + RectangleF tileRect = new RectangleF(); + + Point coords = Astrometrics.LocationToCoordinates(loc); + tileRect.X = coords.X - jump - 1; + tileRect.Width = jump + 1 + jump; + tileRect.Y = coords.Y - jump - 1; + tileRect.Height = jump + 1 + jump; + + // Account for jagged hexes + tileRect.Y += (coords.X % 2 == 0) ? 0 : 0.5f; + tileRect.Inflate(0.35f, 0.15f); + + Size tileSize = new Size((int)Math.Floor(tileRect.Width * scale * Astrometrics.ParsecScaleX), (int)Math.Floor(tileRect.Height * scale * Astrometrics.ParsecScaleY)); + + + // Construct clipping path + List clipPath = new List(jump * 6 + 1); + Point cur = coords; + for (int i = 0; i < jump; ++i) + { + // Move J parsecs to the upper-left (start of border path logic) + cur = Astrometrics.HexNeighbor(cur, 1); + } + clipPath.Add(cur); + for (int dir = 0; dir < 6; ++dir) + { + for (int i = 0; i < jump; ++i) + { + cur = Astrometrics.HexNeighbor(cur, (dir + 3) % 6); // Clockwise from upper left + clipPath.Add(cur); + } + } + + Stylesheet styles = new Stylesheet(scale, options, style); + + // If any names are showing, show them all + if (styles.worldDetails.HasFlag(WorldDetails.KeyNames)) + { + styles.worldDetails |= WorldDetails.AllNames; + } + + // Compute path + float[] edgeX, edgeY; + RenderUtil.HexEdges(styles.microBorderStyle == MicroBorderStyle.Square ? PathUtil.PathType.Square : PathUtil.PathType.Hex, + out edgeX, out edgeY); + PointF[] boundingPathCoords; + byte[] boundingPathTypes; + PathUtil.ComputeBorderPath(clipPath, edgeX, edgeY, out boundingPathCoords, out boundingPathTypes); + + Render.RenderContext ctx = new Render.RenderContext(); + ctx.resourceManager = resourceManager; + ctx.selector = selector; + ctx.tileRect = tileRect; + ctx.scale = scale; + ctx.options = options; + ctx.styles = styles; + ctx.tileSize = tileSize; + ctx.border = border; + + ctx.clipPath = clip ? new XGraphicsPath(boundingPathCoords, boundingPathTypes, XFillMode.Alternate) : null; + ProduceResponse("Jump Map", ctx, tileSize); + } + + #region Web Form Designer generated code + override protected void OnInit(EventArgs e) + { + // + // CODEGEN: This call is required by the ASP.NET Web Form Designer. + // + InitializeComponent(); + base.OnInit(e); + } + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.Load += new System.EventHandler(this.Page_Load); + } + #endregion + } +} diff --git a/JumpWorlds.aspx b/JumpWorlds.aspx new file mode 100644 index 000000000..3c1e3e17f --- /dev/null +++ b/JumpWorlds.aspx @@ -0,0 +1,2 @@ +<%@ Page language="c#" Codebehind="JumpWorlds.aspx.cs" AutoEventWireup="false" Inherits="Maps.Pages.JumpWorlds" %> +<%@ OutputCache Duration="3600" VaryByParam="*" VaryByHeader="Accept"%> diff --git a/JumpWorlds.aspx.cs b/JumpWorlds.aspx.cs new file mode 100644 index 000000000..325e2c241 --- /dev/null +++ b/JumpWorlds.aspx.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Xml; +using System.Xml.Serialization; + +namespace Maps.Pages +{ + /// + /// Summary description for WebForm1. + /// + + public class JumpWorlds : DataPage + { + public override string DefaultContentType { get { return System.Net.Mime.MediaTypeNames.Text.Xml; } } + + private void Page_Load(object sender, System.EventArgs e) + { + if (!ServiceConfiguration.CheckEnabled("jumpworlds", Response)) + { + return; + } + + // NOTE: This (re)initializes a static data structure used for + // resolving names into sector locations, so needs to be run + // before any other objects (e.g. Worlds) are loaded. + ResourceManager resourceManager = new ResourceManager(Server, Cache); + + // + // Jump + // + int jump = Util.Clamp(GetIntOption("jump", 6), 0, 12); + + // + // Coordinates + // + SectorMap map = SectorMap.FromName(SectorMap.DefaultSetting, resourceManager); + Location loc = new Location(map.FromName("Spinward Marches").Location, 1910); + + if (HasOption("sector") && HasOption("hex")) + { + string sectorName = GetStringOption("sector"); + int hex = GetIntOption("hex", 0); + loc = new Location(map.FromName(sectorName).Location, hex); + } + else if (HasOption("sx") && HasOption("sy") && HasOption("hx") && HasOption("hy")) + { + int sx = GetIntOption("sx", 0); + int sy = GetIntOption("sy", 0); + int hx = GetIntOption("hx", 0); + int hy = GetIntOption("hy", 0); + loc = new Location(map.FromLocation(sx, sy).Location, hx * 100 + hy); + } + else if (HasOption("x") && HasOption("y")) + { + loc = Astrometrics.CoordinatesToLocation(GetIntOption("x", 0), GetIntOption("y", 0)); + } + + Selector selector = new HexSelector(map, resourceManager, loc, jump); + + Result data = new Result(); + data.Worlds.AddRange(selector.Worlds); + SendResult(data); + } + + + [XmlRoot(ElementName = "JumpWorlds")] + // public for XML serialization + public class Result + { + public Result() + { + Worlds = new List(); + } + + [XmlElement("World")] + public List Worlds { get; set; } + } + + #region Web Form Designer generated code + override protected void OnInit(EventArgs e) + { + // + // CODEGEN: This call is required by the ASP.NET Web Form Designer. + // + InitializeComponent(); + base.OnInit(e); + } + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.Load += new System.EventHandler(this.Page_Load); + } + #endregion + } +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..0c5975c7c --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,27 @@ +The Traveller Map + +Source Code Copyright (C) 2006-2013 Joshua Bell + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use these files except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +... + +Some source code includes additional copyright and license information. + +... + +The Traveller game in all forms is owned by Far Future Enterprises. Copyright 1977 - 2013 Far Future Enterprises. Traveller is a registered trademark of Far Future Enterprises. Far Future permits web sites and fanzines for this game, provided it contains this notice, that Far Future is notified, and subject to a withdrawal of permission on 90 days notice. The contents of this site are for personal, non-commercial use only. Any use of Far Future Enterprises's copyrighted material or trademarks anywhere on this web site and its files should not be viewed as a challenge to those copyrights or trademarks. In addition, any program/articles/file on this site cannot be republished or distributed without the consent of the author who contributed it. + +GURPS Traveller is a registered trademark of Steve Jackson Games. All rights are reserved by Steve Jackson Games. Any use of Steve Jackson Games' copyrighted material or trademarks anywhere on this Web site and its files should not be viewed as a challenge to those copyrights or trademarks. Any Steve Jackson Games material is used here in accordance with the Steve Jackson Games online policy and notification has been given to Steve Jackson Games in accordance with that policy. + +DGP is a registered trademark of Digest Group Publications. All rights are reserved by Digest Group Publications. Any use of Digest Group Publications' copyrighted material or trademarks anywhere on this Web site and its files should not be viewed as a challenge to those copyrights or trademarks. + +... + +Various data files are Copyright their original authors, noted where possible. + + diff --git a/Location.cs b/Location.cs new file mode 100644 index 000000000..8ab53eddc --- /dev/null +++ b/Location.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Xml.Serialization; +using Json; + +namespace Maps +{ + public struct Location + { + public Location(string sectorName, int hexX, int hexY) + : this() + { + SettingName = SectorMap.DefaultSetting; + + SectorName = sectorName; + + SectorLocation = Point.Empty; + HexLocation = new Point(hexX, hexY); + } + + public Location(string sectorName, int hex) + : this() + { + SettingName = SectorMap.DefaultSetting; + + SectorName = sectorName; + + Hex = hex; + } + + public Location(Point sectorLocation, int hex) + : this() + { + SettingName = SectorMap.DefaultSetting; + + SectorLocation = sectorLocation; + m_sectorName = null; + + Hex = hex; + } + + public Location(Point sectorLocation, Point hexLocation) + : this() + { + SettingName = SectorMap.DefaultSetting; + + SectorLocation = sectorLocation; + m_sectorName = null; + + HexLocation = hexLocation; + } + + private string m_sectorName; + + public string SettingName { get; set; } + public string SectorName { get { return m_sectorName; } set { m_sectorName = value; SectorLocation = SectorMap.FromName(SettingName, value).Location; } } + public int Hex { get { return HexLocation.X * 100 + HexLocation.Y; } set { HexLocation = new Point(value / 100, value % 100); } } + + [XmlIgnore, JsonIgnore] + public Point SectorLocation { get; set; } + + [XmlIgnore, JsonIgnore] + public Point HexLocation { get; set; } + + public override bool Equals(object obj) + { + Location loc = (Location)obj; + + return + (this.SectorLocation == loc.SectorLocation) && + (this.HexLocation == loc.HexLocation); + } + + public static bool operator ==(Location location1, Location location2) { return location1.Equals(location2); } + public static bool operator !=(Location location1, Location location2) { return !location1.Equals(location2); } + + public override int GetHashCode() + { + return SectorLocation.GetHashCode() ^ HexLocation.GetHashCode(); + } + } + + [XmlInclude(typeof(WorldLocation)), XmlInclude(typeof(SubsectorLocation)), XmlInclude(typeof(SectorLocation))] + public abstract class ItemLocation + { + } + + public class WorldLocation : ItemLocation + { + public WorldLocation() { } + + public WorldLocation(Sector sector, World world) + { + if (sector == null) + throw new ArgumentNullException("sector"); + if (world == null) + throw new ArgumentNullException("world"); + + Sector = sector.Location; + World = world.Location; + } + + public WorldLocation(int sector_x, int sector_y, int hex_x, int hex_y) + { + Sector = new Point(sector_x, sector_y); + World = new Point(hex_x, hex_y); + } + + public Point Sector { get; set; } + public Point World { get; set; } + + public void Resolve(SectorMap sectorMap, ResourceManager resourceManager, out Sector sector, out World world) + { + if (sectorMap == null) + { + throw new ArgumentNullException("sectorMap"); + } + + sector = null; + world = null; + + sector = sectorMap.FromLocation(Sector.X, Sector.Y); + if (sector != null) + { + WorldCollection worlds = sector.GetWorlds(resourceManager, cacheResults: true); + if (worlds != null) + { + world = worlds[World.X, World.Y]; + } + } + } + } + + public class SubsectorLocation : ItemLocation + { + public SubsectorLocation() { } + + public SubsectorLocation(Sector sector, Subsector subsector) + { + if (sector == null) + throw new ArgumentNullException("sector"); + if (subsector == null) + throw new ArgumentNullException("subsector"); + + SectorLocation = sector.Location; + Index = subsector.Index[0]; + } + + public SubsectorLocation(int x, int y, char index) + { + SectorLocation = new Point(x, y); + Index = index; + } + + public Point SectorLocation { get; set; } + public char Index { get; set; } + + public void Resolve(SectorMap sectorMap, out Sector sector, out Subsector subsector) + { + if (sectorMap == null) + { + throw new ArgumentNullException("sectorMap"); + } + + sector = null; + subsector = null; + + sector = sectorMap.FromLocation(SectorLocation.X, SectorLocation.Y); + if (sector != null) + { + subsector = sector[Index]; + } + } + } + + public class SectorLocation : ItemLocation + { + public SectorLocation() { } + + public SectorLocation(Sector sector) + { + if (sector == null) + throw new ArgumentNullException("sector"); + + SectorCoords = sector.Location; + } + public SectorLocation(int x, int y) + { + SectorCoords = new Point(x, y); + } + + public Point SectorCoords { get; set; } + + public Sector Resolve(SectorMap sectorMap) + { + if (sectorMap == null) + throw new ArgumentNullException("sectorMap"); + + return sectorMap.FromLocation(SectorCoords.X, SectorCoords.Y); + } + } +} diff --git a/MSEC.aspx b/MSEC.aspx new file mode 100644 index 000000000..91923e4f9 --- /dev/null +++ b/MSEC.aspx @@ -0,0 +1,2 @@ +<%@ Page language="c#" Codebehind="MSEC.aspx.cs" AutoEventWireup="false" Inherits="Maps.Pages.MSEC" %> +<%@ OutputCache Duration="3600" VaryByParam="*" VaryByHeader="Accept"%> diff --git a/MSEC.aspx.cs b/MSEC.aspx.cs new file mode 100644 index 000000000..d50deed88 --- /dev/null +++ b/MSEC.aspx.cs @@ -0,0 +1,91 @@ +using Maps.Serialization; +using System; +using System.IO; +using System.Net.Mime; + +namespace Maps.Pages +{ + /// + /// Summary description for Search. + /// + public class MSEC : DataPage + { + public override string DefaultContentType { get { return System.Net.Mime.MediaTypeNames.Text.Plain; } } + + private void Page_Load(object sender, System.EventArgs e) + { + if (!ServiceConfiguration.CheckEnabled("msec", Response)) + { + return; + } + + // NOTE: This (re)initializes a static data structure used for + // resolving names into sector locations, so needs to be run + // before any other objects (e.g. Worlds) are loaded. + ResourceManager resourceManager = new ResourceManager(Server, Cache); + SectorMap map = SectorMap.FromName(SectorMap.DefaultSetting, resourceManager); + Sector sector; + + if (HasOption("sx") && HasOption("sy")) + { + int sx = GetIntOption("sx", 0); + int sy = GetIntOption("sy", 0); + + sector = map.FromLocation(sx, sy); + + if (sector == null) + { + SendError(404, "Not Found", String.Format("The sector at {0},{1} was not found.", sx, sy)); + return; + } + } + else if (HasOption("sector")) + { + string sectorName = GetStringOption("sector"); + sector = map.FromName(sectorName); + + if (sector == null) + { + SendError(404, "Not Found", String.Format("The specified sector '{0}' was not found.", sectorName)); + return; + } + } + else + { + SendError(404, "Not Found", "No sector specified."); + return; + } + + string data; + using (var writer = new StringWriter()) + { + new MSECSerializer().Serialize(writer, sector); + data = writer.ToString(); + } + + SendResult(data); + } + + + + #region Web Form Designer generated code + override protected void OnInit(EventArgs e) + { + // + // CODEGEN: This call is required by the ASP.NET Web Form Designer. + // + InitializeComponent(); + base.OnInit(e); + } + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.Load += new System.EventHandler(this.Page_Load); + } + #endregion + } +} diff --git a/Maps.csproj b/Maps.csproj new file mode 100644 index 000000000..bd53f72fe --- /dev/null +++ b/Maps.csproj @@ -0,0 +1,849 @@ + + + + + 9.0.30729 + 2.0 + {02AE6284-5044-4A23-B5D6-A86EE9C7DFA1} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Debug + AnyCPU + + + + + Maps + + + JScript + Flow + IE50 + false + Library + Maps + OnBuildSuccess + + + + + + + 4.0 + v4.0 + true + + + + + + + + bin\ + false + 285212672 + false + + + DEBUG;TRACE + + + true + 4096 + false + + + false + false + false + true + 4 + full + prompt + true + + + bin\ + false + 285212672 + false + + + TRACE + + + false + 4096 + false + + + true + false + false + true + 4 + none + prompt + AnyCPU + + + + False + ..\PDFsharp\code\PdfSharp\bin\Release\PdfSharp.dll + + + System + + + + System.Data + + + + System.Drawing + + + System.Web + + + + + + + + System.Web.Mobile + + + + System.XML + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Form + + + Form + + + + + + + + + + + + + Form + + + + + + + + + + + + Designer + + + + + Designer + + + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + + Form + + + + + + + + + + + + + + Form + + + Form + + + + Form + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Form + + + Form + + + + Form + + + + + + Form + + + Form + + + + + + Form + + + + + + + + + + + + + + + + + + + Designer + + + + + + + + + + + + + + + + + + + + + + Form + + + + + + Form + + + Designer + + + Code + + + + Global.asax + + + Overview.aspx + ASPXCodeBehind + + + + Coordinates.aspx + ASPXCodeBehind + + + Codes.aspx + ASPXCodeBehind + + + Admin.aspx + ASPXCodeBehind + + + + + SectorMetaData.aspx + ASPXCodeBehind + + + ASPXCodeBehind + + + Dump.aspx + ASPXCodeBehind + + + ASPXCodeBehind + + + JumpWorlds.aspx + ASPXCodeBehind + + + + + + + + + + + + Universe.aspx + ASPXCodeBehind + + + JumpMap.aspx + ASPXCodeBehind + + + SEC.aspx + ASPXCodeBehind + + + Errors.aspx + ASPXCodeBehind + + + + MSEC.aspx + ASPXCodeBehind + + + Poster.aspx + ASPXCodeBehind + + + Credits.aspx + ASPXCodeBehind + + + Mobile.aspx + ASPXCodeBehind + + + Code + + + Code + + + Search.aspx + ASPXCodeBehind + + + Code + + + Code + + + Code + + + + + Tile.aspx + ASPXCodeBehind + + + + Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + + + + + + True + True + 3454 + / + http://localhost:50103/ + False + False + + + False + + + + + + + \ No newline at end of file diff --git a/Maps.sln b/Maps.sln new file mode 100644 index 000000000..21cc46f04 --- /dev/null +++ b/Maps.sln @@ -0,0 +1,29 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Express 2012 for Web +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Maps", "Maps.csproj", "{02AE6284-5044-4A23-B5D6-A86EE9C7DFA1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "unittests\UnitTests\UnitTests.csproj", "{26F5B96B-4506-43C7-8851-B4B74B78A13C}" + ProjectSection(ProjectDependencies) = postProject + {02AE6284-5044-4A23-B5D6-A86EE9C7DFA1} = {02AE6284-5044-4A23-B5D6-A86EE9C7DFA1} + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {02AE6284-5044-4A23-B5D6-A86EE9C7DFA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02AE6284-5044-4A23-B5D6-A86EE9C7DFA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02AE6284-5044-4A23-B5D6-A86EE9C7DFA1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02AE6284-5044-4A23-B5D6-A86EE9C7DFA1}.Release|Any CPU.Build.0 = Release|Any CPU + {26F5B96B-4506-43C7-8851-B4B74B78A13C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26F5B96B-4506-43C7-8851-B4B74B78A13C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26F5B96B-4506-43C7-8851-B4B74B78A13C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26F5B96B-4506-43C7-8851-B4B74B78A13C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Metadata.cs b/Metadata.cs new file mode 100644 index 000000000..0acde02af --- /dev/null +++ b/Metadata.cs @@ -0,0 +1,86 @@ +using System; +using System.Xml.Serialization; +using System.Collections.Generic; + +namespace Maps +{ + public interface IMetadata + { + [XmlAttribute] + string Title { get; set; } + + [XmlAttribute] + string Author { get; set; } + + [XmlAttribute] + string Source { get; set; } + + [XmlAttribute] + string Publisher { get; set; } + + [XmlAttribute] + string Copyright { get; set; } + + [XmlAttribute] + string Era { get; set; } + + [XmlAttribute] + string Ref { get; set; } + } + + public abstract class MetadataItem : IMetadata + { + private Dictionary m_metaData = new Dictionary(); + + [XmlAttribute] + public string Author { get { string s; m_metaData.TryGetValue("author", out s); return s; } set { m_metaData["author"] = value; } } + + [XmlAttribute] + public string Source { get { string s; m_metaData.TryGetValue("source", out s); return s; } set { m_metaData["source"] = value; } } + + [XmlAttribute] + public string Title{ get { string s; m_metaData.TryGetValue("title", out s); return s; } set { m_metaData["title"] = value; } } + + [XmlAttribute] + public string Publisher { get { string s; m_metaData.TryGetValue("publisher", out s); return s; } set { m_metaData["publisher"] = value; } } + + [XmlAttribute] + public string Copyright { get { string s; m_metaData.TryGetValue("copyright", out s); return s; } set { m_metaData["copyright"] = value; } } + + [XmlAttribute] + public string Era { get { string s; m_metaData.TryGetValue("era", out s); return s; } set { m_metaData["era"] = value; } } + + [XmlAttribute] + public string Ref { get { string s; m_metaData.TryGetValue("ref", out s); return s; } set { m_metaData["ref"] = value; } } + } + + public class MetadataCollection : List, IMetadata + { + #region IMetadata Members + + private Dictionary m_metaData = new Dictionary(); + + [XmlAttribute] + public string Author { get { string s; m_metaData.TryGetValue("author", out s); return s; } set { m_metaData["author"] = value; } } + + [XmlAttribute] + public string Title { get { string s; m_metaData.TryGetValue("title", out s); return s; } set { m_metaData["title"] = value; } } + + [XmlAttribute] + public string Source { get { string s; m_metaData.TryGetValue("source", out s); return s; } set { m_metaData["source"] = value; } } + + [XmlAttribute] + public string Publisher { get { string s; m_metaData.TryGetValue("publisher", out s); return s; } set { m_metaData["publisher"] = value; } } + + [XmlAttribute] + public string Copyright { get { string s; m_metaData.TryGetValue("copyright", out s); return s; } set { m_metaData["copyright"] = value; } } + + [XmlAttribute] + public string Era { get { string s; m_metaData.TryGetValue("era", out s); return s; } set { m_metaData["era"] = value; } } + + [XmlAttribute] + public string Ref { get { string s; m_metaData.TryGetValue("ref", out s); return s; } set { m_metaData["ref"] = value; } } + + #endregion + } +} \ No newline at end of file diff --git a/Mobile.aspx b/Mobile.aspx new file mode 100644 index 000000000..f69910867 --- /dev/null +++ b/Mobile.aspx @@ -0,0 +1,87 @@ +<%@ Page language="c#" Codebehind="Mobile.aspx.cs" AutoEventWireup="false" Inherits="Maps.Pages.MobilePage" EnableViewStateMac="false" codePage="65001"%> + + + + Traveller Map + + + + + + + + + + + + + + +
+
+

+ + + + + + + + + + + +

+

+
+
+ +

+

+ + +

+
+

+ + + +

+

+ + +

+ +
+ (<%# DataBinder.Eval(Container, "DataItem.Details") %>) +

+ + + No matches found +

+
+

+ + Small (128x128) + Medium (192x192) + Large (256x256) + Huge (384x384) + +

+
+
+ + diff --git a/Mobile.aspx.cs b/Mobile.aspx.cs new file mode 100644 index 000000000..8b905c34e --- /dev/null +++ b/Mobile.aspx.cs @@ -0,0 +1,463 @@ +using Maps.Rendering; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Web.UI.WebControls; +using System.Xml.Serialization; + +namespace Maps.Pages +{ + /// + /// Summary description for MobilePage. + /// + public class MobilePage : DataPage + { + public override string DefaultContentType { get { throw new NotImplementedException(); } } + + protected System.Web.UI.HtmlControls.HtmlForm Form1; + protected System.Web.UI.WebControls.ImageMap MapImage; + protected System.Web.UI.WebControls.Button ButtonScrollCoreward; + protected System.Web.UI.WebControls.Button ButtonScrollSpinward; + protected System.Web.UI.WebControls.Button ButtonScrollTrailing; + protected System.Web.UI.WebControls.Button ButtonScrollRimward; + protected System.Web.UI.WebControls.Button ButtonZoomIn; + protected System.Web.UI.WebControls.Button ButtonZoomOut; + protected System.Web.UI.WebControls.TextBox TextBoxSearch; + protected System.Web.UI.WebControls.Button ButtonSearch; + protected System.Web.UI.WebControls.Button ButtonJump; + protected System.Web.UI.WebControls.DataList ResultsDataList; + protected System.Web.UI.WebControls.Label LabelNoResults; + protected System.Web.UI.WebControls.DropDownList DropDownTileSize; + + private const int SmallTileDimension = 128; + private const int LargeTileDimension = 192; + private const int MaxTileDimension = 512; + + private string MakeURL() + { + double x = (double)ViewState["x"]; + double y = (double)ViewState["y"]; + int w = (int)ViewState["w"]; + int h = (int)ViewState["h"]; + double scale = (double)ViewState["scale"]; + + double tx = (x * (scale * Astrometrics.ParsecScaleX) - (w / 2)) / w; + double ty = (y * (scale * Astrometrics.ParsecScaleY) - (h / 2)) / h; + + return "Tile.aspx" + + "?x=" + tx + + "&y=" + ty + + "&w=" + w + + "&h=" + h + + "&scale=" + scale + + "&options=" + (int)(MapOptions)ViewState["options"] + + "&style=" + ViewState["style"]; + } + + private void Scroll(double dx, double dy) + { + double x = (double)ViewState["x"]; + double y = (double)ViewState["y"]; + int w = (int)ViewState["w"]; + int h = (int)ViewState["h"]; + double scale = (double)ViewState["scale"]; + + ViewState["x"] = x + (dx / scale) * w / Astrometrics.ParsecScaleX; + ViewState["y"] = y + (dy / scale) * h / Astrometrics.ParsecScaleY; + + Refresh(); + } + + private void ZoomIn() + { + SetScale((double)ViewState["scale"] * 2); + } + + private void ZoomOut() + { + SetScale((double)ViewState["scale"] / 2); + } + + private void SetScale(double scale) + { + if (scale < ImageGeneratorPage.MinScale) + scale = ImageGeneratorPage.MinScale; + if (scale > ImageGeneratorPage.MaxScale) + scale = ImageGeneratorPage.MaxScale; + + ViewState["scale"] = scale; + Refresh(); + } + + private void SetTileSize(int value) + { + ViewState["w"] = value; + ViewState["h"] = value; + + Refresh(); + } + + private void Refresh() + { + MapImage.ImageUrl = MakeURL(); + + MapImage.BackColor = Color.Black; + + int w = (int)ViewState["w"]; + int h = (int)ViewState["h"]; + + MapImage.Width = w; + MapImage.Height = h; + + int w1 = w / 3; + int w2 = w * 2 / 3; + int h1 = h / 3; + int h2 = h * 2 / 3; + + foreach (HotSpot hotSpot in MapImage.HotSpots) + { + RectangleHotSpot rect = hotSpot as RectangleHotSpot; + if (rect == null) + return; + switch (rect.PostBackValue) + { + case "0": rect.Left = 0; rect.Right = w1; rect.Top = 0; rect.Bottom = h1; break; + case "1": rect.Left = w1; rect.Right = w2; rect.Top = 0; rect.Bottom = h1; break; + case "2": rect.Left = w2; rect.Right = w; rect.Top = 0; rect.Bottom = h1; break; + case "3": rect.Left = 0; rect.Right = w1; rect.Top = h1; rect.Bottom = h2; break; + case "4": rect.Left = w1; rect.Right = w2; rect.Top = h1; rect.Bottom = h2; break; + case "5": rect.Left = w2; rect.Right = w; rect.Top = h1; rect.Bottom = h2; break; + case "6": rect.Left = 0; rect.Right = w1; rect.Top = h2; rect.Bottom = h; break; + case "7": rect.Left = w1; rect.Right = w2; rect.Top = h2; rect.Bottom = h; break; + case "8": rect.Left = w2; rect.Right = w; rect.Top = h2; rect.Bottom = h; break; + } + } + } + + private void Page_Load(object sender, System.EventArgs e) + { + // Put user code to initialize the page here + + // Hitting Enter in the text field should do "Search" - this works cross-browser + this.TextBoxSearch.Attributes.Add( + "onKeyPress", + "if((event.keyCode||event.which)==13){document.getElementById('" + ButtonSearch.ClientID + "').click();return false;}"); + + // Make Search the default button + ClientScript.RegisterHiddenField("__EVENTTARGET", ButtonSearch.ID); + + if (!this.IsPostBack) + { + // ViewState should be pay-for-play - it's enabled on first search + Form1.EnableViewState = false; + + ViewState["x"] = -0.5; + ViewState["y"] = -0.5; + ViewState["scale"] = 1.0; + ViewState["options"] = MapOptions.SectorGrid | MapOptions.SubsectorGrid | MapOptions.SectorsAll | MapOptions.BordersMajor | MapOptions.BordersMinor | MapOptions.NamesMajor | MapOptions.WorldsCapitals | MapOptions.WorldsHomeworlds; + ViewState["style"] = "poster"; + + if (Request.Cookies["MobileMapSize"] != null) + { + string val = Request.Cookies["MobileMapSize"].Value; + + // back compat + if (val == "Large") val = "192"; + if (val == "Small") val = "128"; + this.DropDownTileSize.SelectedValue = val; + } + else + { + this.DropDownTileSize.SelectedValue = LargeTileDimension.ToString(CultureInfo.InvariantCulture); + } + } + + int dim; + Int32.TryParse(DropDownTileSize.SelectedValue, out dim); + dim = Util.Clamp(dim, SmallTileDimension, MaxTileDimension); + + ViewState["w"] = dim; + ViewState["h"] = dim; + + Refresh(); + } + + #region Web Form Designer generated code + override protected void OnInit(EventArgs e) + { + // + // CODEGEN: This call is required by the ASP.NET Web Form Designer. + // + InitializeComponent(); + base.OnInit(e); + } + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.ButtonScrollCoreward.Click += new System.EventHandler(this.BtnCoreward_Click); + this.ButtonScrollSpinward.Click += new System.EventHandler(this.BtnSpinward_Click); + this.ButtonScrollTrailing.Click += new System.EventHandler(this.BtnTrailing_Click); + this.ButtonScrollRimward.Click += new System.EventHandler(this.BtnRimward_Click); + this.ButtonZoomIn.Click += new System.EventHandler(this.BtnZoomIn_Click); + this.ButtonZoomOut.Click += new System.EventHandler(this.BtnZoomOut_Click); + this.ButtonSearch.Click += new System.EventHandler(this.BtnSearch_Click); + this.ButtonJump.Click += new System.EventHandler(this.BtnJump_Click); + this.ResultsDataList.ItemCommand += new System.Web.UI.WebControls.DataListCommandEventHandler(this.ResultsDataList_ItemCommand); + this.DropDownTileSize.SelectedIndexChanged += new EventHandler(DropDownTileSize_SelectedIndexChanged); + this.Load += new System.EventHandler(this.Page_Load); + } + #endregion + + private const double SCROLL_OFFSET = 0.4; + + private void BtnCoreward_Click(object sender, System.EventArgs e) + { + Scroll(0, -SCROLL_OFFSET); + } + + private void BtnSpinward_Click(object sender, System.EventArgs e) + { + Scroll(-SCROLL_OFFSET, 0); + } + + private void BtnTrailing_Click(object sender, System.EventArgs e) + { + Scroll(SCROLL_OFFSET, 0); + } + + private void BtnRimward_Click(object sender, System.EventArgs e) + { + Scroll(0, SCROLL_OFFSET); + } + + private void BtnZoomIn_Click(object sender, System.EventArgs e) + { + ZoomIn(); + } + + private void BtnZoomOut_Click(object sender, System.EventArgs e) + { + ZoomOut(); + } + + private void BtnSearch_Click(object sender, System.EventArgs e) + { + // ViewState should be pay-for-play - it's enabled on first search + Form1.EnableViewState = true; + + string query = TextBoxSearch.Text; + if (String.IsNullOrEmpty(query)) + return; + + ResourceManager resourceManager = new ResourceManager(Server, Cache); + SectorMap map = SectorMap.FromName(SectorMap.DefaultSetting, resourceManager); + + IEnumerable results = SearchEngine.PerformSearch(query, resourceManager, SearchEngine.SearchResultsType.Default, 20); + + DataTable dt = new DataTable(); + dt.Locale = CultureInfo.InvariantCulture; + dt.Columns.Add(new DataColumn("Name", typeof(string))); + dt.Columns.Add(new DataColumn("Details", typeof(string))); + dt.Columns.Add(new DataColumn("Data", typeof(string))); + + if (results != null) + { + foreach (object o in results) + { + StringWriter sw = new StringWriter(CultureInfo.InvariantCulture); + // NOTE: Serialize explicitly using base class, so deserialization handles heterogenous types + XmlSerializer xs = new XmlSerializer(typeof(ItemLocation)); + xs.Serialize(sw, o); + string xml = sw.ToString(); + + DataRow dr = dt.NewRow(); + if (o is WorldLocation) + { + Sector sector; + World world; + ((WorldLocation)o).Resolve(map, resourceManager, out sector, out world); + + dr[0] = world.Name; + dr[1] = String.Format(CultureInfo.InvariantCulture, "{0} {1}", sector.Names[0].Text, world.Hex.ToString("0000", CultureInfo.InvariantCulture)); + dr[2] = xml; + } + else if (o is SubsectorLocation) + { + Sector sector; + Subsector subsector; + ((SubsectorLocation)o).Resolve(map, out sector, out subsector); + + dr[0] = subsector.Name; + dr[1] = String.Format(CultureInfo.InvariantCulture, "Subsector {1} of {0}", sector.Names[0].Text, subsector.Index); + dr[2] = xml; + } + else if (o is SectorLocation) + { + Sector sector = ((SectorLocation)o).Resolve(map); + + dr[0] = sector.Names[0].Text; + dr[1] = "Sector"; + dr[2] = xml; + + } + dt.Rows.Add(dr); + } + + LabelNoResults.Visible = false; + } + else + { + LabelNoResults.Visible = true; + LabelNoResults.Text = "No matches found"; + } + + this.ResultsDataList.DataSource = new DataView(dt); + this.ResultsDataList.DataBind(); + } + + private void BtnJump_Click(object sender, System.EventArgs e) + { + string query = TextBoxSearch.Text; + if (String.IsNullOrEmpty(query)) + return; + + ResourceManager resourceManager = new ResourceManager(Server, Cache); + IEnumerable results = SearchEngine.PerformSearch(query, resourceManager, SearchEngine.SearchResultsType.Worlds, 20); + + // Clear previous search results, if any + ResultsDataList.DataSource = null; + ResultsDataList.DataBind(); + + WorldLocation jump = null; + double distanceSquared = Double.PositiveInfinity; + + if (results != null) + { + foreach (object o in results) + { + WorldLocation worldLocation = o as WorldLocation; + if (worldLocation != null) + { + Point coords = Astrometrics.LocationToCoordinates(worldLocation.Sector, worldLocation.World); + double x = coords.X - 0.5; + double y = coords.Y - (((coords.X % 2) == 0) ? 0.5 : 0); + + double dx = x - (double)ViewState["x"]; + double dy = y - (double)ViewState["y"]; + + double d = dx * dx + dy * dy; + + if (d < distanceSquared) + { + jump = worldLocation; + distanceSquared = d; + } + } + } + } + + + if (jump != null) + { + Point coords = Astrometrics.LocationToCoordinates(jump.Sector, jump.World); + double x = coords.X - 0.5; + double y = coords.Y - (((coords.X % 2) == 0) ? 0.5 : 0); + + ViewState["x"] = x; + ViewState["y"] = y; + ViewState["scale"] = (double)64; + + Refresh(); + } + else + { + LabelNoResults.Visible = true; + LabelNoResults.Text = "No matches found"; + } + } + + private void ResultsDataList_ItemCommand(object source, System.Web.UI.WebControls.DataListCommandEventArgs e) + { + string xml = e.CommandArgument as String; + XmlSerializer xs = new XmlSerializer(typeof(ItemLocation)); + + object o = xs.Deserialize(new StringReader(xml)); + + if (o is WorldLocation) + { + WorldLocation worldLocation = o as WorldLocation; + Point coords = Astrometrics.LocationToCoordinates(worldLocation.Sector, worldLocation.World); + + double x = coords.X - 0.5; + double y = coords.Y - (((coords.X % 2) == 0) ? 0.5 : 0); + + ViewState["x"] = x; + ViewState["y"] = y; + ViewState["scale"] = (double)64; + } + else if (o is SubsectorLocation) + { + SubsectorLocation subsectorLocation = o as SubsectorLocation; + + int nIndex = (subsectorLocation.Index - 'A'); + int hx = (int)((Math.Floor(nIndex % 4.0) + 0.5) * (Astrometrics.SectorWidth / 4)); + int hy = (int)((Math.Floor(nIndex / 4.0) + 0.5) * (Astrometrics.SectorHeight / 4)); + + Point coords = Astrometrics.LocationToCoordinates(subsectorLocation.SectorLocation, new Point(hx, hy)); + + ViewState["x"] = (double)coords.X; + ViewState["y"] = (double)coords.Y; + ViewState["scale"] = (double)32; + } + else if (o is SectorLocation) + { + SectorLocation sectorLocation = o as SectorLocation; + + Point coords = Astrometrics.LocationToCoordinates(sectorLocation.SectorCoords, new Point(Astrometrics.SectorWidth / 2, Astrometrics.SectorHeight / 2)); + + ViewState["x"] = (double)coords.X; + ViewState["y"] = (double)coords.Y; + ViewState["scale"] = (double)6; + } + + Refresh(); + } + + protected void MapImage_Click(object sender, ImageMapEventArgs e) + { + switch (e.PostBackValue) + { + case "0": Scroll(-0.5, -0.5); break; + case "1": Scroll(0, -0.5); break; + case "2": Scroll(0.5, -0.5); break; + case "3": Scroll(-0.5, 0); break; + case "4": ZoomIn(); break; + case "5": Scroll(0.5, 0); break; + case "6": Scroll(-0.5, 0.5); break; + case "7": Scroll(0, 0.5); break; + case "8": Scroll(0.5, 0.5); break; + } + } + + protected void DropDownTileSize_SelectedIndexChanged(object sender, EventArgs e) + { + int dim; + Int32.TryParse(DropDownTileSize.SelectedValue, out dim); + dim = Util.Clamp(dim, SmallTileDimension, MaxTileDimension); + + SetTileSize(dim); + + Response.Cookies["MobileMapSize"].Value = dim.ToString(CultureInfo.InvariantCulture); + Response.Cookies["MobileMapSize"].Expires = DateTime.MaxValue; + } + + } +} diff --git a/Overview.aspx b/Overview.aspx new file mode 100644 index 000000000..e2f124453 --- /dev/null +++ b/Overview.aspx @@ -0,0 +1,2 @@ +<%@ Page language="c#" Codebehind="Overview.aspx.cs" AutoEventWireup="false" Inherits="Maps.Pages.Overview" %> + diff --git a/Overview.aspx.cs b/Overview.aspx.cs new file mode 100644 index 000000000..cbbd52971 --- /dev/null +++ b/Overview.aspx.cs @@ -0,0 +1,89 @@ +using Maps.Rendering; +using System; +using System.Drawing; + +namespace Maps.Pages +{ + public class Overview : ImageGeneratorPage + { + public const int MinDimension = 64; + public const int MaxDimension = 2048; + + public const int NormalTileWidth = 256; + public const int NormalTileHeight = 256; + + + private void Page_Load(object sender, System.EventArgs e) + { + ResourceManager resourceManager = new ResourceManager(Server, Cache); + + MapOptions options = MapOptions.SectorGrid | MapOptions.FilledBorders; + Stylesheet.Style style = Stylesheet.Style.Poster; + ParseOptions(ref options, ref style); + + double x = -0.5; + double y = -0.5; + double scale = 2; + Size tileSize = new Size(1000, 1000); + + RectangleF tileRect = new RectangleF(); + tileRect.X = (float)(x * tileSize.Width / (scale * Astrometrics.ParsecScaleX)); + tileRect.Y = (float)(y * tileSize.Height / (scale * Astrometrics.ParsecScaleY)); + tileRect.Width = (float)(tileSize.Width / (scale * Astrometrics.ParsecScaleX)); + tileRect.Height = (float)(tileSize.Height / (scale * Astrometrics.ParsecScaleY)); + + DateTime dt = DateTime.Now; + + Render.RenderContext ctx = new Render.RenderContext(); + ctx.resourceManager = resourceManager; + ctx.selector = new RectSelector( + SectorMap.FromName(SectorMap.DefaultSetting, resourceManager), + resourceManager, + tileRect); + ctx.tileRect = tileRect; + ctx.scale = scale; + ctx.options = options; + ctx.styles = new Stylesheet(scale, options, style); + ctx.tileSize = tileSize; + ctx.silly = false; + ctx.tiling = true; + + ctx.styles.microRoutes.visible = true; + ctx.styles.macroRoutes.visible = false; + ctx.styles.macroBorders.visible = false; + ctx.styles.microBorders.visible = true; + ctx.styles.capitals.visible = false; + ctx.styles.worlds.visible = true; + ctx.styles.worldDetails = WorldDetails.Dotmap; + ctx.styles.showAllSectorNames = false; + ctx.styles.showSomeSectorNames = false; + ctx.styles.macroNames.visible = false; + ctx.styles.pseudoRandomStars.visible = false; + ctx.styles.fillMicroBorders = true; + + ProduceResponse("Overview", ctx, tileSize); + } + + + #region Web Form Designer generated code + override protected void OnInit(EventArgs e) + { + // + // CODEGEN: This call is required by the ASP.NET Web Form Designer. + // + InitializeComponent(); + base.OnInit(e); + } + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.Load += new System.EventHandler(this.Page_Load); + } + #endregion + + } +} diff --git a/Poster.aspx b/Poster.aspx new file mode 100644 index 000000000..79bd3d8a1 --- /dev/null +++ b/Poster.aspx @@ -0,0 +1,2 @@ +<%@ Page language="c#" Codebehind="Poster.aspx.cs" AutoEventWireup="false" Inherits="Maps.Pages.Poster" ValidateRequest="false" %> +<%@ OutputCache Duration="3600" VaryByParam="*" VaryByHeader="Accept"%> diff --git a/Poster.aspx.cs b/Poster.aspx.cs new file mode 100644 index 000000000..74f74fbd6 --- /dev/null +++ b/Poster.aspx.cs @@ -0,0 +1,200 @@ +using Maps.Rendering; +using System; +using System.Drawing; + +namespace Maps.Pages +{ + /// + /// Summary description for WebForm1. + /// + + public class Poster : ImageGeneratorPage + { + private void Page_Load(object sender, System.EventArgs e) + { + if (!ServiceConfiguration.CheckEnabled("poster", Response)) + { + return; + } + + // NOTE: This (re)initializes a static data structure used for + // resolving names into sector locations, so needs to be run + // before any other objects (e.g. Worlds) are loaded. + ResourceManager resourceManager = new ResourceManager(Server, Cache); + + Selector selector; + RectangleF tileRect = new RectangleF(); + MapOptions options = MapOptions.SectorGrid | MapOptions.SubsectorGrid | MapOptions.BordersMajor | MapOptions.BordersMinor | MapOptions.NamesMajor | MapOptions.NamesMinor | MapOptions.WorldsCapitals | MapOptions.WorldsHomeworlds; + Stylesheet.Style style = Stylesheet.Style.Poster; + ParseOptions(ref options, ref style); + string title; + + if (HasOption("x1") && HasOption("x2") && + HasOption("y1") && HasOption("y2")) + { + // Arbitrary rectangle + + int x1 = GetIntOption("x1", 0); + int x2 = GetIntOption("x1", 0); + int y1 = GetIntOption("x1", 0); + int y2 = GetIntOption("x1", 0); + + tileRect.X = Math.Min(x1, x2); + tileRect.Y = Math.Min(y1, y2); + tileRect.Width = Math.Max(x1, x2) - tileRect.X; + tileRect.Height = Math.Max(y1, y2) - tileRect.Y; + + SectorMap map = SectorMap.FromName(SectorMap.DefaultSetting, resourceManager); + selector = new RectSelector(map, resourceManager, tileRect); + selector.Slop = false; + + tileRect.Offset(-1, -1); + tileRect.Width += 1; + tileRect.Height += 1; + + title = String.Format("Poster ({0},{1}) - ({2},{3})", x1, y1, x2, y2); + } + else + { + // Sector - either POSTed or specified by name + Sector sector = null; + options = options & ~MapOptions.SectorGrid; + + if (Request.HttpMethod == "POST") + { + try + { + sector = GetPostedSector(); + } + catch (Exception ex) + { + SendError(400, "Invalid request", ex.Message); + return; + } + + if (sector == null) + { + SendError(400, "Invalid request", "Either file or data must be supplied in the POST data."); + return; + } + + title = "User Data"; + } + else + { + string sectorName = GetStringOption("sector"); + if (sectorName == null) + { + SendError(404, "Not Found", "No sector specified."); + return; + } + + SectorMap map = SectorMap.FromName(SectorMap.DefaultSetting, resourceManager); + + sector = map.FromName(sectorName); + if (sector == null) + { + SendError(404, "Not Found", String.Format("The specified sector '{0}' was not found.", sectorName)); + return; + } + + title = sector.Names[0].Text; + } + + if (HasOption("subsector") && GetStringOption("subsector").Length > 0) + { + options = options & ~MapOptions.SubsectorGrid; + char ss = GetStringOption("subsector").ToUpperInvariant()[0]; + if (ss < 'A' || ss > 'P') + { + SendError(400, "Invalid subsector", String.Format("The subsector index '{0}' is not valid (must be A...P).", ss)); + return; + } + + int index = (int)(ss) - (int)('A'); + selector = new SubsectorSelector(resourceManager, sector, index); + + tileRect = sector.SubsectorBounds(index); + + options &= ~(MapOptions.SectorGrid | MapOptions.SubsectorGrid); + + title = String.Format("{0} - Subsector {1}", title, ss); + } + else + { + selector = new SectorSelector(resourceManager, sector); + tileRect = sector.Bounds; + + options &= ~(MapOptions.SectorGrid); + } + + // Account for jagged hexes + tileRect.X -= 0.25f; + tileRect.Width += 0.5f; + tileRect.Height += 0.5f; + + if (style == Stylesheet.Style.Candy) + { + tileRect.Width += 0.75f; + } + + } + + const double NormalScale = 64; // pixels/parsec - standard subsector-rendering scale + double scale = Util.Clamp(GetDoubleOption("scale", NormalScale), MinScale, MaxScale); + + int rot = GetIntOption("rotation", 0) % 4; + + Size tileSize = new Size((int)Math.Floor(tileRect.Width * scale * Astrometrics.ParsecScaleX), (int)Math.Floor(tileRect.Height * scale * Astrometrics.ParsecScaleY)); + + int bitmapWidth = tileSize.Width, bitmapHeight = tileSize.Height; + float translateX = 0, translateY = 0, angle = rot * 90; + switch (rot) + { + case 1: // 90 degrees clockwise + bitmapWidth = tileSize.Height; bitmapHeight = tileSize.Width; + translateX = bitmapWidth; + break; + case 2: // 180 degrees + translateX = tileSize.Width; translateY = tileSize.Height; + break; + case 3: // 270 degrees clockwise + bitmapWidth = tileSize.Height; bitmapHeight = tileSize.Width; + translateY = bitmapHeight; + break; + } + + Render.RenderContext ctx = new Render.RenderContext(); + ctx.resourceManager = resourceManager; + ctx.selector = selector; + ctx.tileRect = tileRect; + ctx.scale = scale; + ctx.options = options; + ctx.styles = new Stylesheet(scale, options, style); + ctx.tileSize = tileSize; + ProduceResponse(title, ctx, new Size(bitmapWidth, bitmapHeight), rot, translateX, translateY); + } + + + + #region Web Form Designer generated code + override protected void OnInit(EventArgs e) + { + // + // CODEGEN: This call is required by the ASP.NET Web Form Designer. + // + InitializeComponent(); + base.OnInit(e); + } + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.Load += new System.EventHandler(this.Page_Load); + } + #endregion + } +} diff --git a/Render.cs b/Render.cs new file mode 100644 index 000000000..bd871ddb4 --- /dev/null +++ b/Render.cs @@ -0,0 +1,1445 @@ +//#define SHOW_TIMING + +using PdfSharp.Drawing; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Globalization; +using System.Linq; + +namespace Maps.Rendering +{ + /// + /// Summary description for Render. + /// + public static class Render + { + private static readonly string[] borderFiles = { + @"~/res/Vectors/Imperium.xml", + @"~/res/Vectors/Aslan.xml", + @"~/res/Vectors/Kkree.xml", + @"~/res/Vectors/Vargr.xml", + @"~/res/Vectors/Zhodani.xml", + @"~/res/Vectors/Solomani.xml", + @"~/res/Vectors/Hive.xml", + @"~/res/Vectors/SpinwardClient.xml", + @"~/res/Vectors/RimwardClient.xml", + @"~/res/Vectors/TrailingClient.xml" + }; + + private static readonly string[] riftFiles = { + @"~/res/Vectors/GreatRift.xml", + @"~/res/Vectors/LesserRift.xml", + @"~/res/Vectors/WindhornRift.xml", + @"~/res/Vectors/DelphiRift.xml", + @"~/res/Vectors/ZhdantRift.xml" + }; + + private static readonly string[] routeFiles = { + @"~/res/Vectors/J5Route.xml", + @"~/res/Vectors/J4Route.xml", + @"~/res/Vectors/CoreRoute.xml" + }; + + private static XImage s_sillyImageColor; + private static XImage s_sillyImageGray; + + private static XImage s_backgroundImage; + + // These are loaded as GDI+ Images since we need to derive alpha-variants of them; + // the results are cached as PDFSharp Images (XImage) + private static Image s_galaxyImage; + private static Image s_riftImage; + private static Dictionary s_worldImages; + + public class RenderContext + { + public ResourceManager resourceManager = null; + public Selector selector = null; + public XGraphics graphics = null; + public RectangleF tileRect; + public double scale = double.NaN; + public MapOptions options = 0; + public Stylesheet styles = null; + public Size tileSize; + public XGraphicsPath clipPath = null; + public bool border = false; + public bool silly = false; + public bool tiling = false; + + public XMatrix ImageSpaceToWorldSpace + { + get + { + XMatrix m = new XMatrix(); + m.TranslatePrepend((float)(-this.tileRect.Left * this.scale * Astrometrics.ParsecScaleX), (float)(-this.tileRect.Top * this.scale * Astrometrics.ParsecScaleY)); + m.ScalePrepend((float)this.scale * Astrometrics.ParsecScaleX, (float)this.scale * Astrometrics.ParsecScaleY); + return m; + } + } + } + + /// + /// Performance timer record + /// + private class Timer + { + public DateTime dt; + public string label; + public Timer(string label) + { + this.dt = DateTime.Now; + this.label = label; + } + } + + public static void RenderTile(RenderContext ctx) + { + DateTime dtStart = DateTime.Now; + List timers = new List(); + + if (ctx.resourceManager == null) + throw new ArgumentNullException("resourceManager"); + + if (ctx.graphics == null) + throw new ArgumentNullException("graphics"); + + if (ctx.selector == null) + throw new ArgumentNullException("selector"); + + XSolidBrush solidBrush = new XSolidBrush(); + XPen pen = new XPen(XColor.Empty); + + using (var fonts = new FontCache(ctx.styles)) + { + #region resources + lock (ctx.resourceManager.GetType()) + { + if (ctx.styles.useBackgroundImage && s_backgroundImage == null) + { + s_backgroundImage = XImage.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/Candy/Nebula.png")); + } + if (ctx.styles.showRifts && s_riftImage == null) + { + s_riftImage = Image.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/Candy/Rifts.png")); + } + if (ctx.styles.useGalaxyImage && s_galaxyImage == null) + { + s_galaxyImage = Image.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/Candy/Galaxy.png")); + } + if (ctx.styles.useWorldImages && s_worldImages == null) + { + s_worldImages = new Dictionary { + { "Hyd0", XImage.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/Candy/Hyd0.png")) }, + { "Hyd1", XImage.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/Candy/Hyd1.png")) }, + { "Hyd2", XImage.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/Candy/Hyd2.png")) }, + { "Hyd3", XImage.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/Candy/Hyd3.png")) }, + { "Hyd4", XImage.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/Candy/Hyd4.png")) }, + { "Hyd5", XImage.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/Candy/Hyd5.png")) }, + { "Hyd6", XImage.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/Candy/Hyd6.png")) }, + { "Hyd7", XImage.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/Candy/Hyd7.png")) }, + { "Hyd8", XImage.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/Candy/Hyd8.png")) }, + { "Hyd9", XImage.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/Candy/Hyd9.png")) }, + { "HydA", XImage.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/Candy/HydA.png")) }, + { "Belt", XImage.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/Candy/Belt.png")) } + }; + } + + if (ctx.silly && s_sillyImageColor == null) + { + // Happy face c/o http://bighappyfaces.com/ + s_sillyImageColor = XImage.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/AprilFools/Starburst.png")); + s_sillyImageGray = XImage.FromFile(ctx.resourceManager.Server.MapPath(@"~/res/AprilFools/Starburst_Gray.png")); + } + } + #endregion + + timers.Add(new Timer("preload")); + ////////////////////////////////////////////////////////////// + // + // Image-Space Rendering + // + ////////////////////////////////////////////////////////////// + + // Fill + ctx.graphics.SmoothingMode = XSmoothingMode.HighSpeed; + solidBrush.Color = ctx.styles.backgroundColor; + ctx.graphics.DrawRectangle(solidBrush, 0, 0, ctx.tileSize.Width, ctx.tileSize.Height); + + + //// Draw tile # + //using( var font = new Font( FontFamily.GenericSansSerif, 10 ) ) + //{ + // graphics.DrawString( String.Format( "({0},{1})", x, y ), font, foregroundBrush, 0, 0 ); + // graphics.DrawString( String.Format( "{0},{1}-{2}x{3}", tileRect.X, tileRect.Y, tileRect.Width, tileRect.Height ), font, foregroundBrush, 0, 20 ); + //} + + // Frame it + //graphics.DrawRectangle( Pens.Green, 0, 0, tileSize.Width-1, tileSize.Height-1 ); + + timers.Add(new Timer("imagespace")); + ////////////////////////////////////////////////////////////// + // + // World-Space Rendering + // + ////////////////////////////////////////////////////////////// + + // Transform from image-space to world-space. Set up a reverse transform as well. + XMatrix imageSpaceToWorldSpace = ctx.ImageSpaceToWorldSpace; + + XMatrix worldSpaceToImageSpace = imageSpaceToWorldSpace; + worldSpaceToImageSpace.Invert(); + + ctx.graphics.MultiplyTransform(imageSpaceToWorldSpace); + + //------------------------------------------------------------ + // Explicit Clipping + //------------------------------------------------------------ + + if (ctx.clipPath != null) + { + ctx.graphics.IntersectClip(ctx.clipPath); + } + + //ctx.styles.showPseudoRandomStars = true; + //------------------------------------------------------------ + // Backgrounds + //------------------------------------------------------------ + + // "Exclusion zone" for Charted Space - for perf reasons + RectangleF chartedSpaceBounds = new RectangleF(-512 / Astrometrics.ParsecScaleX, -512 / Astrometrics.ParsecScaleY, 1024 / Astrometrics.ParsecScaleX, 1024 / Astrometrics.ParsecScaleY); + + RectangleF galacticBounds = new RectangleF(-14598.67f, -23084.26f, 29234.1133f, 25662.4746f); // TODO: Don't hardcode + Rectangle galaxyImageRect = new Rectangle(-16557, -24534, 33151, 29062); // Includes plenty of fuzzy slop + + // This transforms the Linehan galactic structure to the Mikesh galactic structure + // See http://travellermap.blogspot.com/2009/03/galaxy-scale-mismatch.html + Matrix xformLinehanToMikesh = new Matrix(0.9181034f, 0.0f, 0.0f, 0.855192542f, 120.672432f, 86.34569f); + + //------------------------------------------------------------ + // Local background (Nebula) + //------------------------------------------------------------ + #region local-background + + // NOTE: Since alpha texture brushes aren't supported without + // creating a new image (slow!) we render the local background + // first, then overlay the deep background over it, for + // basically the same effect since the alphas sum to 1. + + if (ctx.styles.useBackgroundImage && galacticBounds.IntersectsWith(ctx.tileRect)) + { + // Image-space rendering, so save current context + using (RenderUtil.SaveState(ctx.graphics)) + { + // Never fill outside the galaxy + ctx.graphics.IntersectClip(galacticBounds); + + // Map back to image space so it scales/tiles nicely + ctx.graphics.MultiplyTransform(worldSpaceToImageSpace); + + const float backgroundImageScale = 2.0f; + + lock (s_backgroundImage) + { + // Scaled size of the background + double w = s_backgroundImage.PixelWidth * backgroundImageScale; + double h = s_backgroundImage.PixelHeight * backgroundImageScale; + + // Offset of the background, relative to the canvas + double ox = (float)(-ctx.tileRect.Left * ctx.scale * Astrometrics.ParsecScaleX) % w; + double oy = (float)(-ctx.tileRect.Top * ctx.scale * Astrometrics.ParsecScaleY) % h; + if (ox > 0) { ox -= w; } + if (oy > 0) { oy -= h; } + + // Number of copies needed to cover the canvas + int nx = 1 + (int)Math.Floor(ctx.tileSize.Width / w); + int ny = 1 + (int)Math.Floor(ctx.tileSize.Height / h); + if (ox + nx * w < ctx.tileSize.Width) { nx += 1; } + if (oy + ny * h < ctx.tileSize.Height) { ny += 1; } + + for (int x = 0; x < nx; ++x) + { + for (int y = 0; y < ny; ++y) + { + ctx.graphics.DrawImage(s_backgroundImage, ox + x * w, oy + y * h, w + 1, h + 1); + //ctx.graphics.DrawRectangle( XPens.Orange, ox + x * w, oy + y * h, w, h ); + } + } + } + } + } + #endregion + timers.Add(new Timer("nebula")); + + //------------------------------------------------------------ + // Deep background (Galaxy) + //------------------------------------------------------------ + #region galaxy-background + if (ctx.styles.useGalaxyImage && ctx.styles.deepBackgroundOpacity > 0f) + { + using (RenderUtil.SaveState(ctx.graphics)) + { + ctx.graphics.MultiplyTransform(xformLinehanToMikesh); + lock (s_galaxyImage) + { + RenderUtil.DrawImageAlpha(ctx.graphics, ctx.styles.deepBackgroundOpacity, s_galaxyImage, galaxyImageRect); + } + } + } + #endregion + timers.Add(new Timer("galaxy")); + + //------------------------------------------------------------ + // Pseudo-Random Stars + //------------------------------------------------------------ + #region pseudorandom-stars + + if (ctx.styles.pseudoRandomStars.visible) + { + // Render pseudorandom stars based on the tile # and + // scale factor. Note that these are positioned in + // screen space, not world space. + + //const int nStars = 75; + int nMinStars = ctx.tileSize.Width * ctx.tileSize.Height / 300; + int nStars = ctx.scale >= 1 ? nMinStars : (int)(nMinStars / ctx.scale); + + // NOTE: For performance's sake, three different cases are considered: + // (1) Tile is entirely within charted space (most common) - just render + // the pseudorandom stars into the tile + // (2) Tile intersects the galaxy bounds - render pseudorandom stars + // into a texture, then fill the galaxy vector with it + // (3) Tile is entire outside the galaxy - don't render stars + + using (RenderUtil.SaveState(ctx.graphics)) + { + bool drawPseudoRandomStars = false; + + if (chartedSpaceBounds.Contains(ctx.tileRect)) + { + // Within Charted Space - just render pseudo-random stars + drawPseudoRandomStars = true; + } + if (drawPseudoRandomStars) + { + ctx.graphics.SmoothingMode = XSmoothingMode.HighQuality; + solidBrush.Color = ctx.styles.pseudoRandomStars.fillColor; + + Random rand = new Random((((int)ctx.tileRect.Left) << 8) ^ (int)ctx.tileRect.Top); + for (int i = 0; i < nStars; i++) + { + float starX = (float)rand.NextDouble() * ctx.tileRect.Width + ctx.tileRect.X; + float starY = (float)rand.NextDouble() * ctx.tileRect.Height + ctx.tileRect.Y; + float d = (float)rand.NextDouble() * 2; + + //ctx.graphics.DrawRectangle( fonts.foregroundBrush, starX, starY, (float)( d / ctx.scale * Astrometrics.ParsecScaleX ), (float)( d / ctx.scale * Astrometrics.ParsecScaleY ) ); + ctx.graphics.DrawEllipse(solidBrush, starX, starY, (float)(d / ctx.scale * Astrometrics.ParsecScaleX), (float)(d / ctx.scale * Astrometrics.ParsecScaleY)); + } + } + } + } + #endregion + timers.Add(new Timer("pseudorandom")); + + //------------------------------------------------------------ + // Rifts in Charted Space + //------------------------------------------------------------ + #region riftFiles + + if (ctx.styles.showRifts && ctx.styles.riftOpacity > 0f) + { + Rectangle riftImageRect; + riftImageRect = new Rectangle(-1374, -827, 2769, 1754); // Correct + lock (s_riftImage) + { + RenderUtil.DrawImageAlpha(ctx.graphics, ctx.styles.riftOpacity, s_riftImage, riftImageRect); + } + } + #endregion + timers.Add(new Timer("riftFiles")); + + //------------------------------------------------------------ + // April Fool's Day + //------------------------------------------------------------ + #region april-fools + + if (ctx.silly) + { + using (RenderUtil.SaveState(ctx.graphics)) + { + // Render in image-space + ctx.graphics.MultiplyTransform(worldSpaceToImageSpace); + + XImage sillyImage = ctx.styles.grayscale ? s_sillyImageGray : s_sillyImageColor; + + lock (sillyImage) + { + ctx.graphics.DrawImage(sillyImage, 0, 0, ctx.tileSize.Width, ctx.tileSize.Height); + } + } + timers.Add(new Timer("silly")); + } + + #endregion + + //------------------------------------------------------------ + // Macro: Borders object + //------------------------------------------------------------ + #region macro-borders + if (ctx.styles.macroBorders.visible) + { + ctx.styles.macroBorders.pen.Apply(ref pen); + ctx.graphics.SmoothingMode = XSmoothingMode.AntiAlias; + foreach (var vec in borderFiles + .Select(file => ctx.resourceManager.GetXmlFileObject(file, typeof(VectorObject))) + .OfType() + .Where(vec => (vec.MapOptions & ctx.options & MapOptions.BordersMask) != 0)) + { + vec.Draw(ctx.graphics, ctx.tileRect, ctx.options, pen); + } + + } + #endregion + timers.Add(new Timer("macroborder")); + + //------------------------------------------------------------ + // Macro: Route object + //------------------------------------------------------------ + #region macro-routes + + if (ctx.styles.macroRoutes.visible) + { + ctx.styles.macroRoutes.pen.Apply(ref pen); + ctx.graphics.SmoothingMode = XSmoothingMode.AntiAlias; + foreach (var vec in routeFiles + .Select(file => ctx.resourceManager.GetXmlFileObject(file, typeof(VectorObject))) + .OfType() + .Where(vec => (vec.MapOptions & ctx.options & MapOptions.BordersMask) != 0)) + { + vec.Draw(ctx.graphics, ctx.tileRect, ctx.options, pen); + } + } + #endregion + timers.Add(new Timer("macroroute")); + + //------------------------------------------------------------ + // Sector Grid + //------------------------------------------------------------ + #region sector-grid + + ctx.graphics.SmoothingMode = XSmoothingMode.HighSpeed; + + if (ctx.styles.sectorGrid.visible) + { + const int gridSlop = 10; + ctx.styles.sectorGrid.pen.Apply(ref pen); + + for (float h = ((float)(Math.Floor((ctx.tileRect.Left) / Astrometrics.SectorWidth) - 1) - Astrometrics.ReferenceSector.X) * Astrometrics.SectorWidth - Astrometrics.ReferenceHex.X; h <= ctx.tileRect.Right + Astrometrics.SectorWidth; h += Astrometrics.SectorWidth) + { + ctx.graphics.DrawLine(pen, h, ctx.tileRect.Top - gridSlop, h, ctx.tileRect.Bottom + gridSlop); + } + + for (float v = ((float)(Math.Floor((ctx.tileRect.Top) / Astrometrics.SectorHeight) - 1) - Astrometrics.ReferenceSector.Y) * Astrometrics.SectorHeight - Astrometrics.ReferenceHex.Y; v <= ctx.tileRect.Bottom + Astrometrics.SectorHeight; v += Astrometrics.SectorHeight) + { + ctx.graphics.DrawLine(pen, ctx.tileRect.Left - gridSlop, v, ctx.tileRect.Right + gridSlop, v); + } + } + + #endregion + + //------------------------------------------------------------ + // Subsector Grid + //------------------------------------------------------------ + #region subsector-grid + ctx.graphics.SmoothingMode = XSmoothingMode.HighSpeed; + if (ctx.styles.subsectorGrid.visible) + { + const int gridSlop = 10; + ctx.styles.subsectorGrid.pen.Apply(ref pen); + + for (float h = ((float)(Math.Floor((ctx.tileRect.Left) / Astrometrics.SubsectorWidth) - 1) - Astrometrics.ReferenceSector.X) * Astrometrics.SubsectorWidth - Astrometrics.ReferenceHex.X; h <= ctx.tileRect.Right + Astrometrics.SectorWidth; h += Astrometrics.SubsectorWidth) + { + ctx.graphics.DrawLine(pen, h, ctx.tileRect.Top - gridSlop, h, ctx.tileRect.Bottom + gridSlop); + } + + for (float v = ((float)(Math.Floor((ctx.tileRect.Top) / Astrometrics.SubsectorHeight) - 1) - Astrometrics.ReferenceSector.Y) * Astrometrics.SubsectorHeight - Astrometrics.ReferenceHex.Y; v <= ctx.tileRect.Bottom + Astrometrics.SectorHeight; v += Astrometrics.SubsectorHeight) + { + ctx.graphics.DrawLine(pen, ctx.tileRect.Left - gridSlop, v, ctx.tileRect.Right + gridSlop, v); + } + } + #endregion + + //------------------------------------------------------------ + // Parsec Grid + //------------------------------------------------------------ + #region parsec-grid + // TODO: Optimize - timers indicate this is slow + ctx.graphics.SmoothingMode = XSmoothingMode.HighQuality; + if (ctx.styles.parsecGrid.visible) + { + const int parsecSlop = 1; + + int hx = (int)Math.Floor(ctx.tileRect.Left); + int hw = (int)Math.Ceiling(ctx.tileRect.Width); + int hy = (int)Math.Floor(ctx.tileRect.Top); + int hh = (int)Math.Ceiling(ctx.tileRect.Height); + + ctx.styles.parsecGrid.pen.Apply(ref pen); + + switch (ctx.styles.microBorderStyle) + { + case MicroBorderStyle.Square: + for (int px = hx - parsecSlop; px < hx + hw + parsecSlop; px++) + { + float yOffset = ((px % 2) != 0) ? 0.0f : 0.5f; + for (int py = hy - parsecSlop; py < hy + hh + parsecSlop; py++) + { + // TODO: use RenderUtil.(Square|Hex)Edges(X|Y) arrays + const float inset = 0.1f; + ctx.graphics.DrawRectangle(pen, px + inset, py + inset + yOffset, 1 - inset * 2, 1 - inset * 2); + } + } + break; + + case MicroBorderStyle.Hex: + XPoint[] points = new XPoint[4]; + for (int px = hx - parsecSlop; px < hx + hw + parsecSlop; px++) + { + float yOffset = ((px % 2) != 0) ? 0.0f : 0.5f; + for (int py = hy - parsecSlop; py < hy + hh + parsecSlop; py++) + { + // TODO: use RenderUtil.(Square|Hex)Edges(X|Y) arrays + const float hexEdge = 0.18f; // TODO: Need to compute this (should be cos(60), inverse-scaled) + points[0] = new XPoint(px + -hexEdge, py + 0.5f + yOffset); + points[1] = new XPoint(px + hexEdge, py + 1.0f + yOffset); + points[2] = new XPoint(px + 1.0f - hexEdge, py + 1.0f + yOffset); + points[3] = new XPoint(px + 1.0f + hexEdge, py + 0.5f + yOffset); + ctx.graphics.DrawLines(pen, points); + } + } + break; + case MicroBorderStyle.Curve: + // none + break; + } + } + #endregion + timers.Add(new Timer("parsec grids")); + + //------------------------------------------------------------ + // Subsector Names + //------------------------------------------------------------ + #region subsector-names + + if (ctx.styles.subsectorNames.visible) + { + solidBrush.Color = ctx.styles.subsectorNames.textColor; + foreach (Sector sector in ctx.selector.Sectors) + { + for (int i = 0; i < 16; i++) + { + int ssx = i % 4; + int ssy = i / 4; + + Subsector ss = sector[i]; + if (ss == null || String.IsNullOrEmpty(ss.Name)) + continue; + + // TODO: Move to a property on Subsector (requires Subsector to know parent sector) + Point center = Astrometrics.LocationToCoordinates(sector.Location, + new Point(Astrometrics.SubsectorWidth * (2 * ssx + 1) / 2, Astrometrics.SubsectorHeight * (2 * ssy + 1) / 2)); + + RenderUtil.DrawLabel(ctx.graphics, ss.Name, center, ctx.styles.subsectorNames.Font, solidBrush, ctx.styles.subsectorNames.textStyle); + } + } + } + + #endregion + + //------------------------------------------------------------ + // Micro: Borders + //------------------------------------------------------------ + #region micro-borders + if (ctx.styles.microBorders.visible) + { + if (ctx.styles.fillMicroBorders) + { + DrawMicroBorders(ctx, fonts, BorderLayer.Fill); + } + DrawMicroBorders(ctx, fonts, BorderLayer.Stroke); + } + #endregion + timers.Add(new Timer("microborders")); + + //------------------------------------------------------------ + // Micro: Routes + //------------------------------------------------------------ + #region routes + + if (ctx.styles.microRoutes.visible) + { + DrawRoutes(ctx, fonts); + } + #endregion + timers.Add(new Timer("routes")); + + //------------------------------------------------------------ + // Sector Names + //------------------------------------------------------------ + #region sector-names + + if (ctx.styles.showSomeSectorNames || ctx.styles.subsectorNames.visible) + { + foreach (Sector sector in ctx.selector.Sectors + .Where(sector => ctx.styles.showAllSectorNames || (ctx.styles.showSomeSectorNames && sector.Selected)) + .Where(sector => sector.Names.Any())) + { + solidBrush.Color = ctx.styles.sectorName.textColor; + string name = sector.Names[0].Text; + + RenderUtil.DrawLabel(ctx.graphics, name, sector.Center, ctx.styles.sectorName.Font, solidBrush, ctx.styles.sectorName.textStyle); + } + } + + #endregion + + //------------------------------------------------------------ + // Macro: Government / Rift / Route Names + //------------------------------------------------------------ + #region government-rift-names + if (ctx.styles.macroNames.visible) + { + foreach (var vec in borderFiles + .Select(file => ctx.resourceManager.GetXmlFileObject(file, typeof(VectorObject))) + .OfType() + .Where(vec => (vec.MapOptions & ctx.options & MapOptions.NamesMask) != 0)) + { + bool major = vec.MapOptions.HasFlag(MapOptions.NamesMajor); + LabelStyle labelStyle = new LabelStyle(); + labelStyle.Uppercase = major; + XFont font = major ? ctx.styles.macroNames.Font : ctx.styles.macroNames.SmallFont; + solidBrush.Color = major ? ctx.styles.macroNames.textColor : ctx.styles.macroNames.textHighlightColor; + vec.DrawName(ctx.graphics, ctx.tileRect, ctx.options, font, solidBrush, labelStyle); + } + + foreach (var vec in riftFiles + .Select(file => ctx.resourceManager.GetXmlFileObject(file, typeof(VectorObject))) + .OfType() + .Where(vec => (vec.MapOptions & ctx.options & MapOptions.NamesMask) != 0)) + { + bool major = vec.MapOptions.HasFlag(MapOptions.NamesMajor); + LabelStyle labelStyle = new LabelStyle(); + labelStyle.Rotation = 35; + labelStyle.Uppercase = major; + XFont font = major ? ctx.styles.macroNames.Font : ctx.styles.macroNames.SmallFont; + solidBrush.Color = major ? ctx.styles.macroNames.textColor : ctx.styles.macroNames.textHighlightColor; + vec.DrawName(ctx.graphics, ctx.tileRect, ctx.options, font, solidBrush, labelStyle); + } + + if (ctx.styles.macroRoutes.visible) + { + foreach (var vec in routeFiles + .Select(file => ctx.resourceManager.GetXmlFileObject(file, typeof(VectorObject))) + .OfType() + .Where(vec => (vec.MapOptions & ctx.options & MapOptions.NamesMask) != 0)) + { + bool major = vec.MapOptions.HasFlag(MapOptions.NamesMajor); + LabelStyle labelStyle = new LabelStyle(); + labelStyle.Uppercase = major; + XFont font = major ? ctx.styles.macroNames.Font : ctx.styles.macroNames.SmallFont; + solidBrush.Color = major ? ctx.styles.macroRoutes.textColor : ctx.styles.macroRoutes.textHighlightColor; + vec.DrawName(ctx.graphics, ctx.tileRect, ctx.options, font, solidBrush, labelStyle); + } + } + } + + #endregion + + //------------------------------------------------------------ + // Macro: Capitals & Home Worlds + //------------------------------------------------------------ + #region capitals-homeworlds + + if (ctx.styles.capitals.visible && (ctx.options & MapOptions.WorldsMask) != 0) + { + WorldObjectCollection worlds = ctx.resourceManager.GetXmlFileObject(@"~/res/Worlds.xml", typeof(WorldObjectCollection)) as WorldObjectCollection; + if (worlds != null) + { + solidBrush.Color = ctx.styles.capitals.textColor; + foreach (WorldObject world in worlds.Worlds.Where(world => (world.MapOptions & ctx.options) != 0)) + { + world.Paint(ctx.graphics, ctx.tileRect, ctx.options, ctx.styles.capitals.fillColor, + solidBrush, ctx.styles.macroNames.SmallFont); + } + } + } + + #endregion + timers.Add(new Timer("grids and names")); + + //------------------------------------------------------------ + // Micro: Government Names + //------------------------------------------------------------ + #region government-names + + if (ctx.styles.showMicroNames) + { + DrawLabels(ctx, fonts); + } + + #endregion + timers.Add(new Timer("micro")); + + //------------------------------------------------------------ + // Worlds + //------------------------------------------------------------ + #region worlds + if (ctx.styles.worlds.visible) + { + // TODO: selector may be expensive + foreach (World world in ctx.selector.Worlds) { DrawWorld(ctx, fonts, world, WorldLayer.Background); } + foreach (World world in ctx.selector.Worlds) { DrawWorld(ctx, fonts, world, WorldLayer.Foreground); } + } + #endregion + timers.Add(new Timer("worlds")); + +#if SHOW_TIMING + using( RenderUtil.SaveState( ctx.graphics ) ) + { + XFont font = new XFont( FontFamily.GenericSansSerif, 12, XFontStyle.Regular, new XPdfFontOptions(PdfSharp.Pdf.PdfFontEncoding.Unicode) ); + ctx.graphics.MultiplyTransform( worldSpaceToImageSpace ); + double cursorX = 20.0, cursorY = 20.0; + DateTime last = dtStart; + foreach( Timer s in timers ) + { + TimeSpan ts = s.dt - last; + last = s.dt; + for( int dx = -1; dx <= 1; ++dx ) + { + for( int dy = -1; dy <= 1; ++dy ) + { + + ctx.graphics.DrawString( String.Format( "{0} {1}", Math.Round( ts.TotalMilliseconds ), s.label ), font, XBrushes.Black, cursorX + dx, cursorY + dy ); + } + } + ctx.graphics.DrawString( String.Format("{0} {1}", Math.Round(ts.TotalMilliseconds), s.label), font, XBrushes.Yellow, cursorX, cursorY ); + cursorY += 14; + } + } +#endif + + } + } + + private enum WorldLayer { Background, Foreground }; + private static void DrawWorld(RenderContext ctx, FontCache styleRes, World world, WorldLayer layer) + { + bool isCapital = world.IsCapital; + bool isHiPop = world.Population >= 1e9; + bool renderName = ctx.styles.worldDetails.HasFlag(WorldDetails.AllNames) || + (ctx.styles.worldDetails.HasFlag(WorldDetails.KeyNames) && (isCapital || isHiPop)); + bool renderUWP = ctx.styles.worldDetails.HasFlag(WorldDetails.Uwp); + + using (RenderUtil.SaveState(ctx.graphics)) + { + XPen pen = new XPen(XColor.Empty); + XSolidBrush solidBrush = new XSolidBrush(); + + ctx.graphics.SmoothingMode = XSmoothingMode.AntiAlias; + + // Center on the parsec + PointF center = Astrometrics.HexToCenter(world.Coordinates); + + XMatrix matrix = new XMatrix(); + matrix.TranslatePrepend(center.X, center.Y); + matrix.ScalePrepend(ctx.styles.hexContentScale / Astrometrics.ParsecScaleX, ctx.styles.hexContentScale / Astrometrics.ParsecScaleY); + ctx.graphics.MultiplyTransform(matrix, XMatrixOrder.Prepend); + + if (!ctx.styles.useWorldImages) + { + if (layer == WorldLayer.Background) + { + #region Zone + if (ctx.styles.worldDetails.HasFlag(WorldDetails.Zone)) + { + if (world.IsAmber || world.IsRed || world.IsBlue) + { + PenInfo pi = + world.IsAmber ? ctx.styles.amberZone.pen : + world.IsRed ? ctx.styles.redZone.pen : ctx.styles.blueZone.pen; + pi.Apply(ref pen); + + if (renderName && ctx.styles.fillMicroBorders) + { + using (RenderUtil.SaveState(ctx.graphics)) + { + ctx.graphics.IntersectClip(new RectangleF(-.5f, -.5f, 1f, renderUWP ? 0.65f : 0.75f)); + ctx.graphics.DrawEllipse(pen, -0.4f, -0.4f, 0.8f, 0.8f); + } + } + else + { + ctx.graphics.DrawEllipse(pen, -0.4f, -0.4f, 0.8f, 0.8f); + } + } + } + #endregion + + #region Hex + if (ctx.styles.worldDetails.HasFlag(WorldDetails.Hex)) + { + int coords; + switch (ctx.styles.hexCoordinateStyle) + { + default: + case Stylesheet.HexCoordinateStyle.Sector: coords = world.Hex; break; + case Stylesheet.HexCoordinateStyle.Subsector: coords = world.SubsectorHex; break; + } + String hex = coords.ToString("0000", CultureInfo.InvariantCulture); + XSize size = ctx.graphics.MeasureString(hex, ctx.styles.hexNumber.Font); + solidBrush.Color = ctx.styles.hexNumber.textColor; + ctx.graphics.DrawString(hex, ctx.styles.hexNumber.Font, solidBrush, 0.0f, -0.5f, RenderUtil.StringFormatTopCenter); + } + #endregion + } + + if (layer == WorldLayer.Foreground) + { + #region Disc + if (ctx.styles.worldDetails.HasFlag(WorldDetails.Type)) + { + // Blank out world area, so routes are shown correctly + if (!ctx.styles.fillMicroBorders) + { + solidBrush.Color = ctx.styles.backgroundColor; + ctx.graphics.DrawEllipse(solidBrush, -0.15f, -0.15f, 0.3f, 0.3f); + } + + if (world.Size <= 0) + { + #region Asteroid-Belt + if (ctx.styles.worldDetails.HasFlag(WorldDetails.Asteroids)) + { + // Basic pattern, with probability varying per position: + // o o o + // o o o o + // o o o + + int[] lpx = { -2, 0, 2, -3, -1, 1, 3, -2, 0, 2 }; + int[] lpy = { -2, -2, -2, 0, 0, 0, 0, 2, 2, 2 }; + float[] lpr = { 0.5f, 0.9f, 0.5f, 0.6f, 0.9f, 0.9f, 0.6f, 0.5f, 0.9f, 0.5f }; + + solidBrush.Color = ctx.styles.worlds.textColor; + + // Random generator is seeded with world location so it is always the same + Random rand = new Random(world.Coordinates.X ^ world.Coordinates.Y); + for (int i = 0; i < lpx.Length; ++i) + { + if (rand.NextDouble() < lpr[i]) + { + float px = lpx[i] * 0.035f; + float py = lpy[i] * 0.035f; + + float w = 0.04f + (float)rand.NextDouble() * 0.03f; + float h = 0.04f + (float)rand.NextDouble() * 0.03f; + + // If necessary, add jitter here + float dx = 0, dy = 0; + + ctx.graphics.DrawEllipse(solidBrush, + px + dx - w / 2, py + dy - h / 2, w, h); + } + } + } + else + { + // Just a glyph + solidBrush.Color = ctx.styles.worlds.textColor; + RenderUtil.DrawGlyph(ctx.graphics, Glyph.DiamondX, styleRes, solidBrush, 0.0f, 0.0f); + } + #endregion + } + else + { + XColor penColor, brushColor; + ctx.styles.WorldColors(world, out penColor, out brushColor); + + if (!brushColor.IsEmpty) + { + solidBrush.Color = brushColor; + ctx.graphics.DrawEllipse(solidBrush, -0.1f, -0.1f, 0.2f, 0.2f); + } + + if (!penColor.IsEmpty) + { + ctx.styles.worldWater.pen.Apply(ref pen); + pen.Color = penColor; + ctx.graphics.DrawEllipse(pen, -0.1f, -0.1f, 0.2f, 0.2f); + } + } + } + else + { + // Dotmap + solidBrush.Color = ctx.styles.worlds.textColor; + ctx.graphics.DrawEllipse(solidBrush, -0.2f, -0.2f, 0.4f, 0.4f); + } + #endregion + + #region GasGiant + if (ctx.styles.worldDetails.HasFlag(WorldDetails.GasGiant)) + { + if (world.GasGiants > 0) + { + solidBrush.Color = ctx.styles.worlds.textColor; + RenderUtil.DrawGlyph(ctx.graphics, Glyph.Circle, styleRes, solidBrush, ctx.styles.GasGiantPosition.X, ctx.styles.GasGiantPosition.Y); + } + } + #endregion + + #region Name + if (renderName) + { + string name = world.Name; + if (isHiPop) + name = name.ToUpperInvariant(); + + Color textColor = isCapital ? ctx.styles.worlds.textHighlightColor : ctx.styles.worlds.textColor; + + if (ctx.styles.worlds.textStyle.Uppercase) + name = name.ToUpper(); + + DrawWorldLabel(ctx, ctx.styles.worlds.textBackgroundStyle, solidBrush, textColor, ctx.styles.worlds.textStyle.Translation, ctx.styles.worlds.Font, name); + } + #endregion + + #region Starport + if (ctx.styles.worldDetails.HasFlag(WorldDetails.Starport)) + { + string starport = world.Starport.ToString(); + DrawWorldLabel(ctx, ctx.styles.worlds.textBackgroundStyle, solidBrush, ctx.styles.worlds.textColor, ctx.styles.StarportPosition, styleRes.StarportFont, starport); + } + #endregion + + #region UWP + if (renderUWP) + { + string uwp = world.UWP; + solidBrush.Color = ctx.styles.worlds.textColor; + + ctx.graphics.DrawString(uwp, ctx.styles.hexNumber.Font, solidBrush, ctx.styles.StarportPosition.X, -ctx.styles.StarportPosition.Y, RenderUtil.StringFormatCentered); + } + #endregion + + #region Allegiance + // TODO: Mask off background for allegiance + if (ctx.styles.worldDetails.HasFlag(WorldDetails.Allegiance)) + { + if (!world.HasDefaultAllegiance()) + { + string alleg = world.Allegiance; + solidBrush.Color = ctx.styles.worlds.textColor; + + if (ctx.styles.lowerCaseAllegiance) + alleg = alleg.ToLowerInvariant(); + + ctx.graphics.DrawString(alleg, ctx.styles.worlds.SmallFont, solidBrush, ctx.styles.AllegiancePosition.X, ctx.styles.AllegiancePosition.Y, RenderUtil.StringFormatCentered); + } + } + #endregion + + #region Bases + // TODO: Mask off background for glyphs + if (ctx.styles.worldDetails.HasFlag(WorldDetails.Bases)) + { + int num_bases = world.Bases.Length; + + // Base 1 + bool bottomUsed = false; + if (num_bases > 0) + { + Glyph glyph = Glyph.FromBaseCode(world.BaseAllegiance, world.Bases[0]); + if (glyph.Printable) + { + PointF pt = ctx.styles.BaseTopPosition; + if (glyph.Bias == Glyph.GlyphBias.Bottom) + { + pt = ctx.styles.BaseBottomPosition; + bottomUsed = true; + } + + solidBrush.Color = glyph.IsHighlighted ? ctx.styles.worlds.textHighlightColor : ctx.styles.worlds.textColor; + RenderUtil.DrawGlyph(ctx.graphics, glyph, styleRes, solidBrush, pt.X, pt.Y); + } + } + + // Base 2 + if (num_bases > 1) + { + Glyph glyph = Glyph.FromBaseCode(world.Allegiance, world.Bases[1]); + if (glyph.Printable) + { + PointF pt = bottomUsed ? ctx.styles.BaseTopPosition : ctx.styles.BaseBottomPosition; + solidBrush.Color = glyph.IsHighlighted ? ctx.styles.worlds.textHighlightColor : ctx.styles.worlds.textColor; + RenderUtil.DrawGlyph(ctx.graphics, glyph, styleRes, solidBrush, pt.X, pt.Y); + } + } + + // Research Stations + string rs; + if ((rs = world.ResearchStation) != null) + { + Glyph glyph = Glyph.FromResearchCode(rs); + solidBrush.Color = glyph.IsHighlighted ? ctx.styles.worlds.textHighlightColor : ctx.styles.worlds.textColor; + RenderUtil.DrawGlyph(ctx.graphics, glyph, styleRes, solidBrush, ctx.styles.BaseMiddlePosition.X, ctx.styles.BaseMiddlePosition.Y); + } + else if (world.IsReserve) + { + Glyph glyph = Glyph.Reserve; + solidBrush.Color = glyph.IsHighlighted ? ctx.styles.worlds.textHighlightColor : ctx.styles.worlds.textColor; + RenderUtil.DrawGlyph(ctx.graphics, glyph, styleRes, solidBrush, ctx.styles.BaseMiddlePosition.X, 0); + } + else if (world.IsPenalColony) + { + Glyph glyph = Glyph.Prison; + solidBrush.Color = glyph.IsHighlighted ? ctx.styles.worlds.textHighlightColor : ctx.styles.worlds.textColor; + RenderUtil.DrawGlyph(ctx.graphics, glyph, styleRes, solidBrush, ctx.styles.BaseMiddlePosition.X, 0); + } + else if (world.IsExileCamp) + { + Glyph glyph = Glyph.ExileCamp; + solidBrush.Color = glyph.IsHighlighted ? ctx.styles.worlds.textHighlightColor : ctx.styles.worlds.textColor; + RenderUtil.DrawGlyph(ctx.graphics, glyph, styleRes, solidBrush, ctx.styles.BaseMiddlePosition.X, 0); + } + } + #endregion + } + + } + else + { + float imageRadius = ((world.Size <= 0) ? 0.6f : (0.3f * (world.Size / 5.0f + 0.2f))) / 2; + float decorationRadius = imageRadius; + + if (layer == WorldLayer.Background) + { + #region Disc + if (ctx.styles.worldDetails.HasFlag(WorldDetails.Type)) + { + if (world.Size <= 0) + { + const float scaleX = 1.5f; + const float scaleY = 1.0f; + XImage img = s_worldImages["Belt"]; + + lock (img) + { + ctx.graphics.DrawImage(img, -imageRadius * scaleX, -imageRadius * scaleY, imageRadius * 2 * scaleX, imageRadius * 2 * scaleY); + } + } + else + { + XImage img; + switch (world.Hydrographics) + { + default: + case 0x0: img = s_worldImages["Hyd0"]; break; + case 0x1: img = s_worldImages["Hyd1"]; break; + case 0x2: img = s_worldImages["Hyd2"]; break; + case 0x3: img = s_worldImages["Hyd3"]; break; + case 0x4: img = s_worldImages["Hyd4"]; break; + case 0x5: img = s_worldImages["Hyd5"]; break; + case 0x6: img = s_worldImages["Hyd6"]; break; + case 0x7: img = s_worldImages["Hyd7"]; break; + case 0x8: img = s_worldImages["Hyd8"]; break; + case 0x9: img = s_worldImages["Hyd9"]; break; + case 0xA: img = s_worldImages["HydA"]; break; + } + if (img != null) + { + lock (img) + { + ctx.graphics.DrawImage(img, -imageRadius, -imageRadius, imageRadius * 2, imageRadius * 2); + } + } + } + } + else + { + // Dotmap + solidBrush.Color = ctx.styles.worlds.textColor; + ctx.graphics.DrawEllipse(solidBrush, -0.2f, -0.2f, 0.4f, 0.4f); + } + #endregion + } + + if (layer == WorldLayer.Foreground) + { + #region Zone + if (ctx.styles.worldDetails.HasFlag(WorldDetails.Zone)) + { + if (world.IsAmber || world.IsRed || world.IsBlue) + { + PenInfo pi = + world.IsAmber ? ctx.styles.amberZone.pen : + world.IsRed ? ctx.styles.redZone.pen : ctx.styles.blueZone.pen; + pi.Apply(ref pen); + + // TODO: Try and accomplish this using dash pattern + decorationRadius += 0.1f; + ctx.graphics.DrawArc(pen, -decorationRadius, -decorationRadius, decorationRadius * 2, decorationRadius * 2, 5, 80); + ctx.graphics.DrawArc(pen, -decorationRadius, -decorationRadius, decorationRadius * 2, decorationRadius * 2, 95, 80); + ctx.graphics.DrawArc(pen, -decorationRadius, -decorationRadius, decorationRadius * 2, decorationRadius * 2, 185, 80); + ctx.graphics.DrawArc(pen, -decorationRadius, -decorationRadius, decorationRadius * 2, decorationRadius * 2, 275, 80); + } + } + #endregion + + #region GasGiant + if (ctx.styles.worldDetails.HasFlag(WorldDetails.GasGiant)) + { + if (world.GasGiants > 0) + { + decorationRadius += 0.1f; + const float symbolRadius = 0.05f; + solidBrush.Color = ctx.styles.worlds.textHighlightColor; ; + ctx.graphics.DrawEllipse(solidBrush, decorationRadius - symbolRadius, 0.0f - symbolRadius, symbolRadius * 2, symbolRadius * 2); + } + } + #endregion + + #region UWP + if (renderUWP) + { + string uwp = world.UWP; + solidBrush.Color = ctx.styles.worlds.textColor; + + using (RenderUtil.SaveState(ctx.graphics)) + { + XMatrix uwpMatrix = new XMatrix(); + uwpMatrix.TranslatePrepend(decorationRadius, 0.0f); + uwpMatrix.ScalePrepend(ctx.styles.worlds.textStyle.Scale.Width, ctx.styles.worlds.textStyle.Scale.Height); + uwpMatrix.Multiply(uwpMatrix, XMatrixOrder.Prepend); + ctx.graphics.DrawString(uwp, ctx.styles.hexNumber.Font, solidBrush, ctx.styles.StarportPosition.X, -ctx.styles.StarportPosition.Y, RenderUtil.StringFormatCenterLeft); + } + } + #endregion + + #region Name + if (renderName) + { + string name = world.Name; + if (isHiPop) + name = name.ToUpperInvariant(); + + using (RenderUtil.SaveState(ctx.graphics)) + { + Color textColor = isCapital ? ctx.styles.worlds.textHighlightColor : ctx.styles.worlds.textColor; + + if (ctx.styles.worlds.textStyle.Uppercase) + name = name.ToUpper(); + + decorationRadius += 0.1f; + XMatrix imageMatrix = new XMatrix(); + imageMatrix.TranslatePrepend(decorationRadius, 0.0f); + imageMatrix.ScalePrepend(ctx.styles.worlds.textStyle.Scale.Width, ctx.styles.worlds.textStyle.Scale.Height); + imageMatrix.TranslatePrepend(ctx.graphics.MeasureString(name, ctx.styles.worlds.Font).Width / 2, 0.0f); // Left align + ctx.graphics.MultiplyTransform(imageMatrix, XMatrixOrder.Prepend); + + DrawWorldLabel(ctx, ctx.styles.worlds.textBackgroundStyle, solidBrush, textColor, ctx.styles.worlds.textStyle.Translation, ctx.styles.worlds.Font, name); + } + } + #endregion + } + } + } + } + + private static void DrawWorldLabel(RenderContext ctx, TextBackgroundStyle backgroundStyle, XSolidBrush brush, Color color, PointF position, XFont font, string text) + { + XSize size = ctx.graphics.MeasureString(text, font); + + switch (backgroundStyle) + { + case TextBackgroundStyle.None: + break; + + default: + case TextBackgroundStyle.Rectangle: + if (!ctx.styles.fillMicroBorders) + { + // TODO: Implement this with a clipping region instead + brush.Color = ctx.styles.backgroundColor; + ctx.graphics.DrawRectangle(brush, position.X - size.Width / 2, position.Y - size.Height / 2, size.Width, size.Height); + } + break; + + case TextBackgroundStyle.Outline: + case TextBackgroundStyle.Shadow: + { + // TODO: These scaling factors are constant for a render; compute once + + // Invert the current scaling transforms + float sx = 1.0f / ctx.styles.hexContentScale; + float sy = 1.0f / ctx.styles.hexContentScale; + sx *= Astrometrics.ParsecScaleX; + sy *= Astrometrics.ParsecScaleY; + sx /= (float)ctx.scale * Astrometrics.ParsecScaleX; + sy /= (float)ctx.scale * Astrometrics.ParsecScaleY; + + const int outlineSize = 2; + const int outlineSkip = 1; + + int outlineStart = backgroundStyle == TextBackgroundStyle.Outline + ? -outlineSize + : 0; + + brush.Color = ctx.styles.backgroundColor; + + for (int dx = outlineStart; dx <= outlineSize; dx += outlineSkip) + { + for (int dy = outlineStart; dy <= outlineSize; dy += outlineSkip) + { + ctx.graphics.DrawString(text, font, brush, position.X + sx * dx, position.Y + sy * dy, RenderUtil.StringFormatCentered); + } + } + break; + } + } + + brush.Color = color; + ctx.graphics.DrawString(text, font, brush, position.X, position.Y, RenderUtil.StringFormatCentered); + } + + private static void DrawLabels(RenderContext ctx, FontCache styleRes) + { + using (RenderUtil.SaveState(ctx.graphics)) + { + XSolidBrush solidBrush = new XSolidBrush(); + + ctx.graphics.SmoothingMode = XSmoothingMode.AntiAlias; + //ctx.graphics.TextRenderingHint = TextRenderingHint.AntiAlias; + + foreach (Sector sector in ctx.selector.Sectors) + { + solidBrush.Color = ctx.styles.microBorders.textColor; + foreach (Border border in sector.Borders.Where(border => border.ShowLabel)) + { + Allegiance alleg = sector.GetAllegiance(border.Allegiance); + if (alleg != null) + { + string text = alleg.Name; + Point labelHex = border.LabelPosition; + PointF labelPos = Astrometrics.HexToCenter(Astrometrics.LocationToCoordinates(new Location(sector.Location, labelHex))); + // TODO: Replace these with, well, positions! + //labelPos.X -= 0.5f; + //labelPos.Y -= 0.5f; + + if (border.WrapLabel) + { + text = text.Replace(' ', '\n'); + } + + RenderUtil.DrawLabel(ctx.graphics, text, labelPos, ctx.styles.microBorders.Font, solidBrush, ctx.styles.microBorders.textStyle); + } + } + + foreach (Label label in sector.Labels) + { + string text = label.Text; + Point labelHex = new Point(label.Hex / 100, label.Hex % 100); + PointF labelPos = Astrometrics.HexToCenter(Astrometrics.LocationToCoordinates(new Location(sector.Location, labelHex))); + // TODO: Adopt some of the tweaks from .MSEC + //labelPos.X -= 0.5f; + //labelPos.Y -= 0.5f; + + XFont font; + switch (label.Size) + { + case "small": font = ctx.styles.microBorders.SmallFont; break; + case "large": font = ctx.styles.microBorders.LargeFont; break; + default: font = ctx.styles.microBorders.Font; break; + } + + if (!ctx.styles.grayscale && + ColorUtil.NoticeableDifference(label.Color, ctx.styles.backgroundColor) && + (label.Color != Label.DefaultColor)) + { + solidBrush.Color = label.Color; + } + else + { + solidBrush.Color = ctx.styles.microBorders.textColor; + } + RenderUtil.DrawLabel(ctx.graphics, text, labelPos, font, solidBrush, ctx.styles.microBorders.textStyle); + } + } + } + } + + private static void DrawRoutes(RenderContext ctx, FontCache styleRes) + { + using (RenderUtil.SaveState(ctx.graphics)) + { + ctx.graphics.SmoothingMode = XSmoothingMode.AntiAlias; + XPen pen = new XPen(XColor.Empty); + ctx.styles.microRoutes.pen.Apply(ref pen); + + foreach (Sector sector in ctx.selector.Sectors) + { + foreach (Route route in sector.Routes) + { + // Compute source/target sectors (may be offset) + Point startSector = sector.Location, endSector = sector.Location; + startSector.Offset(route.StartOffset); + endSector.Offset(route.EndOffset); + + Location startLocation = new Location(startSector, route.Start); + Location endLocation = new Location(endSector, route.End); + + PointF startPoint = Astrometrics.HexToCenter(Astrometrics.LocationToCoordinates(startLocation)); + PointF endPoint = Astrometrics.HexToCenter(Astrometrics.LocationToCoordinates(endLocation)); + + // If drawing dashed lines twice and the start/end are swapped the + // dashes don't overlap correctly. So "sort" the points. + if ((startPoint.X > endPoint.X) || + (startPoint.X == endPoint.X) && (startPoint.Y > endPoint.Y)) + { + PointF tmp = startPoint; + startPoint = endPoint; + endPoint = tmp; + } + + // HACK + if (ctx.styles.grayscale || + !ColorUtil.NoticeableDifference(route.Color, ctx.styles.backgroundColor)) + { + pen.Color = ctx.styles.microRoutes.pen.color; // default + } + else + { + pen.Color = route.Color; + } + + switch (route.Style) + { + default: + case Route.RouteStyle.Solid: pen.DashStyle = XDashStyle.Solid; break; + case Route.RouteStyle.Dashed: pen.DashStyle = XDashStyle.Dash; break; + case Route.RouteStyle.Dotted: pen.DashStyle = XDashStyle.Dot; break; + } + + // HACK + if (ctx.styles.grayscale && route.Color != Route.DefaultColor && route.Style == Route.DefaultStyle) + { + pen.DashStyle = XDashStyle.Dash; + } + + + ctx.graphics.DrawLine(pen, startPoint, endPoint); + } + } + } + } + + private enum BorderLayer { Fill, Stroke }; + private static void DrawMicroBorders(RenderContext ctx, FontCache styleRes, BorderLayer layer) + { + const byte FILL_ALPHA = 64; + + float[] edgex, edgey; + PathUtil.PathType borderPathType = ctx.styles.microBorderStyle == MicroBorderStyle.Square ? + PathUtil.PathType.Square : PathUtil.PathType.Hex; + RenderUtil.HexEdges(borderPathType, out edgex, out edgey); + + XSolidBrush solidBrush = new XSolidBrush(); + XPen pen = new XPen(XColor.Empty); + ctx.styles.microBorders.pen.Apply(ref pen); + + foreach (Sector sector in ctx.selector.Sectors) + { + XGraphicsPath sectorClipPath = null; + + using (RenderUtil.SaveState(ctx.graphics)) + { + // This looks craptacular for Candy style borders :( + if (ctx.tiling && + (layer == BorderLayer.Fill || ctx.styles.microBorderStyle != MicroBorderStyle.Curve)) + { + Sector.ClipPath clip = sector.ComputeClipPath(borderPathType); + if (!ctx.tileRect.IntersectsWith(clip.bounds)) + continue; + + sectorClipPath = new XGraphicsPath(clip.clipPathPoints, clip.clipPathPointTypes, XFillMode.Alternate); + if (sectorClipPath != null) + { + ctx.graphics.IntersectClip(sectorClipPath); + } + } + + ctx.graphics.SmoothingMode = XSmoothingMode.AntiAlias; + + foreach (Border border in sector.Borders) + { + BorderPath borderPath = border.ComputeGraphicsPath(sector, borderPathType); + + XGraphicsPath drawPath = borderPath.borderPathPoints.Length > 0 ? new XGraphicsPath(borderPath.borderPathPoints, borderPath.borderPathTypes, XFillMode.Alternate) : null; + XGraphicsPath clipPath = new XGraphicsPath(borderPath.clipPathPoints, borderPath.clipPathTypes, XFillMode.Alternate); + Color color; + if (ctx.styles.grayscale || + !ColorUtil.NoticeableDifference(border.Color, ctx.styles.backgroundColor)) + { + color = ctx.styles.microBorders.pen.color; // default + } + else + { + color = border.Color; + } + pen.Color = color; + + if (ctx.styles.microBorderStyle != MicroBorderStyle.Curve) + { + // Clip to the path itself - this means adjacent borders don't clash + using (RenderUtil.SaveState(ctx.graphics)) + { + ctx.graphics.IntersectClip(clipPath); + if (layer == BorderLayer.Fill) + { + solidBrush.Color = Color.FromArgb(FILL_ALPHA, color); + ctx.graphics.DrawPath(solidBrush, clipPath); + } + + if (layer == BorderLayer.Stroke && drawPath != null) + { + ctx.graphics.DrawPath(pen, drawPath); + } + } + } + else + { + if (layer == BorderLayer.Fill) + { + solidBrush.Color = Color.FromArgb(FILL_ALPHA, color); + ctx.graphics.DrawClosedCurve(solidBrush, borderPath.clipPathPoints); + } + + if (layer == BorderLayer.Stroke) + { + foreach (PointF[] curve in borderPath.curvePoints) + { + // TODO: Investigate DrawClosedCurve to handle endings + // Would need to have path computer tell whether + // or not the path was actually a closed loop + // Can do it by clipping borders to sector, but that loses + // bottom/right overlaps + ctx.graphics.DrawCurve(pen, curve, 0.6f); + } + } + } + } + } + } + } + } +} diff --git a/RenderUtil.cs b/RenderUtil.cs new file mode 100644 index 000000000..c53637004 --- /dev/null +++ b/RenderUtil.cs @@ -0,0 +1,689 @@ +using PdfSharp.Drawing; +using PdfSharp.Drawing.Layout; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.Linq; + +namespace Maps.Rendering +{ + public static class RenderUtil + { + private const float hexEdge = 0.18f; // TODO: Need to compute this (should be cos(60), inverse-scaled) + private static readonly float[] HexEdgesX = { -0.5f + hexEdge, -0.5f - hexEdge, -0.5f + hexEdge, 0.5f - hexEdge, 0.5f + hexEdge, 0.5f - hexEdge }; + private static readonly float[] HexEdgesY = { 0.5f, 0f, -0.5f, -0.5f, 0, 0.5f }; + + private static readonly float[] SquareEdgesX = { -0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f }; + private static readonly float[] SquareEdgesY = { 0.5f, 0f, -0.5f, -0.5f, 0, 0.5f }; + + public static void HexEdges(PathUtil.PathType type, out float[] edgeX, out float[] edgeY) + { + edgeX = (type == PathUtil.PathType.Hex) ? RenderUtil.HexEdgesX : RenderUtil.SquareEdgesX; + edgeY = (type == PathUtil.PathType.Hex) ? RenderUtil.HexEdgesY : RenderUtil.SquareEdgesY; + } + + public static void DrawImageAlpha(XGraphics graphics, float alpha, Image image, Rectangle targetRect) + { + if (alpha <= 0f) + { + return; + } + + // Clamp and Quantize + alpha = Math.Min(1f, alpha); + alpha = (float)Math.Round(alpha * 16f) / 16f; + int key = (int)Math.Round(alpha * 16); + + XImage ximage; + + int w = image.Width, h = image.Height; + + lock (image) + { + if (image.Tag == null || !(image.Tag is Dictionary)) + { + image.Tag = new Dictionary(); + } + + Dictionary dict = image.Tag as Dictionary; + if (dict.ContainsKey(key)) + { + ximage = dict[key]; + } + else + { + if (alpha >= 1f) + { + ximage = XImage.FromGdiPlusImage(image); + } + else + { + // Need to construct a new image (PdfSharp can't alpha-render images) + // Memoize these in the image itself, since most requests will be from + // a small set + + Bitmap scratchBitmap = new Bitmap(w, h, PixelFormat.Format32bppArgb); + using (var scratchGraphics = Graphics.FromImage(scratchBitmap)) + { + ColorMatrix matrix = new ColorMatrix(); + matrix.Matrix00 = matrix.Matrix11 = matrix.Matrix22 = 1; + matrix.Matrix33 = alpha; + + ImageAttributes attr = new ImageAttributes(); + attr.SetColorMatrix(matrix); + + scratchGraphics.DrawImage(image, new Rectangle(0, 0, w, h), 0, 0, w, h, GraphicsUnit.Pixel, attr); + } + + ximage = XImage.FromGdiPlusImage(scratchBitmap); + } + dict[key] = ximage; + } + } + + lock (ximage) + { + graphics.DrawImage(ximage, targetRect, new XRect(0, 0, w, h), XGraphicsUnit.Point); + } + } + + public static void DrawGlyph(XGraphics g, Glyph glyph, FontCache styleRes, XBrush brush, float x, float y) + { + XFont font = glyph.Font == GlyphFont.Ding ? styleRes.WingdingFont : styleRes.GlyphFont; + g.DrawString(glyph.Characters, font, brush, x, y, StringFormatCentered); + } + + + private static XStringFormat CreateStringFormat(XStringAlignment alignment, XLineAlignment lineAlignment) + { + XStringFormat format = new XStringFormat(); + format.Alignment = alignment; + format.LineAlignment = lineAlignment; + return format; + } + + public static XStringFormat StringFormatCentered { get { return centeredFormat; } } + private static readonly XStringFormat centeredFormat = CreateStringFormat(XStringAlignment.Center, XLineAlignment.Center); + + public static XStringFormat StringFormatTopLeft { get { return topLeftFormat; } } + private static readonly XStringFormat topLeftFormat = CreateStringFormat(XStringAlignment.Near, XLineAlignment.Near); + + public static XStringFormat StringFormatTopCenter { get { return topCenterFormat; } } + private static readonly XStringFormat topCenterFormat = CreateStringFormat(XStringAlignment.Center, XLineAlignment.Near); + + public static XStringFormat StringFormatTopRight { get { return topRightFormat; } } + private static readonly XStringFormat topRightFormat = CreateStringFormat(XStringAlignment.Far, XLineAlignment.Near); + + public static XStringFormat StringFormatCenterLeft { get { return centerLeftFormat; } } + private static readonly XStringFormat centerLeftFormat = CreateStringFormat(XStringAlignment.Near, XLineAlignment.Center); + + public static void DrawLabel(XGraphics g, string text, PointF labelPos, XFont font, XBrush brush, LabelStyle labelStyle) + { + using (RenderUtil.SaveState(g)) + { + XTextFormatter tf = new XTextFormatter(g); + tf.Alignment = XParagraphAlignment.Center; + + XMatrix matrix = new XMatrix(); + matrix.TranslatePrepend(labelPos.X, labelPos.Y); + matrix.ScalePrepend(1.0f / Astrometrics.ParsecScaleX, 1.0f / Astrometrics.ParsecScaleY); + + if (labelStyle.Uppercase) + text = text.ToUpper(); + if (labelStyle.Wrap) + text = text.Replace(' ', '\n'); + + matrix.TranslatePrepend(labelStyle.Translation.X, labelStyle.Translation.Y); + matrix.RotatePrepend(labelStyle.Rotation); + matrix.ScalePrepend(labelStyle.Scale.Width, labelStyle.Scale.Height); + g.MultiplyTransform(matrix, XMatrixOrder.Prepend); + + XSize size = g.MeasureString(text, font); + size.Width *= 2; // prevent cut-off e.g. when rotated + XRect bounds = new XRect(-size.Width / 2, -size.Height / 2, size.Width, size.Height); + tf.DrawString(text, font, brush, bounds); + } + } + + public static SaveGraphicsState SaveState(XGraphics g) + { + return new SaveGraphicsState(g); + } + + public class SaveGraphicsState : IDisposable + { + private XGraphics g; + private XGraphicsState gs; + + public SaveGraphicsState(XGraphics g) + { + this.g = g; + this.gs = g.Save(); + } + + #region IDisposable Members + + void IDisposable.Dispose() + { + this.g.Restore(this.gs); + } + + #endregion + } + + } + + public enum GlyphFont + { + Ding, + Normal + } + + public struct Glyph + { + public enum GlyphBias + { + None, + Top, + Bottom + } + public GlyphFont Font { get; set; } + public string Characters { get; set; } + public GlyphBias Bias { get; set; } + public bool IsHighlighted { get; set; } + + public Glyph(GlyphFont font, string chars) + : this() + { + Font = font; + Characters = chars; + Bias = GlyphBias.None; + IsHighlighted = false; + } + public bool Printable + { + get { return this.Characters.Length > 0; } + } + public Glyph Highlight + { + get + { + Glyph g = this; + g.IsHighlighted = true; + return g; + } + } + public Glyph BiasBottom + { + get + { + Glyph g = this; + g.Bias = GlyphBias.Bottom; + return g; + } + } + + public Glyph BiasTop + { + get + { + Glyph g = this; + g.Bias = GlyphBias.Top; + return g; + } + } + + + public static Glyph None = new Glyph(GlyphFont.Ding, ""); + + // NOTE: Windings are often used instead of UNICODE equivalents in a common font + // because the glyphs are much higher quality. + // See http://www.alanwood.net/demos/wingdings.html for a good mapping + + public static Glyph Diamond = new Glyph(GlyphFont.Ding, "\x74"); // U+2666 (BLACK DIAMOND SUIT) + public static Glyph DiamondX = new Glyph(GlyphFont.Ding, "\x76"); // U+2756 (BLACK DIAMOND MINUS WHITE X) + public static Glyph Circle = new Glyph(GlyphFont.Ding, "\x9f"); // Alternates: U+2022 (BULLET), U+25CF (BLACK CIRCLE) + public static Glyph Triangle = new Glyph(GlyphFont.Normal, "\x25B2"); // U+25B2 (BLACK UP-POINTING TRIANGLE) + public static Glyph Square = new Glyph(GlyphFont.Normal, "\x25A0"); // U+25A0 (BLACK SQUARE) + public static Glyph Star3Point = new Glyph(GlyphFont.Ding, "\xA9"); // U+25B2 (BLACK UP-POINTING TRIANGLE) + public static Glyph Star4Point = new Glyph(GlyphFont.Ding, "\xAA"); // U+2726 (BLACK FOUR POINTED STAR) + public static Glyph Star5Point = new Glyph(GlyphFont.Ding, "\xAB"); // U+2605 (BLACK STAR) + public static Glyph Star6Point = new Glyph(GlyphFont.Ding, "\xAC"); // U+2736 (BLACK SIX POINTED STAR) + public static Glyph WhiteStar = new Glyph(GlyphFont.Normal, "\u2606"); // U+2606 (WHITE STAR) + public static Glyph StarStar = new Glyph(GlyphFont.Normal, "**"); // U+066D (ARABIC FIVE POINTED STAR) + + // Research Stations + public static Glyph Alpha = new Glyph(GlyphFont.Normal, "\x0391").Highlight; + public static Glyph Beta = new Glyph(GlyphFont.Normal, "\x0392").Highlight; + public static Glyph Gamma = new Glyph(GlyphFont.Normal, "\x0393").Highlight; + public static Glyph Delta = new Glyph(GlyphFont.Normal, "\x0394").Highlight; + public static Glyph Epsilon = new Glyph(GlyphFont.Normal, "\x0395").Highlight; + public static Glyph Zeta = new Glyph(GlyphFont.Normal, "\x0396").Highlight; + public static Glyph Eta = new Glyph(GlyphFont.Normal, "\x0397").Highlight; + public static Glyph Theta = new Glyph(GlyphFont.Normal, "\x0398").Highlight; + + // Other Textual + public static Glyph Prison = new Glyph(GlyphFont.Normal, "P").Highlight; + public static Glyph Reserve = new Glyph(GlyphFont.Normal, "R"); + public static Glyph ExileCamp = new Glyph(GlyphFont.Normal, "X"); + + + public static Glyph FromResearchCode(string rs) + { + Glyph glyph = Glyph.Gamma; + + if (rs.Length == 3) + { + char c = rs[2]; + switch (c) + { + case 'A': glyph = Glyph.Alpha; break; + case 'B': glyph = Glyph.Beta; break; + case 'G': glyph = Glyph.Gamma; break; + case 'D': glyph = Glyph.Delta; break; + case 'E': glyph = Glyph.Epsilon; break; + case 'Z': glyph = Glyph.Zeta; break; + case 'H': glyph = Glyph.Eta; break; + case 'T': glyph = Glyph.Theta; break; + } + } + return glyph; + } + + + private static RegexDictionary s_baseGlyphTable = new GlobDictionary { + + // == Classic ================================================================== + + // *.A => N+S (SEC decode) + // *.B => N+W (SEC decode) + { "V?.C", Glyph.StarStar.BiasBottom }, // Vargr Corsair Base + { "*.D", Glyph.Square.BiasBottom }, // Imperial Depot + { "Hv.E", Glyph.StarStar.BiasBottom }, // Hiver Embassy + // *.F => M+J (SEC decode) + { "V?.G", Glyph.Star5Point.Highlight.BiasTop }, // Vargr Naval Base + // V?.H => C+G (SEC decode) + { "*.J", Glyph.Star5Point.Highlight }, // Naval Base + { "K?.K", Glyph.Star5Point.Highlight.BiasTop }, // K'kree Naval Base + { "Hv.L", Glyph.Star5Point.Highlight.BiasTop }, // Hiver Naval Base + { "*.M", Glyph.Star4Point }, // Military Base + { "*.N", Glyph.Star5Point.BiasTop }, // Imperial Naval Base + { "K?.O", Glyph.Square.Highlight.BiasTop }, // K'kree Naval Outpost + { "Dr.P", Glyph.Star5Point.Highlight.BiasTop }, // Droyne Naval Base + { "Dr.Q", Glyph.Star4Point.BiasBottom }, // Droyne Military Garrison + { "A?.R", Glyph.StarStar.BiasBottom }, // Aslan Clan Base + { "*.S", Glyph.Triangle.BiasBottom }, // Imperial Scout Base + { "A?.T", Glyph.Star5Point.Highlight.BiasTop }, // Aslan Tlaukhu Base + // A?.U => R+T (SEC decode) + { "*.V", Glyph.Circle.BiasBottom }, // Scout/Exploration + { "*.W", Glyph.Triangle.Highlight.BiasBottom }, // Imperial Scout Waystation + { "Zh.X", Glyph.Diamond.Highlight }, // Zhodani Relay Station + { "Zh.Y", Glyph.Square.Highlight }, // Zhodani Depot + { "Zh.Z", Glyph.Diamond }, // Zhodani Naval/Military Base + + // == Other ==================================================================== + // *.2 => N+S (SEC decode) + { "So.F", Glyph.Star5Point.Highlight }, // Solomani Naval Base + { "*.*", Glyph.Circle }, // Independent Base + + // Also seen: + // G = Solomani Naval (conflict) + // P = Prison Camp + // R = Research Station (conflict) + }; + + public static Glyph FromBaseCode(string allegiance, char code) + { + return s_baseGlyphTable.Match(allegiance + "." + code); + } + + } + + + + public class BorderPath + { + public readonly PointF[] borderPathPoints; + public readonly byte[] borderPathTypes; + public readonly PointF[] clipPathPoints; + public readonly byte[] clipPathTypes; + public readonly PointF[][] curvePoints; + + public BorderPath(Border border, Sector sector, PathUtil.PathType type) + { + float[] edgeX, edgeY; + RenderUtil.HexEdges(type, out edgeX, out edgeY); + + int lengthEstimate = border.Path.Length * 3; + List borderPathPoints = new List(lengthEstimate); + List borderPathTypes = new List(lengthEstimate); + List clipPathPoints = new List(lengthEstimate); + List clipPathTypes = new List(lengthEstimate); + List> curves = new List>(lengthEstimate); + List curve = new List(lengthEstimate); + + + // Based on http://dotclue.org/t20/sec2pdf - J Greely rocks my world. + + int checkFirst = 0; + int checkLast = 5; + + int startHex = 0; + bool startHexVisited = false; + + foreach (int hex in border.Path) + { + checkLast = checkFirst + 5; + + if (startHexVisited && hex == startHex) + { + // I'm in the starting hex, and I've been + // there before, so stop testing at neighbor + // 5, no matter what + checkLast = 5; + + // degenerate case, entering for third time + if (checkFirst < 3) + break; + } + else if (!startHexVisited) + { + startHex = hex; + startHexVisited = true; + + // PERF: This seems costly... analyze it! + PointF newPoint = Astrometrics.HexToCenter(Astrometrics.LocationToCoordinates(new Location(sector.Location, hex))); + newPoint.X += edgeX[0]; + newPoint.Y += edgeY[0]; + + // MOVETO + borderPathPoints.Add(newPoint); + borderPathTypes.Add((byte)PathPointType.Start); + + // MOVETO + clipPathPoints.Add(newPoint); + clipPathTypes.Add((byte)PathPointType.Start); + + if (curve.Count > 1) + { + curves.Add(curve); + curve = new List(lengthEstimate); + curve.Add(newPoint); + } + else + { + curve.Clear(); + curve.Add(newPoint); + } + } + + PointF pt = Astrometrics.HexToCenter(Astrometrics.LocationToCoordinates(new Location(sector.Location, hex))); + + int i = checkFirst; + for (int check = checkFirst; check <= checkLast; check++) + { + i = check; + int neighbor = Astrometrics.HexNeighbor(hex, i % 6); + + if (Array.IndexOf(border.Path, neighbor) != -1) // Consider a hash here + break; + + PointF newPoint = new PointF(pt.X + edgeX[(i + 1) % 6], pt.Y + edgeY[(i + 1) % 6]); + + // TODO: Replace this by clipping borders to sector bounds; problem - bottom/right edges protrude + // Only actually render edges within the sector + //if( 1 <= ( hex / 100 ) && ( hex / 100 ) <= 32 && 1 <= ( hex % 100 ) && ( hex % 100 ) <= 40 ) + { + // LINETO + borderPathPoints.Add(newPoint); + borderPathTypes.Add((byte)PathPointType.Line); + + //curve.Add( newPoint ); + + if (Util.InRange(hex / 100, 1, Astrometrics.SectorWidth) && Util.InRange(hex % 100, 1, Astrometrics.SectorHeight)) + { + curve.Add(newPoint); + } + else + { + if (curve.Count > 1) + { + curves.Add(curve); + curve = new List(lengthEstimate); + curve.Add(newPoint); + } + else + { + curve.Clear(); + curve.Add(newPoint); + } + } + + } + /* + else + { + // MOVETO + if( borderPathTypes[ borderPathTypes.Count - 1 ] == (byte)PathPointType.Start ) + { + // Collapse multiple MOVETOs - makes GDI+ happy + borderPathPoints[ borderPathPoints.Count - 1 ] = newPoint; + } + else + { + borderPathPoints.Add( newPoint ); + borderPathTypes.Add( (byte)PathPointType.Start ); + } + + if( curve.Count > 1 ) + { + curves.Add( curve ); + curve = new List( lengthEstimate ); + curve.Add( newPoint ); + } + else + { + curve.Clear(); + curve.Add( newPoint ); + } + } + * */ + // LINETO + clipPathPoints.Add(newPoint); + clipPathTypes.Add((byte)PathPointType.Line); + } + i = i % 6; + // i is the direction to the next border hex, + // and when we get there, we'll have come from + // i + 3, so we start checking with i + 4. + checkFirst = (i + 4) % 6; + } + + // Trim trailing MOVETOs - makes GDI+ happy + while (borderPathTypes.Count > 0 && borderPathTypes[borderPathTypes.Count - 1] == (byte)PathPointType.Start) + { + borderPathTypes.RemoveAt(borderPathTypes.Count - 1); + borderPathPoints.RemoveAt(borderPathPoints.Count - 1); + } + + borderPathTypes[borderPathTypes.Count - 1] |= (byte)PathPointType.CloseSubpath; + + this.borderPathPoints = borderPathPoints.ToArray(); + this.borderPathTypes = borderPathTypes.ToArray(); + this.clipPathPoints = clipPathPoints.ToArray(); + this.clipPathTypes = clipPathTypes.ToArray(); + + if (curve.Count > 1) + { + curves.Add(curve); + } + + this.curvePoints = curves.Select(c => c.ToArray()).ToArray(); + } + } + + public static class ColorUtil + { + public static void RGBtoXYZ(int r, int g, int b, out double x, out double y, out double z) + { + double rl = (double)r / 255.0; + double gl = (double)g / 255.0; + double bl = (double)b / 255.0; + + double sr = (rl > 0.04045) ? Math.Pow((rl + 0.055) / (1 + 0.055), 2.2) : (rl / 12.92); + double sg = (gl > 0.04045) ? Math.Pow((gl + 0.055) / (1 + 0.055), 2.2) : (gl / 12.92); + double sb = (bl > 0.04045) ? Math.Pow((bl + 0.055) / (1 + 0.055), 2.2) : (bl / 12.92); + + x = sr * 0.4124 + sg * 0.3576 + sb * 0.1805; + y = sr * 0.2126 + sg * 0.7152 + sb * 0.0722; + z = sr * 0.0193 + sg * 0.1192 + sb * 0.9505; + } + + private static double Fxyz(double t) + { + return ((t > 0.008856) ? Math.Pow(t, (1.0 / 3.0)) : (7.787 * t + 16.0 / 116.0)); + } + + public static void XYZtoLab(double x, double y, double z, out double l, out double a, out double b) + { + const double D65X = 0.9505, D65Y = 1.0, D65Z = 1.0890; + l = 116.0 * Fxyz(y / D65Y) - 16; + a = 500.0 * (Fxyz(x / D65X) - Fxyz(y / D65Y)); + b = 200.0 * (Fxyz(y / D65Y) - Fxyz(z / D65Z)); + } + + public static double DeltaE76(double l1, double a1, double b1, double l2, double a2, double b2) + { + double c1 = l1 - l2, c2 = a1 - a2, c3 = b1 - b2; + return Math.Sqrt(c1 * c1 + c2 * c2 + c3 * c3); + } + + public static bool NoticeableDifference(Color a, Color b) + { + const double JND = 2.3; + + double ax, ay, az; + double bx, by, bz; + RGBtoXYZ(a.R, a.G, a.B, out ax, out ay, out az); + RGBtoXYZ(b.R, b.G, b.B, out bx, out by, out bz); + + double al, aa, ab; + double bl, ba, bb; + XYZtoLab(ax, ay, az, out al, out aa, out ab); + XYZtoLab(bx, by, bz, out bl, out ba, out bb); + + return DeltaE76(al, aa, ab, bl, ba, bb) > JND; + } + + } + public static class PathUtil + { + public enum PathType : int + { + Hex = 0, + Square = 1, + TypeCount = 2 + }; + + public static RectangleF Bounds(XGraphicsPath path) + { + RectangleF rect = new RectangleF(); + + PointF[] points = path.Internals.GdiPath.PathPoints; + + rect.X = points[0].X; + rect.Y = points[0].Y; + + for (int i = 1; i < points.Length; ++i) + { + PointF pt = points[i]; + if (pt.X < rect.X) { float d = rect.X - pt.X; rect.X = pt.X; rect.Width += d; } + if (pt.Y < rect.Y) { float d = rect.Y - pt.Y; rect.Y = pt.Y; rect.Height += d; } + + if (pt.X > rect.Right) { rect.Width = pt.X - rect.X; } + if (pt.Y > rect.Bottom) { rect.Height = pt.Y - rect.Y; } + } + + return rect; + } + + public static void ComputeBorderPath(List clip, float[] edgeX, float[] edgeY, out PointF[] clipPathPointCoords, out byte[] clipPathPointTypes) + { + // TODO: Consolidate this with border path generation (which is very sector/hex-centric, alas) + + List clipPathPoints = new List(clip.Count * 3); + List clipPathTypes = new List(clip.Count * 3); + + // Algorithm based on http://dotclue.org/t20/sec2pdf - J Greely rocks my world. + + int checkFirst = 0; + int checkLast = 5; + + Point startHex = Point.Empty; ; + bool startHexVisited = false; + + foreach (Point hex in clip) + { + checkLast = checkFirst + 5; + + if (startHexVisited && hex == startHex) + { + // I'm in the starting hex, and I've been + // there before, so stop testing at neighbor + // 5, no matter what + checkLast = 5; + + // degenerate case, entering for third time + if (checkFirst < 3) + break; + } + else if (!startHexVisited) + { + startHex = hex; + startHexVisited = true; + + // PERF: This seems costly... analyze it! + PointF newPoint = Astrometrics.HexToCenter(hex); + newPoint.X += edgeX[0]; + newPoint.Y += edgeY[0]; + + // MOVETO + clipPathPoints.Add(newPoint); + clipPathTypes.Add((byte)PathPointType.Start); + } + + PointF pt = Astrometrics.HexToCenter(hex); + + int i = checkFirst; + for (int check = checkFirst; check <= checkLast; check++) + { + i = check; + Point neighbor = Astrometrics.HexNeighbor(hex, i % 6); + + if (clip.Contains(neighbor)) + break; + + PointF newPoint = new PointF(pt.X + edgeX[(i + 1) % 6], pt.Y + edgeY[(i + 1) % 6]); + + // LINETO + clipPathPoints.Add(newPoint); + clipPathTypes.Add((byte)PathPointType.Line); + } + i = i % 6; + + // i is the direction to the next border hex, + // and when we get there, we'll have come from + // i + 3, so we start checking with i + 4. + checkFirst = (i + 4) % 6; + } + + clipPathPointCoords = clipPathPoints.ToArray(); + clipPathPointTypes = clipPathTypes.ToArray(); + clipPathPointTypes[clipPathPointTypes.Length - 1] |= (byte)PathPointType.CloseSubpath; + } + + } + +} diff --git a/ResourceManager.cs b/ResourceManager.cs new file mode 100644 index 000000000..891426cc3 --- /dev/null +++ b/ResourceManager.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Web; +using System.Web.Caching; +using System.Xml.Serialization; + +namespace Maps +{ + public class LRUCache + { + public LRUCache(int size) + { + if (size <= 0) + throw new ApplicationException("size must be > 0"); + m_size = size; + } + + public object this[string key] + { + get + { + int index = m_keys.FindIndex(delegate(string s) { return s == key; }); + if (index == -1) + { + return null; + } + + if (index == 0) + { + return m_values[0]; + } + + string k = m_keys[index]; + object v = m_values[index]; + m_keys.RemoveAt(index); + m_values.RemoveAt(index); + m_keys.Insert(0, k); + m_values.Insert(0, v); + return v; + } + + set + { + m_keys.Insert(0, key); + m_values.Insert(0, value); + while (m_keys.Count > m_size) + { + m_keys.RemoveAt(m_size); + } + while (m_values.Count > m_size) + { + m_values.RemoveAt(m_size); + } + } + } + + public void Clear() + { + m_keys = new List(); + m_values = new List(); + } + + public int Count { get { return m_keys.Count; } } + + public List.Enumerator GetEnumerator() + { + return m_keys.GetEnumerator(); + } + + private int m_size; + private List m_keys = new List(); + private List m_values = new List(); + } + + public interface IDeserializable + { + void Deserialize(Stream stream, string mediaType); + } + + public class ResourceManager + { + private HttpServerUtility m_serverUtility; + public HttpServerUtility Server { get { return m_serverUtility; } } + + private LRUCache s_cache = new LRUCache(50); + + // TODO: Quick Fix - clean this up later + //private Cache m_cache; + public LRUCache Cache { get { return s_cache; } } + + public ResourceManager(HttpServerUtility serverUtility, Cache cache) + { + m_serverUtility = serverUtility; + //m_cache = cache; + } + + public object GetXmlFileObject(string name, Type type, bool cache = true) + { + if (!cache) + { + using (var stream = new FileStream(Server.MapPath(name), FileMode.Open, FileAccess.Read, FileShare.Read)) + { + XmlSerializer xs = new XmlSerializer(type); + object o = xs.Deserialize(stream); + if (o.GetType() != type) + { + throw new InvalidOperationException(); + } + return o; + } + } + + lock (Cache) + { + object o = Cache[name]; + + if (o == null) + { + o = GetXmlFileObject(name, type, cache: false); + + Cache[name] = o; + } + + return o; + } + } + + public object GetDeserializableFileObject(string name, Type type, bool cacheResults, string mediaType) + { + object obj = null; + + // PERF: Whole cache is locked while loading a single item. Should use finer granularity + lock (Cache) + { + obj = Cache[name]; + + if (obj == null) + { + using (var stream = new FileStream(Server.MapPath(name), FileMode.Open, FileAccess.Read, FileShare.Read)) + { + ConstructorInfo constructorInfoObj = type.GetConstructor( + BindingFlags.Instance | BindingFlags.Public, null, + CallingConventions.HasThis, new Type[0], null); + + if (constructorInfoObj == null) + { + throw new TargetException(); + } + + obj = constructorInfoObj.Invoke(null); + + IDeserializable ides = obj as IDeserializable; + if (ides == null) + { + throw new TargetException(); + } + + ides.Deserialize(stream, mediaType); + } + + if (cacheResults) + { + Cache[name] = obj; + } + } + } + + if (obj.GetType() != type) + { + throw new InvalidOperationException("Object is of the wrong type."); + } + + return obj; + } + } +} diff --git a/SEC.aspx b/SEC.aspx new file mode 100644 index 000000000..4b86a19b2 --- /dev/null +++ b/SEC.aspx @@ -0,0 +1,2 @@ +<%@ Page language="c#" Codebehind="SEC.aspx.cs" AutoEventWireup="false" Inherits="Maps.Pages.SEC" %> +<%@ OutputCache Duration="3600" VaryByParam="*" %> diff --git a/SEC.aspx.cs b/SEC.aspx.cs new file mode 100644 index 000000000..e025e9cb1 --- /dev/null +++ b/SEC.aspx.cs @@ -0,0 +1,114 @@ +using System; +using System.IO; +using System.Net.Mime; +using System.Text; + +namespace Maps.Pages +{ + /// + /// Summary description for Search. + /// + public class SEC : DataPage + { + public override string DefaultContentType { get { return System.Net.Mime.MediaTypeNames.Text.Plain; } } + + private void Page_Load(object sender, System.EventArgs e) + { + if (!ServiceConfiguration.CheckEnabled("sec", Response)) + { + return; + } + + // NOTE: This (re)initializes a static data structure used for + // resolving names into sector locations, so needs to be run + // before any other objects (e.g. Worlds) are loaded. + ResourceManager resourceManager = new ResourceManager(Server, Cache); + SectorMap map = SectorMap.FromName(SectorMap.DefaultSetting, resourceManager); + Sector sector; + + if (HasOption("sx") && HasOption("sy")) + { + int sx = GetIntOption("sx", 0); + int sy = GetIntOption("sy", 0); + + sector = map.FromLocation(sx, sy); + + if (sector == null) + { + SendError(404, "Not Found", String.Format("The sector at {0},{1} was not found.", sx, sy)); + return; + } + } + else if (HasOption("sector")) + { + string sectorName = GetStringOption("sector"); + sector = map.FromName(sectorName); + + if (sector == null) + { + SendError(404, "Not Found", String.Format("The specified sector '{0}' was not found.", sectorName)); + return; + } + } + else + { + SendError(404, "Not Found", "No sector specified."); + return; + } + + WorldFilter filter = null; + if (HasOption("subsector")) + { + string ss = GetStringOption("subsector"); + filter = (World world) => (world.SS == ss); + } + + bool includeMetadata = GetBoolOption("metadata", defaultValue: true); + bool includeHeader = GetBoolOption("header", defaultValue: true); + + string mediaType = GetStringOption("type"); + Encoding encoding;; + switch (mediaType) { + case "SecondSurvey": + case "TabDelimited": + encoding = Util.UTF8_NO_BOM; + break; + default: + encoding = Encoding.GetEncoding(1252); + break; + } + + + string data; + using (var writer = new StringWriter()) + { + // Content + // + sector.Serialize(resourceManager, writer, mediaType, includeMetadata:includeMetadata, includeHeader:includeHeader, filter:filter); + data = writer.ToString(); + } + SendResult(data, encoding); + } + + #region Web Form Designer generated code + override protected void OnInit(EventArgs e) + { + // + // CODEGEN: This call is required by the ASP.NET Web Form Designer. + // + InitializeComponent(); + base.OnInit(e); + } + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.Load += new System.EventHandler(this.Page_Load); + } + #endregion + + } +} diff --git a/Search.aspx b/Search.aspx new file mode 100644 index 000000000..560b6793f --- /dev/null +++ b/Search.aspx @@ -0,0 +1,2 @@ +<%@ Page language="c#" Codebehind="Search.aspx.cs" AutoEventWireup="false" Inherits="Maps.Pages.Search" %> +<%@ OutputCache Duration="3600" VaryByParam="*" VaryByHeader="Accept"%> diff --git a/Search.aspx.cs b/Search.aspx.cs new file mode 100644 index 000000000..41807b453 --- /dev/null +++ b/Search.aspx.cs @@ -0,0 +1,247 @@ +using Json; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Mime; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Serialization; + +namespace Maps.Pages +{ + /// + /// Summary description for Search. + /// + public class Search : DataPage + { + public override string DefaultContentType { get { return System.Net.Mime.MediaTypeNames.Text.Xml; } } + + private static string[] SpecialSearchTerms = { @"(default)", @"(grand tour)", @"(arrival vengeance)", @"(far frontiers)" }; + private static string[] SpecialSearchResultsXml = { @"~/res/search/Default.xml", @"~/res/search/GrandTour.xml", @"~/res/search/ArrivalVengeance.xml", @"~/res/search/FarFrontiers.xml" }; + private static string[] SpecialSearchResultsJson = { @"~/res/search/Default.json", @"~/res/search/GrandTour.json", @"~/res/search/ArrivalVengeance.json", @"~/res/search/FarFrontiers.json" }; + + private static Regex UWP_REGEXP = new Regex(@"^\w{7}-\w$"); + + private void Page_Load(object sender, System.EventArgs e) + { + if (!ServiceConfiguration.CheckEnabled("search", Response)) + { + return; + } + + string query = Request.QueryString["q"]; + if (query == null) + return; + + // Look for special searches + var index = Array.FindIndex(SpecialSearchTerms, s => String.Compare(s, query, ignoreCase: true, culture: CultureInfo.InvariantCulture) == 0); + if (index != -1) + { + if (Request.QueryString["jsonp"] != null) + { + // TODO: Does this include the JSONP headers? + SendFile(JsonConstants.MediaType, SpecialSearchResultsJson[index]); + return; + } + + foreach (var type in AcceptTypes) + { + if (type == JsonConstants.MediaType) + { + SendFile(JsonConstants.MediaType, SpecialSearchResultsJson[index]); + return; + } + if (type == MediaTypeNames.Text.Xml) + { + SendFile(MediaTypeNames.Text.Xml, SpecialSearchResultsXml[index]); + return; + } + } + SendFile(MediaTypeNames.Text.Xml, SpecialSearchResultsXml[index]); + return; + } + + // + // Do the search + // + ResourceManager resourceManager = new ResourceManager(Server, Cache); + SectorMap map = SectorMap.FromName(SectorMap.DefaultSetting, resourceManager); + + query = query.Replace('*', '%'); // Support * and % as wildcards + query = query.Replace('?', '_'); // Support ? and _ as wildcards + + if (UWP_REGEXP.IsMatch(query)) + { + query = "uwp:" + query; + } + + var searchResults = SearchEngine.PerformSearch(query, resourceManager, SearchEngine.SearchResultsType.Default, 40); + + Results resultsList = new Results(); + + if (searchResults != null) + { + resultsList.AddRange(searchResults + .Select(loc => Results.LocationToSearchResult(map, resourceManager, loc)) + .OfType()); + } + + SendResult(resultsList); + } + + [JsonName("Results")] + [XmlRoot(ElementName = "results")] + public class Results + { + public Results() + { + Items = new List(); + } + + [XmlAttribute] + public int Count { get { return Items.Count; } set { /* We only want to serialize, not deserialize */ } } + + // This is necessary to get "clean" XML serialization of a heterogeneous list; + // otherwise the output is sprinkled with xsi:type declarations and the base class + // is used as the element name + [XmlElement(ElementName = "world", Type = typeof(WorldResult))] + [XmlElement(ElementName = "subsector", Type = typeof(SubsectorResult))] + [XmlElement(ElementName = "sector", Type = typeof(SectorResult))] + + public List Items { get; set; } + + public void Add(SearchResultItem item) + { + Items.Add(item); + } + public void AddRange(IEnumerable items) + { + Items.AddRange(items); + } + + [JsonName("World")] + public class WorldResult : SearchResultItem + { + [XmlAttribute("hexX")] + public int HexX { get; set; } + + [XmlAttribute("hexY")] + public int HexY { get; set; } + + [XmlAttribute("sector")] + public string Sector { get; set; } + + [XmlAttribute("uwp")] + public string Uwp { get; set; } + } + + [JsonName("Subsector")] + public class SubsectorResult : SearchResultItem + { + [XmlAttribute("sector")] + public string Sector { get; set; } + + [XmlAttribute("index")] + public string Index { get; set; } + } + + [JsonName("Sector")] + [XmlRoot(ElementName = "sector")] + public class SectorResult : SearchResultItem + { + } + + public class SearchResultItem + { + [XmlAttribute("sectorX")] + public int SectorX { get; set; } + + [XmlAttribute("sectorY")] + public int SectorY { get; set; } + + [XmlAttribute("name")] + public string Name { get; set; } + } + + public static SearchResultItem LocationToSearchResult(SectorMap map, ResourceManager resourceManager, ItemLocation location) + { + if (location is WorldLocation) + { + Sector sector; + World world; + ((WorldLocation)location).Resolve(map, resourceManager, out sector, out world); + + if (sector == null || world == null) + return null; + + WorldResult r = new WorldResult(); + r.SectorX = sector.X; + r.SectorY = sector.Y; + r.HexX = (world.Hex / 100); + r.HexY = (world.Hex % 100); + r.Name = world.Name; + r.Sector = sector.Names[0].Text; + r.Uwp = world.UWP; + + return r; + } + else if (location is SubsectorLocation) + { + Sector sector; + Subsector subsector; + ((SubsectorLocation)location).Resolve(map, out sector, out subsector); + + if (sector == null || subsector == null) + return null; + + SubsectorResult r = new SubsectorResult(); + r.SectorX = sector.X; + r.SectorY = sector.Y; + r.Name = subsector.Name; + r.Index = subsector.Index; + r.Sector = sector.Names[0].Text; + + return r; + } + else if (location is SectorLocation) + { + Sector sector = ((SectorLocation)location).Resolve(map); + + if (sector == null) + return null; + + SectorResult r = new SectorResult(); + r.SectorX = sector.X; + r.SectorY = sector.Y; + r.Name = sector.Names[0].Text; + + return r; + } + + return null; + } + } + + + #region Web Form Designer generated code + override protected void OnInit(EventArgs e) + { + // + // CODEGEN: This call is required by the ASP.NET Web Form Designer. + // + InitializeComponent(); + base.OnInit(e); + } + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.Load += new System.EventHandler(this.Page_Load); + } + #endregion + } +} diff --git a/SearchEngine.cs b/SearchEngine.cs new file mode 100644 index 000000000..b7b0660c0 --- /dev/null +++ b/SearchEngine.cs @@ -0,0 +1,315 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; + +namespace Maps +{ + static class DBUtil + { + public static SqlConnection MakeConnection() + { + string connectionStringName; + + if (System.Web.HttpContext.Current.Request.Url.Host == "localhost") + { + connectionStringName = "SqlDev"; + } + else + { + connectionStringName = "SqlProd"; + } + + string connectionString = System.Configuration.ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString; + SqlConnection conn = new SqlConnection(connectionString); + conn.Open(); + return conn; + } + } + + + + /// + /// Summary description for SearchEngine. + /// + public static class SearchEngine + { + [Flags] + public enum SearchResultsType : int + { + Sectors = 0x0001, + Subsectors = 0x0002, + Worlds = 0x0004, + UWP = 0x0008, + Default = Sectors | Subsectors | Worlds // NOTE: doesn't include UWP + } + + public delegate void StatusCallback(string status); + + public static void PopulateDatabase(ResourceManager resourceManager, StatusCallback callback) + { + // Lock on this class rather than the cacheResults. Tile requests are not + // blocked but we don't index twice. + lock (typeof(SearchEngine)) + { + // NOTE: This (re)initializes a static data structure used for + // resolving names into sector locations, so needs to be run + // before any other objects (e.g. Worlds) are loaded. + SectorMap map = SectorMap.FromName(SectorMap.DefaultSetting, resourceManager); + + using (var connection = DBUtil.MakeConnection()) + { + SqlCommand sqlCommand; + + // + // Repopulate the tables - locally first + // FUTURE: will need to batch this up rather than keep it in memory! + // + + DataTable dt_sectors = new DataTable(); + dt_sectors.Columns.Add(new DataColumn()); + dt_sectors.Columns.Add(new DataColumn()); + dt_sectors.Columns.Add(new DataColumn()); + + DataTable dt_subsectors = new DataTable(); + dt_subsectors.Columns.Add(new DataColumn()); + dt_subsectors.Columns.Add(new DataColumn()); + dt_subsectors.Columns.Add(new DataColumn()); + dt_subsectors.Columns.Add(new DataColumn()); + + DataTable dt_worlds = new DataTable(); + dt_worlds.Columns.Add(new DataColumn()); + dt_worlds.Columns.Add(new DataColumn()); + dt_worlds.Columns.Add(new DataColumn()); + dt_worlds.Columns.Add(new DataColumn()); + dt_worlds.Columns.Add(new DataColumn()); + dt_worlds.Columns.Add(new DataColumn()); + + callback("Parsing data..."); + foreach (Sector sector in map.Sectors) + { + foreach (Name name in sector.Names) + { + DataRow row = dt_sectors.NewRow(); + row.ItemArray = new object[] { sector.X, sector.Y, name.Text }; + dt_sectors.Rows.Add(row); + } + + + foreach (Subsector subsector in sector.Subsectors) + { + DataRow row = dt_subsectors.NewRow(); + row.ItemArray = new object[] { sector.X, sector.Y, subsector.Index, subsector.Name }; + dt_subsectors.Rows.Add(row); + } + +#if DEBUG + if( !sector.Selected ) + continue; +#endif + // NOTE: May need to page this at some point + WorldCollection worlds = sector.GetWorlds(resourceManager, cacheResults: false); + if (worlds != null) + { + + foreach (World world in worlds) + { + DataRow row = dt_worlds.NewRow(); + row.ItemArray = new object[] { + sector.X, + sector.Y, + world.X, + world.Y, + world.Name != null && world.Name.Length > 0 + ? (object)world.Name + : (object)DBNull.Value, + world.UWP }; + + dt_worlds.Rows.Add(row); + } + } + + } + + + // + // Rebuild the tables with fresh schema + // + string[] rebuild_schema = { + "IF EXISTS(SELECT 1 FROM sys.objects WHERE OBJECT_ID = OBJECT_ID(N'sectors') AND type = (N'U')) DROP TABLE sectors", + "CREATE TABLE sectors(x int NOT NULL,y int NOT NULL,name nvarchar(50) NULL)", + "CREATE NONCLUSTERED INDEX sector_name ON sectors ( name ASC ) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)", + + "IF EXISTS(SELECT 1 FROM sys.objects WHERE OBJECT_ID = OBJECT_ID(N'subsectors') AND type = (N'U')) DROP TABLE subsectors", + "CREATE TABLE subsectors (sector_x int NOT NULL, sector_y int NOT NULL, subsector_index char(1) NOT NULL, name nvarchar(50) NULL )", + "CREATE NONCLUSTERED INDEX subsector_name ON subsectors ( name ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)", + + "IF EXISTS(SELECT 1 FROM sys.objects WHERE OBJECT_ID = OBJECT_ID(N'worlds') AND type = (N'U')) DROP TABLE worlds", + "CREATE TABLE worlds( sector_x int NOT NULL, sector_y int NOT NULL, hex_x int NOT NULL, hex_y int NOT NULL, name nvarchar(50) NULL, uwp nchar(9) NULL )", + "CREATE NONCLUSTERED INDEX world_name ON worlds ( name ASC ) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)", + "CREATE NONCLUSTERED INDEX world_uwp ON worlds ( uwp ASC ) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)", + }; + + callback("Rebuilding schema..."); + foreach (string cmd in rebuild_schema) + { + sqlCommand = new SqlCommand(cmd, connection); + sqlCommand.ExecuteNonQuery(); + } + + // + // And shovel the data into the database en masse + // + using (var bulk = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, null)) + { + callback(String.Format("Writing {0} sectors...", dt_sectors.Rows.Count)); + bulk.BatchSize = dt_sectors.Rows.Count; + bulk.DestinationTableName = "sectors"; + bulk.WriteToServer(dt_sectors); + bulk.Close(); + } + using (var bulk = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, null)) + { + callback(String.Format("Writing {0} subsectors...", dt_subsectors.Rows.Count)); + bulk.BatchSize = dt_subsectors.Rows.Count; + bulk.DestinationTableName = "subsectors"; + bulk.WriteToServer(dt_subsectors); + bulk.Close(); + } + using (var bulk = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, null)) + { + callback(String.Format("Writing {0} worlds...", dt_worlds.Rows.Count)); + bulk.BatchSize = 4096; + bulk.DestinationTableName = "worlds"; + bulk.WriteToServer(dt_worlds); + bulk.Close(); + } + } + callback("Complete!"); + } + } + + public static IEnumerable PerformSearch(string query, ResourceManager resourceManager, SearchResultsType types, int numResults) + { + lock (typeof(SearchEngine)) + { + // Ensure the database is not being rebuilt + } + + List results = new List(); + + using (var connection = DBUtil.MakeConnection()) + { + List clauses = new List(); + List terms = new List(); + foreach (string t in query.Split((char[])null, StringSplitOptions.RemoveEmptyEntries)) + { + string term = t; + string clause; + if (term.StartsWith("uwp:")) + { + term = term.Substring(term.IndexOf(':') + 1); + clause = "uwp LIKE @term"; + types = SearchResultsType.UWP; + } + else if (term.StartsWith("exact:")) + { + term = term.Substring(term.IndexOf(':') + 1); + clause = "name LIKE @term"; + } + else if (term.StartsWith("like:")) + { + term = term.Substring(term.IndexOf(':') + 1); + clause = "SOUNDEX(name) = SOUNDEX(@term)"; + } + else if (term.Contains("%") || term.Contains("_")) + { + clause = "name LIKE @term"; + } + else + { + clause = "name LIKE @term + '%' OR name LIKE '% ' + @term + '%'"; + } + + clause = clause.Replace("@term", String.Format("@term{0}", terms.Count)); + clauses.Add("(" + clause + ")"); + terms.Add(term); + } + + string where = String.Join(" AND ", clauses.ToArray()); + + // NOTE: DISTINCT is to filter out "Ley" and "Ley Sector" (different names, same result). + // TODO: Include the searched-for name in the results, and show alternate names in the result set. + // {0} is the list of distinct fields (i.e. coordinates), {1} is the table, {2} is the filter + string query_format = "SELECT DISTINCT TOP " + numResults + " {0},name FROM {1} WHERE {2} ORDER BY name ASC"; + + // Sectors + if (types.HasFlag(SearchResultsType.Sectors) && numResults > 0) + { + string sql = String.Format(query_format, "x, y", "sectors", where); + using (var sqlCommand = new SqlCommand(sql, connection)) + { + for (int i = 0; i < terms.Count; ++i) + { + sqlCommand.Parameters.AddWithValue(String.Format("@term{0}", i), terms[i]); + } + using (var row = sqlCommand.ExecuteReader()) + { + while (row.Read()) + { + results.Add(new SectorLocation(row.GetInt32(0), row.GetInt32(1))); + numResults -= 1; + } + } + } + } + + // Subsectors + if (types.HasFlag(SearchResultsType.Subsectors) && numResults > 0) + { + string sql = String.Format(query_format, "sector_x, sector_y, subsector_index", "subsectors", where); + using (var sqlCommand = new SqlCommand(sql, connection)) + { + for (int i = 0; i < terms.Count; ++i) + { + sqlCommand.Parameters.AddWithValue(String.Format("@term{0}", i), terms[i]); + } + using (var row = sqlCommand.ExecuteReader()) + { + while (row.Read()) + { + char[] chars = new char[1]; + row.GetChars(2, 0, chars, 0, chars.Length); + + results.Add(new SubsectorLocation(row.GetInt32(0), row.GetInt32(1), chars[0])); + } + } + } + } + + // Worlds & UWPs + if ((types.HasFlag(SearchResultsType.Worlds) || types.HasFlag(SearchResultsType.UWP)) && numResults > 0) + { + string sql = String.Format(query_format, "sector_x, sector_y, hex_x, hex_y", "worlds", where); + using (var sqlCommand = new SqlCommand(sql, connection)) + { + for (int i = 0; i < terms.Count; ++i) + { + sqlCommand.Parameters.AddWithValue(String.Format("@term{0}", i), terms[i]); + } + using (var row = sqlCommand.ExecuteReader()) + { + while (row.Read()) + { + results.Add(new WorldLocation(row.GetInt32(0), row.GetInt32(1), row.GetInt32(2), row.GetInt32(3))); + } + } + } + } + } + + return results; + } + } +} diff --git a/Sector.cs b/Sector.cs new file mode 100644 index 000000000..4287edd0c --- /dev/null +++ b/Sector.cs @@ -0,0 +1,971 @@ +using Json; +using Maps.Rendering; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Xml.Serialization; + +namespace Maps +{ + +#if NOT_YET_IMPLEMENTED + [XmlRoot( ElementName = "Setting" )] + public class Setting + { + public Setting() + { + Rifts = new List(); + Borders = new List(); + Worlds = new List(); + SectorCollections = new List(); + } + + private SectorMap m_map; + public SectorMap Map { get { return m_map; } } + + public List Rifts { get; set; } + public List Borders { get; set; } + public List Worlds { get; set; } + public List SectorCollections { get; set; } + + public static Setting GetSetting( string name, ResourceManager resourceManager ) + { + // TODO: Cache these statically + Setting setting = resourceManager.GetXmlFileObject( String.Format( @"~/res/Setting_{0}.xml", name ), typeof( Setting ), false ) as Setting; + + setting.m_map = null; // new SectorMap( /*setting.m_sectorCollections*/ ); + + return setting; + } + } +#endif + + public class MapNotInitializedException : Exception + { + public MapNotInitializedException() + : + base("SectorMap data not initialized") + { + } + } + + public class SectorMap + { + public const string DefaultSetting = "OTU"; + + private static SectorMap s_OTU; + + private SectorCollection m_sectors; + public IList Sectors { get { return m_sectors.Sectors; } } + + private Dictionary m_nameMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + // TODO: Add Dictionary, Sector> for FromLocation lookups + + private SectorMap(List metafiles, ResourceManager resourceManager) + { + foreach (var fileName in metafiles) + { + SectorCollection collection = resourceManager.GetXmlFileObject(fileName, typeof(SectorCollection), cache: false) as SectorCollection; + + if (m_sectors == null) + { + m_sectors = collection; + } + else + { + m_sectors.Merge(collection); + } + } + + m_nameMap.Clear(); + + foreach (var sector in m_sectors.Sectors) + { + if (sector.MetadataFile != null) + { + Sector metadata = resourceManager.GetXmlFileObject(@"~/res/Sectors/" + sector.MetadataFile, typeof(Sector), cache: false) as Sector; + sector.Merge(metadata); + } + + foreach (var name in sector.Names) + { + try + { + m_nameMap.Add(name.Text, sector); + } + catch (ArgumentException) + { + // If it's already in there, ignore it + // FUTURE: Return a list of candidates + } + } + + if (!String.IsNullOrEmpty(sector.Abbreviation) && !m_nameMap.ContainsKey(sector.Abbreviation)) + { + m_nameMap.Add(sector.Abbreviation, sector); + } + } + } + + public static SectorMap FromName(string settingName, ResourceManager resourceManager) + { + if (settingName != SectorMap.DefaultSetting) + { + throw new ArgumentException("Only OTU setting is currently supported."); + } + + lock (typeof(SectorMap)) + { + if (s_OTU == null) + { + List files = new List(2); + files.Add(@"~/res/sectors.xml"); + files.Add(@"~/res/ZhodaniCoreRoute.xml"); + + s_OTU = new SectorMap(files, resourceManager); + } + } + + return s_OTU; + } + + public static void Flush() + { + lock (typeof(SectorMap)) + { + s_OTU = null; + } + } + + public static Sector FromName(string settingName, string sectorName) + { + // TODO: Having this method supports deserializing data that refers generically to + // sectors by name. + // * Consider decoupling sector *names* from sector data + // * Consider Location (the offender) having a default setting + + if (settingName == null) + { + settingName = SectorMap.DefaultSetting; + } + + if (settingName != SectorMap.DefaultSetting) + { + throw new ArgumentException("Only OTU setting is currently supported"); + } + + if (s_OTU == null) + { + throw new MapNotInitializedException(); + } + + return s_OTU.FromName(sectorName); + } + + public Sector FromName(string sectorName) + { + if (m_sectors == null || m_nameMap == null) + throw new MapNotInitializedException(); + + Sector sector; + m_nameMap.TryGetValue(sectorName, out sector); // Using indexer throws exception, this is more performant + return sector; + } + + public Sector FromLocation(int x, int y) + { + if (m_sectors == null) + throw new MapNotInitializedException(); + + // TODO: If perf is a concern, replace this with an array (or some such). + return m_sectors.Sectors.Where(sector => sector.X == x && sector.Y == y).FirstOrDefault(); + } + } + + public class SectorBase : MetadataItem + { + public SectorBase() + { + this.Names = new List(); + } + + public SectorBase(SectorBase other) + { + // TODO: Deep copy? + this.Location = other.Location; + this.Names = other.Names; + this.Abbreviation = other.Abbreviation; + } + + public int X { get { return Location.X; } set { Location = new Point(value, Location.Y); } } + public int Y { get { return Location.Y; } set { Location = new Point(Location.X, value); } } + + [XmlAttribute] + public string Abbreviation { get; set; } + + [XmlIgnore, JsonIgnore] + public Point Location { get; set; } + + [XmlElement("Name")] + public List Names { get; set; } + + [XmlIgnore, JsonIgnore] + public string Domain { get; set; } + + [XmlIgnore, JsonIgnore] + public string AlphaQuadrant { get; set; } + [XmlIgnore, JsonIgnore] + public string BetaQuadrant { get; set; } + [XmlIgnore, JsonIgnore] + public string GammaQuadrant { get; set; } + [XmlIgnore, JsonIgnore] + public string DeltaQuadrant { get; set; } + + [XmlIgnore, JsonIgnore] + public Rectangle Bounds + { + get + { + return new Rectangle( + (this.Location.X * Astrometrics.SectorWidth) - Astrometrics.ReferenceHex.X, + (this.Location.Y * Astrometrics.SectorHeight) - Astrometrics.ReferenceHex.Y, + Astrometrics.SectorWidth, Astrometrics.SectorHeight + ); + } + } + + public Rectangle SubsectorBounds(int index) + { + return new Rectangle( + (this.Location.X * Astrometrics.SectorWidth) - Astrometrics.ReferenceHex.X + (Astrometrics.SubsectorWidth * (index % 4)), + (this.Location.Y * Astrometrics.SectorHeight) - Astrometrics.ReferenceHex.Y + (Astrometrics.SubsectorHeight * (index / 4)), + Astrometrics.SubsectorWidth, + Astrometrics.SubsectorHeight); + } + } + + public class Sector : SectorBase + { + public Sector() + { + } + + public Sector(Stream stream, string mediaType) + { + WorldCollection wc = new WorldCollection(); + wc.Deserialize(stream, mediaType); + foreach (World world in wc) + { + world.Sector = this; + } + m_data = wc; + } + + private MetadataCollection m_subsectors = new MetadataCollection(); + private MetadataCollection m_routes = new MetadataCollection(); + private MetadataCollection