diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/cidades.geojson b/cidades.geojson new file mode 100644 index 0000000..07dd00b --- /dev/null +++ b/cidades.geojson @@ -0,0 +1 @@ +{"features":[{"id":"Amajari ","properties":{"constatacoes":3,"cidade":"Amajari ","estado":"RR","falta de controle social":1,"despesa irregular":1,"falta de controle administrativo":1,"desvio de finalidade":0,"geo_longitude":-62.4364788858074,"geo_latitude":3.73320372216553,"falta de prestacao de contas":0},"type":"Feature","geometry":{"type":"Point","coordinates":[-62.4364788858074,3.73320372216553]}},{"id":"Anajás","properties":{"constatacoes":42,"cidade":"Anajás","estado":"PA","falta de controle social":5,"despesa irregular":12,"falta de controle administrativo":17,"desvio de finalidade":1,"geo_longitude":-49.9620442424099,"geo_latitude":-0.827102765083608,"falta de prestacao de contas":7},"type":"Feature","geometry":{"type":"Point","coordinates":[-49.9620442424099,-0.827102765083608]}},{"id":"Bagre","properties":{"constatacoes":19,"cidade":"Bagre","estado":"PA","falta de controle social":0,"despesa irregular":2,"falta de controle administrativo":16,"desvio de finalidade":0,"geo_longitude":-50.181886105853,"geo_latitude":-2.3727937539989,"falta de prestacao de contas":1},"type":"Feature","geometry":{"type":"Point","coordinates":[-50.181886105853,-2.3727937539989]}},{"id":"Bandeirantes do Tocantins","properties":{"constatacoes":16,"cidade":"Bandeirantes do Tocantins","estado":"TO","falta de controle social":1,"despesa irregular":10,"falta de controle administrativo":4,"desvio de finalidade":1,"geo_longitude":-48.6751190742069,"geo_latitude":-7.90313863795738,"falta de prestacao de contas":0},"type":"Feature","geometry":{"type":"Point","coordinates":[-48.6751190742069,-7.90313863795738]}},{"id":"Benjamin Constant","properties":{"constatacoes":19,"cidade":"Benjamin Constant","estado":"AM","falta de controle social":1,"despesa irregular":10,"falta de controle administrativo":5,"desvio de finalidade":2,"geo_longitude":-70.2779013574385,"geo_latitude":-5.42897223759102,"falta de prestacao de contas":1},"type":"Feature","geometry":{"type":"Point","coordinates":[-70.2779013574385,-5.42897223759102]}},{"id":"Brasiléia","properties":{"constatacoes":7,"cidade":"Brasiléia","estado":"AC","falta de controle social":0,"despesa irregular":1,"falta de controle administrativo":6,"desvio de finalidade":0,"geo_longitude":-69.2069998673404,"geo_latitude":-10.7473334370358,"falta de prestacao de contas":0},"type":"Feature","geometry":{"type":"Point","coordinates":[-69.2069998673404,-10.7473334370358]}},{"id":"Bujaru","properties":{"constatacoes":27,"cidade":"Bujaru","estado":"PA","falta de controle social":2,"despesa irregular":5,"falta de controle administrativo":12,"desvio de finalidade":1,"geo_longitude":-48.0889349969042,"geo_latitude":-1.63417546992712,"falta de prestacao de contas":7},"type":"Feature","geometry":{"type":"Point","coordinates":[-48.0889349969042,-1.63417546992712]}},{"id":"Cachoeira do Píria","properties":{"constatacoes":25,"cidade":"Cachoeira do Píria","estado":"PA","falta de controle social":2,"despesa irregular":5,"falta de controle administrativo":6,"desvio de finalidade":1,"geo_longitude":-46.4396638125261,"geo_latitude":-1.99102550603213,"falta de prestacao de contas":11},"type":"Feature","geometry":{"type":"Point","coordinates":[-46.4396638125261,-1.99102550603213]}},{"id":"Cerejeiras ","properties":{"constatacoes":11,"cidade":"Cerejeiras ","estado":"RO","falta de controle social":0,"despesa irregular":3,"falta de controle administrativo":5,"desvio de finalidade":2,"geo_longitude":-61.2604408478098,"geo_latitude":-13.2032094116601,"falta de prestacao de contas":1},"type":"Feature","geometry":{"type":"Point","coordinates":[-61.2604408478098,-13.2032094116601]}},{"id":"Curralinho","properties":{"constatacoes":13,"cidade":"Curralinho","estado":"PA","falta de controle social":1,"despesa irregular":4,"falta de controle administrativo":1,"desvio de finalidade":0,"geo_longitude":-49.9881154215778,"geo_latitude":-1.58357999173309,"falta de prestacao de contas":7},"type":"Feature","geometry":{"type":"Point","coordinates":[-49.9881154215778,-1.58357999173309]}},{"id":"Igarapé-Miri","properties":{"constatacoes":19,"cidade":"Igarapé-Miri","estado":"PA","falta de controle social":2,"despesa irregular":7,"falta de controle administrativo":8,"desvio de finalidade":0,"geo_longitude":-49.1308525107435,"geo_latitude":-2.0626837745378,"falta de prestacao de contas":2},"type":"Feature","geometry":{"type":"Point","coordinates":[-49.1308525107435,-2.0626837745378]}},{"id":"Inhangapi","properties":{"constatacoes":34,"cidade":"Inhangapi","estado":"PA","falta de controle social":5,"despesa irregular":7,"falta de controle administrativo":14,"desvio de finalidade":0,"geo_longitude":-47.9231113034151,"geo_latitude":-1.46484392570826,"falta de prestacao de contas":8},"type":"Feature","geometry":{"type":"Point","coordinates":[-47.9231113034151,-1.46484392570826]}},{"id":"Itaituba","properties":{"constatacoes":22,"cidade":"Itaituba","estado":"PA","falta de controle social":1,"despesa irregular":9,"falta de controle administrativo":7,"desvio de finalidade":2,"geo_longitude":-56.4964075831386,"geo_latitude":-5.8714887303191,"falta de prestacao de contas":3},"type":"Feature","geometry":{"type":"Point","coordinates":[-56.4964075831386,-5.8714887303191]}},{"id":"Luzinopolis ","properties":{"constatacoes":4,"cidade":"Luzinopolis ","estado":"TO","falta de controle social":0,"despesa irregular":1,"falta de controle administrativo":0,"desvio de finalidade":0,"geo_longitude":-47.8299945498496,"geo_latitude":-6.20601343507278,"falta de prestacao de contas":3},"type":"Feature","geometry":{"type":"Point","coordinates":[-47.8299945498496,-6.20601343507278]}},{"id":"Manoel Urbano","properties":{"constatacoes":19,"cidade":"Manoel Urbano","estado":"AC","falta de controle social":2,"despesa irregular":6,"falta de controle administrativo":5,"desvio de finalidade":1,"geo_longitude":-69.8544769663042,"geo_latitude":-9.37985196122167,"falta de prestacao de contas":5},"type":"Feature","geometry":{"type":"Point","coordinates":[-69.8544769663042,-9.37985196122167]}},{"id":"Normandia ","properties":{"constatacoes":11,"cidade":"Normandia ","estado":"RR","falta de controle social":2,"despesa irregular":2,"falta de controle administrativo":6,"desvio de finalidade":0,"geo_longitude":-60.0644531962823,"geo_latitude":3.87932633480897,"falta de prestacao de contas":1},"type":"Feature","geometry":{"type":"Point","coordinates":[-60.0644531962823,3.87932633480897]}},{"id":"Novo Acordo ","properties":{"constatacoes":24,"cidade":"Novo Acordo ","estado":"TO","falta de controle social":1,"despesa irregular":11,"falta de controle administrativo":11,"desvio de finalidade":1,"geo_longitude":-47.3824314077444,"geo_latitude":-10.1553967179081,"falta de prestacao de contas":0},"type":"Feature","geometry":{"type":"Point","coordinates":[-47.3824314077444,-10.1553967179081]}},{"id":"Pedra Branca do Amapari","properties":{"constatacoes":4,"cidade":"Pedra Branca do Amapari","estado":"AP","falta de controle social":0,"despesa irregular":1,"falta de controle administrativo":1,"desvio de finalidade":1,"geo_longitude":-52.5232890033095,"geo_latitude":1.15820676732735,"falta de prestacao de contas":1},"type":"Feature","geometry":{"type":"Point","coordinates":[-52.5232890033095,1.15820676732735]}},{"id":"Presidente Médici","properties":{"constatacoes":21,"cidade":"Presidente Médici","estado":"RO","falta de controle social":2,"despesa irregular":4,"falta de controle administrativo":13,"desvio de finalidade":0,"geo_longitude":-61.9031511,"geo_latitude":-11.1718397,"falta de prestacao de contas":2},"type":"Feature","geometry":{"type":"Point","coordinates":[-61.9031511,-11.1718397]}},{"id":"Primavera","properties":{"constatacoes":23,"cidade":"Primavera","estado":"PA","falta de controle social":3,"despesa irregular":6,"falta de controle administrativo":8,"desvio de finalidade":1,"geo_longitude":-47.1312062755797,"geo_latitude":-0.952300139721098,"falta de prestacao de contas":5},"type":"Feature","geometry":{"type":"Point","coordinates":[-47.1312062755797,-0.952300139721098]}},{"id":"Primavera de Rondonia","properties":{"constatacoes":22,"cidade":"Primavera de Rondonia","estado":"RO","falta de controle social":0,"despesa irregular":8,"falta de controle administrativo":9,"desvio de finalidade":2,"geo_longitude":-61.3147571977666,"geo_latitude":-11.8781773393525,"falta de prestacao de contas":3},"type":"Feature","geometry":{"type":"Point","coordinates":[-61.3147571977666,-11.8781773393525]}},{"id":"Santa Maria do Pará","properties":{"constatacoes":29,"cidade":"Santa Maria do Pará","estado":"PA","falta de controle social":5,"despesa irregular":10,"falta de controle administrativo":12,"desvio de finalidade":1,"geo_longitude":-47.5215250396842,"geo_latitude":-1.37440764909157,"falta de prestacao de contas":1},"type":"Feature","geometry":{"type":"Point","coordinates":[-47.5215250396842,-1.37440764909157]}},{"id":"Santa Rosa do Tocantins","properties":{"constatacoes":6,"cidade":"Santa Rosa do Tocantins","estado":"TO","falta de controle social":2,"despesa irregular":1,"falta de controle administrativo":3,"desvio de finalidade":0,"geo_longitude":-48.1077399786291,"geo_latitude":-11.3980375399799,"falta de prestacao de contas":0},"type":"Feature","geometry":{"type":"Point","coordinates":[-48.1077399786291,-11.3980375399799]}},{"id":"São Bento do Tocantins","properties":{"constatacoes":19,"cidade":"São Bento do Tocantins","estado":"TO","falta de controle social":2,"despesa irregular":4,"falta de controle administrativo":9,"desvio de finalidade":1,"geo_longitude":-47.939049702623,"geo_latitude":-5.97441477161837,"falta de prestacao de contas":3},"type":"Feature","geometry":{"type":"Point","coordinates":[-47.939049702623,-5.97441477161837]}},{"id":"São Felipe D' Oeste","properties":{"constatacoes":20,"cidade":"São Felipe D' Oeste","estado":"RO","falta de controle social":0,"despesa irregular":5,"falta de controle administrativo":9,"desvio de finalidade":0,"geo_longitude":-61.5024945,"geo_latitude":-11.9013796,"falta de prestacao de contas":6},"type":"Feature","geometry":{"type":"Point","coordinates":[-61.5024945,-11.9013796]}},{"id":"São Sebastião da Boa Vista","properties":{"constatacoes":11,"cidade":"São Sebastião da Boa Vista","estado":"PA","falta de controle social":1,"despesa irregular":1,"falta de controle administrativo":6,"desvio de finalidade":0,"geo_longitude":-49.6801934829464,"geo_latitude":-1.43404556254384,"falta de prestacao de contas":3},"type":"Feature","geometry":{"type":"Point","coordinates":[-49.6801934829464,-1.43404556254384]}},{"id":"Tabatinga","properties":{"constatacoes":57,"cidade":"Tabatinga","estado":"AM","falta de controle social":7,"despesa irregular":21,"falta de controle administrativo":16,"desvio de finalidade":4,"geo_longitude":-69.9367927,"geo_latitude":-4.2300835,"falta de prestacao de contas":9},"type":"Feature","geometry":{"type":"Point","coordinates":[-69.9367927,-4.2300835]}},{"id":"Tefé","properties":{"constatacoes":25,"cidade":"Tefé","estado":"AM","falta de controle social":1,"despesa irregular":6,"falta de controle administrativo":11,"desvio de finalidade":2,"geo_longitude":-65.5288959034801,"geo_latitude":-4.46806215545654,"falta de prestacao de contas":5},"type":"Feature","geometry":{"type":"Point","coordinates":[-65.5288959034801,-4.46806215545654]}},{"id":"Terra Santa ","properties":{"constatacoes":16,"cidade":"Terra Santa ","estado":"PA","falta de controle social":2,"despesa irregular":9,"falta de controle administrativo":3,"desvio de finalidade":1,"geo_longitude":-56.4583452573171,"geo_latitude":-1.95199799551885,"falta de prestacao de contas":1},"type":"Feature","geometry":{"type":"Point","coordinates":[-56.4583452573171,-1.95199799551885]}},{"id":"Uiramutã","properties":{"constatacoes":11,"cidade":"Uiramutã","estado":"RR","falta de controle social":2,"despesa irregular":4,"falta de controle administrativo":5,"desvio de finalidade":0,"geo_longitude":-60.2803592213995,"geo_latitude":4.68423983382354,"falta de prestacao de contas":0},"type":"Feature","geometry":{"type":"Point","coordinates":[-60.2803592213995,4.68423983382354]}},{"id":"Urucurituba","properties":{"constatacoes":32,"cidade":"Urucurituba","estado":"AM","falta de controle social":3,"despesa irregular":15,"falta de controle administrativo":7,"desvio de finalidade":0,"geo_longitude":-57.769868938388,"geo_latitude":-2.81191677831962,"falta de prestacao de contas":7},"type":"Feature","geometry":{"type":"Point","coordinates":[-57.769868938388,-2.81191677831962]}},{"id":"Xambioa","properties":{"constatacoes":8,"cidade":"Xambioa","estado":"TO","falta de controle social":1,"despesa irregular":2,"falta de controle administrativo":5,"desvio de finalidade":0,"geo_longitude":-48.4511126242233,"geo_latitude":-6.54512513149259,"falta de prestacao de contas":0},"type":"Feature","geometry":{"type":"Point","coordinates":[-48.4511126242233,-6.54512513149259]}}],"type":"FeatureCollection"} \ No newline at end of file diff --git a/constatacoes.json.php b/constatacoes.json.php new file mode 100644 index 0000000..07b8115 --- /dev/null +++ b/constatacoes.json.php @@ -0,0 +1,29 @@ + $cell) { + $item[$headers[$key]] = $cell; + } + $data[] = $item; +} + +header('Content-Type: application/json'); +echo json_encode($data); + +?> \ No newline at end of file diff --git a/img/bg.jpg b/img/bg.jpg new file mode 100644 index 0000000..4e4a7a9 Binary files /dev/null and b/img/bg.jpg differ diff --git a/img/bg2.jpg b/img/bg2.jpg new file mode 100644 index 0000000..9e4909f Binary files /dev/null and b/img/bg2.jpg differ diff --git a/img/marker.png b/img/marker.png new file mode 100644 index 0000000..8475c66 Binary files /dev/null and b/img/marker.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..f738515 --- /dev/null +++ b/index.html @@ -0,0 +1,51 @@ + + + + + + + + Educação na Amazônia + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+
+
+ + \ No newline at end of file diff --git a/js/.DS_Store b/js/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/js/.DS_Store differ diff --git a/js/chosen-sprite.png b/js/chosen-sprite.png new file mode 100644 index 0000000..113dc98 Binary files /dev/null and b/js/chosen-sprite.png differ diff --git a/js/chosen.css b/js/chosen.css new file mode 100644 index 0000000..2c47cee --- /dev/null +++ b/js/chosen.css @@ -0,0 +1,396 @@ +/* @group Base */ +.chzn-container { + font-size: 13px; + position: relative; + display: inline-block; + zoom: 1; + *display: inline; +} +.chzn-container .chzn-drop { + background: #fff; + border: 1px solid #aaa; + border-top: 0; + position: absolute; + top: 29px; + left: 0; + -webkit-box-shadow: 0 4px 5px rgba(0,0,0,.15); + -moz-box-shadow : 0 4px 5px rgba(0,0,0,.15); + -o-box-shadow : 0 4px 5px rgba(0,0,0,.15); + box-shadow : 0 4px 5px rgba(0,0,0,.15); + z-index: 1010; +} +/* @end */ + +/* @group Single Chosen */ +.chzn-container-single .chzn-single { + background-color: #ffffff; + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0 ); + background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #ffffff), color-stop(50%, #f6f6f6), color-stop(52%, #eeeeee), color-stop(100%, #f4f4f4)); + background-image: -webkit-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); + background-image: -moz-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); + background-image: -o-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); + background-image: -ms-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); + background-image: linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%); + -webkit-border-radius: 5px; + -moz-border-radius : 5px; + border-radius : 5px; + -moz-background-clip : padding; + -webkit-background-clip: padding-box; + background-clip : padding-box; + border: 1px solid #aaaaaa; + -webkit-box-shadow: 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1); + -moz-box-shadow : 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1); + box-shadow : 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1); + display: block; + overflow: hidden; + white-space: nowrap; + position: relative; + height: 23px; + line-height: 24px; + padding: 0 0 0 8px; + color: #444444; + text-decoration: none; +} +.chzn-container-single .chzn-default { + color: #999; +} +.chzn-container-single .chzn-single span { + margin-right: 26px; + display: block; + overflow: hidden; + white-space: nowrap; + -o-text-overflow: ellipsis; + -ms-text-overflow: ellipsis; + text-overflow: ellipsis; +} +.chzn-container-single .chzn-single abbr { + display: block; + position: absolute; + right: 26px; + top: 6px; + width: 12px; + height: 13px; + font-size: 1px; + background: url('chosen-sprite.png') right top no-repeat; +} +.chzn-container-single .chzn-single abbr:hover { + background-position: right -11px; +} +.chzn-container-single.chzn-disabled .chzn-single abbr:hover { + background-position: right top; +} +.chzn-container-single .chzn-single div { + position: absolute; + right: 0; + top: 0; + display: block; + height: 100%; + width: 18px; +} +.chzn-container-single .chzn-single div b { + background: url('chosen-sprite.png') no-repeat 0 0; + display: block; + width: 100%; + height: 100%; +} +.chzn-container-single .chzn-search { + padding: 3px 4px; + position: relative; + margin: 0; + white-space: nowrap; + z-index: 1010; +} +.chzn-container-single .chzn-search input { + background: #fff url('chosen-sprite.png') no-repeat 100% -22px; + background: url('chosen-sprite.png') no-repeat 100% -22px, -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff)); + background: url('chosen-sprite.png') no-repeat 100% -22px, -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%); + background: url('chosen-sprite.png') no-repeat 100% -22px, -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%); + background: url('chosen-sprite.png') no-repeat 100% -22px, -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%); + background: url('chosen-sprite.png') no-repeat 100% -22px, -ms-linear-gradient(top, #eeeeee 1%, #ffffff 15%); + background: url('chosen-sprite.png') no-repeat 100% -22px, linear-gradient(top, #eeeeee 1%, #ffffff 15%); + margin: 1px 0; + padding: 4px 20px 4px 5px; + outline: 0; + border: 1px solid #aaa; + font-family: sans-serif; + font-size: 1em; +} +.chzn-container-single .chzn-drop { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius : 0 0 4px 4px; + border-radius : 0 0 4px 4px; + -moz-background-clip : padding; + -webkit-background-clip: padding-box; + background-clip : padding-box; +} +/* @end */ + +.chzn-container-single-nosearch .chzn-search input { + position: absolute; + left: -9000px; +} + +/* @group Multi Chosen */ +.chzn-container-multi .chzn-choices { + background-color: #fff; + background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff)); + background-image: -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%); + background-image: -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%); + background-image: -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%); + background-image: -ms-linear-gradient(top, #eeeeee 1%, #ffffff 15%); + background-image: linear-gradient(top, #eeeeee 1%, #ffffff 15%); + border: 1px solid #aaa; + margin: 0; + padding: 0; + cursor: text; + overflow: hidden; + height: auto !important; + height: 1%; + position: relative; +} +.chzn-container-multi .chzn-choices li { + float: left; + list-style: none; +} +.chzn-container-multi .chzn-choices .search-field { + white-space: nowrap; + margin: 0; + padding: 0; +} +.chzn-container-multi .chzn-choices .search-field input { + color: #666; + background: transparent !important; + border: 0 !important; + font-family: sans-serif; + font-size: 100%; + height: 15px; + padding: 5px; + margin: 1px 0; + outline: 0; + -webkit-box-shadow: none; + -moz-box-shadow : none; + -o-box-shadow : none; + box-shadow : none; +} +.chzn-container-multi .chzn-choices .search-field .default { + color: #999; +} +.chzn-container-multi .chzn-choices .search-choice { + -webkit-border-radius: 3px; + -moz-border-radius : 3px; + border-radius : 3px; + -moz-background-clip : padding; + -webkit-background-clip: padding-box; + background-clip : padding-box; + background-color: #e4e4e4; + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f4f4f4', endColorstr='#eeeeee', GradientType=0 ); + background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee)); + background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); + background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); + background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); + background-image: -ms-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); + background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%); + -webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); + -moz-box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); + box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05); + color: #333; + border: 1px solid #aaaaaa; + line-height: 13px; + padding: 3px 20px 3px 5px; + margin: 3px 0 3px 5px; + position: relative; + cursor: default; +} +.chzn-container-multi .chzn-choices .search-choice-focus { + background: #d4d4d4; +} +.chzn-container-multi .chzn-choices .search-choice .search-choice-close { + display: block; + position: absolute; + right: 3px; + top: 4px; + width: 12px; + height: 13px; + font-size: 1px; + background: url('chosen-sprite.png') right top no-repeat; +} +.chzn-container-multi .chzn-choices .search-choice .search-choice-close:hover { + background-position: right -11px; +} +.chzn-container-multi .chzn-choices .search-choice-focus .search-choice-close { + background-position: right -11px; +} +/* @end */ + +/* @group Results */ +.chzn-container .chzn-results { + margin: 0 4px 4px 0; + max-height: 240px; + padding: 0 0 0 4px; + position: relative; + overflow-x: hidden; + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} +.chzn-container-multi .chzn-results { + margin: -1px 0 0; + padding: 0; +} +.chzn-container .chzn-results li { + display: none; + line-height: 15px; + padding: 5px 6px; + margin: 0; + list-style: none; +} +.chzn-container .chzn-results .active-result { + cursor: pointer; + display: list-item; +} +.chzn-container .chzn-results .highlighted { + background-color: #3875d7; + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#3875d7', endColorstr='#2a62bc', GradientType=0 ); + background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #3875d7), color-stop(90%, #2a62bc)); + background-image: -webkit-linear-gradient(top, #3875d7 20%, #2a62bc 90%); + background-image: -moz-linear-gradient(top, #3875d7 20%, #2a62bc 90%); + background-image: -o-linear-gradient(top, #3875d7 20%, #2a62bc 90%); + background-image: -ms-linear-gradient(top, #3875d7 20%, #2a62bc 90%); + background-image: linear-gradient(top, #3875d7 20%, #2a62bc 90%); + color: #fff; +} +.chzn-container .chzn-results li em { + background: #feffde; + font-style: normal; +} +.chzn-container .chzn-results .highlighted em { + background: transparent; +} +.chzn-container .chzn-results .no-results { + background: #f4f4f4; + display: list-item; +} +.chzn-container .chzn-results .group-result { + cursor: default; + color: #999; + font-weight: bold; +} +.chzn-container .chzn-results .group-option { + padding-left: 15px; +} +.chzn-container-multi .chzn-drop .result-selected { + display: none; +} +.chzn-container .chzn-results-scroll { + background: white; + margin: 0 4px; + position: absolute; + text-align: center; + width: 321px; /* This should by dynamic with js */ + z-index: 1; +} +.chzn-container .chzn-results-scroll span { + display: inline-block; + height: 17px; + text-indent: -5000px; + width: 9px; +} +.chzn-container .chzn-results-scroll-down { + bottom: 0; +} +.chzn-container .chzn-results-scroll-down span { + background: url('chosen-sprite.png') no-repeat -4px -3px; +} +.chzn-container .chzn-results-scroll-up span { + background: url('chosen-sprite.png') no-repeat -22px -3px; +} +/* @end */ + +/* @group Active */ +.chzn-container-active .chzn-single { + -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); + -moz-box-shadow : 0 0 5px rgba(0,0,0,.3); + -o-box-shadow : 0 0 5px rgba(0,0,0,.3); + box-shadow : 0 0 5px rgba(0,0,0,.3); + border: 1px solid #5897fb; +} +.chzn-container-active .chzn-single-with-drop { + border: 1px solid #aaa; + -webkit-box-shadow: 0 1px 0 #fff inset; + -moz-box-shadow : 0 1px 0 #fff inset; + -o-box-shadow : 0 1px 0 #fff inset; + box-shadow : 0 1px 0 #fff inset; + background-color: #eee; + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0 ); + background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #eeeeee), color-stop(80%, #ffffff)); + background-image: -webkit-linear-gradient(top, #eeeeee 20%, #ffffff 80%); + background-image: -moz-linear-gradient(top, #eeeeee 20%, #ffffff 80%); + background-image: -o-linear-gradient(top, #eeeeee 20%, #ffffff 80%); + background-image: -ms-linear-gradient(top, #eeeeee 20%, #ffffff 80%); + background-image: linear-gradient(top, #eeeeee 20%, #ffffff 80%); + -webkit-border-bottom-left-radius : 0; + -webkit-border-bottom-right-radius: 0; + -moz-border-radius-bottomleft : 0; + -moz-border-radius-bottomright: 0; + border-bottom-left-radius : 0; + border-bottom-right-radius: 0; +} +.chzn-container-active .chzn-single-with-drop div { + background: transparent; + border-left: none; +} +.chzn-container-active .chzn-single-with-drop div b { + background-position: -18px 1px; +} +.chzn-container-active .chzn-choices { + -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); + -moz-box-shadow : 0 0 5px rgba(0,0,0,.3); + -o-box-shadow : 0 0 5px rgba(0,0,0,.3); + box-shadow : 0 0 5px rgba(0,0,0,.3); + border: 1px solid #5897fb; +} +.chzn-container-active .chzn-choices .search-field input { + color: #111 !important; +} +/* @end */ + +/* @group Disabled Support */ +.chzn-disabled { + cursor: default; + opacity:0.5 !important; +} +.chzn-disabled .chzn-single { + cursor: default; +} +.chzn-disabled .chzn-choices .search-choice .search-choice-close { + cursor: default; +} + +/* @group Right to Left */ +.chzn-rtl { text-align: right; } +.chzn-rtl .chzn-single { padding: 0 8px 0 0; overflow: visible; } +.chzn-rtl .chzn-single span { margin-left: 26px; margin-right: 0; direction: rtl; } + +.chzn-rtl .chzn-single div { left: 3px; right: auto; } +.chzn-rtl .chzn-single abbr { + left: 26px; + right: auto; +} +.chzn-rtl .chzn-choices .search-field input { direction: rtl; } +.chzn-rtl .chzn-choices li { float: right; } +.chzn-rtl .chzn-choices .search-choice { padding: 3px 5px 3px 19px; margin: 3px 5px 3px 0; } +.chzn-rtl .chzn-choices .search-choice .search-choice-close { left: 4px; right: auto; background-position: right top;} +.chzn-rtl.chzn-container-single .chzn-results { margin: 0 0 4px 4px; padding: 0 4px 0 0; } +.chzn-rtl .chzn-results .group-option { padding-left: 0; padding-right: 15px; } +.chzn-rtl.chzn-container-active .chzn-single-with-drop div { border-right: none; } +.chzn-rtl .chzn-search input { + background: #fff url('chosen-sprite.png') no-repeat -38px -22px; + background: url('chosen-sprite.png') no-repeat -38px -22px, -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff)); + background: url('chosen-sprite.png') no-repeat -38px -22px, -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%); + background: url('chosen-sprite.png') no-repeat -38px -22px, -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%); + background: url('chosen-sprite.png') no-repeat -38px -22px, -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%); + background: url('chosen-sprite.png') no-repeat -38px -22px, -ms-linear-gradient(top, #eeeeee 1%, #ffffff 15%); + background: url('chosen-sprite.png') no-repeat -38px -22px, linear-gradient(top, #eeeeee 1%, #ffffff 15%); + padding: 4px 5px 4px 20px; + direction: rtl; +} +/* @end */ \ No newline at end of file diff --git a/js/chosen.jquery.js b/js/chosen.jquery.js new file mode 100644 index 0000000..df3997b --- /dev/null +++ b/js/chosen.jquery.js @@ -0,0 +1,1004 @@ +// Chosen, a Select Box Enhancer for jQuery and Protoype +// by Patrick Filler for Harvest, http://getharvest.com +// +// Version 0.9.8 +// Full source at https://github.com/harvesthq/chosen +// Copyright (c) 2011 Harvest http://getharvest.com + +// MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md +// This file is generated by `cake build`, do not edit it by hand. +(function() { + var SelectParser; + + SelectParser = (function() { + + function SelectParser() { + this.options_index = 0; + this.parsed = []; + } + + SelectParser.prototype.add_node = function(child) { + if (child.nodeName === "OPTGROUP") { + return this.add_group(child); + } else { + return this.add_option(child); + } + }; + + SelectParser.prototype.add_group = function(group) { + var group_position, option, _i, _len, _ref, _results; + group_position = this.parsed.length; + this.parsed.push({ + array_index: group_position, + group: true, + label: group.label, + children: 0, + disabled: group.disabled + }); + _ref = group.childNodes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + option = _ref[_i]; + _results.push(this.add_option(option, group_position, group.disabled)); + } + return _results; + }; + + SelectParser.prototype.add_option = function(option, group_position, group_disabled) { + if (option.nodeName === "OPTION") { + if (option.text !== "") { + if (group_position != null) this.parsed[group_position].children += 1; + this.parsed.push({ + array_index: this.parsed.length, + options_index: this.options_index, + value: option.value, + text: option.text, + html: option.innerHTML, + selected: option.selected, + disabled: group_disabled === true ? group_disabled : option.disabled, + group_array_index: group_position, + classes: option.className, + style: option.style.cssText + }); + } else { + this.parsed.push({ + array_index: this.parsed.length, + options_index: this.options_index, + empty: true + }); + } + return this.options_index += 1; + } + }; + + return SelectParser; + + })(); + + SelectParser.select_to_array = function(select) { + var child, parser, _i, _len, _ref; + parser = new SelectParser(); + _ref = select.childNodes; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + parser.add_node(child); + } + return parser.parsed; + }; + + this.SelectParser = SelectParser; + +}).call(this); + +/* +Chosen source: generate output using 'cake build' +Copyright (c) 2011 by Harvest +*/ + +(function() { + var AbstractChosen, root; + + root = this; + + AbstractChosen = (function() { + + function AbstractChosen(form_field, options) { + this.form_field = form_field; + this.options = options != null ? options : {}; + this.set_default_values(); + this.is_multiple = this.form_field.multiple; + this.set_default_text(); + this.setup(); + this.set_up_html(); + this.register_observers(); + this.finish_setup(); + } + + AbstractChosen.prototype.set_default_values = function() { + var _this = this; + this.click_test_action = function(evt) { + return _this.test_active_click(evt); + }; + this.activate_action = function(evt) { + return _this.activate_field(evt); + }; + this.active_field = false; + this.mouse_on_container = false; + this.results_showing = false; + this.result_highlighted = null; + this.result_single_selected = null; + this.allow_single_deselect = (this.options.allow_single_deselect != null) && (this.form_field.options[0] != null) && this.form_field.options[0].text === "" ? this.options.allow_single_deselect : false; + this.disable_search_threshold = this.options.disable_search_threshold || 0; + this.search_contains = this.options.search_contains || false; + this.choices = 0; + this.single_backstroke_delete = this.options.single_backstroke_delete || false; + return this.max_selected_options = this.options.max_selected_options || Infinity; + }; + + AbstractChosen.prototype.set_default_text = function() { + if (this.form_field.getAttribute("data-placeholder")) { + this.default_text = this.form_field.getAttribute("data-placeholder"); + } else if (this.is_multiple) { + this.default_text = this.options.placeholder_text_multiple || this.options.placeholder_text || "Select Some Options"; + } else { + this.default_text = this.options.placeholder_text_single || this.options.placeholder_text || "Select an Option"; + } + return this.results_none_found = this.form_field.getAttribute("data-no_results_text") || this.options.no_results_text || "No results match"; + }; + + AbstractChosen.prototype.mouse_enter = function() { + return this.mouse_on_container = true; + }; + + AbstractChosen.prototype.mouse_leave = function() { + return this.mouse_on_container = false; + }; + + AbstractChosen.prototype.input_focus = function(evt) { + var _this = this; + if (!this.active_field) { + return setTimeout((function() { + return _this.container_mousedown(); + }), 50); + } + }; + + AbstractChosen.prototype.input_blur = function(evt) { + var _this = this; + if (!this.mouse_on_container) { + this.active_field = false; + return setTimeout((function() { + return _this.blur_test(); + }), 100); + } + }; + + AbstractChosen.prototype.result_add_option = function(option) { + var classes, style; + if (!option.disabled) { + option.dom_id = this.container_id + "_o_" + option.array_index; + classes = option.selected && this.is_multiple ? [] : ["active-result"]; + if (option.selected) classes.push("result-selected"); + if (option.group_array_index != null) classes.push("group-option"); + if (option.classes !== "") classes.push(option.classes); + style = option.style.cssText !== "" ? " style=\"" + option.style + "\"" : ""; + return '
  • ' + option.html + '
  • '; + } else { + return ""; + } + }; + + AbstractChosen.prototype.results_update_field = function() { + if (!this.is_multiple) this.results_reset_cleanup(); + this.result_clear_highlight(); + this.result_single_selected = null; + return this.results_build(); + }; + + AbstractChosen.prototype.results_toggle = function() { + if (this.results_showing) { + return this.results_hide(); + } else { + return this.results_show(); + } + }; + + AbstractChosen.prototype.results_search = function(evt) { + if (this.results_showing) { + return this.winnow_results(); + } else { + return this.results_show(); + } + }; + + AbstractChosen.prototype.keyup_checker = function(evt) { + var stroke, _ref; + stroke = (_ref = evt.which) != null ? _ref : evt.keyCode; + this.search_field_scale(); + switch (stroke) { + case 8: + if (this.is_multiple && this.backstroke_length < 1 && this.choices > 0) { + return this.keydown_backstroke(); + } else if (!this.pending_backstroke) { + this.result_clear_highlight(); + return this.results_search(); + } + break; + case 13: + evt.preventDefault(); + if (this.results_showing) return this.result_select(evt); + break; + case 27: + if (this.results_showing) this.results_hide(); + return true; + case 9: + case 38: + case 40: + case 16: + case 91: + case 17: + break; + default: + return this.results_search(); + } + }; + + AbstractChosen.prototype.generate_field_id = function() { + var new_id; + new_id = this.generate_random_id(); + this.form_field.id = new_id; + return new_id; + }; + + AbstractChosen.prototype.generate_random_char = function() { + var chars, newchar, rand; + chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + rand = Math.floor(Math.random() * chars.length); + return newchar = chars.substring(rand, rand + 1); + }; + + return AbstractChosen; + + })(); + + root.AbstractChosen = AbstractChosen; + +}).call(this); + +/* +Chosen source: generate output using 'cake build' +Copyright (c) 2011 by Harvest +*/ + +(function() { + var $, Chosen, get_side_border_padding, root, + __hasProp = Object.prototype.hasOwnProperty, + __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; }; + + root = this; + + $ = jQuery; + + $.fn.extend({ + chosen: function(options) { + if ($.browser.msie && ($.browser.version === "6.0" || $.browser.version === "7.0")) { + return this; + } + return this.each(function(input_field) { + var $this; + $this = $(this); + if (!$this.hasClass("chzn-done")) { + return $this.data('chosen', new Chosen(this, options)); + } + }); + } + }); + + Chosen = (function(_super) { + + __extends(Chosen, _super); + + function Chosen() { + Chosen.__super__.constructor.apply(this, arguments); + } + + Chosen.prototype.setup = function() { + this.form_field_jq = $(this.form_field); + this.current_value = this.form_field_jq.val(); + return this.is_rtl = this.form_field_jq.hasClass("chzn-rtl"); + }; + + Chosen.prototype.finish_setup = function() { + return this.form_field_jq.addClass("chzn-done"); + }; + + Chosen.prototype.set_up_html = function() { + var container_div, dd_top, dd_width, sf_width; + this.container_id = this.form_field.id.length ? this.form_field.id.replace(/[^\w]/g, '_') : this.generate_field_id(); + this.container_id += "_chzn"; + this.f_width = this.form_field_jq.outerWidth(); + container_div = $("
    ", { + id: this.container_id, + "class": "chzn-container" + (this.is_rtl ? ' chzn-rtl' : ''), + style: 'width: ' + this.f_width + 'px;' + }); + if (this.is_multiple) { + container_div.html('
    '); + } else { + container_div.html('' + this.default_text + '
    '); + } + this.form_field_jq.hide().after(container_div); + this.container = $('#' + this.container_id); + this.container.addClass("chzn-container-" + (this.is_multiple ? "multi" : "single")); + this.dropdown = this.container.find('div.chzn-drop').first(); + dd_top = this.container.height(); + dd_width = this.f_width - get_side_border_padding(this.dropdown); + this.dropdown.css({ + "width": dd_width + "px", + "top": dd_top + "px" + }); + this.search_field = this.container.find('input').first(); + this.search_results = this.container.find('ul.chzn-results').first(); + this.search_field_scale(); + this.search_no_results = this.container.find('li.no-results').first(); + if (this.is_multiple) { + this.search_choices = this.container.find('ul.chzn-choices').first(); + this.search_container = this.container.find('li.search-field').first(); + } else { + this.search_container = this.container.find('div.chzn-search').first(); + this.selected_item = this.container.find('.chzn-single').first(); + sf_width = dd_width - get_side_border_padding(this.search_container) - get_side_border_padding(this.search_field); + this.search_field.css({ + "width": sf_width + "px" + }); + } + this.results_build(); + this.set_tab_index(); + return this.form_field_jq.trigger("liszt:ready", { + chosen: this + }); + }; + + Chosen.prototype.register_observers = function() { + var _this = this; + this.container.mousedown(function(evt) { + return _this.container_mousedown(evt); + }); + this.container.mouseup(function(evt) { + return _this.container_mouseup(evt); + }); + this.container.mouseenter(function(evt) { + return _this.mouse_enter(evt); + }); + this.container.mouseleave(function(evt) { + return _this.mouse_leave(evt); + }); + this.search_results.mouseup(function(evt) { + return _this.search_results_mouseup(evt); + }); + this.search_results.mouseover(function(evt) { + return _this.search_results_mouseover(evt); + }); + this.search_results.mouseout(function(evt) { + return _this.search_results_mouseout(evt); + }); + this.form_field_jq.bind("liszt:updated", function(evt) { + return _this.results_update_field(evt); + }); + this.search_field.blur(function(evt) { + return _this.input_blur(evt); + }); + this.search_field.keyup(function(evt) { + return _this.keyup_checker(evt); + }); + this.search_field.keydown(function(evt) { + return _this.keydown_checker(evt); + }); + if (this.is_multiple) { + this.search_choices.click(function(evt) { + return _this.choices_click(evt); + }); + return this.search_field.focus(function(evt) { + return _this.input_focus(evt); + }); + } else { + return this.container.click(function(evt) { + return evt.preventDefault(); + }); + } + }; + + Chosen.prototype.search_field_disabled = function() { + this.is_disabled = this.form_field_jq[0].disabled; + if (this.is_disabled) { + this.container.addClass('chzn-disabled'); + this.search_field[0].disabled = true; + if (!this.is_multiple) { + this.selected_item.unbind("focus", this.activate_action); + } + return this.close_field(); + } else { + this.container.removeClass('chzn-disabled'); + this.search_field[0].disabled = false; + if (!this.is_multiple) { + return this.selected_item.bind("focus", this.activate_action); + } + } + }; + + Chosen.prototype.container_mousedown = function(evt) { + var target_closelink; + if (!this.is_disabled) { + target_closelink = evt != null ? ($(evt.target)).hasClass("search-choice-close") : false; + if (evt && evt.type === "mousedown" && !this.results_showing) { + evt.stopPropagation(); + } + if (!this.pending_destroy_click && !target_closelink) { + if (!this.active_field) { + if (this.is_multiple) this.search_field.val(""); + $(document).click(this.click_test_action); + this.results_show(); + } else if (!this.is_multiple && evt && (($(evt.target)[0] === this.selected_item[0]) || $(evt.target).parents("a.chzn-single").length)) { + evt.preventDefault(); + this.results_toggle(); + } + return this.activate_field(); + } else { + return this.pending_destroy_click = false; + } + } + }; + + Chosen.prototype.container_mouseup = function(evt) { + if (evt.target.nodeName === "ABBR" && !this.is_disabled) { + return this.results_reset(evt); + } + }; + + Chosen.prototype.blur_test = function(evt) { + if (!this.active_field && this.container.hasClass("chzn-container-active")) { + return this.close_field(); + } + }; + + Chosen.prototype.close_field = function() { + $(document).unbind("click", this.click_test_action); + if (!this.is_multiple) { + this.selected_item.attr("tabindex", this.search_field.attr("tabindex")); + this.search_field.attr("tabindex", -1); + } + this.active_field = false; + this.results_hide(); + this.container.removeClass("chzn-container-active"); + this.winnow_results_clear(); + this.clear_backstroke(); + this.show_search_field_default(); + return this.search_field_scale(); + }; + + Chosen.prototype.activate_field = function() { + if (!this.is_multiple && !this.active_field) { + this.search_field.attr("tabindex", this.selected_item.attr("tabindex")); + this.selected_item.attr("tabindex", -1); + } + this.container.addClass("chzn-container-active"); + this.active_field = true; + this.search_field.val(this.search_field.val()); + return this.search_field.focus(); + }; + + Chosen.prototype.test_active_click = function(evt) { + if ($(evt.target).parents('#' + this.container_id).length) { + return this.active_field = true; + } else { + return this.close_field(); + } + }; + + Chosen.prototype.results_build = function() { + var content, data, _i, _len, _ref; + this.parsing = true; + this.results_data = root.SelectParser.select_to_array(this.form_field); + if (this.is_multiple && this.choices > 0) { + this.search_choices.find("li.search-choice").remove(); + this.choices = 0; + } else if (!this.is_multiple) { + this.selected_item.addClass("chzn-default").find("span").text(this.default_text); + if (this.form_field.options.length <= this.disable_search_threshold) { + this.container.addClass("chzn-container-single-nosearch"); + } else { + this.container.removeClass("chzn-container-single-nosearch"); + } + } + content = ''; + _ref = this.results_data; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + data = _ref[_i]; + if (data.group) { + content += this.result_add_group(data); + } else if (!data.empty) { + content += this.result_add_option(data); + if (data.selected && this.is_multiple) { + this.choice_build(data); + } else if (data.selected && !this.is_multiple) { + this.selected_item.removeClass("chzn-default").find("span").text(data.text); + if (this.allow_single_deselect) this.single_deselect_control_build(); + } + } + } + this.search_field_disabled(); + this.show_search_field_default(); + this.search_field_scale(); + this.search_results.html(content); + return this.parsing = false; + }; + + Chosen.prototype.result_add_group = function(group) { + if (!group.disabled) { + group.dom_id = this.container_id + "_g_" + group.array_index; + return '
  • ' + $("
    ").text(group.label).html() + '
  • '; + } else { + return ""; + } + }; + + Chosen.prototype.result_do_highlight = function(el) { + var high_bottom, high_top, maxHeight, visible_bottom, visible_top; + if (el.length) { + this.result_clear_highlight(); + this.result_highlight = el; + this.result_highlight.addClass("highlighted"); + maxHeight = parseInt(this.search_results.css("maxHeight"), 10); + visible_top = this.search_results.scrollTop(); + visible_bottom = maxHeight + visible_top; + high_top = this.result_highlight.position().top + this.search_results.scrollTop(); + high_bottom = high_top + this.result_highlight.outerHeight(); + if (high_bottom >= visible_bottom) { + return this.search_results.scrollTop((high_bottom - maxHeight) > 0 ? high_bottom - maxHeight : 0); + } else if (high_top < visible_top) { + return this.search_results.scrollTop(high_top); + } + } + }; + + Chosen.prototype.result_clear_highlight = function() { + if (this.result_highlight) this.result_highlight.removeClass("highlighted"); + return this.result_highlight = null; + }; + + Chosen.prototype.results_show = function() { + var dd_top; + if (!this.is_multiple) { + this.selected_item.addClass("chzn-single-with-drop"); + if (this.result_single_selected) { + this.result_do_highlight(this.result_single_selected); + } + } else if (this.max_selected_options <= this.choices) { + this.form_field_jq.trigger("liszt:maxselected", { + chosen: this + }); + return false; + } + dd_top = this.is_multiple ? this.container.height() : this.container.height() - 1; + this.form_field_jq.trigger("liszt:showing_dropdown", { + chosen: this + }); + this.dropdown.css({ + "top": dd_top + "px", + "left": 0 + }); + this.results_showing = true; + this.search_field.focus(); + this.search_field.val(this.search_field.val()); + return this.winnow_results(); + }; + + Chosen.prototype.results_hide = function() { + if (!this.is_multiple) { + this.selected_item.removeClass("chzn-single-with-drop"); + } + this.result_clear_highlight(); + this.form_field_jq.trigger("liszt:hiding_dropdown", { + chosen: this + }); + this.dropdown.css({ + "left": "-9000px" + }); + return this.results_showing = false; + }; + + Chosen.prototype.set_tab_index = function(el) { + var ti; + if (this.form_field_jq.attr("tabindex")) { + ti = this.form_field_jq.attr("tabindex"); + this.form_field_jq.attr("tabindex", -1); + if (this.is_multiple) { + return this.search_field.attr("tabindex", ti); + } else { + this.selected_item.attr("tabindex", ti); + return this.search_field.attr("tabindex", -1); + } + } + }; + + Chosen.prototype.show_search_field_default = function() { + if (this.is_multiple && this.choices < 1 && !this.active_field) { + this.search_field.val(this.default_text); + return this.search_field.addClass("default"); + } else { + this.search_field.val(""); + return this.search_field.removeClass("default"); + } + }; + + Chosen.prototype.search_results_mouseup = function(evt) { + var target; + target = $(evt.target).hasClass("active-result") ? $(evt.target) : $(evt.target).parents(".active-result").first(); + if (target.length) { + this.result_highlight = target; + return this.result_select(evt); + } + }; + + Chosen.prototype.search_results_mouseover = function(evt) { + var target; + target = $(evt.target).hasClass("active-result") ? $(evt.target) : $(evt.target).parents(".active-result").first(); + if (target) return this.result_do_highlight(target); + }; + + Chosen.prototype.search_results_mouseout = function(evt) { + if ($(evt.target).hasClass("active-result" || $(evt.target).parents('.active-result').first())) { + return this.result_clear_highlight(); + } + }; + + Chosen.prototype.choices_click = function(evt) { + evt.preventDefault(); + if (this.active_field && !($(evt.target).hasClass("search-choice" || $(evt.target).parents('.search-choice').first)) && !this.results_showing) { + return this.results_show(); + } + }; + + Chosen.prototype.choice_build = function(item) { + var choice_id, link, + _this = this; + if (this.is_multiple && this.max_selected_options <= this.choices) { + this.form_field_jq.trigger("liszt:maxselected", { + chosen: this + }); + return false; + } + choice_id = this.container_id + "_c_" + item.array_index; + this.choices += 1; + this.search_container.before('
  • ' + item.html + '
  • '); + link = $('#' + choice_id).find("a").first(); + return link.click(function(evt) { + return _this.choice_destroy_link_click(evt); + }); + }; + + Chosen.prototype.choice_destroy_link_click = function(evt) { + evt.preventDefault(); + if (!this.is_disabled) { + this.pending_destroy_click = true; + return this.choice_destroy($(evt.target)); + } else { + return evt.stopPropagation; + } + }; + + Chosen.prototype.choice_destroy = function(link) { + this.choices -= 1; + this.show_search_field_default(); + if (this.is_multiple && this.choices > 0 && this.search_field.val().length < 1) { + this.results_hide(); + } + this.result_deselect(link.attr("rel")); + return link.parents('li').first().remove(); + }; + + Chosen.prototype.results_reset = function() { + this.form_field.options[0].selected = true; + this.selected_item.find("span").text(this.default_text); + if (!this.is_multiple) this.selected_item.addClass("chzn-default"); + this.show_search_field_default(); + this.results_reset_cleanup(); + this.current_value = ''; + this.form_field_jq.trigger("change"); + if (this.active_field) return this.results_hide(); + }; + + Chosen.prototype.results_reset_cleanup = function() { + return this.selected_item.find("abbr").remove(); + }; + + Chosen.prototype.result_select = function(evt) { + var high, high_id, item, position; + if (this.result_highlight) { + high = this.result_highlight; + high_id = high.attr("id"); + this.result_clear_highlight(); + if (this.is_multiple) { + this.result_deactivate(high); + } else { + this.search_results.find(".result-selected").removeClass("result-selected"); + this.result_single_selected = high; + this.selected_item.removeClass("chzn-default"); + } + high.addClass("result-selected"); + position = high_id.substr(high_id.lastIndexOf("_") + 1); + item = this.results_data[position]; + item.selected = true; + this.form_field.options[item.options_index].selected = true; + if (this.is_multiple) { + this.choice_build(item); + } else { + this.selected_item.find("span").first().text(item.text); + if (this.allow_single_deselect) this.single_deselect_control_build(); + } + if (!(evt.metaKey && this.is_multiple)) this.results_hide(); + this.search_field.val(""); + if (this.is_multiple || this.form_field_jq.val() !== this.current_value) { + this.form_field_jq.trigger("change", { + 'selected': this.form_field.options[item.options_index].value + }); + } + this.current_value = this.form_field_jq.val(); + return this.search_field_scale(); + } + }; + + Chosen.prototype.result_activate = function(el) { + return el.addClass("active-result"); + }; + + Chosen.prototype.result_deactivate = function(el) { + return el.removeClass("active-result"); + }; + + Chosen.prototype.result_deselect = function(pos) { + var result, result_data; + result_data = this.results_data[pos]; + result_data.selected = false; + this.form_field.options[result_data.options_index].selected = false; + result = $("#" + this.container_id + "_o_" + pos); + result.removeClass("result-selected").addClass("active-result").show(); + this.result_clear_highlight(); + this.winnow_results(); + this.form_field_jq.trigger("change", { + deselected: this.form_field.options[result_data.options_index].value + }); + return this.search_field_scale(); + }; + + Chosen.prototype.single_deselect_control_build = function() { + if (this.allow_single_deselect && this.selected_item.find("abbr").length < 1) { + return this.selected_item.find("span").first().after(""); + } + }; + + Chosen.prototype.winnow_results = function() { + var found, option, part, parts, regex, regexAnchor, result, result_id, results, searchText, startpos, text, zregex, _i, _j, _len, _len2, _ref; + this.no_results_clear(); + results = 0; + searchText = this.search_field.val() === this.default_text ? "" : $('
    ').text($.trim(this.search_field.val())).html(); + regexAnchor = this.search_contains ? "" : "^"; + regex = new RegExp(regexAnchor + searchText.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), 'i'); + zregex = new RegExp(searchText.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), 'i'); + _ref = this.results_data; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + option = _ref[_i]; + if (!option.disabled && !option.empty) { + if (option.group) { + $('#' + option.dom_id).css('display', 'none'); + } else if (!(this.is_multiple && option.selected)) { + found = false; + result_id = option.dom_id; + result = $("#" + result_id); + if (regex.test(option.html)) { + found = true; + results += 1; + } else if (option.html.indexOf(" ") >= 0 || option.html.indexOf("[") === 0) { + parts = option.html.replace(/\[|\]/g, "").split(" "); + if (parts.length) { + for (_j = 0, _len2 = parts.length; _j < _len2; _j++) { + part = parts[_j]; + if (regex.test(part)) { + found = true; + results += 1; + } + } + } + } + if (found) { + if (searchText.length) { + startpos = option.html.search(zregex); + text = option.html.substr(0, startpos + searchText.length) + '' + option.html.substr(startpos + searchText.length); + text = text.substr(0, startpos) + '' + text.substr(startpos); + } else { + text = option.html; + } + result.html(text); + this.result_activate(result); + if (option.group_array_index != null) { + $("#" + this.results_data[option.group_array_index].dom_id).css('display', 'list-item'); + } + } else { + if (this.result_highlight && result_id === this.result_highlight.attr('id')) { + this.result_clear_highlight(); + } + this.result_deactivate(result); + } + } + } + } + if (results < 1 && searchText.length) { + return this.no_results(searchText); + } else { + return this.winnow_results_set_highlight(); + } + }; + + Chosen.prototype.winnow_results_clear = function() { + var li, lis, _i, _len, _results; + this.search_field.val(""); + lis = this.search_results.find("li"); + _results = []; + for (_i = 0, _len = lis.length; _i < _len; _i++) { + li = lis[_i]; + li = $(li); + if (li.hasClass("group-result")) { + _results.push(li.css('display', 'auto')); + } else if (!this.is_multiple || !li.hasClass("result-selected")) { + _results.push(this.result_activate(li)); + } else { + _results.push(void 0); + } + } + return _results; + }; + + Chosen.prototype.winnow_results_set_highlight = function() { + var do_high, selected_results; + if (!this.result_highlight) { + selected_results = !this.is_multiple ? this.search_results.find(".result-selected.active-result") : []; + do_high = selected_results.length ? selected_results.first() : this.search_results.find(".active-result").first(); + if (do_high != null) return this.result_do_highlight(do_high); + } + }; + + Chosen.prototype.no_results = function(terms) { + var no_results_html; + no_results_html = $('
  • ' + this.results_none_found + ' ""
  • '); + no_results_html.find("span").first().html(terms); + return this.search_results.append(no_results_html); + }; + + Chosen.prototype.no_results_clear = function() { + return this.search_results.find(".no-results").remove(); + }; + + Chosen.prototype.keydown_arrow = function() { + var first_active, next_sib; + if (!this.result_highlight) { + first_active = this.search_results.find("li.active-result").first(); + if (first_active) this.result_do_highlight($(first_active)); + } else if (this.results_showing) { + next_sib = this.result_highlight.nextAll("li.active-result").first(); + if (next_sib) this.result_do_highlight(next_sib); + } + if (!this.results_showing) return this.results_show(); + }; + + Chosen.prototype.keyup_arrow = function() { + var prev_sibs; + if (!this.results_showing && !this.is_multiple) { + return this.results_show(); + } else if (this.result_highlight) { + prev_sibs = this.result_highlight.prevAll("li.active-result"); + if (prev_sibs.length) { + return this.result_do_highlight(prev_sibs.first()); + } else { + if (this.choices > 0) this.results_hide(); + return this.result_clear_highlight(); + } + } + }; + + Chosen.prototype.keydown_backstroke = function() { + if (this.pending_backstroke) { + this.choice_destroy(this.pending_backstroke.find("a").first()); + return this.clear_backstroke(); + } else { + this.pending_backstroke = this.search_container.siblings("li.search-choice").last(); + if (this.single_backstroke_delete) { + return this.keydown_backstroke(); + } else { + return this.pending_backstroke.addClass("search-choice-focus"); + } + } + }; + + Chosen.prototype.clear_backstroke = function() { + if (this.pending_backstroke) { + this.pending_backstroke.removeClass("search-choice-focus"); + } + return this.pending_backstroke = null; + }; + + Chosen.prototype.keydown_checker = function(evt) { + var stroke, _ref; + stroke = (_ref = evt.which) != null ? _ref : evt.keyCode; + this.search_field_scale(); + if (stroke !== 8 && this.pending_backstroke) this.clear_backstroke(); + switch (stroke) { + case 8: + this.backstroke_length = this.search_field.val().length; + break; + case 9: + if (this.results_showing && !this.is_multiple) this.result_select(evt); + this.mouse_on_container = false; + break; + case 13: + evt.preventDefault(); + break; + case 38: + evt.preventDefault(); + this.keyup_arrow(); + break; + case 40: + this.keydown_arrow(); + break; + } + }; + + Chosen.prototype.search_field_scale = function() { + var dd_top, div, h, style, style_block, styles, w, _i, _len; + if (this.is_multiple) { + h = 0; + w = 0; + style_block = "position:absolute; left: -1000px; top: -1000px; display:none;"; + styles = ['font-size', 'font-style', 'font-weight', 'font-family', 'line-height', 'text-transform', 'letter-spacing']; + for (_i = 0, _len = styles.length; _i < _len; _i++) { + style = styles[_i]; + style_block += style + ":" + this.search_field.css(style) + ";"; + } + div = $('
    ', { + 'style': style_block + }); + div.text(this.search_field.val()); + $('body').append(div); + w = div.width() + 25; + div.remove(); + if (w > this.f_width - 10) w = this.f_width - 10; + this.search_field.css({ + 'width': w + 'px' + }); + dd_top = this.container.height(); + return this.dropdown.css({ + "top": dd_top + "px" + }); + } + }; + + Chosen.prototype.generate_random_id = function() { + var string; + string = "sel" + this.generate_random_char() + this.generate_random_char() + this.generate_random_char(); + while ($("#" + string).length > 0) { + string += this.generate_random_char(); + } + return string; + }; + + return Chosen; + + })(AbstractChosen); + + get_side_border_padding = function(elmt) { + var side_border_padding; + return side_border_padding = elmt.outerWidth() - elmt.width(); + }; + + root.get_side_border_padding = get_side_border_padding; + +}).call(this); \ No newline at end of file diff --git a/js/chosen.jquery.min.js b/js/chosen.jquery.min.js new file mode 100644 index 0000000..022d662 --- /dev/null +++ b/js/chosen.jquery.min.js @@ -0,0 +1,10 @@ +// Chosen, a Select Box Enhancer for jQuery and Protoype +// by Patrick Filler for Harvest, http://getharvest.com +// +// Version 0.9.8 +// Full source at https://github.com/harvesthq/chosen +// Copyright (c) 2011 Harvest http://getharvest.com + +// MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md +// This file is generated by `cake build`, do not edit it by hand. +((function(){var a;a=function(){function a(){this.options_index=0,this.parsed=[]}return a.prototype.add_node=function(a){return a.nodeName==="OPTGROUP"?this.add_group(a):this.add_option(a)},a.prototype.add_group=function(a){var b,c,d,e,f,g;b=this.parsed.length,this.parsed.push({array_index:b,group:!0,label:a.label,children:0,disabled:a.disabled}),f=a.childNodes,g=[];for(d=0,e=f.length;d"+a.html+"")},a.prototype.results_update_field=function(){return this.is_multiple||this.results_reset_cleanup(),this.result_clear_highlight(),this.result_single_selected=null,this.results_build()},a.prototype.results_toggle=function(){return this.results_showing?this.results_hide():this.results_show()},a.prototype.results_search=function(a){return this.results_showing?this.winnow_results():this.results_show()},a.prototype.keyup_checker=function(a){var b,c;b=(c=a.which)!=null?c:a.keyCode,this.search_field_scale();switch(b){case 8:if(this.is_multiple&&this.backstroke_length<1&&this.choices>0)return this.keydown_backstroke();if(!this.pending_backstroke)return this.result_clear_highlight(),this.results_search();break;case 13:a.preventDefault();if(this.results_showing)return this.result_select(a);break;case 27:return this.results_showing&&this.results_hide(),!0;case 9:case 38:case 40:case 16:case 91:case 17:break;default:return this.results_search()}},a.prototype.generate_field_id=function(){var a;return a=this.generate_random_id(),this.form_field.id=a,a},a.prototype.generate_random_char=function(){var a,b,c;return a="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",c=Math.floor(Math.random()*a.length),b=a.substring(c,c+1)},a}(),b.AbstractChosen=a}.call(this),function(){var a,b,c,d,e=Object.prototype.hasOwnProperty,f=function(a,b){function d(){this.constructor=a}for(var c in b)e.call(b,c)&&(a[c]=b[c]);return d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype,a};d=this,a=jQuery,a.fn.extend({chosen:function(c){return!a.browser.msie||a.browser.version!=="6.0"&&a.browser.version!=="7.0"?this.each(function(d){var e;e=a(this);if(!e.hasClass("chzn-done"))return e.data("chosen",new b(this,c))}):this}}),b=function(b){function e(){e.__super__.constructor.apply(this,arguments)}return f(e,b),e.prototype.setup=function(){return this.form_field_jq=a(this.form_field),this.current_value=this.form_field_jq.val(),this.is_rtl=this.form_field_jq.hasClass("chzn-rtl")},e.prototype.finish_setup=function(){return this.form_field_jq.addClass("chzn-done")},e.prototype.set_up_html=function(){var b,d,e,f;return this.container_id=this.form_field.id.length?this.form_field.id.replace(/[^\w]/g,"_"):this.generate_field_id(),this.container_id+="_chzn",this.f_width=this.form_field_jq.outerWidth(),b=a("
    ",{id:this.container_id,"class":"chzn-container"+(this.is_rtl?" chzn-rtl":""),style:"width: "+this.f_width+"px;"}),this.is_multiple?b.html('
      '):b.html(''+this.default_text+'
        '),this.form_field_jq.hide().after(b),this.container=a("#"+this.container_id),this.container.addClass("chzn-container-"+(this.is_multiple?"multi":"single")),this.dropdown=this.container.find("div.chzn-drop").first(),d=this.container.height(),e=this.f_width-c(this.dropdown),this.dropdown.css({width:e+"px",top:d+"px"}),this.search_field=this.container.find("input").first(),this.search_results=this.container.find("ul.chzn-results").first(),this.search_field_scale(),this.search_no_results=this.container.find("li.no-results").first(),this.is_multiple?(this.search_choices=this.container.find("ul.chzn-choices").first(),this.search_container=this.container.find("li.search-field").first()):(this.search_container=this.container.find("div.chzn-search").first(),this.selected_item=this.container.find(".chzn-single").first(),f=e-c(this.search_container)-c(this.search_field),this.search_field.css({width:f+"px"})),this.results_build(),this.set_tab_index(),this.form_field_jq.trigger("liszt:ready",{chosen:this})},e.prototype.register_observers=function(){var a=this;return this.container.mousedown(function(b){return a.container_mousedown(b)}),this.container.mouseup(function(b){return a.container_mouseup(b)}),this.container.mouseenter(function(b){return a.mouse_enter(b)}),this.container.mouseleave(function(b){return a.mouse_leave(b)}),this.search_results.mouseup(function(b){return a.search_results_mouseup(b)}),this.search_results.mouseover(function(b){return a.search_results_mouseover(b)}),this.search_results.mouseout(function(b){return a.search_results_mouseout(b)}),this.form_field_jq.bind("liszt:updated",function(b){return a.results_update_field(b)}),this.search_field.blur(function(b){return a.input_blur(b)}),this.search_field.keyup(function(b){return a.keyup_checker(b)}),this.search_field.keydown(function(b){return a.keydown_checker(b)}),this.is_multiple?(this.search_choices.click(function(b){return a.choices_click(b)}),this.search_field.focus(function(b){return a.input_focus(b)})):this.container.click(function(a){return a.preventDefault()})},e.prototype.search_field_disabled=function(){this.is_disabled=this.form_field_jq[0].disabled;if(this.is_disabled)return this.container.addClass("chzn-disabled"),this.search_field[0].disabled=!0,this.is_multiple||this.selected_item.unbind("focus",this.activate_action),this.close_field();this.container.removeClass("chzn-disabled"),this.search_field[0].disabled=!1;if(!this.is_multiple)return this.selected_item.bind("focus",this.activate_action)},e.prototype.container_mousedown=function(b){var c;if(!this.is_disabled)return c=b!=null?a(b.target).hasClass("search-choice-close"):!1,b&&b.type==="mousedown"&&!this.results_showing&&b.stopPropagation(),!this.pending_destroy_click&&!c?(this.active_field?!this.is_multiple&&b&&(a(b.target)[0]===this.selected_item[0]||a(b.target).parents("a.chzn-single").length)&&(b.preventDefault(),this.results_toggle()):(this.is_multiple&&this.search_field.val(""),a(document).click(this.click_test_action),this.results_show()),this.activate_field()):this.pending_destroy_click=!1},e.prototype.container_mouseup=function(a){if(a.target.nodeName==="ABBR"&&!this.is_disabled)return this.results_reset(a)},e.prototype.blur_test=function(a){if(!this.active_field&&this.container.hasClass("chzn-container-active"))return this.close_field()},e.prototype.close_field=function(){return a(document).unbind("click",this.click_test_action),this.is_multiple||(this.selected_item.attr("tabindex",this.search_field.attr("tabindex")),this.search_field.attr("tabindex",-1)),this.active_field=!1,this.results_hide(),this.container.removeClass("chzn-container-active"),this.winnow_results_clear(),this.clear_backstroke(),this.show_search_field_default(),this.search_field_scale()},e.prototype.activate_field=function(){return!this.is_multiple&&!this.active_field&&(this.search_field.attr("tabindex",this.selected_item.attr("tabindex")),this.selected_item.attr("tabindex",-1)),this.container.addClass("chzn-container-active"),this.active_field=!0,this.search_field.val(this.search_field.val()),this.search_field.focus()},e.prototype.test_active_click=function(b){return a(b.target).parents("#"+this.container_id).length?this.active_field=!0:this.close_field()},e.prototype.results_build=function(){var a,b,c,e,f;this.parsing=!0,this.results_data=d.SelectParser.select_to_array(this.form_field),this.is_multiple&&this.choices>0?(this.search_choices.find("li.search-choice").remove(),this.choices=0):this.is_multiple||(this.selected_item.addClass("chzn-default").find("span").text(this.default_text),this.form_field.options.length<=this.disable_search_threshold?this.container.addClass("chzn-container-single-nosearch"):this.container.removeClass("chzn-container-single-nosearch")),a="",f=this.results_data;for(c=0,e=f.length;c'+a("
        ").text(b.label).html()+"")},e.prototype.result_do_highlight=function(a){var b,c,d,e,f;if(a.length){this.result_clear_highlight(),this.result_highlight=a,this.result_highlight.addClass("highlighted"),d=parseInt(this.search_results.css("maxHeight"),10),f=this.search_results.scrollTop(),e=d+f,c=this.result_highlight.position().top+this.search_results.scrollTop(),b=c+this.result_highlight.outerHeight();if(b>=e)return this.search_results.scrollTop(b-d>0?b-d:0);if(c'+b.html+''),d=a("#"+c).find("a").first(),d.click(function(a){return e.choice_destroy_link_click(a)}))},e.prototype.choice_destroy_link_click=function(b){return b.preventDefault(),this.is_disabled?b.stopPropagation:(this.pending_destroy_click=!0,this.choice_destroy(a(b.target)))},e.prototype.choice_destroy=function(a){return this.choices-=1,this.show_search_field_default(),this.is_multiple&&this.choices>0&&this.search_field.val().length<1&&this.results_hide(),this.result_deselect(a.attr("rel")),a.parents("li").first().remove()},e.prototype.results_reset=function(){this.form_field.options[0].selected=!0,this.selected_item.find("span").text(this.default_text),this.is_multiple||this.selected_item.addClass("chzn-default"),this.show_search_field_default(),this.results_reset_cleanup(),this.form_field_jq.trigger("change");if(this.active_field)return this.results_hide()},e.prototype.results_reset_cleanup=function(){return this.selected_item.find("abbr").remove()},e.prototype.result_select=function(a){var b,c,d,e;if(this.result_highlight)return b=this.result_highlight,c=b.attr("id"),this.result_clear_highlight(),this.is_multiple?this.result_deactivate(b):(this.search_results.find(".result-selected").removeClass("result-selected"),this.result_single_selected=b,this.selected_item.removeClass("chzn-default")),b.addClass("result-selected"),e=c.substr(c.lastIndexOf("_")+1),d=this.results_data[e],d.selected=!0,this.form_field.options[d.options_index].selected=!0,this.is_multiple?this.choice_build(d):(this.selected_item.find("span").first().text(d.text),this.allow_single_deselect&&this.single_deselect_control_build()),(!a.metaKey||!this.is_multiple)&&this.results_hide(),this.search_field.val(""),(this.is_multiple||this.form_field_jq.val()!==this.current_value)&&this.form_field_jq.trigger("change",{selected:this.form_field.options[d.options_index].value}),this.current_value=this.form_field_jq.val(),this.search_field_scale()},e.prototype.result_activate=function(a){return a.addClass("active-result")},e.prototype.result_deactivate=function(a){return a.removeClass("active-result")},e.prototype.result_deselect=function(b){var c,d;return d=this.results_data[b],d.selected=!1,this.form_field.options[d.options_index].selected=!1,c=a("#"+this.container_id+"_o_"+b),c.removeClass("result-selected").addClass("active-result").show(),this.result_clear_highlight(),this.winnow_results(),this.form_field_jq.trigger("change",{deselected:this.form_field.options[d.options_index].value}),this.search_field_scale()},e.prototype.single_deselect_control_build=function(){if(this.allow_single_deselect&&this.selected_item.find("abbr").length<1)return this.selected_item.find("span").first().after('')},e.prototype.winnow_results=function(){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s;this.no_results_clear(),j=0,k=this.search_field.val()===this.default_text?"":a("
        ").text(a.trim(this.search_field.val())).html(),g=this.search_contains?"":"^",f=new RegExp(g+k.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&"),"i"),n=new RegExp(k.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&"),"i"),s=this.results_data;for(o=0,q=s.length;o=0||c.html.indexOf("[")===0){e=c.html.replace(/\[|\]/g,"").split(" ");if(e.length)for(p=0,r=e.length;p"+c.html.substr(l+k.length),m=m.substr(0,l)+""+m.substr(l)):m=c.html,h.html(m),this.result_activate(h),c.group_array_index!=null&&a("#"+this.results_data[c.group_array_index].dom_id).css("display","list-item")):(this.result_highlight&&i===this.result_highlight.attr("id")&&this.result_clear_highlight(),this.result_deactivate(h))}}return j<1&&k.length?this.no_results(k):this.winnow_results_set_highlight()},e.prototype.winnow_results_clear=function(){var b,c,d,e,f;this.search_field.val(""),c=this.search_results.find("li"),f=[];for(d=0,e=c.length;d'+this.results_none_found+' ""'),c.find("span").first().html(b),this.search_results.append(c)},e.prototype.no_results_clear=function(){return this.search_results.find(".no-results").remove()},e.prototype.keydown_arrow=function(){var b,c;this.result_highlight?this.results_showing&&(c=this.result_highlight.nextAll("li.active-result").first(),c&&this.result_do_highlight(c)):(b=this.search_results.find("li.active-result").first(),b&&this.result_do_highlight(a(b)));if(!this.results_showing)return this.results_show()},e.prototype.keyup_arrow=function(){var a;if(!this.results_showing&&!this.is_multiple)return this.results_show();if(this.result_highlight)return a=this.result_highlight.prevAll("li.active-result"),a.length?this.result_do_highlight(a.first()):(this.choices>0&&this.results_hide(),this.result_clear_highlight())},e.prototype.keydown_backstroke=function(){return this.pending_backstroke?(this.choice_destroy(this.pending_backstroke.find("a").first()),this.clear_backstroke()):(this.pending_backstroke=this.search_container.siblings("li.search-choice").last(),this.single_backstroke_delete?this.keydown_backstroke():this.pending_backstroke.addClass("search-choice-focus"))},e.prototype.clear_backstroke=function(){return this.pending_backstroke&&this.pending_backstroke.removeClass("search-choice-focus"),this.pending_backstroke=null},e.prototype.keydown_checker=function(a){var b,c;b=(c=a.which)!=null?c:a.keyCode,this.search_field_scale(),b!==8&&this.pending_backstroke&&this.clear_backstroke();switch(b){case 8:this.backstroke_length=this.search_field.val().length;break;case 9:this.results_showing&&!this.is_multiple&&this.result_select(a),this.mouse_on_container=!1;break;case 13:a.preventDefault();break;case 38:a.preventDefault(),this.keyup_arrow();break;case 40:this.keydown_arrow()}},e.prototype.search_field_scale=function(){var b,c,d,e,f,g,h,i,j;if(this.is_multiple){d=0,h=0,f="position:absolute; left: -1000px; top: -1000px; display:none;",g=["font-size","font-style","font-weight","font-family","line-height","text-transform","letter-spacing"];for(i=0,j=g.length;i",{style:f}),c.text(this.search_field.val()),a("body").append(c),h=c.width()+25,c.remove(),h>this.f_width-10&&(h=this.f_width-10),this.search_field.css({width:h+"px"}),b=this.container.height(),this.dropdown.css({top:b+"px"})}},e.prototype.generate_random_id=function(){var b;b="sel"+this.generate_random_char()+this.generate_random_char()+this.generate_random_char();while(a("#"+b).length>0)b+=this.generate_random_char();return b},e}(AbstractChosen),c=function(a){var b;return b=a.outerWidth()-a.width()},d.get_side_border_padding=c}.call(this); \ No newline at end of file diff --git a/js/easey.handlers.js b/js/easey.handlers.js new file mode 100644 index 0000000..2976506 --- /dev/null +++ b/js/easey.handlers.js @@ -0,0 +1,462 @@ +(function(context, MM) { + + easey.TouchHandler = function() { + var handler = {}; + + handler.init = function(map) { + var prevT = 0, + acceleration = 25.0, + speed = null, + maxTapTime = 250, + maxTapDistance = 30, + maxDoubleTapDelay = 350, + drag = 0.10, + locations = {}, + taps = [], + wasPinching = false, + nowPoint = null, + oldPoint = null, + lastMove = null, + lastPinchCenter = null; + + function animate(t) { + var dir = { x: 0, y: 0 }; + var dt = Math.max(0.001,(t - prevT) / 1000.0); + if (nowPoint && oldPoint && + (lastMove > (+new Date() - 50))) { + dir.x = nowPoint.x - oldPoint.x; + dir.y = nowPoint.y - oldPoint.y; + speed.x = dir.x; + speed.y = dir.y; + } else { + speed.x -= speed.x * drag; + speed.y -= speed.y * drag; + if (Math.abs(speed.x) < 0.001) { + speed.x = 0; + } + if (Math.abs(speed.y) < 0.001) { + speed.y = 0; + } + } + if (speed.x || speed.y) { + map.panBy(speed.x, speed.y); + } + prevT = t; + // tick every frame for time-based anim accuracy + MM.getFrame(animate); + } + + + // Test whether touches are from the same source - + // whether this is the same touchmove event. + function sameTouch (event, touch) { + return (event && event.touch) && + (touch.identifier == event.touch.identifier); + } + + function updateTouches (e) { + for (var i = 0; i < e.touches.length; i += 1) { + var t = e.touches[i]; + if (t.identifier in locations) { + var l = locations[t.identifier]; + l.x = t.screenX; + l.y = t.screenY; + l.scale = e.scale; + } + else { + locations[t.identifier] = { + scale: e.scale, + startPos: { x: t.screenX, y: t.screenY }, + x: t.screenX, + y: t.screenY, + time: new Date().getTime() + }; + } + } + } + + function touchStartMachine(e) { + updateTouches(e); + return MM.cancelEvent(e); + } + + function touchMoveMachine(e) { + switch (e.touches.length) { + case 1: + onPanning(e.touches[0]); + break; + case 2: + onPinching(e); + break; + } + updateTouches(e); + return MM.cancelEvent(e); + } + + // Fail early if this isn't a touch device. + // TODO: move to add fn + if (!isTouchable()) return false; + + MM.addEvent(map.parent, 'touchstart', + touchStartMachine); + MM.addEvent(map.parent, 'touchmove', + touchMoveMachine); + MM.addEvent(map.parent, 'touchend', + touchEndMachine); + + prevT = new Date().getTime(); + speed = { x: 0, y: 0 }; + MM.getFrame(animate); + + // Handle a tap event - mainly watch for a doubleTap + function onTap(tap) { + if (taps.length && + (tap.time - taps[0].time) < maxDoubleTapDelay) { + onDoubleTap(tap); + taps = []; + return; + } + taps = [tap]; + } + + // Handle a double tap by zooming in a single zoom level to a + // round zoom. + function onDoubleTap(tap) { + // zoom in to a round number + easey().map(map) + .to(map.pointCoordinate(tap).zoomTo(map.getZoom() + 1)) + .path('about').run(200, function() { + map.dispatchCallback('zoomed'); + }); + } + + function isTouchable () { + var el = document.createElement('div'); + el.setAttribute('ongesturestart', 'return;'); + return (typeof el.ongesturestart === 'function'); + } + + function remove() { + // Fail early if this isn't a touch device. + if (!isTouchable()) return false; + + MM.removeEvent(map.parent, 'touchstart', + touchStartMachine); + MM.removeEvent(map.parent, 'touchmove', + touchMoveMachine); + MM.removeEvent(map.parent, 'touchend', + touchEndMachine); + } + + // Re-transform the actual map parent's CSS transformation + function onPanning(touch) { + lastMove = +new Date(); + oldPoint = nowPoint; + nowPoint = { x: touch.screenX, y: touch.screenY }; + // oldPoint = locations[touch.identifier]; + } + + function onPinching(e) { + // use the first two touches and their previous positions + var t0 = e.touches[0], + t1 = e.touches[1], + p0 = new MM.Point(t0.screenX, t0.screenY), + p1 = new MM.Point(t1.screenX, t1.screenY), + l0 = locations[t0.identifier], + l1 = locations[t1.identifier]; + + // mark these touches so they aren't used as taps/holds + l0.wasPinch = true; + l1.wasPinch = true; + + // scale about the center of these touches + var center = MM.Point.interpolate(p0, p1, 0.5); + + map.zoomByAbout( + Math.log(e.scale) / Math.LN2 - + Math.log(l0.scale) / Math.LN2, + center); + + // pan from the previous center of these touches + var prevCenter = MM.Point.interpolate(l0, l1, 0.5); + + map.panBy(center.x - prevCenter.x, + center.y - prevCenter.y); + wasPinching = true; + lastPinchCenter = center; + } + + // When a pinch event ends, round the zoom of the map. + function onPinched(p) { + // TODO: easing + if (true) { + var z = map.getZoom(), // current zoom + tz = Math.round(z); // target zoom + map.zoomByAbout(tz - z, p); + } + wasPinching = false; + } + + function touchEndMachine(e) { + var now = new Date().getTime(); + // round zoom if we're done pinching + if (e.touches.length === 0 && wasPinching) { + onPinched(lastPinchCenter); + } + + oldPoint = nowPoint = null; + + // Look at each changed touch in turn. + for (var i = 0; i < e.changedTouches.length; i += 1) { + var t = e.changedTouches[i], + loc = locations[t.identifier]; + // if we didn't see this one (bug?) + // or if it was consumed by pinching already + // just skip to the next one + if (!loc || loc.wasPinch) { + continue; + } + + // we now know we have an event object and a + // matching touch that's just ended. Let's see + // what kind of event it is based on how long it + // lasted and how far it moved. + var pos = { x: t.screenX, y: t.screenY }, + time = now - loc.time, + travel = MM.Point.distance(pos, loc.startPos); + if (travel > maxTapDistance) { + // we will to assume that the drag has been handled separately + } else if (time > maxTapTime) { + // close in space, but not in time: a hold + pos.end = now; + pos.duration = time; + onHold(pos); + } else { + // close in both time and space: a tap + pos.time = now; + onTap(pos); + } + } + + // Weird, sometimes an end event doesn't get thrown + // for a touch that nevertheless has disappeared. + // Still, this will eventually catch those ids: + + var validTouchIds = {}; + for (var j = 0; j < e.touches.length; j++) { + validTouchIds[e.touches[j].identifier] = true; + } + for (var id in locations) { + if (!(id in validTouchIds)) { + delete validTouchIds[id]; + } + } + + return MM.cancelEvent(e); + } + }; + + return handler; + }; + + easey.DoubleClickHandler = function() { + var handler = {}, + map; + + function doubleClick(e) { + // Ensure that this handler is attached once. + // Get the point on the map that was double-clicked + var point = MM.getMousePoint(e, map); + z = map.getZoom() + (e.shiftKey ? -1 : 1); + // use shift-double-click to zoom out + easey().map(map) + .to(map.pointCoordinate(MM.getMousePoint(e, map)).zoomTo(z)) + .path('about').run(100, function() { + map.dispatchCallback('zoomed'); + }); + return MM.cancelEvent(e); + } + + handler.init = function(x) { + map = x; + MM.addEvent(map.parent, 'dblclick', doubleClick); + return handler; + }; + + handler.remove = function() { + MM.removeEvent(map.parent, 'dblclick', doubleClick); + }; + + return handler; + }; + + easey.MouseWheelHandler = function() { + var handler = {}, + map, + _zoomDiv, + ea = easey(), + prevTime, + precise = false; + + function mouseWheel(e) { + var delta = 0; + prevTime = prevTime || new Date().getTime(); + + try { + _zoomDiv.scrollTop = 1000; + _zoomDiv.dispatchEvent(e); + delta = 1000 - _zoomDiv.scrollTop; + } catch (error) { + delta = e.wheelDelta || (-e.detail * 5); + } + + // limit mousewheeling to once every 200ms + var timeSince = new Date().getTime() - prevTime; + var point = MM.getMousePoint(e, map); + + function dispatchZoomed() { + map.dispatchCallback('zoomed'); + } + + if (!ea.running()) { + var point = MM.getMousePoint(e, map), + z = map.getZoom(); + ea.map(map) + .easing('easeOut') + .to(map.pointCoordinate(MM.getMousePoint(e, map)).zoomTo(z + (delta > 0 ? 1 : -1))) + .path('about').run(400, dispatchZoomed); + prevTime = new Date().getTime(); + } else if (timeSince > 150){ + ea.zoom(ea.to().zoom + (delta > 0 ? 1 : -1)).from(map.coordinate).resetRun(); + prevTime = new Date().getTime(); + } + + // Cancel the event so that the page doesn't scroll + return MM.cancelEvent(e); + } + + handler.init = function(x) { + map = x; + _zoomDiv = document.body.appendChild(document.createElement('div')); + _zoomDiv.style.cssText = 'visibility:hidden;top:0;height:0;width:0;overflow-y:scroll'; + var innerDiv = _zoomDiv.appendChild(document.createElement('div')); + innerDiv.style.height = '2000px'; + MM.addEvent(map.parent, 'mousewheel', mouseWheel); + return handler; + }; + + handler.precise = function(x) { + if (!arguments.length) return precise; + precise = x; + return handler; + }; + + handler.remove = function() { + MM.removeEvent(map.parent, 'mousewheel', mouseWheel); + _zoomDiv.parentNode.removeChild(_zoomDiv); + }; + + return handler; + }; + + easey.DragHandler = function() { + var handler = {}, + map; + + handler.init = function(map) { + var prevT = 0, + speed = null, + drag = 0.15, + mouseDownPoint = null, + mouseDownTime = 0, + mousePoint = null, + prevMousePoint = null, + moveTime = null, + prevMoveTime = null, + animatedLastPoint = true; + + function mouseDown(e) { + if (e.shiftKey || e.button == 2) return; + MM.addEvent(document, 'mousemove', mouseMove); + MM.addEvent(document, 'mouseup', mouseUp); + mousePoint = prevMousePoint = MM.getMousePoint(e, map); + moveTime = prevMoveTime = +new Date(); + map.parent.style.cursor = 'move'; + return MM.cancelEvent(e); + } + + function mouseMove(e) { + if (mousePoint) { + if (animatedLastPoint) { + prevMousePoint = mousePoint; + prevMoveTime = moveTime; + animatedLastPoint = false; + } + mousePoint = MM.getMousePoint(e, map); + moveTime = +new Date(); + return MM.cancelEvent(e); + } + } + + function mouseUp(e) { + MM.removeEvent(document, 'mousemove', mouseMove); + MM.removeEvent(document, 'mouseup', mouseUp); + if (+new Date() - prevMoveTime < 50) { + dt = Math.max(1, moveTime - prevMoveTime); + var dir = { x: 0, y: 0 }; + dir.x = mousePoint.x - prevMousePoint.x; + dir.y = mousePoint.y - prevMousePoint.y; + speed.x = dir.x / dt; + speed.y = dir.y / dt; + } else { + speed.x = 0; + speed.y = 0; + } + mousePoint = prevMousePoint = null; + moveTime = lastMoveTime = null; + map.parent.style.cursor = ''; + return MM.cancelEvent(e); + } + + function animate(t) { + var dir = { x: 0, y: 0 }; + var dt = Math.max(1, t - prevT); + if (mousePoint && prevMousePoint) { + if (!animatedLastPoint) { + dir.x = mousePoint.x - prevMousePoint.x; + dir.y = mousePoint.y - prevMousePoint.y; + map.panBy(dir.x, dir.y); + animatedLastPoint = true; + } + } else { + // Rough time based animation accuracy + // using a linear approximation approach + speed.x *= Math.pow(1 - drag, dt * 60 / 1000); + speed.y *= Math.pow(1 - drag, dt * 60 / 1000); + if (Math.abs(speed.x) < 0.001) { + speed.x = 0; + } + if (Math.abs(speed.y) < 0.001) { + speed.y = 0; + } + if (speed.x || speed.y) { + map.panBy(speed.x * dt, speed.y * dt); + } + } + prevT = t; + MM.getFrame(animate); + } + + MM.addEvent(map.parent, 'click', function(e) { + map.parent.focus(); + }); + MM.addEvent(map.parent, 'mousedown', mouseDown); + prevT = new Date().getTime(); + speed = { x: 0, y: 0 }; + MM.getFrame(animate); + }; + + return handler; + }; + +})(this, MM); \ No newline at end of file diff --git a/js/easey.js b/js/easey.js new file mode 100644 index 0000000..c573e5c --- /dev/null +++ b/js/easey.js @@ -0,0 +1,261 @@ +(function(context, MM) { + + var easey = function() { + var easey = {}, + running = false, + abort = false; // killswitch for transitions + + var easings = { + easeIn: function(t) { return t * t; }, + easeOut: function(t) { return Math.sin(t * Math.PI / 2); }, + easeInOut: function(t) { return (1 - Math.cos(Math.PI * t)) / 2; }, + linear: function(t) { return t; } + }; + var easing = easings.easeOut; + + // to is the singular coordinate that any transition is based off + // three dimensions: + // + // * to + // * time + // * path + var from, to, map; + + easey.stop = function() { + abort = true; + }; + + easey.running = function() { + return running; + }; + + easey.point = function(x) { + to = map.pointCoordinate(x); + return easey; + }; + + easey.zoom = function(x) { + to = map.enforceZoomLimits(to.zoomTo(x)); + return easey; + }; + + easey.location = function(x) { + to = map.locationCoordinate(x); + return easey; + }; + + easey.from = function(x) { + if (!arguments.length) return from.copy(); + from = x.copy(); + return easey; + }; + + easey.to = function(x) { + if (!arguments.length) return to.copy(); + to = map.enforceZoomLimits(x.copy()); + return easey; + }; + + easey.path = function(x) { + path = paths[x]; + return easey; + }; + + easey.easing = function(x) { + easing = easings[x]; + return easey; + }; + + easey.map = function(x) { + if (!arguments.length) return map; + map = x; + from = map.coordinate.copy(); + to = map.coordinate.copy(); + return easey; + }; + + function interp(a, b, p) { + if (p === 0) return a; + if (p === 1) return b; + return a + ((b - a) * p); + } + + var paths = {}; + + // The screen path simply moves between + // coordinates in a non-geographical way + paths.screen = function(a, b, t) { + var zoom_lerp = interp(a.zoom, b.zoom, t); + var az = a.copy(); + var bz = b.copy().zoomTo(a.zoom); + return (new MM.Coordinate( + interp(az.row, bz.row, t), + interp(az.column, bz.column, t), + az.zoom)).zoomTo(zoom_lerp); + }; + + function ptWithCoords(a, b) { + // distance from the center of the map + var point = new MM.Point(map.dimensions.x / 2, map.dimensions.y / 2); + point.x += map.tileSize.x * (b.column - a.column); + point.y += map.tileSize.y * (b.row - a.row); + return point; + } + + // The screen path means that the b + // coordinate should maintain its point on screen + // throughout the transition, but the map + // should move to its zoom level + paths.about = function(a, b, t) { + var zoom_lerp = interp(a.zoom, b.zoom, t); + + var bs = b.copy().zoomTo(a.zoom); + var az = a.copy().zoomTo(zoom_lerp); + var bz = b.copy().zoomTo(zoom_lerp); + var start = ptWithCoords(a, bs); + var end = ptWithCoords(az, bz); + + az.column -= (start.x - end.x) / map.tileSize.x; + az.row -= (start.y - end.y) / map.tileSize.y; + + return az; + }; + + var path = paths.screen; + + easey.t = function(t) { + map.coordinate = path(from, to, easing(t)); + map.draw(); + return easey; + }; + + easey.future = function(parts) { + var futures = []; + for (var t = 0; t < parts; t++) { + futures.push(path(from, to, t / (parts - 1))); + } + return futures; + }; + + var start; + easey.resetRun = function () { + start = (+ new Date()); + return easey; + }; + + easey.run = function(time, callback) { + + time = time || 1000; + + start = (+new Date()); + + running = true; + + function tick() { + var delta = (+new Date()) - start; + if (abort) { + return void (abort = running = false); + } else if (delta > time) { + running = false; + map.coordinate = path(from, to, 1); + map.draw(); + if (callback) return callback(map); + } else { + map.coordinate = path(from, to, easing(delta / time)); + map.draw(); + MM.getFrame(tick); + } + } + + MM.getFrame(tick); + }; + + // Optimally smooth (constant perceived velocity) and + // efficient (minimal path distance) zooming and panning. + // + // Based on "Smooth and efficient zooming and panning" + // by Jarke J. van Wijk and Wim A.A. Nuij + // + // Model described in section 3, equations 1 through 5 + // Derived equation (9) of optimal path implemented below + easey.optimal = function(V, rho, callback) { + // Section 6 describes user testing of these tunable values + V = V || 0.9; + rho = rho || 1.42; + + function sqr(n) { return n*n; } + function sinh(n) { return (Math.exp(n) - Math.exp(-n)) / 2; } + function cosh(n) { return (Math.exp(n) + Math.exp(-n)) / 2; } + function tanh(n) { return sinh(n) / cosh(n); } + + map.coordinate = from; // For when `from` not current coordinate + + // Width is measured in coordinate units at zoom 0 + var TL = map.pointCoordinate(new MM.Point(0, 0)).zoomTo(0), + BR = map.pointCoordinate(map.dimensions).zoomTo(0), + w0 = Math.max(BR.column - TL.column, BR.row - TL.row), + w1 = w0 * Math.pow(2, from.zoom - to.zoom), + start = from.zoomTo(0), + end = to.zoomTo(0), + c0 = {x: start.column, y: start.row}, + c1 = {x: end.column, y: end.row}, + u0 = 0, + u1 = Math.sqrt(sqr(c1.x - c0.x) + sqr(c1.y - c0.y)); + + function b(i) { + var n = sqr(w1) - sqr(w0) + (i ? -1: 1) * Math.pow(rho, 4) * sqr(u1 - u0), + d = 2 * (i ? w1 : w0) * sqr(rho) * (u1 - u0); + return n/d; + } + + function r(i) { + return Math.log(-b(i) + Math.sqrt(sqr(b(i)) + 1)); + } + + var r0 = r(0), + r1 = r(1), + S = (r1 - r0) / rho; + + // Width + function w(s) { + return w0 * cosh(r0) / cosh (rho * s + r0); + } + + // Zoom + function u(s) { + return (w0 / sqr(rho)) * cosh(r0) * tanh(rho * s + r0) - (w0 / sqr(rho)) * sinh(r0) + u0; + } + + // Special case, when no panning necessary + if (Math.abs(u1) < 0.000001) { + if (Math.abs(w0 - w1) < 0.000001) return; + + // Based on section 4 + var k = w1 < w0 ? -1 : 1; + S = Math.abs(Math.log(w1/w0)) / rho; + u = function(s) { + return u0; + } + w = function(s) { + return w0 * Math.exp(k * rho * s); + } + } + + path = function (a, b, t) { + if (t == 1) return to; + var s = t * S, + us = u(s), + z = a.zoom + (Math.log(w0/w(s)) / Math.LN2), + x = interp(c0.x, c1.x, us/u1), + y = interp(c0.y, c1.y, us/u1); + return new MM.Coordinate(y, x, 0).zoomTo(z); + } + easey.easing('linear'); + easey.run(S / V * 1000, callback); + } + + return easey; + }; + + this.easey = easey; +})(this, com.modestmaps); \ No newline at end of file diff --git a/js/jlinq.jquery.js b/js/jlinq.jquery.js new file mode 100644 index 0000000..23d1557 --- /dev/null +++ b/js/jlinq.jquery.js @@ -0,0 +1,95 @@ +/* + * jLinq - jQuery Extensions + * Hugo Bonacci - hugoware.com + * http://creativecommons.org/licenses/by/3.0/ + */ + +(function() { + + //common functions + var fn = { + + //begins a new jlinq query using a selector + //for the values matched with jQuery + query:function(selector, source) { + + //perform the selector if needed + var matches = jlinq.util.isType(jlinq.type.string, selector) + ? source.find(selector) + : source; + + //convert the object into an array + var records = fn.toArray(matches); + + //then start the new query + var query = jlinq.from(records); + query.$ = source; + return query; + + }, + + //finds the target of a selection + findTarget:function(selector, source) { + if (selector instanceof jQuery) return selector; + if (jlinq.util.isType(jlinq.type.string, selector)) source = source.find(selector); + return selector; + }, + + //performs a selection for records + select:function(selector, records) { + var selection = $(records); + return jlinq.util.isType(jlinq.type.string, selector) + ? selection.find(selector) + : selection; + }, + + //converts a jQuery object into an array + toArray:function(obj) { + var records = []; + obj.each(function(i, v) { records.push($(v)); }); + return records; + } + + }; + + //helper jQuery methods + jlinq.extend([ + + //selects all of the matching records + { name:"$", type:jlinq.command.select, + method:function() { + return fn.select(selector, this.records); + }}, + + //grabs the elements and applies filtering if needed + { name:"get", type:jlinq.command.select, + method:function(selector) { + return fn.select(selector, this.records); + }}, + + //performs additional selectors for records + { name:"include", type:jlinq.command.action, + method:function(selector, source) { + + //find the elements to match against + source = source || this.query.$; + var matches = source.find(selector); + + //merge with the selection + var records = fn.toArray(matches); + this.records = this.records.concat(records); + }} + + ]); + + //extend jQuery + $.fn.query = function(selector) { + return fn.query(selector, this); + } + + //and jlinq + jLinq.$ = function(selector) { + return fn.query(selector, $(document.body)); + }; + +})(); \ No newline at end of file diff --git a/js/jlinq.js b/js/jlinq.js new file mode 100644 index 0000000..e8e0be6 --- /dev/null +++ b/js/jlinq.js @@ -0,0 +1,1333 @@ +/* + * jLinq - 3.0.1 + * Hugo Bonacci - hugoware.com + * http://creativecommons.org/licenses/by/3.0/ + */ + +var jLinq; +var jlinq; +var jl; +(function() { + + //jLinq functionality + var framework = { + + //command types for extensions + command:{ + + //queues a comparison to filter records + query:0, + + //executes all queued commands and filters the records + select:1, + + //performs an immediate action to the query + action:2 + }, + + //common expressions + exp:{ + //gets each part of a dot notation path + get_path:/\./g, + + //escapes string so it can be used in a regular expression + escape_regex:/[\-\[\]\{\}\(\)\*\+\?\.\,\\\^\$\|\#\s]/g + }, + + //common javascript types + type:{ + nothing:-1, + undefined:0, + string:1, + number:2, + array:3, + regex:4, + bool:5, + method:6, + datetime:7, + object:99 + }, + + //contains jLinq commands and functions + library:{ + + //the current commands in jLinq + commands:{}, + + //the type comparisons for jLinq + types:{}, + + //includes a comparison to identify types + addType:function(type, compare) { + framework.library.types[type] = compare; + }, + + //adds a command to the jLinq library + extend:function(commands) { + + //convert to an array if not already + if (!framework.util.isType(framework.type.array, commands)) { + commands = [commands]; + } + + //append each method + framework.util.each(commands, function(command) { + framework.library.commands[command.name] = command; + }); + + }, + + //starts a new jLinq query + query:function(collection, params) { + + //make sure something is there + if (!framework.util.isType(framework.type.array, collection)) { + throw "jLinq can only query arrays of objects."; + } + + //clone the array to prevent changing objects - by default + //this is off + collection = params.clone || (params.clone == null && jLinq.alwaysClone) + ? framework.util.clone(collection) + : collection; + + //holds the state of the current query + var self = { + + //the public instance of the query + instance:{ + + //should this query ignore case + ignoreCase:jLinq.ignoreCase, + + //should the next command be evaluated as not + not:false, + + //the action that was last invoked + lastCommand:null, + + //the name of the last field queried + lastField:null, + + //the current records available + records:collection, + + //records that have been filtered out + removed:[], + + //tells a query to start a new function + or:function() { self.startNewCommandSet(); }, + + //the query creator object + query:{} + + }, + + //determines if the arguments provided meet the + //requirements to be a repeated command + canRepeatCommand:function(args) { + return self.instance.lastCommand != null && + args.length == (self.instance.lastCommand.method.length + 1) && + framework.util.isType(framework.type.string, args[0]) + }, + + //commands waiting to execute + commands:[[]], + + //executes the current query and updated the records + execute:function() { + var results = []; + + //get the current state of the query + var state = self.instance; + + //start checking each record + framework.util.each(self.instance.records, function(record) { + + //update the state + state.record = record; + + //perform the evaluation + if (self.evaluate(state)) { + results.push(record); + } + else { + self.instance.removed.push(record); + } + }); + + //update the matching records + self.instance.records = results; + }, + + //tries to find a value from the path name + findValue:framework.util.findValue, + + //evaluates each queued command for matched + evaluate:function(state) { + + //check each of the command sets + for (var command = 0, l = self.commands.length; command < l; command++) { + + //each set represents an 'or' set - if any + //match then return this worked + var set = self.commands[command]; + if (self.evaluateSet(set, state)) { return true; } + + }; + + //since nothing evaluated, return it failed + return false; + + }, + + //evaluates a single set of commands + evaluateSet:function(set, state) { + + //check each command in this set + for (var item in set) { + if (!set.hasOwnProperty(item)) continue; + //get the details to use + var command = set[item]; + state.value = self.findValue(state.record, command.path); + state.compare = function(types) { return framework.util.compare(state.value, types, state); }; + state.when = function(types) { return framework.util.when(state.value, types, state); }; + + //evaluate the command + try { + var result = command.method.apply(state, command.args); + if (command.not) { result = !result; } + if (!result) { return false; } + } + //errors and exceptions just result in a failed + //to evaluate as true + catch (e) { + return false; + } + + } + + //if nothing failed then return it worked + return true; + + }, + + //repeats the previous command with new + //arguments + repeat:function(arguments) { + + //check if there is anything to repeat + if (!self.instance.lastCommand || arguments == null) { return; } + + //get the array of arguments to work with + arguments = framework.util.toArray(arguments); + + //check if there is a field name has changed, and + //if so, update the arguments to match + if (self.canRepeatCommand(arguments)) { + self.instance.lastField = arguments[0]; + arguments = framework.util.select(arguments, null, 1, null); + } + + //invoke the command now + self.queue(self.instance.lastCommand, arguments); + }, + + //saves a command to evaluate later + queue:function(command, args) { + self.instance.lastCommand = command; + + //the base detail for the command + var detail = { + name:command.name, + method:command.method, + field:self.instance.lastField, + count:command.method.length, + args:args, + not:self.not + }; + + //check to see if there is an extra argument which should + //be the field name argument + if (detail.args.length > command.method.length) { + + //if so, grab the name and update the arguments + detail.field = detail.args[0]; + detail.args = framework.util.remaining(detail.args, 1); + self.instance.lastField = detail.field; + } + + //get the full path for the field name + detail.path = detail.field; + + //queue the command to the current set + self.commands[self.commands.length-1].push(detail); + + //then reset the not state + self.not = false; + + }, + + //creates a new set of methods that should be evaluated + startNewCommandSet:function() { + self.commands.push([]); + }, + + //marks a command to evaluate as NOT + setNot:function() { + self.not = !self.not; + } + + }; + + //append each of the functions + framework.util.each(framework.library.commands, function(command) { + + //Query methods queue up and are not evaluated until + //a selection or action command is called + if (command.type == framework.command.query) { + + //the default action to perform + var action = function() { + self.queue(command, arguments); + return self.instance.query; + }; + + //create the default action + self.instance.query[command.name] = action; + + //orCommand + var name = framework.util.operatorName(command.name); + self.instance.query["or"+name] = function() { + self.startNewCommandSet(); + return action.apply(null, arguments); + }; + + //orNotCommand + self.instance.query["orNot"+name] = function() { + self.startNewCommandSet(); + self.setNot(); + return action.apply(null, arguments); + }; + + //andCommand + self.instance.query["and"+name] = function() { + return action.apply(null, arguments); + }; + + //andNotCommand + self.instance.query["andNot"+name] = function() { + self.setNot(); + return action.apply(null, arguments); + }; + + //notCommand + self.instance.query["not"+name] = function() { + self.setNot(); + return action.apply(null, arguments); + }; + + } + + //Selections commands flush the queue of commands + //before they are executed. A selection command + //must return something (even if it is the current query) + else if (command.type == framework.command.select) { + self.instance.query[command.name] = function() { + + //apply the current changes + self.execute(); + + //get the current state of the query + var state = self.instance; + state.compare = function(value, types) { return framework.util.compare(value, types, state); }; + state.when = function(value, types) { return framework.util.when(value, types, state); }; + + //perform the work + return command.method.apply(state, arguments); + }; + } + + //actions evaluate immediately then return control to + //the query + else if (command.type == framework.command.action) { + self.instance.query[command.name] = function() { + + //get the current state of the query + var state = self.instance; + state.compare = function(value, types) { return framework.util.compare(value, types, state); }; + state.when = function(value, types) { return framework.util.when(value, types, state); }; + + //perform the work + command.method.apply(state, arguments); + return self.instance.query; + }; + } + + }); + + //causes the next command to be an 'or' + self.instance.query.or = function() { + self.startNewCommandSet(); + self.repeat(arguments); + return self.instance.query; + }; + + //causes the next command to be an 'and' (which is default) + self.instance.query.and = function() { + self.repeat(arguments); + return self.instance.query; + }; + + //causes the next command to be a 'not' + self.instance.query.not = function() { + self.setNot(); + self.repeat(arguments); + return self.instance.query; + }; + + //causes the next command to be a 'not' + self.instance.query.andNot = function() { + self.setNot(); + self.repeat(arguments); + return self.instance.query; + }; + + //causes the next command to be a 'not' and 'or' + self.instance.query.orNot = function() { + self.startNewCommandSet(); + self.setNot(); + self.repeat(arguments); + return self.instance.query; + }; + + //return the query information + return self.instance.query; + + } + + }, + + //variety of helper methods + util:{ + + //removes trailing and leading spaces from a value + trim:function(value) { + + //get the string value + value = value == null ? "" : value; + value = value.toString(); + + //trim the spaces + return value.replace(/^\s*|\s*$/g, ""); + + }, + + //clones each item in an array + cloneArray:function(array) { + var result = []; + framework.util.each(array, function(item) { + result.push(framework.util.clone(item)); + }); + return result; + }, + + //creates a copy of an object + clone:function(obj) { + + //for arrays, copy each item + if (framework.util.isType(framework.type.array, obj)) { + return framework.util.cloneArray(obj); + } + //for object check each value + else if (framework.util.isType(framework.type.object, obj)) { + var clone = {}; + for(var item in obj) { + if (obj.hasOwnProperty(item)) clone[item] = framework.util.clone(obj[item]); + } + return clone; + } + //all other types just return the value + else { + return obj; + } + }, + + //creates an invocation handler for a field + //name instead of grabbing values + invoke:function(obj, args) { + //copy the array to avoid breaking any other calls + args = args.concat(); + + //start by getting the path + var path = args[0]; + + //find the method and extract the arguments + var method = framework.util.findValue(obj, path); + args = framework.util.select(args, null, 1, null); + + //if we are invoking a method that hangs off + //another object then we need to find the value + path = path.replace(/\..*$/, ""); + var parent = framework.util.findValue(obj, path); + obj = parent === method ? obj : parent; + + //return the result of the call + try { + var result = method.apply(obj, args); + return result; + } + catch (e) { + return null; + } + + }, + + //gets a path from a field name + getPath:function(path) { + return framework.util.toString(path).split(framework.exp.get_path); + }, + + //searches an object to find a value + findValue:function(obj, path) { + + //start by checking if this is actualy an attempt to + //invoke a value on this property + if (framework.util.isType(framework.type.array, path)) { + return framework.util.invoke(obj, path); + + } + //if this referring to a field + else if (framework.util.isType(framework.type.string, path)) { + + //get each part of the path + path = framework.util.getPath(path); + + //search for the record + var index = 0; + while(obj != null && index < path.length) { + obj = obj[path[index++]]; + } + + //return the final found object + return obj; + + } + //nothing that can be read, just return the value + else { + return obj; + } + + }, + + //returns the value at the provided index + elementAt:function(collection, index) { + return collection && collection.length > 0 && index < collection.length && index >= 0 + ? collection[index] + : null; + }, + + //makes a string save for regular expression searching + regexEscape:function(val) { + return (val ? val : "").toString().replace(framework.exp.escape_regex, "\\$&"); + }, + + //matches expressions to a value + regexMatch:function(expression, source, ignoreCase) { + + //get the string value if needed + if (framework.util.isType(framework.type.regex, expression)) { + expression = expression.source; + } + + //create the actual expression and match + expression = new RegExp(framework.util.toString(expression), ignoreCase ? "gi" : "g"); + return framework.util.toString(source).match(expression) != null; + }, + + //converts a command to an operator name + operatorName:function(name) { + return name.replace(/^\w/, function(match) { return match.toUpperCase(); }); + }, + + //changes a value based on the type + compare:function(value, types, state) { + var result = framework.util.when(value, types, state); + return result == true ? result : false; + }, + + //performs the correct action depending on the type + when:function(value, types, state) { + + //get the kind of object this is + var kind = framework.util.getType(value); + + //check each of the types + for (var item in types) { + if (!types.hasOwnProperty(item)) continue; + var type = framework.type[item]; + if (type == kind) { + return types[item].apply(state, [value]); + } + } + + //if there is a fallback comparison + if (types.other) { return types.other.apply(state, [value]); } + + //no matches were found + return null; + }, + + //performs an action on each item in a collection + each:function(collection, action) { + var index = 0; + for(var item in collection){ + if (collection.hasOwnProperty(item)) action(collection[item], index++); + } + }, + + //performs an action to each item in a collection and then returns the items + grab:function(collection, action) { + var list = []; + framework.util.each(collection, function(item) { + list.push(action(item)); + }); + return list; + }, + + //performs an action on each item in a collection + until:function(collection, action) { + for(var item = 0, l = collection.length; item < l; item++) { + var result = action(collection[item], item + 1); + if (result === true) { return true; } + } + return false; + }, + + //checks if the types match + isType:function(type, value) { + return framework.util.getType(value) == type; + }, + + //finds the type for an object + getType:function(obj) { + + //check if this even has a value + if (obj == null) { return framework.type.nothing; } + + //check each type except object + for (var item in framework.library.types) { + if (framework.library.types[item](obj)) { return item; } + } + + //no matching type was found + return framework.type.object; + }, + + //grabs remaining elements from and array + remaining:function(array, at) { + var results = []; + for(; at < array.length; at++) results.push(array[at]); + return results; + }, + + //append items onto a target object + apply:function(target, source) { + for(var item in source) { + if (source.hasOwnProperty(item)) target[item] = source[item]; + } + return target; + }, + + //performs sorting on a collection of records + reorder:function(collection, fields, ignoreCase) { + + //reverses the fields so that they are organized + //in the correct order + return framework.util._performSort(collection, fields, ignoreCase); + }, + + //handles actual work of reordering (call reorder) + _performSort:function(collection, fields, ignoreCase) { + + //get the next field to use + var field = fields.splice(0, 1); + if (field.length == 0) { return collection; } + field = field[0]; + + //get the name of the field and descending or not + var invoked = framework.util.isType(framework.type.array, field); + var name = (invoked ? field[0] : field); + var desc = name.match(/^\-/); + name = desc ? name.substr(1) : name; + + //updat the name if needed + if (desc) { + if (invoked) { field[0] = name; } else { field = name; } + } + + //IE sorting bug resolved (Thanks @rizil) + //http://webcache.googleusercontent.com/search?q=cache:www.zachleat.com/web/2010/02/24/array-sort/+zach+array+sort + + //create the sorting method for this field + var sort = function(val1, val2) { + + //find the values to compare + var a = framework.util.findValue(val1, field); + var b = framework.util.findValue(val2, field); + + //default to something when null + if (a == null && b == null) { a = 0; b = 0; } + else if (a == null && b != null) { a = 0; b = 1; } + else if (a != null && b == null) { a = 1; b = 0; } + + //check for string values + else if (ignoreCase && + framework.util.isType(framework.type.string, a) && + framework.util.isType(framework.type.string, b)) { + a = a.toLowerCase(); + b = b.toLowerCase(); + } + //if there is a length attribute use it instead + else if (a.length && b.length) { + a = a.length; + b = b.length; + } + + //perform the sorting + var result = (a < b) ? -1 : (a > b) ? 1 : 0; + return desc ? -result : result; + + }; + + //then perform the sorting + collection.sort(sort); + + //check for sub groups if required + if (fields.length > 0) { + + //create the container for the results + var sorted = []; + var groups = framework.util.group(collection, field, ignoreCase); + framework.util.each(groups, function(group) { + var listing = fields.slice(); + var records = framework.util._performSort(group, listing, ignoreCase); + sorted = sorted.concat(records); + }); + + //update the main collection + collection = sorted; + } + + //the final results + return collection; + }, + + //returns groups of unique field values + group:function(records, field, ignoreCase) { + + //create a container to track group names + var groups = {}; + for(var item = 0, l = records.length; item < l; item++) { + //get the values + var record = records[item]; + var alias = framework.util.toString(framework.util.findValue(record, field)); + alias = ignoreCase ? alias.toUpperCase() : alias; + + //check for existing values + if (!groups[alias]) { + groups[alias] = [record]; + } + else { + groups[alias].push(record); + } + + } + + //return the matches + return groups; + + }, + + //compares two values for equality + equals:function(val1, val2, ignoreCase) { + return framework.util.when(val1, { + string:function() { + return framework.util.regexMatch( + "^"+framework.util.regexEscape(val2)+"$", + val1, + ignoreCase); + }, + other:function() { return (val1 == null && val2 == null) || (val1 === val2); } + }); + }, + + //converts an object to an array of elements + toArray:function(obj) { + var items = []; + if (obj.length) { + for (var i = 0; i < obj.length; i++) { items.push(obj[i]); } + } + else { + for (var item in obj) { + if (obj.hasOwnProperty(item)) items.push(obj[item]); + } + } + return items; + }, + + //converts a value into a string + toString:function(val) { + return val == null ? "" : val.toString(); + }, + + //grabs a range of records from a collection + skipTake:function(collection, action, skip, take) { + + //set the defaults + skip = skip == null ? 0 : skip; + take = take == null ? collection.length : take; + + //check if this will return any records + if (skip >= collection.length || + take == 0) { + return []; + } + + //return the results + return framework.util.select(collection, action, skip, skip + take); + }, + + //grabs a range and format for records + select:function(collection, action, start, end) { + + //grab the records if there is a range + start = start == null ? 0 : start; + end = end == null ? collection.length : end; + + //slice the records + var results = collection.slice(start, end); + + //check if this is a mapping method + if (jLinq.util.isType(jLinq.type.object, action)) { + var map = action; + action = function(rec) { + + //map existing values or defaults + // TODO: tests do not cover this method! + var create = {}; + for (var item in map) { + if (!map.hasOwnProperty(item)) continue; + create[item] = rec[item] + ? rec[item] + : map[item]; + } + + //return the created record + return create; + + }; + }; + + //if there is a selection method, use it + if (jLinq.util.isType(jLinq.type.method, action)) { + for (var i = 0; i < results.length; i++) { + var record = results[i]; + results[i] = action.apply(record, [record]); + } + } + + //return the final set of records + return results; + } + + } + + }; + + //default types + framework.library.addType(framework.type.nothing, function(value) { return value == null; }); + framework.library.addType(framework.type.array, function(value) { return value instanceof Array; }); + framework.library.addType(framework.type.string, function(value) { return value.substr && value.toLowerCase; }); + framework.library.addType(framework.type.number, function(value) { return value.toFixed && value.toExponential; }); + framework.library.addType(framework.type.regex, function(value) { return value instanceof RegExp; }); + framework.library.addType(framework.type.bool, function(value) { return value == true || value == false; }); + framework.library.addType(framework.type.method, function(value) { return value instanceof Function; }); + framework.library.addType(framework.type.datetime, function(value) { return value instanceof Date; }); + + //add the default methods + framework.library.extend([ + + //sets a query to ignore case + { name:"ignoreCase", type:framework.command.action, + method:function() { + this.ignoreCase = true; + }}, + + //reverses the current set of records + { name:"reverse", type:framework.command.action, + method:function() { + this.records.reverse(); + }}, + + //sets a query to evaluate case + { name:"useCase", type:framework.command.action, + method:function() { + this.ignoreCase = false; + }}, + + //performs an action for each record + { name:"each", type:framework.command.action, + method:function(action) { + jLinq.util.each(this.records, function(record) { action(record); }); + }}, + + //attaches a value or result of a method to each record + { name:"attach", type:framework.command.action, + method:function(field, action) { + this.when(action, { + method:function() { jLinq.util.each(this.records, function(record) { record[field] = action(record); }); }, + other:function() { jLinq.util.each(this.records, function(record) { record[field] = action; }); } + }); + }}, + + //joins two sets of records by the key information provided + { name:"join", type:framework.command.action, + method:function(source, alias, pk, fk) { + jLinq.util.each(this.records, function(record) { + record[alias] = jLinq.from(source).equals(fk, record[pk]).select(); + }); + }}, + + //joins a second array but uses only the first matched record. Allows for a default for a fallback value + { name:"assign", type:framework.command.action, + method:function(source, alias, pk, fk, fallback) { + jLinq.util.each(this.records, function(record) { + record[alias] = jLinq.from(source).equals(fk, record[pk]).first(fallback); + }); + }}, + + //joins two sets of records by the key information provided + { name:"sort", type:framework.command.action, + method:function() { + var args = jLinq.util.toArray(arguments); + this.records = jLinq.util.reorder(this.records, args, this.ignoreCase); + }}, + + //are the two values the same + { name:"equals", type:framework.command.query, + method:function(value) { + return jLinq.util.equals(this.value, value, this.ignoreCase); + }}, + + //does this start with a value + { name:"starts", type:framework.command.query, + method:function(value) { + return this.compare({ + array:function() { return jLinq.util.equals(this.value[0], value, this.ignoreCase); }, + other:function() { return jLinq.util.regexMatch(("^"+jLinq.util.regexEscape(value)), this.value, this.ignoreCase); } + }); + }}, + + //does this start with a value + { name:"ends", type:framework.command.query, + method:function(value) { + return this.compare({ + array:function() { return jLinq.util.equals(this.value[this.value.length - 1], value, this.ignoreCase); }, + other:function() { return jLinq.util.regexMatch((jLinq.util.regexEscape(value)+"$"), this.value, this.ignoreCase); } + }); + }}, + + //does this start with a value + { name:"contains", type:framework.command.query, + method:function(value) { + return this.compare({ + array:function() { + var ignoreCase = this.ignoreCase; + return jLinq.util.until(this.value, function(item) { return jLinq.util.equals(item, value, ignoreCase); }); + }, + other:function() { return jLinq.util.regexMatch(jLinq.util.regexEscape(value), this.value, this.ignoreCase); } + }); + }}, + + //does this start with a value + { name:"match", type:framework.command.query, + method:function(value) { + return this.compare({ + array:function() { + var ignoreCase = this.ignoreCase; + return jLinq.util.until(this.value, function(item) { return jLinq.util.regexMatch(value, item, ignoreCase); }); + }, + other:function() { return jLinq.util.regexMatch(value, this.value, this.ignoreCase); } + }); + }}, + + //checks if the value matches the type provided + { name:"type", type:framework.command.query, + method:function(type) { + return jLinq.util.isType(type, this.value); + }}, + + //is the value greater than the argument + { name:"greater", type:framework.command.query, + method:function(value) { + return this.compare({ + array:function() { return this.value.length > value; }, + string:function() { return this.value.length > value; }, + other:function() { return this.value > value; } + }); + }}, + + //is the value greater than or equal to the argument + { name:"greaterEquals", type:framework.command.query, + method:function(value) { + return this.compare({ + array:function() { return this.value.length >= value; }, + string:function() { return this.value.length >= value; }, + other:function() { return this.value >= value; } + }); + }}, + + //is the value less than the argument + { name:"less", type:framework.command.query, + method:function(value) { + return this.compare({ + array:function() { return this.value.length < value; }, + string:function() { return this.value.length < value; }, + other:function() { return this.value < value; } + }); + }}, + + //is the value less than or equal to the argument + { name:"lessEquals", type:framework.command.query, + method:function(value) { + return this.compare({ + array:function() { return this.value.length <= value; }, + string:function() { return this.value.length <= value; }, + other:function() { return this.value <= value; } + }); + }}, + + //is the value between the values provided + { name:"between", type:framework.command.query, + method:function(low, high) { + return this.compare({ + array:function() { return this.value.length > low && this.value.length < high; }, + string:function() { return this.value.length > low && this.value.length < high; }, + other:function() { return this.value > low && this.value < high; } + }); + }}, + + //is the value between or equal to the values provided + { name:"betweenEquals", type:framework.command.query, + method:function(low, high) { + return this.compare({ + array:function() { return this.value.length >= low && this.value.length <= high; }, + string:function() { return this.value.length >= low && this.value.length <= high; }, + other:function() { return this.value >= low && this.value <= high; } + }); + }}, + + //returns if a value is null or contains nothing + { name:"empty", type:framework.command.query, + method:function() { + return this.compare({ + array:function() { return this.value.length == 0; }, + string:function() { return jLinq.util.trim(this.value).length == 0; }, + other:function() { return this.value == null; } + }); + }}, + + //returns if a value is true or exists + { name:"is", type:framework.command.query, + method:function() { + return this.compare({ + bool:function() { return this.value === true; }, + other:function() { return this.value != null; } + }); + }}, + + //gets the smallest value from the collection + { name:"min", type:framework.command.select, + method:function(field) { + var matches = jLinq.util.reorder(this.records, [field], this.ignoreCase); + return jLinq.util.elementAt(matches, 0); + }}, + + //gets the largest value from the collection + { name:"max", type:framework.command.select, + method:function(field) { + var matches = jLinq.util.reorder(this.records, [field], this.ignoreCase); + return jLinq.util.elementAt(matches, matches.length - 1); + }}, + + //returns the sum of the values of the field + { name:"sum", type:framework.command.select, + method:function(field) { + var sum; + jLinq.util.each(this.records, function(record) { + var value = jLinq.util.findValue(record, field); + sum = sum == null ? value : (sum + value); + }); + return sum; + }}, + + //returns the sum of the values of the field + { name:"average", type:framework.command.select, + method:function(field) { + var sum; + jLinq.util.each(this.records, function(record) { + var value = jLinq.util.findValue(record, field); + sum = sum == null ? value : (sum + value); + }); + return sum / this.records.length; + }}, + + //skips the requested number of records + { name:"skip", type:framework.command.select, + method:function(skip, selection) { + this.records = this.when(selection, { + method:function() { return jLinq.util.skipTake(this.records, selection, skip, null); }, + object:function() { return jLinq.util.skipTake(this.records, selection, skip, null); }, + other:function() { return jLinq.util.skipTake(this.records, null, skip, null); } + }); + return this.query; + }}, + + //takes the requested number of records + { name:"take", type:framework.command.select, + method:function(take, selection) { + return this.when(selection, { + method:function() { return jLinq.util.skipTake(this.records, selection, null, take); }, + object:function() { return jLinq.util.skipTake(this.records, selection, null, take); }, + other:function() { return jLinq.util.skipTake(this.records, null, null, take); } + }); + }}, + + //skips and takes records + { name:"skipTake", type:framework.command.select, + method:function(skip, take, selection) { + return this.when(selection, { + method:function() { return jLinq.util.skipTake(this.records, selection, skip, take); }, + object:function() { return jLinq.util.skipTake(this.records, selection, skip, take); }, + other:function() { return jLinq.util.skipTake(this.records, null, skip, take); } + }); + }}, + + //selects the remaining records + { name:"select", type:framework.command.select, + method:function(selection) { + return this.when(selection, { + method:function() { return jLinq.util.select(this.records, selection); }, + object:function() { return jLinq.util.select(this.records, selection); }, + other:function() { return this.records; } + }); + }}, + + //selects all of the distinct values for a field + { name:"distinct", type:framework.command.select, + method:function(field) { + var groups = jLinq.util.group(this.records, field, this.ignoreCase); + return jLinq.util.grab(groups, function(record) { + return jLinq.util.findValue(record[0], field); + }); + }}, + + //groups the values of a field by unique values + { name:"group", type:framework.command.select, + method:function(field) { + return jLinq.util.group(this.records, field, this.ignoreCase); + }}, + + //selects records into a new format + { name:"define", type:framework.command.select, + method:function(selection) { + var results = this.when(selection, { + method:function() { return jLinq.util.select(this.records, selection); }, + object:function() { return jLinq.util.select(this.records, selection); }, + other:function() { return this.records; } + }); + return jLinq.from(results); + }}, + + //returns if a collection contains any records + { name:"any", type:framework.command.select, + method:function() { + return this.records.length > 0; + }}, + + //returns if no records matched this query + { name:"none", type:framework.command.select, + method:function() { + return this.records.length == 0; + }}, + + //returns if all records matched the query + { name:"all", type:framework.command.select, + method:function() { + return this.removed.length == 0; + }}, + + //returns the first record found or the fallback value if nothing was found + { name:"first", type:framework.command.select, + method:function(fallback) { + var record = jLinq.util.elementAt(this.records, 0); + return record == null ? fallback : record; + }}, + + //returns the last record found or the fallback value if nothing was found + { name:"last", type:framework.command.select, + method:function(fallback) { + var record = jLinq.util.elementAt(this.records, this.records.length - 1); + return record == null ? fallback : record; + }}, + + //returns the record at the provided index or the fallback value if nothing was found + { name:"at", type:framework.command.select, + method:function(index, fallback) { + var record = jLinq.util.elementAt(this.records, index); + return record == null ? fallback : record; + }}, + + //returns the remaining count of records + { name:"count", type:framework.command.select, + method:function() { + return this.records.length; + }}, + + //selects the remaining records + { name:"removed", type:framework.command.select, + method:function(selection) { + return this.when(selection, { + method:function() { return jLinq.util.select(this.removed, selection); }, + object:function() { return jLinq.util.select(this.removed, selection); }, + other:function() { return this.removed; } + }); + }}, + + //performs a manual comparison of records + { name:"where", type:framework.command.select, + method:function(compare) { + + //filter the selection + var state = this; + var matches = []; + jLinq.util.each(this.records, function(record) { + if (compare.apply(state, [record]) === true) { matches.push(record); } + }); + + //create a new query with matching arguments + var query = jLinq.from(matches); + if (!this.ignoreCase) { query.useCase(); } + return query; + }} + + ]); + + //set the public object + jLinq = { + + //determines if new queries should always be + //cloned to prevent accidental changes to objects + alwaysClone:false, + + //sets the default for jLinq query case checking + ignoreCase:true, + + //command types (select, query, action) + command:framework.command, + + //types of object and values + type:framework.type, + + //allows command to be added to the library + extend:function() { framework.library.extend.apply(null, arguments); }, + + //core function to start and entirely new query + query:function(collection, params) { + return library.framework.query(collection, params); + }, + + //starts a new query with the array provided + from:function(collection) { + return framework.library.query(collection, { clone:false }); + }, + + //returns a list of commands in the library + getCommands:function() { + return framework.util.grab(framework.library.commands, function(command) { + return { + name:command.name, + typeId:command.type, + type:command.type == framework.command.select ? "select" + : command.type == framework.command.query ? "query" + : command.type == framework.command.action ? "action" + : "unknown" + }; + }); + }, + + //helper functions for jLinq + util:{ + + //removes leading and trailing spaces + trim:framework.util.trim, + + //loops and finds a value in an object from a path + findValue:framework.util.findValue, + + //gets an element at the specified index (if any) + elementAt:framework.util.elementAt, + + //returns a regex safe version of a string + regexEscape:framework.util.regexEscape, + + //compares an expression to another string + regexMatch:framework.util.regexMatch, + + //compares equality of two objects + equals:framework.util.equals, + + //gets groups for a collection + group:framework.util.group, + + //updates the order of a collection + reorder:framework.util.reorder, + + //performs a function when a value matches a type + when:framework.util.when, + + //converts an object to an array of values + toArray:framework.util.toArray, + + //loops for each record in a set + each:framework.util.each, + + //grabs a collection of items + grab:framework.util.grab, + + //loops records until one returns true or the end is reached + until:framework.util.until, + + //returns if an object is the provided type + isType:framework.util.isType, + + //determines the matching type for a value + getType:framework.util.getType, + + //applies each source property to the target + apply:framework.util.apply, + + //uses the action to select items from a collection + select:framework.util.select, + + //grabs records for a specific range + skipTake:framework.util.skipTake + + } + }; + + //set the other aliases + jlinq = jLinq; + jl = jLinq; +})(); diff --git a/js/mmg.css b/js/mmg.css new file mode 100644 index 0000000..bf53fb9 --- /dev/null +++ b/js/mmg.css @@ -0,0 +1,42 @@ +.wax-movetip { + z-index:999999; + position:absolute; + } + +.wax-intip { + position:relative; + left:-50%; + max-width:400px; + -webkit-user-select:auto; + background:#fff; + color:#000; + padding:20px; + border-radius:5px; + box-shadow:2px 2px 5px -2px #ccc; + border:2px solid #ccc; + } + +.wax-intip:after, +.wax-intip:before { + top:100%; + border:solid transparent; + content:" "; + height:0; + width:0; + position:absolute; + pointer-events:none; + } + +.wax-intip:after { + border-top-color:#fff; + border-width:15px; + left:50%; + margin-left:-15px; + } + +.wax-intip:before { + border-top-color:#ccc; + border-width:18px; + left:50%; + margin-left:-18px; + } \ No newline at end of file diff --git a/js/mmg.js b/js/mmg.js new file mode 100644 index 0000000..fb712b0 --- /dev/null +++ b/js/mmg.js @@ -0,0 +1,208 @@ +function mmg() { + + var m = {}, + // external list of geojson features + features = [], + // internal list of markers + markers = [], + // internal list of callbacks + callbackManager = new MM.CallbackManager(m, ['drawn', 'markeradded']), + // the absolute position of the parent element + position = null, + // a factory function for creating DOM elements out of + // GeoJSON objects + factory = null, + // a sorter function for sorting GeoJSON objects + // in the DOM + sorter = null, + // a list of urls from which features can be loaded. + // these can be templated with {z}, {x}, and {y} + urls, + // map bounds + left = null, + right = null; + + // reposition a single marker element + function reposition(marker) { + // remember the tile coordinate so we don't have to reproject every time + if (!marker.coord) marker.coord = m.map.locationCoordinate(marker.location); + var pos = m.map.coordinatePoint(marker.coord); + var pos_loc; + + // If this point has wound around the world, adjust its position + // to the new, onscreen location + if (pos.x < 0) { + pos_loc = new MM.Location(marker.location.lat, marker.location.lon); + pos_loc.lon += Math.ceil((left.lon - marker.location.lon) / 360) * 360; + pos = m.map.locationPoint(pos_loc); + marker.coord = m.map.locationCoordinate(pos_loc); + } else if (pos.x > m.map.dimensions.x) { + pos_loc = new MM.Location(marker.location.lat, marker.location.lon); + pos_loc.lon -= Math.ceil((marker.location.lon - right.lon) / 360) * 360; + pos = m.map.locationPoint(pos_loc); + marker.coord = m.map.locationCoordinate(pos_loc); + } + + pos.scale = 1; + pos.width = pos.height = 0; + MM.moveElement(marker.element, pos); + } + + // Adding and removing callbacks is mainly a way to enable mmg_interaction to operate. + // I think there are better ways to do this, by, for instance, having mmg be able to + // register 'binders' to markers, but this is backwards-compatible and equivalent + // externally. + m.addCallback = function(event, callback) { + callbackManager.addCallback(event, callback); + return m; + }; + + m.removeCallback = function(event, callback) { + callbackManager.removeCallback(event, callback); + return m; + }; + + m.draw = function() { + if (!m.map) return; + left = m.map.pointLocation(new MM.Point(0, 0)); + right = m.map.pointLocation(new MM.Point(m.map.dimensions.x, 0)); + callbackManager.dispatchCallback('drawn', m); + for (var i = 0; i < markers.length; i++) { + reposition(markers[i]); + } + }; + + m.add = function(marker) { + if (!marker || !marker.element) return null; + parent.appendChild(marker.element); + markers.push(marker); + callbackManager.dispatchCallback('markeradded', marker); + return marker; + }; + + m.remove = function(marker) { + if (!marker) return null; + parent.removeChild(marker.element); + for (var i = 0; i < markers.length; i++) { + if (markers[i] === marker) { + markers.splice(i, 1); + return marker; + } + } + return marker; + }; + + m.markers = function(x) { + if (!arguments.length) return markers; + }; + + // Public data interface + m.features = function(x) { + // Return features + if (!arguments.length) return features; + + // Clear features + while (parent.hasChildNodes()) { + // removing lastChild iteratively is faster than + // innerHTML = '' + // http://jsperf.com/innerhtml-vs-removechild-yo/2 + parent.removeChild(parent.lastChild); + } + + // clear markers representation + markers = []; + // Set features + if (!x) x = []; + features = x.slice(); + + features.sort(sorter); + + for (var i = 0; i < features.length; i++) { + m.add({ + element: factory(features[i]), + location: new MM.Location(features[i].geometry.coordinates[1], features[i].geometry.coordinates[0]), + data: features[i] + }); + } + + if (m.map && m.map.coordinate) m.map.draw(); + + return m; + }; + + m.url = function(x, callback) { + if (!arguments.length) return urls; + if (typeof reqwest === 'undefined') throw 'reqwest is required for url loading'; + if (typeof x === 'string') x = [x]; + + urls = x; + function add_features(x) { + if (x && x.features) m.features(x.features); + if (callback) callback(x.features, m); + } + + reqwest((urls[0].match(/geojsonp$/)) ? { + url: urls[0] + (~urls[0].indexOf('?') ? '&' : '?') + 'callback=grid', + type: 'jsonp', + jsonpCallback: 'callback', + success: add_features, + error: add_features + } : { + url: urls[0], + type: 'json', + success: add_features, + error: add_features + }); + return m; + }; + + m.extent = function() { + var ext = [{ lat: Infinity, lon: Infinity}, { lat: -Infinity, lon: -Infinity }]; + var ft = m.features(); + for (var i = 0; i < ft.length; i++) { + var coords = ft[i].geometry.coordinates; + if (coords[0] < ext[0].lon) ext[0].lon = coords[0]; + if (coords[1] < ext[0].lat) ext[0].lat = coords[1]; + if (coords[0] > ext[1].lon) ext[1].lon = coords[0]; + if (coords[1] > ext[1].lat) ext[1].lat = coords[1]; + } + return ext; + }; + + // Factory interface + m.factory = function(x) { + if (!arguments.length) return factory; + factory = x; + return m; + }; + m.factory(function defaultFactory(feature) { + var d = document.createElement('div'); + d.className = 'mmg-default'; + d.style.position = 'absolute'; + return d; + }); + + m.sort = function(x) { + if (!arguments.length) return sorter; + sorter = x; + return m; + }; + m.sort(function(a, b) { + return b.geometry.coordinates[1] - + a.geometry.coordinates[1]; + }); + + m.destroy = function() { + if (this.parent.parentNode) { + this.parent.parentNode.removeChild(this.parent); + } + }; + + // The parent DOM element + var parent = document.createElement('div'); + parent.style.cssText = 'position: absolute; top: 0px;' + + 'left: 0px; width: 100%; height: 100%; margin: 0; padding: 0; z-index: 0'; + m.parent = parent; + + return m; +} \ No newline at end of file diff --git a/js/mmg_interaction.js b/js/mmg_interaction.js new file mode 100644 index 0000000..53daab0 --- /dev/null +++ b/js/mmg_interaction.js @@ -0,0 +1,159 @@ +function mmg_interaction(mmg) { + + var mi = {}, + tooltips = [], + exclusive = true, + hide_on_move = true, + show_on_hover = true, + close_timer = null, + formatter; + + mi.formatter = function(x) { + if (!arguments.length) return formatter; + formatter = x; + return mi; + }; + mi.formatter(function(feature) { + var o = '', + props = feature.properties; + + if (props.title) { + o += '
        ' + props.title + '
        '; + } + if (props.description) { + o += '
        ' + props.description + '
        '; + } + + if (typeof html_sanitize !== undefined) { + o = html_sanitize(o, + function(url) { + if (/^(https?:\/\/|data:image)/.test(url)) return url; + }, + function(x) { return x; }); + } + + return o; + }); + + mi.hide_on_move = function(x) { + if (!arguments.length) return hide_on_move; + hide_on_move = x; + return mi; + }; + + mi.exclusive = function(x) { + if (!arguments.length) return exclusive; + exclusive = x; + return mi; + }; + + mi.show_on_hover = function(x) { + if (!arguments.length) return show_on_hover; + show_on_hover = x; + return mi; + }; + + mi.hide_tooltips = function() { + while (tooltips.length) mmg.remove(tooltips.pop()); + for (var i = 0; i < markers.length; i++) { + delete markers[i].clicked; + } + }; + + mi.bind_marker = function(marker) { + var delayed_close = function() { + if (!marker.clicked) close_timer = window.setTimeout(function() { + mi.hide_tooltips(); + }, 200); + }; + + var show = function(e) { + var content = formatter(marker.data); + // Don't show a popup if the formatter returns an + // empty string. This does not do any magic around DOM elements. + if (!content) return; + + if (exclusive && tooltips.length > 0) { + mi.hide_tooltips(); + // We've hidden all of the tooltips, so let's not close + // the one that we're creating as soon as it is created. + if (close_timer) window.clearTimeout(close_timer); + } + + var tooltip = document.createElement('div'); + tooltip.className = 'wax-movetip'; + tooltip.style.width = '100%'; + + var wrapper = tooltip.appendChild(document.createElement('div')); + wrapper.style.cssText = 'position: absolute; pointer-events: none;'; + + var intip = wrapper.appendChild(document.createElement('div')); + intip.className = 'wax-intip'; + intip.style.cssText = 'pointer-events: auto;'; + + if (typeof content == 'string') { + intip.innerHTML = content; + } else { + intip.appendChild(content); + } + + // Align the bottom of the tooltip with the top of its marker + wrapper.style.bottom = marker.element.offsetHeight / 2 + 20 + 'px'; + + if (show_on_hover) { + tooltip.onmouseover = function() { + if (close_timer) window.clearTimeout(close_timer); + }; + tooltip.onmouseout = delayed_close; + } + + var t = { + element: tooltip, + data: {}, + location: marker.location.copy() + }; + tooltips.push(t); + mmg.add(t); + mmg.draw(); + }; + + marker.element.onclick = function() { + show(); + marker.clicked = true; + }; + + if (show_on_hover) { + marker.element.onmouseover = show; + marker.element.onmouseout = delayed_close; + } + }; + + function bindPanned() { + mmg.map.addCallback('panned', function() { + if (hide_on_move) { + while (tooltips.length) { + mmg.remove(tooltips.pop()); + } + } + }); + } + + if (mmg) { + // Remove tooltips on panning + mmg.addCallback('drawn', bindPanned); + mmg.removeCallback('drawn', bindPanned); + + // Bind present markers + var markers = mmg.markers(); + for (var i = 0; i < markers.length; i++) { + mi.bind_marker(markers[i]); + } + + // Bind future markers + mmg.addCallback('markeradded', function(_, marker) { + mi.bind_marker(marker); + }); + } + + return mi; +} \ No newline at end of file diff --git a/js/modestmaps.js b/js/modestmaps.js new file mode 100644 index 0000000..ceb826f --- /dev/null +++ b/js/modestmaps.js @@ -0,0 +1,2940 @@ +/*! + * Modest Maps JS v1.0.0-beta1 + * http://modestmaps.com/ + * + * Copyright (c) 2011 Stamen Design, All Rights Reserved. + * + * Open source under the BSD License. + * http://creativecommons.org/licenses/BSD/ + * + * Versioned using Semantic Versioning (v.major.minor.patch) + * See CHANGELOG and http://semver.org/ for more details. + * + */ + +var previousMM = MM; + +// namespacing for backwards-compatibility +if (!com) { + var com = {}; + if (!com.modestmaps) com.modestmaps = {}; +} + +var MM = com.modestmaps = { + noConflict: function() { + MM = previousMM; + return this; + } +}; + +(function(MM) { + // Make inheritance bearable: clone one level of properties + MM.extend = function(child, parent) { + for (var property in parent.prototype) { + if (typeof child.prototype[property] == "undefined") { + child.prototype[property] = parent.prototype[property]; + } + } + return child; + }; + + MM.getFrame = function () { + // native animation frames + // http://webstuff.nfshost.com/anim-timing/Overview.html + // http://dev.chromium.org/developers/design-documents/requestanimationframe-implementation + // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + // can't apply these directly to MM because Chrome needs window + // to own webkitRequestAnimationFrame (for example) + // perhaps we should namespace an alias onto window instead? + // e.g. window.mmRequestAnimationFrame? + return function(callback) { + (window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function (callback) { + window.setTimeout(function () { + callback(+new Date()); + }, 10); + })(callback); + }; + }(); + + // Inspired by LeafletJS + MM.transformProperty = (function(props) { + if (!this.document) return; // node.js safety + var style = document.documentElement.style; + for (var i = 0; i < props.length; i++) { + if (props[i] in style) { + return props[i]; + } + } + return false; + })(['transformProperty', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']); + + MM.matrixString = function(point) { + // Make the result of point.scale * point.width a whole number. + if (point.scale * point.width % 1) { + point.scale += (1 - point.scale * point.width % 1) / point.width; + } + + var scale = point.scale || 1; + if (MM._browser.webkit3d) { + // return 'scale3d(' + scale + ',' + scale + ', 1) translate3d(' + point.x.toFixed(6) + 'px,' + point.y.toFixed(6) + 'px, 0px)'; + return 'scale3d(' + scale + ',' + scale + ', 1) translate3d(' + point.x.toFixed(0) + 'px,' + point.y.toFixed(0) + 'px, 0px)'; + } else { + return 'scale(' + scale + ',' + scale + ') translate(' + point.x.toFixed(6) + 'px,' + point.y.toFixed(6) + 'px)'; + } + }; + + MM._browser = (function(window) { + return { + webkit: ('WebKitCSSMatrix' in window), + webkit3d: ('WebKitCSSMatrix' in window) && ('m11' in new WebKitCSSMatrix()) + }; + })(this); // use this for node.js global + + MM.moveElement = function(el, point) { + if (MM.transformProperty) { + // Optimize for identity transforms, where you don't actually + // need to change this element's string. Browsers can optimize for + // the .style.left case but not for this CSS case. + if (!point.scale) point.scale = 1; + if (!point.width) point.width = 0; + if (!point.height) point.height = 0; + var ms = MM.matrixString(point); + if (el[MM.transformProperty] !== ms) { + el.style[MM.transformProperty] = + el[MM.transformProperty] = ms; + } + } else { + el.style.left = point.x + 'px'; + el.style.top = point.y + 'px'; + // Don't set width unless asked to: this is performance-intensive + // and not always necessary + if (point.width && point.height && point.scale) { + el.style.width = Math.ceil(point.width * point.scale) + 'px'; + el.style.height = Math.ceil(point.height * point.scale) + 'px'; + } + } + }; + + // Events + // Cancel an event: prevent it from bubbling + MM.cancelEvent = function(e) { + // there's more than one way to skin this cat + e.cancelBubble = true; + e.cancel = true; + e.returnValue = false; + if (e.stopPropagation) { e.stopPropagation(); } + if (e.preventDefault) { e.preventDefault(); } + return false; + }; + + // From underscore.js + MM.bind = function(func, obj) { + var slice = Array.prototype.slice; + var nativeBind = Function.prototype.bind; + if (func.bind === nativeBind && nativeBind) { + return nativeBind.apply(func, slice.call(arguments, 1)); + } + var args = slice.call(arguments, 2); + return function() { + return func.apply(obj, args.concat(slice.call(arguments))); + }; + }; + + MM.coerceLayer = function(layerish) { + if (typeof layerish == 'string') { + // Probably a template string + return new MM.Layer(new MM.TemplatedMapProvider(layerish)); + } else if ('draw' in layerish && typeof layerish.draw == 'function') { + // good enough, though we should probably enforce .parent and .destroy() too + return layerish; + } else { + // probably a MapProvider + return new MM.Layer(layerish); + } + }; + + // see http://ejohn.org/apps/jselect/event.html for the originals + MM.addEvent = function(obj, type, fn) { + if (obj.addEventListener) { + obj.addEventListener(type, fn, false); + if (type == 'mousewheel') { + obj.addEventListener('DOMMouseScroll', fn, false); + } + } else if (obj.attachEvent) { + obj['e'+type+fn] = fn; + obj[type+fn] = function(){ obj['e'+type+fn](window.event); }; + obj.attachEvent('on'+type, obj[type+fn]); + } + }; + + MM.removeEvent = function( obj, type, fn ) { + if (obj.removeEventListener) { + obj.removeEventListener(type, fn, false); + if (type == 'mousewheel') { + obj.removeEventListener('DOMMouseScroll', fn, false); + } + } else if (obj.detachEvent) { + obj.detachEvent('on'+type, obj[type+fn]); + obj[type+fn] = null; + } + }; + + // Cross-browser function to get current element style property + MM.getStyle = function(el,styleProp) { + if (el.currentStyle) + return el.currentStyle[styleProp]; + else if (window.getComputedStyle) + return document.defaultView.getComputedStyle(el,null).getPropertyValue(styleProp); + }; + // Point + MM.Point = function(x, y) { + this.x = parseFloat(x); + this.y = parseFloat(y); + }; + + MM.Point.prototype = { + x: 0, + y: 0, + toString: function() { + return "(" + this.x.toFixed(3) + ", " + this.y.toFixed(3) + ")"; + }, + copy: function() { + return new MM.Point(this.x, this.y); + } + }; + + // Get the euclidean distance between two points + MM.Point.distance = function(p1, p2) { + return Math.sqrt( + Math.pow(p2.x - p1.x, 2) + + Math.pow(p2.y - p1.y, 2)); + }; + + // Get a point between two other points, biased by `t`. + MM.Point.interpolate = function(p1, p2, t) { + return new MM.Point( + p1.x + (p2.x - p1.x) * t, + p1.y + (p2.y - p1.y) * t); + }; + // Coordinate + // ---------- + // An object representing a tile position, at as specified zoom level. + // This is not necessarily a precise tile - `row`, `column`, and + // `zoom` can be floating-point numbers, and the `container()` function + // can be used to find the actual tile that contains the point. + MM.Coordinate = function(row, column, zoom) { + this.row = row; + this.column = column; + this.zoom = zoom; + }; + + MM.Coordinate.prototype = { + + row: 0, + column: 0, + zoom: 0, + + toString: function() { + return "(" + this.row.toFixed(3) + + ", " + this.column.toFixed(3) + + " @" + this.zoom.toFixed(3) + ")"; + }, + // Quickly generate a string representation of this coordinate to + // index it in hashes. + toKey: function() { + // We've tried to use efficient hash functions here before but we took + // them out. Contributions welcome but watch out for collisions when the + // row or column are negative and check thoroughly (exhaustively) before + // committing. + return this.zoom + ',' + this.row + ',' + this.column; + }, + // Clone this object. + copy: function() { + return new MM.Coordinate(this.row, this.column, this.zoom); + }, + // Get the actual, rounded-number tile that contains this point. + container: function() { + // using floor here (not parseInt, ~~) because we want -0.56 --> -1 + return new MM.Coordinate(Math.floor(this.row), + Math.floor(this.column), + Math.floor(this.zoom)); + }, + // Recalculate this Coordinate at a different zoom level and return the + // new object. + zoomTo: function(destination) { + var power = Math.pow(2, destination - this.zoom); + return new MM.Coordinate(this.row * power, + this.column * power, + destination); + }, + // Recalculate this Coordinate at a different relative zoom level and return the + // new object. + zoomBy: function(distance) { + var power = Math.pow(2, distance); + return new MM.Coordinate(this.row * power, + this.column * power, + this.zoom + distance); + }, + // Move this coordinate up by `dist` coordinates + up: function(dist) { + if (dist === undefined) dist = 1; + return new MM.Coordinate(this.row - dist, this.column, this.zoom); + }, + // Move this coordinate right by `dist` coordinates + right: function(dist) { + if (dist === undefined) dist = 1; + return new MM.Coordinate(this.row, this.column + dist, this.zoom); + }, + // Move this coordinate down by `dist` coordinates + down: function(dist) { + if (dist === undefined) dist = 1; + return new MM.Coordinate(this.row + dist, this.column, this.zoom); + }, + // Move this coordinate left by `dist` coordinates + left: function(dist) { + if (dist === undefined) dist = 1; + return new MM.Coordinate(this.row, this.column - dist, this.zoom); + } + }; + // Location + // -------- + MM.Location = function(lat, lon) { + this.lat = parseFloat(lat); + this.lon = parseFloat(lon); + }; + + MM.Location.prototype = { + lat: 0, + lon: 0, + toString: function() { + return "(" + this.lat.toFixed(3) + ", " + this.lon.toFixed(3) + ")"; + }, + copy: function() { + return new MM.Location(this.lat, this.lon); + } + }; + + // returns approximate distance between start and end locations + // + // default unit is meters + // + // you can specify different units by optionally providing the + // earth's radius in the units you desire + // + // Default is 6,378,000 metres, suggested values are: + // + // * 3963.1 statute miles + // * 3443.9 nautical miles + // * 6378 km + // + // see [Formula and code for calculating distance based on two lat/lon locations](http://jan.ucc.nau.edu/~cvm/latlon_formula.html) + MM.Location.distance = function(l1, l2, r) { + if (!r) { + // default to meters + r = 6378000; + } + var deg2rad = Math.PI / 180.0, + a1 = l1.lat * deg2rad, + b1 = l1.lon * deg2rad, + a2 = l2.lat * deg2rad, + b2 = l2.lon * deg2rad, + c = Math.cos(a1) * Math.cos(b1) * Math.cos(a2) * Math.cos(b2), + d = Math.cos(a1) * Math.sin(b1) * Math.cos(a2) * Math.sin(b2), + e = Math.sin(a1) * Math.sin(a2); + return Math.acos(c + d + e) * r; + }; + + // Interpolates along a great circle, f between 0 and 1 + // + // * FIXME: could be heavily optimized (lots of trig calls to cache) + // * FIXME: could be inmproved for calculating a full path + MM.Location.interpolate = function(l1, l2, f) { + if (l1.lat === l2.lat && l1.lon === l2.lon) { + return new MM.Location(l1.lat, l1.lon); + } + var deg2rad = Math.PI / 180.0, + lat1 = l1.lat * deg2rad, + lon1 = l1.lon * deg2rad, + lat2 = l2.lat * deg2rad, + lon2 = l2.lon * deg2rad; + + var d = 2 * Math.asin( + Math.sqrt( + Math.pow(Math.sin((lat1 - lat2) / 2), 2) + + Math.cos(lat1) * Math.cos(lat2) * + Math.pow(Math.sin((lon1 - lon2) / 2), 2))); + + var A = Math.sin((1-f)*d)/Math.sin(d); + var B = Math.sin(f*d)/Math.sin(d); + var x = A * Math.cos(lat1) * Math.cos(lon1) + + B * Math.cos(lat2) * Math.cos(lon2); + var y = A * Math.cos(lat1) * Math.sin(lon1) + + B * Math.cos(lat2) * Math.sin(lon2); + var z = A * Math.sin(lat1) + B * Math.sin(lat2); + + var latN = Math.atan2(z, Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))); + var lonN = Math.atan2(y,x); + + return new MM.Location(latN / deg2rad, lonN / deg2rad); + }; + + // Returns bearing from one point to another + // + // * FIXME: bearing is not constant along significant great circle arcs. + MM.Location.bearing = function(l1, l2) { + var result = Math.atan2( + Math.sin(lon1 - lon2) * + Math.cos(lat2), + Math.cos(lat1) * + Math.sin(lat2) - + Math.sin(lat1) * + Math.cos(lat2) * + Math.cos(lon1 - lon2) + ) / -(Math.PI / 180); + + // map it into 0-360 range + return (result < 0) ? result + 360 : result; + }; + // Extent + // ---------- + // An object representing a map's rectangular extent, defined by its north, + // south, east and west bounds. + + MM.Extent = function(north, west, south, east) { + if (north instanceof MM.Location && + west instanceof MM.Location) { + var northwest = north, + southeast = west; + + north = northwest.lat; + west = northwest.lon; + south = southeast.lat; + east = southeast.lon; + } + if (isNaN(south)) south = north; + if (isNaN(east)) east = west; + this.north = Math.max(north, south); + this.south = Math.min(north, south); + this.east = Math.max(east, west); + this.west = Math.min(east, west); + }; + + MM.Extent.prototype = { + // boundary attributes + north: 0, + south: 0, + east: 0, + west: 0, + + copy: function() { + return new MM.Extent(this.north, this.west, this.south, this.east); + }, + + toString: function(precision) { + if (isNaN(precision)) precision = 3; + return [ + this.north.toFixed(precision), + this.west.toFixed(precision), + this.south.toFixed(precision), + this.east.toFixed(precision) + ].join(", "); + }, + + // getters for the corner locations + northWest: function() { + return new MM.Location(this.north, this.west); + }, + southEast: function() { + return new MM.Location(this.south, this.east); + }, + northEast: function() { + return new MM.Location(this.north, this.east); + }, + southWest: function() { + return new MM.Location(this.south, this.west); + }, + // getter for the center location + center: function() { + return new MM.Location( + this.south + (this.north - this.south) / 2, + this.east + (this.west - this.east) / 2 + ); + }, + + // extend the bounds to include a location's latitude and longitude + encloseLocation: function(loc) { + if (loc.lat > this.north) this.north = loc.lat; + if (loc.lat < this.south) this.south = loc.lat; + if (loc.lon > this.east) this.east = loc.lon; + if (loc.lon < this.west) this.west = loc.lon; + }, + + // extend the bounds to include multiple locations + encloseLocations: function(locations) { + var len = locations.length; + for (var i = 0; i < len; i++) { + this.encloseLocation(locations[i]); + } + }, + + // reset bounds from a list of locations + setFromLocations: function(locations) { + var len = locations.length, + first = locations[0]; + this.north = this.south = first.lat; + this.east = this.west = first.lon; + for (var i = 1; i < len; i++) { + this.encloseLocation(locations[i]); + } + }, + + // extend the bounds to include another extent + encloseExtent: function(extent) { + if (extent.north > this.north) this.north = extent.north; + if (extent.south < this.south) this.south = extent.south; + if (extent.east > this.east) this.east = extent.east; + if (extent.west < this.west) this.west = extent.west; + }, + + // determine if a location is within this extent + containsLocation: function(loc) { + return loc.lat >= this.south && + loc.lat <= this.north && + loc.lon >= this.west && + loc.lon <= this.east; + }, + + // turn an extent into an array of locations containing its northwest + // and southeast corners (used in MM.Map.setExtent()) + toArray: function() { + return [this.northWest(), this.southEast()]; + } + }; + + MM.Extent.fromString = function(str) { + var parts = str.split(/\s*,\s*/); + if (parts.length != 4) { + throw "Invalid extent string (expecting 4 comma-separated numbers)"; + } + return new MM.Extent( + parseFloat(parts[0]), + parseFloat(parts[1]), + parseFloat(parts[2]), + parseFloat(parts[3]) + ); + }; + + MM.Extent.fromArray = function(locations) { + var extent = new MM.Extent(); + extent.setFromLocations(locations); + return extent; + }; + + // Transformation + // -------------- + MM.Transformation = function(ax, bx, cx, ay, by, cy) { + this.ax = ax; + this.bx = bx; + this.cx = cx; + this.ay = ay; + this.by = by; + this.cy = cy; + }; + + MM.Transformation.prototype = { + + ax: 0, + bx: 0, + cx: 0, + ay: 0, + by: 0, + cy: 0, + + transform: function(point) { + return new MM.Point(this.ax * point.x + this.bx * point.y + this.cx, + this.ay * point.x + this.by * point.y + this.cy); + }, + + untransform: function(point) { + return new MM.Point((point.x * this.by - point.y * this.bx - + this.cx * this.by + this.cy * this.bx) / + (this.ax * this.by - this.ay * this.bx), + (point.x * this.ay - point.y * this.ax - + this.cx * this.ay + this.cy * this.ax) / + (this.bx * this.ay - this.by * this.ax)); + } + + }; + + + // Generates a transform based on three pairs of points, + // a1 -> a2, b1 -> b2, c1 -> c2. + MM.deriveTransformation = function(a1x, a1y, a2x, a2y, + b1x, b1y, b2x, b2y, + c1x, c1y, c2x, c2y) { + var x = MM.linearSolution(a1x, a1y, a2x, + b1x, b1y, b2x, + c1x, c1y, c2x); + var y = MM.linearSolution(a1x, a1y, a2y, + b1x, b1y, b2y, + c1x, c1y, c2y); + return new MM.Transformation(x[0], x[1], x[2], y[0], y[1], y[2]); + }; + + // Solves a system of linear equations. + // + // t1 = (a * r1) + (b + s1) + c + // t2 = (a * r2) + (b + s2) + c + // t3 = (a * r3) + (b + s3) + c + // + // r1 - t3 are the known values. + // a, b, c are the unknowns to be solved. + // returns the a, b, c coefficients. + MM.linearSolution = function(r1, s1, t1, r2, s2, t2, r3, s3, t3) { + // make them all floats + r1 = parseFloat(r1); + s1 = parseFloat(s1); + t1 = parseFloat(t1); + r2 = parseFloat(r2); + s2 = parseFloat(s2); + t2 = parseFloat(t2); + r3 = parseFloat(r3); + s3 = parseFloat(s3); + t3 = parseFloat(t3); + + var a = (((t2 - t3) * (s1 - s2)) - ((t1 - t2) * (s2 - s3))) / + (((r2 - r3) * (s1 - s2)) - ((r1 - r2) * (s2 - s3))); + + var b = (((t2 - t3) * (r1 - r2)) - ((t1 - t2) * (r2 - r3))) / + (((s2 - s3) * (r1 - r2)) - ((s1 - s2) * (r2 - r3))); + + var c = t1 - (r1 * a) - (s1 * b); + return [ a, b, c ]; + }; + // Projection + // ---------- + + // An abstract class / interface for projections + MM.Projection = function(zoom, transformation) { + if (!transformation) { + transformation = new MM.Transformation(1, 0, 0, 0, 1, 0); + } + this.zoom = zoom; + this.transformation = transformation; + }; + + MM.Projection.prototype = { + + zoom: 0, + transformation: null, + + rawProject: function(point) { + throw "Abstract method not implemented by subclass."; + }, + + rawUnproject: function(point) { + throw "Abstract method not implemented by subclass."; + }, + + project: function(point) { + point = this.rawProject(point); + if(this.transformation) { + point = this.transformation.transform(point); + } + return point; + }, + + unproject: function(point) { + if(this.transformation) { + point = this.transformation.untransform(point); + } + point = this.rawUnproject(point); + return point; + }, + + locationCoordinate: function(location) { + var point = new MM.Point(Math.PI * location.lon / 180.0, + Math.PI * location.lat / 180.0); + point = this.project(point); + return new MM.Coordinate(point.y, point.x, this.zoom); + }, + + coordinateLocation: function(coordinate) { + coordinate = coordinate.zoomTo(this.zoom); + var point = new MM.Point(coordinate.column, coordinate.row); + point = this.unproject(point); + return new MM.Location(180.0 * point.y / Math.PI, + 180.0 * point.x / Math.PI); + } + }; + + // A projection for equilateral maps, based on longitude and latitude + MM.LinearProjection = function(zoom, transformation) { + MM.Projection.call(this, zoom, transformation); + }; + + // The Linear projection doesn't reproject points + MM.LinearProjection.prototype = { + rawProject: function(point) { + return new MM.Point(point.x, point.y); + }, + rawUnproject: function(point) { + return new MM.Point(point.x, point.y); + } + }; + + MM.extend(MM.LinearProjection, MM.Projection); + + MM.MercatorProjection = function(zoom, transformation) { + // super! + MM.Projection.call(this, zoom, transformation); + }; + + // Project lon/lat points into meters required for Mercator + MM.MercatorProjection.prototype = { + rawProject: function(point) { + return new MM.Point(point.x, + Math.log(Math.tan(0.25 * Math.PI + 0.5 * point.y))); + }, + + rawUnproject: function(point) { + return new MM.Point(point.x, + 2 * Math.atan(Math.pow(Math.E, point.y)) - 0.5 * Math.PI); + } + }; + + MM.extend(MM.MercatorProjection, MM.Projection); + // Providers + // --------- + // Providers provide tile URLs and possibly elements for layers. + // + // MapProvider -> + // TemplatedMapProvider + // + MM.MapProvider = function(getTile) { + if (getTile) { + this.getTile = getTile; + } + }; + + MM.MapProvider.prototype = { + + // these are limits for available *tiles* + // panning limits will be different (since you can wrap around columns) + // but if you put Infinity in here it will screw up sourceCoordinate + tileLimits: [ + new MM.Coordinate(0,0,0), // top left outer + new MM.Coordinate(1,1,0).zoomTo(18) // bottom right inner + ], + + getTileUrl: function(coordinate) { + throw "Abstract method not implemented by subclass."; + }, + + getTile: function(coordinate) { + throw "Abstract method not implemented by subclass."; + }, + + // releaseTile is not required + releaseTile: function(element) { }, + + // use this to tell MapProvider that tiles only exist between certain zoom levels. + // should be set separately on Map to restrict interactive zoom/pan ranges + setZoomRange: function(minZoom, maxZoom) { + this.tileLimits[0] = this.tileLimits[0].zoomTo(minZoom); + this.tileLimits[1] = this.tileLimits[1].zoomTo(maxZoom); + }, + + // return null if coord is above/below row extents + // wrap column around the world if it's outside column extents + // ... you should override this function if you change the tile limits + // ... see enforce-limits in examples for details + sourceCoordinate: function(coord) { + var TL = this.tileLimits[0].zoomTo(coord.zoom), + BR = this.tileLimits[1].zoomTo(coord.zoom), + columnSize = Math.pow(2, coord.zoom), + wrappedColumn; + + if (coord.column < 0) { + wrappedColumn = (coord.column + columnSize) % columnSize; + } else { + wrappedColumn = coord.column % columnSize; + } + + if (coord.row < TL.row || coord.row >= BR.row) { + return null; + } else if (wrappedColumn < TL.column || wrappedColumn >= BR.column) { + return null; + } else { + return new MM.Coordinate(coord.row, wrappedColumn, coord.zoom); + } + } + }; + + /** + * FIXME: need a better explanation here! This is a pretty crucial part of + * understanding how to use ModestMaps. + * + * TemplatedMapProvider is a tile provider that generates tile URLs from a + * template string by replacing the following bits for each tile + * coordinate: + * + * {Z}: the tile's zoom level (from 1 to ~20) + * {X}: the tile's X, or column (from 0 to a very large number at higher + * zooms) + * {Y}: the tile's Y, or row (from 0 to a very large number at higher + * zooms) + * + * E.g.: + * + * var osm = new MM.TemplatedMapProvider("http://tile.openstreetmap.org/{Z}/{X}/{Y}.png"); + * + * Or: + * + * var placeholder = new MM.TemplatedMapProvider("http://placehold.it/256/f0f/fff.png&text={Z}/{X}/{Y}"); + * + */ + MM.TemplatedMapProvider = function(template, subdomains) { + var isQuadKey = template.match(/{(Q|quadkey)}/); + // replace Microsoft style substitution strings + if (isQuadKey) template = template + .replace('{subdomains}', '{S}') + .replace('{zoom}', '{Z}') + .replace('{quadkey}', '{Q}'); + + var hasSubdomains = (subdomains && + subdomains.length && template.indexOf("{S}") >= 0); + + var getTileUrl = function(coordinate) { + var coord = this.sourceCoordinate(coordinate); + if (!coord) { + return null; + } + var base = template; + if (hasSubdomains) { + var index = parseInt(coord.zoom + coord.row + coord.column, 10) % + subdomains.length; + base = base.replace('{S}', subdomains[index]); + } + if (isQuadKey) { + return base + .replace('{Z}', coord.zoom.toFixed(0)) + .replace('{Q}', this.quadKey(coord.row, + coord.column, + coord.zoom)); + } else { + return base + .replace('{Z}', coord.zoom.toFixed(0)) + .replace('{X}', coord.column.toFixed(0)) + .replace('{Y}', coord.row.toFixed(0)); + } + }; + + MM.MapProvider.call(this, getTileUrl); + }; + + MM.TemplatedMapProvider.prototype = { + // quadKey generator + quadKey: function(row, column, zoom) { + var key = ''; + for (var i = 1; i <= zoom; i++) { + key += (((row >> zoom - i) & 1) << 1) | ((column >> zoom - i) & 1); + } + return key || '0'; + }, + getTile: function(coord) { + return this.getTileUrl(coord); + } + }; + + MM.extend(MM.TemplatedMapProvider, MM.MapProvider); + + MM.TemplatedLayer = function(template, subdomains) { + return new MM.Layer(new MM.TemplatedMapProvider(template, subdomains)); + }; + // Event Handlers + // -------------- + + // A utility function for finding the offset of the + // mouse from the top-left of the page + MM.getMousePoint = function(e, map) { + // start with just the mouse (x, y) + var point = new MM.Point(e.clientX, e.clientY); + + // correct for scrolled document + point.x += document.body.scrollLeft + document.documentElement.scrollLeft; + point.y += document.body.scrollTop + document.documentElement.scrollTop; + + // correct for nested offsets in DOM + for (var node = map.parent; node; node = node.offsetParent) { + point.x -= node.offsetLeft; + point.y -= node.offsetTop; + } + return point; + }; + + // A handler that allows mouse-wheel zooming - zooming in + // when page would scroll up, and out when the page would scroll down. + MM.MouseWheelHandler = function(map, precise) { + // only init() if we get a map + if (map) { + this.init(map, precise); + // allow (null, true) as constructor args + } else if (arguments.length > 1) { + this.precise = precise ? true : false; + } + }; + + MM.MouseWheelHandler.prototype = { + precise: false, + + init: function(map) { + this.map = map; + this._mouseWheel = MM.bind(this.mouseWheel, this); + + this._zoomDiv = document.body.appendChild(document.createElement('div')); + this._zoomDiv.style.cssText = 'visibility:hidden;top:0;height:0;width:0;overflow-y:scroll'; + var innerDiv = this._zoomDiv.appendChild(document.createElement('div')); + innerDiv.style.height = '2000px'; + MM.addEvent(map.parent, 'mousewheel', this._mouseWheel); + }, + + remove: function() { + MM.removeEvent(this.map.parent, 'mousewheel', this._mouseWheel); + this._zoomDiv.parentNode.removeChild(this._zoomDiv); + }, + + mouseWheel: function(e) { + var delta = 0; + this.prevTime = this.prevTime || new Date().getTime(); + + try { + this._zoomDiv.scrollTop = 1000; + this._zoomDiv.dispatchEvent(e); + delta = 1000 - this._zoomDiv.scrollTop; + } catch (error) { + delta = e.wheelDelta || (-e.detail * 5); + } + + // limit mousewheeling to once every 200ms + var timeSince = new Date().getTime() - this.prevTime; + + if (Math.abs(delta) > 0 && (timeSince > 200) && !this.precise) { + var point = MM.getMousePoint(e, this.map); + this.map.zoomByAbout(delta > 0 ? 1 : -1, point); + + this.prevTime = new Date().getTime(); + } else if (this.precise) { + var point = MM.getMousePoint(e, this.map); + this.map.zoomByAbout(delta * 0.001, point); + } + + // Cancel the event so that the page doesn't scroll + return MM.cancelEvent(e); + } + }; + + // Handle double clicks, that zoom the map in one zoom level. + MM.DoubleClickHandler = function(map) { + if (map !== undefined) { + this.init(map); + } + }; + + MM.DoubleClickHandler.prototype = { + + init: function(map) { + this.map = map; + this._doubleClick = MM.bind(this.doubleClick, this); + MM.addEvent(map.parent, 'dblclick', this._doubleClick); + }, + + remove: function() { + MM.removeEvent(this.map.parent, 'dblclick', this._doubleClick); + }, + + doubleClick: function(e) { + // Ensure that this handler is attached once. + // Get the point on the map that was double-clicked + var point = MM.getMousePoint(e, this.map); + + // use shift-double-click to zoom out + this.map.zoomByAbout(e.shiftKey ? -1 : 1, point); + + return MM.cancelEvent(e); + } + }; + + // Handle the use of mouse dragging to pan the map. + MM.DragHandler = function(map) { + if (map !== undefined) { + this.init(map); + } + }; + + MM.DragHandler.prototype = { + + init: function(map) { + this.map = map; + this._mouseDown = MM.bind(this.mouseDown, this); + MM.addEvent(map.parent, 'mousedown', this._mouseDown); + }, + + remove: function() { + MM.removeEvent(this.map.parent, 'mousedown', this._mouseDown); + }, + + mouseDown: function(e) { + MM.addEvent(document, 'mouseup', this._mouseUp = MM.bind(this.mouseUp, this)); + MM.addEvent(document, 'mousemove', this._mouseMove = MM.bind(this.mouseMove, this)); + + this.prevMouse = new MM.Point(e.clientX, e.clientY); + this.map.parent.style.cursor = 'move'; + + return MM.cancelEvent(e); + }, + + mouseMove: function(e) { + if (this.prevMouse) { + this.map.panBy( + e.clientX - this.prevMouse.x, + e.clientY - this.prevMouse.y); + this.prevMouse.x = e.clientX; + this.prevMouse.y = e.clientY; + this.prevMouse.t = +new Date(); + } + + return MM.cancelEvent(e); + }, + + mouseUp: function(e) { + MM.removeEvent(document, 'mouseup', this._mouseUp); + MM.removeEvent(document, 'mousemove', this._mouseMove); + + this.prevMouse = null; + this.map.parent.style.cursor = ''; + + return MM.cancelEvent(e); + } + }; + + // A shortcut for adding drag, double click, + // and mouse wheel events to the map. This is the default + // handler attached to a map if the handlers argument isn't given. + MM.MouseHandler = function(map) { + if (map !== undefined) { + this.init(map); + } + }; + + MM.MouseHandler.prototype = { + init: function(map) { + this.map = map; + this.handlers = [ + new MM.DragHandler(map), + new MM.DoubleClickHandler(map), + new MM.MouseWheelHandler(map) + ]; + }, + remove: function() { + for (var i = 0; i < this.handlers.length; i++) { + this.handlers[i].remove(); + } + } + }; + + var HAS_HASHCHANGE = (function() { + var doc_mode = window.documentMode; + return ('onhashchange' in window) && + (doc_mode === undefined || doc_mode > 7); + })(); + + MM.Hash = function(map) { + this.onMapMove = MM.bind(this.onMapMove, this); + this.onHashChange = MM.bind(this.onHashChange, this); + if (map) { + this.init(map); + } + }; + + MM.Hash.prototype = { + map: null, + lastHash: null, + + parseHash: function(hash) { + var args = hash.split("/"); + if (args.length == 3) { + var zoom = parseInt(args[0], 10), + lat = parseFloat(args[1]), + lon = parseFloat(args[2]); + if (isNaN(zoom) || isNaN(lat) || isNaN(lon)) { + return false; + } else { + return { + center: new MM.Location(lat, lon), + zoom: zoom + }; + } + } else { + return false; + } + }, + + formatHash: function(map) { + var center = map.getCenter(), + zoom = map.getZoom(), + precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)); + return "#" + [zoom, + center.lat.toFixed(precision), + center.lon.toFixed(precision) + ].join("/"); + }, + + init: function(map) { + this.map = map; + this.map.addCallback("drawn", this.onMapMove); + // reset the hash + this.lastHash = null; + this.onHashChange(); + + if (!this.isListening) { + this.startListening(); + } + }, + + remove: function() { + this.map = null; + if (this.isListening) { + this.stopListening(); + } + }, + + onMapMove: function(map) { + // bail if we're moving the map (updating from a hash), + // or if the map has no zoom set + if (this.movingMap || this.map.zoom === 0) { + return false; + } + var hash = this.formatHash(map); + if (this.lastHash != hash) { + location.replace(hash); + this.lastHash = hash; + } + }, + + movingMap: false, + update: function() { + var hash = location.hash; + if (hash === this.lastHash) { + // console.info("(no change)"); + return; + } + var sansHash = hash.substr(1), + parsed = this.parseHash(sansHash); + if (parsed) { + // console.log("parsed:", parsed.zoom, parsed.center.toString()); + this.movingMap = true; + this.map.setCenterZoom(parsed.center, parsed.zoom); + this.movingMap = false; + } else { + // console.warn("parse error; resetting:", this.map.getCenter(), this.map.getZoom()); + this.onMapMove(this.map); + } + }, + + // defer hash change updates every 100ms + changeDefer: 100, + changeTimeout: null, + onHashChange: function() { + // throttle calls to update() so that they only happen every + // `changeDefer` ms + if (!this.changeTimeout) { + var that = this; + this.changeTimeout = setTimeout(function() { + that.update(); + that.changeTimeout = null; + }, this.changeDefer); + } + }, + + isListening: false, + hashChangeInterval: null, + startListening: function() { + if (HAS_HASHCHANGE) { + window.addEventListener("hashchange", this.onHashChange, false); + } else { + clearInterval(this.hashChangeInterval); + this.hashChangeInterval = setInterval(this.onHashChange, 50); + } + this.isListening = true; + }, + + stopListening: function() { + if (HAS_HASHCHANGE) { + window.removeEventListener("hashchange", this.onHashChange); + } else { + clearInterval(this.hashChangeInterval); + } + this.isListening = false; + } + }; + MM.TouchHandler = function(map, options) { + if (map) { + this.init(map, options); + } + }; + + MM.TouchHandler.prototype = { + + maxTapTime: 250, + maxTapDistance: 30, + maxDoubleTapDelay: 350, + locations: {}, + taps: [], + wasPinching: false, + lastPinchCenter: null, + + init: function(map, options) { + this.map = map; + options = options || {}; + + // Fail early if this isn't a touch device. + if (!this.isTouchable()) return false; + + this._touchStartMachine = MM.bind(this.touchStartMachine, this); + this._touchMoveMachine = MM.bind(this.touchMoveMachine, this); + this._touchEndMachine = MM.bind(this.touchEndMachine, this); + MM.addEvent(map.parent, 'touchstart', + this._touchStartMachine); + MM.addEvent(map.parent, 'touchmove', + this._touchMoveMachine); + MM.addEvent(map.parent, 'touchend', + this._touchEndMachine); + + this.options = {}; + this.options.snapToZoom = options.snapToZoom || true; + }, + + isTouchable: function() { + var el = document.createElement('div'); + el.setAttribute('ongesturestart', 'return;'); + return (typeof el.ongesturestart === 'function'); + }, + + remove: function() { + // Fail early if this isn't a touch device. + if (!this.isTouchable()) return false; + + MM.removeEvent(this.map.parent, 'touchstart', + this._touchStartMachine); + MM.removeEvent(this.map.parent, 'touchmove', + this._touchMoveMachine); + MM.removeEvent(this.map.parent, 'touchend', + this._touchEndMachine); + }, + + updateTouches: function(e) { + for (var i = 0; i < e.touches.length; i += 1) { + var t = e.touches[i]; + if (t.identifier in this.locations) { + var l = this.locations[t.identifier]; + l.x = t.screenX; + l.y = t.screenY; + l.scale = e.scale; + } + else { + this.locations[t.identifier] = { + scale: e.scale, + startPos: { x: t.screenX, y: t.screenY }, + x: t.screenX, + y: t.screenY, + time: new Date().getTime() + }; + } + } + }, + + // Test whether touches are from the same source - + // whether this is the same touchmove event. + sameTouch: function(event, touch) { + return (event && event.touch) && + (touch.identifier == event.touch.identifier); + }, + + touchStartMachine: function(e) { + this.updateTouches(e); + return MM.cancelEvent(e); + }, + + touchMoveMachine: function(e) { + switch (e.touches.length) { + case 1: + this.onPanning(e.touches[0]); + break; + case 2: + this.onPinching(e); + break; + } + this.updateTouches(e); + return MM.cancelEvent(e); + }, + + touchEndMachine: function(e) { + var now = new Date().getTime(); + // round zoom if we're done pinching + if (e.touches.length === 0 && this.wasPinching) { + this.onPinched(this.lastPinchCenter); + } + + // Look at each changed touch in turn. + for (var i = 0; i < e.changedTouches.length; i += 1) { + var t = e.changedTouches[i], + loc = this.locations[t.identifier]; + // if we didn't see this one (bug?) + // or if it was consumed by pinching already + // just skip to the next one + if (!loc || loc.wasPinch) { + continue; + } + + // we now know we have an event object and a + // matching touch that's just ended. Let's see + // what kind of event it is based on how long it + // lasted and how far it moved. + var pos = { x: t.screenX, y: t.screenY }, + time = now - loc.time, + travel = MM.Point.distance(pos, loc.startPos); + if (travel > this.maxTapDistance) { + // we will to assume that the drag has been handled separately + } else if (time > this.maxTapTime) { + // close in space, but not in time: a hold + pos.end = now; + pos.duration = time; + this.onHold(pos); + } else { + // close in both time and space: a tap + pos.time = now; + this.onTap(pos); + } + } + + // Weird, sometimes an end event doesn't get thrown + // for a touch that nevertheless has disappeared. + // Still, this will eventually catch those ids: + + var validTouchIds = {}; + for (var j = 0; j < e.touches.length; j++) { + validTouchIds[e.touches[j].identifier] = true; + } + for (var id in this.locations) { + if (!(id in validTouchIds)) { + delete validTouchIds[id]; + } + } + + return MM.cancelEvent(e); + }, + + onHold: function(hold) { + // TODO + }, + + // Handle a tap event - mainly watch for a doubleTap + onTap: function(tap) { + if (this.taps.length && + (tap.time - this.taps[0].time) < this.maxDoubleTapDelay) { + this.onDoubleTap(tap); + this.taps = []; + return; + } + this.taps = [tap]; + }, + + // Handle a double tap by zooming in a single zoom level to a + // round zoom. + onDoubleTap: function(tap) { + + var z = this.map.getZoom(), // current zoom + tz = Math.round(z) + 1, // target zoom + dz = tz - z; // desired delate + // zoom in to a round number + var p = new MM.Point(tap.x, tap.y); + this.map.zoomByAbout(dz, p); + }, + + // Re-transform the actual map parent's CSS transformation + onPanning: function(touch) { + var pos = { x: touch.screenX, y: touch.screenY }, + prev = this.locations[touch.identifier]; + this.map.panBy(pos.x - prev.x, pos.y - prev.y); + }, + + onPinching: function(e) { + // use the first two touches and their previous positions + var t0 = e.touches[0], + t1 = e.touches[1], + p0 = new MM.Point(t0.screenX, t0.screenY), + p1 = new MM.Point(t1.screenX, t1.screenY), + l0 = this.locations[t0.identifier], + l1 = this.locations[t1.identifier]; + + // mark these touches so they aren't used as taps/holds + l0.wasPinch = true; + l1.wasPinch = true; + + // scale about the center of these touches + var center = MM.Point.interpolate(p0, p1, 0.5); + + this.map.zoomByAbout( + Math.log(e.scale) / Math.LN2 - + Math.log(l0.scale) / Math.LN2, + center ); + + // pan from the previous center of these touches + var prevCenter = MM.Point.interpolate(l0, l1, 0.5); + + this.map.panBy(center.x - prevCenter.x, + center.y - prevCenter.y); + this.wasPinching = true; + this.lastPinchCenter = center; + }, + + // When a pinch event ends, round the zoom of the map. + onPinched: function(p) { + // TODO: easing + if (this.options.snapToZoom) { + var z = this.map.getZoom(), // current zoom + tz = Math.round(z); // target zoom + this.map.zoomByAbout(tz - z, p); + } + this.wasPinching = false; + } + }; + // CallbackManager + // --------------- + // A general-purpose event binding manager used by `Map` + // and `RequestManager` + + // Construct a new CallbackManager, with an list of + // supported events. + MM.CallbackManager = function(owner, events) { + this.owner = owner; + this.callbacks = {}; + for (var i = 0; i < events.length; i++) { + this.callbacks[events[i]] = []; + } + }; + + // CallbackManager does simple event management for modestmaps + MM.CallbackManager.prototype = { + // The element on which callbacks will be triggered. + owner: null, + + // An object of callbacks in the form + // + // { event: function } + callbacks: null, + + // Add a callback to this object - where the `event` is a string of + // the event name and `callback` is a function. + addCallback: function(event, callback) { + if (typeof(callback) == 'function' && this.callbacks[event]) { + this.callbacks[event].push(callback); + } + }, + + // Remove a callback. The given function needs to be equal (`===`) to + // the callback added in `addCallback`, so named functions should be + // used as callbacks. + removeCallback: function(event, callback) { + if (typeof(callback) == 'function' && this.callbacks[event]) { + var cbs = this.callbacks[event], + len = cbs.length; + for (var i = 0; i < len; i++) { + if (cbs[i] === callback) { + cbs.splice(i,1); + break; + } + } + } + }, + + // Trigger a callback, passing it an object or string from the second + // argument. + dispatchCallback: function(event, message) { + if(this.callbacks[event]) { + for (var i = 0; i < this.callbacks[event].length; i += 1) { + try { + this.callbacks[event][i](this.owner, message); + } catch(e) { + //console.log(e); + // meh + } + } + } + } + }; + // RequestManager + // -------------- + // an image loading queue + MM.RequestManager = function() { + + // The loading bay is a document fragment to optimize appending, since + // the elements within are invisible. See + // [this blog post](http://ejohn.org/blog/dom-documentfragments/). + this.loadingBay = document.createDocumentFragment(); + + this.requestsById = {}; + this.openRequestCount = 0; + + this.maxOpenRequests = 4; + this.requestQueue = []; + + this.callbackManager = new MM.CallbackManager(this, [ + 'requestcomplete', 'requesterror']); + }; + + MM.RequestManager.prototype = { + + // DOM element, hidden, for making sure images dispatch complete events + loadingBay: null, + + // all known requests, by ID + requestsById: null, + + // current pending requests + requestQueue: null, + + // current open requests (children of loadingBay) + openRequestCount: null, + + // the number of open requests permitted at one time, clamped down + // because of domain-connection limits. + maxOpenRequests: null, + + // for dispatching 'requestcomplete' + callbackManager: null, + + addCallback: function(event, callback) { + this.callbackManager.addCallback(event,callback); + }, + + removeCallback: function(event, callback) { + this.callbackManager.removeCallback(event,callback); + }, + + dispatchCallback: function(event, message) { + this.callbackManager.dispatchCallback(event,message); + }, + + // Clear everything in the queue by excluding nothing + clear: function() { + this.clearExcept({}); + }, + + clearRequest: function(id) { + if(id in this.requestsById) { + delete this.requestsById[id]; + } + + for(var i = 0; i < this.requestQueue.length; i++) { + var request = this.requestQueue[i]; + if(request && request.id == id) { + this.requestQueue[i] = null; + } + } + }, + + // Clear everything in the queue except for certain keys, specified + // by an object of the form + // + // { key: throwawayvalue } + clearExcept: function(validIds) { + + // clear things from the queue first... + for (var i = 0; i < this.requestQueue.length; i++) { + var request = this.requestQueue[i]; + if (request && !(request.id in validIds)) { + this.requestQueue[i] = null; + } + } + + // then check the loadingBay... + var openRequests = this.loadingBay.childNodes; + for (var j = openRequests.length-1; j >= 0; j--) { + var img = openRequests[j]; + if (!(img.id in validIds)) { + this.loadingBay.removeChild(img); + this.openRequestCount--; + /* console.log(this.openRequestCount + " open requests"); */ + img.src = img.coord = img.onload = img.onerror = null; + } + } + + // hasOwnProperty protects against prototype additions + // > "The standard describes an augmentable Object.prototype. + // Ignore standards at your own peril." + // -- http://www.yuiblog.com/blog/2006/09/26/for-in-intrigue/ + for (var id in this.requestsById) { + if (!(id in validIds)) { + if (this.requestsById.hasOwnProperty(id)) { + var requestToRemove = this.requestsById[id]; + // whether we've done the request or not... + delete this.requestsById[id]; + if (requestToRemove !== null) { + requestToRemove = + requestToRemove.id = + requestToRemove.coord = + requestToRemove.url = null; + } + } + } + } + }, + + // Given a tile id, check whether the RequestManager is currently + // requesting it and waiting for the result. + hasRequest: function(id) { + return (id in this.requestsById); + }, + + // * TODO: remove dependency on coord (it's for sorting, maybe call it data?) + // * TODO: rename to requestImage once it's not tile specific + requestTile: function(id, coord, url) { + if (!(id in this.requestsById)) { + var request = { id: id, coord: coord.copy(), url: url }; + // if there's no url just make sure we don't request this image again + this.requestsById[id] = request; + if (url) { + this.requestQueue.push(request); + /* console.log(this.requestQueue.length + ' pending requests'); */ + } + } + }, + + getProcessQueue: function() { + // let's only create this closure once... + if (!this._processQueue) { + var theManager = this; + this._processQueue = function() { + theManager.processQueue(); + }; + } + return this._processQueue; + }, + + // Select images from the `requestQueue` and create image elements for + // them, attaching their load events to the function returned by + // `this.getLoadComplete()` so that they can be added to the map. + processQueue: function(sortFunc) { + // When the request queue fills up beyond 8, start sorting the + // requests so that spiral-loading or another pattern can be used. + if (sortFunc && this.requestQueue.length > 8) { + this.requestQueue.sort(sortFunc); + } + while (this.openRequestCount < this.maxOpenRequests && this.requestQueue.length > 0) { + var request = this.requestQueue.pop(); + if (request) { + this.openRequestCount++; + /* console.log(this.openRequestCount + ' open requests'); */ + + // JSLitmus benchmark shows createElement is a little faster than + // new Image() in Firefox and roughly the same in Safari: + // http://tinyurl.com/y9wz2jj http://tinyurl.com/yes6rrt + var img = document.createElement('img'); + + // FIXME: id is technically not unique in document if there + // are two Maps but toKey is supposed to be fast so we're trying + // to avoid a prefix ... hence we can't use any calls to + // `document.getElementById()` to retrieve images + img.id = request.id; + img.style.position = 'absolute'; + // * FIXME: store this elsewhere to avoid scary memory leaks? + // * FIXME: call this 'data' not 'coord' so that RequestManager is less Tile-centric? + img.coord = request.coord; + // add it to the DOM in a hidden layer, this is a bit of a hack, but it's + // so that the event we get in image.onload has srcElement assigned in IE6 + this.loadingBay.appendChild(img); + // set these before img.src to avoid missing an img that's already cached + img.onload = img.onerror = this.getLoadComplete(); + img.src = request.url; + + // keep things tidy + request = request.id = request.coord = request.url = null; + } + } + }, + + _loadComplete: null, + + // Get the singleton `_loadComplete` function that is called on image + // load events, either removing them from the queue and dispatching an + // event to add them to the map, or deleting them if the image failed + // to load. + getLoadComplete: function() { + // let's only create this closure once... + if (!this._loadComplete) { + var theManager = this; + this._loadComplete = function(e) { + // this is needed because we don't use MM.addEvent for images + e = e || window.event; + + // srcElement for IE, target for FF, Safari etc. + var img = e.srcElement || e.target; + + // unset these straight away so we don't call this twice + img.onload = img.onerror = null; + + // pull it back out of the (hidden) DOM + // so that draw will add it correctly later + theManager.loadingBay.removeChild(img); + theManager.openRequestCount--; + delete theManager.requestsById[img.id]; + + /* console.log(theManager.openRequestCount + ' open requests'); */ + + // NB:- complete is also true onerror if we got a 404 + if (e.type === 'load' && (img.complete || + (img.readyState && img.readyState == 'complete'))) { + theManager.dispatchCallback('requestcomplete', img); + } else { + // if it didn't finish clear its src to make sure it + // really stops loading + // FIXME: we'll never retry because this id is still + // in requestsById - is that right? + theManager.dispatchCallback('requesterror', img.src); + img.src = null; + } + + // keep going in the same order + // use `setTimeout()` to avoid the IE recursion limit, see + // http://cappuccino.org/discuss/2010/03/01/internet-explorer-global-variables-and-stack-overflows/ + // and https://github.com/stamen/modestmaps-js/issues/12 + setTimeout(theManager.getProcessQueue(), 0); + + }; + } + return this._loadComplete; + } + + }; + + // Layer + + MM.Layer = function(provider, parent) { + this.parent = parent || document.createElement('div'); + this.parent.style.cssText = 'position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; margin: 0; padding: 0; z-index: 0'; + + this.levels = {}; + + this.requestManager = new MM.RequestManager(); + this.requestManager.addCallback('requestcomplete', this.getTileComplete()); + + if (provider) { + this.setProvider(provider); + } + }; + + MM.Layer.prototype = { + + map: null, // TODO: remove + parent: null, + tiles: null, + levels: null, + + requestManager: null, + tileCacheSize: null, + maxTileCacheSize: null, + + provider: null, + recentTiles: null, + recentTilesById: {}, + + enablePyramidLoading: false, + + _tileComplete: null, + + getTileComplete: function() { + if (!this._tileComplete) { + var theLayer = this; + this._tileComplete = function(manager, tile) { + + // cache the tile itself: + theLayer.tiles[tile.id] = tile; + theLayer.tileCacheSize++; + + // also keep a record of when we last touched this tile: + var record = { + id: tile.id, + lastTouchedTime: new Date().getTime() + }; + theLayer.recentTilesById[tile.id] = record; + theLayer.recentTiles.push(record); + + // position this tile (avoids a full draw() call): + theLayer.positionTile(tile); + }; + } + + return this._tileComplete; + }, + + draw: function() { + // if we're in between zoom levels, we need to choose the nearest: + var baseZoom = Math.round(this.map.coordinate.zoom); + + // these are the top left and bottom right tile coordinates + // we'll be loading everything in between: + var startCoord = this.map.pointCoordinate(new MM.Point(0,0)) + .zoomTo(baseZoom).container(); + var endCoord = this.map.pointCoordinate(this.map.dimensions) + .zoomTo(baseZoom).container().right().down(); + + // tiles with invalid keys will be removed from visible levels + // requests for tiles with invalid keys will be canceled + // (this object maps from a tile key to a boolean) + var validTileKeys = { }; + + // make sure we have a container for tiles in the current level + var levelElement = this.createOrGetLevel(startCoord.zoom); + + // use this coordinate for generating keys, parents and children: + var tileCoord = startCoord.copy(); + + for (tileCoord.column = startCoord.column; + tileCoord.column <= endCoord.column; tileCoord.column++) { + for (tileCoord.row = startCoord.row; + tileCoord.row <= endCoord.row; tileCoord.row++) { + var validKeys = this.inventoryVisibleTile(levelElement, tileCoord); + + while (validKeys.length) { + validTileKeys[validKeys.pop()] = true; + } + } + } + + // i from i to zoom-5 are levels that would be scaled too big, + // i from zoom + 2 to levels. length are levels that would be + // scaled too small (and tiles would be too numerous) + for (var name in this.levels) { + if (this.levels.hasOwnProperty(name)) { + var zoom = parseInt(name,10); + + if (zoom >= startCoord.zoom - 5 && zoom < startCoord.zoom + 2) { + continue; + } + + var level = this.levels[name]; + level.style.display = 'none'; + var visibleTiles = this.tileElementsInLevel(level); + + while (visibleTiles.length) { + this.provider.releaseTile(visibleTiles[0].coord); + this.requestManager.clearRequest(visibleTiles[0].coord.toKey()); + level.removeChild(visibleTiles[0]); + visibleTiles.shift(); + } + } + } + + // levels we want to see, if they have tiles in validTileKeys + var minLevel = startCoord.zoom - 5; + var maxLevel = startCoord.zoom + 2; + + for (var z = minLevel; z < maxLevel; z++) { + this.adjustVisibleLevel(this.levels[z], z, validTileKeys); + } + + // cancel requests that aren't visible: + this.requestManager.clearExcept(validTileKeys); + + // get newly requested tiles, sort according to current view: + this.requestManager.processQueue(this.getCenterDistanceCompare()); + + // make sure we don't have too much stuff: + this.checkCache(); + }, + + // For a given tile coordinate in a given level element, ensure that it's + // correctly represented in the DOM including potentially-overlapping + // parent and child tiles for pyramid loading. + // + // Return a list of valid (i.e. loadable?) tile keys. + inventoryVisibleTile: function(layer_element, tile_coord) { + var tile_key = tile_coord.toKey(), + valid_tile_keys = [tile_key]; + + // Check that the needed tile already exists someplace - add it to the DOM if it does. + if (tile_key in this.tiles) { + var tile = this.tiles[tile_key]; + + // ensure it's in the DOM: + if (tile.parentNode != layer_element) { + layer_element.appendChild(tile); + // if the provider implements reAddTile(), call it + if ("reAddTile" in this.provider) { + this.provider.reAddTile(tile_key, tile_coord, tile); + } + } + + return valid_tile_keys; + } + + // Check that the needed tile has even been requested at all. + if (!this.requestManager.hasRequest(tile_key)) { + var tileToRequest = this.provider.getTile(tile_coord); + if (typeof tileToRequest == 'string') { + this.addTileImage(tile_key, tile_coord, tileToRequest); + // tile must be truish + } else if (tileToRequest) { + this.addTileElement(tile_key, tile_coord, tileToRequest); + } + } + + // look for a parent tile in our image cache + var tileCovered = false; + var maxStepsOut = tile_coord.zoom; + + for (var pz = 1; pz <= maxStepsOut; pz++) { + var parent_coord = tile_coord.zoomBy(-pz).container(); + var parent_key = parent_coord.toKey(); + + if (this.enablePyramidLoading) { + // mark all parent tiles valid + valid_tile_keys.push(parent_key); + var parentLevel = this.createOrGetLevel(parent_coord.zoom); + + //parentLevel.coordinate = parent_coord.copy(); + if (parent_key in this.tiles) { + var parentTile = this.tiles[parent_key]; + if (parentTile.parentNode != parentLevel) { + parentLevel.appendChild(parentTile); + } + } else if (!this.requestManager.hasRequest(parent_key)) { + // force load of parent tiles we don't already have + var tileToAdd = this.provider.getTile(parent_coord); + + if (typeof tileToAdd == 'string') { + this.addTileImage(parent_key, parent_coord, tileToAdd); + } else { + this.addTileElement(parent_key, parent_coord, tileToAdd); + } + } + } else { + // only mark it valid if we have it already + if (parent_key in this.tiles) { + valid_tile_keys.push(parent_key); + tileCovered = true; + break; + } + } + } + + // if we didn't find a parent, look at the children: + if (!tileCovered && !this.enablePyramidLoading) { + var child_coord = tile_coord.zoomBy(1); + + // mark everything valid whether or not we have it: + valid_tile_keys.push(child_coord.toKey()); + child_coord.column += 1; + valid_tile_keys.push(child_coord.toKey()); + child_coord.row += 1; + valid_tile_keys.push(child_coord.toKey()); + child_coord.column -= 1; + valid_tile_keys.push(child_coord.toKey()); + } + + return valid_tile_keys; + }, + + tileElementsInLevel: function(level) { + // this is somewhat future proof, we're looking for DOM elements + // not necessarily elements + var tiles = []; + for (var tile = level.firstChild; tile; tile = tile.nextSibling) { + if (tile.nodeType == 1) { + tiles.push(tile); + } + } + return tiles; + }, + + /** + * For a given level, adjust visibility as a whole and discard individual + * tiles based on values in valid_tile_keys from inventoryVisibleTile(). + */ + adjustVisibleLevel: function(level, zoom, valid_tile_keys) { + // for tracking time of tile usage: + var now = new Date().getTime(); + + if (!level) { + // no tiles for this level yet + return; + } + + var scale = 1; + var theCoord = this.map.coordinate.copy(); + + if (level.childNodes.length > 0) { + level.style.display = 'block'; + scale = Math.pow(2, this.map.coordinate.zoom - zoom); + theCoord = theCoord.zoomTo(zoom); + } else { + level.style.display = 'none'; + } + + var tileWidth = this.map.tileSize.x * scale; + var tileHeight = this.map.tileSize.y * scale; + var center = new MM.Point(this.map.dimensions.x/2, this.map.dimensions.y/2); + var tiles = this.tileElementsInLevel(level); + + while (tiles.length) { + var tile = tiles.pop(); + + if (!valid_tile_keys[tile.id]) { + this.provider.releaseTile(tile.coord); + this.requestManager.clearRequest(tile.coord.toKey()); + level.removeChild(tile); + } else { + // log last-touched-time of currently cached tiles + this.recentTilesById[tile.id].lastTouchedTime = now; + } + } + + // position tiles + MM.moveElement(level, { + x: -(theCoord.column * 256) + center.x, + y: -(theCoord.row * 256) + center.y, + scale: scale + }); + }, + + createOrGetLevel: function(zoom) { + if (zoom in this.levels) { + return this.levels[zoom]; + } + + //console.log('creating level ' + zoom); + var level = document.createElement('div'); + level.id = this.parent.id+'-zoom-'+zoom; + level.style.cssText = this.parent.style.cssText; + level.style.zIndex = zoom; + this.parent.appendChild(level); + this.levels[zoom] = level; + return level; + }, + + addTileImage: function(key, coord, url) { + this.requestManager.requestTile(key, coord, url); + }, + + addTileElement: function(key, coordinate, element) { + // Expected in draw() + element.id = key; + element.coord = coordinate.copy(); + + // cache the tile itself: + this.tiles[key] = element; + this.tileCacheSize++; + + // also keep a record of when we last touched this tile: + var record = { + id: key, + lastTouchedTime: new Date().getTime() + }; + this.recentTilesById[key] = record; + this.recentTiles.push(record); + + this.positionTile(element); + }, + + positionTile: function(tile) { + // position this tile (avoids a full draw() call): + var theCoord = this.map.coordinate.zoomTo(tile.coord.zoom); + + // Start tile positioning and prevent drag for modern browsers + tile.style.cssText = 'position:absolute;-webkit-user-select: none;-webkit-user-drag: none;-moz-user-drag: none;'; + + // Prevent drag for IE + tile.ondragstart = function() { return false; }; + + var tx = tile.coord.column * + this.map.tileSize.x; + var ty = tile.coord.row * + this.map.tileSize.y; + + // TODO: pass only scale or only w/h + MM.moveElement(tile, { + x: Math.round(tx), + y: Math.round(ty), + width: this.map.tileSize.x, + height: this.map.tileSize.y + }); + + // add tile to its level + var theLevel = this.levels[tile.coord.zoom]; + theLevel.appendChild(tile); + + // Support style transition if available. + tile.className = 'map-tile-loaded'; + + // ensure the level is visible if it's still the current level + if (Math.round(this.map.coordinate.zoom) == tile.coord.zoom) { + theLevel.style.display = 'block'; + } + + // request a lazy redraw of all levels + // this will remove tiles that were only visible + // to cover this tile while it loaded: + this.requestRedraw(); + }, + + _redrawTimer: undefined, + + requestRedraw: function() { + // we'll always draw within 1 second of this request, + // sometimes faster if there's already a pending redraw + // this is used when a new tile arrives so that we clear + // any parent/child tiles that were only being displayed + // until the tile loads at the right zoom level + if (!this._redrawTimer) { + this._redrawTimer = setTimeout(this.getRedraw(), 1000); + } + }, + + _redraw: null, + + getRedraw: function() { + // let's only create this closure once... + if (!this._redraw) { + var theLayer = this; + this._redraw = function() { + theLayer.draw(); + theLayer._redrawTimer = 0; + }; + } + return this._redraw; + }, + + numTilesOnScreen: function() { + var tileCount = 0; + for (var name in this.levels) { + if (this.levels.hasOwnProperty(name)) { + var level = this.levels[name]; + tileCount += this.tileElementsInLevel(level).length; + } + } + return tileCount; + }, + + // keeps cache below max size + // (called every time we receive a new tile and add it to the cache) + checkCache: function() { + var maxTiles = Math.max(this.numTilesOnScreen(), this.maxTileCacheSize); + + if (this.tileCacheSize > maxTiles) { + // sort from newest (highest) to oldest (lowest) + this.recentTiles.sort(function(t1, t2) { + return t2.lastTouchedTime < t1.lastTouchedTime ? -1 : + t2.lastTouchedTime > t1.lastTouchedTime ? 1 : 0; + }); + } + + while (this.recentTiles.length && this.tileCacheSize > maxTiles) { + // delete the oldest record + var tileRecord = this.recentTiles.pop(); + var now = new Date().getTime(); + delete this.recentTilesById[tileRecord.id]; + //window.console.log('removing ' + tileRecord.id + + // ' last seen ' + (now-tileRecord.lastTouchedTime) + 'ms ago'); + // now actually remove it from the cache... + var tile = this.tiles[tileRecord.id]; + if (tile.parentNode) { + // I'm leaving this uncommented for now but you should never see it: + alert("Gah: trying to removing cached tile even though it's still in the DOM"); + } else { + delete this.tiles[tileRecord.id]; + this.tileCacheSize--; + } + } + }, + + setProvider: function(newProvider) { + var firstProvider = (this.provider === null); + + // if we already have a provider the we'll need to + // clear the DOM, cancel requests and redraw + if (!firstProvider) { + this.requestManager.clear(); + + for (var name in this.levels) { + if (this.levels.hasOwnProperty(name)) { + var level = this.levels[name]; + + while (level.firstChild) { + this.provider.releaseTile(level.firstChild.coord); + level.removeChild(level.firstChild); + } + } + } + } + + // first provider or not we'll init/reset some values... + + this.tiles = {}; + this.tileCacheSize = 0; + this.maxTileCacheSize = 64; + this.recentTilesById = {}; + this.recentTiles = []; + + // for later: check geometry of old provider and set a new coordinate center + // if needed (now? or when?) + + this.provider = newProvider; + + if (!firstProvider) { + this.draw(); + } + }, + + // compares manhattan distance from center of + // requested tiles to current map center + // NB:- requested tiles are *popped* from queue, so we do a descending sort + getCenterDistanceCompare: function() { + var theCoord = this.map.coordinate.zoomTo(Math.round(this.map.coordinate.zoom)); + + return function(r1, r2) { + if (r1 && r2) { + var c1 = r1.coord; + var c2 = r2.coord; + if (c1.zoom == c2.zoom) { + var ds1 = Math.abs(theCoord.row - c1.row - 0.5) + + Math.abs(theCoord.column - c1.column - 0.5); + var ds2 = Math.abs(theCoord.row - c2.row - 0.5) + + Math.abs(theCoord.column - c2.column - 0.5); + return ds1 < ds2 ? 1 : ds1 > ds2 ? -1 : 0; + } else { + return c1.zoom < c2.zoom ? 1 : c1.zoom > c2.zoom ? -1 : 0; + } + } + return r1 ? 1 : r2 ? -1 : 0; + }; + }, + + // Remove this layer from the DOM, cancel all of its requests + // and unbind any callbacks that are bound to it. + destroy: function() { + this.requestManager.clear(); + this.requestManager.removeCallback('requestcomplete', this.getTileComplete()); + // TODO: does requestManager need a destroy function too? + this.provider = null; + // If this layer was ever attached to the DOM, detach it. + if (this.parent.parentNode) { + this.parent.parentNode.removeChild(this.parent); + } + this.map = null; + } + + }; + + // Map + + // Instance of a map intended for drawing to a div. + // + // * `parent` (required DOM element) + // Can also be an ID of a DOM element + // * `layerOrLayers` (required MM.Layer or Array of MM.Layers) + // each one must implement draw(), destroy(), have a .parent DOM element and a .map property + // (an array of URL templates or MM.MapProviders is also acceptable) + // * `dimensions` (optional Point) + // Size of map to create + // * `eventHandlers` (optional Array) + // If empty or null MouseHandler will be used + // Otherwise, each handler will be called with init(map) + MM.Map = function(parent, layerOrLayers, dimensions, eventHandlers) { + + if (typeof parent == 'string') { + parent = document.getElementById(parent); + if (!parent) { + throw 'The ID provided to modest maps could not be found.'; + } + } + this.parent = parent; + + // we're no longer adding width and height to parent.style but we still + // need to enforce padding, overflow and position otherwise everything screws up + // TODO: maybe console.warn if the current values are bad? + this.parent.style.padding = '0'; + this.parent.style.overflow = 'hidden'; + + var position = MM.getStyle(this.parent, 'position'); + if (position != 'relative' && position != 'absolute') { + this.parent.style.position = 'relative'; + } + + this.layers = []; + if(!(layerOrLayers instanceof Array)) { + layerOrLayers = [ layerOrLayers ]; + } + + for (var i = 0; i < layerOrLayers.length; i++) { + this.addLayer(layerOrLayers[i]); + } + + // default to Google-y Mercator style maps + this.projection = new MM.MercatorProjection(0, + MM.deriveTransformation(-Math.PI, Math.PI, 0, 0, + Math.PI, Math.PI, 1, 0, + -Math.PI, -Math.PI, 0, 1)); + this.tileSize = new MM.Point(256, 256); + + // default 0-18 zoom level + // with infinite horizontal pan and clamped vertical pan + this.coordLimits = [ + new MM.Coordinate(0,-Infinity,0), // top left outer + new MM.Coordinate(1,Infinity,0).zoomTo(18) // bottom right inner + ]; + + // eyes towards null island + this.coordinate = new MM.Coordinate(0.5, 0.5, 0); + + // if you don't specify dimensions we assume you want to fill the parent + // unless the parent has no w/h, in which case we'll still use a default + if (!dimensions) { + dimensions = new MM.Point(this.parent.offsetWidth, + this.parent.offsetHeight); + this.autoSize = true; + // use destroy to get rid of this handler from the DOM + MM.addEvent(window, 'resize', this.windowResize()); + } else { + this.autoSize = false; + // don't call setSize here because it calls draw() + this.parent.style.width = Math.round(dimensions.x) + 'px'; + this.parent.style.height = Math.round(dimensions.y) + 'px'; + } + this.dimensions = dimensions; + + this.callbackManager = new MM.CallbackManager(this, [ + 'zoomed', + 'panned', + 'centered', + 'extentset', + 'resized', + 'drawn' + ]); + + // set up handlers last so that all required attributes/functions are in place if needed + if (eventHandlers === undefined) { + this.eventHandlers = [ + new MM.MouseHandler(this), + new MM.TouchHandler(this) + ]; + } else { + this.eventHandlers = eventHandlers; + if (eventHandlers instanceof Array) { + for (var j = 0; j < eventHandlers.length; j++) { + eventHandlers[j].init(this); + } + } + } + + }; + + MM.Map.prototype = { + + parent: null, // DOM Element + dimensions: null, // MM.Point with x/y size of parent element + + projection: null, // MM.Projection of first known layer + coordinate: null, // Center of map MM.Coordinate with row/column/zoom + tileSize: null, // MM.Point with x/y size of tiles + + coordLimits: null, // Array of [ topLeftOuter, bottomLeftInner ] MM.Coordinates + + layers: null, // Array of MM.Layer (interface = .draw(), .destroy(), .parent and .map) + + callbackManager: null, // MM.CallbackManager, handles map events + + eventHandlers: null, // Array of interaction handlers, just a MM.MouseHandler by default + + autoSize: null, // Boolean, true if we have a window resize listener + + toString: function() { + return 'Map(#' + this.parent.id + ')'; + }, + + // callbacks... + + addCallback: function(event, callback) { + this.callbackManager.addCallback(event, callback); + return this; + }, + + removeCallback: function(event, callback) { + this.callbackManager.removeCallback(event, callback); + return this; + }, + + dispatchCallback: function(event, message) { + this.callbackManager.dispatchCallback(event, message); + return this; + }, + + windowResize: function() { + if (!this._windowResize) { + var theMap = this; + this._windowResize = function(event) { + // don't call setSize here because it sets parent.style.width/height + // and setting the height breaks percentages and default styles + theMap.dimensions = new MM.Point(theMap.parent.offsetWidth, theMap.parent.offsetHeight); + theMap.draw(); + theMap.dispatchCallback('resized', [theMap.dimensions]); + }; + } + return this._windowResize; + }, + + // A convenience function to restrict interactive zoom ranges. + // (you should also adjust map provider to restrict which tiles get loaded, + // or modify map.coordLimits and provider.tileLimits for finer control) + setZoomRange: function(minZoom, maxZoom) { + this.coordLimits[0] = this.coordLimits[0].zoomTo(minZoom); + this.coordLimits[1] = this.coordLimits[1].zoomTo(maxZoom); + }, + + // zooming + zoomBy: function(zoomOffset) { + this.coordinate = this.enforceLimits(this.coordinate.zoomBy(zoomOffset)); + MM.getFrame(this.getRedraw()); + this.dispatchCallback('zoomed', zoomOffset); + return this; + }, + + zoomIn: function() { return this.zoomBy(1); }, + zoomOut: function() { return this.zoomBy(-1); }, + setZoom: function(z) { return this.zoomBy(z - this.coordinate.zoom); }, + + zoomByAbout: function(zoomOffset, point) { + var location = this.pointLocation(point); + + this.coordinate = this.enforceLimits(this.coordinate.zoomBy(zoomOffset)); + var newPoint = this.locationPoint(location); + + this.dispatchCallback('zoomed', zoomOffset); + return this.panBy(point.x - newPoint.x, point.y - newPoint.y); + }, + + // panning + panBy: function(dx, dy) { + this.coordinate.column -= dx / this.tileSize.x; + this.coordinate.row -= dy / this.tileSize.y; + + this.coordinate = this.enforceLimits(this.coordinate); + + // Defer until the browser is ready to draw. + MM.getFrame(this.getRedraw()); + this.dispatchCallback('panned', [dx, dy]); + return this; + }, + + panLeft: function() { return this.panBy(100, 0); }, + panRight: function() { return this.panBy(-100, 0); }, + panDown: function() { return this.panBy(0, -100); }, + panUp: function() { return this.panBy(0, 100); }, + + // positioning + setCenter: function(location) { + return this.setCenterZoom(location, this.coordinate.zoom); + }, + + setCenterZoom: function(location, zoom) { + this.coordinate = this.projection.locationCoordinate(location).zoomTo(parseFloat(zoom) || 0); + MM.getFrame(this.getRedraw()); + this.dispatchCallback('centered', [location, zoom]); + return this; + }, + + extentCoordinate: function(locations, precise) { + // coerce locations to an array if it's a Extent instance + if (locations instanceof MM.Extent) { + locations = locations.toArray(); + } + + var TL, BR; + for (var i = 0; i < locations.length; i++) { + var coordinate = this.projection.locationCoordinate(locations[i]); + if (TL) { + TL.row = Math.min(TL.row, coordinate.row); + TL.column = Math.min(TL.column, coordinate.column); + TL.zoom = Math.min(TL.zoom, coordinate.zoom); + BR.row = Math.max(BR.row, coordinate.row); + BR.column = Math.max(BR.column, coordinate.column); + BR.zoom = Math.max(BR.zoom, coordinate.zoom); + } + else { + TL = coordinate.copy(); + BR = coordinate.copy(); + } + } + + var width = this.dimensions.x + 1; + var height = this.dimensions.y + 1; + + // multiplication factor between horizontal span and map width + var hFactor = (BR.column - TL.column) / (width / this.tileSize.x); + + // multiplication factor expressed as base-2 logarithm, for zoom difference + var hZoomDiff = Math.log(hFactor) / Math.log(2); + + // possible horizontal zoom to fit geographical extent in map width + var hPossibleZoom = TL.zoom - (precise ? hZoomDiff : Math.ceil(hZoomDiff)); + + // multiplication factor between vertical span and map height + var vFactor = (BR.row - TL.row) / (height / this.tileSize.y); + + // multiplication factor expressed as base-2 logarithm, for zoom difference + var vZoomDiff = Math.log(vFactor) / Math.log(2); + + // possible vertical zoom to fit geographical extent in map height + var vPossibleZoom = TL.zoom - (precise ? vZoomDiff : Math.ceil(vZoomDiff)); + + // initial zoom to fit extent vertically and horizontally + var initZoom = Math.min(hPossibleZoom, vPossibleZoom); + + // additionally, make sure it's not outside the boundaries set by map limits + initZoom = Math.min(initZoom, this.coordLimits[1].zoom); + initZoom = Math.max(initZoom, this.coordLimits[0].zoom); + + // coordinate of extent center + var centerRow = (TL.row + BR.row) / 2; + var centerColumn = (TL.column + BR.column) / 2; + var centerZoom = TL.zoom; + return new MM.Coordinate(centerRow, centerColumn, centerZoom).zoomTo(initZoom); + }, + + setExtent: function(locations, precise) { + this.coordinate = this.extentCoordinate(locations, precise); + this.draw(); // draw calls enforceLimits + // (if you switch to getFrame, call enforceLimits first) + + this.dispatchCallback('extentset', locations); + return this; + }, + + // Resize the map's container `
        `, redrawing the map and triggering + // `resized` to make sure that the map's presentation is still correct. + setSize: function(dimensions) { + // Ensure that, whether a raw object or a Point object is passed, + // this.dimensions will be a Point. + this.dimensions = new MM.Point(dimensions.x, dimensions.y); + this.parent.style.width = Math.round(this.dimensions.x) + 'px'; + this.parent.style.height = Math.round(this.dimensions.y) + 'px'; + if (this.autoSize) { + MM.removeEvent(window, 'resize', this.windowResize()); + this.autoSize = false; + } + this.draw(); // draw calls enforceLimits + // (if you switch to getFrame, call enforceLimits first) + this.dispatchCallback('resized', this.dimensions); + return this; + }, + + // projecting points on and off screen + coordinatePoint: function(coord) { + // Return an x, y point on the map image for a given coordinate. + if (coord.zoom != this.coordinate.zoom) { + coord = coord.zoomTo(this.coordinate.zoom); + } + + // distance from the center of the map + var point = new MM.Point(this.dimensions.x / 2, this.dimensions.y / 2); + point.x += this.tileSize.x * (coord.column - this.coordinate.column); + point.y += this.tileSize.y * (coord.row - this.coordinate.row); + + return point; + }, + + // Get a `MM.Coordinate` from an `MM.Point` - returns a new tile-like object + // from a screen point. + pointCoordinate: function(point) { + // new point coordinate reflecting distance from map center, in tile widths + var coord = this.coordinate.copy(); + coord.column += (point.x - this.dimensions.x / 2) / this.tileSize.x; + coord.row += (point.y - this.dimensions.y / 2) / this.tileSize.y; + + return coord; + }, + + // Return an MM.Coordinate (row,col,zoom) for an MM.Location (lat,lon). + locationCoordinate: function(location) { + return this.projection.locationCoordinate(location); + }, + + // Return an MM.Location (lat,lon) for an MM.Coordinate (row,col,zoom). + coordinateLocation: function(coordinate) { + return this.projection.coordinateLocation(coordinate); + }, + + // Return an x, y point on the map image for a given geographical location. + locationPoint: function(location) { + return this.coordinatePoint(this.locationCoordinate(location)); + }, + + // Return a geographical location on the map image for a given x, y point. + pointLocation: function(point) { + return this.coordinateLocation(this.pointCoordinate(point)); + }, + + // inspecting + getExtent: function() { + return new MM.Extent( + this.pointLocation(new MM.Point(0, 0)), + this.pointLocation(this.dimensions) + ); + }, + + extent: function(locations, precise) { + if (locations) { + return this.setExtent(locations, precise); + } else { + return this.getExtent(); + } + }, + + // Get the current centerpoint of the map, returning a `Location` + getCenter: function() { + return this.projection.coordinateLocation(this.coordinate); + }, + + center: function(location) { + if (location) { + return this.setCenter(location); + } else { + return this.getCenter(); + } + }, + + // Get the current zoom level of the map, returning a number + getZoom: function() { + return this.coordinate.zoom; + }, + + zoom: function(zoom) { + if (zoom !== undefined) { + return this.setZoom(zoom); + } else { + return this.getZoom(); + } + }, + + // return a copy of the layers array + getLayers: function() { + return this.layers.slice(); + }, + + // return the layer at the given index + getLayerAt: function(index) { + return this.layers[index]; + }, + + // put the given layer on top of all the others + addLayer: function(layer) { + this.layers.push(layer); + this.parent.appendChild(layer.parent); + layer.map = this; // TODO: remove map property from MM.Layer? + return this; + }, + + // find the given layer and remove it + removeLayer: function(layer) { + for (var i = 0; i < this.layers.length; i++) { + if (layer == this.layers[i]) { + this.removeLayerAt(i); + break; + } + } + return this; + }, + + // replace the current layer at the given index with the given layer + setLayerAt: function(index, layer) { + + if (index < 0 || index >= this.layers.length) { + throw new Error('invalid index in setLayerAt(): ' + index); + } + + if (this.layers[index] != layer) { + + // clear existing layer at this index + if (index < this.layers.length) { + this.layers[index].destroy(); + } + + // pass it on. + this.layers[index] = layer; + this.parent.appendChild(layer.parent); + layer.map = this; // TODO: remove map property from MM.Layer + + MM.getFrame(this.getRedraw()); + } + + return this; + }, + + // put the given layer at the given index, moving others if necessary + insertLayerAt: function(index, layer) { + + if (index < 0 || index > this.layers.length) { + throw new Error('invalid index in insertLayerAt(): ' + index); + } + + if (index == this.layers.length) { + // it just gets tacked on to the end + this.layers.push(layer); + this.parent.appendChild(layer.parent); + } else { + // it needs to get slipped in amongst the others + var other = this.layers[index]; + this.parent.insertBefore(layer.parent, other.parent); + this.layers.splice(index, 0, layer); + } + + layer.map = this; // TODO: remove map property from MM.Layer + + MM.getFrame(this.getRedraw()); + + return this; + }, + + // remove the layer at the given index, call .destroy() on the layer + removeLayerAt: function(index) { + if (index < 0 || index >= this.layers.length) { + throw new Error('invalid index in removeLayer(): ' + index); + } + + // gone baby gone. + var old = this.layers[index]; + this.layers.splice(index, 1); + old.destroy(); + + return this; + }, + + // switch the stacking order of two layers, by index + swapLayersAt: function(i, j) { + + if (i < 0 || i >= this.layers.length || j < 0 || j >= this.layers.length) { + throw new Error('invalid index in swapLayersAt(): ' + index); + } + + var layer1 = this.layers[i], + layer2 = this.layers[j], + dummy = document.createElement('div'); + + // kick layer2 out, replace it with the dummy. + this.parent.replaceChild(dummy, layer2.parent); + + // put layer2 back in and kick layer1 out + this.parent.replaceChild(layer2.parent, layer1.parent); + + // put layer1 back in and ditch the dummy + this.parent.replaceChild(layer1.parent, dummy); + + // now do it to the layers array + this.layers[i] = layer2; + this.layers[j] = layer1; + + return this; + }, + + // limits + + enforceZoomLimits: function(coord) { + var limits = this.coordLimits; + if (limits) { + // clamp zoom level: + var minZoom = limits[0].zoom; + var maxZoom = limits[1].zoom; + if (coord.zoom < minZoom) { + coord = coord.zoomTo(minZoom); + } + else if (coord.zoom > maxZoom) { + coord = coord.zoomTo(maxZoom); + } + } + return coord; + }, + + enforcePanLimits: function(coord) { + + if (this.coordLimits) { + + coord = coord.copy(); + + // clamp pan: + var topLeftLimit = this.coordLimits[0].zoomTo(coord.zoom); + var bottomRightLimit = this.coordLimits[1].zoomTo(coord.zoom); + var currentTopLeft = this.pointCoordinate(new MM.Point(0, 0)) + .zoomTo(coord.zoom); + var currentBottomRight = this.pointCoordinate(this.dimensions) + .zoomTo(coord.zoom); + + // this handles infinite limits: + // (Infinity - Infinity) is Nan + // NaN is never less than anything + if (bottomRightLimit.row - topLeftLimit.row < + currentBottomRight.row - currentTopLeft.row) { + // if the limit is smaller than the current view center it + coord.row = (bottomRightLimit.row + topLeftLimit.row) / 2; + } else { + if (currentTopLeft.row < topLeftLimit.row) { + coord.row += topLeftLimit.row - currentTopLeft.row; + } else if (currentBottomRight.row > bottomRightLimit.row) { + coord.row -= currentBottomRight.row - bottomRightLimit.row; + } + } + if (bottomRightLimit.column - topLeftLimit.column < + currentBottomRight.column - currentTopLeft.column) { + // if the limit is smaller than the current view, center it + coord.column = (bottomRightLimit.column + topLeftLimit.column) / 2; + } else { + if (currentTopLeft.column < topLeftLimit.column) { + coord.column += topLeftLimit.column - currentTopLeft.column; + } else if (currentBottomRight.column > bottomRightLimit.column) { + coord.column -= currentBottomRight.column - bottomRightLimit.column; + } + } + } + + return coord; + }, + + // Prevent accidentally navigating outside the `coordLimits` of the map. + enforceLimits: function(coord) { + return this.enforcePanLimits(this.enforceZoomLimits(coord)); + }, + + // rendering + + // Redraw the tiles on the map, reusing existing tiles. + draw: function() { + // make sure we're not too far in or out: + this.coordinate = this.enforceLimits(this.coordinate); + + // if we don't have dimensions, check the parent size + if (this.dimensions.x <= 0 || this.dimensions.y <= 0) { + if (this.autoSize) { + // maybe the parent size has changed? + var w = this.parent.offsetWidth, + h = this.parent.offsetHeight; + this.dimensions = new MM.Point(w,h); + if (w <= 0 || h <= 0) { + return; + } + } else { + // the issue can only be corrected with setSize + return; + } + } + + // draw layers one by one + for(var i = 0; i < this.layers.length; i++) { + this.layers[i].draw(); + } + + this.dispatchCallback('drawn'); + }, + + _redrawTimer: undefined, + + requestRedraw: function() { + // we'll always draw within 1 second of this request, + // sometimes faster if there's already a pending redraw + // this is used when a new tile arrives so that we clear + // any parent/child tiles that were only being displayed + // until the tile loads at the right zoom level + if (!this._redrawTimer) { + this._redrawTimer = setTimeout(this.getRedraw(), 1000); + } + }, + + _redraw: null, + + getRedraw: function() { + // let's only create this closure once... + if (!this._redraw) { + var theMap = this; + this._redraw = function() { + theMap.draw(); + theMap._redrawTimer = 0; + }; + } + return this._redraw; + }, + + // Attempts to destroy all attachment a map has to a page + // and clear its memory usage. + destroy: function() { + for (var j = 0; j < this.layers.length; j++) { + this.layers[j].destroy(); + } + this.layers = []; + this.projection = null; + for (var i = 0; i < this.eventHandlers.length; i++) { + this.eventHandlers[i].remove(); + } + if (this.autoSize) { + MM.removeEvent(window, 'resize', this.windowResize()); + } + } + }; + // Instance of a map intended for drawing to a div. + // + // * `parent` (required DOM element) + // Can also be an ID of a DOM element + // * `provider` (required MM.MapProvider or URL template) + // * `location` (required MM.Location) + // Location for map to show + // * `zoom` (required number) + MM.mapByCenterZoom = function(parent, layerish, location, zoom) { + var layer = MM.coerceLayer(layerish), + map = new MM.Map(parent, layer, false); + map.setCenterZoom(location, zoom).draw(); + return map; + }; + + // Instance of a map intended for drawing to a div. + // + // * `parent` (required DOM element) + // Can also be an ID of a DOM element + // * `provider` (required MM.MapProvider or URL template) + // * `locationA` (required MM.Location) + // Location of one map corner + // * `locationB` (required MM.Location) + // Location of other map corner + MM.mapByExtent = function(parent, layerish, locationA, locationB) { + var layer = MM.coerceLayer(layerish), + map = new MM.Map(parent, layer, false); + map.setExtent([locationA, locationB]).draw(); + return map; + }; + if (typeof module !== 'undefined' && module.exports) { + module.exports = { + Point: MM.Point, + Projection: MM.Projection, + MercatorProjection: MM.MercatorProjection, + LinearProjection: MM.LinearProjection, + Transformation: MM.Transformation, + Location: MM.Location, + MapProvider: MM.MapProvider, + TemplatedMapProvider: MM.TemplatedMapProvider, + Coordinate: MM.Coordinate, + deriveTransformation: MM.deriveTransformation + }; + } +})(MM); \ No newline at end of file diff --git a/js/modestmaps.min.js b/js/modestmaps.min.js new file mode 100644 index 0000000..c6e65be --- /dev/null +++ b/js/modestmaps.min.js @@ -0,0 +1,14 @@ +/* + * Modest Maps JS v1.0.0-beta1 + * http://modestmaps.com/ + * + * Copyright (c) 2011 Stamen Design, All Rights Reserved. + * + * Open source under the BSD License. + * http://creativecommons.org/licenses/BSD/ + * + * Versioned using Semantic Versioning (v.major.minor.patch) + * See CHANGELOG and http://semver.org/ for more details. + * + */ +var previousMM=MM;if(!com){var com={};if(!com.modestmaps){com.modestmaps={}}}var MM=com.modestmaps={noConflict:function(){MM=previousMM;return this}};(function(b){b.extend=function(e,c){for(var d in c.prototype){if(typeof e.prototype[d]=="undefined"){e.prototype[d]=c.prototype[d]}}return e};b.getFrame=function(){return function(c){(window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(d){window.setTimeout(function(){d(+new Date())},10)})(c)}}();b.transformProperty=(function(e){if(!this.document){return}var d=document.documentElement.style;for(var c=0;cthis.north){this.north=c.lat}if(c.latthis.east){this.east=c.lon}if(c.lonthis.north){this.north=c.north}if(c.souththis.east){this.east=c.east}if(c.west=this.south&&c.lat<=this.north&&c.lon>=this.west&&c.lon<=this.east},toArray:function(){return[this.northWest(),this.southEast()]}};b.Extent.fromString=function(d){var c=d.split(/\s*,\s*/);if(c.length!=4){throw"Invalid extent string (expecting 4 comma-separated numbers)"}return new b.Extent(parseFloat(c[0]),parseFloat(c[1]),parseFloat(c[2]),parseFloat(c[3]))};b.Extent.fromArray=function(c){var d=new b.Extent();d.setFromLocations(c);return d};b.Transformation=function(e,g,c,d,f,h){this.ax=e;this.bx=g;this.cx=c;this.ay=d;this.by=f;this.cy=h};b.Transformation.prototype={ax:0,bx:0,cx:0,ay:0,by:0,cy:0,transform:function(c){return new b.Point(this.ax*c.x+this.bx*c.y+this.cx,this.ay*c.x+this.by*c.y+this.cy)},untransform:function(c){return new b.Point((c.x*this.by-c.y*this.bx-this.cx*this.by+this.cy*this.bx)/(this.ax*this.by-this.ay*this.bx),(c.x*this.ay-c.y*this.ax-this.cx*this.ay+this.cy*this.ax)/(this.bx*this.ay-this.by*this.ax))}};b.deriveTransformation=function(m,l,g,f,c,p,i,h,e,d,o,n){var k=b.linearSolution(m,l,g,c,p,i,e,d,o);var j=b.linearSolution(m,l,f,c,p,h,e,d,n);return new b.Transformation(k[0],k[1],k[2],j[0],j[1],j[2])};b.linearSolution=function(f,o,i,e,n,h,d,m,g){f=parseFloat(f);o=parseFloat(o);i=parseFloat(i);e=parseFloat(e);n=parseFloat(n);h=parseFloat(h);d=parseFloat(d);m=parseFloat(m);g=parseFloat(g);var l=(((h-g)*(o-n))-((i-h)*(n-m)))/(((e-d)*(o-n))-((f-e)*(n-m)));var k=(((h-g)*(f-e))-((i-h)*(e-d)))/(((n-m)*(f-e))-((o-n)*(e-d)));var j=i-(f*l)-(o*k);return[l,k,j]};b.Projection=function(d,c){if(!c){c=new b.Transformation(1,0,0,0,1,0)}this.zoom=d;this.transformation=c};b.Projection.prototype={zoom:0,transformation:null,rawProject:function(c){throw"Abstract method not implemented by subclass."},rawUnproject:function(c){throw"Abstract method not implemented by subclass."},project:function(c){c=this.rawProject(c);if(this.transformation){c=this.transformation.transform(c)}return c},unproject:function(c){if(this.transformation){c=this.transformation.untransform(c)}c=this.rawUnproject(c);return c},locationCoordinate:function(d){var c=new b.Point(Math.PI*d.lon/180,Math.PI*d.lat/180);c=this.project(c);return new b.Coordinate(c.y,c.x,this.zoom)},coordinateLocation:function(d){d=d.zoomTo(this.zoom);var c=new b.Point(d.column,d.row);c=this.unproject(c);return new b.Location(180*c.y/Math.PI,180*c.x/Math.PI)}};b.LinearProjection=function(d,c){b.Projection.call(this,d,c)};b.LinearProjection.prototype={rawProject:function(c){return new b.Point(c.x,c.y)},rawUnproject:function(c){return new b.Point(c.x,c.y)}};b.extend(b.LinearProjection,b.Projection);b.MercatorProjection=function(d,c){b.Projection.call(this,d,c)};b.MercatorProjection.prototype={rawProject:function(c){return new b.Point(c.x,Math.log(Math.tan(0.25*Math.PI+0.5*c.y)))},rawUnproject:function(c){return new b.Point(c.x,2*Math.atan(Math.pow(Math.E,c.y))-0.5*Math.PI)}};b.extend(b.MercatorProjection,b.Projection);b.MapProvider=function(c){if(c){this.getTile=c}};b.MapProvider.prototype={tileLimits:[new b.Coordinate(0,0,0),new b.Coordinate(1,1,0).zoomTo(18)],getTileUrl:function(c){throw"Abstract method not implemented by subclass."},getTile:function(c){throw"Abstract method not implemented by subclass."},releaseTile:function(c){},setZoomRange:function(d,c){this.tileLimits[0]=this.tileLimits[0].zoomTo(d);this.tileLimits[1]=this.tileLimits[1].zoomTo(c)},sourceCoordinate:function(g){var d=this.tileLimits[0].zoomTo(g.zoom),e=this.tileLimits[1].zoomTo(g.zoom),c=Math.pow(2,g.zoom),f;if(g.column<0){f=(g.column+c)%c}else{f=g.column%c}if(g.row=e.row){return null}else{if(f=e.column){return null}else{return new b.Coordinate(g.row,f,g.zoom)}}}};b.TemplatedMapProvider=function(e,c){var f=e.match(/{(Q|quadkey)}/);if(f){e=e.replace("{subdomains}","{S}").replace("{zoom}","{Z}").replace("{quadkey}","{Q}")}var d=(c&&c.length&&e.indexOf("{S}")>=0);var g=function(k){var j=this.sourceCoordinate(k);if(!j){return null}var i=e;if(d){var h=parseInt(j.zoom+j.row+j.column,10)%c.length;i=i.replace("{S}",c[h])}if(f){return i.replace("{Z}",j.zoom.toFixed(0)).replace("{Q}",this.quadKey(j.row,j.column,j.zoom))}else{return i.replace("{Z}",j.zoom.toFixed(0)).replace("{X}",j.column.toFixed(0)).replace("{Y}",j.row.toFixed(0))}};b.MapProvider.call(this,g)};b.TemplatedMapProvider.prototype={quadKey:function(g,e,f){var d="";for(var c=1;c<=f;c++){d+=(((g>>f-c)&1)<<1)|((e>>f-c)&1)}return d||"0"},getTile:function(c){return this.getTileUrl(c)}};b.extend(b.TemplatedMapProvider,b.MapProvider);b.TemplatedLayer=function(d,c){return new b.Layer(new b.TemplatedMapProvider(d,c))};b.getMousePoint=function(g,f){var c=new b.Point(g.clientX,g.clientY);c.x+=document.body.scrollLeft+document.documentElement.scrollLeft;c.y+=document.body.scrollTop+document.documentElement.scrollTop;for(var d=f.parent;d;d=d.offsetParent){c.x-=d.offsetLeft;c.y-=d.offsetTop}return c};b.MouseWheelHandler=function(d,c){if(d){this.init(d,c)}else{if(arguments.length>1){this.precise=c?true:false}}};b.MouseWheelHandler.prototype={precise:false,init:function(d){this.map=d;this._mouseWheel=b.bind(this.mouseWheel,this);this._zoomDiv=document.body.appendChild(document.createElement("div"));this._zoomDiv.style.cssText="visibility:hidden;top:0;height:0;width:0;overflow-y:scroll";var c=this._zoomDiv.appendChild(document.createElement("div"));c.style.height="2000px";b.addEvent(d.parent,"mousewheel",this._mouseWheel)},remove:function(){b.removeEvent(this.map.parent,"mousewheel",this._mouseWheel);this._zoomDiv.parentNode.removeChild(this._zoomDiv)},mouseWheel:function(g){var h=0;this.prevTime=this.prevTime||new Date().getTime();try{this._zoomDiv.scrollTop=1000;this._zoomDiv.dispatchEvent(g);h=1000-this._zoomDiv.scrollTop}catch(d){h=g.wheelDelta||(-g.detail*5)}var f=new Date().getTime()-this.prevTime;if(Math.abs(h)>0&&(f>200)&&!this.precise){var c=b.getMousePoint(g,this.map);this.map.zoomByAbout(h>0?1:-1,c);this.prevTime=new Date().getTime()}else{if(this.precise){var c=b.getMousePoint(g,this.map);this.map.zoomByAbout(h*0.001,c)}}return b.cancelEvent(g)}};b.DoubleClickHandler=function(c){if(c!==undefined){this.init(c)}};b.DoubleClickHandler.prototype={init:function(c){this.map=c;this._doubleClick=b.bind(this.doubleClick,this);b.addEvent(c.parent,"dblclick",this._doubleClick)},remove:function(){b.removeEvent(this.map.parent,"dblclick",this._doubleClick)},doubleClick:function(d){var c=b.getMousePoint(d,this.map);this.map.zoomByAbout(d.shiftKey?-1:1,c);return b.cancelEvent(d)}};b.DragHandler=function(c){if(c!==undefined){this.init(c)}};b.DragHandler.prototype={init:function(c){this.map=c;this._mouseDown=b.bind(this.mouseDown,this);b.addEvent(c.parent,"mousedown",this._mouseDown)},remove:function(){b.removeEvent(this.map.parent,"mousedown",this._mouseDown)},mouseDown:function(c){b.addEvent(document,"mouseup",this._mouseUp=b.bind(this.mouseUp,this));b.addEvent(document,"mousemove",this._mouseMove=b.bind(this.mouseMove,this));this.prevMouse=new b.Point(c.clientX,c.clientY);this.map.parent.style.cursor="move";return b.cancelEvent(c)},mouseMove:function(c){if(this.prevMouse){this.map.panBy(c.clientX-this.prevMouse.x,c.clientY-this.prevMouse.y);this.prevMouse.x=c.clientX;this.prevMouse.y=c.clientY;this.prevMouse.t=+new Date()}return b.cancelEvent(c)},mouseUp:function(c){b.removeEvent(document,"mouseup",this._mouseUp);b.removeEvent(document,"mousemove",this._mouseMove);this.prevMouse=null;this.map.parent.style.cursor="";return b.cancelEvent(c)}};b.MouseHandler=function(c){if(c!==undefined){this.init(c)}};b.MouseHandler.prototype={init:function(c){this.map=c;this.handlers=[new b.DragHandler(c),new b.DoubleClickHandler(c),new b.MouseWheelHandler(c)]},remove:function(){for(var c=0;c7)})();b.Hash=function(c){this.onMapMove=b.bind(this.onMapMove,this);this.onHashChange=b.bind(this.onHashChange,this);if(c){this.init(c)}};b.Hash.prototype={map:null,lastHash:null,parseHash:function(f){var c=f.split("/");if(c.length==3){var d=parseInt(c[0],10),e=parseFloat(c[1]),g=parseFloat(c[2]);if(isNaN(d)||isNaN(e)||isNaN(g)){return false}else{return{center:new b.Location(e,g),zoom:d}}}else{return false}},formatHash:function(f){var c=f.getCenter(),e=f.getZoom(),d=Math.max(0,Math.ceil(Math.log(e)/Math.LN2));return"#"+[e,c.lat.toFixed(d),c.lon.toFixed(d)].join("/")},init:function(c){this.map=c;this.map.addCallback("drawn",this.onMapMove);this.lastHash=null;this.onHashChange();if(!this.isListening){this.startListening()}},remove:function(){this.map=null;if(this.isListening){this.stopListening()}},onMapMove:function(d){if(this.movingMap||this.map.zoom===0){return false}var c=this.formatHash(d);if(this.lastHash!=c){location.replace(c);this.lastHash=c}},movingMap:false,update:function(){var e=location.hash;if(e===this.lastHash){return}var d=e.substr(1),c=this.parseHash(d);if(c){this.movingMap=true;this.map.setCenterZoom(c.center,c.zoom);this.movingMap=false}else{this.onMapMove(this.map)}},changeDefer:100,changeTimeout:null,onHashChange:function(){if(!this.changeTimeout){var c=this;this.changeTimeout=setTimeout(function(){c.update();c.changeTimeout=null},this.changeDefer)}},isListening:false,hashChangeInterval:null,startListening:function(){if(a){window.addEventListener("hashchange",this.onHashChange,false)}else{clearInterval(this.hashChangeInterval);this.hashChangeInterval=setInterval(this.onHashChange,50)}this.isListening=true},stopListening:function(){if(a){window.removeEventListener("hashchange",this.onHashChange)}else{clearInterval(this.hashChangeInterval)}this.isListening=false}};b.TouchHandler=function(d,c){if(d){this.init(d,c)}};b.TouchHandler.prototype={maxTapTime:250,maxTapDistance:30,maxDoubleTapDelay:350,locations:{},taps:[],wasPinching:false,lastPinchCenter:null,init:function(d,c){this.map=d;c=c||{};if(!this.isTouchable()){return false}this._touchStartMachine=b.bind(this.touchStartMachine,this);this._touchMoveMachine=b.bind(this.touchMoveMachine,this);this._touchEndMachine=b.bind(this.touchEndMachine,this);b.addEvent(d.parent,"touchstart",this._touchStartMachine);b.addEvent(d.parent,"touchmove",this._touchMoveMachine);b.addEvent(d.parent,"touchend",this._touchEndMachine);this.options={};this.options.snapToZoom=c.snapToZoom||true},isTouchable:function(){var c=document.createElement("div");c.setAttribute("ongesturestart","return;");return(typeof c.ongesturestart==="function")},remove:function(){if(!this.isTouchable()){return false}b.removeEvent(this.map.parent,"touchstart",this._touchStartMachine);b.removeEvent(this.map.parent,"touchmove",this._touchMoveMachine);b.removeEvent(this.map.parent,"touchend",this._touchEndMachine)},updateTouches:function(g){for(var f=0;fthis.maxTapDistance){}else{if(g>this.maxTapTime){o.end=d;o.duration=g;this.onHold(o)}else{o.time=d;this.onTap(o)}}}var n={};for(var h=0;h=0;e--){var d=c[e];if(!(d.id in g)){this.loadingBay.removeChild(d);this.openRequestCount--;d.src=d.coord=d.onload=d.onerror=null}}for(var l in this.requestsById){if(!(l in g)){if(this.requestsById.hasOwnProperty(l)){var k=this.requestsById[l];delete this.requestsById[l];if(k!==null){k=k.id=k.coord=k.url=null}}}}},hasRequest:function(c){return(c in this.requestsById)},requestTile:function(f,e,c){if(!(f in this.requestsById)){var d={id:f,coord:e.copy(),url:c};this.requestsById[f]=d;if(c){this.requestQueue.push(d)}}},getProcessQueue:function(){if(!this._processQueue){var c=this;this._processQueue=function(){c.processQueue()}}return this._processQueue},processQueue:function(e){if(e&&this.requestQueue.length>8){this.requestQueue.sort(e)}while(this.openRequestCount0){var d=this.requestQueue.pop();if(d){this.openRequestCount++;var c=document.createElement("img");c.id=d.id;c.style.position="absolute";c.coord=d.coord;this.loadingBay.appendChild(c);c.onload=c.onerror=this.getLoadComplete();c.src=d.url;d=d.id=d.coord=d.url=null}}},_loadComplete:null,getLoadComplete:function(){if(!this._loadComplete){var c=this;this._loadComplete=function(f){f=f||window.event;var d=f.srcElement||f.target;d.onload=d.onerror=null;c.loadingBay.removeChild(d);c.openRequestCount--;delete c.requestsById[d.id];if(f.type==="load"&&(d.complete||(d.readyState&&d.readyState=="complete"))){c.dispatchCallback("requestcomplete",d)}else{c.dispatchCallback("requesterror",d.src);d.src=null}setTimeout(c.getProcessQueue(),0)}}return this._loadComplete}};b.Layer=function(d,c){this.parent=c||document.createElement("div");this.parent.style.cssText="position: absolute; top: 0px; left: 0px; width: 100%; height: 100%; margin: 0; padding: 0; z-index: 0";this.levels={};this.requestManager=new b.RequestManager();this.requestManager.addCallback("requestcomplete",this.getTileComplete());if(d){this.setProvider(d)}};b.Layer.prototype={map:null,parent:null,tiles:null,levels:null,requestManager:null,tileCacheSize:null,maxTileCacheSize:null,provider:null,recentTiles:null,recentTilesById:{},enablePyramidLoading:false,_tileComplete:null,getTileComplete:function(){if(!this._tileComplete){var c=this;this._tileComplete=function(e,f){c.tiles[f.id]=f;c.tileCacheSize++;var d={id:f.id,lastTouchedTime:new Date().getTime()};c.recentTilesById[f.id]=d;c.recentTiles.push(d);c.positionTile(f)}}return this._tileComplete},draw:function(){var o=Math.round(this.map.coordinate.zoom);var n=this.map.pointCoordinate(new b.Point(0,0)).zoomTo(o).container();var i=this.map.pointCoordinate(this.map.dimensions).zoomTo(o).container().right().down();var k={};var m=this.createOrGetLevel(n.zoom);var h=n.copy();for(h.column=n.column;h.column<=i.column;h.column++){for(h.row=n.row;h.row<=i.row;h.row++){var d=this.inventoryVisibleTile(m,h);while(d.length){k[d.pop()]=true}}}for(var f in this.levels){if(this.levels.hasOwnProperty(f)){var p=parseInt(f,10);if(p>=n.zoom-5&&p0){d.style.display="block";g=Math.pow(2,this.map.coordinate.zoom-m);l=l.zoomTo(m)}else{d.style.display="none"}var j=this.map.tileSize.x*g;var h=this.map.tileSize.y*g;var c=new b.Point(this.map.dimensions.x/2,this.map.dimensions.y/2);var k=this.tileElementsInLevel(d);while(k.length){var i=k.pop();if(!f[i.id]){this.provider.releaseTile(i.coord);this.requestManager.clearRequest(i.coord.toKey());d.removeChild(i)}else{this.recentTilesById[i.id].lastTouchedTime=e}}b.moveElement(d,{x:-(l.column*256)+c.x,y:-(l.row*256)+c.y,scale:g})},createOrGetLevel:function(c){if(c in this.levels){return this.levels[c]}var d=document.createElement("div");d.id=this.parent.id+"-zoom-"+c;d.style.cssText=this.parent.style.cssText;d.style.zIndex=c;this.parent.appendChild(d);this.levels[c]=d;return d},addTileImage:function(d,e,c){this.requestManager.requestTile(d,e,c)},addTileElement:function(e,f,d){d.id=e;d.coord=f.copy();this.tiles[e]=d;this.tileCacheSize++;var c={id:e,lastTouchedTime:new Date().getTime()};this.recentTilesById[e]=c;this.recentTiles.push(c);this.positionTile(d)},positionTile:function(g){var f=this.map.coordinate.zoomTo(g.coord.zoom);g.style.cssText="position:absolute;-webkit-user-select: none;-webkit-user-drag: none;-moz-user-drag: none;";g.ondragstart=function(){return false};var d=g.coord.column*this.map.tileSize.x;var c=g.coord.row*this.map.tileSize.y;b.moveElement(g,{x:Math.round(d),y:Math.round(c),width:this.map.tileSize.x,height:this.map.tileSize.y});var e=this.levels[g.coord.zoom];e.appendChild(g);g.className="map-tile-loaded";if(Math.round(this.map.coordinate.zoom)==g.coord.zoom){e.style.display="block"}this.requestRedraw()},_redrawTimer:undefined,requestRedraw:function(){if(!this._redrawTimer){this._redrawTimer=setTimeout(this.getRedraw(),1000)}},_redraw:null,getRedraw:function(){if(!this._redraw){var c=this;this._redraw=function(){c.draw();c._redrawTimer=0}}return this._redraw},numTilesOnScreen:function(){var c=0;for(var d in this.levels){if(this.levels.hasOwnProperty(d)){var e=this.levels[d];c+=this.tileElementsInLevel(e).length}}return c},checkCache:function(){var e=Math.max(this.numTilesOnScreen(),this.maxTileCacheSize);if(this.tileCacheSize>e){this.recentTiles.sort(function(h,g){return g.lastTouchedTimeh.lastTouchedTime?1:0})}while(this.recentTiles.length&&this.tileCacheSize>e){var d=this.recentTiles.pop();var c=new Date().getTime();delete this.recentTilesById[d.id];var f=this.tiles[d.id];if(f.parentNode){alert("Gah: trying to removing cached tile even though it's still in the DOM")}else{delete this.tiles[d.id];this.tileCacheSize--}}},setProvider:function(d){var e=(this.provider===null);if(!e){this.requestManager.clear();for(var c in this.levels){if(this.levels.hasOwnProperty(c)){var f=this.levels[c];while(f.firstChild){this.provider.releaseTile(f.firstChild.coord);f.removeChild(f.firstChild)}}}}this.tiles={};this.tileCacheSize=0;this.maxTileCacheSize=64;this.recentTilesById={};this.recentTiles=[];this.provider=d;if(!e){this.draw()}},getCenterDistanceCompare:function(){var c=this.map.coordinate.zoomTo(Math.round(this.map.coordinate.zoom));return function(f,e){if(f&&e){var h=f.coord;var g=e.coord;if(h.zoom==g.zoom){var d=Math.abs(c.row-h.row-0.5)+Math.abs(c.column-h.column-0.5);var i=Math.abs(c.row-g.row-0.5)+Math.abs(c.column-g.column-0.5);return di?-1:0}else{return h.zoomg.zoom?-1:0}}return f?1:e?-1:0}},destroy:function(){this.requestManager.clear();this.requestManager.removeCallback("requestcomplete",this.getTileComplete());this.provider=null;if(this.parent.parentNode){this.parent.parentNode.removeChild(this.parent)}this.map=null}};b.Map=function(g,f,h,k){if(typeof g=="string"){g=document.getElementById(g);if(!g){throw"The ID provided to modest maps could not be found."}}this.parent=g;this.parent.style.padding="0";this.parent.style.overflow="hidden";var c=b.getStyle(this.parent,"position");if(c!="relative"&&c!="absolute"){this.parent.style.position="relative"}this.layers=[];if(!(f instanceof Array)){f=[f]}for(var e=0;e=this.layers.length){throw new Error("invalid index in setLayerAt(): "+c)}if(this.layers[c]!=d){if(cthis.layers.length){throw new Error("invalid index in insertLayerAt(): "+d)}if(d==this.layers.length){this.layers.push(e);this.parent.appendChild(e.parent)}else{var c=this.layers[d];this.parent.insertBefore(e.parent,c.parent);this.layers.splice(d,0,e)}e.map=this;b.getFrame(this.getRedraw());return this},removeLayerAt:function(d){if(d<0||d>=this.layers.length){throw new Error("invalid index in removeLayer(): "+d)}var c=this.layers[d];this.layers.splice(d,1);c.destroy();return this},swapLayersAt:function(d,c){if(d<0||d>=this.layers.length||c<0||c>=this.layers.length){throw new Error("invalid index in swapLayersAt(): "+index)}var g=this.layers[d],e=this.layers[c],f=document.createElement("div");this.parent.replaceChild(f,e.parent);this.parent.replaceChild(e.parent,g.parent);this.parent.replaceChild(g.parent,f);this.layers[d]=e;this.layers[c]=g;return this},enforceZoomLimits:function(f){var d=this.coordLimits;if(d){var e=d[0].zoom;var c=d[1].zoom;if(f.zoomc){f=f.zoomTo(c)}}}return f},enforcePanLimits:function(g){if(this.coordLimits){g=g.copy();var e=this.coordLimits[0].zoomTo(g.zoom);var c=this.coordLimits[1].zoomTo(g.zoom);var d=this.pointCoordinate(new b.Point(0,0)).zoomTo(g.zoom);var f=this.pointCoordinate(this.dimensions).zoomTo(g.zoom);if(c.row-e.rowc.row){g.row-=f.row-c.row}}}if(c.column-e.columnc.column){g.column-=f.column-c.column}}}}return g},enforceLimits:function(c){return this.enforcePanLimits(this.enforceZoomLimits(c))},draw:function(){this.coordinate=this.enforceLimits(this.coordinate);if(this.dimensions.x<=0||this.dimensions.y<=0){if(this.autoSize){var c=this.parent.offsetWidth,e=this.parent.offsetHeight;this.dimensions=new b.Point(c,e);if(c<=0||e<=0){return}}else{return}}for(var d=0;d= BR.row || coord.column < TL.column || coord.column >= BR.column) { + return null; + } + return coord; + }; */ + + //document.getElementById('title').innerHTML = tilejson.name; + //document.getElementById('description').innerHTML = tilejson.description; + //document.getElementById('attribution').innerHTML = tilejson.attribution; + + + /* ----- scrolling function --------- + + var ea = easey().map(m).easing('easeInOut'); + + scrolly = document.getElementById('teste'); + + function update() { + var pos = scrolly.scrollTop / 200; + + ea.from(positions[Math.floor(pos)]) + .to(positions[Math.ceil(pos)]) + .t(pos - Math.floor(pos)); + } + + scrolly.addEventListener('scroll', update, false); + */ + +}); \ No newline at end of file diff --git a/js/site2.js b/js/site2.js new file mode 100644 index 0000000..9d90762 --- /dev/null +++ b/js/site2.js @@ -0,0 +1,418 @@ +/* SITE SETUP */ + + +/* + MAP + +*/ + +var m; +var layer; +var tilejson = { + tilejson: '1.0.0', + scheme: 'xyz', + tiles: [ + 'http://maps.cardume.art.br/v2/eduamazonia/{z}/{x}/{y}.png' + ], + grids: [ + 'http://maps.cardume.art.br/v2/eduamazonia/{z}/{x}/{y}.grid.json' + ], + formatter: function(options, data) { + return data + } +} + +jQuery(document).ready(function() { + layer = new wax.mm.connector(tilejson); + m = new MM.Map('map', new wax.mm.connector(tilejson), null, [ + easey.DragHandler(), + easey.TouchHandler(), + easey.MouseWheelHandler(), + easey.DoubleClickHandler() + ]); + m.setCenterZoom(new MM.Location(-48,-7),4); + + wax.mm.zoomer(m).appendTo(m.parent); + wax.mm.interaction() + .map(m) + .tilejson(tilejson) + .on(wax.tooltip().animate(true).parent(m.parent).events()); + //wax.mm.legend(m, tilejson).appendTo(m.parent); + + var minZoom = 4; + var maxZoom = 8; + var topLeft = new MM.Location(9, -75); + var bottomRight = new MM.Location(-28, -4); + + // -76.1133,-17.8115,-38.1445,6.3153 + + m.setZoomRange(minZoom,maxZoom); + + m.coordLimits = [ + m.locationCoordinate(topLeft).zoomTo(minZoom), + m.locationCoordinate(bottomRight).zoomTo(maxZoom) + ]; + + layer.tileLimits = [ + m.locationCoordinate(topLeft).zoomTo(minZoom), + m.locationCoordinate(bottomRight).zoomTo(maxZoom) + ]; + +}); + +// filter map + + + +var filter_map; +var tilejson_filters = { + tilejson: '1.0.0', + scheme: 'xyz', + tiles: [ + 'http://maps.cardume.art.br/v2/eduamazonia_marcador/{z}/{x}/{y}.png' + ], + formatter: function(options, data) { + return data + } +} +var filter_map_ea; + + +jQuery(document).ready(function() { + filter_map = new MM.Map('filter_map', new wax.mm.connector(tilejson_filters), null); + filter_map.setCenterZoom(new MM.Location(-56,-5),4); + + wax.mm.zoomer(filter_map).appendTo(filter_map.parent); + wax.mm.interaction() + .map(filter_map) + .tilejson(tilejson); + + filter_map_ea = easey().map(filter_map).easing('easeInOut'); + + var minZoom = 4; + var maxZoom = 8; + var topLeft = new MM.Location(7, -77); + var bottomRight = new MM.Location(-18, -43); + + filter_map.setZoomRange(minZoom,maxZoom); + + filter_map.coordLimits = [ + filter_map.locationCoordinate(topLeft).zoomTo(minZoom), + filter_map.locationCoordinate(bottomRight).zoomTo(maxZoom) + ]; + + layer.tileLimits = [ + filter_map.locationCoordinate(topLeft).zoomTo(minZoom), + filter_map.locationCoordinate(bottomRight).zoomTo(maxZoom) + ]; + +}); + +function markerFactory(feature) { + var d = document.createElement('div'); + d.className = 'cidade-marker'; + // $(d).data('cidade', feature.id); + $(d) + .attr('data-cidade', feature.id) + .data('lat', feature.properties.geo_latitude) + .data('lon', feature.properties.geo_longitude) + .append('' + feature.id + ' - ' + feature.properties.estado + ''); + $('option[value="' + feature.id + '"]') + .data('lat', feature.properties.geo_latitude) + .data('lon', feature.properties.geo_longitude); + return d; +} + +$('.cidade-marker').live('click', function() { + var lat = $(this).data('lat'); + var lon = $(this).data('lon'); + var markerId = $(this).data('cidade'); + navigateFilter(lat, lon, markerId); + + $('select.cidade option').attr('selected', false); + $('select.cidade option[value="' + markerId + '"]').attr('selected', true); + $('select.cidade').chosen().trigger('liszt:updated').change(); + +}); + +function navigateFilter(lat, lon, markerId) { + easey().map(filter_map) + .to(filter_map.locationCoordinate({lat: lat, lon: lon}) + .zoomTo(8)) + .run(2000); + + $('.cidade-marker').removeClass('active'); + if(markerId) + $('.cidade-marker[data-cidade="'+markerId+'"]').addClass('active'); +} + + +/* + + GRAPHS + +*/ + +// load graph core +google.load('visualization', '1', {packages: ['corechart']}); + +// declare data vars +var irregularidadesData; +var tiposData; +var programasData; +var cidadesData; + +var selectedFilters = {}; + +$(document).ready(function() { + // prepare general data + $.getJSON('constatacoes.json.php?data=geral', function(data) { + irregularidadesData = data; + }); + + // prepare tipos data + $.getJSON('constatacoes.json.php?data=tipo', function(data) { + tiposData = data; + + // setup dropdown + $.each(tiposData, function(i, tipo) { + $('select.tipo').append(''); + }); + jQuery('select.tipo').chosen().trigger('liszt:updated'); + }); + + // prepare programas data + $.getJSON('constatacoes.json.php?data=programa', function(data) { + programasData = data; + + // setup dropdown + $.each(programasData, function(i, programa) { + $('select.programa').append(''); + }); + jQuery('select.programa').chosen().trigger('liszt:updated'); + }); + + // prepare cidades data + $.getJSON('constatacoes.json.php?data=cidade', function(data) { + cidadesData = data; + + // setup dropdown + $.each(cidadesData, function(i, cidade) { + $('select.cidade').append(''); + }); + jQuery('select.cidade').chosen().trigger('liszt:updated'); + + // setup marker layer + var pointLayer = mmg().factory(markerFactory).url('cidades.geojson', function(feat, l) { + mmg_interaction(l); + }); + filter_map.addLayer(pointLayer); + }); + + $('select').chosen({allow_single_deselect:true}).change(function() { + + $('select.filter').each(function() { + var filterValue = $(this).find('option:selected').val(); + var filterKey = $(this).data('type'); + selectedFilters[filterKey] = filterValue; + }); + + // navigate map if city + if($(this).hasClass('cidade')) { + var markerId = $(this).find('option:selected').val(); + var lat = $(this).find('option:selected').data('lat'); + var lon = $(this).find('option:selected').data('lon'); + navigateFilter(lat, lon, markerId); + } + + theMagic(selectedFilters); + + }); + +}); + +function theMagic(selectedFilters) { + var graphsContainer = $('#graphs'); + graphsContainer.empty(); + if(selectedFilters.cidade && !selectedFilters.tipo && !selectedFilters.programa) { + /*--CIDADE + gráfico coluna + - programa + - tipo + */ + graphsContainer.append('
        '); + drawCidade(selectedFilters.cidade, 'graph01'); + } else if(!selectedFilters.cidade && selectedFilters.tipo && !selectedFilters.programa) { + /*--TIPO + gráfico pizza + - programa + gráfico barra + - cidade + */ + graphsContainer.append('
        '); + drawPieChart('Teste', selectedFilters, 'programa', 'graph01'); + } else if(!selectedFilters.cidade && !selectedFilters.tipo && selectedFilters.programa) { + /*--PROGRAMA + gráfico pizza + - tipo + gráfico barra + - cidade + */ + graphsContainer.append('
        '); + drawPieChart('Teste 2', selectedFilters, 'tipo', 'graph01'); + } else if(selectedFilters.cidade && selectedFilters.tipo && !selectedFilters.programa) { + /*--CIDADE+TIPO + gráfico pizza + - programa + gráfico pizza (total) + - programa + */ + graphsContainer.append('
        '); + drawPieChart('Teste 3', selectedFilters, 'programa', 'graph01'); + } else if(!selectedFilters.cidade && selectedFilters.tipo && selectedFilters.programa) { + /*--TIPO+PROGRAMA + gráfico barra + - cidade + */ + } else if(selectedFilters.cidade && !selectedFilters.tipo && selectedFilters.programa) { + /*--CIDADE+PROGRAMA + gráfico pizza + - tipo + gráfico pizza (total) + - tipo + */ + graphsContainer.append('
        '); + drawPieChart('Filtro', selectedFilters, 'tipo', 'graph01'); + selectedFilters.cidade = ''; + drawPieChart('Total', selectedFilters, 'tipo', 'graph02'); + } else if(selectedFilters.cidade && selectedFilters.tipo && selectedFilters.programa) { + /*--CIDADE+TIPO+PROGRAMA + só lista + */ + } +} + +function getIrregularidadesCount(filters) { + var data = jLinq.from(irregularidadesData); + jQuery.each(filters, function(key, value) { + data = data.starts(key, value); + }); + return data.count(); +} + +function getConstatacoes(filters) { + var data = constatacoesData; + jQuery.each(filters, function(key, value) { + data = data.starts(key, value); + }); + var count = data.count(); + var results = data.select(); + + var output = { + filters: filters, + count: count, + results: results + }; + return output; +} + +function getCidadeGraphData(cidade) { + var data = []; + data[0] = []; + + // setup header + data[0][0] = 'Programa'; + jQuery.each(tiposData, function(index, tipoData) { + data[0][index+1] = tipoData.tipo; + }); + + // rows + jQuery.each(programasData, function(programaIndex, programaData) { + data[programaIndex+1] = []; + data[programaIndex+1][0] = programaData.programa_desc; + jQuery.each(tiposData, function(tipoIndex, tipoData) { + data[programaIndex+1][tipoIndex+1] = getIrregularidadesCount({ + 'programa': programaData.programa, + 'tipo': tipoData.tipo, + 'cidade': cidade + }); + }); + }); + + return data; +} + +function getGraphData(filters, output) { + var data = []; + data[0] = []; + if(output == 'tipo') { + data[0][0] = 'Tipo'; + var outputData = tiposData; + } else if(output == 'programa') { + data[0][0] = 'Programa'; + var outputData = programasData; + } + if(output == 'cidade') { + data[0][0] = 'Cidade'; + var outputData = cidadesData; + } + data[0][1] = 'Irregularidades'; + jQuery.each(outputData, function(i, outData) { + data[i+1] = []; + data[i+1][0] = outData[output]; + filters[output] = outData[output]; + data[i+1][1] = getIrregularidadesCount(filters); + }); + return data; +} + +function drawPieChart(title, filters, output, containerId) { + var wrapper = new google.visualization.ChartWrapper({ + chartType: 'PieChart', + dataTable: getGraphData(filters, output), + options: { + title: title, + width: 473, + height: 400, + backgroundColor: 'transparent' + }, + containerId: containerId + }); + wrapper.draw(); + return; +} + +function drawCidade(cidade, containerId) { + var wrapper = new google.visualization.ChartWrapper({ + chartType: 'ComboChart', + dataTable: getCidadeGraphData(cidade), + options: { + title:'Irregularidades na cidade', + width:473, + height:400, + backgroundColor: 'transparent', + vAxis: {title: 'Irregularidades'}, + hAxis: {title: 'Programa'}, + seriesType: 'bars' + }, + containerId: containerId + }); + wrapper.draw(); + return; +} + +function drawBarChart(title, filters, output) { + var wrapper = new google.visualization.ChartWrapper({ + chartType: 'BarChart', + dataTable: getSimpleGraphData(filters, output), + options: { + title: title, + width: 600, + height: 1500, + backgroundColor: 'transparent', + colors: ['#f00'] + }, + containerId: 'visualization' + }); + wrapper.draw(); +} \ No newline at end of file diff --git a/js/wax.mm.js b/js/wax.mm.js new file mode 100644 index 0000000..a0085dc --- /dev/null +++ b/js/wax.mm.js @@ -0,0 +1,4081 @@ +/* wax - 6.3.0 - v6.0.4-23-gdcc8490 */ + + +!function (name, context, definition) { + if (typeof module !== 'undefined') module.exports = definition(name, context); + else if (typeof define === 'function' && typeof define.amd === 'object') define(definition); + else context[name] = definition(name, context); +}('bean', this, function (name, context) { + var win = window + , old = context[name] + , overOut = /over|out/ + , namespaceRegex = /[^\.]*(?=\..*)\.|.*/ + , nameRegex = /\..*/ + , addEvent = 'addEventListener' + , attachEvent = 'attachEvent' + , removeEvent = 'removeEventListener' + , detachEvent = 'detachEvent' + , doc = document || {} + , root = doc.documentElement || {} + , W3C_MODEL = root[addEvent] + , eventSupport = W3C_MODEL ? addEvent : attachEvent + , slice = Array.prototype.slice + , mouseTypeRegex = /click|mouse(?!(.*wheel|scroll))|menu|drag|drop/i + , mouseWheelTypeRegex = /mouse.*(wheel|scroll)/i + , textTypeRegex = /^text/i + , touchTypeRegex = /^touch|^gesture/i + , ONE = { one: 1 } // singleton for quick matching making add() do one() + + , nativeEvents = (function (hash, events, i) { + for (i = 0; i < events.length; i++) + hash[events[i]] = 1 + return hash + })({}, ( + 'click dblclick mouseup mousedown contextmenu ' + // mouse buttons + 'mousewheel mousemultiwheel DOMMouseScroll ' + // mouse wheel + 'mouseover mouseout mousemove selectstart selectend ' + // mouse movement + 'keydown keypress keyup ' + // keyboard + 'orientationchange ' + // mobile + 'focus blur change reset select submit ' + // form elements + 'load unload beforeunload resize move DOMContentLoaded readystatechange ' + // window + 'error abort scroll ' + // misc + (W3C_MODEL ? // element.fireEvent('onXYZ'... is not forgiving if we try to fire an event + // that doesn't actually exist, so make sure we only do these on newer browsers + 'show ' + // mouse buttons + 'input invalid ' + // form elements + 'touchstart touchmove touchend touchcancel ' + // touch + 'gesturestart gesturechange gestureend ' + // gesture + 'message readystatechange pageshow pagehide popstate ' + // window + 'hashchange offline online ' + // window + 'afterprint beforeprint ' + // printing + 'dragstart dragenter dragover dragleave drag drop dragend ' + // dnd + 'loadstart progress suspend emptied stalled loadmetadata ' + // media + 'loadeddata canplay canplaythrough playing waiting seeking ' + // media + 'seeked ended durationchange timeupdate play pause ratechange ' + // media + 'volumechange cuechange ' + // media + 'checking noupdate downloading cached updateready obsolete ' + // appcache + '' : '') + ).split(' ') + ) + + , customEvents = (function () { + function isDescendant(parent, node) { + while ((node = node.parentNode) !== null) { + if (node === parent) return true + } + return false + } + + function check(event) { + var related = event.relatedTarget + if (!related) return related === null + return (related !== this && related.prefix !== 'xul' && !/document/.test(this.toString()) && !isDescendant(this, related)) + } + + return { + mouseenter: { base: 'mouseover', condition: check } + , mouseleave: { base: 'mouseout', condition: check } + , mousewheel: { base: /Firefox/.test(navigator.userAgent) ? 'DOMMouseScroll' : 'mousewheel' } + } + })() + + , fixEvent = (function () { + var commonProps = 'altKey attrChange attrName bubbles cancelable ctrlKey currentTarget detail eventPhase getModifierState isTrusted metaKey relatedNode relatedTarget shiftKey srcElement target timeStamp type view which'.split(' ') + , mouseProps = commonProps.concat('button buttons clientX clientY dataTransfer fromElement offsetX offsetY pageX pageY screenX screenY toElement'.split(' ')) + , mouseWheelProps = mouseProps.concat('wheelDelta wheelDeltaX wheelDeltaY wheelDeltaZ axis'.split(' ')) // 'axis' is FF specific + , keyProps = commonProps.concat('char charCode key keyCode keyIdentifier keyLocation'.split(' ')) + , textProps = commonProps.concat(['data']) + , touchProps = commonProps.concat('touches targetTouches changedTouches scale rotation'.split(' ')) + , preventDefault = 'preventDefault' + , createPreventDefault = function (event) { + return function () { + if (event[preventDefault]) + event[preventDefault]() + else + event.returnValue = false + } + } + , stopPropagation = 'stopPropagation' + , createStopPropagation = function (event) { + return function () { + if (event[stopPropagation]) + event[stopPropagation]() + else + event.cancelBubble = true + } + } + , createStop = function (synEvent) { + return function () { + synEvent[preventDefault]() + synEvent[stopPropagation]() + synEvent.stopped = true + } + } + , copyProps = function (event, result, props) { + var i, p + for (i = props.length; i--;) { + p = props[i] + if (!(p in result) && p in event) result[p] = event[p] + } + } + + return function (event, isNative) { + var result = { originalEvent: event, isNative: isNative } + if (!event) + return result + + var props + , type = event.type + , target = event.target || event.srcElement + + result[preventDefault] = createPreventDefault(event) + result[stopPropagation] = createStopPropagation(event) + result.stop = createStop(result) + result.target = target && target.nodeType === 3 ? target.parentNode : target + + if (isNative) { // we only need basic augmentation on custom events, the rest is too expensive + if (type.indexOf('key') !== -1) { + props = keyProps + result.keyCode = event.which || event.keyCode + } else if (mouseTypeRegex.test(type)) { + props = mouseProps + result.rightClick = event.which === 3 || event.button === 2 + result.pos = { x: 0, y: 0 } + if (event.pageX || event.pageY) { + result.clientX = event.pageX + result.clientY = event.pageY + } else if (event.clientX || event.clientY) { + result.clientX = event.clientX + doc.body.scrollLeft + root.scrollLeft + result.clientY = event.clientY + doc.body.scrollTop + root.scrollTop + } + if (overOut.test(type)) + result.relatedTarget = event.relatedTarget || event[(type === 'mouseover' ? 'from' : 'to') + 'Element'] + } else if (touchTypeRegex.test(type)) { + props = touchProps + } else if (mouseWheelTypeRegex.test(type)) { + props = mouseWheelProps + } else if (textTypeRegex.test(type)) { + props = textProps + } + copyProps(event, result, props || commonProps) + } + return result + } + })() + + // if we're in old IE we can't do onpropertychange on doc or win so we use doc.documentElement for both + , targetElement = function (element, isNative) { + return !W3C_MODEL && !isNative && (element === doc || element === win) ? root : element + } + + // we use one of these per listener, of any type + , RegEntry = (function () { + function entry(element, type, handler, original, namespaces) { + this.element = element + this.type = type + this.handler = handler + this.original = original + this.namespaces = namespaces + this.custom = customEvents[type] + this.isNative = nativeEvents[type] && element[eventSupport] + this.eventType = W3C_MODEL || this.isNative ? type : 'propertychange' + this.customType = !W3C_MODEL && !this.isNative && type + this.target = targetElement(element, this.isNative) + this.eventSupport = this.target[eventSupport] + } + + entry.prototype = { + // given a list of namespaces, is our entry in any of them? + inNamespaces: function (checkNamespaces) { + var i, j + if (!checkNamespaces) + return true + if (!this.namespaces) + return false + for (i = checkNamespaces.length; i--;) { + for (j = this.namespaces.length; j--;) { + if (checkNamespaces[i] === this.namespaces[j]) + return true + } + } + return false + } + + // match by element, original fn (opt), handler fn (opt) + , matches: function (checkElement, checkOriginal, checkHandler) { + return this.element === checkElement && + (!checkOriginal || this.original === checkOriginal) && + (!checkHandler || this.handler === checkHandler) + } + } + + return entry + })() + + , registry = (function () { + // our map stores arrays by event type, just because it's better than storing + // everything in a single array. uses '$' as a prefix for the keys for safety + var map = {} + + // generic functional search of our registry for matching listeners, + // `fn` returns false to break out of the loop + , forAll = function (element, type, original, handler, fn) { + if (!type || type === '*') { + // search the whole registry + for (var t in map) { + if (t.charAt(0) === '$') + forAll(element, t.substr(1), original, handler, fn) + } + } else { + var i = 0, l, list = map['$' + type], all = element === '*' + if (!list) + return + for (l = list.length; i < l; i++) { + if (all || list[i].matches(element, original, handler)) + if (!fn(list[i], list, i, type)) + return + } + } + } + + , has = function (element, type, original) { + // we're not using forAll here simply because it's a bit slower and this + // needs to be fast + var i, list = map['$' + type] + if (list) { + for (i = list.length; i--;) { + if (list[i].matches(element, original, null)) + return true + } + } + return false + } + + , get = function (element, type, original) { + var entries = [] + forAll(element, type, original, null, function (entry) { return entries.push(entry) }) + return entries + } + + , put = function (entry) { + (map['$' + entry.type] || (map['$' + entry.type] = [])).push(entry) + return entry + } + + , del = function (entry) { + forAll(entry.element, entry.type, null, entry.handler, function (entry, list, i) { + list.splice(i, 1) + if (list.length === 0) + delete map['$' + entry.type] + return false + }) + } + + // dump all entries, used for onunload + , entries = function () { + var t, entries = [] + for (t in map) { + if (t.charAt(0) === '$') + entries = entries.concat(map[t]) + } + return entries + } + + return { has: has, get: get, put: put, del: del, entries: entries } + })() + + // add and remove listeners to DOM elements + , listener = W3C_MODEL ? function (element, type, fn, add) { + element[add ? addEvent : removeEvent](type, fn, false) + } : function (element, type, fn, add, custom) { + if (custom && add && element['_on' + custom] === null) + element['_on' + custom] = 0 + element[add ? attachEvent : detachEvent]('on' + type, fn) + } + + , nativeHandler = function (element, fn, args) { + return function (event) { + event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, true) + return fn.apply(element, [event].concat(args)) + } + } + + , customHandler = function (element, fn, type, condition, args, isNative) { + return function (event) { + if (condition ? condition.apply(this, arguments) : W3C_MODEL ? true : event && event.propertyName === '_on' + type || !event) { + if (event) + event = fixEvent(event || ((this.ownerDocument || this.document || this).parentWindow || win).event, isNative) + fn.apply(element, event && (!args || args.length === 0) ? arguments : slice.call(arguments, event ? 0 : 1).concat(args)) + } + } + } + + , once = function (rm, element, type, fn, originalFn) { + // wrap the handler in a handler that does a remove as well + return function () { + rm(element, type, originalFn) + fn.apply(this, arguments) + } + } + + , removeListener = function (element, orgType, handler, namespaces) { + var i, l, entry + , type = (orgType && orgType.replace(nameRegex, '')) + , handlers = registry.get(element, type, handler) + + for (i = 0, l = handlers.length; i < l; i++) { + if (handlers[i].inNamespaces(namespaces)) { + if ((entry = handlers[i]).eventSupport) + listener(entry.target, entry.eventType, entry.handler, false, entry.type) + // TODO: this is problematic, we have a registry.get() and registry.del() that + // both do registry searches so we waste cycles doing this. Needs to be rolled into + // a single registry.forAll(fn) that removes while finding, but the catch is that + // we'll be splicing the arrays that we're iterating over. Needs extra tests to + // make sure we don't screw it up. @rvagg + registry.del(entry) + } + } + } + + , addListener = function (element, orgType, fn, originalFn, args) { + var entry + , type = orgType.replace(nameRegex, '') + , namespaces = orgType.replace(namespaceRegex, '').split('.') + + if (registry.has(element, type, fn)) + return element // no dupe + if (type === 'unload') + fn = once(removeListener, element, type, fn, originalFn) // self clean-up + if (customEvents[type]) { + if (customEvents[type].condition) + fn = customHandler(element, fn, type, customEvents[type].condition, true) + type = customEvents[type].base || type + } + entry = registry.put(new RegEntry(element, type, fn, originalFn, namespaces[0] && namespaces)) + entry.handler = entry.isNative ? + nativeHandler(element, entry.handler, args) : + customHandler(element, entry.handler, type, false, args, false) + if (entry.eventSupport) + listener(entry.target, entry.eventType, entry.handler, true, entry.customType) + } + + , del = function (selector, fn, $) { + return function (e) { + var target, i, array = typeof selector === 'string' ? $(selector, this) : selector + for (target = e.target; target && target !== this; target = target.parentNode) { + for (i = array.length; i--;) { + if (array[i] === target) { + return fn.apply(target, arguments) + } + } + } + } + } + + , remove = function (element, typeSpec, fn) { + var k, m, type, namespaces, i + , rm = removeListener + , isString = typeSpec && typeof typeSpec === 'string' + + if (isString && typeSpec.indexOf(' ') > 0) { + // remove(el, 't1 t2 t3', fn) or remove(el, 't1 t2 t3') + typeSpec = typeSpec.split(' ') + for (i = typeSpec.length; i--;) + remove(element, typeSpec[i], fn) + return element + } + type = isString && typeSpec.replace(nameRegex, '') + if (type && customEvents[type]) + type = customEvents[type].type + if (!typeSpec || isString) { + // remove(el) or remove(el, t1.ns) or remove(el, .ns) or remove(el, .ns1.ns2.ns3) + if (namespaces = isString && typeSpec.replace(namespaceRegex, '')) + namespaces = namespaces.split('.') + rm(element, type, fn, namespaces) + } else if (typeof typeSpec === 'function') { + // remove(el, fn) + rm(element, null, typeSpec) + } else { + // remove(el, { t1: fn1, t2, fn2 }) + for (k in typeSpec) { + if (typeSpec.hasOwnProperty(k)) + remove(element, k, typeSpec[k]) + } + } + return element + } + + , add = function (element, events, fn, delfn, $) { + var type, types, i, args + , originalFn = fn + , isDel = fn && typeof fn === 'string' + + if (events && !fn && typeof events === 'object') { + for (type in events) { + if (events.hasOwnProperty(type)) + add.apply(this, [ element, type, events[type] ]) + } + } else { + args = arguments.length > 3 ? slice.call(arguments, 3) : [] + types = (isDel ? fn : events).split(' ') + isDel && (fn = del(events, (originalFn = delfn), $)) && (args = slice.call(args, 1)) + // special case for one() + this === ONE && (fn = once(remove, element, events, fn, originalFn)) + for (i = types.length; i--;) addListener(element, types[i], fn, originalFn, args) + } + return element + } + + , one = function () { + return add.apply(ONE, arguments) + } + + , fireListener = W3C_MODEL ? function (isNative, type, element) { + var evt = doc.createEvent(isNative ? 'HTMLEvents' : 'UIEvents') + evt[isNative ? 'initEvent' : 'initUIEvent'](type, true, true, win, 1) + element.dispatchEvent(evt) + } : function (isNative, type, element) { + element = targetElement(element, isNative) + // if not-native then we're using onpropertychange so we just increment a custom property + isNative ? element.fireEvent('on' + type, doc.createEventObject()) : element['_on' + type]++ + } + + , fire = function (element, type, args) { + var i, j, l, names, handlers + , types = type.split(' ') + + for (i = types.length; i--;) { + type = types[i].replace(nameRegex, '') + if (names = types[i].replace(namespaceRegex, '')) + names = names.split('.') + if (!names && !args && element[eventSupport]) { + fireListener(nativeEvents[type], type, element) + } else { + // non-native event, either because of a namespace, arguments or a non DOM element + // iterate over all listeners and manually 'fire' + handlers = registry.get(element, type) + args = [false].concat(args) + for (j = 0, l = handlers.length; j < l; j++) { + if (handlers[j].inNamespaces(names)) + handlers[j].handler.apply(element, args) + } + } + } + return element + } + + , clone = function (element, from, type) { + var i = 0 + , handlers = registry.get(from, type) + , l = handlers.length + + for (;i < l; i++) + handlers[i].original && add(element, handlers[i].type, handlers[i].original) + return element + } + + , bean = { + add: add + , one: one + , remove: remove + , clone: clone + , fire: fire + , noConflict: function () { + context[name] = old + return this + } + } + + if (win[attachEvent]) { + // for IE, clean up on unload to avoid leaks + var cleanup = function () { + var i, entries = registry.entries() + for (i in entries) { + if (entries[i].type && entries[i].type !== 'unload') + remove(entries[i].element, entries[i].type) + } + win[detachEvent]('onunload', cleanup) + win.CollectGarbage && win.CollectGarbage() + } + win[attachEvent]('onunload', cleanup) + } + + return bean +}) +// Copyright Google Inc. +// Licensed under the Apache Licence Version 2.0 +// Autogenerated at Tue Oct 11 13:36:46 EDT 2011 +// @provides html4 +var html4 = {}; +html4.atype = { + NONE: 0, + URI: 1, + URI_FRAGMENT: 11, + SCRIPT: 2, + STYLE: 3, + ID: 4, + IDREF: 5, + IDREFS: 6, + GLOBAL_NAME: 7, + LOCAL_NAME: 8, + CLASSES: 9, + FRAME_TARGET: 10 +}; +html4.ATTRIBS = { + '*::class': 9, + '*::dir': 0, + '*::id': 4, + '*::lang': 0, + '*::onclick': 2, + '*::ondblclick': 2, + '*::onkeydown': 2, + '*::onkeypress': 2, + '*::onkeyup': 2, + '*::onload': 2, + '*::onmousedown': 2, + '*::onmousemove': 2, + '*::onmouseout': 2, + '*::onmouseover': 2, + '*::onmouseup': 2, + '*::style': 3, + '*::title': 0, + 'a::accesskey': 0, + 'a::coords': 0, + 'a::href': 1, + 'a::hreflang': 0, + 'a::name': 7, + 'a::onblur': 2, + 'a::onfocus': 2, + 'a::rel': 0, + 'a::rev': 0, + 'a::shape': 0, + 'a::tabindex': 0, + 'a::target': 10, + 'a::type': 0, + 'area::accesskey': 0, + 'area::alt': 0, + 'area::coords': 0, + 'area::href': 1, + 'area::nohref': 0, + 'area::onblur': 2, + 'area::onfocus': 2, + 'area::shape': 0, + 'area::tabindex': 0, + 'area::target': 10, + 'bdo::dir': 0, + 'blockquote::cite': 1, + 'br::clear': 0, + 'button::accesskey': 0, + 'button::disabled': 0, + 'button::name': 8, + 'button::onblur': 2, + 'button::onfocus': 2, + 'button::tabindex': 0, + 'button::type': 0, + 'button::value': 0, + 'canvas::height': 0, + 'canvas::width': 0, + 'caption::align': 0, + 'col::align': 0, + 'col::char': 0, + 'col::charoff': 0, + 'col::span': 0, + 'col::valign': 0, + 'col::width': 0, + 'colgroup::align': 0, + 'colgroup::char': 0, + 'colgroup::charoff': 0, + 'colgroup::span': 0, + 'colgroup::valign': 0, + 'colgroup::width': 0, + 'del::cite': 1, + 'del::datetime': 0, + 'dir::compact': 0, + 'div::align': 0, + 'dl::compact': 0, + 'font::color': 0, + 'font::face': 0, + 'font::size': 0, + 'form::accept': 0, + 'form::action': 1, + 'form::autocomplete': 0, + 'form::enctype': 0, + 'form::method': 0, + 'form::name': 7, + 'form::onreset': 2, + 'form::onsubmit': 2, + 'form::target': 10, + 'h1::align': 0, + 'h2::align': 0, + 'h3::align': 0, + 'h4::align': 0, + 'h5::align': 0, + 'h6::align': 0, + 'hr::align': 0, + 'hr::noshade': 0, + 'hr::size': 0, + 'hr::width': 0, + 'iframe::align': 0, + 'iframe::frameborder': 0, + 'iframe::height': 0, + 'iframe::marginheight': 0, + 'iframe::marginwidth': 0, + 'iframe::width': 0, + 'img::align': 0, + 'img::alt': 0, + 'img::border': 0, + 'img::height': 0, + 'img::hspace': 0, + 'img::ismap': 0, + 'img::name': 7, + 'img::src': 1, + 'img::usemap': 11, + 'img::vspace': 0, + 'img::width': 0, + 'input::accept': 0, + 'input::accesskey': 0, + 'input::align': 0, + 'input::alt': 0, + 'input::autocomplete': 0, + 'input::checked': 0, + 'input::disabled': 0, + 'input::ismap': 0, + 'input::maxlength': 0, + 'input::name': 8, + 'input::onblur': 2, + 'input::onchange': 2, + 'input::onfocus': 2, + 'input::onselect': 2, + 'input::readonly': 0, + 'input::size': 0, + 'input::src': 1, + 'input::tabindex': 0, + 'input::type': 0, + 'input::usemap': 11, + 'input::value': 0, + 'ins::cite': 1, + 'ins::datetime': 0, + 'label::accesskey': 0, + 'label::for': 5, + 'label::onblur': 2, + 'label::onfocus': 2, + 'legend::accesskey': 0, + 'legend::align': 0, + 'li::type': 0, + 'li::value': 0, + 'map::name': 7, + 'menu::compact': 0, + 'ol::compact': 0, + 'ol::start': 0, + 'ol::type': 0, + 'optgroup::disabled': 0, + 'optgroup::label': 0, + 'option::disabled': 0, + 'option::label': 0, + 'option::selected': 0, + 'option::value': 0, + 'p::align': 0, + 'pre::width': 0, + 'q::cite': 1, + 'select::disabled': 0, + 'select::multiple': 0, + 'select::name': 8, + 'select::onblur': 2, + 'select::onchange': 2, + 'select::onfocus': 2, + 'select::size': 0, + 'select::tabindex': 0, + 'table::align': 0, + 'table::bgcolor': 0, + 'table::border': 0, + 'table::cellpadding': 0, + 'table::cellspacing': 0, + 'table::frame': 0, + 'table::rules': 0, + 'table::summary': 0, + 'table::width': 0, + 'tbody::align': 0, + 'tbody::char': 0, + 'tbody::charoff': 0, + 'tbody::valign': 0, + 'td::abbr': 0, + 'td::align': 0, + 'td::axis': 0, + 'td::bgcolor': 0, + 'td::char': 0, + 'td::charoff': 0, + 'td::colspan': 0, + 'td::headers': 6, + 'td::height': 0, + 'td::nowrap': 0, + 'td::rowspan': 0, + 'td::scope': 0, + 'td::valign': 0, + 'td::width': 0, + 'textarea::accesskey': 0, + 'textarea::cols': 0, + 'textarea::disabled': 0, + 'textarea::name': 8, + 'textarea::onblur': 2, + 'textarea::onchange': 2, + 'textarea::onfocus': 2, + 'textarea::onselect': 2, + 'textarea::readonly': 0, + 'textarea::rows': 0, + 'textarea::tabindex': 0, + 'tfoot::align': 0, + 'tfoot::char': 0, + 'tfoot::charoff': 0, + 'tfoot::valign': 0, + 'th::abbr': 0, + 'th::align': 0, + 'th::axis': 0, + 'th::bgcolor': 0, + 'th::char': 0, + 'th::charoff': 0, + 'th::colspan': 0, + 'th::headers': 6, + 'th::height': 0, + 'th::nowrap': 0, + 'th::rowspan': 0, + 'th::scope': 0, + 'th::valign': 0, + 'th::width': 0, + 'thead::align': 0, + 'thead::char': 0, + 'thead::charoff': 0, + 'thead::valign': 0, + 'tr::align': 0, + 'tr::bgcolor': 0, + 'tr::char': 0, + 'tr::charoff': 0, + 'tr::valign': 0, + 'ul::compact': 0, + 'ul::type': 0 +}; +html4.eflags = { + OPTIONAL_ENDTAG: 1, + EMPTY: 2, + CDATA: 4, + RCDATA: 8, + UNSAFE: 16, + FOLDABLE: 32, + SCRIPT: 64, + STYLE: 128 +}; +html4.ELEMENTS = { + 'a': 0, + 'abbr': 0, + 'acronym': 0, + 'address': 0, + 'applet': 16, + 'area': 2, + 'b': 0, + 'base': 18, + 'basefont': 18, + 'bdo': 0, + 'big': 0, + 'blockquote': 0, + 'body': 49, + 'br': 2, + 'button': 0, + 'canvas': 0, + 'caption': 0, + 'center': 0, + 'cite': 0, + 'code': 0, + 'col': 2, + 'colgroup': 1, + 'dd': 1, + 'del': 0, + 'dfn': 0, + 'dir': 0, + 'div': 0, + 'dl': 0, + 'dt': 1, + 'em': 0, + 'fieldset': 0, + 'font': 0, + 'form': 0, + 'frame': 18, + 'frameset': 16, + 'h1': 0, + 'h2': 0, + 'h3': 0, + 'h4': 0, + 'h5': 0, + 'h6': 0, + 'head': 49, + 'hr': 2, + 'html': 49, + 'i': 0, + 'iframe': 4, + 'img': 2, + 'input': 2, + 'ins': 0, + 'isindex': 18, + 'kbd': 0, + 'label': 0, + 'legend': 0, + 'li': 1, + 'link': 18, + 'map': 0, + 'menu': 0, + 'meta': 18, + 'nobr': 0, + 'noembed': 4, + 'noframes': 20, + 'noscript': 20, + 'object': 16, + 'ol': 0, + 'optgroup': 0, + 'option': 1, + 'p': 1, + 'param': 18, + 'pre': 0, + 'q': 0, + 's': 0, + 'samp': 0, + 'script': 84, + 'select': 0, + 'small': 0, + 'span': 0, + 'strike': 0, + 'strong': 0, + 'style': 148, + 'sub': 0, + 'sup': 0, + 'table': 0, + 'tbody': 1, + 'td': 1, + 'textarea': 8, + 'tfoot': 1, + 'th': 1, + 'thead': 1, + 'title': 24, + 'tr': 1, + 'tt': 0, + 'u': 0, + 'ul': 0, + 'var': 0 +}; +html4.ueffects = { + NOT_LOADED: 0, + SAME_DOCUMENT: 1, + NEW_DOCUMENT: 2 +}; +html4.URIEFFECTS = { + 'a::href': 2, + 'area::href': 2, + 'blockquote::cite': 0, + 'body::background': 1, + 'del::cite': 0, + 'form::action': 2, + 'img::src': 1, + 'input::src': 1, + 'ins::cite': 0, + 'q::cite': 0 +}; +html4.ltypes = { + UNSANDBOXED: 2, + SANDBOXED: 1, + DATA: 0 +}; +html4.LOADERTYPES = { + 'a::href': 2, + 'area::href': 2, + 'blockquote::cite': 2, + 'body::background': 1, + 'del::cite': 2, + 'form::action': 2, + 'img::src': 1, + 'input::src': 1, + 'ins::cite': 2, + 'q::cite': 2 +};; +// Copyright (C) 2006 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file 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. + +/** + * @fileoverview + * An HTML sanitizer that can satisfy a variety of security policies. + * + *

        + * The HTML sanitizer is built around a SAX parser and HTML element and + * attributes schemas. + * + * @author mikesamuel@gmail.com + * @requires html4 + * @overrides window + * @provides html, html_sanitize + */ + +/** + * @namespace + */ +var html = (function (html4) { + var lcase; + // The below may not be true on browsers in the Turkish locale. + if ('script' === 'SCRIPT'.toLowerCase()) { + lcase = function (s) { return s.toLowerCase(); }; + } else { + /** + * {@updoc + * $ lcase('SCRIPT') + * # 'script' + * $ lcase('script') + * # 'script' + * } + */ + lcase = function (s) { + return s.replace( + /[A-Z]/g, + function (ch) { + return String.fromCharCode(ch.charCodeAt(0) | 32); + }); + }; + } + + var ENTITIES = { + lt : '<', + gt : '>', + amp : '&', + nbsp : '\240', + quot : '"', + apos : '\'' + }; + + // Schemes on which to defer to uripolicy. Urls with other schemes are denied + var WHITELISTED_SCHEMES = /^(?:https?|mailto|data)$/i; + + var decimalEscapeRe = /^#(\d+)$/; + var hexEscapeRe = /^#x([0-9A-Fa-f]+)$/; + /** + * Decodes an HTML entity. + * + * {@updoc + * $ lookupEntity('lt') + * # '<' + * $ lookupEntity('GT') + * # '>' + * $ lookupEntity('amp') + * # '&' + * $ lookupEntity('nbsp') + * # '\xA0' + * $ lookupEntity('apos') + * # "'" + * $ lookupEntity('quot') + * # '"' + * $ lookupEntity('#xa') + * # '\n' + * $ lookupEntity('#10') + * # '\n' + * $ lookupEntity('#x0a') + * # '\n' + * $ lookupEntity('#010') + * # '\n' + * $ lookupEntity('#x00A') + * # '\n' + * $ lookupEntity('Pi') // Known failure + * # '\u03A0' + * $ lookupEntity('pi') // Known failure + * # '\u03C0' + * } + * + * @param name the content between the '&' and the ';'. + * @return a single unicode code-point as a string. + */ + function lookupEntity(name) { + name = lcase(name); // TODO: π is different from Π + if (ENTITIES.hasOwnProperty(name)) { return ENTITIES[name]; } + var m = name.match(decimalEscapeRe); + if (m) { + return String.fromCharCode(parseInt(m[1], 10)); + } else if (!!(m = name.match(hexEscapeRe))) { + return String.fromCharCode(parseInt(m[1], 16)); + } + return ''; + } + + function decodeOneEntity(_, name) { + return lookupEntity(name); + } + + var nulRe = /\0/g; + function stripNULs(s) { + return s.replace(nulRe, ''); + } + + var entityRe = /&(#\d+|#x[0-9A-Fa-f]+|\w+);/g; + /** + * The plain text of a chunk of HTML CDATA which possibly containing. + * + * {@updoc + * $ unescapeEntities('') + * # '' + * $ unescapeEntities('hello World!') + * # 'hello World!' + * $ unescapeEntities('1 < 2 && 4 > 3 ') + * # '1 < 2 && 4 > 3\n' + * $ unescapeEntities('<< <- unfinished entity>') + * # '<< <- unfinished entity>' + * $ unescapeEntities('/foo?bar=baz©=true') // & often unescaped in URLS + * # '/foo?bar=baz©=true' + * $ unescapeEntities('pi=ππ, Pi=Π\u03A0') // FIXME: known failure + * # 'pi=\u03C0\u03c0, Pi=\u03A0\u03A0' + * } + * + * @param s a chunk of HTML CDATA. It must not start or end inside an HTML + * entity. + */ + function unescapeEntities(s) { + return s.replace(entityRe, decodeOneEntity); + } + + var ampRe = /&/g; + var looseAmpRe = /&([^a-z#]|#(?:[^0-9x]|x(?:[^0-9a-f]|$)|$)|$)/gi; + var ltRe = //g; + var quotRe = /\"/g; + var eqRe = /\=/g; // Backslash required on JScript.net + + /** + * Escapes HTML special characters in attribute values as HTML entities. + * + * {@updoc + * $ escapeAttrib('') + * # '' + * $ escapeAttrib('"<<&==&>>"') // Do not just escape the first occurrence. + * # '"<<&==&>>"' + * $ escapeAttrib('Hello !') + * # 'Hello <World>!' + * } + */ + function escapeAttrib(s) { + // Escaping '=' defangs many UTF-7 and SGML short-tag attacks. + return s.replace(ampRe, '&').replace(ltRe, '<').replace(gtRe, '>') + .replace(quotRe, '"').replace(eqRe, '='); + } + + /** + * Escape entities in RCDATA that can be escaped without changing the meaning. + * {@updoc + * $ normalizeRCData('1 < 2 && 3 > 4 && 5 < 7&8') + * # '1 < 2 && 3 > 4 && 5 < 7&8' + * } + */ + function normalizeRCData(rcdata) { + return rcdata + .replace(looseAmpRe, '&$1') + .replace(ltRe, '<') + .replace(gtRe, '>'); + } + + + // TODO(mikesamuel): validate sanitizer regexs against the HTML5 grammar at + // http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html + // http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html + // http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html + // http://www.whatwg.org/specs/web-apps/current-work/multipage/tree-construction.html + + /** token definitions. */ + var INSIDE_TAG_TOKEN = new RegExp( + // Don't capture space. + '^\\s*(?:' + // Capture an attribute name in group 1, and value in group 3. + // We capture the fact that there was an attribute in group 2, since + // interpreters are inconsistent in whether a group that matches nothing + // is null, undefined, or the empty string. + + ('(?:' + + '([a-z][a-z-]*)' // attribute name + + ('(' // optionally followed + + '\\s*=\\s*' + + ('(' + // A double quoted string. + + '\"[^\"]*\"' + // A single quoted string. + + '|\'[^\']*\'' + // The positive lookahead is used to make sure that in + // , the value for bar is blank, not "baz=boo". + + '|(?=[a-z][a-z-]*\\s*=)' + // An unquoted value that is not an attribute name. + // We know it is not an attribute name because the previous + // zero-width match would've eliminated that possibility. + + '|[^>\"\'\\s]*' + + ')' + ) + + ')' + ) + '?' + + ')' + ) + // End of tag captured in group 3. + + '|(\/?>)' + // Don't capture cruft + + '|[\\s\\S][^a-z\\s>]*)', + 'i'); + + var OUTSIDE_TAG_TOKEN = new RegExp( + '^(?:' + // Entity captured in group 1. + + '&(\\#[0-9]+|\\#[x][0-9a-f]+|\\w+);' + // Comment, doctypes, and processing instructions not captured. + + '|<\!--[\\s\\S]*?--\>|]*>|<\\?[^>*]*>' + // '/' captured in group 2 for close tags, and name captured in group 3. + + '|<(\/)?([a-z][a-z0-9]*)' + // Text captured in group 4. + + '|([^<&>]+)' + // Cruft captured in group 5. + + '|([<&>]))', + 'i'); + + /** + * Given a SAX-like event handler, produce a function that feeds those + * events and a parameter to the event handler. + * + * The event handler has the form:{@code + * { + * // Name is an upper-case HTML tag name. Attribs is an array of + * // alternating upper-case attribute names, and attribute values. The + * // attribs array is reused by the parser. Param is the value passed to + * // the saxParser. + * startTag: function (name, attribs, param) { ... }, + * endTag: function (name, param) { ... }, + * pcdata: function (text, param) { ... }, + * rcdata: function (text, param) { ... }, + * cdata: function (text, param) { ... }, + * startDoc: function (param) { ... }, + * endDoc: function (param) { ... } + * }} + * + * @param {Object} handler a record containing event handlers. + * @return {Function} that takes a chunk of html and a parameter. + * The parameter is passed on to the handler methods. + */ + function makeSaxParser(handler) { + return function parse(htmlText, param) { + htmlText = String(htmlText); + var htmlLower = null; + + var inTag = false; // True iff we're currently processing a tag. + var attribs = []; // Accumulates attribute names and values. + var tagName = void 0; // The name of the tag currently being processed. + var eflags = void 0; // The element flags for the current tag. + var openTag = void 0; // True if the current tag is an open tag. + + if (handler.startDoc) { handler.startDoc(param); } + + while (htmlText) { + var m = htmlText.match(inTag ? INSIDE_TAG_TOKEN : OUTSIDE_TAG_TOKEN); + htmlText = htmlText.substring(m[0].length); + + if (inTag) { + if (m[1]) { // attribute + // setAttribute with uppercase names doesn't work on IE6. + var attribName = lcase(m[1]); + var decodedValue; + if (m[2]) { + var encodedValue = m[3]; + switch (encodedValue.charCodeAt(0)) { // Strip quotes + case 34: case 39: + encodedValue = encodedValue.substring( + 1, encodedValue.length - 1); + break; + } + decodedValue = unescapeEntities(stripNULs(encodedValue)); + } else { + // Use name as value for valueless attribs, so + // + // gets attributes ['type', 'checkbox', 'checked', 'checked'] + decodedValue = attribName; + } + attribs.push(attribName, decodedValue); + } else if (m[4]) { + if (eflags !== void 0) { // False if not in whitelist. + if (openTag) { + if (handler.startTag) { + handler.startTag(tagName, attribs, param); + } + } else { + if (handler.endTag) { + handler.endTag(tagName, param); + } + } + } + + if (openTag + && (eflags & (html4.eflags.CDATA | html4.eflags.RCDATA))) { + if (htmlLower === null) { + htmlLower = lcase(htmlText); + } else { + htmlLower = htmlLower.substring( + htmlLower.length - htmlText.length); + } + var dataEnd = htmlLower.indexOf('' ? '>' : '&', + param); + } + } + } + } + + if (handler.endDoc) { handler.endDoc(param); } + }; + } + + /** + * Returns a function that strips unsafe tags and attributes from html. + * @param {Function} sanitizeAttributes + * maps from (tagName, attribs[]) to null or a sanitized attribute array. + * The attribs array can be arbitrarily modified, but the same array + * instance is reused, so should not be held. + * @return {Function} from html to sanitized html + */ + function makeHtmlSanitizer(sanitizeAttributes) { + var stack; + var ignoring; + return makeSaxParser({ + startDoc: function (_) { + stack = []; + ignoring = false; + }, + startTag: function (tagName, attribs, out) { + if (ignoring) { return; } + if (!html4.ELEMENTS.hasOwnProperty(tagName)) { return; } + var eflags = html4.ELEMENTS[tagName]; + if (eflags & html4.eflags.FOLDABLE) { + return; + } else if (eflags & html4.eflags.UNSAFE) { + ignoring = !(eflags & html4.eflags.EMPTY); + return; + } + attribs = sanitizeAttributes(tagName, attribs); + // TODO(mikesamuel): relying on sanitizeAttributes not to + // insert unsafe attribute names. + if (attribs) { + if (!(eflags & html4.eflags.EMPTY)) { + stack.push(tagName); + } + + out.push('<', tagName); + for (var i = 0, n = attribs.length; i < n; i += 2) { + var attribName = attribs[i], + value = attribs[i + 1]; + if (value !== null && value !== void 0) { + out.push(' ', attribName, '="', escapeAttrib(value), '"'); + } + } + out.push('>'); + } + }, + endTag: function (tagName, out) { + if (ignoring) { + ignoring = false; + return; + } + if (!html4.ELEMENTS.hasOwnProperty(tagName)) { return; } + var eflags = html4.ELEMENTS[tagName]; + if (!(eflags & (html4.eflags.UNSAFE | html4.eflags.EMPTY + | html4.eflags.FOLDABLE))) { + var index; + if (eflags & html4.eflags.OPTIONAL_ENDTAG) { + for (index = stack.length; --index >= 0;) { + var stackEl = stack[index]; + if (stackEl === tagName) { break; } + if (!(html4.ELEMENTS[stackEl] + & html4.eflags.OPTIONAL_ENDTAG)) { + // Don't pop non optional end tags looking for a match. + return; + } + } + } else { + for (index = stack.length; --index >= 0;) { + if (stack[index] === tagName) { break; } + } + } + if (index < 0) { return; } // Not opened. + for (var i = stack.length; --i > index;) { + var stackEl = stack[i]; + if (!(html4.ELEMENTS[stackEl] + & html4.eflags.OPTIONAL_ENDTAG)) { + out.push(''); + } + } + stack.length = index; + out.push(''); + } + }, + pcdata: function (text, out) { + if (!ignoring) { out.push(text); } + }, + rcdata: function (text, out) { + if (!ignoring) { out.push(text); } + }, + cdata: function (text, out) { + if (!ignoring) { out.push(text); } + }, + endDoc: function (out) { + for (var i = stack.length; --i >= 0;) { + out.push(''); + } + stack.length = 0; + } + }); + } + + // From RFC3986 + var URI_SCHEME_RE = new RegExp( + "^" + + "(?:" + + "([^:\/?#]+)" + // scheme + ":)?" + ); + + /** + * Strips unsafe tags and attributes from html. + * @param {string} htmlText to sanitize + * @param {Function} opt_uriPolicy -- a transform to apply to uri/url + * attribute values. If no opt_uriPolicy is provided, no uris + * are allowed ie. the default uriPolicy rewrites all uris to null + * @param {Function} opt_nmTokenPolicy : string -> string? -- a transform to + * apply to names, ids, and classes. If no opt_nmTokenPolicy is provided, + * all names, ids and classes are passed through ie. the default + * nmTokenPolicy is an identity transform + * @return {string} html + */ + function sanitize(htmlText, opt_uriPolicy, opt_nmTokenPolicy) { + var out = []; + makeHtmlSanitizer( + function sanitizeAttribs(tagName, attribs) { + for (var i = 0; i < attribs.length; i += 2) { + var attribName = attribs[i]; + var value = attribs[i + 1]; + var atype = null, attribKey; + if ((attribKey = tagName + '::' + attribName, + html4.ATTRIBS.hasOwnProperty(attribKey)) + || (attribKey = '*::' + attribName, + html4.ATTRIBS.hasOwnProperty(attribKey))) { + atype = html4.ATTRIBS[attribKey]; + } + if (atype !== null) { + switch (atype) { + case html4.atype.NONE: break; + case html4.atype.SCRIPT: + case html4.atype.STYLE: + value = null; + break; + case html4.atype.ID: + case html4.atype.IDREF: + case html4.atype.IDREFS: + case html4.atype.GLOBAL_NAME: + case html4.atype.LOCAL_NAME: + case html4.atype.CLASSES: + value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value; + break; + case html4.atype.URI: + var parsedUri = ('' + value).match(URI_SCHEME_RE); + if (!parsedUri) { + value = null; + } else if (!parsedUri[1] || + WHITELISTED_SCHEMES.test(parsedUri[1])) { + value = opt_uriPolicy && opt_uriPolicy(value); + } else { + value = null; + } + break; + case html4.atype.URI_FRAGMENT: + if (value && '#' === value.charAt(0)) { + value = opt_nmTokenPolicy ? opt_nmTokenPolicy(value) : value; + if (value) { value = '#' + value; } + } else { + value = null; + } + break; + default: + value = null; + break; + } + } else { + value = null; + } + attribs[i + 1] = value; + } + return attribs; + })(htmlText, out); + return out.join(''); + } + + return { + escapeAttrib: escapeAttrib, + makeHtmlSanitizer: makeHtmlSanitizer, + makeSaxParser: makeSaxParser, + normalizeRCData: normalizeRCData, + sanitize: sanitize, + unescapeEntities: unescapeEntities + }; +})(html4); + +var html_sanitize = html.sanitize; + +// Exports for closure compiler. Note this file is also cajoled +// for domado and run in an environment without 'window' +if (typeof window !== 'undefined') { + window['html'] = html; + window['html_sanitize'] = html_sanitize; +} +// Loosen restrictions of Caja's +// html-sanitizer to allow for styling +html4.ATTRIBS['*::style'] = 0; +html4.ELEMENTS['style'] = 0; + +html4.ATTRIBS['a::target'] = 0; + +html4.ELEMENTS['video'] = 0; +html4.ATTRIBS['video::src'] = 0; +html4.ATTRIBS['video::poster'] = 0; +html4.ATTRIBS['video::controls'] = 0; + +html4.ELEMENTS['audio'] = 0; +html4.ATTRIBS['audio::src'] = 0; +html4.ATTRIBS['video::autoplay'] = 0; +html4.ATTRIBS['video::controls'] = 0; +/*! + * mustache.js - Logic-less {{mustache}} templates with JavaScript + * http://github.com/janl/mustache.js + */ +var Mustache = (typeof module !== "undefined" && module.exports) || {}; + +(function (exports) { + + exports.name = "mustache.js"; + exports.version = "0.5.0-dev"; + exports.tags = ["{{", "}}"]; + exports.parse = parse; + exports.compile = compile; + exports.render = render; + exports.clearCache = clearCache; + + // This is here for backwards compatibility with 0.4.x. + exports.to_html = function (template, view, partials, send) { + var result = render(template, view, partials); + + if (typeof send === "function") { + send(result); + } else { + return result; + } + }; + + var _toString = Object.prototype.toString; + var _isArray = Array.isArray; + var _forEach = Array.prototype.forEach; + var _trim = String.prototype.trim; + + var isArray; + if (_isArray) { + isArray = _isArray; + } else { + isArray = function (obj) { + return _toString.call(obj) === "[object Array]"; + }; + } + + var forEach; + if (_forEach) { + forEach = function (obj, callback, scope) { + return _forEach.call(obj, callback, scope); + }; + } else { + forEach = function (obj, callback, scope) { + for (var i = 0, len = obj.length; i < len; ++i) { + callback.call(scope, obj[i], i, obj); + } + }; + } + + var spaceRe = /^\s*$/; + + function isWhitespace(string) { + return spaceRe.test(string); + } + + var trim; + if (_trim) { + trim = function (string) { + return string == null ? "" : _trim.call(string); + }; + } else { + var trimLeft, trimRight; + + if (isWhitespace("\xA0")) { + trimLeft = /^\s+/; + trimRight = /\s+$/; + } else { + // IE doesn't match non-breaking spaces with \s, thanks jQuery. + trimLeft = /^[\s\xA0]+/; + trimRight = /[\s\xA0]+$/; + } + + trim = function (string) { + return string == null ? "" : + String(string).replace(trimLeft, "").replace(trimRight, ""); + }; + } + + var escapeMap = { + "&": "&", + "<": "<", + ">": ">", + '"': '"', + "'": ''', + "/": '/' + }; + + function escapeHTML(string) { + return String(string).replace(/[&<>"'\/]/g, function (s) { + return escapeMap[s] || s; + }); + } + + /** + * Adds the `template`, `line`, and `file` properties to the given error + * object and alters the message to provide more useful debugging information. + */ + function debug(e, template, line, file) { + file = file || "