Ejemplo n.º 1
0
// This is not a robust function.
func (t Track) TouchdownPDT() time.Time {
	var s time.Time
	// Start halfway through, and see where that gets us
	for i := int(len(t) / 2); i < len(t); i++ {
		s = t[i].TimestampUTC
		if t[i].AltitudeFeet == 0 {
			return date.InPdt(s)
		}
	}
	return date.InPdt(s)
}
Ejemplo n.º 2
0
func (r SCRow) ToCSV() []string {
	return []string{
		r.F.Id.Designator.IATAAirlineDesignator,
		r.F.Id.Designator.String(),
		r.F.Id.Origin,
		date.InPdt(r.F.EnterUTC).Format("2006/01/02"),
		date.InPdt(r.F.EnterUTC).Format("15:04:05.999999999"),
		fmt.Sprintf("%v", r.F.TagList()),
		fmt.Sprintf("%d", r.NumComplaints),
		fmt.Sprintf("%d", r.NumSpeedbrakes),
		fmt.Sprintf("%d", r.WeightedComplaints),
	}
}
Ejemplo n.º 3
0
func (r CBRow) ToCSV() []string {
	return []string{
		r.F.Id.Designator.String(), r.F.Id.Registration, r.F.Id.ModeS,
		date.InPdt(r.TP.TimestampUTC).Format("2006/01/02"),
		date.InPdt(r.TP.TimestampUTC).Format("15:04:05.999999999"),
		fmt.Sprintf("%.1f", r.A.DistNM),
		fmt.Sprintf("%.0f", r.TP.AltitudeFeet),
		fmt.Sprintf("%.0f", r.A.BelowBy),
		fmt.Sprintf("%.4f", r.TP.Latlong.Lat),
		fmt.Sprintf("%.4f", r.TP.Latlong.Long),
		r.TP.LongSource(),
	}
}
Ejemplo n.º 4
0
func (r SERFR1Row) ToCSV() []string {
	return []string{
		r.F.Id.Designator.IATAAirlineDesignator,
		r.F.Id.Designator.String(),
		r.F.Id.Origin,
		r.F.Id.Destination,
		r.F.Id.Registration,
		r.F.Id.ModeS,
		date.InPdt(r.F.EnterUTC).Format("2006/01/02"),
		date.InPdt(r.F.EnterUTC).Format("15:04:05.999999999"),
		bool2string(r.HadAdsb),
		bool2string(r.ClassBViolation),
	}
}
Ejemplo n.º 5
0
func (r SERFR1AtRow) ToCSV() []string {
	return []string{
		r.F.Id.Designator.IATAAirlineDesignator,
		r.F.Id.Designator.String(),
		r.F.Id.Origin,
		r.F.Id.Destination,
		r.F.Id.Registration,
		r.F.Id.ModeS,
		date.InPdt(r.ITP.TimestampUTC).Format("2006/01/02"),
		date.InPdt(r.ITP.TimestampUTC).Format("15:04:05.999999999"),
		fmt.Sprintf("%.0f", r.ITP.SpeedKnots),
		fmt.Sprintf("%.0f", r.ITP.AltitudeFeet),
		fmt.Sprintf("%.0f", r.ITP.Post.TimestampUTC.Sub(r.ITP.Pre.TimestampUTC).Seconds()),
	}
}
Ejemplo n.º 6
0
func (r BrixxRow) ToCSV() []string {
	return []string{
		r.F.Id.Designator.IATAAirlineDesignator,
		r.F.Id.Designator.String(),
		r.F.Id.Registration,
		r.F.Id.ModeS,
		r.F.Id.Origin,
		r.F.Id.Destination,
		r.Source,
		fmt.Sprintf("%.0f", r.TP.Latlong.Lat),
		fmt.Sprintf("%.0f", r.TP.Latlong.Long),
		date.InPdt(r.TP.TimestampUTC).Format("2006/01/02"),
		date.InPdt(r.TP.TimestampUTC).Format("15:04:05.999999999"),
	}
}
Ejemplo n.º 7
0
func playback2FlightIdentifier(r FlightPlaybackResponse, f *flightdb.FlightIdentifier) error {
	flight := r.Result.Response.Data.Flight
	id := flight.Identification
	if id.Number.Default != "" {
		if err := flightdb.ParseIATA(&f.Designator, id.Number.Default); err != nil {
			return err
		}
	}

	f.Origin, f.Destination = flight.Airport.Origin.Code.Iata, flight.Airport.Destination.Code.Iata

	// It would be preferable to find the scheduled date of departure for this; else we
	// misidentify those flights which get delayed past midnight.

	if flight.Track == nil || len(flight.Track) == 0 {
		return fmt.Errorf("playback2FightIdentifier: no track data, so no departure date!")
	}
	epoch := flight.Track[0].Timestamp
	f.DepartureDate = date.InPdt(time.Unix(int64(epoch), 0))

	if f.ForeignKeys == nil {
		f.ForeignKeys = map[string]string{}
	}
	f.ForeignKeys["fr24"] = id.Hex

	f.Registration = flight.Aircraft.Identification.Registration
	f.Callsign = id.Callsign // ?? Is this right ? If so, where is ICAO ??
	f.ModeS = flight.Aircraft.Identification.ModeS

	return nil
}
Ejemplo n.º 8
0
func FixupComplaint(c *types.Complaint, key *datastore.Key) {
	// 0. Snag the key, so we can refer to this object later
	c.DatastoreKey = key.Encode()

	// 1. GAE datastore helpfully converts timezones to UTC upon storage; fix that
	c.Timestamp = date.InPdt(c.Timestamp)

	// 2. Compute the flight details URL, if within 24 days
	age := date.NowInPdt().Sub(c.Timestamp)
	if age < time.Hour*24 {
		// c.AircraftOverhead.Fr24Url = c.AircraftOverhead.PlaybackUrl()

		c.AircraftOverhead.Fr24Url = "http://flightaware.com/live/flight/" +
			c.AircraftOverhead.FlightNumber
		// Or: http://flightaware.com/live/flight/UAL337/history/20151215/ [0655Z/KLAX/KSFO]
		// date is UTC of departure time; might be tricky to guess :/
	}

	// 3. Compute distances, if we have an aircraft
	if c.AircraftOverhead.FlightNumber != "" {
		a := c.AircraftOverhead
		aircraftPos := geo.Latlong{a.Lat, a.Long}
		observerPos := geo.Latlong{c.Profile.Lat, c.Profile.Long}
		c.Dist2KM = observerPos.Dist(aircraftPos)
		c.Dist3KM = observerPos.Dist3(aircraftPos, a.Altitude)
	}
}
Ejemplo n.º 9
0
func (r SkimRow) ToCSV() []string {
	e := r.A.Events[0]
	return []string{
		r.F.Id.Designator.IATAAirlineDesignator,
		r.F.Id.Designator.String(),
		r.F.Id.Registration,
		r.F.Id.ModeS,
		r.F.Id.Origin,
		r.F.Id.Destination,
		fmt.Sprintf("%.1f", r.A.Events[0].StartNM),
		fmt.Sprintf("%.1f", r.A.Events[0].EndNM),
		date.InPdt(e.StartTP.TimestampUTC).Format("2006/01/02"),
		date.InPdt(e.StartTP.TimestampUTC).Format("15:04:05.999999999"),
		fmt.Sprintf("%.0f", e.EndTP.TimestampUTC.Sub(e.StartTP.TimestampUTC).Seconds()),
		r.Source,
	}
}
Ejemplo n.º 10
0
func (fs *FlightSnapshot) ToJSString() string {
	return fmt.Sprintf("{pos:{lat:%.9f,lng:%.9f}, "+
		"heading:%.0f, alt:%.0f, speed:%.0f, "+
		"flightnumber:%q, icao24:%q, callsign:%q, reg:%q, "+
		"t:%q, enterutc:%q, url:%q"+
		"}",
		fs.Pos.Latlong.Lat, fs.Pos.Latlong.Long,
		fs.Pos.Heading, fs.Pos.AltitudeFeet, fs.Pos.SpeedKnots,
		fs.F.Id.Designator, fs.F.Id.ModeS, fs.F.Id.Callsign, fs.F.Id.Registration,
		date.InPdt(fs.Pos.TimestampUTC).Format("01/02 15:04:05 MST"),
		fs.F.EnterUTC.Format("15:04:05 MST"),
		fmt.Sprintf("/fdb/lookup?map=1&id=%s", fs.F.Id),
	)
}
Ejemplo n.º 11
0
func complaintsForHandler(w http.ResponseWriter, r *http.Request) {
	ctx := req2ctx(r)

	s, e := widget.FormValueEpochTime(r, "start"), widget.FormValueEpochTime(r, "end")
	if r.FormValue("start") == "" {
		s, e = date.WindowForToday()
	}
	flightnumber := r.FormValue("flight")
	if e.Sub(s) > (time.Hour * 24) {
		http.Error(w, "time span too wide", http.StatusInternalServerError)
		return
	} else if s.Year() < 2015 {
		http.Error(w, "times in the past", http.StatusInternalServerError)
		return
	} else if flightnumber == "" {
		http.Error(w, "no flightnumber", http.StatusInternalServerError)
		return
	}

	cdb := complaintdb.NewDB(ctx)

	times, err := cdb.GetComplaintTimesInSpanByFlight(s, e, flightnumber)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	if r.FormValue("debug") != "" {
		str := fmt.Sprintf("* %s\n* %s\n* %s\n* [%d]\n\n", s, e, flightnumber, len(times))
		for i, t := range times {
			str += fmt.Sprintf("%3d  %s\n", i, date.InPdt(t))
		}
		w.Header().Set("Content-Type", "text/plain")
		w.Write([]byte(str))
		return
	}

	w.Header().Set("Content-Type", "application/json")
	jsonBytes, err := json.Marshal(times)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Write(jsonBytes)
}
Ejemplo n.º 12
0
func buildLegend(t time.Time) string {
	legend := date.InPdt(t).Format("15:04:05 MST (2006/01/02)")

	legend += " [" +
		legendUrl(t, -3600, "-1h") + ", " +
		legendUrl(t, -1200, "-20m") + ", " +
		legendUrl(t, -600, "-10m") + ", " +
		legendUrl(t, -300, "-5m") + ", " +
		legendUrl(t, -60, "-1m") + ", " +
		legendUrl(t, -30, "-30s") + "; " +
		legendUrl(t, 30, "+30s") + "; " +
		legendUrl(t, 60, "+1m") + ", " +
		legendUrl(t, 300, "+5m") + ", " +
		legendUrl(t, 600, "+10m") + ", " +
		legendUrl(t, 1200, "+20m") + ", " +
		legendUrl(t, 3600, "+1h") +
		"]"
	return legend
}
Ejemplo n.º 13
0
func (f Flight) SFOClassB(trackKey string, metars *metar.Archive) (violation bool, cbt ClassBTrack) {
	violation, cbt = false, ClassBTrack{}

	track := f.Track
	if trackKey != "" {
		track = f.Tracks[trackKey]
	}

	for i, tp := range track {
		cbtp := ClassBTrackPoint{TP: &track[i]}

		cbtp.Debug = date.InPdt(tp.TimestampUTC).Format("** 2006/01/02, 15:04:05 MST") +
			tp.TimestampUTC.Format(" (2006/01/02, 15:04:05 MST)\n") +
			"** Trackpoint [" + fmt.Sprintf("%d", i) + "] source: <b>" + track.LongSource() + "</b>\n"

		if f.Id.Destination != "SFO" {
			cbtp.Debug += "* Altitude: " + fmt.Sprintf("%.0f", tp.AltitudeFeet) + "\n" +
				"* VerticalRate: " + fmt.Sprintf("%.0f", tp.VerticalRate) + " feet/min\n" +
				"* Heading: " + fmt.Sprintf("%.0f", tp.Heading) + "\n"
			//"* Flight not a scheduled SFO arrival\n"

		} else {
			sfo.SFOClassBMap.ClassBPointAnalysis(tp.Latlong, tp.SpeedKnots, tp.AltitudeFeet, 200, &cbtp.A)
			cbtp.Debug += "* VerticalRate: " + fmt.Sprintf("%.0f", tp.VerticalRate) + " feet/min\n"
			cbtp.Debug += cbtp.A.Reasoning

			kLimit := 15.5
			if cbtp.A.DistNM < kLimit {
				cbtp.A.AllowThisPoint = true
				cbtp.Debug += fmt.Sprintf("** ClassB excursion detection disabled within %.1fNM\n", kLimit)
			} else if cbtp.A.IsViolation() {
				violation = true // We have at least one violating point in this trail
			}
		}

		cbt = append(cbt, cbtp)
	}

	return
}
Ejemplo n.º 14
0
func PostComplaint(client *http.Client, p types.ComplainerProfile, c types.Complaint) (string, error) {
	first, last := p.SplitName()
	addr := p.GetStructuredAddress()
	if c.Activity == "" {
		c.Activity = "Loud noise"
	}

	debug, submitkey, err := GetSubmitkey(client)
	if err != nil {
		return debug, err
	}
	debug += fmt.Sprintf("We got submitkey=%s\n", submitkey)

	// {{{ Populate form

	vals := url.Values{
		"response": {"json"},

		"contactmethod": {"App"},
		"app_key":       {"TUC8uDJMooVMvf7hew93nhUGcWgw"},

		"caller_code": {p.CallerCode},
		"name":        {first},
		"surname":     {last},
		"address1":    {addr.Street},
		"address2":    {""},
		"zipcode":     {addr.Zip},
		"city":        {addr.City},
		"state":       {addr.State},
		"email":       {p.EmailAddress},

		"airports": {"KSFO"}, // KOAK, KSJC, KSAN
		"month":    {date.InPdt(c.Timestamp).Format("1")},
		"day":      {date.InPdt(c.Timestamp).Format("2")},
		"year":     {date.InPdt(c.Timestamp).Format("2006")},
		"hour":     {date.InPdt(c.Timestamp).Format("15")},
		"min":      {date.InPdt(c.Timestamp).Format("4")},

		"aircraftcategory": {"J"},
		"eventtype":        {"Loud noise"}, // perhaps map c.Activity to something ?
		"comments":         {c.Description},
		"responserequired": {"N"},
		"enquirytype":      {"C"},

		"submit":    {"Submit complaint"},
		"submitkey": {submitkey},

		"nowebtrak":       {"1"},
		"defaulttime":     {"0"},
		"webtraklinkback": {""},
		"title":           {""},
		"homephone":       {""},
		"workphone":       {""},
		"cellphone":       {""},
	}

	if c.AircraftOverhead.FlightNumber != "" {
		vals.Add("acid", c.AircraftOverhead.Callsign)
		vals.Add("aacode", c.AircraftOverhead.Id2)
		vals.Add("tailnumber", c.AircraftOverhead.Registration)
		vals.Add("aircrafttype", c.AircraftOverhead.EquipType)

		//vals.Add("adflag", "??") // Operation type (A, D or O for Arr, Dept or Overflight)
		//vals.Add("beacon", "??") // SSR code (eg 210)
	}

	// }}}

	debug += "Submitting these vals:-\n"
	for k, v := range vals {
		debug += fmt.Sprintf(" * %-20.20s: %v\n", k, v)
	}

	if resp, err := client.PostForm("https://"+bksvHost+bksvPath, vals); err != nil {
		return debug, err

	} else {

		defer resp.Body.Close()
		body, _ := ioutil.ReadAll(resp.Body)
		if resp.StatusCode >= 400 {
			debug += fmt.Sprintf("ComplaintPOST: HTTP err '%s'\nBody:-\n%s\n--\n", resp.Status, body)
			return debug, fmt.Errorf("ComplaintPOST: HTTP err %s", resp.Status)
		}

		var jsonMap map[string]interface{}
		if err := json.Unmarshal([]byte(body), &jsonMap); err != nil {
			debug += fmt.Sprintf("ComplaintPOST: JSON unmarshal '%v'\nBody:-\n%s\n--\n", err, body)
			return debug, fmt.Errorf("ComplaintPOST: JSON unmarshal %v", err)

			/* Fall back ?
			if !regexp.MustCompile(`(?i:received your complaint)`).MatchString(string(body)) {
				debug += fmt.Sprintf("BKSV body ...\n%s\n------\n", string(body))
				return debug,fmt.Errorf("Returned response did not say 'received your complaint'")
			} else {
				debug += "Success !\n"+string(body)
			}
			*/

		} else if v := jsonMap["result"]; v == nil {
			return debug, fmt.Errorf("ComplaintPOST: jsonmap had no 'result'.\nBody:-\n%s\n--\n", body)

		} else {
			result := v.(string)
			if result == "1" {
				debug += "Json Success !\n"
			} else {
				debug += fmt.Sprintf("Json result not '1':-\n%#v\n--\n", jsonMap)
				return debug, fmt.Errorf("ComplaintPOST: result='%s'", result)
			}
		}
	}

	return debug, nil
}
Ejemplo n.º 15
0
func templateFormatPdt(t time.Time, format string) string {
	return date.InPdt(t).Format(format)
}
Ejemplo n.º 16
0
func queryHandler(w http.ResponseWriter, r *http.Request) {
	if r.FormValue("date") == "" && r.FormValue("epoch") == "" {
		var params = map[string]interface{}{
			"TwoHoursAgo": date.NowInPdt().Add(-2 * time.Hour),
		}
		if err := templates.ExecuteTemplate(w, "fdb-queryform", params); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}

	db := fdb.NewDB(r)
	db.Memcache = true

	var t time.Time
	if r.FormValue("epoch") != "" {
		if epoch, err := strconv.ParseInt(r.FormValue("epoch"), 10, 64); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		} else {
			t = time.Unix(epoch, 0)
		}
	} else {
		var err2 error
		t, err2 = date.ParseInPdt("2006/01/02 15:04:05", r.FormValue("date")+" "+r.FormValue("time"))
		if err2 != nil {
			http.Error(w, err2.Error(), http.StatusInternalServerError)
			return
		}
	}

	var refPoint *geo.Latlong = nil
	if r.FormValue("lat") != "" {
		refPoint = &geo.Latlong{}
		var err error
		if refPoint.Lat, err = strconv.ParseFloat(r.FormValue("lat"), 64); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		if refPoint.Long, err = strconv.ParseFloat(r.FormValue("long"), 64); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	}

	if snapshots, err := db.LookupSnapshotsAtTimestampUTC(t.UTC(), refPoint, 1000); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	} else {
		var params = map[string]interface{}{
			"Legend":        buildLegend(t),
			"SearchTimeUTC": t.UTC(),
			"SearchTime":    date.InPdt(t),
			"Flights":       snapshots2params(snapshots),

			"FlightsJS":  ftype.FlightSnapshotSet(snapshots).ToJSVar(),
			"MapsAPIKey": kGoogleMapsAPIKey,
			"Center":     sfo.KLatlongSERFR1,
			"Zoom":       9,
			// "CaptureArea": fdb.KBoxSnarfingCatchment,  // comment out, as we don't want it in this view
		}

		if r.FormValue("resultformat") == "json" {
			for i, _ := range snapshots {
				snapshots[i].F.Track = nil
				snapshots[i].F.Tracks = nil
			}
			js, err := json.Marshal(snapshots)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			w.Header().Set("Content-Type", "application/json")
			w.Write(js)

		} else {
			templateName := "fdb-queryresults-map"
			if r.FormValue("resultformat") == "list" {
				templateName = "fdb-queryresults-list"
			}

			if err := templates.ExecuteTemplate(w, templateName, params); err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
			}
		}
	}
}
Ejemplo n.º 17
0
func PopulateForm(c types.Complaint, submitkey string) url.Values {
	first, last := c.Profile.SplitName()
	addr := c.Profile.GetStructuredAddress()
	if c.Activity == "" {
		c.Activity = "Loud noise"
	}

	vals := url.Values{
		"response": {"json"},

		"contactmethod": {"App"},
		"apiKey":        {"399734e01c8cd5c21205599689cc77f2a50467f28e6f5d58a69f2b097d71b839c20e0051175107e74130ae9a3bbaccbe51ec5742e6ca3e51ff40cc1a8f401009"},

		"caller_code": {c.Profile.CallerCode},
		"name":        {first},
		"surname":     {last},
		"address1":    {addr.Street},
		"address2":    {""},
		"zipcode":     {addr.Zip},
		"city":        {addr.City},
		"state":       {addr.State},
		"email":       {c.Profile.EmailAddress},

		"airports": {"KSFO"}, // KOAK, KSJC, KSAN
		"month":    {date.InPdt(c.Timestamp).Format("1")},
		"day":      {date.InPdt(c.Timestamp).Format("2")},
		"year":     {date.InPdt(c.Timestamp).Format("2006")},
		"hour":     {date.InPdt(c.Timestamp).Format("15")},
		"min":      {date.InPdt(c.Timestamp).Format("4")},

		"aircraftcategory": {"J"},
		"eventtype":        {"Loud noise"}, // perhaps map c.Activity to something ?
		"comments":         {c.Description},
		"responserequired": {"N"},
		"enquirytype":      {"C"},

		"submit": {"Submit complaint"},
		//"submitkey":        {submitkey},

		"nowebtrak":       {"1"},
		"defaulttime":     {"0"},
		"webtraklinkback": {""},
		"title":           {""},
		"homephone":       {""},
		"workphone":       {""},
		"cellphone":       {""},

		"browser_name":     {c.Browser.Name},
		"browser_version":  {c.Browser.Version},
		"browser_vendor":   {c.Browser.Vendor},
		"browser_uuid":     {c.Browser.UUID},
		"browser_platform": {c.Browser.Platform},
	}

	if c.AircraftOverhead.FlightNumber != "" {
		vals.Add("acid", c.AircraftOverhead.Callsign)
		vals.Add("aacode", c.AircraftOverhead.Id2)
		vals.Add("tailnumber", c.AircraftOverhead.Registration)
		vals.Add("aircrafttype", c.AircraftOverhead.EquipType)

		//vals.Add("adflag", "??") // Operation type (A, D or O for Arr, Dept or Overflight)
		//vals.Add("beacon", "??") // Squawk SSR code (eg 2100)
	}

	return vals
}
Ejemplo n.º 18
0
func (e SkimEvent) String() string {
	return fmt.Sprintf("%.0fft [%.1f,%.1f]NM; from %s for %s",
		e.Altitude, e.StartNM, e.EndNM,
		date.InPdt(e.StartTP.TimestampUTC).Format("15:04:06 MST"),
		e.EndTP.TimestampUTC.Sub(e.StartTP.TimestampUTC))
}
Ejemplo n.º 19
0
func (f Flight) Legend() string {
	l := fmt.Sprintf("<b>%s</b> (%s,%s) %s<br/>Tags=<b>%v</b> Tracks=%v<br/>Route=<b>%v</b>",
		f.Id, f.Id.Callsign, f.Id.ModeS, date.InPdt(f.EnterUTC).Format("2006/01/02 15:03 MST"),
		f.JustTagList(), f.TrackList(), f.WaypointList())
	return l
}
Ejemplo n.º 20
0
func (track Track) SkimsToSFO(feetWithin float64, minDurationNM float64, minDistNM, maxDistNM float64) (AltitudeTrack, SkimAnalysis) {
	at := AltitudeTrack{}
	a := SkimAnalysis{Events: []SkimEvent{}}

	// Build this up first, then mutate it during analysis below
	for i, tp := range track {
		atp := AltitudeTrackPoint{TP: &track[i]}

		atp.Debug = date.InPdt(tp.TimestampUTC).Format("** 2006/01/02, 15:04:05 MST") +
			tp.TimestampUTC.Format(" (2006/01/02, 15:04:05 MST)\n") +
			"** Trackpoint [" + fmt.Sprintf("%d", i) + "] source: <b>" + track.LongSource() + "</b>\n" +
			"* Altitude: " + fmt.Sprintf("%.0f", tp.AltitudeFeet) + "\n" +
			"* Heading: " + fmt.Sprintf("%.0f", tp.Heading) + "\n"
		at = append(at, atp)
	}

	skimAltitudes := []float64{8000, 6000}
	for _, targetAltitude := range skimAltitudes {
		iStart := -1 // Index when a skim starts
		initHeading := 0.0

		for i, tp := range track {
			if math.Abs(tp.AltitudeFeet-targetAltitude) < feetWithin {
				// We are within !!
				if iStart < 0 {
					iStart = i
					initHeading = tp.Heading
				}

				// Bail if we deviate by more than 10 degrees (i.e. delay vectoring)
				if math.Abs(initHeading-tp.Heading) > 10.0 {
					iStart = -1
				}

			} else {
				// We are outside. Flush a skim event if we were just inside, and it looks interesting
				if iStart > 0 {
					e := SkimEvent{
						Altitude: targetAltitude,
						StartTP:  &track[iStart],
						EndTP:    &track[i-1],
					}
					e.StartNM = sfo.KLatlongSFO.DistNM(e.StartTP.Latlong)
					e.EndNM = sfo.KLatlongSFO.DistNM(e.EndTP.Latlong)

					// Is this event long enough ? Does it overlap with our range of interest ?
					if (math.Abs(e.StartNM-e.EndNM) >= minDurationNM) &&
						(e.EndNM <= maxDistNM) && (e.StartNM >= minDistNM) {
						a.Events = append(a.Events, e)

						for j := iStart; j <= i; j++ {
							at[j].Flagged = true
							at[j].Debug += fmt.Sprintf("* target:%.0fft, tolerance:%.0ft\n",
								targetAltitude, feetWithin)
						}
					}
					iStart = -1
				}
			}
		}
	}

	return at, a
}