igcfs/go-igc

View on GitHub
parse.go

Summary

Maintainability
A
0 mins
Test Coverage
// Copyright ©2017 The ezgliding Authors.
//
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package igc

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "strconv"
    "strings"
    "time"
)

const (
    // TimeFormat is the golang time.Parse format for IGC time.
    TimeFormat = "150405"
    // DateFormat is the golang time.Parse format for IGC time.
    DateFormat = "020106"
)

// ParseLocation returns a Track object corresponding to the given file.
//
// It calls Parse internally, so the file content should be in IGC format.
func ParseLocation(location string) (Track, error) {
    var content []byte

    // case http
    if strings.HasPrefix(location, "http") {
        resp, err := http.Get(location)
        if err == nil {
            defer resp.Body.Close()
            content, err = ioutil.ReadAll(resp.Body)
            if err != nil {
                return Track{}, err
            }
            return Parse(string(content))
        }
    }

    // let's try if it is local file
    resp, err := ioutil.ReadFile(location)
    if err != nil {
        return Track{}, err
    }
    content = resp

    return Parse(string(content))
}

// Parse returns a Track object corresponding to the given content.
//
// The value of content should be a text string with all the flight data
// in the IGC format.
func Parse(content string) (Track, error) {

    p := parser{}

    parsingDispath := map[byte]func(string, *Track) error{
        'A': p.parseA,
        'B': p.parseB,
        'D': p.parseD,
        'E': p.parseE,
        'F': p.parseF,
        'G': p.parseG,
        'H': p.parseH,
        'I': parseIJ('I', &p),
        'J': parseIJ('J', &p),
        'K': p.parseK,
        'L': p.parseL,
    }

    f := NewTrack()
    var err error
    lines := strings.Split(content, "\n")
    for i := range lines {
        line := strings.TrimSpace(lines[i])
        // ignore empty lines
        if len(strings.Trim(line, " ")) < 1 {
            continue
        }

        if fn, ok := parsingDispath[line[0]]; ok {
            err = fn(line, &f)
        } else if line[0] == 'C' {
            if !p.taskDone {
                err = p.parseC(lines[i:], &f)
            }
        } else {
            err = fmt.Errorf("invalid record :: %v", line)
        }

        if err != nil {
            return f, err
        }
    }

    return f, nil
}

type field struct {
    start int64
    end   int64
    tlc   string
}

type parser struct {
    IFields  []field
    JFields  []field
    taskDone bool
    numSat   int
}

func (p *parser) parseA(line string, f *Track) error {
    if len(line) < 7 {
        return fmt.Errorf("line too short :: %v", line)
    }
    f.Manufacturer = line[1:4]
    f.UniqueID = line[4:7]
    f.AdditionalData = line[7:]
    return nil
}

func (p *parser) parseB(line string, f *Track) (err error) {
    // handle all panic calls here
    defer func() {
        if r := recover(); r != nil {
            err = r.(error)
        }
    }()

    if len(line) < 35 {
        panic(fmt.Errorf("line too short :: %v", line))
    }

    pt := NewPointFromDMD(
        line[7:15], line[15:24])

    var e error
    pt.Time, e = time.Parse(TimeFormat, line[1:7])
    if e != nil {
        panic(e)
    }
    if line[24] == 'A' || line[24] == 'V' {
        pt.FixValidity = line[24]
    } else {
        panic(fmt.Errorf("invalid fix validity :: %v", line[24]))
    }
    pt.PressureAltitude, e = strconv.ParseInt(line[25:30], 10, 64)
    if e != nil {
        panic(e)
    }
    pt.GNSSAltitude, e = strconv.ParseInt(line[30:35], 10, 64)
    if e != nil {
        panic(e)
    }
    for _, f := range p.IFields {
        pt.IData[f.tlc] = line[f.start-1 : f.end]
    }
    pt.NumSatellites = p.numSat
    f.Points = append(f.Points, pt)
    return
}

func (p *parser) parseC(lines []string, f *Track) (errRet error) {
    // handle all panic calls here
    defer func() {
        if r := recover(); r != nil {
            errRet = r.(error)
        }
    }()

    line := lines[0]
    if len(line) < 25 {
        panic(fmt.Errorf("wrong line size :: %v", line))
    }
    var err error
    var nTP int
    if nTP, err = strconv.Atoi(line[23:25]); err != nil {
        panic(fmt.Errorf("invalid number of turnpoints :: %v", line))
    }
    if len(lines) < 5+nTP {
        panic(fmt.Errorf("invalid number of C record lines :: %v", lines))
    }
    if f.Task.DeclarationDate, err = time.Parse(DateFormat+TimeFormat, lines[0][1:13]); err != nil {
        f.Task.DeclarationDate = time.Time{}
    }
    if f.Task.Date, err = time.Parse(DateFormat, lines[0][13:19]); err != nil {
        f.Task.Date = time.Time{}
    }
    if f.Task.Number, err = strconv.Atoi(line[19:23]); err != nil {
        panic(err)
    }
    f.Task.Description = line[25:]
    if f.Task.Takeoff, err = p.taskPoint(lines[1]); err != nil {
        panic(err)
    }
    if f.Task.Start, err = p.taskPoint(lines[2]); err != nil {
        panic(err)
    }
    for i := 0; i < nTP; i++ {
        var tp Point
        if tp, err = p.taskPoint(lines[3+i]); err != nil {
            panic(err)
        }
        f.Task.Turnpoints = append(f.Task.Turnpoints, tp)
    }
    if f.Task.Finish, err = p.taskPoint(lines[3+nTP]); err != nil {
        panic(err)
    }
    if f.Task.Landing, err = p.taskPoint(lines[4+nTP]); err != nil {
        panic(err)
    }
    p.taskDone = true
    return
}

func (p *parser) taskPoint(line string) (Point, error) {
    if len(line) < 18 {
        return Point{}, fmt.Errorf("line too short :: %v", line)
    }
    pt := NewPointFromDMD(
        line[1:9], line[9:18])
    pt.Description = line[18:]
    return pt, nil
}

func (p *parser) parseD(line string, f *Track) error {
    if len(line) < 6 {
        return fmt.Errorf("line too short :: %v", line)
    }
    if line[1] == '2' {
        f.DGPSStationID = line[2:6]
    }
    return nil
}

func (p *parser) parseE(line string, f *Track) error {
    if len(line) < 10 {
        return fmt.Errorf("line too short :: %v", line)
    }
    t, err := time.Parse(TimeFormat, line[1:7])
    if err != nil {
        return err
    }
    f.Events = append(f.Events, Event{Time: t, Type: line[7:10], Data: line[10:]})
    return nil
}

func (p *parser) parseF(line string, f *Track) error {
    if len(line) < 7 {
        return fmt.Errorf("line too short :: %v", line)
    }
    t, err := time.Parse(TimeFormat, line[1:7])
    if err != nil {
        return err
    }
    ids := make([]string, 0)
    for i := 7; i < len(line)-1; i = i + 2 {
        ids = append(ids, line[i:i+2])
    }
    f.Satellites = append(f.Satellites, Satellite{Time: t, Ids: ids})
    p.numSat = len(ids)
    return nil
}

func (p *parser) parseG(line string, f *Track) error {
    f.Signature = f.Signature + line[1:]
    return nil
}

func (p *parser) parseH(line string, f *Track) error {
    var err error
    if len(line) < 5 {
        return fmt.Errorf("line too short :: %v", line)
    }

    parsingDispath := map[string]*string{
        "PLT": &f.Pilot,
        "CM2": &f.Crew,
        "GTY": &f.GliderType,
        "GID": &f.GliderID,
        "DTM": &f.GPSDatum,
        "RFW": &f.FirmwareVersion,
        "RHW": &f.HardwareVersion,
        "FTY": &f.FlightRecorder,
        "PRS": &f.PressureSensor,
        "CID": &f.CompetitionID,
        "CCL": &f.CompetitionClass,
    }

    key := line[2:5]

    if field, ok := parsingDispath[key]; ok {
        *field = stripUpTo(line[5:], ":")
    } else {
        switch key {
        case "GPS":
            f.GPS = line[5:]
        case "DTE":
            if len(line) < 11 {
                return fmt.Errorf("line too short :: %v", line)
            }
            f.Date, err = time.Parse(DateFormat, line[5:11])
        case "FXA":
            if len(line) < 8 {
                err = fmt.Errorf("line too short :: %v", line)
            } else {
                f.FixAccuracy, err = strconv.ParseInt(line[5:8], 10, 64)
            }
        case "TZN":
            z, errFloat := strconv.ParseFloat(stripUpTo(line[5:], ":"), 64)
            if errFloat != nil {
                err = errFloat
            } else {
                f.Timezone = int(z)
            }
        default:
            err = fmt.Errorf("unknown record :: %v", line)
        }
    }
    return err
}

func parseIJ(ij byte, p *parser) func(string, *Track) error {
    return func(line string, t *Track) error {
        if len(line) < 3 {
            return fmt.Errorf("line too short :: %v", line)
        }
        n, err := strconv.ParseInt(line[1:3], 10, 0)
        if err != nil {
            return fmt.Errorf("invalid number of %v fields :: %v", ij, line)
        }
        if len(line) != int(n*7+3) {
            return fmt.Errorf("wrong line size :: %v", line)
        }
        for i := 0; i < int(n); i++ {
            s := i*7 + 3
            start, _ := strconv.ParseInt(line[s:s+2], 10, 0)
            end, _ := strconv.ParseInt(line[s+2:s+4], 10, 0)
            tlc := line[s+4 : s+7]
            switch ij {
            case 'I':
                p.IFields = append(p.IFields, field{start: start, end: end, tlc: tlc})
            case 'J':
                p.JFields = append(p.JFields, field{start: start, end: end, tlc: tlc})
            }
        }
        return nil
    }
}

func (p *parser) parseK(line string, f *Track) error {
    if len(line) < 7 {
        return fmt.Errorf("line too short :: %v", line)
    }
    t, err := time.Parse(TimeFormat, line[1:7])
    if err != nil {
        return err
    }
    fields := make(map[string]string)
    for _, f := range p.JFields {
        fields[f.tlc] = line[f.start-1 : f.end]
    }
    f.K = append(f.K, K{Time: t, Fields: fields})
    return nil
}

func (p *parser) parseL(line string, f *Track) error {
    if len(line) < 4 {
        return fmt.Errorf("line too short :: %v", line)
    }
    f.Logbook = append(f.Logbook, LogEntry{Type: line[1:4], Text: line[4:]})
    return nil
}

func stripUpTo(s string, sep string) string {
    i := strings.Index(s, sep)
    if i == -1 {
        return s
    }
    return s[i+1:]
}