Mercurial > repos > other > exif-graphr
view exif-graphr.js @ 11:aaf1d6954a2a default tip
Ensure we always have data dir for ExifGraphr cache
author | IBBoard <dev@ibboard.co.uk> |
---|---|
date | Sun, 13 Aug 2023 16:56:40 +0100 |
parents | a42497c3964c |
children |
line wrap: on
line source
var debug = false; var photo_data = {}; document.getElementById('submit').onclick = visualise; document.getElementById('username').onkeypress = function() { if (event.keyCode==13) { visualise(); return false; } } /* Helper functions */ function byID(d) { return d.id; } function byValue(d) { return d; } function makeUrl(method, vals) { vals = vals || {}; var parts = []; if (debug) { for (var key in vals) { parts.push(encodeURIComponent(key) + '-' + encodeURIComponent(vals[key])); } var extra = parts.join('-'); if (extra != '') { extra = '-' + extra; } var url = "./data-" + method + extra + ".json"; } else { for (var key in vals) { parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(vals[key])); } var extra = parts.join('&'); if (extra != '') { extra = '&' + extra; } var url = "https://ibboard.co.uk/exif-graphr/api.php?method=" + method + extra; } return url; } /* The real code */ function visualise() { photo_data = {}; var userFoundFunc = function(error,data) { if (error) { alert(error.responseText ? error.responseText : error.statusText); return; } if (data.stat != 'ok') { if (data.message) { alert(data.message); } else { alert("Unknown error"); } return; } var nsid = getNSID(data); getPhotos(nsid); } var username = document.getElementById('username').value; if (/[0-9]+@N[0-9]+/.test(username)) { getPhotos(username); } else if (/https?:\/\/www\.flickr\.com\/photos\/[0-9a-z]+(\/.*)+/.test(username)) { getNSID = function(data) { return data.user.id; }; d3.json(makeUrl('flickr.urls.lookupUser', { 'url': username }), userFoundFunc); } else { getNSID = function(data) { return data.user.nsid; }; d3.json(makeUrl('flickr.people.findByUsername', { 'username': username }), userFoundFunc); } } function getPhotos(nsid) { d3.json(makeUrl('flickr.people.getPublicPhotos', { 'user_id': nsid, 'per_page' : 100, 'extras': 'url_t' }), function(error, data) { data.photos.photo.forEach(getExif); }); } function getExif(photo) { var photo_id = photo.id; var thumb_url = photo.url_t; var url = "https://flickr.com/photos/" + photo.owner + "/" + photo_id; d3.json(makeUrl('flickr.photos.getExif', { 'photo_id' : photo_id }), function(error, data) { if (error || data.code) { console.log("Error fetching details for " + photo_id + ": " + data.message); return; } var exifs = data.photo.exif; var len = exifs.length; var exposure = ''; var f_number = ''; var iso = ''; var focal_length = ''; var timestamp = ''; var make = ''; var model = ''; for (var i = 0; i < len; i++) { var exif = exifs[i]; if (exif.tagspace == 'ExifIFD') { if (exif.tag == 'ExposureTime') { if (exif.clean) { exposure = exif.clean._content; } else if (/^[0-9]+(\.[0-9]+)?$/.test(exif.raw._content)) { exposure = exif.raw._content + " sec (" + exif.raw._content + ")"; } else { console.log("No clean exposure time for "+photo.id+". Raw was "+exif.raw._content); continue; } } else if (exif.tag == 'FNumber') { f_number = exif.raw._content; } else if (exif.tag == 'ISO') { iso = exif.raw._content; } else if (exif.tag == 'FocalLength') { if (/^[0-9\.]+ mm/.test(exif.raw._content)) { focal_length = parseInt(exif.raw._content); } } else if (exif.tag == 'DateTimeOriginal') { 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 ')); } } else if (exif.tagspace == 'IFD0') { if (exif.tag == 'Make') { make = exif.raw._content; } else if (exif.tag == 'Model') { model = exif.raw._content; } else if (exif.tag == 'FocalLength') { if (!focal_length) { focal_length = exif.raw._content.replace(/^([0-9]+(\.[0-9]+)?).*$/, '$1'); } } else if (exif.tag == 'ExposureTime') { if (!exposure) { exposure = exif.raw._content + " sec (" + exif.raw._content + ")"; } } else if (exif.tag == 'FNumber') { if (!f_number) { f_number = Math.round(exif.raw._content / 100); /* Trial and error guess - it is a rational number */ } } } } var exp_matches = exposure.match(/^(1\/)?([0-9\.]+) sec( \(([^\)]+)\))?/); var exp_parts = [ '', '' ]; if (exp_matches) { if (exp_matches[1]) { // We got a non-decimal numeric version exp_parts[0] = 1 / exp_matches[2]; } else { exp_parts[0] = exp_matches[2]; } if (exp_matches[4]) { exp_parts[1] = exp_matches[4]; var exp_fraction = exp_parts[1].match(/^1\/([0-9]+)$/); if (exp_fraction) { //Override for accuracy - the "clean" version rounds too much - 1/909 through 1/2000 ⇒ 0.001 exp_parts[0] = 1 / exp_fraction[1]; } } else { exp_parts[1] = exp_parts[0] >= 1 ? exp_parts[0] : "1/"+(1/exp_parts[0]); } } var exp_sec = exp_parts[0]; var exp_str = exp_parts[1]; var camera = ''; make = make.replace(/ CORPORATION$/, ''); if (make && model && !(model.indexOf(make) === 0)) { camera = make + " " + model; } else { camera = model; } 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 }; update(); }); } function clone(obj) { if (null == obj || "object" != typeof obj) return obj; var copy = obj.constructor(); for (var attr in obj) { if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]); } return copy; } function hashcode(s){ return s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0); } var margin = {top: 40, right: 20, bottom: 50, left: 55}, width = 960 - margin.left - margin.right, height = 600 - margin.top - margin.bottom, axisHeight = height - margin.bottom; var svg = d3.select("#graph").append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); var photoTable = d3.select("#photoTable"); var legend = d3.select("#legend").append("svg").attr("height", 0); var legendGradients = legend.append("defs"); var legends = legend.append("g"); var tip = d3.tip() .attr('class', 'd3-tip') .offset([-10, 0]) .html(function(d) { 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; }); svg.call(tip); var flAxisScale = d3.scaleLinear() .rangeRound([axisHeight, 0]); var flAxis = d3.axisLeft(flAxisScale); var expAxisScale = d3.scaleLog() .rangeRound([0, width]); var expAxis = d3.axisTop(expAxisScale) .tickFormat(function(d) { if (d < 1) { return "1∕" + (1 / d).toFixed(2).replace(/\.?0+$/, ''); } else { return d; } }); var circlesG = svg.append("svg:g"); var axises = svg.append("svg:g"); var saturationScale = d3.scaleLog().range([30,90]); axises.append("g") .attr("class", "y axis") .call(flAxis); axises.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + (axisHeight) + ")") .call(expAxis) .selectAll("text") .style("text-anchor", "start") .attr("y", 0) .attr("x", 9) .attr("dy", ".35em") .attr("transform", function(d) { return "rotate(90)" }); axises.append("text") .attr("transform", "translate(" + (width / 2) + " ," + (height + (margin.bottom/2)) + ")") .style("text-anchor", "middle") .text("Shutter Speed (Exposure) in Seconds"); svg.append("text") .attr("transform", "rotate(-90)") .attr("y", 0 - margin.left) .attr("x",0 - (height / 2)) .attr("dy", "1em") .style("text-anchor", "middle") .text("Focal Length in mm"); var timeline = d3.select("#timeline").append("svg") .attr("width", 960) .attr("height", 150); var timelineScale = d3.scaleTime().rangeRound([0,960]); var timelineAxis = d3.axisBottom(timelineScale); var timelineAxes = timeline.append("g").attr("transform", "translate(0,75)"); var timelineNodes = timeline.append("g").attr("transform", "translate(0,75)"); var furthestBee = -Infinity; var maxBeeDistance = 70; function update() { var photos = []; for (var key in photo_data) { if (photo_data.hasOwnProperty(key)) { photos.push(photo_data[key]); } } var photosWithExif = photos.filter(function(d) { return d.focal_length != '' && d.exposure != '' && d.f_number != '' && d.iso != '';}); saturationScale.domain([d3.min(photosWithExif, function(val) { return val.iso; }), d3.max(photosWithExif, function(val) { return val.iso; })]); update_graph(photosWithExif); update_table(photos); update_legend(photosWithExif); update_beeswarm(photosWithExif); } function update_graph(filteredPhotos) { flAxisScale.domain([0, d3.max(filteredPhotos, function(val) { return val.focal_length; }) * 1.1]); flAxis.scale(flAxisScale); axises.selectAll(".y.axis").call(flAxis); expAxisScale.domain([d3.min(filteredPhotos, function(val) { return val.exposure; }) * 0.9, d3.max(filteredPhotos, function(val) { return val.exposure; }) * 1.1]); expAxis.scale(expAxisScale); axises.selectAll(".x.axis").call(expAxis) .selectAll("text") .style("text-anchor", "start") .attr("y", 0) .attr("x", 9) .attr("dy", ".35em") .attr("transform", function(d) { return "rotate(90)" }); var circles = circlesG.selectAll("circle") .data(filteredPhotos, byID); circles.exit().remove(); var circleEnter = circles.enter() .append("circle") .on('mouseover', tip.show) .on('mouseout', tip.hide) // .transition() // .duration(750); circles/*.transition().duration(250)*/ .merge(circleEnter) .attr("cx", function (d) { return expAxisScale(d.exposure); }) .attr("cy", function (d) { return flAxisScale(d.focal_length); }) .attr("r", function (d) { return 50 / d.f_number; }) .attr("fill", function (d) { var h = make_camera_hue(d.camera); var s = saturationScale(d.iso); return "hsl("+h+","+s+"%,50%)";}) .attr("stroke", "#000") .attr("stroke-width", 1); 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; } }); } function make_camera_hue(camera) { return (hashcode(camera) % 90) * 4; } function update_table(photos) { var photoRows = photoTable.selectAll("tr").data(photos, byID); photoRows.exit().remove(); photoRows.enter() .append("tr"); photoRows.sort(function(a,b){ return b.timestamp - a.timestamp;}); photoCells = photoRows.selectAll("td") .data(function(photo) { var thumb = '<a href="' + photo.url + '"><img src="' + photo.thumb + '" alt="Photo ' + photo.id + '" /></a>'; 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 ]; }) .enter() .append("td") .html(function(d) { return d; }); } function update_legend(photos) { var legendBlockWidth = 200; var legendBlockHeight = 20; var cameras = d3.set(photos.map(function(d){ return d.camera ? d.camera : "Unknown"; })).values(); legend.attr("width", 400) .attr("height", cameras.length * legendBlockHeight + legendBlockHeight); var gradients = legendGradients.selectAll("linearGradient").data(cameras, byValue); gradients.exit().remove(); var newGradients = gradients.enter().append("linearGradient") .attr("id", function(d,i) { return "Gradient" + i; }); newGradients.append("stop") .attr("offset", "0%") .attr("stop-color", function(camera){var h = make_camera_hue(camera); return "hsl("+h+","+saturationScale.range()[0]+"%,50%)";}); newGradients.append("stop") .attr("offset", "100%") .attr("stop-color", function(camera){var h = make_camera_hue(camera); return "hsl("+h+","+saturationScale.range()[1]+"%,50%)";}); var cameraLegends = legends.selectAll("g").data(cameras, byValue); cameraLegends.exit().remove(); var newCameraLegends = cameraLegends.enter().append("g") .on('mouseover', function(d) { setCircleOpacity(d, 0.1); }) .on('mouseout', function(d) { setCircleOpacity(d, 1); }); newCameraLegends.append("title").text(function(d){return d;}); newCameraLegends.append("text") .text(function(d){ return ": " + d; }) //Fudge positioning so that hover opacity doesn't stop between gradient and label .attr("x", legendBlockWidth - 5) .attr("y", function(d,i) { return i * legendBlockHeight + (legendBlockHeight / 4) * 3; }) newCameraLegends.append("rect") .attr("x", 0) .attr("y", function(d,i){ return i * legendBlockHeight; }) .attr("width", legendBlockWidth) .attr("height", legendBlockHeight) .attr("fill", function(d,i) { return "url(#Gradient" + i + ")"; }) } function setCircleOpacity(camera, opacity) { // Set opacity of all circles that AREN'T photos from this camera to highlight the ones that are circlesG.selectAll("circle") .attr('opacity', function (d) { return (d.camera != camera) ? opacity : 1; }); } function update_beeswarm(photos) { var datedPhotos = photos.filter(function(d) { return d.timestamp != ''; }); var domain = [d3.min(datedPhotos, function(val) { return new Date(val.timestamp); }), d3.max(datedPhotos, function(val) { return new Date(val.timestamp); })]; var domainSpread = domain[1].getTime() - domain[0].getTime(); domain = [new Date(domain[0].getTime() - (domainSpread * 0.01)), new Date(domain[1].getTime() + (domainSpread * 0.01))]; timelineScale.domain(domain); timelineAxes.call(timelineAxis); var swarm = d3.beeswarm() .data(datedPhotos) .distributeOn(function (d) { return timelineScale(new Date(d.timestamp)); }) .radius(4) .orientation('horizontal') .side('symetric') .arrange(); furthestBee = d3.max(swarm, function(bee) { return Math.abs(bee.y);}); var nodes = timelineNodes.selectAll('circle') .data(swarm); nodes.exit().remove(); var newNodes = nodes.enter().append('circle'); nodes.merge(newNodes) .attr('cx', function(bee) { return bee.x; }) .attr('cy', function(bee) { return calculateBeeY(bee); }) .attr('r', 4) .attr("fill", function (bee) { var d = bee.datum; var h = make_camera_hue(d.camera); var s = saturationScale(d.iso); return "hsl("+h+","+s+"%,50%)";}) .attr("stroke", "#000") .attr("stroke-width", 1); } function calculateBeeY(bee) { if (furthestBee < maxBeeDistance) { return bee.y; } else { return bee.y % maxBeeDistance; } }