├── .gitattributes ├── pic └── gear.png ├── README.md ├── css └── style.css ├── sftp-config.json ├── index.html └── javascript └── main.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /pic/gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immortalmice/Air-Map-Near/master/pic/gear.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Air Map Near 2 | 3 | Get your location from browser, and find the nearest detector to show the PM2.5 value. 4 | Set the specific location is also work. 5 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | #center{ 2 | text-align: center; 3 | position: absolute; 4 | } 5 | #gear{ 6 | position: absolute; 7 | top: 10px; 8 | right: 10px; 9 | width: 36px; 10 | height: 36px; 11 | cursor: pointer; 12 | z-index: 100; 13 | } 14 | #gps{ 15 | position: absolute; 16 | top: 10px; 17 | left: 10px; 18 | } 19 | #moreInfo{ 20 | position: absolute; 21 | bottom: 310px; 22 | right: 10px; 23 | cursor: pointer; 24 | } 25 | #auto_gps{ 26 | margin-top: 10px; 27 | } 28 | #order_gps{ 29 | margin-top: 10px; 30 | display: none; 31 | } 32 | #degree_div{ 33 | padding-left: 20px; 34 | padding-right: 20px; 35 | } 36 | #decimal_div{ 37 | padding-left: 20px; 38 | padding-right: 20px; 39 | display: none; 40 | } 41 | #Map{ 42 | position: absolute; 43 | bottom: 0px; 44 | right: 0px; 45 | 46 | } 47 | .col-sm-3{ 48 | margin-top: 10px; 49 | padding-left: 0px; 50 | padding-right: 10px; 51 | } 52 | @media screen and (min-width: 768px){ 53 | .last{ 54 | padding-right: 0px; 55 | } 56 | } 57 | @media screen and (max-width: 768px){ 58 | #gps_type{ 59 | margin-right: 10px; 60 | } 61 | } -------------------------------------------------------------------------------- /sftp-config.json: -------------------------------------------------------------------------------- 1 | { 2 | // The tab key will cycle through the settings when first created 3 | // Visit http://wbond.net/sublime_packages/sftp/settings for help 4 | 5 | // sftp, ftp or ftps 6 | "type": "sftp", 7 | 8 | "save_before_upload": true, 9 | "upload_on_save": true, 10 | "sync_down_on_open": false, 11 | "sync_skip_deletes": false, 12 | "sync_same_age": true, 13 | "confirm_downloads": false, 14 | "confirm_sync": true, 15 | "confirm_overwrite_newer": false, 16 | 17 | "host": "-----", 18 | "user": "--------", 19 | "password": "-------", 20 | "port": "------", 21 | 22 | "remote_path": "/var/www/html/", 23 | "ignore_regexes": [ 24 | "\\.sublime-(project|workspace)", "sftp-config(-alt\\d?)?\\.json", 25 | "sftp-settings\\.json", "/venv/", "\\.svn/", "\\.hg/", "\\.git/", 26 | "\\.bzr", "_darcs", "CVS", "\\.DS_Store", "Thumbs\\.db", "desktop\\.ini" 27 | ], 28 | //"file_permissions": "664", 29 | //"dir_permissions": "775", 30 | 31 | //"extra_list_connections": 0, 32 | 33 | "connect_timeout": 30, 34 | //"keepalive": 120, 35 | //"ftp_passive_mode": true, 36 | //"ftp_obey_passive_host": false, 37 | //"ssh_key_file": "~/.ssh/id_rsa", 38 | //"sftp_flags": ["-F", "/path/to/ssh_config"], 39 | 40 | //"preserve_modification_times": false, 41 | //"remote_time_offset_in_hours": 0, 42 | //"remote_encoding": "utf-8", 43 | //"remote_locale": "C", 44 | //"allow_config_upload": false, 45 | } 46 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 最近的測值 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 |

20 | 21 |
22 |

23 |

24 |

25 |

26 |
27 | 詳細資料 28 | 146 |
147 | 148 | -------------------------------------------------------------------------------- /javascript/main.js: -------------------------------------------------------------------------------- 1 | //link: https://airmap.g0v.asper.tw/api/nearby?lat=24.256&lng=120.7435 2 | //https://maps.google.com.tw/maps?f=q&hl=zh-TW&geocode=&q=25.07595,121.6919&z=16&output=embed&t= 3 | 4 | var position; 5 | var JSONdata; 6 | var time_gap = 10000; 7 | var mode = 2;//1:averge, 2:single 8 | var gps_on = true; 9 | var timeHandle; 10 | var link = "https://airmap.g0v.asper.tw/api/nearby"; 11 | 12 | function resizeing(){ 13 | var w_width = $(window).width(); 14 | var w_height = $(window).height(); 15 | var c_width = $("#center").width(); 16 | var c_height = $("#center").height(); 17 | $("#plate").width(w_width + 'px'); 18 | $("#plate").height(w_height + 'px'); 19 | $("#center").css({'left' : (w_width - c_width) / 2, 'top' : (w_height - c_height) / 2 - 150}); 20 | } 21 | 22 | function color(v){ 23 | if(v < 11){ 24 | return "9CFF9C"; 25 | }else if(v < 23){ 26 | return "31FF00"; 27 | }else if(v < 35){ 28 | return "31CF00"; 29 | }else if(v < 41){ 30 | return "FFFF00"; 31 | }else if(v < 47){ 32 | return "FFCF00"; 33 | }else if(v < 53){ 34 | return "FF9A00"; 35 | }else if(v < 58){ 36 | return "FF6363"; 37 | }else if(v < 64){ 38 | return "FF0000"; 39 | }else if(v < 70){ 40 | return "990000"; 41 | }else{ 42 | return "CD2FFE"; 43 | } 44 | } 45 | 46 | function advise(v, str){ 47 | if(str === "normal"){ 48 | if(v <= 53){ 49 | return "正常戶外活動。"; 50 | }else if(v <= 70){ 51 | return "應考慮減少戶外活動"; 52 | }else{ 53 | return "應避免戶外活動"; 54 | } 55 | }else if(str === "Special"){ 56 | if(v <= 35){ 57 | return "正常戶外活動。"; 58 | }else if(v <= 53){ 59 | return "應考慮減少戶外活動"; 60 | }else if(v <= 70){ 61 | return "應減少戶外活動"; 62 | }else{ 63 | return "應避免戶外活動"; 64 | } 65 | } 66 | } 67 | 68 | function getValue(str, arrary){ 69 | for(var i = 0; i <= arrary.length-1; i ++){ 70 | if(arrary[i].name == str){ 71 | return arrary[i].value; 72 | } 73 | } 74 | } 75 | 76 | function gmapLink(lat, lng){ 77 | return "https://maps.google.com.tw/maps?f=q&hl=zh-TW&geocode=&q=" + lat + "," + lng + "&z=16&output=embed&t=" 78 | } 79 | 80 | function tableGenerator(){ 81 | if(!JSONdata) return "無資料"; 82 | var str = ""; 83 | str += ""; 84 | 85 | str += ""; 86 | str += ""; 87 | str += ""; 88 | str += ""; 89 | str += ""; 90 | str += ""; 91 | str +=""; 92 | 93 | str += ""; 94 | for(var i = 0; i <= JSONdata.sites.length-1; i ++){ 95 | var data = JSONdata.sites[i]; 96 | str += ""; 97 | str += ""; 98 | str += ""; 99 | str += ""; 100 | str += ""; 101 | str += ""; 102 | str += ""; 103 | } 104 | 105 | str += "
#名稱PM2.5濕度溫度
" + (i+1) + "" + (data.SiteName === null ? "-" : data.SiteName) + "" + (data.Data.Dust2_5 === null ? "-" : data.Data.Dust2_5) + "" + (data.Data.Humidity === null ? "-" : data.Data.Humidity) + "" + (data.Data.Temperature === null ? "-" : data.Data.Temperature) + "
"; 106 | 107 | return str; 108 | } 109 | 110 | function updateGpsInfo(){ 111 | var latitude = position.lat; 112 | var longitude = position.lng; 113 | if(Object.is(latitude, NaN) || Object.is(longitude, NaN)){ 114 | $("#gps").html('錯誤的位置'); 115 | return; 116 | } 117 | $("#gps").html("(" + Math.round(latitude*100000)/100000 + "," + Math.round(longitude*100000)/100000 + ")"); 118 | $("#Map").attr("src", gmapLink(latitude, longitude)); 119 | resizeing(); 120 | } 121 | 122 | function ApplyOptions(data){ 123 | if(getValue("value_mode", data) === "averge"){ 124 | mode = 1; 125 | }else{ 126 | mode = 2; 127 | } 128 | if(getValue("gps_mode", data) === "auto"){ 129 | gps_on = true; 130 | time_gap = parseInt(getValue("time_gap", data)) * 1000; 131 | }else{ 132 | var lat, lng; 133 | gps_on = false; 134 | if(getValue("gps_type", data) === "十進位制"){ 135 | lat = parseFloat(getValue("inputlat", data)); 136 | lng = parseFloat(getValue("inputlng", data)); 137 | }else{ 138 | lat1 = parseInt(getValue("inputlat1", data)); 139 | lat2 = parseInt(getValue("inputlat2", data)); 140 | lat3 = parseFloat(getValue("inputlat3", data)); 141 | if(!Object.is(lat1, NaN) && !Object.is(lat2, NaN) && !Object.is(lat3, NaN)){ 142 | lat = lat1 + (lat2 / 60) + (lat3 / 3600); 143 | if(getValue('latNS', data) === 'S'){ 144 | lat *= -1; 145 | } 146 | }else{ 147 | lat = NaN; 148 | } 149 | lng1 = parseInt(getValue("inputlng1", data)); 150 | lng2 = parseInt(getValue("inputlng2", data)); 151 | lng3 = parseFloat(getValue("inputlng3", data)); 152 | if(!Object.is(lng1, NaN) && !Object.is(lng2, NaN) && !Object.is(lng3, NaN)){ 153 | lng = lng1 + (lng2 / 60) + (lng3 / 3600); 154 | if(getValue('latWE', data) === 'W'){ 155 | lng *= -1; 156 | } 157 | }else{ 158 | lng = NaN; 159 | } 160 | } 161 | position = { 162 | 'lat' : lat, 163 | 'lng' : lng 164 | } 165 | } 166 | 167 | updateGpsInfo(); 168 | } 169 | 170 | function mapServiceProvider(latitude, longitude) { 171 | position = { 172 | 'lat' : latitude, 173 | 'lng' : longitude 174 | } 175 | console.log(position); 176 | updateGpsInfo(); 177 | } 178 | 179 | function gps(f){ 180 | var subtitle_text = $("#subtitle"); 181 | if(navigator.geolocation) { 182 | navigator.geolocation.getCurrentPosition(function(position){ 183 | mapServiceProvider(position.coords.latitude, position.coords.longitude); 184 | if(f)f(); 185 | resizeing(); 186 | }, 187 | function(error) { 188 | switch (error.code) { 189 | case error.TIMEOUT: 190 | subtitle_text.html('連線逾時'); 191 | break; 192 | case error.POSITION_UNAVAILABLE: 193 | subtitle_text.html('無法取得定位'); 194 | break; 195 | case error.PERMISSION_DENIED: 196 | subtitle_text.html('請允許瀏覽器的GPS定位功能後重新整理'); 197 | break; 198 | case error.UNKNOWN_ERROR: 199 | subtitle_text.html('不明的錯誤,請稍候再試'); 200 | break; 201 | } 202 | resizeing(); 203 | }); 204 | }else if(window.google && google.gears){ 205 | try { 206 | var geo = google.gears.factory.create('beta.geolocation'); 207 | geo.getCurrentPosition(function(p){ 208 | mapServiceProvider(p.latitude, p.longitude); 209 | if(f)f(); 210 | }, function (err){ 211 | subtitle_text.html('Error:' + err); 212 | }, {enableHighAccuracy: true, gearsRequestAddress: true}); 213 | }catch(e) { 214 | subtitle_text.html('定位失敗'); 215 | resizeing(); 216 | } 217 | }else{ 218 | subtitle_text.html('請允許瀏覽器的GPS定位功能後重新整理'); 219 | resizeing(); 220 | } 221 | resizeing(); 222 | } 223 | 224 | function loop(){ 225 | resizeing(); 226 | if(position){ 227 | if(gps_on) gps(); 228 | if(!Object.is(position.lat, NaN) && !Object.is(position.lng, NaN)){ 229 | $.getJSON(link, {lat: position.lat, lng: position.lng}).done(function(data){ 230 | console.log(data); 231 | JSONdata = data; 232 | var data_length = data.sites.length; 233 | var v = 0; 234 | if(data_length != 0){ 235 | if(mode === 1){ 236 | $("#subtitle").html("來自附近" + data_length + "個站點"); 237 | var total = 0; 238 | for(var i = 0; i <= data_length-1; i ++){ 239 | total += data.sites[i].Data.Dust2_5; 240 | } 241 | v = total / data_length; 242 | }else if(mode === 2){ 243 | var minDistance = Infinity; 244 | var minIndex = -1; 245 | for(var i = 0; i <= data_length-1; i ++){ 246 | var gps_ = data.sites[i].LatLng; 247 | var dis = Math.pow(gps_.lat - position.lat, 2) + Math.pow(gps_.lng - position.lng, 2); 248 | console.log(dis); 249 | if(dis < minDistance){ 250 | minDistance = dis; 251 | minIndex = i; 252 | } 253 | } 254 | $("#subtitle").html(data.sites[minIndex].SiteName); 255 | v = data.sites[minIndex].Data.Dust2_5; 256 | } 257 | }else{ 258 | $("#subtitle").html("附近沒有站點"); 259 | } 260 | $("#value").html((Math.round((v * 100)) / 100) + "μg/m3"); 261 | $("#plate").css({"background-color": "#" + color(v)}); 262 | $("#normalAdvise").html("一般民眾:" + advise(v, "normal")); 263 | $("#specialAdvise").html("敏感性族群:" + advise(v, "Special")); 264 | resizeing(); 265 | }); 266 | } 267 | timeHandle = setTimeout(loop, time_gap); 268 | }else{ 269 | timeHandle = setTimeout(loop, 1000); 270 | } 271 | resizeing(); 272 | } 273 | 274 | $(document).ready(function(){ 275 | resizeing(); 276 | $(window).resize(resizeing); 277 | 278 | $("#value").html(0); 279 | $("#subtitle").html("Loading..."); 280 | resizeing(); 281 | 282 | $("#gear").click(function(){$("#optionModal").modal('show');}); 283 | $("#moreInfo").click(function(){ 284 | $("#infoModal").modal('show'); 285 | $("#infoModalBody").html(tableGenerator()); 286 | }); 287 | $("#optionApply").click(function(){ 288 | $("#optionModal").modal('hide'); 289 | var formData = $("#optionForm").serializeArray(); 290 | console.log(formData); 291 | clearTimeout(timeHandle); 292 | 293 | ApplyOptions(formData); 294 | 295 | loop(); 296 | }); 297 | 298 | $("#auto_gps_option").click(function(){ 299 | console.log("select auto"); 300 | $("#auto_gps").css({"display": "block"}); 301 | $("#order_gps").css({"display": "none"}); 302 | }); 303 | $("#order_gps_option").click(function(){ 304 | console.log("select order"); 305 | $("#auto_gps").css({"display": "none"}); 306 | $("#order_gps").css({"display": "block"}); 307 | }); 308 | $("#gps_type").change(function(){ 309 | console.log($("#gps_type").val()); 310 | if($("#gps_type").val() === "度分秒制"){ 311 | $("#degree_div").css({"display": "block"}); 312 | $("#decimal_div").css({"display": "none"}); 313 | }else{ 314 | $("#degree_div").css({"display": "none"}); 315 | $("#decimal_div").css({"display": "block"}); 316 | } 317 | }); 318 | 319 | $("#set_gps").click(function(){ 320 | gps(function(){ 321 | $("#inputlat").val(position.lat); 322 | $("#inputlng").val(position.lng); 323 | if(position.lat < 0){ 324 | $("#latNS").val("S"); 325 | }else{ 326 | $("#latNS").val("N"); 327 | } 328 | if(position.lng < 0){ 329 | $("#latWE").val("W"); 330 | }else{ 331 | $("#latWE").val("E"); 332 | } 333 | var lat2 = position.lat % 1; 334 | var lat1 = position.lat - lat2; 335 | lat2 *= 60; 336 | var lat3 = lat2 % 1; 337 | lat2 = lat2 - lat3; 338 | lat3 *= 60; 339 | var lng2 = position.lng % 1; 340 | var lng1 = position.lng - lng2; 341 | lng2 *= 60; 342 | var lng3 = lng2 % 1; 343 | lng2 = lng2 - lng3; 344 | lng3 *= 60; 345 | 346 | $("#inputlat1").val(lat1); 347 | $("#inputlat2").val(lat2); 348 | $("#inputlat3").val(lat3); 349 | $("#inputlng1").val(lng1); 350 | $("#inputlng2").val(lng2); 351 | $("#inputlng3").val(lng3); 352 | }); 353 | }); 354 | 355 | gps(); 356 | loop(); 357 | }); --------------------------------------------------------------------------------