diff --git a/.gitignore b/.gitignore index 420d7748..5e402b08 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,9 @@ dropin.cache _trial_temp*/ -# our files -/shinysdr/deps/ +# Non-git deps +/shinysdr/deps/require.js +/shinysdr/deps/text.js # GRC-generated shinysdr/test/manual/channel_filter_testbed.py diff --git a/.gitmodules b/.gitmodules index 11557c92..65bbec32 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "shinysdr/deps/measviz"] path = shinysdr/deps/measviz url = https://github.com/kpreid/measviz/ +[submodule "shinysdr/deps/geodesy"] + path = shinysdr/deps/geodesy + url = https://github.com/chrisveness/geodesy diff --git a/shinysdr/deps/geodesy b/shinysdr/deps/geodesy new file mode 160000 index 00000000..2c8900f5 --- /dev/null +++ b/shinysdr/deps/geodesy @@ -0,0 +1 @@ +Subproject commit 2c8900f59ef984695629fd03f1950484cd6eb26f diff --git a/shinysdr/i/network/app.py b/shinysdr/i/network/app.py index dc34c656..a5cb1299 100644 --- a/shinysdr/i/network/app.py +++ b/shinysdr/i/network/app.py @@ -19,6 +19,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals +import StringIO import os import six @@ -48,9 +49,9 @@ from shinysdr.values import SubscriptionContext -def _make_static_resource(pathname): +def _make_static_resource(pathname, cls=static.File): # str() because if we happen to pass unicode as the pathname then directory listings break (discovered with Twisted 16.4.1). - r = static.File(str(pathname), + r = cls(str(pathname), defaultType=b'text/plain', ignoredExts=[b'.html']) r.contentTypes[b'.csv'] = b'text/csv' @@ -172,6 +173,66 @@ def announce(self, open_client): self.__log.info('Visit {url}', url=url) +class ConcatenatedReaders(object): + def __init__(self, files): + self.__files = files + self.__current_file = 0 + + def seek(self, offset): + for i, f in enumerate(self.__files): + f.seek(0, os.SEEK_END) + length = f.tell() + if offset > length: + offset -= length + continue + f.seek(offset, os.SEEK_SET) + self.__current_file = i + return + + def read(self, n=-1): + out = defaultstr("") + while n != 0 and self.__current_file < len(self.__files): + part = self.__files[self.__current_file].read(n) + out += part + if n < 0 or len(part) < n: + self.__current_file += 1 + if self.__current_file < len(self.__files): + self.__files[self.__current_file].seek(0, os.SEEK_SET) + n -= len(part) + return out + + def close(self): + for f in self.__files: + f.close() + + +class WrappedStaticFile(static.File): + prefix = "" + suffix = "" + + def openForReading(self): + f = self.open() + return ConcatenatedReaders([ + StringIO.StringIO(defaultstr(self.prefix)), + f, + StringIO.StringIO(defaultstr(self.suffix)), + ]) + + def getFileSize(self): + return len(self.prefix) + self.getsize() + len(self.suffix) + + +class CommonJSStaticFile(WrappedStaticFile): + """ + Serves a CommonJS-style source file with a RequireJS wrapper. + """ + prefix = """define(function (require, exports, module) { +""" + suffix = """ +}); +""" + + def _put_root_static(wcommon, container_resource): """Place all the simple resources, that are not necessarily sourced from files but at least are unchanging and public.""" @@ -184,6 +245,12 @@ def _put_root_static(wcommon, container_resource): client.putChild(name, _make_static_resource(os.path.join(deps_path, name))) for name in ['measviz.js', 'measviz.css']: client.putChild(name, _make_static_resource(os.path.join(deps_path, 'measviz/src', name))) + geodesy = SlashedResource() + client.putChild('geodesy', geodesy) + for name in ['latlon-spherical.js', 'dms.js']: + geodesy.putChild(name, _make_static_resource( + os.path.join(deps_path, 'geodesy', name), + CommonJSStaticFile)) # Link deps into /test/. test = container_resource.children['test'] @@ -211,11 +278,11 @@ def _put_plugin_resources(client_resource): for resource_def in getPlugins(_IClientResourceDef, shinysdr.plugins): # Add the plugin's resource to static serving plugin_resources.putChild(resource_def.key, resource_def.resource) - plugin_resource_url = '/client/plugins/' + urllib.parse.quote(resource_def.key, safe='') + '/' + plugin_resource_url = 'plugins/' + urllib.parse.quote(resource_def.key, safe='') + '/' # Tell the client to load the plugins # TODO constrain path values to be relative (not on a different origin, to not leak urls) if resource_def.load_css_path is not None: - load_list_css.append(plugin_resource_url + resource_def.load_cs_path) + load_list_css.append('/client/' + plugin_resource_url + resource_def.load_cs_path) if resource_def.load_js_path is not None: # TODO constrain value to be in the directory load_list_js.append(plugin_resource_url + resource_def.load_js_path) diff --git a/shinysdr/i/webparts/block.template.xhtml b/shinysdr/i/webparts/block.template.xhtml index 5f62a301..0e9bad66 100644 --- a/shinysdr/i/webparts/block.template.xhtml +++ b/shinysdr/i/webparts/block.template.xhtml @@ -47,6 +47,7 @@ 'use strict'; requirejs.config({ baseUrl: "/client", + nodeIdCompat: true, }); requirejs(['main'], function (main) { main({ diff --git a/shinysdr/i/webparts/index.template.xhtml b/shinysdr/i/webparts/index.template.xhtml index c0616ab0..23437b71 100644 --- a/shinysdr/i/webparts/index.template.xhtml +++ b/shinysdr/i/webparts/index.template.xhtml @@ -100,6 +100,7 @@ 'use strict'; requirejs.config({ baseUrl: "/client", + nodeIdCompat: true, }); requirejs(['main'], function (main) { main({ diff --git a/shinysdr/i/webstatic/client/coordination.js b/shinysdr/i/webstatic/client/coordination.js index 88ffb83e..005fea95 100644 --- a/shinysdr/i/webstatic/client/coordination.js +++ b/shinysdr/i/webstatic/client/coordination.js @@ -19,10 +19,12 @@ define([ 'require', + 'geodesy/latlon-spherical', './types', './values', ], ( require, + LatLon, import_types, import_values ) => { @@ -36,6 +38,7 @@ define([ ConstantCell, LocalCell, StorageCell, + findImplementersInBlockCell, makeBlock, } = import_values; @@ -115,7 +118,32 @@ define([ // (This will require knowledge of retuning which is currently done implicitly on the server side.) if (record) { + // TODO: The server really needs to track the selected record because both the source and the record can move, and something needs to constantly recalculate the bearing and drive the rotator. + // TODO: Provide a way for the user to disable this (e.g. in EME). selectedRecord.set(record); + if (record.location) { + const componentsCell = radio.source.get().components; + const positionedDevices = findImplementersInBlockCell( + undefined, + componentsCell, + 'shinysdr.devices.IPositionedDevice').get(); + const rotators = findImplementersInBlockCell( + undefined, + componentsCell, + 'shinysdr.plugins.hamlib.IRotator').get(); + if (positionedDevices.length && rotators.length) { + const track = positionedDevices[0].track.get(); + const start = new LatLon(track.latitude.value, track.longitude.value); + let bearing = start.bearingTo(new LatLon(record.location[0], record.location[1])); + rotators.forEach(rotator => { + // TODO: It doesn't seem like I should have to do this here; the azimuth cell should know how to unwrap bearings. + if (bearing > rotator.Azimuth.type.getMax()) { + bearing -= 360; + } + rotator.Azimuth.set(bearing); + }); + } + } } return receiver; @@ -172,4 +200,4 @@ define([ exports.ClientStateObject = ClientStateObject; return Object.freeze(exports); -}); \ No newline at end of file +}); diff --git a/shinysdr/i/webstatic/client/map/map-core.js b/shinysdr/i/webstatic/client/map/map-core.js index c3c78737..5cd6c342 100644 --- a/shinysdr/i/webstatic/client/map/map-core.js +++ b/shinysdr/i/webstatic/client/map/map-core.js @@ -19,6 +19,7 @@ define([ 'require', + 'geodesy/latlon-spherical', '../domtools', '../events', '../gltools', @@ -34,6 +35,7 @@ define([ 'text!./curves-f.glsl', ], ( require, + LatLon, import_domtools, import_events, import_gltools, @@ -91,7 +93,6 @@ define([ const { cos, sin, - asin, atan2 } = Math; @@ -100,10 +101,10 @@ define([ // Degree trig functions. // We use degrees in this module because degrees are standard for latitude and longitude, and are also useful for more exact calculations because 360 is exactly representable as a floating-point number whereas 2π is not. // TODO: Look at the edge cases and see if it would be useful to have dcos & dsin do modulo 360, so we get that exactness for them. - var RADIANS_PER_DEGREE = Math.PI / 180; + const RADIANS_PER_DEGREE = Math.PI / 180; + const DEGREES_PER_RADIAN = 180 / Math.PI; function dcos(x) { return cos(RADIANS_PER_DEGREE * x); } function dsin(x) { return sin(RADIANS_PER_DEGREE * x); } - function dasin(x) { return asin(x)/RADIANS_PER_DEGREE; } function datan2(x, y) { return atan2(x, y)/RADIANS_PER_DEGREE; } function mean(array) { @@ -1692,17 +1693,10 @@ define([ const smoothStep = 1; function greatCircleLineAlong(lat, lon, bearing) { const line = []; - const sinlat = dsin(lat), coslat = dcos(lat); - const sinazimuth = dsin(bearing), cosazimuth = dcos(bearing); + const start = new LatLon(lat, lon); for (let angle = 0; angle < 180 + smoothStep/2; angle += smoothStep) { - // Algorithm adapted from https://github.com/chrisveness/geodesy/blob/v1.1.2/latlon-spherical.js#L212 - const targetLat = dasin( - sinlat*dcos(angle) + - coslat*dsin(angle)*cosazimuth); - const targetLon = lon + datan2( - sinazimuth*dsin(angle)*coslat, - dcos(angle)-sinlat*dsin(targetLat)); - line.push(Object.freeze({position: Object.freeze([targetLat, targetLon])})); + const point = start.destinationPoint(angle, bearing, DEGREES_PER_RADIAN); + line.push(Object.freeze({position: Object.freeze([point.lat, point.lon])})); } return Object.freeze(line); } diff --git a/shinysdr/i/webstatic/test/index.html b/shinysdr/i/webstatic/test/index.html index 3d78641b..1a428d67 100644 --- a/shinysdr/i/webstatic/test/index.html +++ b/shinysdr/i/webstatic/test/index.html @@ -28,16 +28,20 @@
-