Module imo_001_22_area_notice
[hide private]
[frames] | no frames]

Source Code for Module imo_001_22_area_notice

   1  #!/usr/bin/env python 
   2  __author__    = 'Kurt Schwehr' 
   3  __version__   = '$Revision: 4799 $'.split()[1] 
   4  __revision__  = __version__ # For pylint 
   5  __date__ = '$Date: 2006-09-25 11:09:02 -0400 (Mon, 25 Sep 2006) $'.split()[1] 
   6  __copyright__ = '2009' 
   7  __license__   = 'GPL v3' 
   8  __contact__   = 'kurt at ccom.unh.edu' 
   9   
  10   
  11   
  12  __doc__ =''' 
  13  Trying to do a more sane design for AIS BBM message 
  14   
  15  @requires: U{Python<http://python.org/>} >= 2.6 
  16  @requires: U{epydoc<http://epydoc.sourceforge.net/>} >= 3.0.1 
  17  @requires: U{lxml<http://codespeak.net/lxml/lxmlhtml.html>} >= 2.0 
  18  @requires: U{shapely<http://pypi.python.org/pypi/Shapely/>} 
  19  @requires: U{BitVector<http://pypi.python.org/pypi/BitVector/>} 
  20  @requires: U{pyproj<http://code.google.com/p/pyproj/>} 
  21  @requires: U{geojson<http://pypi.python.org/pypi/geojson/>} 
  22   
  23  @license: GPL v3 
  24  @undocumented: __doc__ 
  25  @since: 2009-Jun-01 
  26  @status: under development 
  27  @organization: U{CCOM<http://ccom.unh.edu/>}  
  28   
  29  @todo: handle polyline and polygons that span multiple subareas 
  30  @todo: handle text that spans adjacent subareas 
  31  ''' 
  32   
  33  # http://blog.lucanatali.it/2006/12/nmea-checksum-in-python.html 
  34   
  35  import sys 
  36  #from decimal import Decimal 
  37  import datetime 
  38  from operator import xor # for checksum 
  39   
  40  import operator 
  41  #from math import * 
  42  import math 
  43   
  44  from pyproj import Proj 
  45  import shapely.geometry 
  46  #import geojson 
  47   
  48  import lxml 
  49  from lxml.html import builder as E 
  50   
  51  from BitVector import BitVector 
  52   
  53  import binary, aisstring 
  54   
  55   
  56  kml_head = '''<?xml version="1.0" encoding="UTF-8"?> 
  57  <kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom"> 
  58  <Document> 
  59  ''' 
  60  'Beginning of a KML file for visualization' 
  61   
  62  kml_tail = '''</Document> 
  63  </kml> 
  64  ''' 
  65  'Finish a kml file' 
66 67 -def lon_to_utm_zone(lon):
68 return int(( lon + 180 ) / 6) + 1
69 70 iso8601_timeformat = '%Y-%m-%dT%H:%M:%SZ' 71 '''ISO time format for NetworkLinkControl strftime 72 @see: U{KML Tutorial<http://code.google.com/apis/kml/documentation/kml_21tutorial.html#updates>} 73 ''' 74 75 76 nmea_talkers = { 77 'AG':'Autopilot - General', 78 'AI':'Automatic Identification System', 79 'AP':'Autopilot - Magnetic', 80 'CC':'Computer - Programmed Calculator (outdated)', 81 'CD':'Communications - Digital Selective Calling (DSC)', 82 'CM':'Computer - Memory Data (outdated)', 83 'CS':'Communications - Satellite', 84 'CT':'Communications - Radio-Telephone (MF/HF)', 85 'CV':'Communications - Radio-Telephone (VHF)', 86 'CX':'Communications - Scanning Receiver', 87 'DE':'DECCA Navigation (outdated)', 88 'DF':'Direction Finder', 89 'EC':'Electronic Chart Display & Information System (ECDIS)', 90 'EP':'Emergency Position Indicating Beacon (EPIRB)', 91 'ER':'Engine Room Monitoring Systems', 92 'GP':'Global Positioning System (GPS)', 93 'HC':'Heading - Magnetic Compass', 94 'HE':'Heading - North Seeking Gyro', 95 'HN':'Heading - Non North Seeking Gyro', 96 'II':'Integrated Instrumentation', 97 'IN':'Integrated Navigation', 98 'LA':'Loran A (outdated)', 99 'LC':'Loran C', 100 'MP':'Microwave Positioning System (outdated)', 101 'OM':'OMEGA Navigation System (outdated)', 102 'OS':'Distress Alarm System (outdated)', 103 'RA':'RADAR and/or ARPA', 104 'SD':'Sounder, Depth', 105 'SN':'Electronic Positioning System, other/general', 106 'SS':'Sounder, Scanning', 107 'TI':'Turn Rate Indicator', 108 'TR':'TRANSIT Navigation System', 109 'VD':'Velocity Sensor, Doppler, other/general', 110 'DM':'Velocity Sensor, Speed Log, Water, Magnetic', # Should this be VD? 111 'VW':'Velocity Sensor, Speed Log, Water, Mechanical', 112 'WI':'Weather Instruments ', 113 'YC':'Transducer - Temperature (outdated)', 114 'YD':'Transducer - Displacement, Angular or Linear (outdated)', 115 'YF':'Transducer - Frequency (outdated)', 116 'YL':'Transducer - Level (outdated)', 117 'YP':'Transducer - Pressure (outdated)', 118 'YR':'Transducer - Flow Rate (outdated)', 119 'YT':'Transducer - Tachometer (outdated)', 120 'YV':'Transducer - Volume (outdated)', 121 'YX':'Transducer', 122 'ZA':'Timekeeper - Atomic Clock', 123 'ZC':'Timekeeper - Chronometer', 124 'ZQ':'Timekeeper - Quartz', 125 'ZV':'Timekeeper - Radio Update, WWV or WWVH', 126 } 127 '''Prefixes for NMEA strings that say where a message originated. http://gpsd.berlios.de/NMEA.txt 128 BBM messages may require having EC as the prefix. 129 ''' 130 131 notice_type = { 132 'cau_mammals_not_obs': 0, 133 'cau_mammals_reduce_speed': 1, 134 'cau_mammals_stay_clear': 2, 135 'cau_mammals_report_sightings': 3, 136 'cau_habitat_reduce_speed': 4, 137 'cau_habitat_stay_clear': 5, 138 'cau_habitat_no_fishing_or_anchoring': 6, 139 'cau_congestion': 8, 140 'cau_event': 9, 141 'cau_divers': 10, 142 'cau_swimmers': 11, 143 'cau_dredging': 12, 144 'cau_surveying': 13, 145 'cau_underwater_ops': 14, 146 'cau_seaplane_ops': 15, 147 'cau_nets_in_water': 16, 148 'cau_cluster_fishing_vessels': 17, 149 'cau_fairway_closed': 18, 150 'cau_harbor_closed': 19, 151 'cau_risk_see_text': 20, 152 'cau_auv_ops': 21, 153 'env_storm_front': 23, 154 'env_ice': 24, 155 'env_storm': 25, 156 'env_wind': 26, 157 'env_waves': 27, 158 'env_restr_vis': 28, 159 'env_currents': 29, 160 'env_icing': 30, 161 'res_no_fishing': 32, 162 'res_no_anchoring': 33, 163 'res_entry_approval_req': 34, 164 'res_no_entry': 35, 165 'res_military_ops': 36, 166 'res_firing_danger': 37, 167 'anc_open': 40, 168 'anc_closed': 41, 169 'anc_prohibited': 42, 170 'anc_deep_draft': 43, 171 'anc_Shallow': 44, 172 'anc_transfer': 45, 173 'sec_1': 56, 174 'sec_2': 57, 175 'sec_3': 58, 176 'dis_adrift': 64, 177 'dis_sinking': 65, 178 'dis_abandoning': 66, 179 'dis_requ_medical': 67, 180 'dis_flooding': 68, 181 'dis_fire_explosion': 69, 182 'dis_grounding': 70, 183 'dis_collision': 71, 184 'dis_listing_capsizing': 72, 185 'dis_under_assault': 73, 186 'dis_person_overboard': 74, 187 'dis_sar': 75, 188 'dis_pollution': 76, 189 'inst_contact_vts_here': 80, 190 'inst_contact_port_admin_here': 81, 191 'inst_do_not_proceed_beyond_here': 82, 192 'inst_await_instr_here': 83, 193 'info_pilot_boarding': 88, 194 'info_icebreaker_staging': 89, 195 'info_refuge': 90, 196 'info_pos_icebreakers': 91, 197 'info_pos_response_units': 92, 198 'chart_sunken_vessel': 96, 199 'chart_Submerged_obj': 97, 200 'chart_Semi_submerged_obj': 98, 201 'chart_shoal': 99, 202 'chart_shoal_due_North': 100, 203 'chart_Shoal_due_North': 100, 204 'chart_Shoal_due_East': 101, 205 'chart_Shoal_due_South': 102, 206 'chart_Shoal_due_West': 103, 207 'chart_channel_obstruction': 104, 208 'chart_reduced_vert_clearance': 105, 209 'chart_bridge_closed': 106, 210 'chart_bridge_part_open': 107, 211 'chart_bridge_fully_open': 108, 212 'report_of_icing': 112, 213 'report_of_see_text': 114, 214 'other_see_text': 125, 215 'cancel_area_notice': 126, 216 'undefined': 127, 217 0: 'Caution Area: Marine mammals NOT observed', 218 1: 'Caution Area: Marine mammals in area - Reduce Speed', 219 2: 'Caution Area: Marine mammals in area - Stay Clear', 220 3: 'Caution Area: Marine mammals in area - Report Sightings', 221 4: 'Caution Area: Protected Habitat - Reduce Speed', 222 5: 'Caution Area: Protected Habitat - Stay Clear', 223 6: 'Caution Area: Protected Habitat - No fishing or anchoring', 224 7: 'Reserved', 225 8: 'Caution Area: Traffic congestion', 226 9: 'Caution Area: Marine event', 227 10: 'Caution Area: Divers down', 228 11: 'Caution Area: Swim area', 229 12: 'Caution Area: Dredge operations', 230 13: 'Caution Area: Survey operations', 231 14: 'Caution Area: Underwater operation', 232 15: 'Caution Area: Seaplane operations', 233 16: 'Caution Area: Fishery - nets in water', 234 17: 'Caution Area: Cluster of fishing vessels', 235 18: 'Caution Area: Fairway closed', 236 19: 'Caution Area: Harbor closed', 237 20: 'Caution Area: Risk - define in free text field', 238 21: 'Caution Area: Underwater vehicle operation', 239 22: 'Reserved', 240 23: 'Storm front (line squall)', 241 24: 'Env. Caution Area: Hazardous sea ice', 242 25: 'Env. Caution Area: Storm warning (storm cell or line of storms)', 243 26: 'Env. Caution Area: High wind', 244 27: 'Env. Caution Area: High waves', 245 28: 'Env. Caution Area: Restricted visibility (fog, rain, etc)', 246 29: 'Env. Caution Area: Strong currents', 247 30: 'Env. Caution Area: Heavy icing', 248 31: 'Reserved', 249 32: 'Restricted Area: Fishing prohibited', 250 33: 'Restricted Area: No anchoring.', 251 34: 'Restricted Area: Entry approval required prior to transit', 252 35: 'Restricted Area: Entry prohibited', 253 36: 'Restricted Area: Active military OPAREA', 254 37: 'Restricted Area: Firing - danger area.', 255 38: 'Reserved', 256 39: 'Reserved', 257 40: 'Anchorage Area: Anchorage open', 258 41: 'Anchorage Area: Anchorage closed', 259 42: 'Anchorage Area: Anchoring prohibited', 260 43: 'Anchorage Area: Deep draft anchorage', 261 44: 'Anchorage Area: Shallow draft anchorage', 262 45: 'Anchorage Area: Vessel transfer operations', 263 46: 'Reserved', 264 47: 'Reserved', 265 48: 'Reserved', 266 49: 'Reserved', 267 50: 'Reserved', 268 51: 'Reserved', 269 52: 'Reserved', 270 53: 'Reserved', 271 54: 'Reserved', 272 55: 'Reserved', 273 56: 'Security Alert - Level 1', 274 57: 'Security Alert - Level 2', 275 58: 'Security Alert - Level 3', 276 59: 'Reserved', 277 60: 'Reserved', 278 61: 'Reserved', 279 62: 'Reserved', 280 63: 'Reserved', 281 64: 'Distress Area: Vessel disabled and adrift', 282 65: 'Distress Area: Vessel sinking', 283 66: 'Distress Area: Vessel abandoning ship', 284 67: 'Distress Area: Vessel requests medical assistance', 285 68: 'Distress Area: Vessel flooding', 286 69: 'Distress Area: Vessel fire/explosion', 287 70: 'Distress Area: Vessel grounding', 288 71: 'Distress Area: Vessel collision', 289 72: 'Distress Area: Vessel listing/capsizing', 290 73: 'Distress Area: Vessel under assault', 291 74: 'Distress Area: Person overboard', 292 75: 'Distress Area: SAR area', 293 76: 'Distress Area: Pollution response area', 294 77: 'Reserved', 295 78: 'Reserved', 296 79: 'Reserved', 297 80: 'Instruction: Contact VTS at this point/juncture', 298 81: 'Instruction: Contact Port Administration at this point/juncture', 299 82: 'Instruction: Do not proceed beyond this point/juncture', 300 83: 'Instruction: Await instructions prior to proceeding beyond this point/juncture', 301 84: 'Reserved', 302 85: 'Reserved', 303 86: 'Reserved', 304 87: 'Reserved', 305 88: 'Information: Pilot boarding position', 306 89: 'Information: Icebreaker waiting area', 307 90: 'Information: Places of refuge', 308 91: 'Information: Position of icebreakers', 309 92: 'Information: Location of response units', 310 93: 'Reserved', 311 94: 'Reserved', 312 95: 'Reserved', 313 96: 'Chart Feature: Sunken vessel', 314 97: 'Chart Feature: Submerged object', 315 98: 'Chart Feature: Semi-submerged object', 316 99: 'Chart Feature: Shoal area', 317 100: 'Chart Feature: Shoal area due North', 318 101: 'Chart Feature: Shoal area due East', 319 102: 'Chart Feature: Shoal area due South', 320 103: 'Chart Feature: Shoal area due West', 321 104: 'Chart Feature: Channel obstruction', 322 105: 'Chart Feature: Reduced vertical clearance', 323 106: 'Chart Feature: Bridge closed', 324 107: 'Chart Feature: Bridge partially open', 325 108: 'Chart Feature: Bridge fully open', 326 109: 'Reserved', 327 110: 'Reserved', 328 111: 'Reserved', 329 112: 'Report from ship: Icing info', 330 113: 'Reserved', 331 114: 'Report from ship: Miscellaneous information - define in free text field', 332 115: 'Reserved', 333 116: 'Reserved', 334 117: 'Reserved', 335 118: 'Reserved', 336 119: 'Reserved', 337 120: 'Reserved', 338 121: 'Reserved', 339 122: 'Reserved', 340 123: 'Reserved', 341 124: 'Reserved', 342 125: 'Other - Define in free text field', 343 126: 'Cancellation - cancel area as identified by Message Linka', 344 127: 'Undefined (default)', 345 } 346 ''' by name or number. 347 348 cau == caution area 349 res == restricted 350 anc == anchorage 351 env == environmental caution 352 sec == security 353 des == distress 354 inst == instructional 355 info == informational 356 chart == chart features'''
357 358 -def _make_short_notice():
359 d = {} 360 for k,v in notice_type.iteritems(): 361 if isinstance (k,str): 362 d[v] = k 363 return d
364 365 short_notice = _make_short_notice()
366 367 -def frange(start, stop=None, step=None):
368 'range but with float steps' 369 if stop is None: 370 stop = float(start) 371 start = 0.0 372 if step is None: 373 step = 1.0 374 cur = float(start) 375 while cur < stop: 376 yield cur 377 cur += step
378
379 380 -def vec_add(a,b):
381 return map(operator.add,a,b)
382
383 -def vec_rot(a, theta):
384 'counter clockwise rotation by theta radians' 385 x,y = a 386 x1 = x * math.cos(theta) - y * math.sin(theta) 387 y1 = x * math.sin(theta) + y * math.cos(theta) 388 return x1,y1
389
390 -def deg2rad(degrees):
391 return (degrees / 180.) * math.pi
392 -def rad2deg(radians):
393 return (radians / math.pi) * 180.
394
395 -def geom2kml(geom_dict):
396 '''Convert a geointerface geometry to KML 397 398 @param geom_dict: Dictionary containing 'geometry' as defined by the geo interface / geojson / shapely 399 ''' 400 geom_type = geom_dict['geometry']['type'] 401 geom_coords = geom_dict['geometry']['coordinates'] 402 403 if geom_type == 'Point': 404 return '<Point><coordinates>{lon},{lat},0</coordinates></Point>'.format(lon = geom_coords[0], lat = geom_coords[1]) 405 elif geom_type == 'Polygon': 406 o = ['<Polygon><outerBoundaryIs><LinearRing><coordinates>'] 407 for pt in geom_coords: 408 o.append('\t%f,%f,0' % (pt[0],pt[1])) 409 o.append('</coordinates></LinearRing></outerBoundaryIs></Polygon>') 410 return '\n'.join(o) 411 412 elif geom_type == 'LineString': 413 o = ['<LineString><coordinates>'] 414 for pt in geom_coords: 415 o.append('\t%f,%f,0' % (pt[0],pt[1])) 416 o.append('</coordinates></LineString>') 417 return '\n'.join(o) 418 419 raise ValueError('Not a recognized __geo_interface__ type: %s' % (geom_type))
420
421 422 423 -class AisException(Exception):
424 pass
425
426 -class AisPackingException(AisException):
427 - def __init__(self, fieldname, value):
428 self.fieldname = fieldname 429 self.value = value
430 - def __repr__(self):
431 return "Validation on %s failed (value %s) while packing" % (self.fieldname, self.value)
432
433 -class AisUnpackingException(AisException):
434 - def __init__(self, fieldname, value):
435 self.fieldname = fieldname 436 self.value = value
437 - def __repr__(self):
438 return "Validation on %s failed (value %s) while unpacking" % (self.fieldname, self.value)
439
440 441 442 -def nmea_checksum_hex(sentence):
443 nmea = map(ord, sentence.split('*')[0]) 444 checksum = reduce(xor, nmea) 445 #print 'checksum:',checksum, hex(checksum) 446 return hex(checksum).split('x')[1].upper()
447
448 -class AIVDM (object):
449 '''AIS VDM Object for AIS top level messages 1 through 64. 450 451 Class attribute payload_bits must be set by the child class. 452 '''
453 - def __init__(self, message_id = None, repeat_indicator = None, source_mmsi = None):
454 self.message_id = message_id 455 self.repeat_indicator = repeat_indicator 456 self.source_mmsi = source_mmsi
457
458 - def get_bits(self):
459 '''Child classes must implement this. Return a BitVector 460 representation. Child classes do NOT include the Message ID, repeat indicator, or source mmsi''' 461 raise NotImplementedError()
462
463 - def get_bits_header(self, message_id = None, repeat_indicator = None, source_mmsi = None):
464 if message_id is None: message_id = self.message_id 465 if repeat_indicator is None: repeat_indicator = self.repeat_indicator 466 if source_mmsi is None: source_mmsi = self.source_mmsi 467 468 if message_id is None or message_id<1 or message_id>63: 469 raise AisPackingException('message_id must be valid',message_id) 470 if repeat_indicator is None or repeat_indicator<0 or repeat_indicator>3: 471 raise AisPackingException('repeat_indicator must be valid',repeat_indicator) 472 473 #print '\naivdm_header:',message_id,repeat_indicator,source_mmsi 474 bvList = [] 475 bvList.append(binary.setBitVectorSize(BitVector(intVal=message_id),6)) 476 bvList.append(binary.setBitVectorSize(BitVector(intVal=repeat_indicator),2)) 477 bvList.append(binary.setBitVectorSize(BitVector(intVal=source_mmsi),30)) 478 bv = binary.joinBV(bvList) 479 if len(bv) != 38: 480 raise AisPackingExpeption('invalid header size',len(bv)) 481 return bv
482 483 # See __geo_interface__ 484 # def get_json(self): 485 # 'Child classes must implement this. Return a json object' 486 # raise NotImplementedError() 487
488 - def get_aivdm(self, sequence_num = None, channel = 'A', normal_form=False, source_mmsi=None, repeat_indicator=None):
489 '''return the nmea string as if it had been received. Assumes that payload_bits has already been set 490 @param sequence_num: Which channel of AIVDM on the local serial line (in 0..9) 491 @param channel: VHF radio channel ("A" or "B") 492 @param normal_form: Set to true to always return aone line NMEA message. False allows multi-sentence messages 493 @return: AIVDM sentences 494 @rtype: list (even for normal_form for consistency) 495 ''' 496 if sequence_num is not None and (sequence_num <= 0 or sequence_num >= 9): 497 raise AisPackingException('sequence_num',sequence_num) 498 if channel not in ('A','B'): 499 raise AisPackingException('channel',channel) 500 501 if repeat_indicator is None: 502 try: 503 repeat_indicator = self.repeat_indicator 504 except: 505 repeat_indicator = 0 506 507 if source_mmsi is None: 508 try: 509 source_mmsi = self.source_mmsi 510 except: 511 raise AisPackingException('source_mmsi',source_mmsi) 512 513 header = self.get_bits_header(repeat_indicator=repeat_indicator,source_mmsi=source_mmsi) 514 payload, pad = binary.bitvectoais6(header + self.get_bits()) 515 516 if sequence_num is None: 517 sequence_num = '' 518 519 if normal_form: 520 # Build one big NMEA string no matter what 521 sentence = '!AIVDM,{tot_sentences},{sentence_num},{sequence_num},{channel},{payload},{pad}'.format( 522 tot_sentences=1, sentence_num=1, 523 sequence_num=sequence_num, channel=channel, 524 payload=payload, pad=pad 525 ) 526 return [sentence + '*' + nmea_checksum_hex(sentence),] 527 528 max_payload_char = 43 529 530 sentences = [] 531 tot_sentences = 1 + len(payload) / max_payload_char 532 sentence_num = 0 533 for i in range(tot_sentences-1): 534 sentence_num = i+1 535 payload_part = payload[i*max_payload_char:(i+1)*max_payload_char] 536 sentence = '!AIVDM,{tot_sentences},{sentence_num},{sequence_num},{channel},{payload},{pad}'.format( 537 tot_sentences=tot_sentences, sentence_num=sentence_num, 538 sequence_num=sequence_num, channel=channel, 539 payload=payload_part, pad=0 540 ) 541 sentences.append(sentence + '*' + nmea_checksum_hex(sentence)) 542 543 sentence_num += 1 544 payload_part = payload[(sentence_num-1)*max_payload_char:] 545 sentence = '!AIVDM,{tot_sentences},{sentence_num},{sequence_num},{channel},{payload},{pad}'.format( 546 tot_sentences=tot_sentences, sentence_num=sentence_num, 547 sequence_num=sequence_num, channel=channel, 548 payload=payload_part, pad=pad # The last part gets the pad 549 ) 550 sentences.append(sentence + '*' + nmea_checksum_hex(sentence)) 551 552 return sentences
553
554 - def kml(self,with_style=False,full=False,with_time=False):
555 '''return kml str for google earth 556 @param with_style: if style is True, it will use the standard style. Set to a name for a custom style 557 @param with_time: enable timestamps in Google Earth 558 ''' 559 o = [] 560 if full: 561 o.append(kml_head) 562 o.append(file('areanotice_styles.kml').read()) 563 html = self.html() 564 for area in self.areas: 565 geo_i = area.__geo_interface__ 566 if 'geometry' not in geo_i: 567 #print 'Skipping area:',str(area) 568 continue 569 kml_shape = geom2kml(geo_i) 570 571 o.append('<Placemark>') 572 try: 573 o.append('<name>%s</name>' % (self.name)) 574 except: 575 o.append('<name>%s</name>' % (short_notice[self.area_type].replace('_',' '),)) 576 if with_style: 577 if isinstance(with_style,str): 578 o.append('<styleUrl>%s</styleUrl>'% (with_style,)) 579 o.append('<styleUrl>#AreaNotice_%d</styleUrl>' % self.area_type) 580 o.append('<description>') 581 o.append('<i>AreaNotice - %s</i>' % (notice_type[self.area_type],) ) 582 o.append(html) 583 o.append('</description>') 584 585 o.append(kml_shape) 586 if with_time: 587 start = datetime.datetime.strftime(self.when,iso8601_timeformat) 588 end = datetime.datetime.strftime(self.when + datetime.timedelta(minutes=self.duration),iso8601_timeformat) 589 o.append('''<TimeSpan><begin>%s</begin><end>%s</end></TimeSpan>''' % (start,end)) 590 591 o.append('</Placemark>\n') 592 593 if full: 594 o.append(kml_tail) 595 596 return '\n'.join(o)
597
598 599 -class BBM (AIVDM):
600 ''' Binary Broadcast Message - MessageID 8. Generically support messages of this type 601 BBM defined in 80_330e_PAS - IEC/PAS 61162-100 Ed.1 602 603 Maritime navigation and radiocommunication equipment and systems - 604 Digital interfaces - Part 100: Singlto IEC 61162-1 for the UAIS 605 606 NMEA BBM can be 8, 19, or 21. It can also handle the text message 14. 607 ''' 608 max_payload_char = 41 609 'Maximum length of characters that can go inside the BBM payload' 610
611 - def __init__(self,message_id = 8):
612 assert message_id in (8, 19, 21) 613 self.message_id = message_id
614
615 - def get_bbm(self, talker='EC', sequence_num = None, channel = 'A'):
616 if not isinstance(talker,str) or len(talker) != 2: 617 AisPackingException('talker',talker) 618 if sequence_num is not None and (sequence_num <= 0 or sequence_num >= 9): 619 raise AisPackingException('sequence_num',sequence_num) 620 if channel not in ('A','B'): 621 raise AisPackingException('channel',channel) 622 623 if sequence_num is None: 624 sequence_num = 3 625 626 payload, pad = binary.bitvectoais6(self.get_bits()) 627 628 sentences = [] 629 tot_sentences = 1 + len(payload) / self.max_payload_char 630 sentence_num = 0 631 for i in range(tot_sentences-1): 632 sentence_num = i+1 633 payload_part = payload[i*self.max_payload_char:(i+1)*self.max_payload_char] 634 sentence = '!{talker}BBM,{tot_sentences},{sentence_num},{sequence_num},{channel},{msg_type},{payload},{pad}'.format( 635 talker=talker, 636 tot_sentences=tot_sentences, sentence_num=sentence_num, 637 sequence_num=sequence_num, channel=channel, 638 msg_type=self.message_id, 639 payload=payload_part, pad=0 640 ) 641 sentences.append(sentence + '*' + nmea_checksum_hex(sentence)) 642 643 sentence_num += 1 644 payload_part = payload[(sentence_num-1)*self.max_payload_char:] 645 sentence = '!{talker}BBM,{tot_sentences},{sentence_num},{sequence_num},{channel},{msg_type},{payload},{pad}'.format( 646 talker=talker, 647 tot_sentences=tot_sentences, sentence_num=sentence_num, 648 sequence_num=sequence_num, channel=channel, 649 msg_type=self.message_id, 650 payload=payload_part, pad=pad # The last part gets the pad 651 ) 652 sentences.append(sentence + '*' + nmea_checksum_hex(sentence)) 653 654 return sentences
655
656 -class AreaNoticeSubArea(object):
657
658 - def __str__(self):
659 return self.__unicode__()
660
661 662 -class AreaNoticeCirclePt(AreaNoticeSubArea):
663 area_shape = 0
664 - def __init__(self, lon=None, lat=None, radius=0, bits=None):
665 '''@param radius: 0 is a point, otherwise less than or equal to 409500m. Scale factor is automatic 666 @param bits: string of 1's and 0's or a BitVector 667 ''' 668 if lon is not None: 669 assert lon >= -180. and lon <= 180. 670 self.lon = lon 671 assert lat >= -90. and lat <= 90. 672 self.lat = lat 673 674 assert radius >= 0 and radius < 409500 675 self.radius = radius 676 677 if radius / 100. >= 4095: self.scale_factor_raw = 3 678 elif radius / 10. > 4095: self.scale_factor_raw = 2 679 elif radius > 4095: self.scale_factor_raw = 1 680 else: self.scale_factor_raw = 0 681 682 self.scale_factor = (1,10,100,1000)[self.scale_factor_raw] 683 self.radius_scaled = radius / self.scale_factor 684 return 685 686 elif bits is not None: 687 self.decode_bits(bits) 688 return 689 690 return # Return an empty object
691 692
693 - def decode_bits(self,bits):
694 if len(bits) != 90: raise AisUnpackingException('bit length',len(bits)) 695 if isinstance(bits,str): 696 bits = BitVector(bitstring = bits) 697 elif isinstance(bits, list) or isinstance(bits,tuple): 698 bits = BitVector ( bitlist = bits) 699 700 self.area_shape = int( bits[:3] ) 701 self.scale_factor_raw = int( bits[3:5] ) 702 self.scale_factor = (1,10,100,1000)[self.scale_factor_raw] 703 self.lon = binary.signedIntFromBV( bits[ 5:33] ) / 600000. 704 self.lat = binary.signedIntFromBV( bits[33:60] ) / 600000. 705 self.radius_scaled = int( bits[60:72] ) 706 707 self.radius = self.radius_scaled * self.scale_factor 708 709 spare = int( bits[72:90] )
710 #assert 0 == spare 711 712
713 - def get_bits(self):
714 'Build a BitVector for this area' 715 bvList = [] 716 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.area_shape ), 3) ) 717 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.scale_factor_raw), 2 ) ) 718 bvList.append( binary.bvFromSignedInt( int(self.lon*600000), 28 ) ) 719 bvList.append( binary.bvFromSignedInt( int(self.lat*600000), 27 ) ) 720 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.radius_scaled), 12 ) ) 721 bvList.append( binary.setBitVectorSize( BitVector(intVal=0), 18 ) ) # spare 722 bv = binary.joinBV(bvList) 723 if 90 != len(bv): 724 print 'len:',[len(b) for b in bvList] 725 raise AisPackingException('area not 90 bits',len(bv)) 726 #assert 90==len(bv) 727 return bv
728
729 - def __unicode__(self):
730 if self.radius == 0.: 731 return 'AreaNoticeCirclePt: Point at (%.4f,%.4f)' % (self.lon,self.lat) 732 return 'AreaNoticeCirclePt: Circle centered at (%.4f,%.4f) - radius %dm' % (self.lon,self.lat,self.radius)
733 734 # def __str__(self): 735 # return self.__unicode__() 736
737 - def geom(self):
738 #if 'geom_geographic' not in self.__dict__: 739 # If I do this, will need to make sure that I invalidate the cache 740 if self.radius <= 0.01: 741 return shapely.geometry.Point(self.lon,self.lat) 742 743 # Circle 744 zone = lon_to_utm_zone(self.lon) 745 params = {'proj':'utm','zone':zone} 746 proj = Proj(params) 747 748 utm_center = proj(self.lon,self.lat) 749 pt = shapely.geometry.Point(utm_center) 750 circle_utm = pt.buffer(self.radius) #9260) 751 752 circle = shapely.geometry.Polygon( [ proj(pt[0],pt[1],inverse=True) for pt in circle_utm.boundary.coords]) 753 754 return circle
755 756 @property
757 - def __geo_interface__(self):
758 'Provide a Geo Interface for GeoJSON serialization' 759 # Would be better if there was a GeoJSON Circle type! 760 761 762 if self.radius == 0.: 763 return {'area_shape': self.area_shape, 764 'area_shape_name': 'point', 765 'geometry': {'type': 'Point', 'coordinates': [self.lon, self.lat] } 766 } 767 768 # self.radius > 0 ... circle 769 r = { 770 'area_shape': self.area_shape, 771 'area_shape_name': 'circle', 772 'radius':self.radius, 773 'geometry': {'type': 'Polygon', 'coordinates': tuple(self.geom().boundary.coords) }, 774 #'geometry': {'type': 'Polygon', 'coordinates': [pt for pt in self.geom().boundary.coords]}, 775 # Leaving out scale_factor 776 } 777 return r
778
779 -class AreaNoticeRectangle(AreaNoticeSubArea):
780 area_shape = 1
781 - def __init__(self, lon=None, lat=None, east_dim=0, north_dim=0, orientation_deg=0, bits=None):
782 ''' 783 Rotatable rectangle 784 @param lon: WGS84 longitude 785 @param lat: WGS84 latitude 786 @param east_dim: width in meters (this gets confusing for larger angles). 0 is a north-south line 787 @param north_dim: height in meters (this gets confusing for larger angles). 0 is an east-west line 788 @param orientation_deg: degrees CW 789 790 @todo: great get/set for dimensions and allow for setting scale factor. 791 @todo: or just over rule the attribute get and sets 792 @todo: allow user to force the scale factor 793 @todo: Should this be raising a ValueError 794 ''' 795 if lon is not None: 796 assert lon >= -180. and lon <= 180. 797 self.lon = lon 798 assert lat >= -90. and lat <= 90. 799 self.lat = lat 800 801 assert 0 <= east_dim and east_dim <= 255000 # 25.5 km 802 assert 0 <= north_dim and north_dim <= 255000 803 804 assert 0 <= orientation_deg and orientation_deg < 360 805 806 if east_dim / 100. >= 255 or north_dim / 100. >= 255: self.scale_factor_raw = 3 807 elif east_dim / 10. >= 255 or north_dim / 100. >= 255: self.scale_factor_raw = 2 808 elif east_dim >= 255 or north_dim >= 255: self.scale_factor_raw = 1 809 else: self.scale_factor_raw = 0 810 self.scale_factor = (1,10,100,1000)[self.scale_factor_raw] 811 812 self.e_dim = east_dim 813 self.n_dim = north_dim 814 self.e_dim_scaled = east_dim / self.scale_factor 815 self.n_dim_scaled = east_dim / self.scale_factor 816 817 self.orientation_deg = orientation_deg 818 819 elif bits is not None: 820 self.decode_bits(bits)
821
822 - def decode_bits(self,bits):
823 if len(bits) != 90: raise AisUnpackingException('bit length',len(bits)) 824 if isinstance(bits,str): 825 bits = BitVector(bitstring = bits) 826 elif isinstance(bits, list) or isinstance(bits,tuple): 827 bits = BitVector ( bitlist = bits) 828 829 self.area_shape = int( bits[:3] ) 830 self.scale_factor = int( bits[3:5] ) 831 self.lon = binary.signedIntFromBV( bits[ 5:33] ) / 600000. 832 self.lat = binary.signedIntFromBV( bits[33:60] ) / 600000. 833 self.e_dim_scaled = int ( bits[60:68] ) 834 self.n_dim_scaled = int ( bits[68:76] ) 835 836 self.e_dim = self.e_dim_scaled * (1,10,100,1000)[self.scale_factor] 837 self.n_dim = self.n_dim_scaled * (1,10,100,1000)[self.scale_factor] 838 839 self.orientation_deg = int ( bits[76:85] ) 840 841 self.spare = int ( bits[85:90] )
842
843 - def get_bits(self):
844 bvList = [] 845 bvList.append( binary.setBitVectorSize( BitVector(intVal=0), 3 ) ) # area_shape/type = 0 846 #xsscale_factor = {1:0,10:1,100:2,1000:3}[self.scale_factor] 847 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.scale_factor_raw), 2 ) ) 848 bvList.append( binary.bvFromSignedInt( int(self.lon*600000), 28 ) ) 849 bvList.append( binary.bvFromSignedInt( int(self.lat*600000), 27 ) ) 850 #print 'dim:',self.e_dim_scaled,self.n_dim_scaled, self.scale_factor 851 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.e_dim_scaled), 8 ) ) 852 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.n_dim_scaled), 8 ) ) 853 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.orientation_deg), 9 ) ) 854 bvList.append( binary.setBitVectorSize( BitVector(intVal=0), 5 ) ) # spare 855 #print '\nlen:',[len(b) for b in bvList] 856 bv = binary.joinBV(bvList) 857 assert 90==len(bv) 858 return bv
859
860 - def __unicode__(self):
861 return 'AreaNoticeRectangle: (%.4f,%.4f) [%d,%d]m rot: %d deg' % (self.lon,self.lat,self.e_dim,self.n_dim,self.orientation_deg)
862
863 - def __str__(self):
864 return self.__unicode__()
865 866
867 - def geom(self):
868 'return shapely geometry object' 869 zone = lon_to_utm_zone(self.lon) 870 params = {'proj':'utm', 'zone':zone} 871 proj = Proj(params) 872 873 p1 = proj(self.lon,self.lat) 874 875 pts = [(0,0), (self.e_dim,0), (self.e_dim,self.n_dim), (0,self.n_dim)] 876 877 #print 'before:',pts 878 rot = deg2rad(-self.orientation_deg) 879 pts = [vec_rot(pt,rot) for pt in pts] 880 881 pts = [vec_add(p1,pt) for pt in pts] 882 pts = [proj(*pt,inverse=True) for pt in pts] 883 884 return shapely.geometry.Polygon(pts)
885 886 @property
887 - def __geo_interface__(self):
888 '''Provide a Geo Interface for GeoJSON serialization 889 @todo: Write the code to build the polygon with rotation''' 890 r = { 891 'area_shape': self.area_shape, 'area_shape_name': 'rectangle', 892 'orientation': self.orientation_deg, 893 'e_dim': self.e_dim, 'n_dim': self.n_dim, 894 'geometry': {'type':'Polygon', 'coordinates': tuple(self.geom().boundary.coords) }, 895 } 896 897 return r
898
899 900 -class AreaNoticeSector(AreaNoticeSubArea):
901 area_shape = 2
902 - def __init__(self, lon=None, lat=None, radius=0, left_bound_deg=0, right_bound_deg=0, bits=None):
903 ''' 904 A pie slice 905 906 @param lon: WGS84 longitude 907 @param lat: WGS84 latitude 908 @param radius: width in meters 909 @param left_bound_deg: Orientation of the left boundary. CW from True North 910 @param right_bound_deg: Orientation of the right boundary. CW from True North 911 912 @todo: great get/set for dimensions and allow for setting scale factor. 913 @todo: or just over rule the attribute get and sets 914 @todo: allow user to force the scale factor 915 @todo: Should this be raising a ValueError 916 ''' 917 if lon is not None: 918 assert lon >= -180. and lon <= 180. 919 self.lon = lon 920 assert lat >= -90. and lat <= 90. 921 self.lat = lat 922 923 assert 0 <= radius and radius <= 25500 924 925 assert 0 <= left_bound_deg and left_bound_deg < 360 926 assert 0 <= right_bound_deg and right_bound_deg < 360 927 928 assert left_bound_deg <= right_bound_deg 929 930 if radius / 100. >= 4095: self.scale_factor_raw = 3 931 elif radius / 10. > 4095: self.scale_factor_raw = 2 932 elif radius > 4095: self.scale_factor_raw = 1 933 else: self.scale_factor_raw = 0 934 self.scale_factor = (1,10,100,1000)[self.scale_factor_raw] 935 self.radius = radius 936 self.radius_scaled = int( radius / self.scale_factor) 937 938 self.left_bound_deg = left_bound_deg 939 self.right_bound_deg = right_bound_deg 940 941 elif bits is not None: 942 self.decode_bits(bits)
943
944 - def decode_bits(self,bits):
945 if len(bits) != 90: raise AisUnpackingException('bit length',len(bits)) 946 if isinstance(bits,str): 947 bits = BitVector(bitstring = bits) 948 elif isinstance(bits, list) or isinstance(bits,tuple): 949 bits = BitVector ( bitlist = bits) 950 951 self.area_shape = int( bits[:3] ) 952 self.scale_factor = int( bits[3:5] ) 953 self.lon = binary.signedIntFromBV( bits[ 5:33] ) / 600000. 954 self.lat = binary.signedIntFromBV( bits[33:60] ) / 600000. 955 self.radius_scaled = int ( bits[60:72] ) 956 957 self.radius = self.radius_scaled * (1,10,100,1000)[self.scale_factor] 958 959 self.left_bound_deg = int ( bits[72:81] ) 960 self.right_bound_deg = int ( bits[81:90] )
961 962
963 - def get_bits(self):
964 965 'Build a BitVector for this area' 966 bvList = [] 967 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.area_shape ), 3) ) 968 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.scale_factor_raw), 2 ) ) 969 bvList.append( binary.bvFromSignedInt( int(self.lon*600000), 28 ) ) 970 bvList.append( binary.bvFromSignedInt( int(self.lat*600000), 27 ) ) 971 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.radius_scaled), 12 ) ) 972 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.left_bound_deg), 9 ) ) 973 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.right_bound_deg), 9 ) ) 974 975 #print 'len:',[len(bv) for bv in bvList] 976 bv = binary.joinBV(bvList) 977 assert 90==len(bv) 978 return bv
979
980 - def __unicode__(self):
981 return 'AreaNoticeSector: (%.4f,%.4f) %d rot: %d to %d deg' % (self.lon, self.lat, self.radius, 982 self.left_bound_deg, self.right_bound_deg)
983 - def __str__(self):
984 return self.__unicode__()
985
986 - def geom(self):
987 'return shapely geometry object' 988 zone = lon_to_utm_zone(self.lon) 989 params = {'proj':'utm', 'zone':zone} 990 proj = Proj(params) 991 992 p1 = proj(self.lon,self.lat) 993 994 pts = [ vec_rot( (0,self.radius), deg2rad(-angle) ) for angle in frange(self.left_bound_deg, self.right_bound_deg+0.01, 0.5) ] 995 pts = [(0,0),] + pts + [(0,0),] 996 997 pts = [vec_add(p1,pt) for pt in pts] # Move to the right place in the world 998 pts = [proj(*pt,inverse=True) for pt in pts] # Project back to geographic 999 1000 return shapely.geometry.Polygon(pts)
1001 1002 @property
1003 - def __geo_interface__(self):
1004 '''Provide a Geo Interface for GeoJSON serialization 1005 @todo: Write the code to build the polygon with rotation''' 1006 r = { 1007 'area_shape': self.area_shape, 'area_shape_name': 'sector', 1008 'left_bound': self.left_bound_deg, 1009 'right_bound': self.right_bound_deg, 1010 'radius': self.radius, 1011 'geometry': {'type':'Polygon', 'coordinates': tuple(self.geom().boundary.coords) }, 1012 } 1013 1014 return r
1015
1016 -class AreaNoticePolyline(AreaNoticeSubArea):
1017 area_shape = 3
1018 - def __init__(self, points=None, 1019 lon=None, lat=None, 1020 bits=None):
1021 '''A line or open area. If an area, this is the area to the 1022 left of the line. The line starts at the prior line. Must set p1 1023 or provide bits. You will not be able to get the geometry if you 1024 do not provide a lon,lat for the starting point 1025 1026 @param points: 1 to 4 relative offsets (angle in degrees [0..360] , distance in meters) 1027 @param lon: WGS84 longitude of the starting point. Must match the previous point 1028 @param lat: WGS84 longitude of the starting point. Must match the previous point 1029 @param bits: bits to decode from 1030 @todo: FIX: make sure that the AreaNotice decode bits passes the lon, lat 1031 @todo: FIX: Handle sectors that cross 0/360 1032 ''' 1033 1034 if lon is not None: 1035 assert lon >= -180. and lon <= 180. 1036 self.lon = lon 1037 assert lat >= -90. and lat <= 90. 1038 self.lat = lat 1039 1040 1041 if points is not None: 1042 assert len(points)>0 and len(points)<5 1043 self.points = points 1044 1045 max_dist = max([pt[1] for pt in points]) 1046 if max_dist / 100. >= 2047: self.scale_factor_raw = 3 1047 elif max_dist / 10. > 2047: self.scale_factor_raw = 2 1048 elif max_dist > 2047: self.scale_factor_raw = 1 1049 else: self.scale_factor_raw = 0 1050 self.scale_factor = (1,10,100,1000)[self.scale_factor_raw] 1051 1052 elif bits is not None: 1053 self.decode_bits(bits)
1054
1055 - def decode_bits(self,bits):
1056 if len(bits) != 90: raise AisUnpackingException('bit length',len(bits)) 1057 if isinstance(bits,str): 1058 bits = BitVector(bitstring = bits) 1059 elif isinstance(bits, list) or isinstance(bits,tuple): 1060 bits = BitVector ( bitlist = bits) 1061 1062 self.area_shape = int( bits[:3] ) 1063 self.scale_factor = int( bits[3:5] ) 1064 1065 self.points = [] 1066 for i in range(4): 1067 base = 5 + i*21 1068 angle = int ( bits[base:base+10] ) 1069 dist_scaled = int ( bits[base+10:base+10+11] ) 1070 dist = dist_scaled * (1,10,100,1000)[self.scale_factor] 1071 self.points.append((angle,dist)) 1072 if 720 == dist_scaled: 1073 break
1074 1075
1076 - def get_bits(self):
1077 'Build a BitVector for this area' 1078 bvList = [] 1079 bvList.append( binary.setBitVectorSize( BitVector(intVal=0), 3 ) ) # area_shape/type = 0 1080 1081 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.scale_factor_raw), 2 ) ) 1082 1083 bvList = [] 1084 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.area_shape ), 3) ) 1085 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.scale_factor_raw), 2 ) ) 1086 1087 # FIX: check range of points 1088 for pt in self.points: 1089 #print 'scale_factor:',self.scale_factor 1090 #print 'polyline_seg:',pt,self.scale_factor, pt[1] / self.scale_factor, len(BitVector(intVal=pt[1] / self.scale_factor)) 1091 bvList.append( binary.setBitVectorSize( BitVector(intVal=pt[0]), 10 ) ) 1092 bvList.append( binary.setBitVectorSize( BitVector(intVal=pt[1] / self.scale_factor), 11 ) ) 1093 1094 for i in range(4 - len(self.points)): 1095 bvList.append( binary.setBitVectorSize( BitVector(intVal=0), 21 ) ) 1096 1097 bvList.append( BitVector(intVal=0) ) 1098 1099 bv = binary.joinBV(bvList) 1100 if len(bv) != 90: 1101 print 'len:',[len(b) for b in bvList] 1102 raise AisPackingException('area not 90 bits',len(bv)) 1103 1104 #raise AisPackingException('wrong size',len(bv)) 1105 return bv
1106
1107 - def __unicode__(self):
1108 return 'AreaNoticePolyline: (%.4f,%.4f) %d points' % ( self.lon, self.lat, len(self.points) )
1109 1110 # def __str__(self): 1111 # return self.__unicode__() 1112
1113 - def geom(self):
1114 zone = lon_to_utm_zone(self.lon) 1115 params = {'proj':'utm','zone':zone} 1116 proj = Proj(params) 1117 1118 p1 = proj(self.lon,self.lat) 1119 1120 pts = [(0,0)] 1121 cur = (0,0) 1122 for pt in self.points: 1123 alpha = deg2rad(pt[0]) 1124 d = pt[1] 1125 x,y = d * math.sin(alpha), d * math.cos(alpha) 1126 cur = vec_add(cur,(x,y)) 1127 #print 'step:',pt,cur 1128 pts.append(cur) 1129 1130 1131 pts = [vec_add(p1,pt) for pt in pts] 1132 pts = [proj(*pt,inverse=True) for pt in pts] 1133 return shapely.geometry.LineString(pts)
1134 1135 @property
1136 - def __geo_interface__(self):
1137 '''Provide a Geo Interface for GeoJSON serialization 1138 @todo: Write the code to build the polygon with rotation''' 1139 r = { 1140 'area_shape':self.area_shape, 'area_shape_name': 'waypoints/polyline', 1141 'geometry': {'type':'LineString', 'coordinates': tuple(self.geom().coords) }, 1142 } 1143 1144 return r
1145
1146 1147 -class AreaNoticePolygon(AreaNoticePolyline):
1148 'Polyline that wraps back to the beginning' 1149 area_shape = 4 1150 area_name = 'polygon' 1151
1152 - def __unicode__(self):
1153 return 'AreaNoticePolylon: (%.4f,%.4f) %d points' % ( self.lon, self.lat, len(self.points) )
1154
1155 - def geom(self):
1156 zone = lon_to_utm_zone(self.lon) 1157 params = {'proj':'utm','zone':zone} 1158 proj = Proj(params) 1159 1160 p1 = proj(self.lon,self.lat) 1161 1162 pts = [(0,0)] 1163 cur = (0,0) 1164 for pt in self.points: 1165 alpha = deg2rad(pt[0]) 1166 d = pt[1] 1167 x,y = d * math.sin(alpha), d * math.cos(alpha) 1168 cur = vec_add(cur,(x,y)) 1169 pts.append(cur) 1170 1171 1172 #print 'pts:',pts 1173 1174 pts = [vec_add(p1,pt) for pt in pts] 1175 pts = [proj(*pt,inverse=True) for pt in pts] 1176 return shapely.geometry.Polygon(pts)
1177 1178 @property
1179 - def __geo_interface__(self):
1180 '''Provide a Geo Interface for GeoJSON serialization 1181 @todo: Write the code to build the polygon with rotation''' 1182 r = { 1183 'area_shape':self.area_shape, 'area_shape_name': self.area_name, 1184 'geometry': {'type':'Polygon', 'coordinates': tuple(self.geom().boundary.coords) }, 1185 } 1186 1187 return r
1188
1189 -class AreaNoticeFreeText(AreaNoticeSubArea):
1190 area_shape = 4 1191 area_name = 'freetext'
1192 - def __init__(self,text=None, bits=None):
1193 if text is not None: 1194 text = text.upper() 1195 #if len(text) > 14: 1196 # sys.stderr.write('text too long') 1197 assert len(text) <= 14 1198 for c in text: 1199 assert c in aisstring.characterDict 1200 self.text = text 1201 elif bits is not None: 1202 self.decode_bits(bits)
1203 1204
1205 - def decode_bits(self,bits):
1206 if len(bits) != 90: raise AisUnpackingException('bit length',len(bits)) 1207 if isinstance(bits,str): 1208 bits = BitVector(bitstring = bits) 1209 elif isinstance(bits, list) or isinstance(bits,tuple): 1210 bits = BitVector ( bitlist = bits) 1211 1212 area_shape = int( bits[:3] ) 1213 assert self.area_shape == area_shape 1214 self.text = aisstring.decode(bits[3:-1]) 1215 self.spare = int(bits[-1])
1216
1217 - def get_bits(self):
1218 'Build a BitVector for this area' 1219 bvList = [] 1220 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.area_shape), 3 )) 1221 text = self.text.ljust(14,'@') 1222 bvList.append(aisstring.encode(text)) 1223 bvList.append( BitVector( bitstring = '000' ) ) # spare 1224 bv = binary.joinBV(bvList) 1225 if 90 != len(bv): 1226 print 'len_freetext:',[len(b) for b in bvList] 1227 # FIX raise 1228 assert 90==len(bv) 1229 return bv
1230
1231 - def __unicode__(self):
1232 return 'AreaNoticeFreeText: "%s"' % (self.text,)
1233
1234 - def __str__(self):
1235 return self.__unicode__()
1236
1237 - def geom(self):
1238 # FIX: should this somehow have a position? 1239 return None
1240 1241 @property
1242 - def __geo_interface__(self):
1243 'Provide a Geo Interface for GeoJSON serialization' 1244 # FIX: should this return geometry? Probably not as this text gets built into the message text for other geom 1245 return {'area_shape': self.area_shape, 1246 'area_shape_name': self.area_name, 1247 # No geometry... 'geometry': {'type': 'Point', 'coordinates': [self.lon, self.lat] } 1248 }
1249
1250 -class AreaNotice(BBM):
1251 - def __init__(self,area_type=None,when=None,duration=None,link_id=0, nmea_strings=None):
1252 ''' 1253 @param area_type: 0..127 based on table 11.10 1254 @param when: when the notice starts 1255 @type when: datetime (UTC) 1256 @param duration: minutes for the notice to be in effect 1257 @param nmea_strings: Pass 1 or more nmea strings as a list 1258 ''' 1259 if nmea_strings != None: 1260 self.decode_nmea(nmea_strings) 1261 1262 elif area_type is not None and when is not None and duration is not None: 1263 # We are creating a new message 1264 assert area_type >= 0 and area_type <= 127 1265 self.area_type = area_type 1266 assert isinstance(when,datetime.datetime) 1267 self.when = when 1268 assert duration < 2**18 - 1 # Last number reserved for undefined... what does undefined mean? 1269 self.duration = duration 1270 self.link_id = link_id 1271 1272 self.areas = [] 1273 self.dac = 1 1274 self.fi = 22 1275 1276 BBM.__init__(self, message_id = 8) # FIX: move to the beginning of this method
1277
1278 - def __unicode__(self,verbose=False):
1279 result = 'AreaNotice: type=%d start=%s duration=%d m link_id=%d sub-areas: %d' % ( 1280 self.area_type, str(self.when), self.duration, self.link_id, len(self.areas) ) 1281 if not verbose: 1282 return result 1283 if verbose: 1284 results = [result,] 1285 for item in self.areas: 1286 results.append('\t'+unicode(item)) 1287 return '\n'.join(results)
1288
1289 - def __str__(self,verbose=False):
1290 return self.__unicode__(verbose)
1291
1292 - def html(self, efactory=False):
1293 '''return an embeddable html representation 1294 @param efactory: return lxml E-factory''' 1295 l = E.OL() 1296 for area in self.areas: 1297 l.append(E.LI(str(area))) 1298 if efactory: 1299 return 1300 return lxml.html.tostring(E.DIV(E.P(self.__str__()),l))
1301 1302 1303 @property
1304 - def __geo_interface__(self):
1305 'Return dictionary compatible with GeoJSON-AIVD' 1306 try: 1307 repeat = self.repeat_indicator 1308 except: 1309 repeat = 0 1310 if repeat is None: repeat = 0 1311 1312 try: 1313 mmsi = self.source_mmsi 1314 except: 1315 mmsi = 0 1316 1317 r = { 1318 'msgtype':self.message_id, 1319 'repeat': repeat, 1320 'mmsi': mmsi, 1321 "bbm": { 1322 'bbm_type':(self.dac,self.fi), 1323 'bbm_name':'area_notice', 1324 'areas': [] 1325 } 1326 } 1327 1328 #print 'areas:',len(self.areas) 1329 #print 'bbm:',r['bbm'] 1330 for area in self.areas: 1331 #print 'area_geo:',area.__geo_interface__ 1332 r['bbm']['areas'].append(area.__geo_interface__) 1333 1334 return r
1335
1336 - def add_subarea(self,area):
1337 #print 'len_subareas_before:',len(self.areas) 1338 assert len(self.areas) < 10 1339 self.areas.append(area)
1340
1341 - def get_bits(self,include_bin_hdr=False, mmsi=None, no_include_dac_fi=False):
1342 '''@param include_bin_hdr: If true, include the standard message header with source mmsi''' 1343 bvList = [] 1344 if include_bin_hdr: 1345 bvList.append( binary.setBitVectorSize( BitVector(intVal=8), 6 ) ) # Messages ID 1346 bvList.append( binary.setBitVectorSize( BitVector(intVal=0), 2 ) ) # Repeat Indicator 1347 bvList.append( binary.setBitVectorSize( BitVector(intVal=mmsi), 30 ) ) 1348 1349 if include_bin_hdr or not no_include_dac_fi: 1350 bvList.append( BitVector( bitstring = '00' ) ) # Spare 1351 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.dac), 10 ) ) 1352 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.fi), 6 ) ) 1353 1354 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.link_id), 10 ) ) 1355 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.area_type), 7 ) ) 1356 1357 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.when.month), 4 ) ) 1358 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.when.day), 5 ) ) 1359 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.when.hour), 5 ) ) 1360 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.when.minute), 6 ) ) 1361 1362 bvList.append( binary.setBitVectorSize( BitVector(intVal=self.duration), 18 ) ) 1363 1364 stdlen = sum([len(b) for b in bvList]) 1365 1366 for i,area in enumerate(self.areas): 1367 bvList.append(area.get_bits()) 1368 return binary.joinBV(bvList)
1369 1370 1371 # def get_fetcher_formatter(self): 1372 # '''return string for USCG/Alion fetcher formatter''' 1373 # pass 1374