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
+ }
+ }
}