From af4b638e22f440bfbaaa09501be83d5d92b32064 Mon Sep 17 00:00:00 2001 From: Matt Grifith Date: Mon, 11 May 2020 14:28:57 -0500 Subject: [PATCH 1/7] Pin pyshp library as >2.0 is a breaking api change. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6dc0596..07dc6f3 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ package_data={'': ['**/**/*.js','**/**/*.html','**/**/*.css','**/**/*.png']}, scripts=['scan_route_plotter'], install_requires=[ - 'pyshp', + 'pyshp (1.2.12)', 'cefpython3', 'lxml', 'numpy' From 72e55d161b5da543f1b1183a5708c29f469dcdd9 Mon Sep 17 00:00:00 2001 From: Matt Grifith Date: Wed, 27 May 2020 13:27:27 -0500 Subject: [PATCH 2/7] Initial update to new pyshp api --- RoutePlotter/SHPParse.py | 126 +++++++++++++++++++++------------------ setup.py | 2 +- 2 files changed, 70 insertions(+), 58 deletions(-) diff --git a/RoutePlotter/SHPParse.py b/RoutePlotter/SHPParse.py index 2bf868c..dfb55c5 100644 --- a/RoutePlotter/SHPParse.py +++ b/RoutePlotter/SHPParse.py @@ -1,25 +1,29 @@ import shapefile import os -SHAPE_TYPES={'Polygon':5,'Point':1} -KEYS = 'lon','lat' +SHAPE_TYPES = {'Polygon': 5, 'Point': 1} +KEYS = 'lon', 'lat' + + def findPointCoords(shpfile): shpf = shapefile.Reader(shpfile) coords = [] for shape in shpf.shapes(): if shape.shapeType == SHAPE_TYPES['Point']: - coords.append(dict(zip(KEYS,shape.points[0]))) + coords.append(dict(zip(KEYS, shape.points[0]))) return coords + def findPolyCoords(shpfile): shpf = shapefile.Reader(shpfile) coords = [] for shape in shpf.shapes(): if shape.shapeType == SHAPE_TYPES['Polygon']: - coords+=[[dict(zip(KEYS,p))for p in shape.points]] + coords += [[dict(zip(KEYS, p)) for p in shape.points]] return coords -def findRegionType(shpfile,types_to_check=("Polygon","Point")): + +def findRegionType(shpfile, types_to_check=("Polygon", "Point")): shpf = shapefile.Reader(shpfile) for shape in shpf.shapes(): for t in types_to_check: @@ -27,45 +31,48 @@ def findRegionType(shpfile,types_to_check=("Polygon","Point")): return t return None + def findMeta(shpfile): shpf = shapefile.Reader(shpfile) keys = [key[0] for key in shpf.fields[1:]] - out = {key:[] for key in keys} + out = {key: [] for key in keys} for record in shpf.records(): - for idx,value in enumerate(record): + for idx, value in enumerate(record): out[keys[idx]].append(value) return out -def coordDictListToCoord2DList(coord_dict_list,alt=0): - coords = list(map(lambda c:[c['lon'],c['lat'],alt],coord_dict_list)) - nested_coords = [[p1,p2]for p1,p2 in zip(coords[::2],coords[1::2])] + +def coordDictListToCoord2DList(coord_dict_list, alt=0): + coords = list(map(lambda c: [c['lon'], c['lat'], alt], coord_dict_list)) + nested_coords = [[p1, p2] for p1, p2 in zip(coords[::2], coords[1::2])] return [coords] -def planOutlineFromCoords(fname,regions,alt,approach,bearing,sidelap, - inst,names,vehic='fullscale',units='US'): - polyw = shapefile.Writer(shapefile.POLYGON) - polyw.field('name','C',40) - polyw.field('alt','F',12) - polyw.field('approach','F',12) - polyw.field('bearing','F',12) - polyw.field('sidelap','F',12) - polyw.field('inst','C',40) - polyw.field('frame','S',6) - polyw.field('fov','S',8) - polyw.field('ifov','S',8) - polyw.field('pixels','F',10) - polyw.field('vehicle','C',20) - polyw.field('units','C',10) - for area,name in zip(regions,names): - bounds = coordDictListToCoord2DList(area,0) - polyw.poly(parts=bounds) +def planOutlineFromCoords(fname, regions, alt, approach, bearing, sidelap, + inst, names, vehic='fullscale', units='US'): + polyw = shapefile.Writer(fname, shapeType=shapefile.POLYGON) + polyw.field('name', 'C', 40) + polyw.field('alt', 'F', 12) + polyw.field('approach', 'F', 12) + polyw.field('bearing', 'F', 12) + polyw.field('sidelap', 'F', 12) + polyw.field('inst', 'C', 40) + polyw.field('frame', 'S', 6) + polyw.field('fov', 'S', 8) + polyw.field('ifov', 'S', 8) + polyw.field('pixels', 'F', 10) + polyw.field('vehicle', 'C', 20) + polyw.field('units', 'C', 10) + for area, name in zip(regions, names): + bounds = coordDictListToCoord2DList(area, 0) + polyw.poly(bounds) + #polyw.poly(parts=bounds) polyw.record( name, alt, approach, bearing, - sidelap*100, + sidelap * 100, inst.name, inst.frame, inst.fieldOfView, @@ -74,35 +81,40 @@ def planOutlineFromCoords(fname,regions,alt,approach,bearing,sidelap, vehic, units ) - polyw.save(fname) - -def flightPlanFromCoords(outpath,coords,scanlinebounds,alt,speed): + #polyw.save(fname) + polyw.close() +#TODO: alt and speed do not seem to be used in writing shp files? +def flightPlanFromCoords(outpath, coords, scanlinebounds, alt, speed): if not os.path.isdir(outpath): os.makedirs(outpath) - linew = shapefile.Writer(shapefile.POLYLINE) - linew.field('idx','N',10) - footw = shapefile.Writer(shapefile.POLYGON) - footw.field('idx','N',10) - pointw = shapefile.Writer(shapefile.POINT) - pointw.field('idx','N',10) - pointw.field('type','C',40) - for i in range(int(len(coords)/4)): - #write the scan area first - bounds = coordDictListToCoord2DList(scanlinebounds[i],0) - footw.poly(parts=bounds) - footw.record(str(i),'Scanline Bounds') - #then the flight line - line=coordDictListToCoord2DList(coords[4*i:4*i+4]) - linew.poly(parts=line,shapeType=shapefile.POLYLINE) + linew = shapefile.Writer(os.path.join(outpath, 'scanlines'), shapeType=shapefile.POLYLINEZ) + linew.field('idx', 'N', 10) + footw = shapefile.Writer(os.path.join(outpath, 'footprints'), shapeType=shapefile.POLYGONZ) + footw.field('idx', 'N', 10) + pointw = shapefile.Writer(os.path.join(outpath, 'points'), shapeType=shapefile.POINTZ) + pointw.field('idx', 'N', 10) + pointw.field('type', 'C', 40) + for i in range(int(len(coords) / 4)): + # write the scan area first + bounds = coordDictListToCoord2DList(scanlinebounds[i], 0) + #footw.poly(parts=bounds) + footw.polyz(bounds) + footw.record(str(i), 'Scanline Bounds') + # then the flight line + line = coordDictListToCoord2DList(coords[4 * i:4 * i + 4]) + #linew.poly(parts=line, shapeType=shapefile.POLYLINE) + linew.linez(line) linew.record(i) - #then the start/end/entry/exit points of the flight line - pointw.point(*line[0][0]) - pointw.point(*line[0][1]) - pointw.point(*line[0][2]) - pointw.point(*line[0][3]) - [pointw.record(i,p)for p in["START","ENTER","EXIT","END"]] - - linew.save(os.path.join(outpath,'scanlines')) - footw.save(os.path.join(outpath,'footprints')) - pointw.save(os.path.join(outpath,'points')) + # then the start/end/entry/exit points of the flight line + pointw.pointz(*line[0][0]) + pointw.pointz(*line[0][1]) + pointw.pointz(*line[0][2]) + pointw.pointz(*line[0][3]) + [pointw.record(i, p) for p in ["START", "ENTER", "EXIT", "END"]] + linew.close() + footw.close() + pointw.close() + #linew.save(os.path.join(outpath, 'scanlines')) + #footw.save(os.path.join(outpath, 'footprints')) + #pointw.save(os.path.join(outpath, 'points')) diff --git a/setup.py b/setup.py index 07dc6f3..0b96c40 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ package_data={'': ['**/**/*.js','**/**/*.html','**/**/*.css','**/**/*.png']}, scripts=['scan_route_plotter'], install_requires=[ - 'pyshp (1.2.12)', + 'pyshp (2.1.0)', 'cefpython3', 'lxml', 'numpy' From fb82ef5b6a7deb6ca088f299ebe7f7e8dbd7387b Mon Sep 17 00:00:00 2001 From: Matt Grifith Date: Wed, 27 May 2020 13:35:54 -0500 Subject: [PATCH 3/7] Forgot to version up for compatibility with new pyshp api --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0b96c40..88d6ec3 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from distutils.core import setup setup(name='RoutePlotter', - version='1.0', + version='1.1', description='Scanning spectrometer flight-route calculator with gui', author='Matthew Westphall', author_email='w.matthew.he@gmail.com', From b47fc1869ffbccc9ac97e05b26bbc00d8d8bc12f Mon Sep 17 00:00:00 2001 From: Matt Grifith Date: Thu, 28 May 2020 14:28:45 -0500 Subject: [PATCH 4/7] Added Pass count to flight information. --- RoutePlotter/gui/index.html | 3 +++ RoutePlotter/gui/js/map.js | 1 + 2 files changed, 4 insertions(+) diff --git a/RoutePlotter/gui/index.html b/RoutePlotter/gui/index.html index 9f3e1d8..7af7a54 100644 --- a/RoutePlotter/gui/index.html +++ b/RoutePlotter/gui/index.html @@ -390,6 +390,9 @@

Scan Parameters 🛈

Flight Stats 🛈

+ + Number of passes: 0 + diff --git a/RoutePlotter/gui/js/map.js b/RoutePlotter/gui/js/map.js index d371aa6..1578c4d 100644 --- a/RoutePlotter/gui/js/map.js +++ b/RoutePlotter/gui/js/map.js @@ -445,6 +445,7 @@ var setScanSpeed=function(speed){ var createPathCallback= function(coords,bounds,dist,speed,pxsize,scanlines){ coords = _.map(coords,cleanPyCoords); bounds = _.map(bounds,cleanPyCoords); + $('#scan_passes').html(scanlines.length); if(UNITS=='US'){ $('#scan_len').html(Math.round(km2mi(dist))); $('#px_size').html(m2ft(pxsize).toFixed(2)); From ee6394f9395827eaaa2ebdfd030a073b242a3f13 Mon Sep 17 00:00:00 2001 From: Matt Grifith Date: Wed, 1 Jul 2020 20:36:50 -0500 Subject: [PATCH 5/7] Bump version for new features: - You can now set trigger points (percent of approach) to signal start and end of capture - When exporting shp files, there will now be 'trigger_points' and 'trigger_lines' that represent when the spectrometer should start and stop capture. --- RoutePlotter/SHPParse.py | 38 +++++++-- RoutePlotter/ScanArea.py | 151 +++++++++++++++++++++++++++++++++++- RoutePlotter/gui.py | 26 ++++++- RoutePlotter/gui/index.html | 22 +++++- RoutePlotter/gui/js/map.js | 56 ++++++++++--- setup.py | 2 +- 6 files changed, 270 insertions(+), 25 deletions(-) diff --git a/RoutePlotter/SHPParse.py b/RoutePlotter/SHPParse.py index dfb55c5..048c245 100644 --- a/RoutePlotter/SHPParse.py +++ b/RoutePlotter/SHPParse.py @@ -49,7 +49,7 @@ def coordDictListToCoord2DList(coord_dict_list, alt=0): def planOutlineFromCoords(fname, regions, alt, approach, bearing, sidelap, - inst, names, vehic='fullscale', units='US'): + inst, names, vehic='fullscale', units='US', starttrig=0.5, endtrig=0.5): polyw = shapefile.Writer(fname, shapeType=shapefile.POLYGON) polyw.field('name', 'C', 40) polyw.field('alt', 'F', 12) @@ -63,6 +63,8 @@ def planOutlineFromCoords(fname, regions, alt, approach, bearing, sidelap, polyw.field('pixels', 'F', 10) polyw.field('vehicle', 'C', 20) polyw.field('units', 'C', 10) + polyw.field('trigstart', 'F', 6, 2) + polyw.field('trigend', 'F', 6, 2) for area, name in zip(regions, names): bounds = coordDictListToCoord2DList(area, 0) polyw.poly(bounds) @@ -79,12 +81,14 @@ def planOutlineFromCoords(fname, regions, alt, approach, bearing, sidelap, inst.crossFieldOfView, inst.pixels, vehic, - units + units, + starttrig, + endtrig ) #polyw.save(fname) polyw.close() #TODO: alt and speed do not seem to be used in writing shp files? -def flightPlanFromCoords(outpath, coords, scanlinebounds, alt, speed): +def flightPlanFromCoords(outpath, coords, scanlinebounds, alt, speed, trigcoords=None): if not os.path.isdir(outpath): os.makedirs(outpath) linew = shapefile.Writer(os.path.join(outpath, 'scanlines'), shapeType=shapefile.POLYLINEZ) @@ -94,6 +98,18 @@ def flightPlanFromCoords(outpath, coords, scanlinebounds, alt, speed): pointw = shapefile.Writer(os.path.join(outpath, 'points'), shapeType=shapefile.POINTZ) pointw.field('idx', 'N', 10) pointw.field('type', 'C', 40) + + trigpointsw = None + triglinesw = None + if trigcoords: + trigpointsw = shapefile.Writer(os.path.join(outpath, 'trigger_points'), shapeType=shapefile.POINTZ) + triglinesw = shapefile.Writer(os.path.join(outpath, 'trigger_lines'), shapeType=shapefile.POLYLINEZ) + + trigpointsw.field('idx', 'N', 10) + trigpointsw.field('type', 'C', 40) + + triglinesw.field('idx', 'N', 10) + for i in range(int(len(coords) / 4)): # write the scan area first bounds = coordDictListToCoord2DList(scanlinebounds[i], 0) @@ -112,9 +128,19 @@ def flightPlanFromCoords(outpath, coords, scanlinebounds, alt, speed): pointw.pointz(*line[0][3]) [pointw.record(i, p) for p in ["START", "ENTER", "EXIT", "END"]] + if trigcoords: + trigline = coordDictListToCoord2DList(trigcoords[i]) + triglinesw.linez(trigline) + triglinesw.record(i) + + trigpointsw.pointz(*trigline[0][0]) + trigpointsw.pointz(*trigline[0][1]) + for p in ["START","END"]: + trigpointsw.record(i,p) + linew.close() footw.close() pointw.close() - #linew.save(os.path.join(outpath, 'scanlines')) - #footw.save(os.path.join(outpath, 'footprints')) - #pointw.save(os.path.join(outpath, 'points')) + if trigcoords: + triglinesw.close() + trigpointsw.close() \ No newline at end of file diff --git a/RoutePlotter/ScanArea.py b/RoutePlotter/ScanArea.py index 6389092..56e0d3b 100644 --- a/RoutePlotter/ScanArea.py +++ b/RoutePlotter/ScanArea.py @@ -117,6 +117,8 @@ def __init__(self,home,perimeter,spectrometer=None,alt = None,bearing=None, self.setBearing(bearing) self._buildEdges() self._coords = [] + self._trigCoords = [] + self._trigBounds = [] self._alt = alt self._spectrometer = spectrometer self._name = name @@ -124,6 +126,8 @@ def __init__(self,home,perimeter,spectrometer=None,alt = None,bearing=None, self._waypoints = [] self._sidelap = 0 self._overshoot = 0 + self._trigstart = 0.5 + self._trigend = 0.5 self._find_bounds = find_scanline_bounds self._scanline_bounds = [] @@ -134,6 +138,11 @@ def setOvershoot(self,overshoot): #distance to pass beyond borders of scanarea before turning #useful for fullscale flight vehicles self._overshoot = overshoot + def setTrigStart(self, trigstart): + self._trigstart = trigstart + + def setTrigEnd(self,trigend): + self._trigend = trigend def setFindScanLineBounds(self,find_bounds): self._find_bounds = find_bounds @@ -171,6 +180,14 @@ def boundBox(self): def scanLineBoundBoxes(self): return self._scanline_bounds + @property + def triggerBoundBoxes(self): + return self._trigBounds + + @property + def trigCoords(self): + return self._trigCoords + def _computeCenter(self,perimeter): """Find the center of the set of points. This is the cartesian center rather than geographic, hopefully it doesn't make that much of a @@ -246,6 +263,20 @@ def _addScanlineBoundBox(self,scan_edge): self._scanline_bounds.append(bound_box) + def _addTrigLineBoundBox(self,trig_edge): + bound_box = [] + normal_dir = (trig_edge.bearing+90)%360 + antinormal_dir = (normal_dir+180)%360 + bound_box.append(llmath.atDistAndBearing(trig_edge.start, + self._scanline_width/18,normal_dir)) + bound_box.append(llmath.atDistAndBearing(trig_edge.start, + self._scanline_width/18,antinormal_dir)) + bound_box.append(llmath.atDistAndBearing(trig_edge.end, + self._scanline_width/18,antinormal_dir)) + bound_box.append(llmath.atDistAndBearing(trig_edge.end, + self._scanline_width/18,normal_dir)) + + self._trigBounds.append(bound_box) def _findIntersectionsInDirection(self,direction,start,curr_point=None): """Travel self._travel_width meters in direction, then scan for an intersect @@ -316,6 +347,85 @@ def _findIntersectionsInDirection(self,direction,start,curr_point=None): found_intersect = False return dir_coords[1:] + def _findTriggerIntersectionsInDirection(self, direction, start, curr_point=None): + """Travel self._travel_width meters in direction, then scan for an intersect + with an edge both parallel and antiparallel to self._leading_edge. + If no intersect is found in either direction, we've exited the ScanArea + and can return + """ + curr_point = curr_point or llmath.atDistAndBearing( + self._perimeter[0], self._travel_width, direction) + found_intersect = True + + dir_coords = [start] + trig_coords = [] + + # check to make sure we're not going to be creating too many lines + max_point = llmath.atDistAndBearing(curr_point, + self._travel_width * self.MAX_LINES, direction) + for t_dir in self._travel_dir, self._opp_dir: + for edge in self._edges: + intersect = edge.intersection(max_point, t_dir) + if intersect: + # there will be too many lines in the area, throw an error + raise ScanLineDensityError + + while found_intersect: + # don't check the leading edge + new_points = [] + for t_dir in self._travel_dir, self._opp_dir: + for edge in self._edges: + intersect = edge.intersection(curr_point, t_dir) + if intersect: + new_points.append(intersect['point']) + found_intersect = True + + curr_point = llmath.atDistAndBearing( + curr_point, self._travel_width, direction) + # append new_points to dir_coords such that the new_point closer + # to the last coord is added first + if len(new_points) > 0: + # if we cross multiple times (eg in a concave area), just take + # the two most extreme + if len(new_points) > 2: + n = len(new_points) + combos = [(i, j + 1, j - i) for i in range(n) for j in range(i + 1, n)] + new_edge = sorted([Edge(*new_points[slice(*c)]) + for c in combos], key=lambda x: x.length)[-1] + else: + new_edge = Edge(*new_points) + + new_points = new_edge.endpoints + #if self._find_bounds: + # self._addScanlineBoundBox(new_edge) + + # if we have an overshoot, stick 2 additional points beyond + # the extrema + trig_points = [] + if self._overshoot > 0: + tg1 = llmath.atDistAndBearing(new_points[0], + self._overshoot * self._trigstart, (180 + new_edge.bearing) % 360) + tg2 = llmath.atDistAndBearing(new_points[1], + self._overshoot * self._trigend, new_edge.bearing) + new_points = [tg1, *new_points, tg2] + trig_points = [tg1,tg2] + else: + trig_points = [new_points[0],new_points[1]] + if self._find_bounds: + self._addTrigLineBoundBox(Edge(*trig_points)) + + + dists_from_coords = new_edge.distanceTo(dir_coords[-1]) + if dists_from_coords[0] > dists_from_coords[1]: + dir_coords += new_points[::-1] + trig_coords.append(trig_points[::-1]) + else: + dir_coords += new_points + trig_coords.append(trig_points) + else: + found_intersect = False + return trig_coords + def findScanLines(self): self._scanline_bounds = [] self._coords = [self._home] @@ -340,12 +450,16 @@ def findScanLines(self): self._coords += self._findIntersectionsInDirection( parallel_dir1,self._perimeter[2],first_point)[::first_traverse] + self._trigCoords += self._findTriggerIntersectionsInDirection( + parallel_dir1, self._perimeter[2], first_point)[::first_traverse] if first_point is not None: first_point = llmath.atDistAndBearing( self._center,self._travel_width,parallel_dir2) self._coords += self._findIntersectionsInDirection( parallel_dir2,self._coords[-1],first_point) + self._trigCoords += self._findTriggerIntersectionsInDirection( + parallel_dir2, self._coords[-1], first_point) #flip things around so that we start by travelling the shorter #route from home at the beginning @@ -354,6 +468,7 @@ def findScanLines(self): if end_dists[0] > end_dists[1]: print(len(self._coords),end=' ') self._coords = [self._home]+self._coords[1:][::-1] + self._trigCoords = self._trigCoords[::-1] print(len(self._coords)) return self._coords @@ -441,6 +556,9 @@ def __init__(self,home,spectrometer=None,alt = None,bearing=None, self._sidelap = 0 self._coords = None self._overshoot = overshoot + self._trigstart = 0.5 + self._trigend = 0.5 + self._trigCoords = [] self._vehicle = 'quadcopter' self._names = names self._find_bounds = find_scanline_bounds @@ -481,6 +599,16 @@ def setOvershoot(self,overshoot): for sa in self.scanAreas: sa.setOvershoot(self._overshoot) + def setTrigStart(self, trigstart): + self._trigstart = trigstart + for sa in self.scanAreas: + sa.setTrigStart(self._trigstart) + + def setTrigEnd(self, trigend): + self._trigend = trigend + for sa in self.scanAreas: + sa.setTrigEnd(self._trigend) + def setSidelap(self,sidelap): self._sidelap = sidelap for sa in self.scanAreas: @@ -540,6 +668,13 @@ def flattenCoords(self): #and returns to home at the end return flat_coords + def flattenTrigCoords(self): + flat_coords = [] + for coords in self._trigCoords: + flat_coords += coords + + return flat_coords + def findScanLines(self): """Find the scan lines of each ScanArea, then chain them together""" self._coords = [] @@ -549,6 +684,7 @@ def findScanLines(self): sa.setHome(home) new_coords = sa.findScanLines()[1:] self._coords.append(new_coords) + self._trigCoords.append(sa.trigCoords) home = self._coords[-1][-1] if self._vehicle == 'quadcopter': @@ -581,6 +717,17 @@ def scanLineBoundBoxes(self): boxes += sa.scanLineBoundBoxes return boxes + @property + def triggerBoundBoxes(self): + boxes = [] + for sa in self._scanareas: + boxes += sa.triggerBoundBoxes + return boxes + + @property + def triggerCoords(self): + return self._trigCoords + def plot(self,show=True): for sa in self.scanAreas[:-1]: sa.plot(show=False,include=['perimeter','bounds']) @@ -606,7 +753,7 @@ def toShapeFile(self,fname): self.findScanLines() speed = self._spectrometer.squareScanSpeedAt(self._alt) SHPParse.flightPlanFromCoords(fname, - self.flattenCoords(),self.scanLineBoundBoxes,self._alt,speed) + self.flattenCoords(), self.scanLineBoundBoxes, self._alt, speed, self.flattenTrigCoords()) def toProjectShapeFile(self,fname,units): """ @@ -620,7 +767,7 @@ def toProjectShapeFile(self,fname,units): print(self._spectrometer._name) SHPParse.planOutlineFromCoords(fname,perims,self._alt,self._overshoot, self._bearing,self._sidelap,self._spectrometer,names, - self._vehicle,units) + self._vehicle,units,self._trigstart,self._trigend) @classmethod def fromProjectShapeFile(Cls,shp_fname,home=None): diff --git a/RoutePlotter/gui.py b/RoutePlotter/gui.py index f5aa442..99507ff 100644 --- a/RoutePlotter/gui.py +++ b/RoutePlotter/gui.py @@ -93,7 +93,9 @@ def __init__(self): self._region = None self._spectrometer = None self._vehicle="fullscale" - self._overshoot = 30 + self._overshoot = 30 + self._trigstart = 0.5 + self._trigend = 0.5 self._sidelap = .2 self._names = [] self.p = None @@ -110,6 +112,14 @@ def setOvershoot(self,val): if(self._isfloat(val)): self._overshoot = max(1,float(val)) + def setTrigStart(self,val): + if(self._isfloat(val)): + self._trigstart = float(val)/100.0 + + def setTrigEnd(self,val): + if(self._isfloat(val)): + self._trigend = float(val)/100.0 + def setSidelap(self,val): if(self._isfloat(val)): self._sidelap = float(val)/100. @@ -177,13 +187,19 @@ def finishLoad(self,fname): self._bearing = meta['bearing'][0] self._sidelap = meta['sidelap'][0] self._overshoot = meta['approach'][0] + try: + self._trigstart = float(meta['trigstart'][0]) + self._trigend = float(meta['trigend'][0]) + except: + self._trigstart = 0.5 + self._trigend = 0.5 #print(self._alt,self._bearing,self._sidelap,self._overshoot) self._spectrometer = spectrometer self.js_callback.Call(coords,self._vehicle,self._alt,self._bearing, self._sidelap*100,self._overshoot, spectrometer.fieldOfView, spectrometer.crossFieldOfView,spectrometer._px, - spectrometer._name,meta['name'] + spectrometer._name,meta['name'],self._trigstart,self._trigend ) def polygonizePoints(self,points,js_callback): @@ -207,6 +223,8 @@ def createPath(self,coords,js_callback,err_callback): region.setBearing(self._bearing) region.setSidelap(self._sidelap) region.setOvershoot(self._overshoot) + region.setTrigStart(self._trigstart) + region.setTrigEnd(self._trigend) region.setFindScanLineBounds(True) scanner = self._spectrometer or Spectrometer.HeadwallNanoHyperspec() @@ -216,12 +234,14 @@ def createPath(self,coords,js_callback,err_callback): try: region.findScanLines() coords = region.flattenCoords() + trigCoords = region.flattenTrigCoords() bounds= region.boundBox scanlines=region.scanLineBoundBoxes + trigBoxes = region.triggerBoundBoxes dist = "%.2f"%(region.totalScanLength/1000) speed = "%.2f"%region.scanVelocity self._region = region - js_callback.Call(coords,bounds,dist,speed,px_size,scanlines) + js_callback.Call(coords,bounds,dist,speed,px_size,scanlines,trigCoords,trigBoxes) except ScanArea.ScanLineDensityError: err_callback.Call() diff --git a/RoutePlotter/gui/index.html b/RoutePlotter/gui/index.html index 7af7a54..abb6802 100644 --- a/RoutePlotter/gui/index.html +++ b/RoutePlotter/gui/index.html @@ -104,6 +104,16 @@

Parameters Help

For a quadcopter, this can usually be set to a small value, but for an airplane a larger value can give a pilot more time to align their flight path.

+

+ Trigger Start specifies where along the approach to + trigger the spectrometer to start. 100% starts the spectrometer as soon as approach + begins, and 0% starts at the exact edge of the scan area. +

+

+ Trigger Stop specifies where along the approach to + trigger the spectrometer to stop. 100% starts the spectrometer as the end of the exit approach + end, and 0% stops at the exact edge of the scan area. +

Bearing is the angle at which scan lines will pass through the scan area. It is given relative to North. @@ -375,11 +385,19 @@

Scan Parameters 🛈

Distance (mi):
- + - + + + + + + + + + diff --git a/RoutePlotter/gui/js/map.js b/RoutePlotter/gui/js/map.js index 1578c4d..251299c 100644 --- a/RoutePlotter/gui/js/map.js +++ b/RoutePlotter/gui/js/map.js @@ -312,14 +312,20 @@ var setHomeMarker = function(latlng){ var scanLines=[]; var scanLineBounds=[]; +var trigLines=[]; +var trigLinesBounds=[]; var scanPath; -var setAirplaneScanPath = function(latlngs,scanlines){ + + +var setAirplaneScanPath = function(latlngs,scanlines,triggLines,triggBounds){ if(scanPath) scanPath.setMap(null); if(homeMarker) homeMarker.setMap(null); if(scanLines.length > 0){ _.each(scanLines,(line)=>line.setMap(null)); _.each(scanLineBounds,(box)=>box.setMap(null)); + _.each(trigLines,(line)=>line.setMap(null)); + _.each(trigLinesBounds,(box)=>box.setMap(null)); }; _.each(_.groupBy(latlngs,(ll,idx)=>Math.floor(idx/4)),function(lls,idx){ @@ -333,13 +339,31 @@ var setAirplaneScanPath = function(latlngs,scanlines){ zIndex:999 })); scanLineBounds.push(new google.maps.Polygon({ - paths:_.map(scanlines[idx],(c)=>cleanPyCoords(c)), - strokeColor:'#0000ff', - strokeOpacity:0, - strokeWeight:0, - fillColor: '#4B0082', - fillOpacity: 0.40, - map:map + paths:_.map(scanlines[idx],(c)=>cleanPyCoords(c)), + strokeColor:'#0000ff', + strokeOpacity:0, + strokeWeight:0, + fillColor: '#4B0082', + fillOpacity: 0.40, + map:map, + })); + trigLines.push(new google.maps.Polyline({ + path:_.map(triggLines[idx],(c)=>cleanPyCoords(c)), + geodesic:true, + strokeColor: '#00FF00', + strokeOpacity:1.0, + strokeWeight: 4, + map:map, + zIndex:999 + })); + trigLinesBounds.push(new google.maps.Polygon({ + paths:_.map(triggBounds[idx],(c)=>cleanPyCoords(c)), + strokeColor:'#0000ff', + strokeOpacity:0, + strokeWeight:0, + fillColor: '#00CC82', + fillOpacity: 0.20, + map: map })); }); @@ -442,7 +466,7 @@ var setScanSpeed=function(speed){ } }; -var createPathCallback= function(coords,bounds,dist,speed,pxsize,scanlines){ +var createPathCallback= function(coords,bounds,dist,speed,pxsize,scanlines,trigCoords,trigBoxes){ coords = _.map(coords,cleanPyCoords); bounds = _.map(bounds,cleanPyCoords); $('#scan_passes').html(scanlines.length); @@ -455,7 +479,7 @@ var createPathCallback= function(coords,bounds,dist,speed,pxsize,scanlines){ } setScanSpeed(speed); if($('#vehicle').val()=='fullscale') - setAirplaneScanPath(coords,scanlines); + setAirplaneScanPath(coords,scanlines,trigCoords,trigBoxes); else setScanPath(coords); @@ -475,7 +499,7 @@ var createPathFailedCallback = function(){ } var loadFileCallback = function(coords,vehicle,alt,bearing,sidelap, - overshoot,fov,ifov,px,s_name,p_names){ + overshoot,fov,ifov,px,s_name,p_names,trigstart,trigend){ if(UNITS=='US'){ alt = m2ft(alt); overshoot = km2mi(overshoot/1000); @@ -493,6 +517,8 @@ var loadFileCallback = function(coords,vehicle,alt,bearing,sidelap, $('#ifov').val(ifov); $('#px').val(px); $('#spectrometer').val(s_name); + $('#trigstart').val(trigstart*100); + $('#trigend').val(trigend*100); _.each(coords,function(perim,idx){ perim = _.map(perim,cleanPyCoords); userDrawnRegion.addPolyFromCoords(perim,p_names[idx]); @@ -594,6 +620,14 @@ $(document).ready(function(){ external.setOvershoot(1000*$(this).val()); generatePath(); }); + $('#trigstart').change(function(){ + external.setTrigStart($(this).val()); + generatePath(); + }); + $('#trigend').change(function(){ + external.setTrigEnd($(this).val()); + generatePath(); + }); $('#sidelap').change(function(){ external.setSidelap($(this).val()); generatePath(); diff --git a/setup.py b/setup.py index 88d6ec3..26386ae 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from distutils.core import setup setup(name='RoutePlotter', - version='1.1', + version='1.2', description='Scanning spectrometer flight-route calculator with gui', author='Matthew Westphall', author_email='w.matthew.he@gmail.com', From 48e1cacbfe1558e6c716aaef5fb0754f62da87f8 Mon Sep 17 00:00:00 2001 From: Matt Grifith Date: Fri, 17 Jul 2020 11:51:26 -0500 Subject: [PATCH 6/7] Pin all library dependancies --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 26386ae..98eb70d 100644 --- a/setup.py +++ b/setup.py @@ -11,9 +11,9 @@ package_data={'': ['**/**/*.js','**/**/*.html','**/**/*.css','**/**/*.png']}, scripts=['scan_route_plotter'], install_requires=[ - 'pyshp (2.1.0)', - 'cefpython3', - 'lxml', - 'numpy' + 'pyshp==2.1.0', + 'cefpython3==66.0', + 'lxml==4.5.2', + 'numpy==1.19.0' ], ) From 2ee77d8e9fb332ae373fbdce48a9bedaea6b37a9 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 17 Jul 2020 12:02:15 -0500 Subject: [PATCH 7/7] Update README.md --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 44fb8da..58db072 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,15 @@ Pushbroom Planner is a Python3 gui application that generates flight paths for aircraft carrying pushbroom scanners. ## Installation -The installation of this package requires both Python3 and pip3. Python3 comes -with Ubuntu by default, but pip3 must be installed from the package manager -via `sudo apt install python3-pip`. On Windows, Python3 and pip3 can be -downloaded and installed from [the Python website](https://www.python.org/). - +The installation of this package requires both Python3 and pip3. It's +currently targeted at Python 3.7. Python3 comes with Ubuntu by default, +but pip3 must be installed from the package manager via `sudo apt install +python3-pip`. On Windows, Python3 and pip3 can be downloaded and installed +from [the Python website](https://www.python.org/). +If using this application in production as a stand alone program, it's best +to put it in a Python virtual environement, ie: `python3.7 -m venv +ScanlineFlightPlanner` and then `. ScanFlightPlanner/bin/activate`. After installing Python3 and downloading this package, just navigate to the directory of this README and run the command `pip3 install .` A script called `scan_route_plotter` will be added in your `$PATH`, running this script will
Altitude (ft):
Approach (mi):
Trigger Start (%):
Trigger End (%):
Bearing (°):