├── .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 |
132 |
133 |
134 |
137 |
138 |
139 |
140 |
143 |
144 |
145 |
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 += "PM2.5 | ";
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 += "| " + (i+1) + " | ";
98 | str += "" + (data.SiteName === null ? "-" : data.SiteName) + " | ";
99 | str += "" + (data.Data.Dust2_5 === null ? "-" : data.Data.Dust2_5) + " | ";
100 | str += "" + (data.Data.Humidity === null ? "-" : data.Data.Humidity) + " | ";
101 | str += "" + (data.Data.Temperature === null ? "-" : data.Data.Temperature) + " | ";
102 | str += "
";
103 | }
104 |
105 | str += "
";
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 | });
--------------------------------------------------------------------------------