supervisely 6.73.438__py3-none-any.whl → 6.73.513__py3-none-any.whl
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.
- supervisely/__init__.py +137 -1
- supervisely/_utils.py +81 -0
- supervisely/annotation/annotation.py +8 -2
- supervisely/annotation/json_geometries_map.py +14 -11
- supervisely/annotation/label.py +80 -3
- supervisely/api/annotation_api.py +14 -11
- supervisely/api/api.py +59 -38
- supervisely/api/app_api.py +11 -2
- supervisely/api/dataset_api.py +74 -12
- supervisely/api/entities_collection_api.py +10 -0
- supervisely/api/entity_annotation/figure_api.py +52 -4
- supervisely/api/entity_annotation/object_api.py +3 -3
- supervisely/api/entity_annotation/tag_api.py +63 -12
- supervisely/api/guides_api.py +210 -0
- supervisely/api/image_api.py +72 -1
- supervisely/api/labeling_job_api.py +83 -1
- supervisely/api/labeling_queue_api.py +33 -7
- supervisely/api/module_api.py +9 -0
- supervisely/api/project_api.py +71 -26
- supervisely/api/storage_api.py +3 -1
- supervisely/api/task_api.py +13 -2
- supervisely/api/team_api.py +4 -3
- supervisely/api/video/video_annotation_api.py +119 -3
- supervisely/api/video/video_api.py +65 -14
- supervisely/api/video/video_figure_api.py +24 -11
- supervisely/app/__init__.py +1 -1
- supervisely/app/content.py +23 -7
- supervisely/app/development/development.py +18 -2
- supervisely/app/fastapi/__init__.py +1 -0
- supervisely/app/fastapi/custom_static_files.py +1 -1
- supervisely/app/fastapi/multi_user.py +105 -0
- supervisely/app/fastapi/subapp.py +88 -42
- supervisely/app/fastapi/websocket.py +77 -9
- supervisely/app/singleton.py +21 -0
- supervisely/app/v1/app_service.py +18 -2
- supervisely/app/v1/constants.py +7 -1
- supervisely/app/widgets/__init__.py +6 -0
- supervisely/app/widgets/activity_feed/__init__.py +0 -0
- supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
- supervisely/app/widgets/activity_feed/style.css +78 -0
- supervisely/app/widgets/activity_feed/template.html +22 -0
- supervisely/app/widgets/card/card.py +20 -0
- supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
- supervisely/app/widgets/classes_list_selector/template.html +60 -93
- supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
- supervisely/app/widgets/classes_table/classes_table.py +1 -0
- supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
- supervisely/app/widgets/dialog/dialog.py +12 -0
- supervisely/app/widgets/dialog/template.html +2 -1
- supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
- supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
- supervisely/app/widgets/fast_table/fast_table.py +184 -60
- supervisely/app/widgets/fast_table/template.html +1 -1
- supervisely/app/widgets/heatmap/__init__.py +0 -0
- supervisely/app/widgets/heatmap/heatmap.py +564 -0
- supervisely/app/widgets/heatmap/script.js +533 -0
- supervisely/app/widgets/heatmap/style.css +233 -0
- supervisely/app/widgets/heatmap/template.html +21 -0
- supervisely/app/widgets/modal/__init__.py +0 -0
- supervisely/app/widgets/modal/modal.py +198 -0
- supervisely/app/widgets/modal/template.html +10 -0
- supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
- supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
- supervisely/app/widgets/radio_tabs/template.html +1 -0
- supervisely/app/widgets/select/select.py +6 -3
- supervisely/app/widgets/select_class/__init__.py +0 -0
- supervisely/app/widgets/select_class/select_class.py +363 -0
- supervisely/app/widgets/select_class/template.html +50 -0
- supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
- supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
- supervisely/app/widgets/select_tag/__init__.py +0 -0
- supervisely/app/widgets/select_tag/select_tag.py +352 -0
- supervisely/app/widgets/select_tag/template.html +64 -0
- supervisely/app/widgets/select_team/select_team.py +37 -4
- supervisely/app/widgets/select_team/template.html +4 -5
- supervisely/app/widgets/select_user/__init__.py +0 -0
- supervisely/app/widgets/select_user/select_user.py +270 -0
- supervisely/app/widgets/select_user/template.html +13 -0
- supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
- supervisely/app/widgets/select_workspace/template.html +9 -12
- supervisely/app/widgets/table/table.py +68 -13
- supervisely/app/widgets/tree_select/tree_select.py +2 -0
- supervisely/aug/aug.py +6 -2
- supervisely/convert/base_converter.py +1 -0
- supervisely/convert/converter.py +2 -2
- supervisely/convert/image/csv/csv_converter.py +24 -15
- supervisely/convert/image/image_converter.py +3 -1
- supervisely/convert/image/image_helper.py +48 -4
- supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
- supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
- supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
- supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
- supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
- supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
- supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
- supervisely/convert/pointcloud/las/las_converter.py +13 -1
- supervisely/convert/pointcloud/las/las_helper.py +110 -11
- supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
- supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
- supervisely/convert/video/__init__.py +1 -0
- supervisely/convert/video/multi_view/__init__.py +0 -0
- supervisely/convert/video/multi_view/multi_view.py +543 -0
- supervisely/convert/video/sly/sly_video_converter.py +359 -3
- supervisely/convert/video/video_converter.py +24 -4
- supervisely/convert/volume/dicom/dicom_converter.py +13 -5
- supervisely/convert/volume/dicom/dicom_helper.py +30 -18
- supervisely/geometry/constants.py +1 -0
- supervisely/geometry/geometry.py +4 -0
- supervisely/geometry/helpers.py +5 -1
- supervisely/geometry/oriented_bbox.py +676 -0
- supervisely/geometry/polyline_3d.py +110 -0
- supervisely/geometry/rectangle.py +2 -1
- supervisely/io/env.py +76 -1
- supervisely/io/fs.py +21 -0
- supervisely/nn/benchmark/base_evaluator.py +104 -11
- supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
- supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
- supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
- supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
- supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
- supervisely/nn/inference/cache.py +43 -18
- supervisely/nn/inference/gui/serving_gui_template.py +5 -2
- supervisely/nn/inference/inference.py +916 -222
- supervisely/nn/inference/inference_request.py +55 -10
- supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
- supervisely/nn/inference/predict_app/gui/gui.py +676 -488
- supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
- supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
- supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
- supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
- supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
- supervisely/nn/inference/predict_app/gui/utils.py +236 -119
- supervisely/nn/inference/predict_app/predict_app.py +2 -2
- supervisely/nn/inference/session.py +43 -35
- supervisely/nn/inference/tracking/bbox_tracking.py +118 -35
- supervisely/nn/inference/tracking/point_tracking.py +5 -1
- supervisely/nn/inference/tracking/tracker_interface.py +10 -1
- supervisely/nn/inference/uploader.py +139 -12
- supervisely/nn/live_training/__init__.py +7 -0
- supervisely/nn/live_training/api_server.py +111 -0
- supervisely/nn/live_training/artifacts_utils.py +243 -0
- supervisely/nn/live_training/checkpoint_utils.py +229 -0
- supervisely/nn/live_training/dynamic_sampler.py +44 -0
- supervisely/nn/live_training/helpers.py +14 -0
- supervisely/nn/live_training/incremental_dataset.py +146 -0
- supervisely/nn/live_training/live_training.py +497 -0
- supervisely/nn/live_training/loss_plateau_detector.py +111 -0
- supervisely/nn/live_training/request_queue.py +52 -0
- supervisely/nn/model/model_api.py +9 -0
- supervisely/nn/model/prediction.py +2 -1
- supervisely/nn/model/prediction_session.py +26 -14
- supervisely/nn/prediction_dto.py +19 -1
- supervisely/nn/tracker/base_tracker.py +11 -1
- supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
- supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
- supervisely/nn/tracker/botsort_tracker.py +94 -65
- supervisely/nn/tracker/utils.py +4 -5
- supervisely/nn/tracker/visualize.py +93 -93
- supervisely/nn/training/gui/classes_selector.py +16 -1
- supervisely/nn/training/gui/train_val_splits_selector.py +52 -31
- supervisely/nn/training/train_app.py +46 -31
- supervisely/project/data_version.py +115 -51
- supervisely/project/download.py +1 -1
- supervisely/project/pointcloud_episode_project.py +37 -8
- supervisely/project/pointcloud_project.py +30 -2
- supervisely/project/project.py +14 -2
- supervisely/project/project_meta.py +27 -1
- supervisely/project/project_settings.py +32 -18
- supervisely/project/versioning/__init__.py +1 -0
- supervisely/project/versioning/common.py +20 -0
- supervisely/project/versioning/schema_fields.py +35 -0
- supervisely/project/versioning/video_schema.py +221 -0
- supervisely/project/versioning/volume_schema.py +87 -0
- supervisely/project/video_project.py +717 -15
- supervisely/project/volume_project.py +623 -5
- supervisely/template/experiment/experiment.html.jinja +4 -4
- supervisely/template/experiment/experiment_generator.py +14 -21
- supervisely/template/live_training/__init__.py +0 -0
- supervisely/template/live_training/header.html.jinja +96 -0
- supervisely/template/live_training/live_training.html.jinja +51 -0
- supervisely/template/live_training/live_training_generator.py +464 -0
- supervisely/template/live_training/sly-style.css +402 -0
- supervisely/template/live_training/template.html.jinja +18 -0
- supervisely/versions.json +28 -26
- supervisely/video/sampling.py +39 -20
- supervisely/video/video.py +41 -12
- supervisely/video_annotation/video_figure.py +38 -4
- supervisely/video_annotation/video_object.py +29 -4
- supervisely/volume/stl_converter.py +2 -0
- supervisely/worker_api/agent_rpc.py +24 -1
- supervisely/worker_api/rpc_servicer.py +31 -7
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/METADATA +58 -40
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/RECORD +203 -155
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
- supervisely_lib/__init__.py +6 -1
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
Vue.component('heatmap-image', {
|
|
2
|
+
template: `
|
|
3
|
+
<div
|
|
4
|
+
class="heatmap-container"
|
|
5
|
+
:style="widthStyle"
|
|
6
|
+
@click="$emit('click')"
|
|
7
|
+
>
|
|
8
|
+
<div class="heatmap-header">
|
|
9
|
+
<div class="opacity-slider" @click.stop>
|
|
10
|
+
<div class="opacity-label">
|
|
11
|
+
<span class="opacity-label-text">Opacity:</span>
|
|
12
|
+
<span class="opacity-value">{{opacity}}%</span>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<el-slider
|
|
16
|
+
type="range"
|
|
17
|
+
:min="0"
|
|
18
|
+
:max="100"
|
|
19
|
+
:step="1"
|
|
20
|
+
:value="opacity"
|
|
21
|
+
@input="$emit('update:opacity', $event)"
|
|
22
|
+
class="slider"
|
|
23
|
+
>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="legend" @click.stop>
|
|
27
|
+
<span class="legend-label legend-min">{{ formatValue(minValue) }}</span>
|
|
28
|
+
<div class="legend-gradient" :style="{ background: gradientStyle }"></div>
|
|
29
|
+
<span class="legend-label legend-max">{{ formatValue(maxValue) }}</span>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="image-container" :style="imageContainerStyle">
|
|
33
|
+
<div
|
|
34
|
+
class="image-wrapper"
|
|
35
|
+
ref="wrapper"
|
|
36
|
+
:style="imageWrapperStyle"
|
|
37
|
+
@click.stop="handleImageClick"
|
|
38
|
+
@mouseleave="handleMouseLeave"
|
|
39
|
+
@mousedown="handleMouseDown"
|
|
40
|
+
@mousemove="handleMouseMove"
|
|
41
|
+
@mouseup="handleMouseUp"
|
|
42
|
+
@wheel="handleWheel"
|
|
43
|
+
>
|
|
44
|
+
<img
|
|
45
|
+
class="base-image"
|
|
46
|
+
:src="backgroundUrl"
|
|
47
|
+
@load="handleImageLoad"
|
|
48
|
+
draggable="false"
|
|
49
|
+
>
|
|
50
|
+
<img
|
|
51
|
+
class="overlay-image"
|
|
52
|
+
:style="{ opacity: opacity / 100 }"
|
|
53
|
+
:src="maskUrl"
|
|
54
|
+
draggable="false"
|
|
55
|
+
>
|
|
56
|
+
|
|
57
|
+
<div
|
|
58
|
+
v-if="clickedValue !== null"
|
|
59
|
+
class="click-indicator"
|
|
60
|
+
:class="{ 'hiding': isHiding }"
|
|
61
|
+
:style="indicatorStyle"
|
|
62
|
+
>
|
|
63
|
+
<div class="click-dot"></div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div
|
|
67
|
+
v-if="clickedValue !== null"
|
|
68
|
+
class="value-popup"
|
|
69
|
+
:class="['popup-position-' + popupPosition, { 'hiding': isHiding }]"
|
|
70
|
+
:style="popupStyle"
|
|
71
|
+
>
|
|
72
|
+
<div class="value-popup-content">
|
|
73
|
+
<span class="value-popup-value">{{ formatValue(clickedValue) }}</span>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="value-popup-arrow"></div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
`,
|
|
80
|
+
data() {
|
|
81
|
+
return {
|
|
82
|
+
naturalWidth: null,
|
|
83
|
+
naturalHeight: null,
|
|
84
|
+
clickX: 0,
|
|
85
|
+
clickY: 0,
|
|
86
|
+
popupX: 0,
|
|
87
|
+
popupY: 0,
|
|
88
|
+
popupPosition: 'top', // 'top', 'bottom', 'right', 'left'
|
|
89
|
+
isHiding: false, // Flag for fade-out animation
|
|
90
|
+
zoom: 1.0,
|
|
91
|
+
panX: 0,
|
|
92
|
+
panY: 0,
|
|
93
|
+
isDragging: false,
|
|
94
|
+
hasDragged: false,
|
|
95
|
+
dragStartX: 0,
|
|
96
|
+
dragStartY: 0,
|
|
97
|
+
dragStartPanX: 0,
|
|
98
|
+
dragStartPanY: 0,
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
computed: {
|
|
102
|
+
gradientStyle() {
|
|
103
|
+
return `linear-gradient(to right, ${this.legendColors.join(', ')})`;
|
|
104
|
+
},
|
|
105
|
+
widthStyle() {
|
|
106
|
+
const styles = {};
|
|
107
|
+
|
|
108
|
+
if (this.width) {
|
|
109
|
+
styles.width = typeof this.width === 'number' ? `${this.width}px` : this.width;
|
|
110
|
+
} else if (this.height) {
|
|
111
|
+
// Use naturalWidth/Height from loaded image, or fallback to maskWidth/Height
|
|
112
|
+
const effectiveWidth = this.naturalWidth || this.maskWidth;
|
|
113
|
+
const effectiveHeight = this.naturalHeight || this.maskHeight;
|
|
114
|
+
|
|
115
|
+
if (effectiveWidth && effectiveHeight) {
|
|
116
|
+
const heightValue = typeof this.height === 'number' ? this.height : parseFloat(this.height);
|
|
117
|
+
const aspectRatio = effectiveWidth / effectiveHeight;
|
|
118
|
+
styles.width = `${heightValue * aspectRatio}px`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return styles;
|
|
123
|
+
},
|
|
124
|
+
imageWrapperStyle() {
|
|
125
|
+
return {
|
|
126
|
+
cursor: this.zoom > 1 ? (this.isDragging ? 'grabbing' : 'grab') : 'pointer',
|
|
127
|
+
transform: `scale(${this.zoom}) translate(${this.panX}px, ${this.panY}px)`, // Swapped order
|
|
128
|
+
transformOrigin: 'center center',
|
|
129
|
+
transition: this.isDragging ? 'none' : 'transform 0.1s ease-out',
|
|
130
|
+
width: '100%',
|
|
131
|
+
height: '100%'
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
indicatorStyle() {
|
|
135
|
+
return {
|
|
136
|
+
left: `${this.clickX}px`,
|
|
137
|
+
top: `${this.clickY}px`,
|
|
138
|
+
transform: `translate(-50%, -50%) scale(${1 / this.zoom})`
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
popupStyle() {
|
|
142
|
+
const baseStyle = {
|
|
143
|
+
left: `${this.popupX}px`,
|
|
144
|
+
top: `${this.popupY}px`
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Adjust transform based on position
|
|
148
|
+
const inverseScale = 1 / this.zoom;
|
|
149
|
+
switch (this.popupPosition) {
|
|
150
|
+
case 'top':
|
|
151
|
+
baseStyle.transform = `translate(-50%, -50%) scale(${inverseScale}) translate(0, -100%)`;
|
|
152
|
+
break;
|
|
153
|
+
case 'bottom':
|
|
154
|
+
baseStyle.transform = `translate(-50%, -50%) scale(${inverseScale}) translate(0, 100%)`;
|
|
155
|
+
break;
|
|
156
|
+
case 'right':
|
|
157
|
+
baseStyle.transform = `translate(-50%, -50%) scale(${inverseScale}) translate(calc(50% + 16px), 0)`;
|
|
158
|
+
break;
|
|
159
|
+
case 'left':
|
|
160
|
+
baseStyle.transform = `translate(-50%, -50%) scale(${inverseScale}) translate(calc(-50% - 16px), 0)`;
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return baseStyle;
|
|
165
|
+
},
|
|
166
|
+
imageContainerStyle() {
|
|
167
|
+
const styles = {
|
|
168
|
+
overflow: 'hidden',
|
|
169
|
+
position: 'relative',
|
|
170
|
+
maxWidth: '100%'
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const hasWidth = this.width !== undefined && this.width !== null;
|
|
174
|
+
const hasHeight = this.height !== undefined && this.height !== null;
|
|
175
|
+
|
|
176
|
+
if (hasWidth && hasHeight) {
|
|
177
|
+
styles.width = typeof this.width === 'number' ? `${this.width}px` : this.width;
|
|
178
|
+
styles.height = typeof this.height === 'number' ? `${this.height}px` : this.height;
|
|
179
|
+
return styles;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (hasHeight) {
|
|
183
|
+
styles.maxHeight = typeof this.height === 'number' ? `${this.height}px` : this.height;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const effectiveWidth = this.naturalWidth || this.maskWidth;
|
|
187
|
+
const effectiveHeight = this.naturalHeight || this.maskHeight;
|
|
188
|
+
|
|
189
|
+
if (effectiveWidth && effectiveHeight) {
|
|
190
|
+
styles.aspectRatio = `${effectiveWidth} / ${effectiveHeight}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return styles;
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
methods: {
|
|
197
|
+
handleImageLoad(event) {
|
|
198
|
+
this.naturalWidth = event.target.naturalWidth;
|
|
199
|
+
this.naturalHeight = event.target.naturalHeight;
|
|
200
|
+
},
|
|
201
|
+
handleImageClick(event) {
|
|
202
|
+
if (this.isDragging || this.hasDragged) return;
|
|
203
|
+
|
|
204
|
+
const wrapper = this.$refs.wrapper;
|
|
205
|
+
if (!wrapper) {
|
|
206
|
+
console.warn('[Heatmap] Wrapper not found', { maskUrl: this.maskUrl, backgroundUrl: this.backgroundUrl });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Get image element first to calculate position relative to actual image
|
|
211
|
+
const imgEl = wrapper.querySelector('.overlay-image');
|
|
212
|
+
if (!imgEl) {
|
|
213
|
+
console.warn('[Heatmap] Overlay image element not found', { maskUrl: this.maskUrl, backgroundUrl: this.backgroundUrl });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const container = wrapper.parentElement; // Use parent container which doesn't transform
|
|
218
|
+
|
|
219
|
+
const imgRect = imgEl.getBoundingClientRect();
|
|
220
|
+
|
|
221
|
+
// Get click position relative to actual image (not wrapper!)
|
|
222
|
+
const relativeX = event.clientX - imgRect.left;
|
|
223
|
+
const relativeY = event.clientY - imgRect.top;
|
|
224
|
+
|
|
225
|
+
// Check if click is within image bounds with small tolerance for edge cases
|
|
226
|
+
const tolerance = 1; // 1px tolerance for edge clicks with browser zoom
|
|
227
|
+
if (relativeX < -tolerance || relativeY < -tolerance ||
|
|
228
|
+
relativeX > imgRect.width + tolerance || relativeY > imgRect.height + tolerance) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Clamp coordinates to image bounds (handle edge cases from browser zoom)
|
|
233
|
+
const clampedX = Math.max(0, Math.min(relativeX, imgRect.width - 0.01));
|
|
234
|
+
const clampedY = Math.max(0, Math.min(relativeY, imgRect.height - 0.01));
|
|
235
|
+
|
|
236
|
+
// Set visual indicator position (relative to image, not wrapper)
|
|
237
|
+
this.clickX = clampedX / this.zoom;
|
|
238
|
+
this.clickY = clampedY / this.zoom;
|
|
239
|
+
this.popupX = clampedX / this.zoom;
|
|
240
|
+
this.popupY = clampedY / this.zoom;
|
|
241
|
+
|
|
242
|
+
// Determine best popup position based on click location
|
|
243
|
+
const popupHeight = 40; // Approximate popup height
|
|
244
|
+
const popupWidth = 80; // Approximate popup width (half width for centered popup)
|
|
245
|
+
const margin = 20; // Minimum margin from edges
|
|
246
|
+
|
|
247
|
+
const containerRect = container.getBoundingClientRect();
|
|
248
|
+
const screenClickX = event.clientX - containerRect.left;
|
|
249
|
+
const screenClickY = event.clientY - containerRect.top;
|
|
250
|
+
|
|
251
|
+
// Check available space in each direction (relative to image, not wrapper)
|
|
252
|
+
const spaceTop = screenClickY;
|
|
253
|
+
const spaceBottom = containerRect.height - screenClickY;
|
|
254
|
+
const spaceLeft = screenClickX;
|
|
255
|
+
const spaceRight = containerRect.width - screenClickX;
|
|
256
|
+
|
|
257
|
+
// Check if popup would overflow horizontally when positioned top/bottom
|
|
258
|
+
const wouldOverflowLeft = screenClickX < (popupWidth / 2);
|
|
259
|
+
const wouldOverflowRight = (containerRect.width - screenClickX) < (popupWidth / 2);
|
|
260
|
+
|
|
261
|
+
// Logic: prefer top, but if at edges use left/right
|
|
262
|
+
if (spaceTop > popupHeight + margin && !wouldOverflowLeft && !wouldOverflowRight) {
|
|
263
|
+
// Enough space on top and won't overflow horizontally
|
|
264
|
+
this.popupPosition = 'top';
|
|
265
|
+
}
|
|
266
|
+
else if (wouldOverflowRight && spaceLeft > popupWidth + margin) {
|
|
267
|
+
// Point is at right edge, show popup on left
|
|
268
|
+
this.popupPosition = 'left';
|
|
269
|
+
}
|
|
270
|
+
else if (wouldOverflowLeft && spaceRight > popupWidth + margin) {
|
|
271
|
+
// Point is at left edge, show popup on right
|
|
272
|
+
this.popupPosition = 'right';
|
|
273
|
+
}
|
|
274
|
+
else if (spaceTop > popupHeight + margin) {
|
|
275
|
+
// Use top even if might slightly overflow (better than nothing)
|
|
276
|
+
this.popupPosition = 'top';
|
|
277
|
+
}
|
|
278
|
+
else if (spaceBottom > popupHeight + margin && !wouldOverflowLeft && !wouldOverflowRight) {
|
|
279
|
+
// If no space on top, show popup below (if won't overflow)
|
|
280
|
+
this.popupPosition = 'bottom';
|
|
281
|
+
}
|
|
282
|
+
else if (spaceRight > popupWidth + margin) {
|
|
283
|
+
// Show on right if there's space
|
|
284
|
+
this.popupPosition = 'right';
|
|
285
|
+
}
|
|
286
|
+
else if (spaceLeft > popupWidth + margin) {
|
|
287
|
+
// Show on left if there's space
|
|
288
|
+
this.popupPosition = 'left';
|
|
289
|
+
}
|
|
290
|
+
else if (spaceBottom > popupHeight + margin) {
|
|
291
|
+
// Fallback to bottom even if might overflow
|
|
292
|
+
this.popupPosition = 'bottom';
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
// Final fallback: top
|
|
296
|
+
this.popupPosition = 'top';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Use mask dimensions from server
|
|
300
|
+
const maskWidth = this.maskWidth;
|
|
301
|
+
const maskHeight = this.maskHeight;
|
|
302
|
+
|
|
303
|
+
if (!maskWidth || !maskHeight) {
|
|
304
|
+
console.warn('[Heatmap] Mask dimensions not available', {
|
|
305
|
+
maskUrl: this.maskUrl,
|
|
306
|
+
maskWidth: this.maskWidth,
|
|
307
|
+
maskHeight: this.maskHeight
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Get PNG file dimensions (naturalWidth/Height of the loaded image)
|
|
313
|
+
const pngWidth = imgEl.naturalWidth;
|
|
314
|
+
const pngHeight = imgEl.naturalHeight;
|
|
315
|
+
|
|
316
|
+
if (!pngWidth || !pngHeight) {
|
|
317
|
+
console.warn('[Heatmap] PNG dimensions not available', {
|
|
318
|
+
maskUrl: this.maskUrl,
|
|
319
|
+
naturalWidth: imgEl.naturalWidth,
|
|
320
|
+
naturalHeight: imgEl.naturalHeight
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Two-step coordinate transformation:
|
|
326
|
+
// 1. From screen coordinates to PNG coordinates
|
|
327
|
+
// 2. From PNG coordinates to mask coordinates
|
|
328
|
+
|
|
329
|
+
// Step 1: Scale from displayed size to PNG size
|
|
330
|
+
const displayToImageScaleX = pngWidth / imgRect.width;
|
|
331
|
+
const displayToImageScaleY = pngHeight / imgRect.height;
|
|
332
|
+
|
|
333
|
+
const pngX = clampedX * displayToImageScaleX;
|
|
334
|
+
const pngY = clampedY * displayToImageScaleY;
|
|
335
|
+
|
|
336
|
+
// Step 2: Scale from PNG size to mask size
|
|
337
|
+
const imageTomaskScaleX = maskWidth / pngWidth;
|
|
338
|
+
const imageTomaskScaleY = maskHeight / pngHeight;
|
|
339
|
+
|
|
340
|
+
let maskX = Math.floor(pngX * imageTomaskScaleX);
|
|
341
|
+
let maskY = Math.floor(pngY * imageTomaskScaleY);
|
|
342
|
+
|
|
343
|
+
// Clamp to mask bounds
|
|
344
|
+
maskX = Math.min(Math.max(maskX, 0), maskWidth - 1);
|
|
345
|
+
maskY = Math.min(Math.max(maskY, 0), maskHeight - 1);
|
|
346
|
+
|
|
347
|
+
// Update state - this will trigger server-side callback
|
|
348
|
+
this.$emit('update:mask-x', maskX);
|
|
349
|
+
this.$emit('update:mask-y', maskY);
|
|
350
|
+
|
|
351
|
+
// Reset hiding state for new click
|
|
352
|
+
this.isHiding = false;
|
|
353
|
+
|
|
354
|
+
// Don't set clicked-value here - server will set it after getting value from mask
|
|
355
|
+
this.$emit('update:clicked-value', null);
|
|
356
|
+
|
|
357
|
+
// Call server callback after Vue updates state
|
|
358
|
+
if (this.onImageClick) {
|
|
359
|
+
this.$nextTick(() => {
|
|
360
|
+
this.onImageClick();
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
handleMouseLeave() {
|
|
365
|
+
this.isDragging = false;
|
|
366
|
+
|
|
367
|
+
if (this.clickedValue === null) return;
|
|
368
|
+
|
|
369
|
+
this.isHiding = true;
|
|
370
|
+
|
|
371
|
+
setTimeout(() => {
|
|
372
|
+
this.$emit('update:clicked-value', null);
|
|
373
|
+
this.$emit('update:mask-x', null);
|
|
374
|
+
this.$emit('update:mask-y', null);
|
|
375
|
+
this.isHiding = false;
|
|
376
|
+
}, 300);
|
|
377
|
+
},
|
|
378
|
+
formatValue(value) {
|
|
379
|
+
if (value === null || value === undefined) return 'N/A';
|
|
380
|
+
if (Number.isInteger(value)) {
|
|
381
|
+
return value.toString();
|
|
382
|
+
}
|
|
383
|
+
const abs = Math.abs(value);
|
|
384
|
+
let decimals;
|
|
385
|
+
if (abs >= 1000) decimals = 1;
|
|
386
|
+
else if (abs >= 100) decimals = 2;
|
|
387
|
+
else if (abs >= 1) decimals = 3;
|
|
388
|
+
else if (abs >= 0.01) decimals = 4;
|
|
389
|
+
else decimals = 5;
|
|
390
|
+
|
|
391
|
+
return parseFloat(value.toFixed(decimals)).toString();
|
|
392
|
+
},
|
|
393
|
+
handleMouseDown(event) {
|
|
394
|
+
if (this.zoom > 1) {
|
|
395
|
+
this.isDragging = true;
|
|
396
|
+
this.hasDragged = false; // Reset flag
|
|
397
|
+
this.dragStartX = event.clientX;
|
|
398
|
+
this.dragStartY = event.clientY;
|
|
399
|
+
this.dragStartPanX = this.panX;
|
|
400
|
+
this.dragStartPanY = this.panY;
|
|
401
|
+
event.preventDefault();
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
calculatePanLimits() {
|
|
405
|
+
const wrapper = this.$refs.wrapper;
|
|
406
|
+
if (!wrapper) return null;
|
|
407
|
+
|
|
408
|
+
const container = wrapper.parentElement;
|
|
409
|
+
if (!container) return null;
|
|
410
|
+
|
|
411
|
+
const rect = container.getBoundingClientRect();
|
|
412
|
+
const containerWidth = rect.width;
|
|
413
|
+
const containerHeight = rect.height;
|
|
414
|
+
|
|
415
|
+
const imgRect = wrapper.getBoundingClientRect();
|
|
416
|
+
const imgWidth = imgRect.width
|
|
417
|
+
const imgHeight = imgRect.height
|
|
418
|
+
|
|
419
|
+
maxPanDistance = 0.2
|
|
420
|
+
const maxPanX = (imgWidth - containerWidth) / 2 + containerWidth * maxPanDistance;
|
|
421
|
+
const maxPanY = (imgHeight - containerHeight) / 2 + containerHeight * maxPanDistance;
|
|
422
|
+
|
|
423
|
+
return {
|
|
424
|
+
minX: -maxPanX / this.zoom,
|
|
425
|
+
maxX: maxPanX / this.zoom,
|
|
426
|
+
minY: -maxPanY / this.zoom,
|
|
427
|
+
maxY: maxPanY / this.zoom
|
|
428
|
+
};
|
|
429
|
+
},
|
|
430
|
+
handleMouseMove(event) {
|
|
431
|
+
if (this.isDragging) {
|
|
432
|
+
const deltaX = event.clientX - this.dragStartX;
|
|
433
|
+
const deltaY = event.clientY - this.dragStartY;
|
|
434
|
+
|
|
435
|
+
// Mark as dragged if moved more than a small threshold
|
|
436
|
+
if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) {
|
|
437
|
+
this.hasDragged = true;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
let newPanX = this.dragStartPanX + deltaX / this.zoom;
|
|
441
|
+
let newPanY = this.dragStartPanY + deltaY / this.zoom;
|
|
442
|
+
|
|
443
|
+
const limits = this.calculatePanLimits();
|
|
444
|
+
if (limits) {
|
|
445
|
+
newPanX = Math.max(limits.minX, Math.min(limits.maxX, newPanX));
|
|
446
|
+
newPanY = Math.max(limits.minY, Math.min(limits.maxY, newPanY));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
this.panX = newPanX;
|
|
450
|
+
this.panY = newPanY;
|
|
451
|
+
|
|
452
|
+
event.preventDefault();
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
handleMouseUp() {
|
|
456
|
+
this.isDragging = false;
|
|
457
|
+
},
|
|
458
|
+
handleWheel(event) {
|
|
459
|
+
event.preventDefault();
|
|
460
|
+
|
|
461
|
+
const wrapper = this.$refs.wrapper;
|
|
462
|
+
if (!wrapper) return;
|
|
463
|
+
|
|
464
|
+
// Use the parent container (image-container) which doesn't transform
|
|
465
|
+
const container = wrapper.parentElement;
|
|
466
|
+
if (!container) return;
|
|
467
|
+
|
|
468
|
+
const delta = -event.deltaY;
|
|
469
|
+
const zoomSpeed = 0.001;
|
|
470
|
+
const oldZoom = this.zoom;
|
|
471
|
+
let newZoom = oldZoom + delta * zoomSpeed;
|
|
472
|
+
|
|
473
|
+
// Clamp zoom between 1x and 10x
|
|
474
|
+
newZoom = Math.max(1.0, Math.min(10.0, newZoom));
|
|
475
|
+
|
|
476
|
+
if (newZoom === 1.0) {
|
|
477
|
+
this.zoom = 1.0;
|
|
478
|
+
this.panX = 0;
|
|
479
|
+
this.panY = 0;
|
|
480
|
+
this.hasDragged = false;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (oldZoom === newZoom) return;
|
|
485
|
+
|
|
486
|
+
// Use CONTAINER rect (which doesn't transform)
|
|
487
|
+
const rect = container.getBoundingClientRect();
|
|
488
|
+
|
|
489
|
+
// Mouse position relative to container center
|
|
490
|
+
const mouseX = event.clientX - rect.left;
|
|
491
|
+
const mouseY = event.clientY - rect.top;
|
|
492
|
+
|
|
493
|
+
const centerX = rect.width / 2;
|
|
494
|
+
const centerY = rect.height / 2;
|
|
495
|
+
|
|
496
|
+
const offsetX = mouseX - centerX;
|
|
497
|
+
const offsetY = mouseY - centerY;
|
|
498
|
+
|
|
499
|
+
// Adjust pan based on zoom change
|
|
500
|
+
this.panX = this.panX + offsetX * (1/newZoom - 1/oldZoom);
|
|
501
|
+
this.panY = this.panY + offsetY * (1/newZoom - 1/oldZoom);
|
|
502
|
+
this.zoom = newZoom;
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
watch: {
|
|
506
|
+
backgroundUrl() {
|
|
507
|
+
this.naturalWidth = null;
|
|
508
|
+
this.naturalHeight = null;
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
props: {
|
|
512
|
+
backgroundUrl: String,
|
|
513
|
+
maskUrl: String,
|
|
514
|
+
opacity: Number,
|
|
515
|
+
width: [Number, String],
|
|
516
|
+
height: [Number, String],
|
|
517
|
+
maskWidth: Number,
|
|
518
|
+
maskHeight: Number,
|
|
519
|
+
legendColors: {
|
|
520
|
+
type: Array,
|
|
521
|
+
default: () => ['#0000FF', '#00FF00', '#FFFF00', '#FF0000']
|
|
522
|
+
},
|
|
523
|
+
minValue: Number,
|
|
524
|
+
maxValue: Number,
|
|
525
|
+
clickedValue: Number,
|
|
526
|
+
maskX: Number,
|
|
527
|
+
maskY: Number,
|
|
528
|
+
onImageClick: {
|
|
529
|
+
type: Function,
|
|
530
|
+
default: null
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
});
|