From 51eab6e2aa2e9b13e198be10090de4278032aa4c Mon Sep 17 00:00:00 2001 From: Hari Mukti Date: Tue, 28 Nov 2023 11:36:10 +0700 Subject: [PATCH] feat: building block for encoding (#47) * add manufacturers.json for mapping devices * feat: implement building block for encoding * feat(FE): implement building block for encoding * fix: product name should not contain manufacturer name --- src/components/OpenActivity.vue | 82 +++--- src/spec/activity-service.ts | 73 ++++++ .../activity-service/activity/fit/creator.go | 6 - .../activity/fit/manufacturer.go | 31 +++ .../activity-service/activity/fit/service.go | 38 ++- .../activity-service/activity/fit/session.go | 4 +- .../activity-service/activity/gpx/service.go | 3 +- .../activity-service/activity/tcx/service.go | 2 +- src/wasm/activity-service/activity/util.go | 17 -- src/wasm/activity-service/kit/kit.go | 21 +- src/wasm/activity-service/main.go | 86 ++++++- src/wasm/activity-service/manufacturers.json | 81 ++++++ src/wasm/activity-service/scale/scale.go | 234 ++++++++++++++++++ src/wasm/activity-service/service/result.go | 47 ---- .../activity-service/service/result/decode.go | 47 ++++ .../activity-service/service/result/encode.go | 41 +++ .../service/result/manufacturer-list.go | 17 ++ src/wasm/activity-service/service/service.go | 56 +++-- src/workers/activity-service.ts | 41 ++- 19 files changed, 794 insertions(+), 133 deletions(-) create mode 100644 src/spec/activity-service.ts create mode 100644 src/wasm/activity-service/activity/fit/manufacturer.go create mode 100644 src/wasm/activity-service/manufacturers.json create mode 100644 src/wasm/activity-service/scale/scale.go delete mode 100644 src/wasm/activity-service/service/result.go create mode 100644 src/wasm/activity-service/service/result/decode.go create mode 100644 src/wasm/activity-service/service/result/encode.go create mode 100644 src/wasm/activity-service/service/result/manufacturer-list.go diff --git a/src/components/OpenActivity.vue b/src/components/OpenActivity.vue index c1a2621..d1ddbe6 100644 --- a/src/components/OpenActivity.vue +++ b/src/components/OpenActivity.vue @@ -46,7 +46,14 @@ import { Summary } from '@/spec/summary' :isWebAssemblySupported="isWebAssemblySupported" > -
Supported files: *.fit, *.gpx, *.tcx
+ +
+ Supported files: *.fit, *.gpx, *.tcx + + Instantiating WebAssembly + +
+
import { ActivityFile, Lap, Record, SPORT_GENERIC, Session } from '@/spec/activity' +import { DecodeResult, EncodeResult, ManufacturerListResult } from '@/spec/activity-service' import { Feature } from 'ol' import { GeoJSON } from 'ol/format' import { shallowRef } from 'vue' @@ -297,24 +305,6 @@ if (isWebAssemblySupported == false) { alert('Sorry, it appears that your browser does not support WebAssembly :(') } -class Result { - err: string | null = null - activities: Array - decodeTook: number - serializationTook: number - totalElapsed: number - - constructor(json?: any) { - const casted = json as Result - - this.err = casted?.err - this.activities = casted?.activities - this.decodeTook = casted?.decodeTook - this.serializationTook = casted?.serializationTook - this.totalElapsed = casted?.totalElapsed - } -} - // shallowRef const sessions = shallowRef(new Array()) const activities = shallowRef(new Array()) @@ -330,19 +320,20 @@ const selectedSessions = shallowRef(new Array()) const selectedLaps = shallowRef(new Array()) const selectedFeatures = shallowRef(new Array()) const selectedGraphRecords = shallowRef(new Array()) +const manufacturerList = shallowRef(new ManufacturerListResult()) export default { data() { return { loading: false, - decodeWorker: new Worker(new URL('@/workers/activity-service.ts', import.meta.url), { + activityService: new Worker(new URL('@/workers/activity-service.ts', import.meta.url), { type: 'module' }), - decodeBeginTimestamp: 0, sessionSelected: NONE, summary: new Summary(), hoveredRecord: new Record(), - hoveredRecordFreeze: new Boolean() + hoveredRecordFreeze: new Boolean(), + isActivityServiceReady: false } }, computed: { @@ -450,17 +441,33 @@ export default { Promise.all(promisers) .then((arr) => { - this.decodeBeginTimestamp = new Date().getTime() this.loading = true - this.decodeWorker.postMessage(arr) + this.activityService.postMessage({ type: 'decode', input: arr }) }) .catch((e: string) => { console.log(e) alert(e) }) }, - decodeWorkerOnMessage(e: MessageEvent) { - const result = new Result(e.data) + activityServiceOnMessage(e: MessageEvent) { + const [type, result, elapsed] = [e.data.type, e.data.result, e.data.elapsed] + switch (type) { + case 'isReady': + this.isActivityServiceReady = true + break + case 'decode': + this.decodeHandler(result, elapsed) + break + case 'encode': + this.encodeHandler(result, elapsed) + break + case 'manufacturerList': + this.manufacturerListHandler(result) + break + } + }, + decodeHandler(result: DecodeResult, elapsed: number) { + result = new DecodeResult(result) if (result.err != null) { console.error(`Decode: ${result.err}`) alert(`Decode: ${result.err}`) @@ -469,15 +476,14 @@ export default { } // Instrumenting... - const totalDuration = new Date().getTime() - this.decodeBeginTimestamp console.group('Decoding:') console.group('Spent on WASM:') console.debug('Decode took:\t\t', result.decodeTook, 'ms') console.debug('Serialization took:\t', result.serializationTook, 'ms') console.debug('Total elapsed:\t\t', result.totalElapsed, 'ms') console.groupEnd() - console.debug('Interop WASM to JS:\t', totalDuration - result.totalElapsed, 'ms') - console.debug('Total elapsed:\t\t', totalDuration, 'ms') + console.debug('Interop WASM to JS:\t', elapsed - result.totalElapsed, 'ms') + console.debug('Total elapsed:\t\t', elapsed, 'ms') console.groupEnd() requestAnimationFrame(() => { @@ -485,7 +491,19 @@ export default { this.scrollTop() }) }, - preprocessing(result: Result) { + encodeHandler(result: EncodeResult, elapsed: number) { + result = new EncodeResult(result) + if (result.err != null) { + console.error(`Encode: ${result.err}`) + alert(`Encode: ${result.err}`) + this.loading = false + return + } + }, + manufacturerListHandler(result: ManufacturerListResult) { + manufacturerList.value = new ManufacturerListResult(result) + }, + preprocessing(result: DecodeResult) { console.time('Preprocessing') activities.value = result.activities @@ -756,7 +774,9 @@ export default { }, mounted() { document.getElementById('fileInput')?.addEventListener('change', this.fileInputEventListener) - this.decodeWorker.onmessage = this.decodeWorkerOnMessage + this.activityService.onmessage = this.activityServiceOnMessage + this.activityService.postMessage({ type: 'isReady' }) + this.activityService.postMessage({ type: 'manufacturerList' }) this.selectSession(this.sessionSelected) } } diff --git a/src/spec/activity-service.ts b/src/spec/activity-service.ts new file mode 100644 index 0000000..22cbd1a --- /dev/null +++ b/src/spec/activity-service.ts @@ -0,0 +1,73 @@ +import type { ActivityFile } from './activity' + +export class DecodeResult { + err: string | null = null + activities: Array + decodeTook: number + serializationTook: number + totalElapsed: number + + constructor(json?: any) { + const casted = json as DecodeResult + + this.err = casted?.err + this.activities = casted?.activities + this.decodeTook = casted?.decodeTook + this.serializationTook = casted?.serializationTook + this.totalElapsed = casted?.totalElapsed + } +} + +export class EncodeResult { + err: string | null = null + encodeTook: number + serializationTook: number + totalElapsed: number + fileName: string + fileType: string + fileBytes: Uint8Array + + constructor(data?: any) { + const casted = data as EncodeResult + this.err = casted?.err + this.encodeTook = casted?.encodeTook + this.serializationTook = casted?.serializationTook + this.totalElapsed = casted?.totalElapsed + this.fileName = casted?.fileName + this.fileType = casted?.fileType + this.fileBytes = casted?.fileBytes + } +} + +export class ManufacturerListResult { + manufacturers: Manufacturer[] = [] + + constructor(data?: any) { + const casted = data as ManufacturerListResult + this.manufacturers = casted?.manufacturers + } +} + +export class Manufacturer { + id: number = 0 + name: string = '' + products: Product[] = [] + + constructor(data?: any) { + const casted = data as Manufacturer + this.id = casted?.id + this.name = casted?.name + this.products = casted?.products + } +} + +export class Product { + id: number = 0 + name: string = '' + + constructor(data?: any) { + const casted = data as Product + this.id = casted?.id + this.name = casted?.name + } +} diff --git a/src/wasm/activity-service/activity/fit/creator.go b/src/wasm/activity-service/activity/fit/creator.go index 56cfde2..7675b31 100644 --- a/src/wasm/activity-service/activity/fit/creator.go +++ b/src/wasm/activity-service/activity/fit/creator.go @@ -1,11 +1,7 @@ package fit import ( - "strconv" - "github.com/muktihari/fit/kit/datetime" - "github.com/muktihari/fit/kit/typeconv" - "github.com/muktihari/fit/profile/typedef" "github.com/muktihari/fit/profile/untyped/fieldnum" "github.com/muktihari/fit/proto" "github.com/muktihari/openactivity-fit/activity" @@ -23,14 +19,12 @@ func NewCreator(mesg proto.Message) activity.Creator { continue } m.Manufacturer = &manufacturer - m.Name = activity.FormatTitle(typeconv.ToUint16[typedef.Manufacturer](field.Value).String()) case fieldnum.FileIdProduct: product, ok := field.Value.(uint16) if !ok { continue } m.Product = &product - m.Name += " (" + strconv.FormatUint(uint64(product), 10) + ")" case fieldnum.FileIdTimeCreated: m.TimeCreated = datetime.ToTime(field.Value) } diff --git a/src/wasm/activity-service/activity/fit/manufacturer.go b/src/wasm/activity-service/activity/fit/manufacturer.go new file mode 100644 index 0000000..b8049cf --- /dev/null +++ b/src/wasm/activity-service/activity/fit/manufacturer.go @@ -0,0 +1,31 @@ +package fit + +type Manufacturer struct { + ID uint16 + Name string + Products []ManufacturerProduct +} + +func (m *Manufacturer) ToMap() map[string]any { + products := make([]any, len(m.Products)) + for i := range m.Products { + products[i] = m.Products[i].ToMap() + } + return map[string]any{ + "id": uint16(m.ID), + "name": m.Name, + "products": products, + } +} + +type ManufacturerProduct struct { + ID uint16 + Name string +} + +func (p *ManufacturerProduct) ToMap() map[string]any { + return map[string]any{ + "id": p.ID, + "name": p.Name, + } +} diff --git a/src/wasm/activity-service/activity/fit/service.go b/src/wasm/activity-service/activity/fit/service.go index c0db8e2..7f3d07a 100644 --- a/src/wasm/activity-service/activity/fit/service.go +++ b/src/wasm/activity-service/activity/fit/service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "strconv" "github.com/muktihari/fit/decoder" "github.com/muktihari/openactivity-fit/activity" @@ -13,11 +14,15 @@ import ( var _ activity.Service = &service{} type service struct { - preprocessor *preprocessor.Preprocessor + preprocessor *preprocessor.Preprocessor + manufacturers map[uint16]Manufacturer } -func NewService(preproc *preprocessor.Preprocessor) activity.Service { - return &service{preprocessor: preproc} +func NewService(preproc *preprocessor.Preprocessor, manufacturers map[uint16]Manufacturer) activity.Service { + return &service{ + preprocessor: preproc, + manufacturers: manufacturers, + } } func (s *service) Decode(ctx context.Context, r io.Reader) ([]activity.Activity, error) { @@ -60,6 +65,11 @@ func (s *service) convertListenerResultToActivity(result *ListenerResult) *activ s.sanitize(result) + creator := result.Creator + if creator.Manufacturer != nil && creator.Product != nil { + creator.Name = s.creatorName(*creator.Manufacturer, *creator.Product) + } + act := &activity.Activity{ Creator: *result.Creator, Timezone: result.Timezone, @@ -223,3 +233,25 @@ func (s *service) finalizeSession(ses *activity.Session) { sesFromLaps := activity.NewSessionFromLaps(ses.Laps, ses.Sport) activity.CombineSession(ses, sesFromLaps) } + +func (s *service) creatorName(manufacturerID, productID uint16) string { + manufacturer, ok := s.manufacturers[manufacturerID] + if !ok { + return activity.Unknown + } + + var productName string + for i := range manufacturer.Products { + product := manufacturer.Products[i] + if product.ID == productID { + productName = product.Name + break + } + } + + if productName == "" { + productName = "(" + strconv.FormatUint(uint64(productID), 10) + ")" + } + + return manufacturer.Name + " " + productName +} diff --git a/src/wasm/activity-service/activity/fit/session.go b/src/wasm/activity-service/activity/fit/session.go index 87403f8..724e1aa 100644 --- a/src/wasm/activity-service/activity/fit/session.go +++ b/src/wasm/activity-service/activity/fit/session.go @@ -26,7 +26,7 @@ func NewSession(mesg proto.Message) *activity.Session { if !ok || sport == basetype.EnumInvalid { continue } - ses.Sport = activity.FormatTitle(typedef.Sport(sport).String()) + ses.Sport = kit.FormatTitle(typedef.Sport(sport).String()) case fieldnum.SessionTotalMovingTime: totalMovingTime, ok := field.Value.(uint32) if !ok || totalMovingTime == basetype.Uint32Invalid { @@ -149,7 +149,7 @@ func NewSession(mesg proto.Message) *activity.Session { } } - if ses.Sport == activity.FormatTitle(typedef.SportAll.String()) { + if ses.Sport == kit.FormatTitle(typedef.SportAll.String()) { ses.Sport = activity.SportGeneric } diff --git a/src/wasm/activity-service/activity/gpx/service.go b/src/wasm/activity-service/activity/gpx/service.go index 552e5c3..6e6332c 100644 --- a/src/wasm/activity-service/activity/gpx/service.go +++ b/src/wasm/activity-service/activity/gpx/service.go @@ -8,6 +8,7 @@ import ( "github.com/muktihari/openactivity-fit/activity" "github.com/muktihari/openactivity-fit/activity/gpx/schema" + "github.com/muktihari/openactivity-fit/kit" "github.com/muktihari/openactivity-fit/preprocessor" ) @@ -60,7 +61,7 @@ func (s *service) Decode(ctx context.Context, r io.Reader) ([]activity.Activity, for i := range gpx.Tracks { // Sessions trk := gpx.Tracks[i] - sport := activity.FormatTitle(trk.Type) + sport := kit.FormatTitle(trk.Type) if sport == "" || sport == "Other" { sport = activity.SportGeneric } diff --git a/src/wasm/activity-service/activity/tcx/service.go b/src/wasm/activity-service/activity/tcx/service.go index d797e89..c048987 100644 --- a/src/wasm/activity-service/activity/tcx/service.go +++ b/src/wasm/activity-service/activity/tcx/service.go @@ -68,7 +68,7 @@ func (s *service) Decode(ctx context.Context, r io.Reader) ([]activity.Activity, continue } - sport := activity.FormatTitle(a.Activity.Sport) + sport := kit.FormatTitle(a.Activity.Sport) if sport == "" || sport == "Other" { sport = activity.SportGeneric } diff --git a/src/wasm/activity-service/activity/util.go b/src/wasm/activity-service/activity/util.go index b9ba224..3aae66d 100644 --- a/src/wasm/activity-service/activity/util.go +++ b/src/wasm/activity-service/activity/util.go @@ -1,12 +1,7 @@ package activity import ( - "strings" "time" - "unicode" - - "golang.org/x/text/cases" - "golang.org/x/text/language" ) const ( @@ -48,18 +43,6 @@ func HasPace(sport string) bool { } } -// FormatTitle returns init capital for every word. "snow boarding", "snow_boarding", "SNOW_boardinG" -> "Show Boarding". -func FormatTitle(s string) string { - s = strings.Map(func(r rune) rune { - if !unicode.IsLetter(r) && !unicode.IsDigit(r) { - return ' ' - } - return r - }, s) - s = cases.Title(language.English).String(s) - return s -} - func isBelong(timestamp, startTime, endTime time.Time) bool { if timestamp.Equal(startTime) { return true diff --git a/src/wasm/activity-service/kit/kit.go b/src/wasm/activity-service/kit/kit.go index 0d7c949..78cefc6 100644 --- a/src/wasm/activity-service/kit/kit.go +++ b/src/wasm/activity-service/kit/kit.go @@ -1,6 +1,13 @@ package kit -import "golang.org/x/exp/constraints" +import ( + "strings" + "unicode" + + "golang.org/x/exp/constraints" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) // Ptr returns pointer of v func Ptr[T any](v T) *T { return &v } @@ -23,3 +30,15 @@ func PickNonZeroValuePtr[T constraints.Integer | constraints.Float](x, y *T) *T } return x } + +// FormatTitle returns init capital for every word. "snow boarding", "snow_boarding", "SNOW_boardinG" -> "Show Boarding". +func FormatTitle(s string) string { + s = strings.Map(func(r rune) rune { + if !unicode.IsLetter(r) && !unicode.IsDigit(r) { + return ' ' + } + return r + }, s) + s = cases.Title(language.English).String(s) + return s +} diff --git a/src/wasm/activity-service/main.go b/src/wasm/activity-service/main.go index 39273bc..83e58d7 100644 --- a/src/wasm/activity-service/main.go +++ b/src/wasm/activity-service/main.go @@ -3,37 +3,100 @@ package main import ( "bytes" "context" + _ "embed" + "encoding/json" "fmt" "io" + "strconv" "syscall/js" + "github.com/muktihari/fit/profile/typedef" "github.com/muktihari/openactivity-fit/activity/fit" "github.com/muktihari/openactivity-fit/activity/gpx" "github.com/muktihari/openactivity-fit/activity/tcx" + "github.com/muktihari/openactivity-fit/kit" "github.com/muktihari/openactivity-fit/preprocessor" "github.com/muktihari/openactivity-fit/service" + "github.com/muktihari/openactivity-fit/service/result" + "golang.org/x/exp/slices" ) +//go:embed manufacturers.json +var manufacturerJson []byte + func main() { + manufacturers, err := makeManufacturers() + if err != nil { + fmt.Printf("could not make manufactures mapping: %v\n", err) + } + preproc := preprocessor.New() - fs := fit.NewService(preproc) + fs := fit.NewService(preproc, manufacturers) gs := gpx.NewService(preproc) ts := tcx.NewService(preproc) - s := service.New(fs, gs, ts) + s := service.New(fs, gs, ts, manufacturers) - js.Global().Set("decode", Decode(s)) + js.Global().Set("decode", createDecodeFunc(s)) + js.Global().Set("encode", createEncodeFunc(s)) + js.Global().Set("manufacturerList", createManufacturerListFunc(s)) fmt.Println("WebAssembly: Activity Service Instantiated") select {} // never exit } -func Decode(s service.Service) js.Func { +func makeManufacturers() (manufacturers map[uint16]fit.Manufacturer, err error) { + manufacturers = make(map[uint16]fit.Manufacturer) + + var source map[string]fit.Manufacturer + if err = json.Unmarshal(manufacturerJson, &source); err != nil { + return + } + + manufacturerIDs := typedef.ListManufacturer() + garminProductIDs := typedef.ListGarminProduct() + + for i := range manufacturerIDs { + manufacturerID := manufacturerIDs[i] + manufacturer := fit.Manufacturer{ + ID: uint16(manufacturerID), + Name: kit.FormatTitle(manufacturerID.String()), + } + + if manufacturer.ID == uint16(typedef.ManufacturerGarmin) { + for j := range garminProductIDs { + product := fit.ManufacturerProduct{ + ID: uint16(garminProductIDs[j]), + Name: kit.FormatTitle(garminProductIDs[j].String()), + } + manufacturer.Products = append(manufacturer.Products, product) + } + } + + if m, ok := source[strconv.FormatUint(uint64(manufacturer.ID), 10)]; ok { + manufacturer.Name = m.Name + manufacturer.Products = m.Products + } + + slices.SortFunc(manufacturer.Products, func(a, b fit.ManufacturerProduct) int { + if a.Name < b.Name { + return -1 + } + return 1 + }) + + manufacturers[manufacturer.ID] = manufacturer + } + + return +} + +func createDecodeFunc(s service.Service) js.Func { return js.FuncOf(func(this js.Value, args []js.Value) any { input := args[0] // input is an Array if input.Length() == 0 { - return service.Result{Err: fmt.Errorf("no input is passed")}.ToMap() + return result.Decode{Err: fmt.Errorf("no input is passed")}.ToMap() } rs := make([]io.Reader, input.Length()) @@ -49,3 +112,16 @@ func Decode(s service.Service) js.Func { return result.ToMap() }) } + +func createEncodeFunc(s service.Service) js.Func { + return js.FuncOf(func(this js.Value, args []js.Value) any { + result := s.Encode(context.Background(), nil) + return result.ToMap() + }) +} + +func createManufacturerListFunc(s service.Service) js.Func { + return js.FuncOf(func(this js.Value, args []js.Value) any { + return s.ManufacturerList().ToMap() + }) +} diff --git a/src/wasm/activity-service/manufacturers.json b/src/wasm/activity-service/manufacturers.json new file mode 100644 index 0000000..6c6dc93 --- /dev/null +++ b/src/wasm/activity-service/manufacturers.json @@ -0,0 +1,81 @@ +{ + "23": { + "id": 23, + "name": "Suunto", + "products": [ + { "id": 1, "name": "x9" }, + { "id": 2, "name": "x10" }, + { "id": 3, "name": "x6" }, + { "id": 4, "name": "Memory Belt" }, + { "id": 6, "name": "t6" }, + { "id": 7, "name": "t6c" }, + { "id": 8, "name": "t6d" }, + { "id": 9, "name": "t4" }, + { "id": 10, "name": "t4c" }, + { "id": 11, "name": "t4d" }, + { "id": 12, "name": "t3" }, + { "id": 13, "name": "t3c" }, + { "id": 14, "name": "t3d" }, + { "id": 15, "name": "m4" }, + { "id": 16, "name": "m5" }, + { "id": 17, "name": "Quest" }, + { "id": 18, "name": "Ambit" }, + { "id": 19, "name": "Ambit 2" }, + { "id": 20, "name": "Ambit 2 S" }, + { "id": 21, "name": "Ambit 2 R" }, + { "id": 22, "name": "Ambit 3 Peak" }, + { "id": 23, "name": "Ambit 3 Sport" }, + { "id": 24, "name": "Ambit 3 Run" }, + { "id": 25, "name": "Ambit 3 Vertical" }, + { "id": 26, "name": "Traverse" }, + { "id": 27, "name": "Traverse Alpha " }, + { "id": 28, "name": "Spartan Sport" }, + { "id": 29, "name": "Spartan Ultra" }, + { "id": 30, "name": "Spartan Sport Wrist HR" }, + { "id": 31, "name": "Spartan Trainer Wrist HR" }, + { "id": 32, "name": "Spartan Sport Wrist HR Baro" }, + { "id": 33, "name": "3" }, + { "id": 34, "name": "9 Baro" }, + { "id": 35, "name": "9" }, + { "id": 36, "name": "5" }, + { "id": 37, "name": "EON Core" }, + { "id": 38, "name": "EON Steel" }, + { "id": 39, "name": "D5" }, + { "id": 40, "name": "7" }, + { "id": 41, "name": "EON Steel Black" }, + { "id": 42, "name": "9 Peak" }, + { "id": 43, "name": "Cobra 3" }, + { "id": 44, "name": "D4" }, + { "id": 45, "name": "D4f" }, + { "id": 46, "name": "D4i" }, + { "id": 47, "name": "D6" }, + { "id": 48, "name": "D6i" }, + { "id": 49, "name": "D9" }, + { "id": 50, "name": "D9tx" }, + { "id": 51, "name": "DX" }, + { "id": 52, "name": "Vyper Novo" }, + { "id": 53, "name": "Zoop Novo" }, + { "id": 54, "name": "Zoop Novo Rental" }, + { "id": 55, "name": "GPS Track POD" }, + { "id": 56, "name": "5 Peak" } + ] + }, + "265": { + "id": 265, + "name": "Strava", + "products": [{ "id": 101, "name": "Web" }] + }, + "267": { + "id": 267, + "name": "Bryton", + "products": [{ "id": 1901, "name": "Rider 420" }] + }, + "292": { + "id": 292, + "name": "XOSS", + "products": [ + { "id": 11, "name": "G+" }, + { "id": 100, "name": "G+" } + ] + } +} diff --git a/src/wasm/activity-service/scale/scale.go b/src/wasm/activity-service/scale/scale.go new file mode 100644 index 0000000..12a4f42 --- /dev/null +++ b/src/wasm/activity-service/scale/scale.go @@ -0,0 +1,234 @@ +package scale + +import ( + "time" + + "github.com/muktihari/openactivity-fit/accumulator" + "github.com/muktihari/openactivity-fit/activity" + "github.com/muktihari/openactivity-fit/kit" +) + +type Result struct { + CombinedRecords []*activity.Record + SessionRecords [][]*activity.Record +} + +type Scale struct { + percentage float64 +} + +func New(percentage float64) *Scale { + return &Scale{percentage: percentage} +} + +func (s *Scale) Scale(activities []*activity.Activity) Result { + var combinedRecords []*activity.Record + var sessionRecords [][]*activity.Record + + prevSesDistance := 0.0 + for i := range activities { + act := activities[i] + + for j := range act.Sessions { + ses := act.Sessions[j] + + sessionRecords = append(sessionRecords, s.scaleRecords(ses.Records)) + + for k := range ses.Records { + rec := cloneRecord(ses.Records[k]) + if rec.Distance != nil { + *rec.Distance += prevSesDistance + } + combinedRecords = append(combinedRecords, rec) + } + + prevSesDistance = lastDistance(ses.Records) + } + } + + combinedRecords = s.scaleRecords(combinedRecords) + + return Result{ + CombinedRecords: combinedRecords, + SessionRecords: sessionRecords, + } +} + +func (s *Scale) scaleRecords(records []*activity.Record) []*activity.Record { + summarizeByDistance := hasDistance(records) + var threshold float64 // the unit value is either distance meters or duration seconds depends on conditions below: + if summarizeByDistance { + ld := lastDistance(records) + threshold = ld * s.percentage + } else { + st := startTime(records) + et := endTime(records) + dur := et.Sub(st).Seconds() + threshold = dur * s.percentage + } + + var timestamp time.Time + var positionLat *float64 + var positionLong *float64 + var distance *float64 + + var ( + altitudeAccumu = new(accumulator.Accumulator[float64]) + cadenceAccumu = new(accumulator.Accumulator[uint8]) + heartrateAccumu = new(accumulator.Accumulator[uint8]) + speedAccumu = new(accumulator.Accumulator[float64]) + powerAccumu = new(accumulator.Accumulator[uint16]) + tempAccumu = new(accumulator.Accumulator[int8]) + paceAccumu = new(accumulator.Accumulator[float64]) + gradeAccumu = new(accumulator.Accumulator[float64]) + ) + + summarizedRecords := make([]*activity.Record, 0) + + var curIndex int + for i := range records { + rec := records[i] + cur := records[curIndex] + + var delta float64 + if summarizeByDistance { + if rec.Distance != nil && cur.Distance != nil { + delta = *rec.Distance - *cur.Distance + } + } else { + if !rec.Timestamp.IsZero() && !cur.Timestamp.IsZero() { + delta = rec.Timestamp.Sub(cur.Timestamp).Seconds() + } + } + + if delta > threshold { + newRec := &activity.Record{ + Timestamp: timestamp, + PositionLat: positionLat, + PositionLong: positionLong, + Distance: distance, + Altitude: altitudeAccumu.Avg(), + Cadence: cadenceAccumu.Avg(), + HeartRate: heartrateAccumu.Avg(), + Speed: speedAccumu.Avg(), + Power: powerAccumu.Avg(), + Temperature: tempAccumu.Avg(), + Grade: gradeAccumu.Avg(), + Pace: paceAccumu.Avg(), + } + + summarizedRecords = append(summarizedRecords, newRec) + + // Reset + timestamp = time.Time{} + positionLat = nil + positionLong = nil + distance = nil + altitudeAccumu = new(accumulator.Accumulator[float64]) + cadenceAccumu = new(accumulator.Accumulator[uint8]) + heartrateAccumu = new(accumulator.Accumulator[uint8]) + speedAccumu = new(accumulator.Accumulator[float64]) + powerAccumu = new(accumulator.Accumulator[uint16]) + tempAccumu = new(accumulator.Accumulator[int8]) + paceAccumu = new(accumulator.Accumulator[float64]) + gradeAccumu = new(accumulator.Accumulator[float64]) + + curIndex = i + } + + if timestamp.IsZero() { + timestamp = rec.Timestamp + } + if positionLat == nil { + positionLat = rec.PositionLat + } + if positionLong == nil { + positionLong = rec.PositionLong + } + if distance == nil { + distance = rec.Distance + } + + altitudeAccumu.Collect(rec.Altitude) + cadenceAccumu.Collect(rec.Cadence) + heartrateAccumu.Collect(rec.HeartRate) + speedAccumu.Collect(rec.Speed) + powerAccumu.Collect(rec.Power) + tempAccumu.Collect(rec.Temperature) + paceAccumu.Collect(rec.Pace) + gradeAccumu.Collect(rec.Grade) + } + + return summarizedRecords +} + +func cloneRecord(rec *activity.Record) *activity.Record { + clone := new(activity.Record) + + clone.Timestamp = rec.Timestamp + if rec.Distance != nil { + clone.Distance = kit.Ptr(*rec.Distance) + } + + clone.PositionLat = rec.PositionLat + clone.PositionLong = rec.PositionLong + clone.Altitude = rec.Altitude + clone.HeartRate = rec.HeartRate + clone.Cadence = rec.Cadence + clone.Speed = rec.Speed + clone.Power = rec.Power + clone.Temperature = rec.Temperature + clone.Pace = rec.Pace + clone.Grade = rec.Grade + + return clone +} + +func startTime(records []*activity.Record) time.Time { + var t time.Time + for i := range records { + rec := records[i] + if !t.IsZero() { + break + } + t = rec.Timestamp + } + return t +} + +func endTime(records []*activity.Record) time.Time { + var t time.Time + for i := len(records) - 1; i >= 0; i-- { + rec := records[i] + if !t.IsZero() { + break + } + t = rec.Timestamp + } + return t +} + +func lastDistance(records []*activity.Record) float64 { + var d float64 + for i := len(records) - 1; i >= 0; i-- { + rec := records[i] + if d > 0 { + break + } + if rec.Distance != nil { + d = *rec.Distance + } + } + return d +} + +func hasDistance(records []*activity.Record) bool { + for i := range records { + rec := records[i] + if rec.Distance != nil && *rec.Distance > 0 { + return true + } + } + return false + +} diff --git a/src/wasm/activity-service/service/result.go b/src/wasm/activity-service/service/result.go deleted file mode 100644 index fa0f6d6..0000000 --- a/src/wasm/activity-service/service/result.go +++ /dev/null @@ -1,47 +0,0 @@ -package service - -import ( - "time" - - "github.com/muktihari/openactivity-fit/activity" -) - -type Result struct { - Err error - DecodeTook time.Duration - SerializationTook time.Duration - TotalElapsed time.Duration - Activities []activity.Activity -} - -func (r Result) ToMap() map[string]any { - if r.Err != nil { - return map[string]any{"err": r.Err.Error()} - } - - begin := time.Now() - - activities := make([]any, len(r.Activities)) - for i := range r.Activities { - activities[i] = r.Activities[i].ToMap() - } - - r.SerializationTook = time.Since(begin) - r.TotalElapsed = r.DecodeTook + r.SerializationTook - - m := map[string]any{ - "err": nil, - "activities": activities, - "decodeTook": r.DecodeTook.Milliseconds(), - "serializationTook": r.SerializationTook.Milliseconds(), - "totalElapsed": r.TotalElapsed.Milliseconds(), - } - - return m -} - -type DecodeResult struct { - Err error - Index int - Activity *activity.Activity -} diff --git a/src/wasm/activity-service/service/result/decode.go b/src/wasm/activity-service/service/result/decode.go new file mode 100644 index 0000000..eac413a --- /dev/null +++ b/src/wasm/activity-service/service/result/decode.go @@ -0,0 +1,47 @@ +package result + +import ( + "time" + + "github.com/muktihari/openactivity-fit/activity" +) + +type Decode struct { + Err error + DecodeTook time.Duration + SerializationTook time.Duration + TotalElapsed time.Duration + Activities []activity.Activity +} + +func (d Decode) ToMap() map[string]any { + if d.Err != nil { + return map[string]any{"err": d.Err.Error()} + } + + begin := time.Now() + + activities := make([]any, len(d.Activities)) + for i := range d.Activities { + activities[i] = d.Activities[i].ToMap() + } + + d.SerializationTook = time.Since(begin) + d.TotalElapsed = d.DecodeTook + d.SerializationTook + + m := map[string]any{ + "err": nil, + "activities": activities, + "decodeTook": d.DecodeTook.Milliseconds(), + "serializationTook": d.SerializationTook.Milliseconds(), + "totalElapsed": d.TotalElapsed.Milliseconds(), + } + + return m +} + +type DecodeWorker struct { + Err error + Index int + Activity *activity.Activity +} diff --git a/src/wasm/activity-service/service/result/encode.go b/src/wasm/activity-service/service/result/encode.go new file mode 100644 index 0000000..d990b77 --- /dev/null +++ b/src/wasm/activity-service/service/result/encode.go @@ -0,0 +1,41 @@ +package result + +import "time" + +type Encode struct { + Err error + EncodeTook time.Duration + SerializationTook time.Duration + TotalElapsed time.Duration + FileName string + FileType string + FileBytes []byte +} + +func (e Encode) ToMap() map[string]any { + if e.Err != nil { + return map[string]any{"err": e.Err.Error()} + } + + begin := time.Now() + + fileBytes := make([]any, len(e.FileBytes)) + for i := range e.FileBytes { + fileBytes[i] = e.FileBytes[i] + } + + e.SerializationTook = time.Since(begin) + e.TotalElapsed = e.EncodeTook + e.SerializationTook + + m := map[string]any{ + "err": nil, + "fileName": e.FileName, + "fileType": e.FileType, + "fileBytes": fileBytes, + "decodeTook": e.EncodeTook.Milliseconds(), + "serializationTook": e.SerializationTook.Milliseconds(), + "totalElapsed": e.TotalElapsed.Milliseconds(), + } + + return m +} diff --git a/src/wasm/activity-service/service/result/manufacturer-list.go b/src/wasm/activity-service/service/result/manufacturer-list.go new file mode 100644 index 0000000..8638d56 --- /dev/null +++ b/src/wasm/activity-service/service/result/manufacturer-list.go @@ -0,0 +1,17 @@ +package result + +import "github.com/muktihari/openactivity-fit/activity/fit" + +type ManufacturerList struct { + Manufacturers []fit.Manufacturer +} + +func (m ManufacturerList) ToMap() map[string]any { + manufacturers := make([]any, len(m.Manufacturers)) + for i := range m.Manufacturers { + manufacturers[i] = m.Manufacturers[i].ToMap() + } + return map[string]any{ + "manufacturers": manufacturers, + } +} diff --git a/src/wasm/activity-service/service/service.go b/src/wasm/activity-service/service/service.go index 4544916..9de2f56 100644 --- a/src/wasm/activity-service/service/service.go +++ b/src/wasm/activity-service/service/service.go @@ -9,6 +9,8 @@ import ( "time" "github.com/muktihari/openactivity-fit/activity" + "github.com/muktihari/openactivity-fit/activity/fit" + "github.com/muktihari/openactivity-fit/service/result" "golang.org/x/exp/slices" ) @@ -26,24 +28,28 @@ const ( ) type Service interface { - Decode(ctx context.Context, rs []io.Reader) Result + Decode(ctx context.Context, rs []io.Reader) result.Decode + Encode(ctx context.Context, activities []activity.Activity) result.Encode + ManufacturerList() result.ManufacturerList } type service struct { - fitService activity.Service - gpxService activity.Service - tcxService activity.Service + fitService activity.Service + gpxService activity.Service + tcxService activity.Service + manufacturers map[uint16]fit.Manufacturer } -func New(fitService, gpxService, tcxService activity.Service) Service { +func New(fitService, gpxService, tcxService activity.Service, manufacturers map[uint16]fit.Manufacturer) Service { return &service{ - fitService: fitService, - gpxService: gpxService, - tcxService: tcxService, + fitService: fitService, + gpxService: gpxService, + tcxService: tcxService, + manufacturers: manufacturers, } } -func (s *service) Decode(ctx context.Context, rs []io.Reader) Result { +func (s *service) Decode(ctx context.Context, rs []io.Reader) result.Decode { begin := time.Now() ctx, cancel := context.WithCancel(ctx) @@ -52,11 +58,11 @@ func (s *service) Decode(ctx context.Context, rs []io.Reader) Result { var wg sync.WaitGroup wg.Add(len(rs)) rc := make(chan io.Reader, len(rs)) - resc := make(chan DecodeResult, len(rs)) + resc := make(chan result.DecodeWorker, len(rs)) for i := range rs { i := i - go s.worker(ctx, rc, resc, &wg, i) + go s.decodeWorker(ctx, rc, resc, &wg, i) } for i := range rs { @@ -86,7 +92,7 @@ func (s *service) Decode(ctx context.Context, rs []io.Reader) Result { <-done if err != nil { - return Result{Err: err} + return result.Decode{Err: err} } slices.SortStableFunc(activities, func(a, b activity.Activity) int { @@ -96,23 +102,23 @@ func (s *service) Decode(ctx context.Context, rs []io.Reader) Result { return 1 }) - return Result{ + return result.Decode{ DecodeTook: time.Since(begin), Activities: activities, } } -func (s *service) worker(ctx context.Context, rc <-chan io.Reader, resc chan<- DecodeResult, wg *sync.WaitGroup, index int) { +func (s *service) decodeWorker(ctx context.Context, rc <-chan io.Reader, resc chan<- result.DecodeWorker, wg *sync.WaitGroup, index int) { defer wg.Done() activities, err := s.decode(ctx, <-rc) if err != nil { - resc <- DecodeResult{Err: err, Index: index} + resc <- result.DecodeWorker{Err: err, Index: index} return } for i := range activities { - resc <- DecodeResult{Activity: &activities[i], Index: index} + resc <- result.DecodeWorker{Activity: &activities[i], Index: index} } } @@ -142,3 +148,21 @@ func (s *service) readType(r io.Reader) (FileType, error) { } return FileType(b[0]), nil } + +func (s *service) Encode(ctx context.Context, activities []activity.Activity) result.Encode { + return result.Encode{Err: fmt.Errorf("encode: Not yet implemented")} +} + +func (s *service) ManufacturerList() result.ManufacturerList { + manufacturers := make([]fit.Manufacturer, 0, len(s.manufacturers)) + for _, v := range s.manufacturers { + manufacturers = append(manufacturers, v) + } + slices.SortFunc(manufacturers, func(a, b fit.Manufacturer) int { + if a.Name < b.Name { + return -1 + } + return 1 + }) + return result.ManufacturerList{Manufacturers: manufacturers} +} diff --git a/src/workers/activity-service.ts b/src/workers/activity-service.ts index df94350..8d9d504 100644 --- a/src/workers/activity-service.ts +++ b/src/workers/activity-service.ts @@ -2,7 +2,42 @@ import { activityService } from '@/workers/wasm-services' onmessage = async (e) => { await activityService - // @ts-ignore - const result = decode(e.data) - postMessage(result) + + const begin = new Date() + + switch (e.data.type) { + case 'isReady': + postMessage({ type: e.data.type }) + break + case 'decode': { + // @ts-ignore + const result = decode(e.data.input) + postMessage({ + type: e.data.type, + result: result, + elapsed: new Date().getTime() - begin.getTime() + }) + break + } + case 'encode': { + // @ts-ignore + const result = encode(e.data.input) + postMessage({ + type: e.data.type, + result: result, + elapsed: new Date().getTime() - begin.getTime() + }) + break + } + case 'manufacturerList': { + // @ts-ignore + const manufacturers = manufacturerList(e.data.input) + postMessage({ + type: e.data.type, + result: manufacturers, + elapsed: new Date().getTime() - begin.getTime() + }) + break + } + } }