@apollo-annotation/jbrowse-plugin-apollo 0.3.7 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.esm.js +2371 -1642
- package/dist/index.esm.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.cjs.development.js +2384 -1641
- package/dist/jbrowse-plugin-apollo.cjs.development.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.cjs.production.min.js +1 -1
- package/dist/jbrowse-plugin-apollo.cjs.production.min.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.umd.development.js +4387 -2952
- package/dist/jbrowse-plugin-apollo.umd.development.js.map +1 -1
- package/dist/jbrowse-plugin-apollo.umd.production.min.js +1 -1
- package/dist/jbrowse-plugin-apollo.umd.production.min.js.map +1 -1
- package/package.json +15 -15
- package/src/ApolloInternetAccount/model.ts +48 -13
- package/src/BackendDrivers/CollaborationServerDriver.ts +23 -2
- package/src/ChangeManager.ts +33 -13
- package/src/FeatureDetailsWidget/ApolloTranscriptDetailsWidget.tsx +64 -5
- package/src/FeatureDetailsWidget/TranscriptSequence.tsx +70 -73
- package/src/FeatureDetailsWidget/TranscriptWidgetEditLocation.tsx +33 -31
- package/src/LinearApolloDisplay/components/LinearApolloDisplay.tsx +60 -72
- package/src/LinearApolloDisplay/glyphs/BoxGlyph.ts +50 -194
- package/src/LinearApolloDisplay/glyphs/GeneGlyph.ts +441 -180
- package/src/LinearApolloDisplay/glyphs/GenericChildGlyph.ts +53 -34
- package/src/LinearApolloDisplay/glyphs/Glyph.ts +7 -9
- package/src/LinearApolloDisplay/stateModel/base.ts +34 -43
- package/src/LinearApolloDisplay/stateModel/layouts.ts +3 -2
- package/src/LinearApolloDisplay/stateModel/mouseEvents.ts +32 -261
- package/src/LinearApolloDisplay/stateModel/rendering.ts +43 -343
- package/src/LinearApolloReferenceSequenceDisplay/components/LinearApolloReferenceSequenceDisplay.tsx +87 -0
- package/src/LinearApolloReferenceSequenceDisplay/components/index.ts +1 -0
- package/src/LinearApolloReferenceSequenceDisplay/configSchema.ts +7 -0
- package/src/LinearApolloReferenceSequenceDisplay/index.ts +3 -0
- package/src/LinearApolloReferenceSequenceDisplay/stateModel/base.ts +227 -0
- package/src/LinearApolloReferenceSequenceDisplay/stateModel/index.ts +25 -0
- package/src/LinearApolloReferenceSequenceDisplay/stateModel/rendering.ts +481 -0
- package/src/LinearApolloSixFrameDisplay/components/LinearApolloSixFrameDisplay.tsx +95 -38
- package/src/LinearApolloSixFrameDisplay/glyphs/GeneGlyph.ts +221 -201
- package/src/LinearApolloSixFrameDisplay/glyphs/Glyph.ts +12 -8
- package/src/LinearApolloSixFrameDisplay/stateModel/base.ts +42 -4
- package/src/LinearApolloSixFrameDisplay/stateModel/layouts.ts +4 -8
- package/src/LinearApolloSixFrameDisplay/stateModel/mouseEvents.ts +73 -97
- package/src/LinearApolloSixFrameDisplay/stateModel/rendering.ts +49 -61
- package/src/TabularEditor/HybridGrid/Feature.tsx +16 -14
- package/src/TabularEditor/HybridGrid/HybridGrid.tsx +7 -5
- package/src/components/AddAssembly.tsx +1 -1
- package/src/components/AddAssemblyAliases.tsx +1 -1
- package/src/components/AddChildFeature.tsx +5 -2
- package/src/components/AddFeature.tsx +9 -3
- package/src/components/AddRefSeqAliases.tsx +9 -9
- package/src/components/CopyFeature.tsx +3 -1
- package/src/components/CreateApolloAnnotation.tsx +1 -0
- package/src/components/DeleteAssembly.tsx +1 -1
- package/src/components/EditZoomThresholdDialog.tsx +69 -0
- package/src/components/FilterFeatures.tsx +7 -7
- package/src/components/FilterTranscripts.tsx +6 -6
- package/src/components/ImportFeatures.tsx +1 -1
- package/src/components/ManageChecks.tsx +1 -1
- package/src/components/MergeTranscripts.tsx +12 -15
- package/src/components/OntologyTermMultiSelect.tsx +11 -11
- package/src/components/OpenLocalFile.tsx +11 -7
- package/src/components/ViewCheckResults.tsx +1 -1
- package/src/components/index.ts +1 -0
- package/src/config.ts +6 -0
- package/src/index.ts +42 -105
- package/src/makeDisplayComponent.tsx +0 -1
- package/src/menus/index.ts +1 -0
- package/src/{ApolloInternetAccount/addMenuItems.ts → menus/topLevelMenu.ts} +56 -47
- package/src/menus/topLevelMenuAdmin.ts +154 -0
- package/src/session/session.ts +162 -116
- package/src/util/annotationFeatureUtils.ts +15 -21
- package/src/util/displayUtils.ts +149 -0
- package/src/util/glyphUtils.ts +152 -0
- package/src/util/mouseEventsUtils.ts +32 -0
package/src/session/session.ts
CHANGED
|
@@ -26,8 +26,8 @@ import { autorun, observable } from 'mobx'
|
|
|
26
26
|
import {
|
|
27
27
|
type Instance,
|
|
28
28
|
type SnapshotOut,
|
|
29
|
+
addDisposer,
|
|
29
30
|
applySnapshot,
|
|
30
|
-
flow,
|
|
31
31
|
getRoot,
|
|
32
32
|
getSnapshot,
|
|
33
33
|
types,
|
|
@@ -54,12 +54,15 @@ export interface Collaborator {
|
|
|
54
54
|
locations: UserLocation[]
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export interface HoveredFeature {
|
|
58
|
+
feature: AnnotationFeature
|
|
59
|
+
bp: number
|
|
60
|
+
}
|
|
61
|
+
|
|
57
62
|
export function extendSession(
|
|
58
63
|
pluginManager: PluginManager,
|
|
59
64
|
sessionModel: ReturnType<typeof types.model>,
|
|
60
65
|
) {
|
|
61
|
-
const aborter = new AbortController()
|
|
62
|
-
const { signal } = aborter
|
|
63
66
|
const AnnotationFeatureExtended = pluginManager.evaluateExtensionPoint(
|
|
64
67
|
'Apollo-extendAnnotationFeature',
|
|
65
68
|
AnnotationFeatureModel,
|
|
@@ -70,7 +73,12 @@ export function extendSession(
|
|
|
70
73
|
apolloDataStore: types.optional(ClientDataStore, { typeName: 'Client' }),
|
|
71
74
|
apolloSelectedFeature: types.safeReference(AnnotationFeatureExtended),
|
|
72
75
|
jobsManager: types.optional(ApolloJobModel, {}),
|
|
76
|
+
isLocked: types.optional(types.boolean, false),
|
|
73
77
|
})
|
|
78
|
+
.volatile(() => ({
|
|
79
|
+
apolloHoveredFeature: undefined as HoveredFeature | undefined,
|
|
80
|
+
abortController: new AbortController(),
|
|
81
|
+
}))
|
|
74
82
|
.extend(() => {
|
|
75
83
|
const collabs = observable.array<Collaborator>([])
|
|
76
84
|
|
|
@@ -95,10 +103,13 @@ export function extendSession(
|
|
|
95
103
|
}
|
|
96
104
|
})
|
|
97
105
|
.actions((self) => ({
|
|
98
|
-
apolloSetSelectedFeature(feature?: AnnotationFeature) {
|
|
106
|
+
apolloSetSelectedFeature(feature?: AnnotationFeature | string) {
|
|
99
107
|
// @ts-expect-error Not sure why TS thinks these MST types don't match
|
|
100
108
|
self.apolloSelectedFeature = feature
|
|
101
109
|
},
|
|
110
|
+
apolloSetHoveredFeature(feature?: HoveredFeature) {
|
|
111
|
+
self.apolloHoveredFeature = feature
|
|
112
|
+
},
|
|
102
113
|
addApolloTrackConfig(assembly: AssemblyModel, baseURL?: string) {
|
|
103
114
|
const trackId = `apollo_track_${assembly.name}`
|
|
104
115
|
const hasTrack = (self as unknown as AbstractSessionModel).tracks.some(
|
|
@@ -127,6 +138,18 @@ export function extendSession(
|
|
|
127
138
|
})
|
|
128
139
|
}
|
|
129
140
|
},
|
|
141
|
+
toggleLocked() {
|
|
142
|
+
self.isLocked = !self.isLocked
|
|
143
|
+
},
|
|
144
|
+
getPluginConfiguration() {
|
|
145
|
+
const { jbrowse } = getRoot<ApolloRootModel>(self)
|
|
146
|
+
const pluginConfiguration =
|
|
147
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
148
|
+
jbrowse.configuration.ApolloPlugin as Instance<
|
|
149
|
+
typeof ApolloPluginConfigurationSchema
|
|
150
|
+
>
|
|
151
|
+
return pluginConfiguration
|
|
152
|
+
},
|
|
130
153
|
broadcastLocations() {
|
|
131
154
|
const { internetAccounts } = getRoot<ApolloRootModel>(self)
|
|
132
155
|
const locations: {
|
|
@@ -184,132 +207,155 @@ export function extendSession(
|
|
|
184
207
|
}
|
|
185
208
|
},
|
|
186
209
|
}))
|
|
210
|
+
.volatile((self) => ({
|
|
211
|
+
previousSnapshot: getSnapshot(self),
|
|
212
|
+
}))
|
|
187
213
|
.actions((self) => ({
|
|
188
|
-
afterCreate
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
afterCreate() {
|
|
215
|
+
applySnapshot(self, { name: self.name, id: self.id })
|
|
216
|
+
// @ts-expect-error type is missing on ApolloRootModel
|
|
217
|
+
const { internetAccounts, jbrowse, reloadPluginManagerCallback } =
|
|
218
|
+
getRoot<ApolloRootModel>(self)
|
|
219
|
+
addDisposer(
|
|
220
|
+
self,
|
|
221
|
+
autorun(
|
|
222
|
+
() => {
|
|
223
|
+
// broadcastLocations() // **** This is not working and therefore we need to duplicate broadcastLocations() -method code here because autorun() does not observe changes otherwise
|
|
224
|
+
const locations: {
|
|
225
|
+
assemblyName: string
|
|
226
|
+
refName: string
|
|
227
|
+
start: number
|
|
228
|
+
end: number
|
|
229
|
+
}[] = []
|
|
230
|
+
for (const view of (self as unknown as AbstractSessionModel)
|
|
231
|
+
.views) {
|
|
232
|
+
if (view.type !== 'LinearGenomeView') {
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
const lgv = view as unknown as LinearGenomeViewModel
|
|
236
|
+
if (lgv.initialized) {
|
|
237
|
+
const { dynamicBlocks } = lgv
|
|
238
|
+
// eslint-disable-next-line unicorn/no-array-for-each
|
|
239
|
+
dynamicBlocks.forEach((block) => {
|
|
240
|
+
if (block.regionNumber !== undefined) {
|
|
241
|
+
const { assemblyName, end, refName, start } = block
|
|
242
|
+
const assembly =
|
|
243
|
+
self.apolloDataStore.assemblies.get(assemblyName)
|
|
244
|
+
if (
|
|
245
|
+
assembly &&
|
|
246
|
+
assembly.backendDriverType ===
|
|
247
|
+
'CollaborationServerDriver'
|
|
248
|
+
) {
|
|
249
|
+
locations.push({ assemblyName, refName, start, end })
|
|
250
|
+
}
|
|
217
251
|
}
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (locations.length === 0) {
|
|
256
|
+
for (const internetAccount of internetAccounts) {
|
|
257
|
+
if ('baseURL' in internetAccount) {
|
|
258
|
+
internetAccount.postUserLocation([])
|
|
218
259
|
}
|
|
219
|
-
}
|
|
260
|
+
}
|
|
261
|
+
return
|
|
220
262
|
}
|
|
221
|
-
|
|
222
|
-
|
|
263
|
+
|
|
264
|
+
const allLocations: UserLocation[] = []
|
|
223
265
|
for (const internetAccount of internetAccounts) {
|
|
224
266
|
if ('baseURL' in internetAccount) {
|
|
225
|
-
|
|
267
|
+
for (const location of locations) {
|
|
268
|
+
const tmpLoc: UserLocation = {
|
|
269
|
+
assemblyId: location.assemblyName,
|
|
270
|
+
refSeq: location.refName,
|
|
271
|
+
start: location.start,
|
|
272
|
+
end: location.end,
|
|
273
|
+
}
|
|
274
|
+
allLocations.push(tmpLoc)
|
|
275
|
+
}
|
|
276
|
+
internetAccount.postUserLocation(allLocations)
|
|
226
277
|
}
|
|
227
278
|
}
|
|
228
|
-
|
|
229
|
-
}
|
|
279
|
+
},
|
|
280
|
+
{ name: 'ApolloSessionBroadcastLocations' },
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
addDisposer(
|
|
284
|
+
self,
|
|
285
|
+
autorun(
|
|
286
|
+
async (reaction) => {
|
|
287
|
+
// When the initial config.json loads, it doesn't include the Apollo
|
|
288
|
+
// tracks, which would result in a potentially invalid session snapshot
|
|
289
|
+
// if any tracks are open. Here we copy the session snapshot, apply an
|
|
290
|
+
// empty session snapshot, and then restore the original session
|
|
291
|
+
// snapshot after the updated config.json loads.
|
|
292
|
+
const pluginConfiguration =
|
|
293
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
294
|
+
jbrowse.configuration.ApolloPlugin as Instance<
|
|
295
|
+
typeof ApolloPluginConfigurationSchema
|
|
296
|
+
>
|
|
297
|
+
const hasRole = readConfObject(
|
|
298
|
+
pluginConfiguration,
|
|
299
|
+
'hasRole',
|
|
300
|
+
) as boolean
|
|
301
|
+
if (hasRole) {
|
|
302
|
+
// @ts-expect-error not sure why snapshot type is wrong for snapshot
|
|
303
|
+
applySnapshot(self, self.previousSnapshot)
|
|
304
|
+
reaction.dispose()
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const { signal } = self.abortController
|
|
309
|
+
// fetch and initialize assemblies for each of our Apollo internet accounts
|
|
310
|
+
for (const internetAccount of internetAccounts as ApolloInternetAccountModel[]) {
|
|
311
|
+
if (internetAccount.type !== 'ApolloInternetAccount') {
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
230
314
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
315
|
+
const { baseURL } = internetAccount
|
|
316
|
+
const uri = new URL('jbrowse/config.json', baseURL).href
|
|
317
|
+
const fetch = internetAccount.getFetcher({
|
|
318
|
+
locationType: 'UriLocation',
|
|
319
|
+
uri,
|
|
320
|
+
})
|
|
321
|
+
let response: Response
|
|
322
|
+
try {
|
|
323
|
+
response = await fetch(uri, { signal })
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (!self.abortController.signal.aborted) {
|
|
326
|
+
console.error(error)
|
|
240
327
|
}
|
|
241
|
-
|
|
328
|
+
continue
|
|
329
|
+
}
|
|
330
|
+
if (!response.ok) {
|
|
331
|
+
const errorMessage = await createFetchErrorMessage(
|
|
332
|
+
response,
|
|
333
|
+
'Failed to fetch assemblies',
|
|
334
|
+
)
|
|
335
|
+
console.error(errorMessage)
|
|
336
|
+
continue
|
|
242
337
|
}
|
|
243
|
-
|
|
338
|
+
let jbrowseConfig
|
|
339
|
+
try {
|
|
340
|
+
jbrowseConfig = await response.json()
|
|
341
|
+
} catch (error) {
|
|
342
|
+
console.error(error)
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
345
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
346
|
+
reloadPluginManagerCallback(
|
|
347
|
+
jbrowseConfig,
|
|
348
|
+
self.previousSnapshot,
|
|
349
|
+
)
|
|
350
|
+
reaction.dispose()
|
|
244
351
|
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
352
|
+
},
|
|
353
|
+
{ name: 'ApolloSessionLoadConfig' },
|
|
354
|
+
),
|
|
248
355
|
)
|
|
249
|
-
|
|
250
|
-
// tracks, which would result in a potentially invalid session snapshot
|
|
251
|
-
// if any tracks are open. Here we copy the session snapshot, apply an
|
|
252
|
-
// empty session snapshot, and then restore the original session
|
|
253
|
-
// snapshot after the updated config.json loads.
|
|
254
|
-
// @ts-expect-error type is missing on ApolloRootModel
|
|
255
|
-
const { internetAccounts, jbrowse, reloadPluginManagerCallback } =
|
|
256
|
-
getRoot<ApolloRootModel>(self)
|
|
257
|
-
const pluginConfiguration =
|
|
258
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
259
|
-
jbrowse.configuration.ApolloPlugin as Instance<
|
|
260
|
-
typeof ApolloPluginConfigurationSchema
|
|
261
|
-
>
|
|
262
|
-
const hasRole = readConfObject(
|
|
263
|
-
pluginConfiguration,
|
|
264
|
-
'hasRole',
|
|
265
|
-
) as boolean
|
|
266
|
-
if (hasRole) {
|
|
267
|
-
return
|
|
268
|
-
}
|
|
269
|
-
const sessionSnapshot = getSnapshot(self)
|
|
270
|
-
const { id, name } = sessionSnapshot
|
|
271
|
-
applySnapshot(self, { name, id })
|
|
272
|
-
|
|
273
|
-
// fetch and initialize assemblies for each of our Apollo internet accounts
|
|
274
|
-
for (const internetAccount of internetAccounts as ApolloInternetAccountModel[]) {
|
|
275
|
-
if (internetAccount.type !== 'ApolloInternetAccount') {
|
|
276
|
-
continue
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const { baseURL } = internetAccount
|
|
280
|
-
const uri = new URL('jbrowse/config.json', baseURL).href
|
|
281
|
-
const fetch = internetAccount.getFetcher({
|
|
282
|
-
locationType: 'UriLocation',
|
|
283
|
-
uri,
|
|
284
|
-
})
|
|
285
|
-
let response: Response
|
|
286
|
-
try {
|
|
287
|
-
response = yield fetch(uri, { signal })
|
|
288
|
-
} catch (error) {
|
|
289
|
-
console.error(error)
|
|
290
|
-
continue
|
|
291
|
-
}
|
|
292
|
-
if (!response.ok) {
|
|
293
|
-
const errorMessage = yield createFetchErrorMessage(
|
|
294
|
-
response,
|
|
295
|
-
'Failed to fetch assemblies',
|
|
296
|
-
)
|
|
297
|
-
console.error(errorMessage)
|
|
298
|
-
continue
|
|
299
|
-
}
|
|
300
|
-
let jbrowseConfig
|
|
301
|
-
try {
|
|
302
|
-
jbrowseConfig = yield response.json()
|
|
303
|
-
} catch (error) {
|
|
304
|
-
console.error(error)
|
|
305
|
-
continue
|
|
306
|
-
}
|
|
307
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
308
|
-
reloadPluginManagerCallback(jbrowseConfig, sessionSnapshot)
|
|
309
|
-
}
|
|
310
|
-
}),
|
|
356
|
+
},
|
|
311
357
|
beforeDestroy() {
|
|
312
|
-
|
|
358
|
+
self.abortController.abort('destroying session model')
|
|
313
359
|
},
|
|
314
360
|
}))
|
|
315
361
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { type AnnotationFeature } from '@apollo-annotation/mst'
|
|
2
2
|
|
|
3
|
-
import { type MousePosition } from '../LinearApolloDisplay/stateModel/mouseEvents'
|
|
4
|
-
|
|
5
3
|
export function getFeatureName(feature: AnnotationFeature) {
|
|
6
4
|
const { attributes } = feature
|
|
7
5
|
const name = attributes.get('gff_name')
|
|
@@ -75,44 +73,40 @@ function getParents(feature: AnnotationFeature): AnnotationFeature[] {
|
|
|
75
73
|
return parents
|
|
76
74
|
}
|
|
77
75
|
|
|
78
|
-
export function
|
|
79
|
-
|
|
76
|
+
export function getRelatedFeatures(
|
|
77
|
+
feature: AnnotationFeature,
|
|
78
|
+
bp: number,
|
|
80
79
|
includeSiblings = false,
|
|
81
80
|
): AnnotationFeature[] {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
clickedFeatures.push(mousePosition.featureAndGlyphUnderMouse.feature)
|
|
87
|
-
for (const x of getParents(mousePosition.featureAndGlyphUnderMouse.feature)) {
|
|
88
|
-
clickedFeatures.push(x)
|
|
81
|
+
const relatedFeatures: AnnotationFeature[] = []
|
|
82
|
+
relatedFeatures.push(feature)
|
|
83
|
+
for (const x of getParents(feature)) {
|
|
84
|
+
relatedFeatures.push(x)
|
|
89
85
|
}
|
|
90
|
-
const
|
|
91
|
-
const children = getChildren(mousePosition.featureAndGlyphUnderMouse.feature)
|
|
86
|
+
const children = getChildren(feature)
|
|
92
87
|
for (const child of children) {
|
|
93
88
|
if (child.min < bp && child.max >= bp) {
|
|
94
|
-
|
|
89
|
+
relatedFeatures.push(child)
|
|
95
90
|
}
|
|
96
91
|
}
|
|
97
92
|
if (!includeSiblings) {
|
|
98
|
-
return
|
|
93
|
+
return relatedFeatures
|
|
99
94
|
}
|
|
100
95
|
|
|
101
96
|
// Also add siblings , i.e. features having the same parent as the clicked
|
|
102
97
|
// one and intersecting the click position
|
|
103
|
-
if (
|
|
104
|
-
const siblings =
|
|
105
|
-
mousePosition.featureAndGlyphUnderMouse.feature.parent.children
|
|
98
|
+
if (feature.parent) {
|
|
99
|
+
const siblings = feature.parent.children
|
|
106
100
|
if (siblings) {
|
|
107
101
|
for (const [, sib] of siblings) {
|
|
108
|
-
if (sib._id ==
|
|
102
|
+
if (sib._id == feature._id) {
|
|
109
103
|
continue
|
|
110
104
|
}
|
|
111
105
|
if (sib.min < bp && sib.max >= bp) {
|
|
112
|
-
|
|
106
|
+
relatedFeatures.push(sib)
|
|
113
107
|
}
|
|
114
108
|
}
|
|
115
109
|
}
|
|
116
110
|
}
|
|
117
|
-
return
|
|
111
|
+
return relatedFeatures
|
|
118
112
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { type CheckResultIdsType } from '@apollo-annotation/mst'
|
|
2
|
+
import { makeStyles } from 'tss-react/mui'
|
|
3
|
+
|
|
4
|
+
export { default as EditZoomThresholdDialog } from '../components/EditZoomThresholdDialog'
|
|
5
|
+
|
|
6
|
+
export type Coord = [number, number]
|
|
7
|
+
|
|
8
|
+
export const useStyles = makeStyles()((theme) => ({
|
|
9
|
+
canvasContainer: {
|
|
10
|
+
position: 'relative',
|
|
11
|
+
left: 0,
|
|
12
|
+
},
|
|
13
|
+
canvas: {
|
|
14
|
+
position: 'absolute',
|
|
15
|
+
left: 0,
|
|
16
|
+
},
|
|
17
|
+
center: {
|
|
18
|
+
display: 'flex',
|
|
19
|
+
justifyContent: 'center',
|
|
20
|
+
},
|
|
21
|
+
ellipses: {
|
|
22
|
+
textOverflow: 'ellipsis',
|
|
23
|
+
overflow: 'hidden',
|
|
24
|
+
},
|
|
25
|
+
avatar: {
|
|
26
|
+
position: 'static',
|
|
27
|
+
height: '100%',
|
|
28
|
+
width: '100%',
|
|
29
|
+
overflow: 'visible',
|
|
30
|
+
color: theme.palette.warning.light,
|
|
31
|
+
backgroundColor: theme.palette.warning.contrastText,
|
|
32
|
+
},
|
|
33
|
+
box: {
|
|
34
|
+
position: 'absolute',
|
|
35
|
+
overflow: 'visible',
|
|
36
|
+
},
|
|
37
|
+
badge: {
|
|
38
|
+
display: 'inline-block',
|
|
39
|
+
},
|
|
40
|
+
loading: {
|
|
41
|
+
position: 'absolute',
|
|
42
|
+
right: theme.spacing(3),
|
|
43
|
+
zIndex: 10,
|
|
44
|
+
pointerEvents: 'none',
|
|
45
|
+
textAlign: 'right',
|
|
46
|
+
},
|
|
47
|
+
locked: {
|
|
48
|
+
position: 'absolute',
|
|
49
|
+
right: theme.spacing(3),
|
|
50
|
+
top: theme.spacing(6),
|
|
51
|
+
zIndex: 1,
|
|
52
|
+
pointerEvents: 'none',
|
|
53
|
+
textAlign: 'right',
|
|
54
|
+
},
|
|
55
|
+
}))
|
|
56
|
+
|
|
57
|
+
export interface CheckResultCluster<T> {
|
|
58
|
+
_id: string
|
|
59
|
+
message: string
|
|
60
|
+
start: number
|
|
61
|
+
count: number
|
|
62
|
+
members: T[]
|
|
63
|
+
range: { min: number; max: number }
|
|
64
|
+
featureIds: CheckResultIdsType
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function clusterResultByMessage<
|
|
68
|
+
T extends {
|
|
69
|
+
_id: string
|
|
70
|
+
start: number
|
|
71
|
+
end: number
|
|
72
|
+
message: string
|
|
73
|
+
ids: CheckResultIdsType
|
|
74
|
+
},
|
|
75
|
+
>(
|
|
76
|
+
items: readonly T[],
|
|
77
|
+
width: number,
|
|
78
|
+
touchesAsOverlap: boolean,
|
|
79
|
+
): CheckResultCluster<T>[] {
|
|
80
|
+
const byMsg = new Map<string, T[]>()
|
|
81
|
+
for (const it of items) {
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
83
|
+
;(byMsg.get(it.message) ?? byMsg.set(it.message, []).get(it.message)!).push(
|
|
84
|
+
it,
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const clusters: CheckResultCluster<T>[] = []
|
|
89
|
+
const overlaps = (aEnd: number, bStart: number) =>
|
|
90
|
+
touchesAsOverlap ? bStart <= aEnd : bStart < aEnd
|
|
91
|
+
|
|
92
|
+
for (const [message, arr] of byMsg.entries()) {
|
|
93
|
+
if (arr.length === 0) {
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
arr.sort((a, b) => a.start - b.start)
|
|
98
|
+
|
|
99
|
+
let group: T[] = [arr[0]]
|
|
100
|
+
let curMin = arr[0].start
|
|
101
|
+
let curMax = arr[0].start + width
|
|
102
|
+
|
|
103
|
+
const pushResult = () => {
|
|
104
|
+
const starts = group.map((d) => d.start).sort((a, b) => a - b)
|
|
105
|
+
const mid = Math.floor(starts.length / 2)
|
|
106
|
+
const median: number =
|
|
107
|
+
starts.length % 2 ? starts[mid] : (starts[mid - 1] + starts[mid]) / 2
|
|
108
|
+
const clusterId = group[0]._id
|
|
109
|
+
const featureIds = group[0].ids
|
|
110
|
+
|
|
111
|
+
clusters.push({
|
|
112
|
+
_id: clusterId,
|
|
113
|
+
message,
|
|
114
|
+
start: median,
|
|
115
|
+
count: group.length,
|
|
116
|
+
members: [...group],
|
|
117
|
+
range: { min: curMin, max: curMax },
|
|
118
|
+
featureIds,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (let i = 1; i < arr.length; i++) {
|
|
123
|
+
const it = arr[i]
|
|
124
|
+
const itStart = it.start
|
|
125
|
+
const itEnd = itStart + width
|
|
126
|
+
|
|
127
|
+
if (overlaps(curMax, itStart)) {
|
|
128
|
+
group.push(it)
|
|
129
|
+
if (itStart < curMin) {
|
|
130
|
+
curMin = itStart
|
|
131
|
+
}
|
|
132
|
+
if (itEnd > curMax) {
|
|
133
|
+
curMax = itEnd
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
pushResult()
|
|
137
|
+
group = [it]
|
|
138
|
+
curMin = itStart
|
|
139
|
+
curMax = itEnd
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
pushResult()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
clusters.sort(
|
|
146
|
+
(a, b) => a.message.localeCompare(b.message) || a.start - b.start,
|
|
147
|
+
)
|
|
148
|
+
return clusters
|
|
149
|
+
}
|