Mercurial > repos > other > exif-graphr
comparison exif-graphr.js @ 0:42c058ce5b7c
Initial public commit of Exif-Graphr
author | IBBoard <dev@ibboard.co.uk> |
---|---|
date | Sun, 14 Aug 2016 20:46:16 +0100 |
parents | |
children | a11817a35877 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:42c058ce5b7c |
---|---|
1 var debug = false; | |
2 var photo_data = {}; | |
3 | |
4 /* Helper functions */ | |
5 function byID(d) { return d.id; } | |
6 function byValue(d) { return d; } | |
7 | |
8 function makeUrl(method, vals) { | |
9 vals = vals || {}; | |
10 var parts = []; | |
11 if (debug) { | |
12 for (var key in vals) { | |
13 parts.push(encodeURIComponent(key) + '-' + encodeURIComponent(vals[key])); | |
14 } | |
15 var extra = parts.join('-'); | |
16 if (extra != '') { | |
17 extra = '-' + extra; | |
18 } | |
19 var url = "./data-" + method + extra + ".json"; | |
20 } else { | |
21 for (var key in vals) { | |
22 parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(vals[key])); | |
23 } | |
24 var extra = parts.join('&'); | |
25 if (extra != '') { | |
26 extra = '&' + extra; | |
27 } | |
28 var url = "https://ibboard.co.uk/exif-graphr/api.php?method=" + method + extra; | |
29 } | |
30 | |
31 return url; | |
32 } | |
33 | |
34 /* The real code */ | |
35 function visualise() { | |
36 photo_data = {}; | |
37 var userFoundFunc = function(error,data) { | |
38 if (error) { | |
39 alert(error.responseText ? error.responseText : error.statusText); | |
40 return; | |
41 } | |
42 if (data.stat != 'ok') { | |
43 if (data.message) { | |
44 alert(data.message); | |
45 } else { | |
46 alert("Unknown error"); | |
47 } | |
48 return; | |
49 } | |
50 var nsid = getNSID(data); | |
51 getPhotos(nsid); | |
52 } | |
53 | |
54 var username = document.getElementById('username').value; | |
55 | |
56 if (/[0-9]+@N[0-9]+/.test(username)) { | |
57 getPhotos(username); | |
58 } else if (/https?:\/\/www\.flickr\.com\/photos\/[0-9a-z]+(\/.*)+/.test(username)) { | |
59 getNSID = function(data) { | |
60 return data.user.id; | |
61 }; | |
62 d3.json(makeUrl('flickr.urls.lookupUser', { 'url': username }), userFoundFunc); | |
63 } else { | |
64 getNSID = function(data) { | |
65 return data.user.nsid; | |
66 }; | |
67 d3.json(makeUrl('flickr.people.findByUsername', { 'username': username }), userFoundFunc); | |
68 } | |
69 } | |
70 | |
71 function getPhotos(nsid) { | |
72 d3.json(makeUrl('flickr.people.getPublicPhotos', { 'user_id': nsid, 'per_page' : 100, 'extras': 'url_t' }), function(error, data) { | |
73 data.photos.photo.forEach(getExif); | |
74 }); | |
75 } | |
76 | |
77 function getExif(photo) { | |
78 var photo_id = photo.id; | |
79 var thumb_url = photo.url_t; | |
80 var url = "https://flickr.com/photos/" + photo.owner + "/" + photo_id; | |
81 d3.json(makeUrl('flickr.photos.getExif', { 'photo_id' : photo_id }), function(error, data) { | |
82 if (error || data.code) { | |
83 console.log("Error fetching details for " + photo_id + ": " + data.message); | |
84 return; | |
85 } | |
86 var exifs = data.photo.exif; | |
87 var len = exifs.length; | |
88 var exposure = ''; | |
89 var f_number = ''; | |
90 var iso = ''; | |
91 var focal_length = ''; | |
92 var timestamp = ''; | |
93 var make = ''; | |
94 var model = ''; | |
95 | |
96 for (var i = 0; i < len; i++) { | |
97 var exif = exifs[i]; | |
98 if (exif.tagspace == 'ExifIFD') { | |
99 if (exif.tag == 'ExposureTime') { | |
100 if (exif.clean) { | |
101 exposure = exif.clean._content; | |
102 } else if (/^[0-9]+(\.[0-9]+)?$/.test(exif.raw._content)) { | |
103 exposure = exif.raw._content + " sec (" + exif.raw._content + ")"; | |
104 } else { | |
105 console.log("No clean exposure time for "+photo.id+". Raw was "+exif.raw._content); continue; | |
106 } | |
107 } else if (exif.tag == 'FNumber') { | |
108 f_number = exif.raw._content; | |
109 } else if (exif.tag == 'ISO') { | |
110 iso = exif.raw._content; | |
111 } else if (exif.tag == 'FocalLength') { | |
112 if (/^[0-9\.]+ mm/.test(exif.raw._content)) { | |
113 focal_length = parseInt(exif.raw._content); | |
114 } | |
115 } else if (exif.tag == 'DateTimeOriginal') { | |
116 timestamp = Date.parse(exif.raw._content.replace(/^([0-9][0-9][0-9][0-9]):([0-9][0-9]):([0-9][0-9]) /, '$1/$2/$3 ')); | |
117 } | |
118 } else if (exif.tagspace == 'IFD0') { | |
119 if (exif.tag == 'Make') { | |
120 make = exif.raw._content; | |
121 } else if (exif.tag == 'Model') { | |
122 model = exif.raw._content; | |
123 } else if (exif.tag == 'FocalLength') { | |
124 if (!focal_length) { focal_length = exif.raw._content.replace(/^([0-9]+(\.[0-9]+)?).*$/, '$1'); } | |
125 } else if (exif.tag == 'ExposureTime') { | |
126 if (!exposure) { exposure = exif.raw._content + " sec (" + exif.raw._content + ")"; } | |
127 } else if (exif.tag == 'FNumber') { | |
128 if (!f_number) { f_number = Math.round(exif.raw._content / 100); /* Trial and error guess - it is a rational number */ } | |
129 } | |
130 | |
131 } | |
132 } | |
133 | |
134 var exp_matches = exposure.match(/^(1\/)?([0-9\.]+) sec( \(([^\)]+)\))?/); | |
135 var exp_parts = [ '', '' ]; | |
136 if (exp_matches) { | |
137 if (exp_matches[1]) { | |
138 // We got a non-decimal numeric version | |
139 exp_parts[0] = 1 / exp_matches[2]; | |
140 } else { | |
141 exp_parts[0] = exp_matches[2]; | |
142 } | |
143 | |
144 if (exp_matches[4]) { | |
145 exp_parts[1] = exp_matches[4]; | |
146 var exp_fraction = exp_parts[1].match(/^1\/([0-9]+)$/); | |
147 if (exp_fraction) { | |
148 //Override for accuracy - the "clean" version rounds too much - 1/909 through 1/2000 ⇒ 0.001 | |
149 exp_parts[0] = 1 / exp_fraction[1]; | |
150 } | |
151 } else { | |
152 exp_parts[1] = exp_parts[0] >= 1 ? exp_parts[0] : "1/"+(1/exp_parts[0]); | |
153 } | |
154 } | |
155 var exp_sec = exp_parts[0]; | |
156 var exp_str = exp_parts[1]; | |
157 | |
158 var camera = ''; | |
159 make = make.replace(/ CORPORATION$/, ''); | |
160 if (make && model && !(model.indexOf(make) === 0)) { | |
161 camera = make + " " + model; | |
162 } else { | |
163 camera = model; | |
164 } | |
165 | |
166 photo_data[photo.id] = { 'id': photo.id, 'exposure': +exp_sec, 'exposure_string': exp_str, 'f_number': f_number, 'iso': +iso, 'focal_length': +focal_length, 'camera': camera, 'make': make, 'model' : model, 'timestamp': timestamp, 'thumb': thumb_url, 'url': url }; | |
167 update(); | |
168 }); | |
169 } | |
170 | |
171 function clone(obj) { | |
172 if (null == obj || "object" != typeof obj) return obj; | |
173 var copy = obj.constructor(); | |
174 for (var attr in obj) { | |
175 if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]); | |
176 } | |
177 return copy; | |
178 } | |
179 | |
180 function hashcode(s){ | |
181 return s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0); | |
182 } | |
183 | |
184 var margin = {top: 40, right: 20, bottom: 50, left: 55}, | |
185 width = 960 - margin.left - margin.right, | |
186 height = 600 - margin.top - margin.bottom, | |
187 axisHeight = height - margin.bottom; | |
188 var svg = d3.select("#graph").append("svg") | |
189 .attr("width", width + margin.left + margin.right) | |
190 .attr("height", height + margin.top + margin.bottom) | |
191 .append("g") | |
192 .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
193 var photoTable = d3.select("#photoTable"); | |
194 var legend = d3.select("#legend").append("svg"); | |
195 var legendGradients = legend.append("defs"); | |
196 var legends = legend.append("g"); | |
197 | |
198 var tip = d3.tip() | |
199 .attr('class', 'd3-tip') | |
200 .offset([-10, 0]) | |
201 .html(function(d) { | |
202 return "Photo " + d.id + '<br /><img src="' + d.thumb + '" alt="Photo ' + d.id + '" />' + "<br />Aperture: f/" + d.f_number + "<br />Focal Length: " + d.focal_length + "<br />Exposure: " + d.exposure_string + "<br />ISO: " + d.iso + "<br />Camera: " + d.camera; | |
203 }); | |
204 svg.call(tip); | |
205 | |
206 var flAxisScale = d3.scale.linear() | |
207 .rangeRound([axisHeight, 0]); | |
208 var flAxis = d3.svg.axis().orient("left").scale(flAxisScale); | |
209 var expAxisScale = d3.scale.log() | |
210 .rangeRound([0, width]); | |
211 var expAxis = d3.svg.axis().scale(expAxisScale) | |
212 .tickFormat(function(d) { if (d < 1) { return "1∕" + (1 / d).toFixed(2).replace(/\.?0+$/, ''); } else { return d; } }); | |
213 var circlesG = svg.append("svg:g"); | |
214 var axises = svg.append("svg:g"); | |
215 var saturationScale = d3.scale.log().range([30,90]); | |
216 axises.append("g") | |
217 .attr("class", "y axis") | |
218 .call(flAxis); | |
219 axises.append("g") | |
220 .attr("class", "x axis") | |
221 .attr("transform", "translate(0," + (axisHeight) + ")") | |
222 .call(expAxis) | |
223 .selectAll("text") | |
224 .style("text-anchor", "start") | |
225 .attr("y", 0) | |
226 .attr("x", 9) | |
227 .attr("dy", ".35em") | |
228 .attr("transform", function(d) { | |
229 return "rotate(90)" | |
230 }); | |
231 axises.append("text") | |
232 .attr("transform", "translate(" + (width / 2) + " ," + (height + (margin.bottom/2)) + ")") | |
233 .style("text-anchor", "middle") | |
234 .text("Shutter Speed (Exposure) in Seconds"); | |
235 svg.append("text") | |
236 .attr("transform", "rotate(-90)") | |
237 .attr("y", 0 - margin.left) | |
238 .attr("x",0 - (height / 2)) | |
239 .attr("dy", "1em") | |
240 .style("text-anchor", "middle") | |
241 .text("Focal Length in mm"); | |
242 | |
243 function update() { | |
244 var photos = []; | |
245 for (var key in photo_data) { | |
246 if (photo_data.hasOwnProperty(key)) { | |
247 photos.push(photo_data[key]); | |
248 } | |
249 } | |
250 | |
251 var photosWithExif = photos.filter(function(d) { return d.focal_length != '' && d.exposure != '' && d.f_number != '' && d.iso != '';}); | |
252 saturationScale.domain([d3.min(photosWithExif, function(val) { return val.iso; }), d3.max(photosWithExif, function(val) { return val.iso; })]); | |
253 | |
254 update_graph(photosWithExif); | |
255 update_table(photos); | |
256 update_legend(photosWithExif); | |
257 } | |
258 | |
259 function update_graph(filteredPhotos) { | |
260 flAxisScale.domain([d3.min(filteredPhotos, function(val) { return val.focal_length; }) * 0.9, d3.max(filteredPhotos, function(val) { return val.focal_length; }) * 1.1]); | |
261 flAxis.scale(flAxisScale); | |
262 axises.selectAll(".y.axis").call(flAxis); | |
263 expAxisScale.domain([d3.min(filteredPhotos, function(val) { return val.exposure; }) * 0.9, d3.max(filteredPhotos, function(val) { return val.exposure; }) * 1.1]); | |
264 expAxis.scale(expAxisScale); | |
265 axises.selectAll(".x.axis").call(expAxis) | |
266 .selectAll("text") | |
267 .style("text-anchor", "start") | |
268 .attr("y", 0) | |
269 .attr("x", 9) | |
270 .attr("dy", ".35em") | |
271 .attr("transform", function(d) { | |
272 return "rotate(90)" | |
273 }); | |
274 var circles = circlesG.selectAll("circle") | |
275 .data(filteredPhotos, byID); | |
276 circles.exit().remove(); | |
277 circles.enter() | |
278 .append("circle") | |
279 .on('mouseover', tip.show) | |
280 .on('mouseout', tip.hide) | |
281 // .transition() | |
282 // .duration(750); | |
283 | |
284 circles/*.transition().duration(250)*/ | |
285 .attr("cx", function (d) { return expAxisScale(d.exposure); }) | |
286 .attr("cy", function (d) { return flAxisScale(d.focal_length); }) | |
287 .attr("r", function (d) { return 50 / d.f_number; }) | |
288 .attr("fill", function (d) { var h = make_camera_hue(d.camera); var s = saturationScale(d.iso); return "hsl("+h+","+s+"%,50%)";}) | |
289 .attr("stroke", "#000") | |
290 .attr("stroke-width", 1); | |
291 circles.sort(function(a, b) { var f_sort = a.f_number - b.f_number; if (f_sort != 0) { return f_sort; } else { return a.exposure - b.exposure; } }); | |
292 } | |
293 | |
294 function make_camera_hue(camera) { | |
295 return (hashcode(camera) % 90) * 4; | |
296 } | |
297 | |
298 function update_table(photos) { | |
299 var photoRows = photoTable.selectAll("tr").data(photos, byID); | |
300 photoRows.exit().remove(); | |
301 photoRows.enter() | |
302 .append("tr"); | |
303 photoRows.sort(function(a,b){ return b.timestamp - a.timestamp;}); | |
304 photoCells = photoRows.selectAll("td") | |
305 .data(function(photo) { | |
306 var thumb = '<a href="' + photo.url + '"><img src="' + photo.thumb + '" alt="Photo ' + photo.id + '" /></a>'; | |
307 return [ thumb, photo.camera, photo.exposure_string != '' ? photo.exposure_string + "s" : '', photo.focal_length != '' ? photo.focal_length + "mm" : '', photo.f_number != '' ? "f/" + photo.f_number : '', photo.iso ]; }) | |
308 .enter() | |
309 .append("td") | |
310 .html(function(d) { return d; }); | |
311 } | |
312 | |
313 function update_legend(photos) { | |
314 var legendBlockWidth = 200; | |
315 var legendBlockHeight = 20; | |
316 var cameras = d3.set(photos.map(function(d){ return d.camera ? d.camera : "Unknown"; })).values(); | |
317 legend.attr("width", 400) | |
318 .attr("height", cameras.length * legendBlockHeight + legendBlockHeight); | |
319 var gradients = legendGradients.selectAll("linearGradient").data(cameras, byValue); | |
320 gradients.exit().remove(); | |
321 var newGradients = gradients.enter().append("linearGradient") | |
322 .attr("id", function(d,i) { return "Gradient" + i; }); | |
323 newGradients.append("stop") | |
324 .attr("offset", "0%") | |
325 .attr("stop-color", function(camera){var h = make_camera_hue(camera); return "hsl("+h+","+saturationScale.range()[0]+"%,50%)";}); | |
326 newGradients.append("stop") | |
327 .attr("offset", "100%") | |
328 .attr("stop-color", function(camera){var h = make_camera_hue(camera); return "hsl("+h+","+saturationScale.range()[1]+"%,50%)";}); | |
329 var cameraLegends = legends.selectAll("g").data(cameras, byValue); | |
330 cameraLegends.exit().remove(); | |
331 var newCameraLegends = cameraLegends.enter().append("g") | |
332 newCameraLegends.append("title").text(function(d){return d;}); | |
333 newCameraLegends.append("rect") | |
334 .attr("x", 0) | |
335 .attr("y", function(d,i){ return i * legendBlockHeight; }) | |
336 .attr("width", legendBlockWidth) | |
337 .attr("height", legendBlockHeight) | |
338 .attr("fill", function(d,i) { return "url(#Gradient" + i + ")"; }) | |
339 .on('mouseover', function(d) { setCircleOpacity(d, 0.1); }) | |
340 .on('mouseout', function(d) { setCircleOpacity(d, 1); }); | |
341 newCameraLegends.append("text") | |
342 .text(function(d){ return d; }) | |
343 .attr("x", legendBlockWidth + 5) | |
344 .attr("y", function(d,i) { return i * legendBlockHeight + (legendBlockHeight / 4) * 3; }); | |
345 } | |
346 | |
347 function setCircleOpacity(camera, opacity) { | |
348 // Set opacity of all circles that AREN'T photos from this camera to highlight the ones that are | |
349 circlesG.selectAll("circle") | |
350 .attr('opacity', function (d) { return (d.camera != camera) ? opacity : 1; }); | |
351 } |