输运模拟修改热力图
This commit is contained in:
parent
3aff577410
commit
cbfdbbc672
42
src/gis/ol/flexpartContour/flexpartContour.worker.ts
Normal file
42
src/gis/ol/flexpartContour/flexpartContour.worker.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { contours as createContours } from 'd3';
|
||||||
|
import type {
|
||||||
|
ContourWorkerRequest,
|
||||||
|
ContourWorkerResponse,
|
||||||
|
SerializedContour,
|
||||||
|
} from './flexpartContourTypes';
|
||||||
|
|
||||||
|
// Keep the worker independent from the DOM and OpenLayers.
|
||||||
|
const workerScope = self as unknown as {
|
||||||
|
onmessage: ((event: MessageEvent<ContourWorkerRequest>) => void) | null;
|
||||||
|
postMessage: (message: ContourWorkerResponse) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
workerScope.onmessage = (event) => {
|
||||||
|
const request = event.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const generated = createContours()
|
||||||
|
.size([request.width, request.height])
|
||||||
|
.thresholds(request.thresholds)
|
||||||
|
.smooth(true)(request.values as unknown as number[]);
|
||||||
|
|
||||||
|
const response: ContourWorkerResponse = {
|
||||||
|
id: request.id,
|
||||||
|
contours: generated.map((item) => {
|
||||||
|
const contour = item as unknown as SerializedContour;
|
||||||
|
return {
|
||||||
|
value: contour.value,
|
||||||
|
coordinates: contour.coordinates,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
workerScope.postMessage(response);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
workerScope.postMessage({
|
||||||
|
id: request.id,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
119
src/gis/ol/flexpartContour/flexpartContourData.ts
Normal file
119
src/gis/ol/flexpartContour/flexpartContourData.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
import type {
|
||||||
|
FlexpartColorScale,
|
||||||
|
FlexpartGridAxes,
|
||||||
|
FlexpartGridFrame,
|
||||||
|
FlexpartGridPoint,
|
||||||
|
} from './flexpartContourTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expands one sparse backend response into the dense row-major grid required
|
||||||
|
* by Marching Squares. Grid positions absent from data.points remain zero.
|
||||||
|
*
|
||||||
|
* colorScale is passed separately so one scale can be reused across frames.
|
||||||
|
*/
|
||||||
|
export function createGridFrameFromSparsePoints(
|
||||||
|
points: readonly FlexpartGridPoint[],
|
||||||
|
axes: FlexpartGridAxes,
|
||||||
|
colorScale: FlexpartColorScale,
|
||||||
|
validTime?: string,
|
||||||
|
): FlexpartGridFrame {
|
||||||
|
if (!Array.isArray(points)) {
|
||||||
|
throw new Error('Frame point data must be an array.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!axes) {
|
||||||
|
throw new Error('Complete longitude and latitude axes are required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [longitudes, longitudeIndexes] = prepareAxis(
|
||||||
|
axes.longitudes,
|
||||||
|
'longitudes',
|
||||||
|
);
|
||||||
|
const [latitudes, latitudeIndexes] = prepareAxis(
|
||||||
|
axes.latitudes,
|
||||||
|
'latitudes',
|
||||||
|
);
|
||||||
|
const values = new Float64Array(
|
||||||
|
longitudes.length * latitudes.length,
|
||||||
|
);
|
||||||
|
const occupied = new Uint8Array(values.length);
|
||||||
|
|
||||||
|
points.forEach((point, pointIndex) => {
|
||||||
|
if (!point) {
|
||||||
|
throw new Error(`Point ${pointIndex} must be an object.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isFinite(point.lon)
|
||||||
|
|| !Number.isFinite(point.lat)
|
||||||
|
|| !Number.isFinite(point.value)) {
|
||||||
|
throw new Error(
|
||||||
|
`Point ${pointIndex} must contain finite lon, lat and value numbers.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const longitudeIndex = longitudeIndexes.get(point.lon);
|
||||||
|
const latitudeIndex = latitudeIndexes.get(point.lat);
|
||||||
|
|
||||||
|
if (longitudeIndex === undefined || latitudeIndex === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`Point lon=${point.lon}, lat=${point.lat} is not present in the axes.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gridIndex = latitudeIndex * longitudes.length + longitudeIndex;
|
||||||
|
if (occupied[gridIndex] === 1) {
|
||||||
|
throw new Error(
|
||||||
|
`Duplicate point found at lon=${point.lon}, lat=${point.lat}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
occupied[gridIndex] = 1;
|
||||||
|
// Missing positions and accidentally returned nonpositive values stay zero.
|
||||||
|
if (point.value > 0) {
|
||||||
|
values[gridIndex] = point.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
longitudes,
|
||||||
|
latitudes,
|
||||||
|
values,
|
||||||
|
colorMin: colorScale.colorMin,
|
||||||
|
colorMax: colorScale.colorMax,
|
||||||
|
levels: colorScale.levels,
|
||||||
|
validTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareAxis(
|
||||||
|
axis: readonly number[],
|
||||||
|
name: string,
|
||||||
|
): [number[], Map<number, number>] {
|
||||||
|
if (!Array.isArray(axis) || axis.length < 2) {
|
||||||
|
throw new Error(`${name} must contain at least two coordinates.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const coordinates = Array.from(axis);
|
||||||
|
const indexes = new Map<number, number>();
|
||||||
|
let direction = 0;
|
||||||
|
|
||||||
|
coordinates.forEach((coordinate, index) => {
|
||||||
|
if (!Number.isFinite(coordinate)) {
|
||||||
|
throw new Error(`${name}[${index}] must be a finite number.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index > 0) {
|
||||||
|
const difference = coordinate - coordinates[index - 1];
|
||||||
|
const currentDirection = Math.sign(difference);
|
||||||
|
if (currentDirection === 0
|
||||||
|
|| (direction !== 0 && currentDirection !== direction)) {
|
||||||
|
throw new Error(`${name} must be strictly monotonic.`);
|
||||||
|
}
|
||||||
|
direction = currentDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
indexes.set(coordinate, index);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [coordinates, indexes];
|
||||||
|
}
|
||||||
56
src/gis/ol/flexpartContour/flexpartContourExample.ts
Normal file
56
src/gis/ol/flexpartContour/flexpartContourExample.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import {
|
||||||
|
addFlexpartContourLayer,
|
||||||
|
} from './flexpartContourLayer';
|
||||||
|
import { createGridFrameFromSparsePoints } from './flexpartContourData';
|
||||||
|
import type {
|
||||||
|
FlexpartColorScale,
|
||||||
|
FlexpartContourLayerOptions,
|
||||||
|
FlexpartContourLayerResult,
|
||||||
|
FlexpartGridAxes,
|
||||||
|
FlexpartGridPoint,
|
||||||
|
} from './flexpartContourTypes';
|
||||||
|
|
||||||
|
/** Render already-loaded sparse data directly without sending a request. */
|
||||||
|
export async function renderBackwardData(
|
||||||
|
points: readonly FlexpartGridPoint[],
|
||||||
|
axes: FlexpartGridAxes,
|
||||||
|
colorScale: FlexpartColorScale,
|
||||||
|
options: FlexpartContourLayerOptions = {},
|
||||||
|
): Promise<FlexpartContourLayerResult> {
|
||||||
|
const frame = createGridFrameFromSparsePoints(
|
||||||
|
points,
|
||||||
|
axes,
|
||||||
|
colorScale,
|
||||||
|
);
|
||||||
|
|
||||||
|
return addFlexpartContourLayer(frame, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The frame API only returns nonzero points. Complete axes and the color
|
||||||
|
* scale are obtained separately and reused across frame requests.
|
||||||
|
*/
|
||||||
|
export async function renderBackwardFrame(
|
||||||
|
time: string,
|
||||||
|
axes: FlexpartGridAxes,
|
||||||
|
colorScale: FlexpartColorScale,
|
||||||
|
): Promise<void> {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/flexpart/backward/frame?time=${encodeURIComponent(time)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`FLEXPART frame request failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const points = await response.json() as FlexpartGridPoint[];
|
||||||
|
await renderBackwardData(
|
||||||
|
points,
|
||||||
|
axes,
|
||||||
|
colorScale,
|
||||||
|
{
|
||||||
|
opacity: 0.78,
|
||||||
|
layerName: 'flexpart-contour-layer',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
612
src/gis/ol/flexpartContour/flexpartContourLayer.ts
Normal file
612
src/gis/ol/flexpartContour/flexpartContourLayer.ts
Normal file
|
|
@ -0,0 +1,612 @@
|
||||||
|
import type OlMap from 'ol/Map';
|
||||||
|
import type { Extent } from 'ol/extent';
|
||||||
|
import { getWidth } from 'ol/extent';
|
||||||
|
import ImageLayer from 'ol/layer/Image';
|
||||||
|
import ImageCanvas from 'ol/source/ImageCanvas';
|
||||||
|
import { transform, transformExtent } from 'ol/proj';
|
||||||
|
import { obtainViewer2D } from '@/gis/ol/map';
|
||||||
|
import type {
|
||||||
|
ContourWorkerRequest,
|
||||||
|
ContourWorkerResponse,
|
||||||
|
FlexpartContourLayerOptions,
|
||||||
|
FlexpartContourLayerResult,
|
||||||
|
FlexpartGridFrame,
|
||||||
|
SerializedContour,
|
||||||
|
} from './flexpartContourTypes';
|
||||||
|
import { PYTHON_CONTOUR_COLORS } from './flexpartContourTypes';
|
||||||
|
|
||||||
|
const WEB_MERCATOR_MAX_LATITUDE = 85.0511287798066;
|
||||||
|
|
||||||
|
interface ProjectedContour {
|
||||||
|
color: string;
|
||||||
|
path: Path2D;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PendingRequest {
|
||||||
|
resolve: (value: SerializedContour[]) => void;
|
||||||
|
reject: (reason?: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PreparedContourGrid {
|
||||||
|
longitudes: readonly number[];
|
||||||
|
values: Float64Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLogLevels(
|
||||||
|
colorMin: number,
|
||||||
|
colorMax: number,
|
||||||
|
bandCount = 10,
|
||||||
|
): number[] {
|
||||||
|
if (!Number.isFinite(colorMin)
|
||||||
|
|| !Number.isFinite(colorMax)
|
||||||
|
|| colorMin <= 0
|
||||||
|
|| colorMax <= colorMin
|
||||||
|
|| bandCount < 1) {
|
||||||
|
throw new Error('Invalid logarithmic color range.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const minExponent = Math.log10(colorMin);
|
||||||
|
const maxExponent = Math.log10(colorMax);
|
||||||
|
|
||||||
|
return Array.from({ length: bandCount + 1 }, (_, index) => (
|
||||||
|
10 ** (
|
||||||
|
minExponent
|
||||||
|
+ (maxExponent - minExponent) * index / bandCount
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FlexpartContourRenderer {
|
||||||
|
private readonly map: OlMap;
|
||||||
|
|
||||||
|
private readonly source: ImageCanvas;
|
||||||
|
|
||||||
|
private readonly layer: ImageLayer<ImageCanvas>;
|
||||||
|
|
||||||
|
private readonly worker: Worker;
|
||||||
|
|
||||||
|
private readonly pending = new Map<number, PendingRequest>();
|
||||||
|
|
||||||
|
private requestId = 0;
|
||||||
|
|
||||||
|
private frameVersion = 0;
|
||||||
|
|
||||||
|
private projectedContours: ProjectedContour[] = [];
|
||||||
|
|
||||||
|
private layerExtent?: Extent;
|
||||||
|
|
||||||
|
private readonly colors: readonly string[];
|
||||||
|
|
||||||
|
private readonly wrapX: boolean;
|
||||||
|
|
||||||
|
private readonly worldWidth?: number;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
map: OlMap,
|
||||||
|
options: FlexpartContourLayerOptions = {},
|
||||||
|
) {
|
||||||
|
this.map = map;
|
||||||
|
this.colors = options.colors ?? PYTHON_CONTOUR_COLORS;
|
||||||
|
this.wrapX = options.wrapX ?? true;
|
||||||
|
|
||||||
|
const projection = map.getView().getProjection();
|
||||||
|
const projectionCode = projection.getCode();
|
||||||
|
const projectionExtent = projection.getExtent();
|
||||||
|
this.worldWidth = this.wrapX
|
||||||
|
&& projection.canWrapX()
|
||||||
|
&& projectionExtent
|
||||||
|
? getWidth(projectionExtent)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
this.source = new ImageCanvas({
|
||||||
|
projection: projectionCode,
|
||||||
|
ratio: 1,
|
||||||
|
canvasFunction: (
|
||||||
|
extent,
|
||||||
|
_resolution,
|
||||||
|
_pixelRatio,
|
||||||
|
size,
|
||||||
|
) => this.drawCanvas(extent, size),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.layer = new ImageLayer({
|
||||||
|
source: this.source,
|
||||||
|
opacity: options.opacity ?? 0.78,
|
||||||
|
zIndex: options.zIndex ?? 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.layerName) {
|
||||||
|
this.layer.set('name', options.layerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.worker = new Worker(
|
||||||
|
new URL('./flexpartContour.worker.ts', import.meta.url),
|
||||||
|
{ type: 'module' },
|
||||||
|
);
|
||||||
|
|
||||||
|
this.worker.onmessage = (
|
||||||
|
event: MessageEvent<ContourWorkerResponse>,
|
||||||
|
) => {
|
||||||
|
const request = this.pending.get(event.data.id);
|
||||||
|
if (!request) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pending.delete(event.data.id);
|
||||||
|
|
||||||
|
if (event.data.error) {
|
||||||
|
request.reject(new Error(event.data.error));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
request.resolve(event.data.contours ?? []);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.worker.onerror = (event) => {
|
||||||
|
const error = new Error(
|
||||||
|
event.message || 'Contour worker failed.',
|
||||||
|
);
|
||||||
|
this.pending.forEach((request) => request.reject(error));
|
||||||
|
this.pending.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
map.addLayer(this.layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setFrame(
|
||||||
|
frame: FlexpartGridFrame,
|
||||||
|
): Promise<FlexpartContourLayerResult> {
|
||||||
|
this.validateFrame(frame);
|
||||||
|
|
||||||
|
const version = ++this.frameVersion;
|
||||||
|
const levels = this.resolveLevels(frame);
|
||||||
|
const normalizedValues = this.prepareValues(frame);
|
||||||
|
const contourGrid = this.prepareCyclicLongitudeGrid(
|
||||||
|
frame.longitudes,
|
||||||
|
frame.latitudes.length,
|
||||||
|
normalizedValues,
|
||||||
|
);
|
||||||
|
const rawContours = await this.calculateContours(
|
||||||
|
contourGrid.longitudes.length,
|
||||||
|
frame.latitudes.length,
|
||||||
|
contourGrid.values,
|
||||||
|
levels.slice(0, -1),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (version !== this.frameVersion) {
|
||||||
|
throw new Error('A newer FLEXPART frame superseded this frame.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const projection = this.map.getView().getProjection();
|
||||||
|
this.projectedContours = rawContours.map((contour, index) => ({
|
||||||
|
color: this.colors[index],
|
||||||
|
path: this.createProjectedPath(
|
||||||
|
contour,
|
||||||
|
contourGrid.longitudes,
|
||||||
|
frame.latitudes,
|
||||||
|
projection.getCode(),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.layerExtent = this.createLayerExtent(
|
||||||
|
frame.longitudes,
|
||||||
|
frame.latitudes,
|
||||||
|
projection.getCode(),
|
||||||
|
);
|
||||||
|
// A wrapped global layer must not be clipped to the canonical world.
|
||||||
|
this.layer.setExtent(this.worldWidth ? undefined : this.layerExtent);
|
||||||
|
this.layer.setVisible(true);
|
||||||
|
this.source.changed();
|
||||||
|
this.map.render();
|
||||||
|
|
||||||
|
return {
|
||||||
|
layerExtent: this.layerExtent,
|
||||||
|
levels,
|
||||||
|
validTime: frame.validTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getLayer(): ImageLayer<ImageCanvas> {
|
||||||
|
return this.layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisible(visible: boolean): void {
|
||||||
|
this.layer.setVisible(visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpacity(opacity: number): void {
|
||||||
|
this.layer.setOpacity(Math.max(0, Math.min(1, opacity)));
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.projectedContours = [];
|
||||||
|
this.source.changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.worker.terminate();
|
||||||
|
this.map.removeLayer(this.layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateFrame(frame: FlexpartGridFrame): void {
|
||||||
|
const width = frame.longitudes.length;
|
||||||
|
const height = frame.latitudes.length;
|
||||||
|
|
||||||
|
if (width < 2 || height < 2) {
|
||||||
|
throw new Error('Longitude and latitude need at least two points.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNestedValues(frame.values)) {
|
||||||
|
if (frame.values.length !== height
|
||||||
|
|| frame.values.some((row) => row.length !== width)) {
|
||||||
|
throw new Error(
|
||||||
|
`values must be a ${height}x${width} two-dimensional array.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (frame.values.length !== width * height) {
|
||||||
|
throw new Error(
|
||||||
|
`values length must be ${width * height}, got ${frame.values.length}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.validateAxis(frame.longitudes, 'longitudes');
|
||||||
|
this.validateAxis(frame.latitudes, 'latitudes');
|
||||||
|
|
||||||
|
if (!Number.isFinite(frame.colorMin)
|
||||||
|
|| !Number.isFinite(frame.colorMax)
|
||||||
|
|| frame.colorMin <= 0
|
||||||
|
|| frame.colorMax <= frame.colorMin) {
|
||||||
|
throw new Error('Invalid colorMin/colorMax.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateAxis(axis: readonly number[], name: string): void {
|
||||||
|
let direction = 0;
|
||||||
|
|
||||||
|
for (let index = 0; index < axis.length; index++) {
|
||||||
|
if (!Number.isFinite(axis[index])) {
|
||||||
|
throw new Error(`${name} contains a non-finite coordinate.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const difference = axis[index] - axis[index - 1];
|
||||||
|
const currentDirection = Math.sign(difference);
|
||||||
|
|
||||||
|
if (currentDirection === 0
|
||||||
|
|| (direction !== 0 && currentDirection !== direction)) {
|
||||||
|
throw new Error(`${name} must be strictly monotonic.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
direction = currentDirection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveLevels(frame: FlexpartGridFrame): number[] {
|
||||||
|
const levels = frame.levels
|
||||||
|
? Array.from(frame.levels)
|
||||||
|
: createLogLevels(frame.colorMin, frame.colorMax);
|
||||||
|
|
||||||
|
if (levels.length !== this.colors.length + 1) {
|
||||||
|
throw new Error(
|
||||||
|
`levels must contain ${this.colors.length + 1} values.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < levels.length; index++) {
|
||||||
|
if (!Number.isFinite(levels[index])
|
||||||
|
|| levels[index] <= 0
|
||||||
|
|| (index > 0 && levels[index] <= levels[index - 1])) {
|
||||||
|
throw new Error('levels must be positive and strictly increasing.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return levels;
|
||||||
|
}
|
||||||
|
|
||||||
|
private prepareValues(frame: FlexpartGridFrame): Float64Array {
|
||||||
|
const width = frame.longitudes.length;
|
||||||
|
const height = frame.latitudes.length;
|
||||||
|
const values = new Float64Array(width * height);
|
||||||
|
|
||||||
|
for (let row = 0; row < height; row++) {
|
||||||
|
for (let column = 0; column < width; column++) {
|
||||||
|
const sourceValue = isNestedValues(frame.values)
|
||||||
|
? frame.values[row][column]
|
||||||
|
: frame.values[row * width + column];
|
||||||
|
const value = Number(sourceValue);
|
||||||
|
|
||||||
|
// Positive values below colorMin still influence contour interpolation.
|
||||||
|
// The first threshold keeps those cells transparent without log10 data.
|
||||||
|
values[row * width + column] = Number.isFinite(value) && value > 0
|
||||||
|
? value
|
||||||
|
: Number.NaN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private prepareCyclicLongitudeGrid(
|
||||||
|
longitudes: readonly number[],
|
||||||
|
height: number,
|
||||||
|
values: Float64Array,
|
||||||
|
): PreparedContourGrid {
|
||||||
|
if (!this.wrapX || !isGlobalCyclicAxis(longitudes)) {
|
||||||
|
return { longitudes, values };
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceWidth = longitudes.length;
|
||||||
|
const targetWidth = sourceWidth + 1;
|
||||||
|
const direction = Math.sign(
|
||||||
|
longitudes[sourceWidth - 1] - longitudes[0],
|
||||||
|
);
|
||||||
|
const cyclicLongitudes = [
|
||||||
|
...longitudes,
|
||||||
|
longitudes[0] + direction * 360,
|
||||||
|
];
|
||||||
|
const cyclicValues = new Float64Array(targetWidth * height);
|
||||||
|
|
||||||
|
for (let row = 0; row < height; row++) {
|
||||||
|
const sourceOffset = row * sourceWidth;
|
||||||
|
const targetOffset = row * targetWidth;
|
||||||
|
cyclicValues.set(
|
||||||
|
values.subarray(sourceOffset, sourceOffset + sourceWidth),
|
||||||
|
targetOffset,
|
||||||
|
);
|
||||||
|
cyclicValues[targetOffset + sourceWidth] = values[sourceOffset];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
longitudes: cyclicLongitudes,
|
||||||
|
values: cyclicValues,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateContours(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
values: Float64Array,
|
||||||
|
thresholds: number[],
|
||||||
|
): Promise<SerializedContour[]> {
|
||||||
|
const id = ++this.requestId;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.pending.set(id, { resolve, reject });
|
||||||
|
|
||||||
|
const request: ContourWorkerRequest = {
|
||||||
|
id,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
values,
|
||||||
|
thresholds,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.worker.postMessage(request, [
|
||||||
|
values.buffer,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private createProjectedPath(
|
||||||
|
contour: SerializedContour,
|
||||||
|
longitudes: readonly number[],
|
||||||
|
latitudes: readonly number[],
|
||||||
|
projectionCode: string,
|
||||||
|
): Path2D {
|
||||||
|
const path = new Path2D();
|
||||||
|
|
||||||
|
contour.coordinates.forEach((polygon) => {
|
||||||
|
polygon.forEach((ring) => {
|
||||||
|
ring.forEach(([gridX, gridY], pointIndex) => {
|
||||||
|
const lon = interpolateGridCoordinate(longitudes, gridX);
|
||||||
|
const lat = interpolateGridCoordinate(latitudes, gridY);
|
||||||
|
const projected = transform(
|
||||||
|
[lon, clampLatitude(lat)],
|
||||||
|
'EPSG:4326',
|
||||||
|
projectionCode,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pointIndex === 0) {
|
||||||
|
path.moveTo(projected[0], projected[1]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
path.lineTo(projected[0], projected[1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
path.closePath();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createLayerExtent(
|
||||||
|
longitudes: readonly number[],
|
||||||
|
latitudes: readonly number[],
|
||||||
|
projectionCode: string,
|
||||||
|
): Extent {
|
||||||
|
const lonMin = interpolateGridCoordinate(longitudes, 0);
|
||||||
|
const lonMax = interpolateGridCoordinate(longitudes, longitudes.length);
|
||||||
|
const latMin = interpolateGridCoordinate(latitudes, 0);
|
||||||
|
const latMax = interpolateGridCoordinate(latitudes, latitudes.length);
|
||||||
|
|
||||||
|
return transformExtent(
|
||||||
|
[
|
||||||
|
Math.min(lonMin, lonMax),
|
||||||
|
clampLatitude(Math.min(latMin, latMax)),
|
||||||
|
Math.max(lonMin, lonMax),
|
||||||
|
clampLatitude(Math.max(latMin, latMax)),
|
||||||
|
],
|
||||||
|
'EPSG:4326',
|
||||||
|
projectionCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private drawCanvas(
|
||||||
|
extent: Extent,
|
||||||
|
size: readonly number[],
|
||||||
|
): HTMLCanvasElement {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = Math.max(1, Math.round(size[0]));
|
||||||
|
canvas.height = Math.max(1, Math.round(size[1]));
|
||||||
|
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
if (!context || extent[2] <= extent[0] || extent[3] <= extent[1]) {
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const scaleX = canvas.width / (extent[2] - extent[0]);
|
||||||
|
const scaleY = canvas.height / (extent[3] - extent[1]);
|
||||||
|
|
||||||
|
context.setTransform(
|
||||||
|
scaleX,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
-scaleY,
|
||||||
|
-extent[0] * scaleX,
|
||||||
|
extent[3] * scaleY,
|
||||||
|
);
|
||||||
|
|
||||||
|
const worldOffsets = this.worldWidth
|
||||||
|
? [-this.worldWidth, 0, this.worldWidth]
|
||||||
|
: [0];
|
||||||
|
|
||||||
|
// Lower thresholds are drawn first; higher thresholds cover them.
|
||||||
|
this.projectedContours.forEach((contour) => {
|
||||||
|
context.fillStyle = contour.color;
|
||||||
|
worldOffsets.forEach((offset) => {
|
||||||
|
context.save();
|
||||||
|
context.translate(offset, 0);
|
||||||
|
context.fill(contour.path, 'evenodd');
|
||||||
|
context.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultRenderer: FlexpartContourRenderer | undefined;
|
||||||
|
|
||||||
|
/** Direct adapter for the existing olContainer map. */
|
||||||
|
export async function addFlexpartContourLayer(
|
||||||
|
frame: FlexpartGridFrame,
|
||||||
|
options: FlexpartContourLayerOptions = {},
|
||||||
|
): Promise<FlexpartContourLayerResult> {
|
||||||
|
const map = obtainViewer2D('olContainer') as OlMap | undefined;
|
||||||
|
|
||||||
|
if (!map) {
|
||||||
|
throw new Error('OpenLayers map olContainer is not initialized.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defaultRenderer) {
|
||||||
|
defaultRenderer = new FlexpartContourRenderer(map, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultRenderer.setFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearFlexpartContourLayer(): void {
|
||||||
|
defaultRenderer?.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function destroyFlexpartContourLayer(): void {
|
||||||
|
defaultRenderer?.destroy();
|
||||||
|
defaultRenderer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Move the existing olContainer view to a Pacific-centered longitude. */
|
||||||
|
// export function centerFlexpartMap(
|
||||||
|
// longitude = 180,
|
||||||
|
// latitude = 0,
|
||||||
|
// duration = 0,
|
||||||
|
// ): void {
|
||||||
|
// const map = obtainViewer2D('olContainer') as OlMap | undefined;
|
||||||
|
|
||||||
|
// if (!map) {
|
||||||
|
// throw new Error('OpenLayers map olContainer is not initialized.');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!Number.isFinite(longitude)
|
||||||
|
// || !Number.isFinite(latitude)
|
||||||
|
// || !Number.isFinite(duration)
|
||||||
|
// || duration < 0) {
|
||||||
|
// throw new Error('Invalid map center or animation duration.');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const center = transform(
|
||||||
|
// [longitude, clampLatitude(latitude)],
|
||||||
|
// 'EPSG:4326',
|
||||||
|
// map.getView().getProjection(),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if (duration > 0) {
|
||||||
|
// map.getView().animate({ center, duration });
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
// map.getView().setCenter(center);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
function interpolateGridCoordinate(
|
||||||
|
coordinates: readonly number[],
|
||||||
|
gridPosition: number,
|
||||||
|
): number {
|
||||||
|
const centerPosition = gridPosition - 0.5;
|
||||||
|
const lastIndex = coordinates.length - 1;
|
||||||
|
|
||||||
|
if (centerPosition <= 0) {
|
||||||
|
return coordinates[0]
|
||||||
|
+ centerPosition * (coordinates[1] - coordinates[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (centerPosition >= lastIndex) {
|
||||||
|
return coordinates[lastIndex]
|
||||||
|
+ (centerPosition - lastIndex)
|
||||||
|
* (coordinates[lastIndex] - coordinates[lastIndex - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerIndex = Math.floor(centerPosition);
|
||||||
|
const ratio = centerPosition - lowerIndex;
|
||||||
|
|
||||||
|
return coordinates[lowerIndex]
|
||||||
|
+ (coordinates[lowerIndex + 1] - coordinates[lowerIndex]) * ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampLatitude(latitude: number): number {
|
||||||
|
return Math.max(
|
||||||
|
-WEB_MERCATOR_MAX_LATITUDE,
|
||||||
|
Math.min(WEB_MERCATOR_MAX_LATITUDE, latitude),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNestedValues(
|
||||||
|
values: FlexpartGridFrame['values'],
|
||||||
|
): values is readonly (readonly number[])[] {
|
||||||
|
return Array.isArray(values)
|
||||||
|
&& values.length > 0
|
||||||
|
&& Array.isArray(values[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGlobalCyclicAxis(longitudes: readonly number[]): boolean {
|
||||||
|
if (longitudes.length < 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = Math.abs(
|
||||||
|
longitudes[longitudes.length - 1] - longitudes[0],
|
||||||
|
);
|
||||||
|
const averageStep = span / (longitudes.length - 1);
|
||||||
|
const coverage = span + averageStep;
|
||||||
|
const tolerance = Math.max(1e-6, averageStep * 0.1);
|
||||||
|
|
||||||
|
return Math.abs(coverage - 360) <= tolerance;
|
||||||
|
}
|
||||||
92
src/gis/ol/flexpartContour/flexpartContourTypes.ts
Normal file
92
src/gis/ol/flexpartContour/flexpartContourTypes.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import type { Extent } from 'ol/extent';
|
||||||
|
|
||||||
|
/** One nonzero grid item returned by the frame-data API. */
|
||||||
|
export interface FlexpartGridPoint {
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete coordinate axes obtained separately and reused across frames.
|
||||||
|
*/
|
||||||
|
export interface FlexpartGridAxes {
|
||||||
|
longitudes: readonly number[];
|
||||||
|
latitudes: readonly number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color scale calculated separately from frame data and reused across frames.
|
||||||
|
* All values use the same physical unit as FlexpartGridPoint.value.
|
||||||
|
*/
|
||||||
|
export interface FlexpartColorScale {
|
||||||
|
colorMin: number;
|
||||||
|
colorMax: number;
|
||||||
|
/** Optional. When absent, 10 logarithmic bands are generated. */
|
||||||
|
levels?: readonly number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FLEXPART frame contract.
|
||||||
|
* Values may be a two-dimensional array [latitude][longitude] or a flattened
|
||||||
|
* row-major array: values[latIndex * lonCount + lonIndex].
|
||||||
|
* Values must already be converted to the displayed physical unit.
|
||||||
|
* Do not send log10(values); the renderer uses logarithmic thresholds.
|
||||||
|
*/
|
||||||
|
export interface FlexpartGridFrame {
|
||||||
|
longitudes: readonly number[];
|
||||||
|
latitudes: readonly number[];
|
||||||
|
values: ArrayLike<number> | readonly (readonly number[])[];
|
||||||
|
colorMin: number;
|
||||||
|
colorMax: number;
|
||||||
|
/** Optional. When absent, 10 logarithmic bands are generated. */
|
||||||
|
levels?: readonly number[];
|
||||||
|
validTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlexpartContourLayerOptions {
|
||||||
|
opacity?: number;
|
||||||
|
zIndex?: number;
|
||||||
|
colors?: readonly string[];
|
||||||
|
layerName?: string;
|
||||||
|
/** Draw global contours in adjacent wrapped worlds. Defaults to true. */
|
||||||
|
wrapX?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SerializedContour {
|
||||||
|
value: number;
|
||||||
|
coordinates: number[][][][];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContourWorkerRequest {
|
||||||
|
id: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
values: Float64Array;
|
||||||
|
thresholds: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContourWorkerResponse {
|
||||||
|
id: number;
|
||||||
|
contours?: SerializedContour[];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlexpartContourLayerResult {
|
||||||
|
layerExtent: Extent;
|
||||||
|
levels: number[];
|
||||||
|
validTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PYTHON_CONTOUR_COLORS = [
|
||||||
|
'bisque',
|
||||||
|
'tan',
|
||||||
|
'lightblue',
|
||||||
|
'dodgerblue',
|
||||||
|
'lightgreen',
|
||||||
|
'green',
|
||||||
|
'yellow',
|
||||||
|
'orange',
|
||||||
|
'red',
|
||||||
|
'darkred',
|
||||||
|
] as const;
|
||||||
|
|
@ -17,6 +17,8 @@ import { windDataHandle } from './tool/handle/windDataHandle';
|
||||||
import { addHeadMap, removeHeadMap } from './tool/heatMap';
|
import { addHeadMap, removeHeadMap } from './tool/heatMap';
|
||||||
import { addWeathLayer, removeWeathLayer, setWeathLayerVisity } from './tool/weath';
|
import { addWeathLayer, removeWeathLayer, setWeathLayerVisity } from './tool/weath';
|
||||||
import { addWindLayer, removeWindLayer, setWindLayerVisity } from './tool/wind';
|
import { addWindLayer, removeWindLayer, setWindLayerVisity } from './tool/wind';
|
||||||
|
import { clearFlexpartContourLayer } from './flexpartContour/flexpartContourLayer';
|
||||||
|
|
||||||
|
|
||||||
// 当前模块信息
|
// 当前模块信息
|
||||||
let activeMoudleInf: any = {
|
let activeMoudleInf: any = {
|
||||||
|
|
@ -128,6 +130,10 @@ const palyDateEvent: any = {
|
||||||
setShouldAnimate(false);
|
setShouldAnimate(false);
|
||||||
// 清理自定义热力图
|
// 清理自定义热力图
|
||||||
clearGridFeatures();
|
clearGridFeatures();
|
||||||
|
|
||||||
|
|
||||||
|
// 清空画面,图层保留
|
||||||
|
clearFlexpartContourLayer();
|
||||||
// 清理热力图
|
// 清理热力图
|
||||||
clearHeatmapFeatures();
|
clearHeatmapFeatures();
|
||||||
// 移除标记点
|
// 移除标记点
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,9 @@ export function initMap2D(id: string, options: MapOptions = {}): void {
|
||||||
});
|
});
|
||||||
|
|
||||||
// 创建视图实例
|
// 创建视图实例
|
||||||
const view = new View(cenView);
|
const view = new View({
|
||||||
|
...cenView,
|
||||||
|
});
|
||||||
|
|
||||||
// 创建地图实例
|
// 创建地图实例
|
||||||
const map = new Map({
|
const map = new Map({
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
function initPlaybackWorker() {
|
function initPlaybackWorker() {
|
||||||
const queryApi = {
|
const queryApi = {
|
||||||
symn: {
|
symn: {
|
||||||
url: '/transportResult/getDiffusionData',
|
url: "/transportResult/getDiffusionData",
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
},
|
},
|
||||||
hsjmnNDFB: {
|
hsjmnNDFB: {
|
||||||
url: '/diffusionData/getDiffusionResult',
|
url: "/diffusionData/getDiffusionResult",
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
},
|
},
|
||||||
hsjmnJLFB: {
|
hsjmnJLFB: {
|
||||||
url: '/doseData/getDoseResult',
|
url: "/doseData/getDoseResult",
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let serverIp = '';
|
let serverIp = "";
|
||||||
let token = '';
|
let token = "";
|
||||||
const baseURL = '/api';
|
const baseURL = "/api";
|
||||||
let symnNum = 0; // 当前请求次数(从 0 开始)
|
let symnNum = 0; // 当前请求次数(从 0 开始)
|
||||||
let symmMaxNum = 30; // 总循环次数
|
let symmMaxNum = 30; // 总循环次数
|
||||||
const symnResult = new Map(); // 存储所有请求结果
|
const symnResult = new Map(); // 存储所有请求结果
|
||||||
|
|
@ -34,6 +34,9 @@ function initPlaybackWorker() {
|
||||||
let lastTargetTs = null; // 关键:记录上一次匹配的时间戳(初始为 null)
|
let lastTargetTs = null; // 关键:记录上一次匹配的时间戳(初始为 null)
|
||||||
let lonWidth = null;
|
let lonWidth = null;
|
||||||
let latWidth = null;
|
let latWidth = null;
|
||||||
|
let latGroupData = null;
|
||||||
|
let lonGroupData = null;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 二分查找:找到当前时间对应的目标时间戳
|
* 二分查找:找到当前时间对应的目标时间戳
|
||||||
|
|
@ -41,17 +44,15 @@ function initPlaybackWorker() {
|
||||||
* @returns 匹配的时间戳(无匹配返回 null)
|
* @returns 匹配的时间戳(无匹配返回 null)
|
||||||
*/
|
*/
|
||||||
function findTargetTimestamp(currentTime) {
|
function findTargetTimestamp(currentTime) {
|
||||||
if (!timeList || timeList.length === 0)
|
if (!timeList || timeList.length === 0) return null;
|
||||||
return null;
|
|
||||||
|
|
||||||
const len = timeList.length;
|
const len = timeList.length;
|
||||||
if (currentTime <= timeList[0])
|
if (currentTime <= timeList[0]) return timeList[0];
|
||||||
return timeList[0];
|
if (currentTime >= timeList[len - 1]) return timeList[len - 1];
|
||||||
if (currentTime >= timeList[len - 1])
|
|
||||||
return timeList[len - 1];
|
|
||||||
|
|
||||||
// 二分查找核心
|
// 二分查找核心
|
||||||
let left = 0; let right = len - 1;
|
let left = 0;
|
||||||
|
let right = len - 1;
|
||||||
while (left <= right) {
|
while (left <= right) {
|
||||||
const mid = Math.floor((left + right) / 2);
|
const mid = Math.floor((left + right) / 2);
|
||||||
const midTs = timeList[mid];
|
const midTs = timeList[mid];
|
||||||
|
|
@ -59,11 +60,9 @@ function initPlaybackWorker() {
|
||||||
|
|
||||||
if (currentTime >= midTs && currentTime < nextTs) {
|
if (currentTime >= midTs && currentTime < nextTs) {
|
||||||
return midTs;
|
return midTs;
|
||||||
}
|
} else if (midTs < currentTime) {
|
||||||
else if (midTs < currentTime) {
|
|
||||||
left = mid + 1;
|
left = mid + 1;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
right = mid - 1;
|
right = mid - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -88,7 +87,7 @@ function initPlaybackWorker() {
|
||||||
while (current.isBefore(end) || current.isSame(end)) {
|
while (current.isBefore(end) || current.isSame(end)) {
|
||||||
timeStamps.push(current.valueOf()); // 时间戳(毫秒)
|
timeStamps.push(current.valueOf()); // 时间戳(毫秒)
|
||||||
hsjmnQueryTimeList.push(accumulatedHours); // 累计小时数
|
hsjmnQueryTimeList.push(accumulatedHours); // 累计小时数
|
||||||
current = current.add(1, 'hour'); // 每6小时递增
|
current = current.add(1, "hour"); // 每6小时递增
|
||||||
accumulatedHours += 1; // 累计小时数+6
|
accumulatedHours += 1; // 累计小时数+6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,14 +101,14 @@ function initPlaybackWorker() {
|
||||||
onmessage = function (event) {
|
onmessage = function (event) {
|
||||||
const wData = event.data;
|
const wData = event.data;
|
||||||
switch (wData.code) {
|
switch (wData.code) {
|
||||||
case 'init':
|
case "init":
|
||||||
serverIp = wData.value.serverIp;
|
serverIp = wData.value.serverIp;
|
||||||
token = wData.value.token;
|
token = wData.value.token;
|
||||||
importScripts(`${wData.value.serverIp}/worker/fetchHttp.js`);
|
importScripts(`${wData.value.serverIp}/worker/fetchHttp.js`);
|
||||||
importScripts(`${wData.value.serverIp}/worker/turf.min.js`);
|
importScripts(`${wData.value.serverIp}/worker/turf.min.js`);
|
||||||
importScripts(`${wData.value.serverIp}/worker/dayjs.min.js`);
|
importScripts(`${wData.value.serverIp}/worker/dayjs.min.js`);
|
||||||
break;
|
break;
|
||||||
case 'clearCache':
|
case "clearCache":
|
||||||
isAllowSymn = true;
|
isAllowSymn = true;
|
||||||
isAllowHsjmnDNFB = true;
|
isAllowHsjmnDNFB = true;
|
||||||
isAllowHsjmnJLFB = true;
|
isAllowHsjmnJLFB = true;
|
||||||
|
|
@ -117,14 +116,17 @@ function initPlaybackWorker() {
|
||||||
hsjmnResult1.clear();
|
hsjmnResult1.clear();
|
||||||
hsjmnResult2.clear();
|
hsjmnResult2.clear();
|
||||||
break;
|
break;
|
||||||
case 'querySYMN':
|
case "querySYMN":
|
||||||
// 初始化循环参数(每次启动前重置)
|
// 初始化循环参数(每次启动前重置)
|
||||||
if (isAllowSymn) {
|
if (isAllowSymn) {
|
||||||
symnNum = 0;
|
symnNum = 0;
|
||||||
symmMaxNum = wData.data.globalAttr.time;
|
symmMaxNum = wData.data.globalAttr.time;
|
||||||
lonWidth = wData.data.globalAttr.dxout;
|
lonWidth = wData.data.globalAttr.dxout;
|
||||||
latWidth = wData.data.globalAttr.dyout;
|
latWidth = wData.data.globalAttr.dyout;
|
||||||
|
latGroupData = wData.data.globalAttr.latData;
|
||||||
|
lonGroupData = wData.data.globalAttr.lonData;
|
||||||
cycleBaseParams = wData.data;
|
cycleBaseParams = wData.data;
|
||||||
|
console.log(wData,"console.log(wData)")
|
||||||
|
|
||||||
symnResult.clear();
|
symnResult.clear();
|
||||||
isCycleAborted = false;
|
isCycleAborted = false;
|
||||||
|
|
@ -134,32 +136,33 @@ function initPlaybackWorker() {
|
||||||
for (let i = 0; i < symmMaxNum; i++) {
|
for (let i = 0; i < symmMaxNum; i++) {
|
||||||
// 核心逻辑:累加 3 小时后,获取毫秒时间戳
|
// 核心逻辑:累加 3 小时后,获取毫秒时间戳
|
||||||
const currentTimeStamp = dayjs(wData.data.timeRange.formatStartTime)
|
const currentTimeStamp = dayjs(wData.data.timeRange.formatStartTime)
|
||||||
.add(i * 3, 'hour') // 每次加 3 小时
|
.add(i * 3, "hour") // 每次加 3 小时
|
||||||
.valueOf(); // 转换为毫秒时间戳(等价于 .getTime())
|
.valueOf(); // 转换为毫秒时间戳(等价于 .getTime())
|
||||||
|
|
||||||
timeList.push(currentTimeStamp);
|
timeList.push(currentTimeStamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSYMNplaybackData(queryApi.symn);
|
getSYMNplaybackData(queryApi.symn);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
postMessage({
|
postMessage({
|
||||||
code: 'querySYMNFirstSuccess', // 第一次成功消息码
|
code: "querySYMNFirstSuccess", // 第一次成功消息码
|
||||||
data: {
|
data: {
|
||||||
index: symnNum, // 第一次成功的索引(0 开始)
|
index: symnNum, // 第一次成功的索引(0 开始)
|
||||||
result: symnResult.get(timeList[0]),
|
result: symnResult.get(timeList[0]),
|
||||||
lonWidth,
|
lonWidth,
|
||||||
latWidth,
|
latWidth,
|
||||||
taskId: cycleBaseParams?.taskId || '',
|
taskId: cycleBaseParams?.taskId || "",
|
||||||
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
postMessage({
|
postMessage({
|
||||||
code: 'querySYMNAllSuccess', // 明确“所有请求成功”消息码
|
code: "querySYMNAllSuccess", // 明确“所有请求成功”消息码
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'drawSYMN':{
|
case "drawSYMN":
|
||||||
|
{
|
||||||
// 查找当前时间对应的目标时间戳
|
// 查找当前时间对应的目标时间戳
|
||||||
const targetTs = findTargetTimestamp(wData.data);
|
const targetTs = findTargetTimestamp(wData.data);
|
||||||
|
|
||||||
|
|
@ -175,7 +178,7 @@ function initPlaybackWorker() {
|
||||||
lastTargetTs = targetTs; // 更新记录的时间戳
|
lastTargetTs = targetTs; // 更新记录的时间戳
|
||||||
|
|
||||||
postMessage({
|
postMessage({
|
||||||
code: 'drawSYMN', // 第一次成功消息码
|
code: "drawSYMN", // 第一次成功消息码
|
||||||
data: targetData,
|
data: targetData,
|
||||||
lonWidth,
|
lonWidth,
|
||||||
latWidth,
|
latWidth,
|
||||||
|
|
@ -183,69 +186,74 @@ function initPlaybackWorker() {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// 新增:主动终止循环请求(外部触发)
|
// 新增:主动终止循环请求(外部触发)
|
||||||
case 'abortSYMN':
|
case "abortSYMN":
|
||||||
isCycleAborted = true; // 标记终止
|
isCycleAborted = true; // 标记终止
|
||||||
break;
|
break;
|
||||||
case 'queryHSJMNType':
|
case "queryHSJMNType":
|
||||||
postMessage({
|
postMessage({
|
||||||
code: 'queryHSJMNType', // 第一次成功消息码
|
code: "queryHSJMNType", // 第一次成功消息码
|
||||||
data: wData.data,
|
data: wData.data,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'queryHSJMNndfb':
|
case "queryHSJMNndfb":
|
||||||
if (isAllowHsjmnDNFB) {
|
if (isAllowHsjmnDNFB) {
|
||||||
cycleBaseParams = wData.data;
|
cycleBaseParams = wData.data;
|
||||||
const res = generateTimeStampsAndHours(wData.data.timeRange.formatStartTime, wData.data.timeRange.formatEndTime);
|
const res = generateTimeStampsAndHours(
|
||||||
|
wData.data.timeRange.formatStartTime,
|
||||||
|
wData.data.timeRange.formatEndTime,
|
||||||
|
);
|
||||||
timeList = res.timeStamps;
|
timeList = res.timeStamps;
|
||||||
hsjmnQueryTimeList = res.hsjmnQueryTimeList;
|
hsjmnQueryTimeList = res.hsjmnQueryTimeList;
|
||||||
isCycleAborted = false;
|
isCycleAborted = false;
|
||||||
lastTargetTs = null;
|
lastTargetTs = null;
|
||||||
getHSJMNplaybackData1(queryApi.hsjmnNDFB);
|
getHSJMNplaybackData1(queryApi.hsjmnNDFB);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
postMessage({
|
postMessage({
|
||||||
code: 'queryHSJMNFirstSuccess',
|
code: "queryHSJMNFirstSuccess",
|
||||||
data: {
|
data: {
|
||||||
result: hsjmnResult1.get(timeList[0]), // 首次成功的完整结果
|
result: hsjmnResult1.get(timeList[0]), // 首次成功的完整结果
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// 发送接口1全部完成消息
|
// 发送接口1全部完成消息
|
||||||
postMessage({
|
postMessage({
|
||||||
code: 'queryHSJMNAllSuccess',
|
code: "queryHSJMNAllSuccess",
|
||||||
data: {
|
data: {
|
||||||
successCount: hsjmnResult1.size,
|
successCount: hsjmnResult1.size,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'queryHSJMNjlfb':
|
case "queryHSJMNjlfb":
|
||||||
if (isAllowHsjmnJLFB) {
|
if (isAllowHsjmnJLFB) {
|
||||||
cycleBaseParams = wData.data;
|
cycleBaseParams = wData.data;
|
||||||
const res = generateTimeStampsAndHours(wData.data.timeRange.formatStartTime, wData.data.timeRange.formatEndTime);
|
const res = generateTimeStampsAndHours(
|
||||||
|
wData.data.timeRange.formatStartTime,
|
||||||
|
wData.data.timeRange.formatEndTime,
|
||||||
|
);
|
||||||
timeList = res.timeStamps;
|
timeList = res.timeStamps;
|
||||||
hsjmnQueryTimeList = res.hsjmnQueryTimeList;
|
hsjmnQueryTimeList = res.hsjmnQueryTimeList;
|
||||||
isCycleAborted = false;
|
isCycleAborted = false;
|
||||||
lastTargetTs = null;
|
lastTargetTs = null;
|
||||||
getHSJMNplaybackData2(queryApi.hsjmnJLFB);
|
getHSJMNplaybackData2(queryApi.hsjmnJLFB);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
postMessage({
|
postMessage({
|
||||||
code: 'queryHSJMNFirstSuccess',
|
code: "queryHSJMNFirstSuccess",
|
||||||
data: {
|
data: {
|
||||||
result: hsjmnResult2.get(timeList[0]), // 首次成功的完整结果
|
result: hsjmnResult2.get(timeList[0]), // 首次成功的完整结果
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// 发送接口1全部完成消息
|
// 发送接口1全部完成消息
|
||||||
postMessage({
|
postMessage({
|
||||||
code: 'queryHSJMNAllSuccess',
|
code: "queryHSJMNAllSuccess",
|
||||||
data: {
|
data: {
|
||||||
successCount: hsjmnResult1.size,
|
successCount: hsjmnResult1.size,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'drawHSJMN':{
|
case "drawHSJMN":
|
||||||
console.log('1111111111111111111111111111111');
|
{
|
||||||
|
console.log("1111111111111111111111111111111");
|
||||||
|
|
||||||
// 查找当前时间对应的目标时间戳
|
// 查找当前时间对应的目标时间戳
|
||||||
const targetTs = findTargetTimestamp(wData.data);
|
const targetTs = findTargetTimestamp(wData.data);
|
||||||
|
|
@ -263,7 +271,7 @@ function initPlaybackWorker() {
|
||||||
lastTargetTs = targetTs; // 更新记录的时间戳
|
lastTargetTs = targetTs; // 更新记录的时间戳
|
||||||
|
|
||||||
postMessage({
|
postMessage({
|
||||||
code: 'drawHSJMN', // 第一次成功消息码
|
code: "drawHSJMN", // 第一次成功消息码
|
||||||
data: {
|
data: {
|
||||||
hsjmnResult1Count: targetData1,
|
hsjmnResult1Count: targetData1,
|
||||||
hsjmnResult2Count: targetData2,
|
hsjmnResult2Count: targetData2,
|
||||||
|
|
@ -285,7 +293,7 @@ function initPlaybackWorker() {
|
||||||
// 只有未终止且所有请求完成,才发送“所有成功”消息
|
// 只有未终止且所有请求完成,才发送“所有成功”消息
|
||||||
if (!isCycleAborted) {
|
if (!isCycleAborted) {
|
||||||
postMessage({
|
postMessage({
|
||||||
code: 'querySYMNAllSuccess', // 明确“所有请求成功”消息码
|
code: "querySYMNAllSuccess", // 明确“所有请求成功”消息码
|
||||||
});
|
});
|
||||||
isAllowSymn = false; // 标记后续不再请求
|
isAllowSymn = false; // 标记后续不再请求
|
||||||
}
|
}
|
||||||
|
|
@ -303,7 +311,10 @@ function initPlaybackWorker() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchHttp(url, { params: param, token }, (res) => {
|
fetchHttp(url, { params: param, token }, (res) => {
|
||||||
console.log(`第 ${symnNum + 1}/${symmMaxNum} 次请求结果`, res.success ? '成功' : '失败');
|
console.log(
|
||||||
|
`第 ${symnNum + 1}/${symmMaxNum} 次请求结果`,
|
||||||
|
res.success ? "成功" : "失败",
|
||||||
|
);
|
||||||
|
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
symnResult.set(timeList[symnNum], res.result);
|
symnResult.set(timeList[symnNum], res.result);
|
||||||
|
|
@ -312,31 +323,35 @@ function initPlaybackWorker() {
|
||||||
if (!isFirstSuccessSent) {
|
if (!isFirstSuccessSent) {
|
||||||
isFirstSuccessSent = true;
|
isFirstSuccessSent = true;
|
||||||
postMessage({
|
postMessage({
|
||||||
code: 'querySYMNFirstSuccess', // 第一次成功消息码
|
code: "querySYMNFirstSuccess", // 第一次成功消息码
|
||||||
data: {
|
data: {
|
||||||
index: symnNum, // 第一次成功的索引(0 开始)
|
index: symnNum, // 第一次成功的索引(0 开始)
|
||||||
result: res.result,
|
result: res.result,
|
||||||
lonWidth,
|
lonWidth,
|
||||||
latWidth,
|
latWidth,
|
||||||
taskId: cycleBaseParams?.taskId || '',
|
taskId: cycleBaseParams?.taskId || "",
|
||||||
|
latGroupData,
|
||||||
|
lonGroupData
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
symnNum++;
|
symnNum++;
|
||||||
getSYMNplaybackData(params); // 发起下一次请求
|
getSYMNplaybackData(params); // 发起下一次请求
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// 发送“某一次失败”消息(仅发送当前失败,然后终止循环)
|
// 发送“某一次失败”消息(仅发送当前失败,然后终止循环)
|
||||||
isCycleAborted = true;
|
isCycleAborted = true;
|
||||||
console.error(`❌ 第 ${symnNum + 1} 次请求失败:`, res.msg || '未知错误');
|
console.error(
|
||||||
|
`❌ 第 ${symnNum + 1} 次请求失败:`,
|
||||||
|
res.msg || "未知错误",
|
||||||
|
);
|
||||||
postMessage({
|
postMessage({
|
||||||
code: 'querySYMNOneFail', // 单次失败消息码
|
code: "querySYMNOneFail", // 单次失败消息码
|
||||||
data: {
|
data: {
|
||||||
index: symnNum, // 失败的索引
|
index: symnNum, // 失败的索引
|
||||||
msg: res.msg || '未知错误',
|
msg: res.msg || "未知错误",
|
||||||
successCount: symnResult.size, // 失败前已成功的次数
|
successCount: symnResult.size, // 失败前已成功的次数
|
||||||
taskId: cycleBaseParams?.taskId || '',
|
taskId: cycleBaseParams?.taskId || "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -374,7 +389,7 @@ function initPlaybackWorker() {
|
||||||
if (currentIndex >= totalCount) {
|
if (currentIndex >= totalCount) {
|
||||||
// 发送接口1全部完成消息
|
// 发送接口1全部完成消息
|
||||||
postMessage({
|
postMessage({
|
||||||
code: 'queryHSJMNAllSuccess',
|
code: "queryHSJMNAllSuccess",
|
||||||
data: {
|
data: {
|
||||||
successCount: hsjmnResult1.size,
|
successCount: hsjmnResult1.size,
|
||||||
},
|
},
|
||||||
|
|
@ -395,14 +410,17 @@ function initPlaybackWorker() {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. 发送请求
|
// 3. 发送请求
|
||||||
fetchHttp(requestUrl, { params: requestParam, token }, (res) => {
|
fetchHttp(
|
||||||
|
requestUrl,
|
||||||
|
{ params: requestParam, token },
|
||||||
|
(res) => {
|
||||||
// 存储结果到Map(key: 时间戳,value: 接口返回的result)
|
// 存储结果到Map(key: 时间戳,value: 接口返回的result)
|
||||||
hsjmnResult1.set(timeList[currentIndex], res.result);
|
hsjmnResult1.set(timeList[currentIndex], res.result);
|
||||||
|
|
||||||
// 首次请求成功:发送专属消息
|
// 首次请求成功:发送专属消息
|
||||||
if (isFirstSend) {
|
if (isFirstSend) {
|
||||||
postMessage({
|
postMessage({
|
||||||
code: 'queryHSJMNFirstSuccess',
|
code: "queryHSJMNFirstSuccess",
|
||||||
data: {
|
data: {
|
||||||
result: res.result, // 首次成功的完整结果
|
result: res.result, // 首次成功的完整结果
|
||||||
},
|
},
|
||||||
|
|
@ -418,7 +436,8 @@ function initPlaybackWorker() {
|
||||||
// 失败回调
|
// 失败回调
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error(`❌ 接口1第 ${currentIndex + 1} 次请求失败:`, error);
|
console.error(`❌ 接口1第 ${currentIndex + 1} 次请求失败:`, error);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// 启动接口1的循环请求
|
// 启动接口1的循环请求
|
||||||
requestSingle();
|
requestSingle();
|
||||||
|
|
@ -444,7 +463,7 @@ function initPlaybackWorker() {
|
||||||
if (currentIndex >= totalCount) {
|
if (currentIndex >= totalCount) {
|
||||||
// 发送接口1全部完成消息
|
// 发送接口1全部完成消息
|
||||||
postMessage({
|
postMessage({
|
||||||
code: 'queryHSJMNAllSuccess',
|
code: "queryHSJMNAllSuccess",
|
||||||
data: {
|
data: {
|
||||||
successCount: hsjmnResult1.size,
|
successCount: hsjmnResult1.size,
|
||||||
},
|
},
|
||||||
|
|
@ -465,14 +484,17 @@ function initPlaybackWorker() {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. 发送请求
|
// 3. 发送请求
|
||||||
fetchHttp(requestUrl, { params: requestParam, token }, (res) => {
|
fetchHttp(
|
||||||
|
requestUrl,
|
||||||
|
{ params: requestParam, token },
|
||||||
|
(res) => {
|
||||||
// 存储结果到Map(key: 时间戳,value: 接口返回的result)
|
// 存储结果到Map(key: 时间戳,value: 接口返回的result)
|
||||||
hsjmnResult2.set(timeList[currentIndex], res.result);
|
hsjmnResult2.set(timeList[currentIndex], res.result);
|
||||||
|
|
||||||
// 首次请求成功:发送专属消息
|
// 首次请求成功:发送专属消息
|
||||||
if (isFirstSend) {
|
if (isFirstSend) {
|
||||||
postMessage({
|
postMessage({
|
||||||
code: 'queryHSJMNFirstSuccess',
|
code: "queryHSJMNFirstSuccess",
|
||||||
data: {
|
data: {
|
||||||
result: res.result, // 首次成功的完整结果
|
result: res.result, // 首次成功的完整结果
|
||||||
},
|
},
|
||||||
|
|
@ -488,7 +510,8 @@ function initPlaybackWorker() {
|
||||||
// 失败回调
|
// 失败回调
|
||||||
(error) => {
|
(error) => {
|
||||||
console.error(`❌ 接口1第 ${currentIndex + 1} 次请求失败:`, error);
|
console.error(`❌ 接口1第 ${currentIndex + 1} 次请求失败:`, error);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// 启动接口1的循环请求
|
// 启动接口1的循环请求
|
||||||
requestSingle();
|
requestSingle();
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,23 @@
|
||||||
import type { WorkerFunc } from './worketBlob';
|
import type { WorkerFunc } from "./worketBlob";
|
||||||
import dayjs from 'dayjs';
|
import dayjs from "dayjs";
|
||||||
import { serverIp } from '@/gis/common/config';
|
import { serverIp } from "@/gis/common/config";
|
||||||
import EventBus from '@/gis/common/eventBus/eventBus.ts';
|
import EventBus from "@/gis/common/eventBus/eventBus.ts";
|
||||||
import { fitViewByExtent } from '@/gis/ol/index.ts';
|
import { fitViewByExtent } from "@/gis/ol/index.ts";
|
||||||
import { setShouldAnimate } from '@/gis/ol/olClock';
|
import { setShouldAnimate } from "@/gis/ol/olClock";
|
||||||
import { addCustomGridLayer } from '@/gis/ol/olGirdHeatmap';
|
import { addCustomGridLayer } from "@/gis/ol/olGirdHeatmap";
|
||||||
import { addHeatmapLayer } from '@/gis/ol/olHeatmap';
|
import { addHeatmapLayer } from "@/gis/ol/olHeatmap";
|
||||||
// import { addIsolineLayer } from '@/gis/ol/olIsoLine';
|
// import { addIsolineLayer } from '@/gis/ol/olIsoLine';
|
||||||
import playbackWorker from './playbackWorker';
|
import playbackWorker from "./playbackWorker";
|
||||||
import { getWorketImportUrl } from './worketBlob'; // 导入 WorkerFunc 类型
|
import { getWorketImportUrl } from "./worketBlob"; // 导入 WorkerFunc 类型
|
||||||
|
import type { FlexpartColorScale } from "@/gis/ol/flexpartContour/flexpartContourTypes";
|
||||||
|
import type {
|
||||||
|
FlexpartGridAxes,
|
||||||
|
FlexpartGridPoint,
|
||||||
|
} from "@/gis/ol/flexpartContour/flexpartContourTypes";
|
||||||
|
import {
|
||||||
|
renderBackwardData,
|
||||||
|
} from '@/gis/ol/flexpartContour/flexpartContourExample';
|
||||||
|
|
||||||
// 定义 Worker 消息数据的类型接口
|
// 定义 Worker 消息数据的类型接口
|
||||||
interface WorkerMessage {
|
interface WorkerMessage {
|
||||||
code: string;
|
code: string;
|
||||||
|
|
@ -27,22 +36,27 @@ type PostMessageData = Record<string, any>;
|
||||||
let playbackWorkerObj: Worker | null = null;
|
let playbackWorkerObj: Worker | null = null;
|
||||||
export const playback_EventBusCallback = new EventBus();
|
export const playback_EventBusCallback = new EventBus();
|
||||||
|
|
||||||
let hsjmnType = '浓度分布';
|
let hsjmnType = "浓度分布";
|
||||||
|
|
||||||
|
let axes: FlexpartGridAxes = {};
|
||||||
|
let colorScale: FlexpartColorScale = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算经纬度点集合的 extent(地理范围)
|
* 计算经纬度点集合的 extent(地理范围)
|
||||||
* @param points 经纬度点数组 [{ lon: 经度, lat: 纬度, ... }]
|
* @param points 经纬度点数组 [{ lon: 经度, lat: 纬度, ... }]
|
||||||
* @returns extent 数组 [minLon, minLat, maxLon, maxLat],若无有效点返回 null
|
* @returns extent 数组 [minLon, minLat, maxLon, maxLat],若无有效点返回 null
|
||||||
*/
|
*/
|
||||||
function calculatePointsExtent(points: Array<{ lon: number; lat: number }>): number[] | null {
|
function calculatePointsExtent(
|
||||||
|
points: Array<{ lon: number; lat: number }>,
|
||||||
|
): number[] | null {
|
||||||
if (!points || points.length === 0) {
|
if (!points || points.length === 0) {
|
||||||
console.warn('无有效经纬度点,无法计算 extent');
|
console.warn("无有效经纬度点,无法计算 extent");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取所有经度和纬度
|
// 提取所有经度和纬度
|
||||||
const lons = points.map(p => p.lon);
|
const lons = points.map((p) => p.lon);
|
||||||
const lats = points.map(p => p.lat);
|
const lats = points.map((p) => p.lat);
|
||||||
|
|
||||||
// 计算最小/最大经纬度(添加 0.01 缓冲,避免点贴边)
|
// 计算最小/最大经纬度(添加 0.01 缓冲,避免点贴边)
|
||||||
const minLon = Math.min(...lons) - 0.01;
|
const minLon = Math.min(...lons) - 0.01;
|
||||||
|
|
@ -58,16 +72,16 @@ function calculatePointsExtent(points: Array<{ lon: number; lat: number }>): num
|
||||||
*/
|
*/
|
||||||
export function initPlaybackWorkerMa(): void {
|
export function initPlaybackWorkerMa(): void {
|
||||||
// 校验 playbackWorker 是符合要求的 Worker 函数(类型约束)
|
// 校验 playbackWorker 是符合要求的 Worker 函数(类型约束)
|
||||||
if (typeof playbackWorker !== 'function' || !playbackWorker.name) {
|
if (typeof playbackWorker !== "function" || !playbackWorker.name) {
|
||||||
throw new Error('playbackWorker 必须是具名函数,且无外部依赖');
|
throw new Error("playbackWorker 必须是具名函数,且无外部依赖");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建 worket 对象(getWorketImportUrl 已约束入参为 WorkerFunc)
|
// 创建 worket 对象(getWorketImportUrl 已约束入参为 WorkerFunc)
|
||||||
const workerUrl = getWorketImportUrl(playbackWorker as WorkerFunc);
|
const workerUrl = getWorketImportUrl(playbackWorker as WorkerFunc);
|
||||||
playbackWorkerObj = new Worker(workerUrl);
|
playbackWorkerObj = new Worker(workerUrl);
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem("token");
|
||||||
playbackWorkerObj.postMessage({
|
playbackWorkerObj.postMessage({
|
||||||
code: 'init',
|
code: "init",
|
||||||
value: {
|
value: {
|
||||||
serverIp,
|
serverIp,
|
||||||
token,
|
token,
|
||||||
|
|
@ -78,7 +92,9 @@ export function initPlaybackWorkerMa(): void {
|
||||||
playbackWorkerObj.onmessage = function (event: MessageEvent<WorkerMessage>) {
|
playbackWorkerObj.onmessage = function (event: MessageEvent<WorkerMessage>) {
|
||||||
const weaData = event.data;
|
const weaData = event.data;
|
||||||
switch (weaData.code) {
|
switch (weaData.code) {
|
||||||
case 'querySYMNFirstSuccess':{ // 输运模拟第一次接口请求完成,开始绘制自定义热力图
|
case "querySYMNFirstSuccess":
|
||||||
|
{
|
||||||
|
// 输运模拟第一次接口请求完成,开始绘制自定义热力图
|
||||||
// 定义最大值
|
// 定义最大值
|
||||||
let maxValue = 0;
|
let maxValue = 0;
|
||||||
let minValue = 0;
|
let minValue = 0;
|
||||||
|
|
@ -87,7 +103,7 @@ export function initPlaybackWorkerMa(): void {
|
||||||
if (item.value > maxValue) {
|
if (item.value > maxValue) {
|
||||||
maxValue = item.value;
|
maxValue = item.value;
|
||||||
}
|
}
|
||||||
if (item.value !== '-Infinity' && item.value < minValue) {
|
if (item.value !== "-Infinity" && item.value < minValue) {
|
||||||
minValue = item.value;
|
minValue = item.value;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
@ -99,8 +115,29 @@ export function initPlaybackWorkerMa(): void {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
let maxColorSymn = Number(sessionStorage.getItem("symnMaxColor"));
|
||||||
|
let colorArr = [];
|
||||||
|
for (let i = 0; i < 11; i++) {
|
||||||
|
let str = "1e";
|
||||||
|
str += maxColorSymn - i;
|
||||||
|
colorArr.unshift(Number(str));
|
||||||
|
}
|
||||||
|
|
||||||
|
colorScale = {
|
||||||
|
colorMin: colorArr[0],
|
||||||
|
colorMax: colorArr[10],
|
||||||
|
levels: colorArr,
|
||||||
|
};
|
||||||
|
|
||||||
|
axes = {
|
||||||
|
longitudes:weaData.data.lonGroupData,
|
||||||
|
latitudes:weaData.data.latGroupData,
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBackwardData(gridData,axes,colorScale);
|
||||||
// 加载自定义热力图
|
// 加载自定义热力图
|
||||||
addCustomGridLayer(gridData, maxValue, minValue);
|
// addCustomGridLayer(gridData, maxValue, minValue);
|
||||||
|
|
||||||
// 开始播放
|
// 开始播放
|
||||||
setShouldAnimate(true);
|
setShouldAnimate(true);
|
||||||
|
|
@ -117,7 +154,9 @@ export function initPlaybackWorkerMa(): void {
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'drawSYMN':{ // 绘制输运模拟自定义热力图
|
case "drawSYMN":
|
||||||
|
{
|
||||||
|
// 绘制输运模拟自定义热力图
|
||||||
// 定义最大值
|
// 定义最大值
|
||||||
let maxValue = 0;
|
let maxValue = 0;
|
||||||
let minValue = 0;
|
let minValue = 0;
|
||||||
|
|
@ -127,7 +166,7 @@ export function initPlaybackWorkerMa(): void {
|
||||||
if (item.value > maxValue) {
|
if (item.value > maxValue) {
|
||||||
maxValue = item.value;
|
maxValue = item.value;
|
||||||
}
|
}
|
||||||
if (item.value !== '-Infinity' && item.value < minValue) {
|
if (item.value !== "-Infinity" && item.value < minValue) {
|
||||||
minValue = item.value;
|
minValue = item.value;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
@ -140,7 +179,8 @@ export function initPlaybackWorkerMa(): void {
|
||||||
});
|
});
|
||||||
|
|
||||||
// 加载自定义热力图
|
// 加载自定义热力图
|
||||||
addCustomGridLayer(gridData, maxValue, minValue);
|
// addCustomGridLayer(gridData, maxValue, minValue);
|
||||||
|
renderBackwardData(gridData,axes,colorScale);
|
||||||
// 更改最大值
|
// 更改最大值
|
||||||
// playback_EventBusCallback.publish('symnLegendUpdateMax', {
|
// playback_EventBusCallback.publish('symnLegendUpdateMax', {
|
||||||
// maxValue,
|
// maxValue,
|
||||||
|
|
@ -148,29 +188,31 @@ export function initPlaybackWorkerMa(): void {
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'querySYMNOneFail': // 输运模拟接口请求失败
|
case "querySYMNOneFail": // 输运模拟接口请求失败
|
||||||
// 处理失败场景(如提示用户、重试循环)
|
// 处理失败场景(如提示用户、重试循环)
|
||||||
console.error('⚠️ 请求失败', weaData.data);
|
console.error("⚠️ 请求失败", weaData.data);
|
||||||
break;
|
break;
|
||||||
case 'querySYMNAllSuccess': // 输运模拟所有接口请求完成
|
case "querySYMNAllSuccess": // 输运模拟所有接口请求完成
|
||||||
// 解除进度条禁用
|
// 解除进度条禁用
|
||||||
playback_EventBusCallback.publish('timeSliderUiData', {
|
playback_EventBusCallback.publish("timeSliderUiData", {
|
||||||
key: 'isDisabledSlider',
|
key: "isDisabledSlider",
|
||||||
value: false,
|
value: false,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'queryHSJMNType':
|
case "queryHSJMNType":
|
||||||
hsjmnType = weaData.data;
|
hsjmnType = weaData.data;
|
||||||
break;
|
break;
|
||||||
case 'queryHSJMNFirstSuccess': { // 核事件模拟第一次请求完成,开始绘制
|
case "queryHSJMNFirstSuccess":
|
||||||
|
{
|
||||||
|
// 核事件模拟第一次请求完成,开始绘制
|
||||||
// 显示图例
|
// 显示图例
|
||||||
playback_EventBusCallback.publish('legendUiData', {
|
playback_EventBusCallback.publish("legendUiData", {
|
||||||
key: 'showLengend',
|
key: "showLengend",
|
||||||
value: true,
|
value: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
let data = null;
|
let data = null;
|
||||||
if (hsjmnType === '浓度分布') {
|
if (hsjmnType === "浓度分布") {
|
||||||
data = weaData.data.result;
|
data = weaData.data.result;
|
||||||
// 加工数据
|
// 加工数据
|
||||||
const gridData = data.dataList.map((item: any) => {
|
const gridData = data.dataList.map((item: any) => {
|
||||||
|
|
@ -190,12 +232,11 @@ export function initPlaybackWorkerMa(): void {
|
||||||
// 添加热力图
|
// 添加热力图
|
||||||
addHeatmapLayer(gridData, data.max === 0 ? 1 : data.max);
|
addHeatmapLayer(gridData, data.max === 0 ? 1 : data.max);
|
||||||
// 动态改变图例最大值
|
// 动态改变图例最大值
|
||||||
playback_EventBusCallback.publish('symnLegendUpdateMax', {
|
playback_EventBusCallback.publish("symnLegendUpdateMax", {
|
||||||
maxValue: data.max,
|
maxValue: data.max,
|
||||||
minValue: data.min,
|
minValue: data.min,
|
||||||
});
|
});
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
data = weaData.data.result;
|
data = weaData.data.result;
|
||||||
|
|
||||||
// 加工数据
|
// 加工数据
|
||||||
|
|
@ -213,7 +254,9 @@ export function initPlaybackWorkerMa(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加工数据
|
// 加工数据
|
||||||
const gridData = data.dataList.filter(item => item[2] >= 1.5).map((item: any) => {
|
const gridData = data.dataList
|
||||||
|
.filter((item) => item[2] >= 1.5)
|
||||||
|
.map((item: any) => {
|
||||||
return {
|
return {
|
||||||
lon: item[0],
|
lon: item[0],
|
||||||
lat: item[1],
|
lat: item[1],
|
||||||
|
|
@ -226,7 +269,7 @@ export function initPlaybackWorkerMa(): void {
|
||||||
// 渲染等值线
|
// 渲染等值线
|
||||||
// addIsolineLayer(gridData); // 可自定义等值线间隔
|
// addIsolineLayer(gridData); // 可自定义等值线间隔
|
||||||
// 动态改变图例最大值
|
// 动态改变图例最大值
|
||||||
playback_EventBusCallback.publish('symnLegendUpdateMax', {
|
playback_EventBusCallback.publish("symnLegendUpdateMax", {
|
||||||
maxValue: data.max,
|
maxValue: data.max,
|
||||||
minValue: data.min,
|
minValue: data.min,
|
||||||
});
|
});
|
||||||
|
|
@ -236,13 +279,14 @@ export function initPlaybackWorkerMa(): void {
|
||||||
setShouldAnimate(true);
|
setShouldAnimate(true);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'drawHSJMN':{ // 绘制核事件模拟自定义热力图
|
case "drawHSJMN":
|
||||||
|
{
|
||||||
|
// 绘制核事件模拟自定义热力图
|
||||||
// 加工数据
|
// 加工数据
|
||||||
if (!weaData.data)
|
if (!weaData.data) return;
|
||||||
return;
|
|
||||||
|
|
||||||
let data = null;
|
let data = null;
|
||||||
if (hsjmnType === '浓度分布') {
|
if (hsjmnType === "浓度分布") {
|
||||||
data = weaData.data.hsjmnResult1Count;
|
data = weaData.data.hsjmnResult1Count;
|
||||||
if (data) {
|
if (data) {
|
||||||
const gridData = data.dataList.map((item: any) => {
|
const gridData = data.dataList.map((item: any) => {
|
||||||
|
|
@ -256,16 +300,17 @@ export function initPlaybackWorkerMa(): void {
|
||||||
// 添加热力图
|
// 添加热力图
|
||||||
addHeatmapLayer(gridData, data.max === 0 ? 1 : data.max);
|
addHeatmapLayer(gridData, data.max === 0 ? 1 : data.max);
|
||||||
// 动态改变图例最大值
|
// 动态改变图例最大值
|
||||||
playback_EventBusCallback.publish('symnLegendUpdateMax', {
|
playback_EventBusCallback.publish("symnLegendUpdateMax", {
|
||||||
maxValue: data.max,
|
maxValue: data.max,
|
||||||
minValue: data.min,
|
minValue: data.min,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
data = weaData.data.hsjmnResult2Count;
|
data = weaData.data.hsjmnResult2Count;
|
||||||
if (data) {
|
if (data) {
|
||||||
const gridData = data.dataList.filter(item => item[2] >= 1.5).map((item: any) => {
|
const gridData = data.dataList
|
||||||
|
.filter((item) => item[2] >= 1.5)
|
||||||
|
.map((item: any) => {
|
||||||
return {
|
return {
|
||||||
lon: item[0],
|
lon: item[0],
|
||||||
lat: item[1],
|
lat: item[1],
|
||||||
|
|
@ -278,7 +323,7 @@ export function initPlaybackWorkerMa(): void {
|
||||||
// 渲染等值线
|
// 渲染等值线
|
||||||
// addIsolineLayer(gridData); // 可自定义等值线间隔
|
// addIsolineLayer(gridData); // 可自定义等值线间隔
|
||||||
// 动态改变图例最大值
|
// 动态改变图例最大值
|
||||||
playback_EventBusCallback.publish('symnLegendUpdateMax', {
|
playback_EventBusCallback.publish("symnLegendUpdateMax", {
|
||||||
maxValue: data.max,
|
maxValue: data.max,
|
||||||
minValue: data.min,
|
minValue: data.min,
|
||||||
});
|
});
|
||||||
|
|
@ -286,10 +331,10 @@ export function initPlaybackWorkerMa(): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'queryHSJMNAllSuccess': // 核事件模拟所有接口请求完成
|
case "queryHSJMNAllSuccess": // 核事件模拟所有接口请求完成
|
||||||
// 解除进度条禁用
|
// 解除进度条禁用
|
||||||
playback_EventBusCallback.publish('timeSliderUiData', {
|
playback_EventBusCallback.publish("timeSliderUiData", {
|
||||||
key: 'isDisabledSlider',
|
key: "isDisabledSlider",
|
||||||
value: false,
|
value: false,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
@ -298,7 +343,7 @@ export function initPlaybackWorkerMa(): void {
|
||||||
|
|
||||||
// 监听 Worker 错误(可选:增强错误处理)
|
// 监听 Worker 错误(可选:增强错误处理)
|
||||||
playbackWorkerObj.onerror = function (error) {
|
playbackWorkerObj.onerror = function (error) {
|
||||||
console.error('Worker 执行错误:', error.message, '行号:', error.lineno);
|
console.error("Worker 执行错误:", error.message, "行号:", error.lineno);
|
||||||
// 可根据需求添加错误上报或重试逻辑
|
// 可根据需求添加错误上报或重试逻辑
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -323,9 +368,8 @@ export function sendPlaybackPostMassage(
|
||||||
// 确保 playbackWorkerObj 已初始化(TS 类型守卫)
|
// 确保 playbackWorkerObj 已初始化(TS 类型守卫)
|
||||||
if (playbackWorkerObj) {
|
if (playbackWorkerObj) {
|
||||||
playbackWorkerObj.postMessage(massage);
|
playbackWorkerObj.postMessage(massage);
|
||||||
}
|
} else {
|
||||||
else {
|
throw new Error("Web Worker 初始化失败,无法发送消息");
|
||||||
throw new Error('Web Worker 初始化失败,无法发送消息');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,17 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const unitValue = ref(null);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.startExp, // 函数写法,监听props属性(推荐)
|
() => props.startExp, // 函数写法,监听props属性(推荐)
|
||||||
(newVal, oldVal) => {
|
(newVal, oldVal) => {
|
||||||
console.log("父组件传过来的值变化了", newVal, oldVal);
|
console.log("父组件传过来的值变化了", newVal, oldVal);
|
||||||
// 在这里写你的业务:生成10个对数刻度、重新渲染图表等
|
// 在这里写你的业务:生成10个对数刻度、重新渲染图表等
|
||||||
handleGenTicks(newVal);
|
handleGenTicks(newVal);
|
||||||
|
sessionStorage.setItem("symnMaxColor",newVal);
|
||||||
|
|
||||||
|
unitValue.value = JSON.parse(sessionStorage.getItem("ssmnDataItem")).taskMode==-1?"m<sup>-3</sup>":"mBq/m<sup>3</sup>"
|
||||||
},
|
},
|
||||||
{ immediate: true } // immediate:true 组件初始化时立刻执行一次,非常常用!
|
{ immediate: true } // immediate:true 组件初始化时立刻执行一次,非常常用!
|
||||||
);
|
);
|
||||||
|
|
@ -55,7 +60,7 @@ function handleGenTicks(maxVal) {
|
||||||
label: maxVal - index,
|
label: maxVal - index,
|
||||||
};
|
};
|
||||||
|
|
||||||
colorList.value.push(item1);
|
colorList.value.unshift(item1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -72,6 +77,7 @@ function handleGenTicks(maxVal) {
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 刻度:10个,一一对应每个色块 -->
|
<!-- 刻度:10个,一一对应每个色块 -->
|
||||||
|
<span style="position: absolute;right: 40px;bottom: 12px; white-space: nowrap;" v-html="unitValue"></span>
|
||||||
<div class="ticks">
|
<div class="ticks">
|
||||||
<span v-for="(item, index) in colorList" :key="index"
|
<span v-for="(item, index) in colorList" :key="index"
|
||||||
>10<sup>{{ item.label }}</sup></span
|
>10<sup>{{ item.label }}</sup></span
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ import sxfx from "./timingAnalysis/index.vue";
|
||||||
import { playback_EventBusCallback } from "@/gis/worker/playbackWorkerMa";
|
import { playback_EventBusCallback } from "@/gis/worker/playbackWorkerMa";
|
||||||
import { activeMoudleInf } from "@/gis/ol";
|
import { activeMoudleInf } from "@/gis/ol";
|
||||||
|
|
||||||
import { getRwNcMaxValue } from "@/utils/axios/fskjk";
|
import { getRwNcMaxValue,stopRw } from "@/utils/axios/fskjk";
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const formEl = ref<FormInstance>();
|
const formEl = ref<FormInstance>();
|
||||||
|
|
@ -1020,6 +1020,14 @@ function getStations() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 停止任务
|
||||||
|
function stopTaskRw(taskId){
|
||||||
|
console.log(taskId,"222")
|
||||||
|
stopRw({taskId},(res)=>{
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
getList();
|
getList();
|
||||||
// 查询所有核设施
|
// 查询所有核设施
|
||||||
getNuclearfacility();
|
getNuclearfacility();
|
||||||
|
|
@ -1046,7 +1054,7 @@ getStations();
|
||||||
<el-input v-model="form.taskName" />
|
<el-input v-model="form.taskName" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="t('taskMode')">
|
<el-form-item :label="t('taskMode')">
|
||||||
<el-input :value="form.taskMode === -1 ? '反向模拟' : '正向模拟'" />
|
<el-input :value="form.taskMode === -1 ? '反向模拟' : form.taskMode === 2?'持续正向模拟':'正向模拟'" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="t('taskType')">
|
<el-form-item :label="t('taskType')">
|
||||||
<el-input :value="form.taskType === 1 ? '手动创建' : '自动创建'" />
|
<el-input :value="form.taskType === 1 ? '手动创建' : '自动创建'" />
|
||||||
|
|
@ -1277,6 +1285,7 @@ getStations();
|
||||||
class="custom-select"
|
class="custom-select"
|
||||||
>
|
>
|
||||||
<ElOption label="正向模拟" :value="1" />
|
<ElOption label="正向模拟" :value="1" />
|
||||||
|
<ElOption label="正向持续模拟" :value="2" />
|
||||||
<ElOption label="反向模拟" :value="-1" />
|
<ElOption label="反向模拟" :value="-1" />
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
@ -1346,7 +1355,7 @@ getStations();
|
||||||
<ElTableColumn prop="taskName" :label="t('taskName')" />
|
<ElTableColumn prop="taskName" :label="t('taskName')" />
|
||||||
<ElTableColumn prop="taskMode" :label="t('taskMode')">
|
<ElTableColumn prop="taskMode" :label="t('taskMode')">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ scope.row.taskMode === 1 ? "正向模拟" : "反向模拟" }}
|
{{ scope.row.taskMode === 1 ? "正向模拟" : scope.row.taskMode === 2?'持续正向模拟':'正向模拟' }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="taskType" :label="t('taskType')">
|
<ElTableColumn prop="taskType" :label="t('taskType')">
|
||||||
|
|
@ -1514,6 +1523,15 @@ getStations();
|
||||||
>
|
>
|
||||||
{{ t("operationLog") }}
|
{{ t("operationLog") }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
|
||||||
|
<ElButton
|
||||||
|
type="text"
|
||||||
|
style="color: #36e7f7;"
|
||||||
|
v-if="scope.row.taskMode==2"
|
||||||
|
@click="stopTaskRw(scope.row.id)"
|
||||||
|
>
|
||||||
|
停止任务
|
||||||
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
</ElTable>
|
</ElTable>
|
||||||
|
|
@ -1593,6 +1611,7 @@ getStations();
|
||||||
@change="clearChild"
|
@change="clearChild"
|
||||||
>
|
>
|
||||||
<ElOption label="正向模拟" :value="1" />
|
<ElOption label="正向模拟" :value="1" />
|
||||||
|
<ElOption label="正向持续模拟" :value="2" />
|
||||||
<ElOption label="反向模拟" :value="-1" />
|
<ElOption label="反向模拟" :value="-1" />
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
|
||||||
|
|
@ -114,3 +114,23 @@ export async function getRwNcMaxValue(
|
||||||
callback?.(response);
|
callback?.(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 停止任务
|
||||||
|
export async function stopRw(
|
||||||
|
params: any,
|
||||||
|
callback?: (data: any) => void,
|
||||||
|
): Promise<any> {
|
||||||
|
const response = await request({
|
||||||
|
url: `${serverIp}${baseURL}/transportTask/stopTask`,
|
||||||
|
method: "PUT",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.code === 200) {
|
||||||
|
ElMessage({
|
||||||
|
type: "success",
|
||||||
|
message: "停止成功",
|
||||||
|
});
|
||||||
|
callback?.(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user