// allMarkers returns the URL fragment containing all the markers of the specified GPS records to be appended // to a static maps URL. // // Static maps documentation: // https://developers.google.com/maps/documentation/staticmaps/ func allMarkers(records []*ds.GPS) string { // Returned string will be around 1 KB, allocated reasonable buffer: b := bytes.NewBuffer(make([]byte, 0, 1280)) // MARKERS var prev *ds.GPS for idx, r := range records { if !r.Track() { prev = r continue } // Determine color. Default marker color: blue; // First records after a Start event: green // Last records before a Stop event: red clr := "blue" if prev != nil && prev.Evt() == ds.EvtStop { clr = "red" } // Check next if idx < len(records)-1 && records[idx+1].Evt() == ds.EvtStart { clr = "green" } fmt.Fprintf(b, "&markers=color:%s|label:%c|%f,%f", clr, r.Label, r.GeoPoint.Lat, r.GeoPoint.Lng) prev = r } // PATHS i := 0 for _, r := range records { if !r.Track() { i = 0 continue } if i == 0 { b.WriteString("&path=") } else if i > 0 { b.WriteString("|") } fmt.Fprintf(b, "%f,%f", r.GeoPoint.Lat, r.GeoPoint.Lng) i++ } return b.String() }
// checkAlert checks the specified alert. func checkAlert(c appengine.Context, a *ds.Alert, accKeyID int64) { const alertDurationMin = 5 const alertDuration = alertDurationMin * time.Minute // Get latest car GPS records carRecords, err := getDevRecords(c, a.CarDevID) if err != nil { return } // Check if car GPS records are received properly: if time.Since(carRecords[0].Created) > alertDuration { c.Warningf("No car GPS records found in the last %d minutes!", alertDurationMin) sendAlert(c, accKeyID, "Car GPS device gone dark!", carGoneDarkAlertMail) return } if a.PersMobDevID == 0 { c.Debugf("Car GPS records found in the last 5 minutes. No Personal Mobile device specified.") return } carMoved := devMoved(carRecords) if !carMoved { // Nothing more to do if car is not moving c.Infof("Car is not moving. Ok.") return } c.Infof("Car is moving!") // Get latest personal mobile GPS records persMobRecords, err := getDevRecords(c, a.PersMobDevID) if err != nil { return } // Check if personal mobile GPS records are received properly: if time.Since(persMobRecords[0].Created) > alertDuration { c.Warningf("No personal mobile GPS records found in the last %d minutes!", alertDurationMin) sendAlert(c, accKeyID, "Car is moving without you!", carMovingWithoutYouMail) return } persMobMoved := devMoved(persMobRecords) // Do not draw fast conclusion here if personal mobile is not moving, // it might be GPS tracking was just turned on and we don't have 2 track records yet // or they are not at great distance. But if we don't even have 1 track record, // that's hijacking-suspicious: if persMobMoved { c.Infof("Personal mobile is also moving!") } else { c.Infof("Personal mobile is NOT moving!") } var pg1 *ds.GPS for _, r := range persMobRecords { if r.Track() { pg1 = r break } } if pg1 == nil || time.Since(pg1.Created) > alertDuration { // Car is moving and we don't have recent track record from personal mobile! c.Warningf("No personal mobile GPS track record found in the last %d minutes!", alertDurationMin) sendAlert(c, accKeyID, "Car is moving without you!", carMovingWithoutYouMail) return } // Check distance: // We have a track record for both devices for sure (because both moved which also ensures having at least 2!). var cg1, cg2 *ds.GPS for _, r := range carRecords { if r.Track() { if cg1 == nil { cg1 = r } else if cg2 == nil { cg2 = r break } } } cg1.Dd = logic.Distance(cg2.GeoPoint.Lat, cg2.GeoPoint.Lng, cg1.GeoPoint.Lat, cg1.GeoPoint.Lng) // [m] cg1.Dt = cg1.Created.Sub(cg2.Created) // duration cv := float64(cg1.Dd) / cg1.Dt.Seconds() // Car speed [m/s] cpdt := math.Abs(cg1.Created.Sub(pg1.Created).Seconds()) // [s] dist := logic.Distance(cg1.GeoPoint.Lat, cg1.GeoPoint.Lng, pg1.GeoPoint.Lat, pg1.GeoPoint.Lng) c.Debugf("Car movement speed: %.1f km/h", cv*3.6) c.Debugf("Delta T between latest Car and PersMob GPS records: %d s", int64(cpdt)) c.Debugf("Car - PersMob distance: %d m", dist) var alertMargin int64 = 500 // [m] // Increase alert margin based on the movement speed of the car and the delta time between // the last car and personal mobile GPS records. // Also if this delta time is greater, accuracy decreases/drops. // So also increase alert margin based on delta time: 6 meters for every second. // (It is an effect like increasing car speed by 6 m/s = 21.6 km/h.) // BE RESTRICTIVE: Only do this correction if personal mobile is also moving! // If not, do not let the car get far away (if for example pers mob is not moving, // the car could get kilometers away before alert would be sent). if persMobMoved { alertMargin += int64(cv*cpdt + cpdt*6) } c.Debugf("Using alert margin distance: %d m", alertMargin) if dist > alertMargin { c.Warningf("Personal mobile is not moving together with car!") sendAlert(c, accKeyID, "Car is moving without you!", carMovingWithoutYouMail) return } c.Infof("They are moving together. Ok.") }
// logs is the logic implementation of the Logs page. func logs(p *page.Params) { c := p.AppCtx // First get devices var devices []*ds.Device if devices, p.Err = cache.GetDevListForAccKey(c, p.Account.GetKey(c)); p.Err != nil { return } p.Custom["Devices"] = devices fv := p.Request.FormValue p.Custom["Before"] = fv("before") p.Custom["After"] = fv("after") p.Custom["SearchLoc"] = fv("loc") if fv("devID") == "" { // No device chosen yet return } var err error var devID int64 if devID, err = strconv.ParseInt(string(fv("devID")), 10, 64); err != nil { p.ErrorMsg = "Invalid Device! Please select a Device from the list below." return } // Check if device is owned by the user: var dev *ds.Device for _, d := range devices { if d.KeyID == devID { dev = d p.Custom["Device"] = d break } } if dev == nil { p.ErrorMsg = "You do not have access to the specified Device! Please select a Device from the list below." return } // Parse filters: var before time.Time if fv("before") != "" { if before, err = p.ParseTime(timeLayout, strings.TrimSpace(fv("before"))); err != nil { p.ErrorMsg = template.HTML(`Invalid <span class="highlight">Before</span>!`) return } // Add 1 second to the parsed time because fraction of a second is not parsed but exists, // so this new time will also include records which has the same time up to the second part and has millisecond part too. before = before.Add(time.Second) } var after time.Time if fv("after") != "" { if after, err = p.ParseTime(timeLayout, strings.TrimSpace(fv("after"))); err != nil { p.ErrorMsg = template.HTML(`Invalid <span class="highlight">After</span>!`) return } } var searchLoc appengine.GeoPoint areaCode := int64(-1) if dev.Indexed() && fv("loc") != "" { // GPS coordinates; lat must be in range -90..90, lng must be in range -180..180 baseErr := template.HTML(`Invalid <span class="highlight">Location</span>!`) var coords = strings.Split(strings.TrimSpace(fv("loc")), ",") if len(coords) != 2 { p.ErrorMsg = baseErr return } searchLoc.Lat, err = strconv.ParseFloat(coords[0], 64) if err != nil { p.ErrorMsg = baseErr return } searchLoc.Lng, err = strconv.ParseFloat(coords[1], 64) if err != nil { p.ErrorMsg = baseErr return } if !searchLoc.Valid() { p.ErrorMsg = template.HTML(`Invalid <span class="highlight">Location</span> specified by latitude and longitude! Valid range: [-90, 90] latitude and [-180, 180] longitude`) return } areaCode = AreaCodeForGeoPt(dev.AreaSize, searchLoc.Lat, searchLoc.Lng) } var page int cursorsString := fv("cursors") var cursors = strings.Split(cursorsString, ";")[1:] // Split always returns at least 1 element (and we use semicolon separator before cursors) // Form values if fv("page") == "" { page = 1 } else { page, err = strconv.Atoi(fv("page")) if err != nil || page < 1 { page = 1 } if page > len(cursors) { // If page is provided, so are (should be) the cursors page = len(cursors) } } switch { case fv("submitFirstPage") != "": page = 1 case fv("submitPrevPage") != "": if page > 1 { page-- } case fv("submitNextPage") != "": page++ } pageSize := p.Account.GetLogsPageSize() if ps := fv("pageSize"); ps != "" && ps != strconv.Itoa(pageSize) { // Page size has been changed (on Settings page), drop cursors, render page 1 page = 1 cursorsString = "" cursors = make([]string, 0, 1) } // 'ts all good, proceed with the query: q := datastore.NewQuery(ds.ENameGPS).Filter(ds.PNameDevKeyID+"=", devID) if !before.IsZero() { q = q.Filter(ds.PNameCreated+"<", before) } if !after.IsZero() { q = q.Filter(ds.PNameCreated+">", after) } if areaCode >= 0 { q = q.Filter(ds.PNameAreaCodes+"=", areaCode) } q = q.Order("-" + ds.PNameCreated).Limit(pageSize) var records = make([]*ds.GPS, 0, pageSize) // If there is a cursor, set it. // Page - cursor index mapping: cursors[page-2] // 1st page: no cursor, 2nd page: cursors[0], 3nd page: cursors[1], ... if page > 1 && page <= len(cursors)+1 { var cursor datastore.Cursor if cursor, p.Err = datastore.DecodeCursor(cursors[page-2]); p.Err != nil { return } q = q.Start(cursor) } // Iterate over the results: t := q.Run(c) for { r := new(ds.GPS) _, err := t.Next(r) if err == datastore.Done { break } if err != nil { // Datastore error p.Err = err return } records = append(records, r) r.Dd = -1 // For now, will be set if applicable if r.Track() { // Check the previous (in time) record and calculate distance. // If previous is not a Track, check the one before that etc. for i := len(records) - 2; i >= 0; i-- { if prev := records[i]; prev.Track() { prev.Dd = Distance(r.GeoPoint.Lat, r.GeoPoint.Lng, prev.GeoPoint.Lat, prev.GeoPoint.Lng) prev.Dt = prev.Created.Sub(r.Created) break } } } } if len(records) == 0 { // End of list reached, disable Next page button: p.Custom["EndOfList"] = true } if page == 1 || page > len(cursors) { // Get updated cursor and store it for next page: var cursor datastore.Cursor if cursor, p.Err = t.Cursor(); p.Err != nil { return } cursorString := cursor.String() if page == 1 { // If new records were inserted, they appear on the first page in which case // the cursor for the 2nd page changes (and all other cursors will change). // In this case drop all the cursors: if len(cursors) > 0 && cursors[0] != cursorString { cursorsString = "" cursors = make([]string, 0, 1) } } else { // When end of list is reached, the same cursor will be returned if len(records) == 0 && page == len(cursors)+1 && cursors[page-2] == cursorString { // Add 1 extra, empty page, but not more. if page > 2 && cursors[page-3] == cursorString { // An extra, empty page has already been added, do not add more: page-- } } } if page > len(cursors) { cursors = append(cursors, cursorString) cursorsString += ";" + cursorString } } // Calculate labels: '1'..'9' then 'A'... for i, lbl := len(records)-1, '1'; i >= 0; i-- { if r := records[i]; r.Track() { r.Label = lbl if lbl == '9' { lbl = 'A' - 1 } lbl++ } } p.Custom["CursorList"] = cursors p.Custom["Cursors"] = cursorsString p.Custom["Page"] = page p.Custom["PageSize"] = pageSize p.Custom["RecordOffset"] = (page-1)*pageSize + 1 p.Custom["Records"] = records if p.Mobile { p.Custom["MapWidth"], p.Custom["MapHeight"] = p.Account.GetMobMapPrevSize() p.Custom["MapImgFormat"] = p.Account.GetMobMapImgFormat() } else { p.Custom["MapWidth"], p.Custom["MapHeight"] = p.Account.GetMapPrevSize() } p.Custom["APIKey"] = "AIzaSyCEU_tZ1n0-mMg4woGKIfPqdbi0leSKvjg" p.Custom["AllMarkers"] = allMarkers(records) if len(records) == 0 { if page == 1 { if before.IsZero() && after.IsZero() && areaCode < 0 { p.Custom["PrintNoRecordsForDev"] = true } else { p.Custom["PrintNoMatchForFilters"] = true } } else { p.Custom["PrintNoMoreRecords"] = true } } }
// gpsHandler is the handler of the requests originating from (GPS) clients // reporting GPS coordinates, start/stop events. func gpsHandler(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) // General logs for all requests c.Debugf("Location: %s;%s;%s;%s", r.Header.Get("X-AppEngine-Country"), r.Header.Get("X-AppEngine-Region"), r.Header.Get("X-AppEngine-City"), r.Header.Get("X-AppEngine-CityLatLong")) // If device id is invalid, we want to return sliently and not let the client know about it. // So first check non-user related params first, because else if the client would intentionally // provide an invalid param and get no error, he/she would know that the device id is invalid. RandID := r.FormValue("dev") if RandID == "" { c.Errorf("Missing Device ID (dev) parameter!") http.Error(w, "Missing Device ID (dev) parameter!", http.StatusBadRequest) return } // Do a check for RandID length: it is used to construct a memcache key which has a 250 bytes limit! if len(RandID) > 100 { c.Errorf("Invalid Device ID (dev) parameter!") http.Error(w, "Invalid Device ID (dev) parameter!", http.StatusBadRequest) return } gps := ds.GPS{Created: time.Now()} var err error tracker := r.FormValue("tracker") if tracker != "" { // Start-stop event switch tracker { case "start": gps.AreaCodes = []int64{int64(ds.EvtStart)} case "stop": gps.AreaCodes = []int64{int64(ds.EvtStop)} default: c.Errorf("Invalid tracker parameter!") http.Error(w, "Invalid tracker parameter!", http.StatusBadRequest) return } } else { // GPS coordinates; lat must be in range -90..90, lng must be in range -180..180 gps.GeoPoint.Lat, err = strconv.ParseFloat(r.FormValue("lat"), 64) if err != nil { c.Errorf("Missing or invalid latitude (lat) parameter!") http.Error(w, "Missing or invalid latitude (lat) parameter!", http.StatusBadRequest) return } gps.GeoPoint.Lng, err = strconv.ParseFloat(r.FormValue("lon"), 64) if err != nil { c.Errorf("Missing or invalid longitude (lon) parameter!") http.Error(w, "Missing or invalid longitude (lon) parameter!", http.StatusBadRequest) return } if !gps.GeoPoint.Valid() { c.Errorf("Invalid geopoint specified by latitude (lat) and longitude (lon) parameters (valid range: [-90, 90] latitude and [-180, 180] longitude)!") http.Error(w, "Invalid geopoint specified by latitude (lat) and longitude (lon) parameters (valid range: [-90, 90] latitude and [-180, 180] longitude)!", http.StatusBadRequest) return } } var dev *ds.Device dev, err = cache.GetDevice(c, RandID) if err != nil { c.Errorf("Failed to get Device with cache.GetDevice(): %v", err) http.Error(w, "", http.StatusInternalServerError) return } if dev == nil { // Invalid RandID. Do nothing and return silently. return } gps.DevKeyID = dev.KeyID if tracker == "" && dev.Indexed() { gps.AreaCodes = logic.AreaCodesForGeoPt(dev.AreaSize, gps.GeoPoint.Lat, gps.GeoPoint.Lng) } var gpsKey *datastore.Key if dev.DelOldLogs() { // There is a (positive) Logs Retention for the device. // We could delete old records, but that costs us a lot of Datastore write ops // (exactly the same as saving a new record). // Instead I query for old records (beyond the Logs Retention), // and "resave" to those records (basically save a new record with an existing key). // This way no deletion has to be paid for. // Optimal solution would be to pick the oldest record, but that would require a new index (G: d, t) // which is a "waste", so I just use the existing index (G: d, -t). This will result in the first record // that is just over the Retention period. // Load the existing record by key (strong consistency) and check if its timestamp is truely // beyond the retention (because a concurrent resave might have happened). // Query more than 1 record (limit>1) because if the latest is just concurrently resaved, // we have more records without executing another query. And small operations are free (reading keys). t := time.Now().Add(-24 * time.Hour * time.Duration(dev.LogsRetention)) q := datastore.NewQuery(ds.ENameGPS). Filter(ds.PNameDevKeyID+"=", dev.KeyID). Filter(ds.PNameCreated+"<", t). Order("-" + ds.PNameCreated).KeysOnly().Limit(7) keys, err := q.GetAll(c, nil) if err != nil { c.Errorf("Failed to list GPS records beyond Retention period: %v", err) http.Error(w, "", http.StatusInternalServerError) return } for _, key := range keys { // Load by key (strongly consistent) var oldGps ds.GPS if err = datastore.Get(c, key, &oldGps); err != nil { c.Errorf("Failed to load GPS by Key: %v", err) http.Error(w, "", http.StatusInternalServerError) return } if t.After(oldGps.Created) { // Good: it is still older (not resaved concurrently). We will use this! gpsKey = key break } } } if gpsKey == nil { // No current record to resave, create a new incomplete key for a new record: gpsKey = datastore.NewIncompleteKey(c, ds.ENameGPS, nil) } _, err = datastore.Put(c, gpsKey, &gps) if err != nil { c.Errorf("Failed to store GPS record: %v", err) http.Error(w, "", http.StatusInternalServerError) return } }