|
<!DOCTYPE html> |
|
<head> |
|
<meta charset="utf-8"> |
|
|
|
<script src="//d3js.org/d3.v3.min.js"></script> |
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script> |
|
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script> |
|
|
|
<style> |
|
|
|
path { |
|
fill: none; |
|
stroke-linecap: round; |
|
stroke-linejoin: round; |
|
} |
|
|
|
text { |
|
font: 10px sans-serif; |
|
} |
|
|
|
.horizon { |
|
stroke: #000; |
|
stroke-width: 1.5px; |
|
} |
|
|
|
.graticule { |
|
stroke: #000; |
|
stroke-opacity: .15; |
|
} |
|
|
|
.solar-path { |
|
stroke: #f00; |
|
stroke-width: 2px; |
|
} |
|
|
|
.sun circle { |
|
fill: red; |
|
stroke: #000; |
|
} |
|
|
|
.sun text { |
|
text-anchor: middle; |
|
} |
|
|
|
.ticks--sun circle { |
|
fill: red; |
|
stroke: #fff; |
|
stroke-width: 2px; |
|
} |
|
|
|
.ticks--sun text { |
|
text-shadow: 0 1px 0 #fff, 0 -1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff; |
|
} |
|
|
|
.ticks line { |
|
stroke: #000; |
|
} |
|
|
|
.ticks text { |
|
text-anchor: middle; |
|
} |
|
|
|
.ticks--azimuth text:nth-of-type(9n + 1) { |
|
font-weight: bold; |
|
font-size: 14px; |
|
} |
|
|
|
#waiting { |
|
font: 14px sans-serif; |
|
position: absolute; |
|
top: 540px; |
|
left: 240px; |
|
width: 480px; |
|
margin: auto; |
|
text-align: center; |
|
} |
|
|
|
#waiting b { |
|
font-size: 24px; |
|
line-height: 1.5em; |
|
} |
|
</style> |
|
|
|
<style> |
|
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; } |
|
#inputSliders { font-family:sans-serif;outline:none;margin-top:10px} |
|
.inputgroup {border:none;} |
|
.slider { width:420px;float:left;padding:10px;} |
|
.slider2 { width:290px;float:left;padding:10px;} |
|
label { float:left;font-weight:bold;padding-bottom:10px;} |
|
input[type=range] { float:left;clear:left;margin-right:10px;width:380px;} |
|
.slider2 input[type=range] { float:left;clear:left;margin-right:10px;width:180px;} |
|
input[type=range]::-ms-track { background: transparent;border-color: transparent;color: transparent;-webkit-appearance: none} |
|
input[type=range]::-webkit-slider-runnable-track { height: 5px;background:#7c7c7c; margin-top: -4px;} |
|
input[type=range]::-webkit-slider-thumb { margin-top:-6px;} |
|
#inputSliders p {padding-top:10px;} |
|
</style> |
|
|
|
</head> |
|
|
|
<body> |
|
<div id="inputSliders"> |
|
<form id="sliders" autocomplete="off"> |
|
<fieldset class="inputgroup"> |
|
<div class="slider2"> |
|
<label>Latitude</label> |
|
<input type="range" name="lat" id="lat" value="45" min="-90" max="90" step = "1"><p id="latoutput">45</p></div> |
|
<div class="slider2" > |
|
<label>Longitude</label> |
|
<input type="range" name="lon" id="lon" value="-70" min="-180" max="180" step = "1"><p id="lonoutput">-70</p></div> |
|
<div class="slider2" > |
|
<label>Time Zone</label> |
|
<input type="range" name="tz" id="tz" value="-4" min="-12" max="12" step = "1"><p id="zoneoutput">GMT-4</p></div> |
|
<div class="slider2"> |
|
<label>Month</label> |
|
<input type="range" name="month" id="month" value="3" min="1" max="12" step = "1"><p id="monthoutput">3</p></div> |
|
<div class="slider2" > |
|
<label>Day</label> |
|
<input type="range" name="day" id="day" value="21" min="1" max="31" step = "1"><p id="dayoutput">21</p></div> |
|
<div class="slider2"> |
|
<label>Hour</label> |
|
<input type="range" name="hour" id="hour" value="10.5" min="1" max="24" step = "0.25"><p id="houroutput">10.5</p></div> |
|
</fieldset> |
|
</form> |
|
</div> |
|
|
|
<svg width="960" height="960"></svg> |
|
<script src="solar-calculator.js"></script> |
|
|
|
|
|
<script> |
|
// Inputs |
|
var Lat = parseFloat($("#lat").val()); |
|
var Lon = parseFloat($("#lon").val()); |
|
var TimeZone = parseFloat($("#tz").val()); |
|
var Month = parseFloat($("#month").val()); |
|
var Day = parseFloat($("#day").val()); |
|
var Hour = parseFloat($("#hour").val()); |
|
|
|
// Update the displayed values for the sliders |
|
$("#lat").on("input", function(event) { |
|
Lat = parseFloat($(this).val()); |
|
$("#latoutput").text(Lat.toString()); |
|
refresh(); |
|
}); |
|
$("#lon").on("input", function(event) { |
|
Lon = parseFloat($(this).val()); |
|
$("#lonoutput").text(Lon.toString()); |
|
refresh(); |
|
}); |
|
$("#tz").on("input", function(event) { |
|
TimeZone = parseFloat($(this).val()); |
|
$("#zoneoutput").text('GMT' + TimeZone.toString()); |
|
refresh(); |
|
}); |
|
$("#month").on("input", function(event) { |
|
Month = parseFloat($(this).val()); |
|
$("#monthoutput").text(Month.toString()); |
|
refresh(); |
|
}); |
|
$("#day").on("input", function(event) { |
|
Day = parseFloat($(this).val()); |
|
$("#dayoutput").text(Day.toString()); |
|
refresh(); |
|
}); |
|
$("#hour").on("input", function(event) { |
|
Hour = parseFloat($(this).val()); |
|
$("#houroutput").text(Hour.toString()); |
|
refresh(); |
|
}); |
|
|
|
|
|
// Sunpath |
|
var svg = d3.select("svg"), |
|
width = +svg.attr("width"), |
|
height = +svg.attr("height"), |
|
scale = width * .45; |
|
|
|
var formatTime = d3.time.format("%I"), |
|
formatNumber = d3.format(".1f"), |
|
formatAngle = function(d) { return formatNumber(d) + "°"; }; |
|
|
|
var projection = d3.geo.projection(flippedStereographic) |
|
.scale(scale) |
|
.clipAngle(130) |
|
.rotate([0, -90]) |
|
.translate([width / 2 + .5, height / 2 + .5]) |
|
.precision(.1); |
|
|
|
var path = d3.geo.path() |
|
.projection(projection); |
|
|
|
svg.append("path") |
|
.datum(d3.geo.circle().origin([0, 90]).angle(90)) |
|
.attr("class", "horizon") |
|
.attr("d", path); |
|
|
|
svg.append("path") |
|
.datum(d3.geo.graticule()) |
|
.attr("class", "graticule") |
|
.attr("d", path); |
|
|
|
var ticksAzimuth = svg.append("g") |
|
.attr("class", "ticks ticks--azimuth"); |
|
|
|
ticksAzimuth.selectAll("line") |
|
.data(d3.range(360)) |
|
.enter().append("line") |
|
.each(function(d) { |
|
var p0 = projection([d, 0]), |
|
p1 = projection([d, d % 10 ? -1 : -2]); |
|
|
|
d3.select(this) |
|
.attr("x1", p0[0]) |
|
.attr("y1", p0[1]) |
|
.attr("x2", p1[0]) |
|
.attr("y2", p1[1]); |
|
}); |
|
|
|
ticksAzimuth.selectAll("text") |
|
.data(d3.range(0, 360, 10)) |
|
.enter().append("text") |
|
.each(function(d) { |
|
var p = projection([d, -4]); |
|
|
|
d3.select(this) |
|
.attr("x", p[0]) |
|
.attr("y", p[1]); |
|
}) |
|
.attr("dy", ".35em") |
|
.text(function(d) { return d === 0 ? "N" : d === 90 ? "E" : d === 180 ? "S" : d === 270 ? "W" : d + "°"; }); |
|
|
|
svg.append("g") |
|
.attr("class", "ticks ticks--elevation") |
|
.selectAll("text") |
|
.data(d3.range(10, 91, 10)) |
|
.enter().append("text") |
|
.each(function(d) { |
|
var p = projection([0, d]); |
|
|
|
d3.select(this) |
|
.attr("x", p[0]) |
|
.attr("y", p[1]); |
|
}) |
|
.attr("dy", ".35em") |
|
.text(function(d) { return d + "°"; }); |
|
|
|
|
|
refresh(); |
|
|
|
function refresh() { |
|
var solar = solarCalculator([Lon, Lat]); |
|
|
|
svg.selectAll('.solar-path').remove(); |
|
svg.selectAll('.sun').remove(); |
|
svg.selectAll('.ticks--sun').remove(); |
|
|
|
svg.insert("path", ".sphere") |
|
.attr("class", "solar-path"); |
|
|
|
var sun = svg.insert("g", ".sphere") |
|
.attr("class", "sun"); |
|
|
|
sun.append("circle") |
|
.attr("r", 5); |
|
|
|
sun.append("text") |
|
.attr("class", "sun-label sun-label--azimuth") |
|
.attr("dy", ".71em") |
|
.attr("y", 10); |
|
|
|
sun.append("text") |
|
.attr("class", "sun-label sun-label--elevation") |
|
.attr("dy", "1.81em") |
|
.attr("y", 10); |
|
|
|
var tickSun = svg.insert("g", ".sphere") |
|
.attr("class", "ticks ticks--sun") |
|
.selectAll("g"); |
|
|
|
offset = (new Date().getTimezoneOffset())/60 |
|
now = new Date(2000, Month-1, Day, Hour - TimeZone - offset, (Hour%parseInt(Hour))*60) |
|
start = d3.time.day.floor(now) |
|
end = d3.time.day.offset(start, 1) |
|
|
|
svg.select(".solar-path") |
|
.datum({type: "LineString", coordinates: d3.time.minutes(start, end).map(solar.position)}) |
|
.attr("d", path); |
|
|
|
sun |
|
.datum(solar.position(now)) |
|
.attr("transform", function(d) { return "translate(" + projection(d) + ")"; }); |
|
|
|
sun.select(".sun-label--azimuth") |
|
.text(function(d) { return formatAngle(d[0]) + " φ"; }); |
|
|
|
sun.select(".sun-label--elevation") |
|
.text(function(d) { return formatAngle(d[1]) + " θ"; }); |
|
|
|
tickSun = tickSun |
|
.data(d3.time.hours(start, end), function(d) { return +d; }); |
|
|
|
tickSun.exit().remove(); |
|
|
|
var tickSunEnter = tickSun.enter().append("g") |
|
.attr("transform", function(d) { return "translate(" + projection(solar.position(d)) + ")"; }); |
|
|
|
tickSunEnter.append("circle") |
|
.attr("r", 2.5); |
|
|
|
tickSunEnter.append("text") |
|
.attr("dy", "-.31em") |
|
.attr("y", -6) |
|
.text(function(d) { return d.getHours() + TimeZone + offset}); |
|
} |
|
|
|
|
|
d3.select(self.frameElement).style("height", height + 200 + "px"); |
|
|
|
function flippedStereographic(λ, φ) { |
|
var cosλ = Math.cos(λ), |
|
cosφ = Math.cos(φ), |
|
k = 1 / (1 + cosλ * cosφ); |
|
return [ |
|
k * cosφ * Math.sin(λ), |
|
-k * Math.sin(φ) |
|
]; |
|
} |
|
|
|
</script> |
|
|
|
</body> |