AI Chat SDK
Click Export Project Template in the AI chat toolbar Click to Download Project Code. See the full codebase in src\chat\sdk.ts.
import vjmap, { Map } from "vjmap";
import vjcommon from "vjcommon";
export class VjChatSDK {
map: Map;
context: Record<string, any>;
constructor(map: Map, context: Record<string, any>) {
this.map = map;
this.context = context;
}
/**
* Get user custom data keys by prefix from server
* @param {string} prefix Key name prefix
* @returns {Promise<string[]>} Returns array of retrieved keys
* @example
* // Get keys by prefix from server, prefix test
* let keys = await this.getCustomDataKeysByPrefixServer("test");
* console.log(keys);
*/
async getCustomDataKeysByPrefixServer(prefix) {
let svc = this.map.getService();
let res = await svc.getCustomDataKeysByPrefix(prefix);
return res?.keys || [];
}
/**
* Create point marker data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {string} options.text Text to display, optional, default "index ${props.index}"
* @param {number} options.minZoom Min zoom to display, optional, default 0
* @param {number} options.maxZoom Max zoom to display, optional, default 24
* @param {boolean} options.isDrawLinePoint Include line points when drawing, optional, default false
* @param {string} options.color Marker color, optional, default'#3FB1CE'
* @param {number[]} options.offset XY pixel offset [0,0], negative = left/up
* @param {number} options.rotation Marker rotation, clockwise, default 0
* @param {string} options.anchor Marker anchor:center、top、bottom、left、right、top-left、top-right、bottom-left、bottom-right, default center
* @param {boolean} options.draggable Draggable, optional, default false
* @param {string} options.animationType Animation: MAP_ANIMATION_NONE, MAP_ANIMATION_BOUNCE, MAP_ANIMATION_DROP
* @param {string} options.rotationAlignment Rotation alignment: auto, map, viewport, default auto
* @param {string} options.pitchAlignment Pitch alignment: auto, map, viewport, default auto
* @param {number} options.scale Scale, default 1, 41px x 27px
* @param {number} options.scaleMaxZoom Scale max zoom (0-24), auto-scale below
* @param {number} options.height Elevation, CAD units
* @param {boolean} options.removeWhenNoInMapView Auto show/hide in view
* @param {number} options.removeWhenNoInMapViewPadding View padding for auto show/hide, default 500
* @param {number} options.clickTolerance Click tolerance, inherits map default
* @param {string} options.customImage Custom image URL/base64/svg
* @param {number} options.customImageWidth Custom image width
* @param {number} options.customImageHeight Custom image height
* @param {string} options.customHtml Custom HTML element JS code
* @param {string} options.popupHtml Popup HTML content
* @param {[number,number]} options.popupOffset Popup XY offset
* @param {string} options.popupAnchor Popup anchor, same as anchor
* @param {boolean} options.closeButton Show close button
* @param {boolean} options.closeOnClick Close popup on map click
* @param {boolean} options.closeOnMove Close popup on map move
* @param {boolean} options.focusAfterOpen Focus first focusable in popup
* @param {string} options.className CSS class for popup
* @param {string} options.maxWidth Popup max width, default'240px'
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Create marker layer layer19 from random GeoJSON
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createMarkerDataLayer(geojson, {
* layerName: 'layer19'
* });
* console.log(layer);
*/
async createMarkerDataLayer(data, options) {
options = options || {};
let type = "marker";
if (options.height) {
options.height = this.map.geoLengthToHeight(options.height);
}
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
color: '#3FB1CE',
closeButton: true,
closeOnClick: true,
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createMarkerDataLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Remove a function button
* @param {string} name Button name
* @example
* // Remove function button with name "Zoom in one level"
* this.removeButtonItem("Zoom in one level")
*/
removeButtonItem(name, func) {
this.context.ui.removeButtonItem(name, func);
// Add undo record
this.context.ui.addUndoRecord(() => {
this.addButtonItem(name, func);
});
}
/**
* Capture rectangle coordinate
* @param {boolean} isSnap Enable snap, default false
* @returns {Promise<number[] | undefined>} Returns [x1,y1,x2,y2] or undefined if cancelled
* @example
* // Capture rectangle coordinate
* const rect = await this.actionDrawRectangle();
* if (!rect) return;
* // Rectangle bottom-left
* const leftBottomX = rect[0];
* const leftBottomY = rect[1];
* // Rectangle top-right
* const rightTopX = rect[2];
* const rightTopY = rect[3];
* console.log(leftBottomX, leftBottomY, rightTopX, rightTopY);
*/
async actionDrawRectangle(isSnap) {
this.context.tools.logInfo("Please draw a rectangle on the map")
let snapObj = {};
if (isSnap) {
// Snap to DWG points
vjcommon.getMapSnapPoints(this.map, snapObj);
}
let drawRect = await vjmap.Draw.actionDrawRectangle(this.map, isSnap ? {
api: {
getSnapFeatures: snapObj
}
} : {});
if (drawRect.cancel) {
return; // Cancelled
}
let co = drawRect.features[0].geometry.coordinates[0];
co = this.map.fromLngLat(co);
return [co[0].x, co[0].y, co[2].x, co[1].y];
}
/**
* Get current map zoom
* @returns {number} Current map zoom level
*/
getZoom() { this.map.getZoom() }
/**
* Create rotating halo data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {string} options.text Text to display, optional, default "index ${props.index}"
* @param {number} options.minZoom Min zoom to display, optional, default 0
* @param {number} options.maxZoom Max zoom to display, optional, default 24
* @param {boolean} options.isDrawLinePoint Include line points when drawing, optional, default false
* @param {number} options.width Pixel width, optional, default 24
* @param {number} options.markerHeight Pixel height, optional
* @param {string} options.color1 Color value 1, optional
* @param {string} options.color2 Color value 2, optional
* @param {number} options.textFontSize Font size, optional
* @param {string} options.textColor Text color, optional
* @param {boolean} options.draggable Draggable, optional, default false
* @param {string} options.animationType Animation type, optional: MAP_ANIMATION_NONE, MAP_ANIMATION_BOUNCE, MAP_ANIMATION_DROP
* @param {number} options.rotation Rotation angle, optional
* @param {string} options.rotationAlignment Rotation alignment, optional: auto, map, viewport
* @param {string} options.pitchAlignment Pitch alignment, optional: auto, map, viewport
* @param {number} options.height Elevation value, optional
* @param {number} options.scaleMaxZoom Max zoom for scaling, optional
* @param {boolean} options.removeWhenNoInMapView Auto show/hide in view, optional
* @param {number} options.removeWhenNoInMapViewPadding Padding for auto show/hide, optional, default 500
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Create rotating halo layer from random GeoJSON in map bounds, layer name layer23
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createRotatingMarkerLayer(geojson, {
* layerName: 'layer23'
* });
* console.log(layer);
*/
async createRotatingMarkerLayer(data, options) {
options = options || {};
let type = "rotatingMarker";
if (options.height) {
options.height = this.map.geoLengthToHeight(options.height);
}
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
text: "ID:${props.index}",
width: 80,
markerHeight: 80,
color1: "#ff0000",
color2: "#ffff00",
textFontSize: 15,
textColor: "#00ffff",
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createRotatingMarkerLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Get map metadata
* @param {object | object[]} options Parameters, optional
* @param {string} [options.mapid] Map ID. Optional, empty means current opened map
* @param {string} [options.version] Map version, uses current when empty.
* @returns {Promise<Object>} Returns map metadata, undefined on failure. Fields:
* @example
* // Get metadata for map sys_hello
* let res = await this.metadata({
* mapid: "sys_hello"
* });
*/
async metadata(options) {
let svc = this.map.getService();
options = options || {};
let res = await svc.metadata(options.mapid, options.version);
if (res) {
res.bounds = vjmap.GeoBounds.fromString(res.bounds).toArray();
res.dbBounds = vjmap.GeoBounds.fromString(res.dbBounds).toArray();
res.drawBounds = vjmap.GeoBounds.fromString(res.drawBounds).toArray();
res.initBounds = vjmap.GeoBounds.fromString(res.initBounds).toArray();
if (res.clipbounds) {
res.clipbounds = vjmap.GeoBounds.fromString(res.clipbounds).toArray();
} else {
res.clipbounds = null;
}
res.layouts = res.layouts.split(", ");
}
return res;
}
/**
* Create temporary map from fileid
* @param {string} fileid File ID
* @param {object} options Other params (optional):
* @param {boolean} [options.GeomRender] Geometry render mode. false=memory, true=geometry render. Default false
* @param {number} [options.expireTime] Auto-delete after idle minutes. Default 5
* @param {boolean} [options.isVisible] Visible. If true, listable via ListMaps. Default false
* @param {boolean} options.lightTheme Use light theme. Empty = dark theme
* @returns {string} Created map ID
* @example
* // Create temporary map from fileid "c0febd8e49c7"
* let mapid = await this.createTempMap("c0febd8e49c7");
* console.log(mapid);
*/
async createTempMap(fileid, options) {
options = options || {};
let svc = this.map.getService().clone();
let mapid = vjmap.getTempMapId(options.expireTime || 5, options.isVisible);
// JS code
let res = await svc.updateMap({
mapid: mapid,
fileid: fileid,
mapopenway: options.GeomRender === true ? vjmap.MapOpenWay.GeomRender : vjmap.MapOpenWay.Memory,
style: options.lightTheme ? vjmap.openMapLightStyle() : vjmap.openMapDarkStyle(),
})
return res.mapid;
}
/**
* Create line data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {string} options.lineColor Line color, optional, default '#ff0000'
* @param {number} options.lineWidth Line width, optional, default 1
* @param {number} options.lineOpacity Line opacity, optional, 0-1
* @param {number} options.lineGapWidth Line gap width, optional, default 1, pixels
* @param {number} options.lineOffset Line offset, optional. Positive=right/inward, negative=left/outward
* @param {number} options.lineBlur Line blur, optional, must be less than line width
* @param {[number,number]} options.lineDasharray Dash pattern, optional, e.g. [1,1]
* @param {[number,number]} options.lineTranslate Translate [x,y], optional, negative=left/up, pixels
* @param {string} options.lineCap Line cap: 'butt', 'round', 'square'
* @param {string} options.lineJoin Line join: 'bevel', 'round', 'miter'
* @param {string} options.linePattern Line pattern, optional, must match preloaded image, width power of 2
* @param {string} options.lineGradient Line gradient, optional
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Create line layer from random GeoJSON in map bounds, layer name layer17
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createLineLayer(geojson, {
* layerName: 'layer17'
* });
* console.log(layer);
*/
async createLineLayer(data, options) {
options = options || {};
let type = "line";
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
lineColor: '#ff0000',
lineWidth: 1,
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createLineLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Measure area
* @example
* // Measure area
* await this.measureArea();
*/
measureArea() {
vjcommon.runMeasureCmd(this.map, "measureArea");
}
/**
* Set map theme: dark, light, or high-tech
* @param {string} theme Theme: "dark", "light", "high-tech"
* @example
* // Example 1
* // Set dark theme
* await this.setMapTheme("dark");
* // Example 2
* // Set light theme
* await this.setMapTheme("light");
* // Example 3
* // Set high-tech theme
* await this.setMapTheme("high-tech");
*/
async setMapTheme(theme) {
if (!this.context.global.updateMapStyleObj) {
this.context.global.updateMapStyleObj = vjcommon.createUpdateMapStyleObj(this.map);
}
const updateMapStyleObj = this.context.global.updateMapStyleObj;
let style = {
backcolor: theme === "light" ? 0xffffff : 0,
};
if (theme === "high-tech") {
style.expression = "var color := gFilterCustomTheme(gInColorRed, gInColorGreen, gInColorBlue, 200, 200, 0.1);gOutColorRed[0] := gRed(color);gOutColorGreen[0] := gGreen(color);gOutColorBlue[0] := gBlue(color);";
}
const curStyle = {...updateMapStyleObj.getCurStyle()};
await updateMapStyleObj.updateStyle(style)
// Add undo record
this.context.ui.addUndoRecord(async () => {
await updateMapStyleObj.updateStyle(curStyle)
});
}
/**
* Set map max/min zoom level
* @param {object} options Object with maxZoom and minZoom
* @param {number} options.maxZoom Max zoom to set (optional)
* @param {number} options.minZoom Min zoom to set (optional)
* @example
* this.setMaxMinZoom({maxZoom: 10}); // Set max zoom to 10
*/
setMaxMinZoom(options) {
options = options || {};
if (options.maxZoom !== undefined) {
let oldMaxZoom = this.map.getMaxZoom(); // Get current max zoom
this.map.setMaxZoom(options.maxZoom); // Set new max zoom
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.setMaxZoom(oldMaxZoom);
});
}
if (options.minZoom !== undefined) {
let oldMinZoom = this.map.getMinZoom(); // Get current min zoom
this.map.setMinZoom(options.minZoom); // Set new min zoom
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.setMinZoom(oldMinZoom);
});
}
}
/**
* Get custom data from server
* @param {string | string[]} key Key name(s), array for batch.
* @param {object} options retDataType: empty=data+props, "value"=data only, "prop"=props only. contentType: empty=json, "image"=image
* @returns {Promise<object>} Returns {data, prop, status}
* @example
* // Get custom data for key testKey
* let data = await this.getCustomDataServer("testKey");
* console.log(data);
* // Batch get custom data for keys testKey1, testKey2
* let dataArray = await this.getCustomDataServer(["testKey1", "testKey2"]);
* console.log(dataArray);
*/
async getCustomDataServer(key, options) {
let svc = this.map.getService();
return await svc.getCustomData(key, options);
}
/**
* Set whether map shows line width
* @param {boolean} isShowLineWidth Whether to show line width
* @example
* // Show line width
* await this.setMapShowLineWidth(true);
* // Hide line width
* await this.setMapShowLineWidth(false);
*/
async setMapShowLineWidth(isShowLineWidth) {
if (!this.context.global.updateMapStyleObj) {
this.context.global.updateMapStyleObj = vjcommon.createUpdateMapStyleObj(this.map);
}
const updateMapStyleObj = this.context.global.updateMapStyleObj;
const oldLineweight = updateMapStyleObj.getCurStyle()?.lineweight;
await updateMapStyleObj.updateStyle({
lineweight: isShowLineWidth ? 1 : 0
})
// Add undo record
this.context.ui.addUndoRecord(async () => {
await updateMapStyleObj.updateStyle({
lineweight: oldLineweight
})
});
}
/**
* Delete map
* @param {object} options Parameters
* @param {string} options.mapid Map ID
* @param {string} options.version Version. Use "*" to delete all versions
* @param {number} [options.retainVersionMaxCount] When deleting all, max latest versions to retain. Optional
* @returns {Promise<boolean>} true=success, false=failure
* @example
* // Example 1
* // Delete all versions of map hello
* let res1 = await this.deleteMap({
* mapid: "hello",
* version: "*"
* });
* console.log(res1);
* // Example 2
* // Delete v1 version of map hello
* let res2 = await this.deleteMap({
* mapid: "hello",
* version: "v1"
* });
* console.log(res2);
*/
async deleteMap(options) {
let svc = this.map.getService();
let res = await svc.cmdDeleteMap(options.mapid, options.version, options.retainVersionMaxCount);
return res && res.status;
}
/**
* Create breathing aperture point data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {string} options.text Text to display, optional, default "index ${props.index}"
* @param {number} options.minZoom Min zoom to display, optional, default 0
* @param {number} options.maxZoom Max zoom to display, optional, default 24
* @param {boolean} options.isDrawLinePoint Include line points when drawing, optional, default false
* @param {number} options.width Pixel width, optional, default 24
* @param {number} options.markerHeight Pixel height, optional
* @param {string} options.color1 Color value 1, optional
* @param {string} options.color2 Color value 2, optional
* @param {number} options.textFontSize Font size, optional
* @param {string} options.textColor Text color, optional
* @param {boolean} options.draggable Draggable, optional, default false
* @param {string} options.animationType Animation type, optional: MAP_ANIMATION_NONE, MAP_ANIMATION_BOUNCE, MAP_ANIMATION_DROP
* @param {number} options.rotation Rotation angle, optional
* @param {string} options.rotationAlignment Rotation alignment, optional: auto, map, viewport
* @param {string} options.pitchAlignment Pitch alignment, optional: auto, map, viewport
* @param {number} options.height Elevation value, optional
* @param {number} options.scaleMaxZoom Max zoom for scaling, optional
* @param {boolean} options.removeWhenNoInMapView Auto show/hide in view, optional
* @param {number} options.removeWhenNoInMapViewPadding Padding for auto show/hide, optional, default 500
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Create breathing aperture layer from random GeoJSON, layer name layer6
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createBreathingMarkerLayer(geojson, {
* layerName: 'layer6',
* text: 'Index: ${props.index}'
* });
* console.log(layer);
*/
async createBreathingMarkerLayer(data, options) {
options = options || {};
let type = "breathingMarker";
if (options.height) {
options.height = this.map.geoLengthToHeight(options.height);
}
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
"width": 80,
"markerHeight": 80,
"color1": "#ff0000",
"color2": "#ffff00",
"textFontSize": 15,
"textColor": "#00ffff",
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createBreathingMarkerLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Capture a point coordinate
* @param {boolean} isSnap Enable snap, default false
* @returns {Promise<[number,number] | undefined>} Returns [x,y] or undefined if cancelled
* @example
* const pt = await this.actionDrawPoint(true); // Capture point with snap
* if (!pt) return;
* // Point coordinates
* const x = pt[0];
* const y = pt[1];
* console.log(x, y);
*/
async actionDrawPoint(isSnap) {
this.context.tools.logInfo("Please draw a point on the map")
let snapObj = {};
if (isSnap) {
// Snap to DWG points
vjcommon.getMapSnapPoints(this.map, snapObj);
}
let drawPoint = await vjmap.Draw.actionDrawPoint(this.map, isSnap ? {
api: {
getSnapFeatures: snapObj
}
} : {});
if (drawPoint.cancel) {
return; // Cancelled
}
let co = drawPoint.features[0].geometry.coordinates;
co = this.map.fromLngLat(co);
return [co.x, co.y];
}
/**
* Update data layer by layer name and data content
* @param {string} layerName Layer name, required
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @example
* // Update layer1 data with random GeoJSON
* let layerName = 'layer1';
* let geojson = this.randomGeojsonInMapBounds();
* this.updateDataLayerSource(layerName, geojson);
*/
updateDataLayerSource(layerName, data) {
let item = this.context.global.layers.find(layer => layer.layerName == layerName);
if(item) {
item.data = data;
let oldData = {...item.data};
let layerId = item.layerId;
let sourceId = item.sourceId;
if (item.layer && typeof(item.layer.getLayerId) == "function") {
layerId = item.layer.getLayerId();
let style = this.map.getStyle();
let layer = style.layers.find(layer => layer.id == layerId);
if (layer && layer.source) {
sourceId = layer.source;
}
}
this.map.setData(sourceId, this.map.toLngLat(data));
// Add undo record
this.context.ui.addUndoRecord(() => {
this.updateDataLayerSource(layerName, oldData);
});
}
}
/**
* Transform GeoJSON by four parameters
* @param {Object} data GeoJSON to transform, required
* @param {Object} transform Four params: {dx, dy}, {scale}, {rotate} (radians), required
* @param {boolean} isLngLatCoord If input is lng/lat. false=CAD coords, true=map lng/lat. Optional
* @returns {Object} Transformed GeoJSON
* @example
* // Transform GeoJSON: translate [100,200], scale 0.5, rotate 0.5 rad
* let newGeojson = this.geojsonTransform(geojson, {dx: 100, dy: 200, scale: 0.5, rotate: 0.5});
* console.log(newGeojson);
*/
geojsonTransform(data, transform, isLngLatCoord) {
return vjmap.transform.convert(data, (pt) => {
let point = isLngLatCoord === true ? this.map.fromLngLat(vjmap.geoPoint(pt)) : vjmap.geoPoint(pt);
let newPoint = vjmap.coordTransfromByFourParamter(point, {
dx: transform.dx || 0,
dy: transform.dy || 0,
scale: transform.scale || 1,
rotate: transform.rotate || 0
});
return isLngLatCoord === true ? this.map.toLngLat(newPoint) : [newPoint.x, newPoint.y];
});
}
/**
* Capture 3D polyline
* @param {object} options Parameters
* @param {number} options.searchRadiusRatio Text search radius multiplier, default 2
* @param {boolean} isSnap Enable snap for drawn points, default true
* @param {boolean} isSnapMap Enable snap to map points, default false
* @example
* // Capture 3D polyline
* await this.pick3dPolylines();
*/
async pick3dPolylines(options) {
// Capture 3D line - auto-search text for elevation
// Coords as [x,z,y], use for 3D pipe generation
// https://vjmap.com/map3d/demo/#/demo/this.map/builtin/objects/03builtinobjtubepath
options = options || {};
let svc = this.map.getService();
let mapAllTexts;
let textMeanHeight = 0; // Average text height in drawing
const getAllTexts = async ()=> {
if (mapAllTexts) return mapAllTexts; // If already fetched
// Entity type ID to name mapping
const { entTypeIdMap } = await svc.getConstData();
const getTypeNameById = name => Object.keys(entTypeIdMap).find(e => entTypeIdMap[e] === name);
let queryTextEntTypeId = getTypeNameById("AcDbText"); // Single-line text
let queryMTextEntTypeId = getTypeNameById("AcDbMText"); // Multi-line text
let queryAttDefEntTypeId = getTypeNameById("AcDbAttributeDefinition"); // Attribute definition
let queryAttEntTypeId = getTypeNameById("AcDbAttribute"); // Attribute text
let query = await svc.conditionQueryFeature({
condition: `name='${queryTextEntTypeId}' or name='${queryMTextEntTypeId}' or name='${queryAttDefEntTypeId}' or name='${queryAttEntTypeId}'`, // WHERE clause, see "Server condition and expression query" docs
fields: "objectid,envelop,text",
geom: true,
limit: 100000 // Large limit to fetch all. Default 100 if omitted
})
if (query.error) {
this.map.logInfo(query.error, "error");
return;
} else {
mapAllTexts = []
textMeanHeight = 0
if (query.recordCount > 0) {
for(let i = 0; i < query.recordCount; i++) {
let bounds = this.map.getEnvelopBounds(query.result[i].envelop);
mapAllTexts.push({
position: bounds.center(),
text: query.result[i].text
});
textMeanHeight += bounds.height()
}
textMeanHeight = textMeanHeight / query.recordCount
}
}
return mapAllTexts
}
function isNumeric(value) {
const regex = /^[0-9]*\.?[0-9]+$/; // Match number with optional decimal
return regex.test(value);
}
let snapObj = {};
const collect3Dlines = async () => {
// Send ESC to map to cancel. Cancel previous capture if any
this.map.fire("keyup",{keyCode:27})
this.map.logInfo("Draw line on map, press Enter or ConfirmEnd in right-click menu")
let elevations = []
if (options.isSnapMap && !snapObj.features) {
vjcommon.getMapSnapPoints(this.map, snapObj);
}
let line = await vjmap.Draw.actionDrawLineSting(this.map, {
snap: options.isSnap === false ? false : true,
api: {
getSnapFeatures: snapObj
},
updatecoordinate: async (e) => {
if (!e.lnglat) return;
if (e.isClick) {
let curIdx = e.feature.coordinates.length - 1
const co = this.map.fromLngLat(e.feature.coordinates[curIdx]);
// Get all text near current point
let allTexts = await getAllTexts()
let radius = textMeanHeight * (options.searchRadiusRatio || 2); // Search radius = 2x text height, adjust as needed
// Filter by is-numeric first, then by distance
let texts = allTexts.filter(item => isNumeric(item.text)).filter(item => vjmap.geoPoint(co).distanceTo(item.position) <= radius)
texts = texts.map(t => t.text); // Get content only
// Show input dialog and get user input
let elevation = await this.context.ui.selectSuggestionValue(texts, {mustNumber: true, title: "Select or enter elevation"})
if (elevation === undefined) {
// Cancel capture
this.map.fire("keyup",{keyCode:27})
return;
}
elevations.push(elevation)
}
}, contextMenu: (e) => {
new vjmap.ContextMenu({
event: e.event.originalEvent,
theme: "dark", //light
width: "250px",
items: [
{
label: 'Confirm',
onClick: () => {
// Send Enter to map
this.map.fire("keyup", { keyCode: 13 })
}
},
{
label: 'Cancel',
onClick: () => {
// Send ESC to map to cancel
this.map.fire("keyup", { keyCode: 27 })
}
}, {
label: 'Delete last point',
onClick: () => {
// Send Backspace to delete last point
this.map.fire("keyup", { keyCode: 8 })
elevations.pop(); // Remove last element
}
}, {
label: 'End',
onClick: () => {
// Send ESC to map to cancel
this.map.fire("keyup", { keyCode: 27 })
}
}
]
});
}
})
if (line.cancel) {
return // Cancelled
}
let feature = line.features[0]
feature.properties.elevations = elevations;
feature.properties.typename = "3dpolyline"
feature.properties.line_width = 12;
feature.properties.color = vjmap.randomColor()
this.map.getDrawLayer().add(feature)
if (options.isSnap !== false) {
// Add drawn line to snap
snapObj.features = snapObj.features || [];
snapObj.features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: feature.geometry.coordinates
}
});
}
}
this.addButtonItem("Start collection", async () => {
await collect3Dlines()
})
this.addButtonItem("Get 3D line data", async () => {
let data = this.map.getDrawLayer().getAll();
data = this.map.fromLngLat(data);
let features = data.features.filter(item => item.properties.typename === "3dpolyline") || [];
let result = [];
for(let i = 0; i < features.length; i++) {
let coords = features[i].geometry.coordinates;
let elevations = features[i].properties.elevations;
let data = coords.map((co, idx) => [co[0], co[1], elevations[idx]]);
result.push({
paths: data,
name: i
})
}
await this.showDataInfoDialog(result, false, "3D line data")
})
this.addButtonItem("Exit collection", async () => {
// Cancel capture
this.map.fire("keyup",{keyCode:27})
let data = this.map.getDrawLayer().getAll();
data.features = data.features.filter(item => item.properties.typename !== "3dpolyline")
this.map.getDrawLayer().set(data);
this.removeButtonItem("Start collection")
this.removeButtonItem("Get 3D line data")
this.removeButtonItem("Exit collection")
})
}
/**
* Generate QR code image from content
* @param {string} content QR code content
* @param {object} options Options
* @param {number} options.size QR size, default 256
* @param {boolean} options.disableOpen Return URL only, don't open image, default false
* @returns {Promise<string>} QR code image URL
* @example
* // Generate QR code for "https://www.vjmap.com"
* let url = await this.qrcodeUrl("https://www.vjmap.com");
* console.log(url);
*/
async qrcodeUrl(content, options) {
options = options || {};
let svc = this.map.getService();
let res = await svc.qrcodeUrl(content, options.size);
if (options.disableOpen) return res;
window.open(res);
return res;
}
/**
* Delete a point marker
* @param {string} id Marker ID
* @example
* this.deleteMarker("marker_1"); // Delete marker with id marker_1
*/
deleteMarker(id) {
let markers = this.map.getMarkers();
let marker = markers.find(m => m.id == id);
if (marker) {
let point = this.map.fromLngLat(marker.getLngLat());
let oldOptions = {};
oldOptions.color = marker._color;
oldOptions.data = marker.data;
oldOptions.draggable = marker.isDraggable();
oldOptions.rotation = marker.getRotation();
if (marker.getPopup()) {
marker.getPopup().remove();
}
marker.remove()
// Add undo record
this.context.ui.addUndoRecord(() => {
this.addMarker(point, oldOptions);
});
}
}
/**
* Convert CAD geometry coords to map lng/lat
* @returns {[number, number]} [lng, lat]
* @example
* // Convert CAD coords [1000, 2000] to lng/lat
* let point = this.geoPointToLngLat([1000, 2000]);
* let lng = point[0];
* let lat = point[1];
* console.log(lng, lat);
*/
geoPointToLngLat(pt) {
let lngLat = this.map.toLngLat(pt);
let lng = lngLat[0];
let lat = lngLat[1];
return [lng, lat];
}
/**
* Add fullscreen control
* @param {string} options.container Fullscreen container, optional
* @param {'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'} options.position Control position, optional, default 'top-right'
* @example
* // Add fullscreen control, top-left position
* this.addMapFullscreenControl({ position: 'top-left' });
*/
addMapFullscreenControl(options) {
options = options || {};
this.context.global.controls = this.context.global.controls || {};
if (this.context.global.controls.fullscreenControl) {
this.map.removeControl(this.context.global.controls.fullscreenControl.control);
}
if (options.container) {
options.container = document.querySelector(options.container);
}
let fullscreenControl = new vjmap.FullscreenControl(options);
this.map.addControl(fullscreenControl, options.position);
this.context.global.controls.fullscreenControl = {
control: fullscreenControl,
position: options.position,
options: options
};
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.removeControl(fullscreenControl);
delete this.context.global.controls.fullscreenControl;
});
}
/**
* Manually draw a polygon interactively
* @param {object} options Config params, optional
* @param {boolean} options.isSnap Enable snap, optional, default false
* @param {string} options.color Color #RRGGBB, optional
* @param {boolean} options.isFill Fill, default true, optional
* @param {number} options.opacity Opacity, default 1, optional
* @param {number} options.extrusionHeight Extrusion height, optional
* @param {number} options.data Data value, optional
* @returns {Promise<Object | undefined>} Drawn entity or undefined if cancelled. Object includes:
* @example
* // Manually draw a polygon
* const polygon = await this.drawPolygon({
* isSnap: true,
* color: "#FF0000",
* isFill: true,
* opacity: 0.8
* });
* if (!polygon) return;
* console.log("Polygon ID:", polygon.id);
* console.log("Polygon points:", polygon.points);
*/
async drawPolygon(options) {
let points = await this.actionDrawPolygon(options.isSnap);
if (!points) return;
return this.createPolygon(points, options);
}
/**
* Create point symbol cluster data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @param {string} options.iconImage Icon name for image background
* @param {number} options.iconRotate Icon rotation angle, clockwise, degrees, default 0
* @param {[number,number]} options.iconOffset Icon offset from anchor, [x,y], positive = right/down
* @param {string} options.iconAnchor Icon anchor position, values: "center", "left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"
* @param {string} options.iconRotationAlignment Icon rotation alignment, values: "map", "viewport", "auto"
* @param {string} options.iconPitchAlignment Icon pitch alignment, values: "map", "viewport", "auto"
* @param {boolean} options.iconAllowOverlap Allow icon overlap, default false
* @param {number} options.iconOpacity Icon opacity 0-1, default 1
* @param {boolean} options.iconIgnorePlacement Ignore collision detection, default false
* @param {boolean} options.iconOptional Icon overlap option, default false
* @param {number} options.iconSize Icon scale, default 1
* @param {string} options.iconTextFit Icon text fit, values: "none", "width", "height", "both"
* @param {[number,number,number,number]} options.iconTextFitPadding Icon text fit padding [top, right, bottom, left]
* @param {number} options.iconPadding Icon padding, default 1px
* @param {boolean} options.iconKeepUpright Keep icon upright, default false
* @param {string} options.iconColor SDF icon color, default"#000000"
* @param {string} options.iconHaloColor SDF icon halo color, default"rgba(0, 0, 0, 0)"
* @param {number} options.iconHaloWidth Icon halo width, default 0
* @param {number} options.iconHaloBlur Icon halo blur, default 0
* @param {[number,number]} options.iconTranslate Icon translate [x,y]
* @param {string} options.iconTranslateAnchor Icon translate anchor, values: "map", "viewport"
* @param {string|Expression} options.textField Text label content
* @param {string[]} options.textFont Text font, default["Open Sans Regular","Arial Unicode MS Regular"]
* @param {number} options.textSize Text size, default 16
* @param {boolean} options.textAllowOverlap Allow text overlap, default false
* @param {string} options.textColor Text color, default"#000000"
* @param {number} options.textOpacity Text opacity 0-1, default 1
* @param {[number,number]} options.textOffset Text offset [x,y]
* @param {number} options.textRotate Text rotation angle
* @param {number} options.textPadding Text padding, default 2
* @param {boolean} options.textKeepUpright Keep text upright, default true
* @param {number} options.textMaxWidth Text max width, default 10em
* @param {number} options.textLineHeight Text line height, default 1.2
* @param {number} options.textLetterSpacing Letter spacing, default 0
* @param {string} options.textJustify Text justify, values: "auto", "left", "center", "right"
* @param {string} options.textAnchor Text anchor position
* @param {string} options.textRotationAlignment Text rotation alignment
* @param {string} options.textPitchAlignment Text pitch alignment
* @param {number} options.textMaxAngle Max angle between chars, default 45°
* @param {string[]} options.textWritingMode Text writing mode
* @param {string} options.textTransform Text transform, values: "none", "uppercase", "lowercase"
* @param {number} options.textRadialOffset Text radial offset
* @param {string[]} options.textVariableAnchor Text variable anchor array
* @param {boolean} options.textIgnorePlacement Ignore text placement
* @param {boolean} options.textOptional Text optional
* @param {string} options.textHaloColor Text halo color
* @param {number} options.textHaloWidth Text halo width
* @param {number} options.textHaloBlur Text halo blur
* @param {[number,number]} options.textTranslate Text translate
* @param {string} options.textTranslateAnchor Text translate anchor
* @param {string} options.symbolPlacement Symbol placement, values: "point", "line", "line-center"
* @param {number} options.symbolSpacing Symbol spacing, default 250
* @param {boolean} options.symbolAvoidEdges Avoid edge collision
* @param {string} options.symbolZOrder Symbol z-order, values: "auto", "viewport-y", "source"
* @param {number} options.clusterMaxZoom Cluster max zoom, default 10
* @param {number} options.clusterRadius Cluster radius in pixels, default 60
* @param {Array<[number, string]>} options.outerColors Outer circle value/color [[value,color],...], default[[1000, 'rgba(253, 156, 115, 0.6)'], [100, 'rgba(241, 211, 87, 0.6)'], [0, 'rgba(181, 226, 140, 0.6)']]
* @param {number} options.outerCircleRadius Outer circle radius px, default 20
* @param {Array<[number, string]>} options.innerColors Inner circle value/color [[value,color],...], default[[1000, 'rgba(241, 128, 23, 0.6)'], [100, 'rgba(240, 194, 12, 0.6)'], [0, 'rgba(110, 204, 57, 0.6)']]
* @param {number} options.innerCircleRadius Inner circle radius px, default 15
* @param {number} options.clusterTextSize Cluster text size px, default 12
* @param {string} options.color Marker color, optional, default'#3FB1CE'
* @param {number[]} options.offset XY pixel offset [0,0], negative = left/up
* @param {number} options.rotation Marker rotation, clockwise, default 0
* @param {string} options.anchor Marker anchor:center、top、bottom、left、right、top-left、top-right、bottom-left、bottom-right, default center
* @param {boolean} options.draggable Draggable, optional, default false
* @param {string} options.animationType Animation: MAP_ANIMATION_NONE, MAP_ANIMATION_BOUNCE, MAP_ANIMATION_DROP
* @param {string} options.rotationAlignment Rotation alignment: auto, map, viewport, default auto
* @param {string} options.pitchAlignment Pitch alignment: auto, map, viewport, default auto
* @param {number} options.scale Scale, default 1, 41px x 27px
* @param {number} options.scaleMaxZoom Scale max zoom (0-24), auto-scale below
* @param {number} options.height Elevation, CAD units
* @param {boolean} options.removeWhenNoInMapView Auto show/hide in view
* @param {number} options.removeWhenNoInMapViewPadding View padding for auto show/hide, default 500
* @param {number} options.clickTolerance Click tolerance, inherits map default
* @param {string} options.customImage Custom image URL/base64/svg
* @param {number} options.customImageWidth Custom image width
* @param {number} options.customImageHeight Custom image height
* @param {string} options.customHtml Custom HTML element JS code
* @param {string} options.popupHtml Popup HTML content
* @param {[number,number]} options.popupOffset Popup XY offset
* @param {string} options.popupAnchor Popup anchor, same as anchor
* @param {boolean} options.closeButton Show close button
* @param {boolean} options.closeOnClick Close popup on map click
* @param {boolean} options.closeOnMove Close popup on map move
* @param {boolean} options.focusAfterOpen Focus first focusable in popup
* @param {string} options.className CSS class for popup
* @param {string} options.maxWidth Popup max width, default'240px'
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Create symbol cluster layer layer25 with icon _markerIcon_
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createSymbolClusterDataLayer(geojson, {
* layerName: 'layer25',
* iconImage: '_markerIcon_'
* });
* console.log(layer);
*/
async createSymbolClusterDataLayer(data, options) {
options = options || {};
let type = "symbolCluster";
if (options.height) {
options.height = this.map.geoLengthToHeight(options.height);
}
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
clusterMaxZoom: 10,
clusterRadius: 60,
outerColors: [[1000, 'rgba(253, 156, 115, 0.6)'], [100, 'rgba(241, 211, 87, 0.6)'], [0, 'rgba(181, 226, 140, 0.6)']],
outerCircleRadius: 20,
innerColors: [[1000, 'rgba(241, 128, 23, 0.6)'], [100, 'rgba(240, 194, 12, 0.6)'], [0, 'rgba(110, 204, 57, 0.6)']],
innerCircleRadius: 15,
clusterTextSize: 12,
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createSymbolClusterDataLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Get map center coordinates
* @returns {[number, number]} Map center coords as [x,y]
*/
getCenter() {
let center = this.map.fromLngLat(this.map.getCenter());
return [center.x, center.y];
}
/**
* Auto-extract table from map
* @returns {Promise<GeoJSON>} Returns extracted table GeoJSON, undefined on failure
* @example
* // Example 1
* // Auto-extract table from map
* let geojson = await this.extractTale();
* if (!geojson) {
* console.log("Table extraction failed");
* } else {
* console.log("Table extracted, GeoJSON:", geojson);
* }
* // Example 2
* // Manually select range for auto-extract table from map
* let res2 = await this.extractTale({
* isSelectBounds: true
* });
* console.log(res2);
*/
async extractTale(options) {
options = options || {};
let svc = this.map.getService();
options = {
mapid: options.mapid || svc.currentMapParam().mapid,
bounds: "",
condition: "",
debugInfo: [],
digit: 4,
tol: 0,
tableEdgeMinPoint: 12,
tableTextMinCount: 4,
cellMaxArea: 90,
cellEmptyRatio: 90,
unLinkLineRatio: 20,
tableMaxCellCount: 100000,
noHvLineSegCount: 0,
seachTableMost: false,
version: svc.currentMapParam()?.version,
layer: svc.currentMapParam()?.layer,
// Use expression query for memory, spatial query for geometry render
geom: svc.currentMapParam()?.mapopenway != vjmap.MapOpenWay.Memory,
...options,
};
if (options.isSelectBounds) {
// For manual mode, select area first
if (this.map.logInfo) this.map.logInfo("Please select rectangular area covering the table", "success", 10000);
// Draw rectangle on map as clip region
let snapObj = {}; // Snap entities
vjcommon.getMapSnapPoints(this.map, snapObj);
let res = await vjmap.Draw.actionDrawRectangle(this.map, {
api: {
getSnapFeatures: snapObj // Snap features assigned via features property
},
contextMenu: (e) => {
// Send Enter to map
this.map.fire("keyup", {keyCode:13})
}
});
if (res.cancel) {
return // Cancelled
}
// Get drawn coordinates
let rectCoord = this.map.fromLngLat(res.features[0].geometry.coordinates[0]);
// Get bottom-left and bottom-right points
let bounds = [rectCoord[0].x, rectCoord[0].y, rectCoord[2].x, rectCoord[2].y];
options.bounds = bounds.join(',');
}
if (Array.isArray(options.bounds)) {
options.bounds = JSON.stringify(options.bounds);
}
if (options.showSettingForm !== false) {
options = await this.showDataModifyDlg({
data: options,
title: "Auto-extract table settings",
showForm: true
})
if (!options) return;
}
let result = await svc.cmdExtractTale(options);
if (result && result.error) {
if (this.map.logInfo) this.map.logInfo(result.error, "error", 10000);
return;
}
let tables = result.tables || [];
let geoDatas = {
"type": "FeatureCollection",
"features": []
}
for(let t = 0; t < tables.length; t++) {
let rect = vjmap.GeoBounds.fromString(tables[t].rect);
let pts = rect.toPointArray();
pts.push(pts[0]); // Close
geoDatas.features.push(
{
"id": t,
"type": "Feature",
"properties": {
color: vjmap.randomColor(),
index: t,
data: tables[t],
tableDatas: tables[t].datas
},
"geometry": {
"coordinates": [pts],
"type": "Polygon"
}
})
}
return geoDatas;
}
/**
* Show message
* @param {string} message Message content, required
* @param {string} type Message type: warning, error, info, success, optional, default error
* @example
* // Example 1
* // Show error message
* this.showMessage("Error occurred", "error");
* // Example 2
* // Show success message
* this.showMessage("Completed", "success");
*/
async showMessage(message, type) {
return await this.context.ui.showMessage(message, type || "error");
}
/**
* Delete user custom data from server
* @param {string | string[]} key Key name(s), array for batch
* @param {boolean} isPrefix Delete all keys with prefix, default false
* @returns {Promise<boolean>} Returns true on success, false on failure
* @example
* // Delete user custom data key drawData from server
* await this.deleteCustomDataServer("drawData");
*/
async deleteCustomDataServer(key, isPrefix = false) {
let svc = this.map.getService();
let res = await svc.deleteCustomData(key, isPrefix);
return res?.status === "ok";
}
/**
* Create raster tile layer
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {number} options.rasterOpacity Image opacity 0-1, default 1
* @param {number} options.rasterHueRotate Hue rotation degrees, default 0
* @param {number} options.rasterBrightnessMin Min brightness 0-1, default 0
* @param {number} options.rasterBrightnessMax Max brightness 0-1, default 1
* @param {number} options.rasterSaturation Saturation -1 to 1, default 0
* @param {number} options.rasterContrast Contrast -1 to 1, default 0
* @param {number} options.rasterFadeDuration Tile fade duration ms, default 300
* @param {string} options.rasterResampling Resampling: linear, nearest, default linear
* @param {string} options.tileMapId Tile mapid, optional
* @param {string} options.tileMapVersion Tile version, optional
* @param {string} options.tileMapLayer Tile style layer name, optional
* @param {string|Array<string>} options.tiles Tile URL, supports variables {x},{y},{z},{serviceUrl},{accessToken}
* @param {number} options.tileSize Tile size, default 512
* @param {number[]} options.bounds Tile bounds [minx,miny,maxx,maxy], optional
* @param {string} options.scheme Tile scheme: xyz (Y from top), tms (Y from bottom), default xyz
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Create raster tile layer, tile mapid is sys_hello
* let layer = await this.createRasterLayer({
* tileMapId: "sys_hello",
* });
* console.log(layer);
*/
async createRasterLayer(options) {
options = options || {};
let type = "raster";
let randomId = vjmap.RandomID();
let sourceId = `source_${type}_${randomId}`;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
sourceId,
layerId,
...options,
type: type
}
if (options.tileMapId && !options.tiles) {
let layer = options.tileMapLayer;
if (!layer) {
let style = await this.map.getService().createStyle({
backcolor: this.map.getService().currentMapParam().darkMode ? 0x000000 : 0xFFFFFF
}, options.tileMapId, options.tileMapVersion);
layer = style.stylename
}
let tileUrl = this.map.getService().rasterTileUrl({
mapid: options.tileMapId,
version: options.tileMapVersion,
layer: layer,
})
options.tiles = tileUrl;
}
if (options.bounds) {
const p1 = this.map.toLngLat([options.bounds[0], options.bounds[1]]);
const p2 = this.map.toLngLat([options.bounds[2], options.bounds[3]]);
options.bounds = [p1[0], p1[1], p2[0], p2[1]];
}
let sourceData = {
type: "raster",
tiles: Array.isArray(options.tiles) ? options.tiles : [options.tiles],
tileSize: options.tileSize || 256
}
if (options.bounds) {
sourceData.bounds = options.bounds;
}
if (options.scheme) {
sourceData.scheme = options.scheme;
}
this.map.addRasterSource(sourceId, sourceData);
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data: sourceData, options, layer, visible: true, create: this.createRasterLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Show data panel with content
* @param {any} data Data content, required
* @param {object} options Data panel config, optional
* @param {string} options.title Data panel title, optional
* @param {number} options.width Data panel width, optional
* @param {number} options.height Data panel height, optional
* @param {number} options.left Data panel left, optional
* @param {number} options.top Data panel top, optional
* @param {string} options.position Data panel position, default top-left
* @param {boolean} options.showForm Show as form, default false
* @example
* // Show data panel with content, data is {"a":1,"b":2}, data panel title is "Data View", width 300, height 200, left 50, top 100, position top-left, shown as form
* this.showDataPanel({a:1,b:2}, {title: "Data View", width: 300, height: 200, left: 50, top: 100, position: "top-left", showForm: true})
*/
showDataPanel(data, options) {
options = options || {};
options.title = options.title || "Data View";
this.context.ui.setDataPanelContent(data, options);
}
/**
* Set map center coordinates
* @param {[number, number]} point Center point [x,y]
* @example
* this.setCenter([100, 20]); // Set center to x=100, y=20
*/
setCenter(point) {
let oldCenter = this.map.fromLngLat(this.map.getCenter()) ; // Get current center
this.map.setCenter(this.map.toLngLat(point)); // Set new center
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.setCenter(this.map.toLngLat(oldCenter));
});
}
/**
* Clear map data
* @param {object} options Options
* @param {boolean} [options.clearMarkers] Clear all markers
* @param {boolean} [options.clearDraw] Clear all draw layers
* @param {boolean} [options.clearOtherLayerData] Clear all data except base map
* @param {boolean} [options.clearAllData] Clear all map data
* @example
* // Example 1
* // Clear all markers
* this.clearMapData({clearMarkers: true});
* // Example 2
* // Clear all draw layers
* this.clearMapData({clearDraw: true});
* // Example 3
* // Clear all data except base map
* this.clearMapData({clearOtherLayerData: true});
* // Example 4
* // Clear all map data
* this.clearMapData({clearAllData: true});
*/
clearMapData(options) {
options = options || {};
if (options.clearMarkers === true) {
this.map.removeMarkers();
this.map.removePopups();
}
if (options.clearDraw === true) {
let drawLayer = this.map.getDrawLayer();
drawLayer.deleteAll();
}
if (options.clearOtherLayerData === true) {
this.map.removeMarkers();
this.map.removePopups();
// Remove all draw layers
// @ts-ignore
const controls = this.map._controls;
for(let k = controls.length - 1; k >= 0; k--) {
if (controls[k] instanceof vjmap.Draw.Tool) {
this.map.removeControl(controls[k])
}
}
let sources = this.map.getStyle().sources;
for(let item in sources) {
if (item != "highlight-source" && sources[item].type == "geojson")
{
// Remove geojson only, keep click-highlight layers
this.map.removeSourceEx(item);
}
}
}
if (options.clearAllData === true) {
if (this.map.hasVectorLayer()) {
this.map.disableVectorLayerHoverHighlight()
} else {
this.map.disableLayerClickHighlight();
}
// Remove all draw layers
// @ts-ignore
const controls = this.map._controls;
for(let k = controls.length - 1; k >= 0; k--) {
if (controls[k] instanceof vjmap.Draw.Tool) {
this.map.removeControl(controls[k])
}
}
let sources = this.map.getStyle().sources;
for(let item in sources) {
// @ts-ignore
this.map.removeSourceEx(item);
}
this.map.removeMarkers();
this.map.removePopups();
}
}
/**
* Move layer
* @param {string} layerName Layer name, required
* @param {object} options Other params, optional
* @param {string} options.beforeLayerName Move before this layer, optional
* @param {string} options.topMost Move to top, optional
* @param {string} options.bottomMost Move to bottom, optional
* @example
* // Example 1
* // Move layer1 to top
* let layerName = 'layer1';
* this.moveLayer(layerName, {
* topMost: true
* });
* // Example 2
* // Move layer1 to bottom
* this.moveLayer(layerName, {
* bottomMost: true
* });
* // Example 3
* // Move layer1 before layer2
* this.moveLayer(layerName, {
* beforeLayerName: 'layer2'
* });
*/
moveLayer(layerName, options) {
let item = this.context.global.layers.find(layer => layer.layerName == layerName);
if(item) {
item.visible = true;
let layerId = item.layerId;
if (item.layer && typeof(item.layer.getLayerId) == "function") {
layerId = item.layer.getLayerId();
}
let layers = this.map.getStyle().layers;
let oldBeforeLayerId;
let index = layers.findIndex(item => item.id == layerId);
if (index < layers.length - 1) {
oldBeforeLayerId = layers[index + 1].id;
}
if (options.topMost) {
this.map.moveLayer(layerId);
} else if (options.bottomMost) {
this.map.moveLayer(layerId, layers[0].id);
} else if (options.beforeLayerName) {
let itemBefore = this.context.global.layers.find(layer => layer.layerName == options.beforeLayerName);
let beforeLayerId;
if (itemBefore) {
beforeLayerId = itemBefore.layerId;
if (itemBefore.layer && typeof(itemBefore.layer.getLayerId) == "function") {
beforeLayerId = itemBefore.layer.getLayerId();
}
}
this.map.moveLayer(beforeLayerId);
}
// Add undo record
this.context.ui.addUndoRecord(() => {
if (oldBeforeLayerId) {
this.map.moveLayer(layerId, oldBeforeLayerId);
}
});
}
}
/**
* Manually draw a line interactively
* @param {object} options Config params, optional
* @param {boolean} options.isSnap Enable snap, optional, default false
* @param {string} options.color Color #RRGGBB, optional
* @param {number} options.width Line width, default 1, optional
* @param {number} options.data Data value, optional
* @returns {Promise<Object | undefined>} Drawn entity or undefined if cancelled. Object includes:
* @example
* // Manually draw a line
* const line = await this.drawLineString({
* isSnap: true,
* color: "#FF0000",
* width: 2
* });
* if (!line) return;
* console.log("Line ID:", line.id);
* console.log("Line points:", line.points);
*/
async drawLineString(options) {
let points = await this.actionDrawLineString(options.isSnap);
if (!points) return;
return this.createLineString(points, options);
}
/**
* Get map max/min zoom
* @returns {number} Current max zoom
*/
getMaxZoom() { this.map.getMaxZoom() }
/**
* Remove map fullscreen control
* @example
* // Remove map fullscreen control
* this.removeMapFullscreenControl();
*/
removeMapFullscreenControl() {
this.context.global.controls = this.context.global.controls || {};
let options;
if (this.context.global.controls.fullscreenControl) {
options = {...this.context.global.controls.fullscreenControl.options}
this.map.removeControl(this.context.global.controls.fullscreenControl.control);
delete this.context.global.controls.fullscreenControl;
}
// Add undo record
this.context.ui.addUndoRecord(() => {
if (options) {
this.addMapFullscreenControl(options);
}
});
}
/**
* Show confirm dialog
* @param {string} message Dialog content, optional
* @param {string} title Dialog title, optional
* @param {string} type Dialog type: warning, error, info, success
* @returns {boolean} Returns true for OK, false for Cancel
* @example
* // Show confirm dialog with message "Delete?"
* let res = await this.messageBoxConfirm("Delete?", "Prompt", "warning");
* if (res) {
* console.log("Clicked OK")
* } else {
* console.log("Clicked Cancel")
* }
*/
async messageBoxConfirm(message, title, type) {
return await this.context.ui.confirm(message || "Confirm?", title || "Prompt", type || "warning");
}
/**
* Get map rotation angle
* @returns {number} Current map rotation
*/
getBearing() { this.map.getBearing() }
/**
* Get map service version and env info
* @example
* // Get map service version and env info, show returned data in data info dialog, title is map service version info, shown as form
* let res = await this.getMapServerInfo();
* if (res) {
* await this.showDataInfoDialog(res, true, "Map service version info");
* }
*/
async getMapServerInfo() {
let svc = this.map.getService();
const _url = svc.baseUrl() + "version";
let res = await svc._get(_url, {});
if (res && "data" in res) {
let data = res["data"];
return {
version: data.version,
os: data.os,
arch: data.arch
};
}
}
/**
* Set map raster base style
* @param {object} options Other params, optional
* @param {number} options.rasterOpacity Image opacity 0-1, default 1
* @param {number} options.rasterHueRotate Hue rotation degrees, default 0
* @param {number} options.rasterBrightnessMin Min brightness 0-1, default 0
* @param {number} options.rasterBrightnessMax Max brightness 0-1, default 1
* @param {number} options.rasterSaturation Saturation -1 to 1, default 0
* @param {number} options.rasterContrast Contrast -1 to 1, default 0
* @param {boolean} options.showSettingForm Show settings dialog, default true
* @example
* // Set map raster base style
* this.setMapRasterLayerStyle();
*/
async setMapRasterLayerStyle(options) {
options = options || {};
let showSettingForm = options.showSettingForm === undefined ? true : options.showSettingForm;
let oldStyle = {...this.map.getLayerStyle(this.map.getService().rasterLayerId())?.paint};
// Convert style to camelCase
let formattedOldStyle = {};
for (let key in oldStyle) {
let camelCaseKey = key.replace(/-([a-z])/g, (match, p1) => p1.toUpperCase());
formattedOldStyle[camelCaseKey] = oldStyle[key];
}
oldStyle = formattedOldStyle;
delete options.showSettingForm;
options = {
rasterOpacity: oldStyle.rasterOpacity || 1,
rasterHueRotate: oldStyle.rasterHueRotate || 0,
rasterBrightnessMin: oldStyle.rasterBrightnessMin || 0,
rasterBrightnessMax: oldStyle.rasterBrightnessMax || 1,
rasterSaturation: oldStyle.rasterSaturation || 0,
rasterContrast: oldStyle.rasterContrast || 0,
...options,
}
let svc = this.map.getService();
let layerId = svc.rasterLayerId();
const setRasterStyle = (data) => {
this.map.setRasterOpacity(layerId, data.rasterOpacity || 1);
this.map.setRasterHueRotate(layerId, data.rasterHueRotate || 0);
this.map.setRasterBrightnessMin(layerId, data.rasterBrightnessMin || 0);
this.map.setRasterBrightnessMax(layerId, data.rasterBrightnessMax || 1);
this.map.setRasterSaturation(layerId, data.rasterSaturation || 0);
this.map.setRasterContrast(layerId, data.rasterContrast || 0);
}
if (showSettingForm) {
let dataOptions = {
rasterOpacity: {
type:"slider",
field:"rasterOpacity",
title:"Image opacity",
value:options.rasterOpacity,
props:{
min: 0,
max: 1,
step: 0.01
}
},
rasterHueRotate: {
type:"slider",
field:"rasterHueRotate",
title:"Hue rotation",
value: options.rasterHueRotate,
props:{
min: 0,
max: 360
}
},
rasterBrightnessMin: {
type:"slider",
field:"rasterBrightnessMin",
title:"Min brightness",
value: options.rasterBrightnessMin,
props:{
min: 0,
max: 1,
step: 0.01
}
},
rasterBrightnessMax: {
type:"slider",
field:"rasterBrightnessMax",
title:"Max brightness",
value: options.rasterBrightnessMax,
props:{
min: 0,
max: 1,
step: 0.01
}
},
rasterSaturation: {
type:"slider",
field:"rasterSaturation",
title:"Saturation",
value: options.rasterSaturation,
props:{
min: -1,
max: 1,
step: 0.01
}
},
rasterContrast: {
type:"slider",
field:"rasterContrast",
title:"Contrast",
value: options.rasterContrast,
props:{
min: -1,
max: 1,
step: 0.01
}
},
}
options = await this.showDataModifyDlg({
data: dataOptions,
title: "Raster base style settings",
showForm: true,
applyCallback: (data) => {
setRasterStyle(data);
}
})
if (!options) {
// Restore style if user cancels
setRasterStyle(oldStyle);
return
}
}
setRasterStyle(options);
// Add undo record
this.context.ui.addUndoRecord(() => {
setRasterStyle(oldStyle);
});
}
/**
* Set map zoom level
* @param {number} zoom Zoom level to set
* @example
* this.setZoom(10); // Set zoom to 10
* // Zoom in one level
* let zoom = this.getZoom(); // Get original zoom
* zoom = zoom + 1; // Zoom in one level
* this.setZoom(zoom);
*/
setZoom(zoom) {
let oldZoom = this.map.getZoom(); // Get current zoom
this.map.setZoom(zoom) // Set new zoom
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.setZoom(oldZoom);
});
}
/**
* Measure coordinates
* @example
* // Measure coordinates
* await this.measureCoordinate();
*/
measureCoordinate() {
vjcommon.runMeasureCmd(this.map, "measureCoordinate");
}
/**
* Create contour lines from data
* @param {Object} options Config params
* @param {number[]} options.breaks Contour breaks
* @param {string} [options.model] Interpolation model: 'exponential', 'gaussian', 'spherical', default 'exponential',
* @param {number} [options.sigma2] Variance param, default 0
* @param {number} [options.alpha] Alpha prior, default 100
* @param {string[]} [options.colors] Contour fill colors
* @param {number} [options.width] Contour width, default 500
* @param {number} [options.contoursSize] Contour intervals, default 20
* @param {[number, number, number, number]} [options.extent] Extent to draw, auto from data
* @param {number} [options.maxHeight] Max extrusion height, default 100
* @param {[number, number]} [options.clipBounds] Clip bounds, optional
* @param {boolean} [options.drawSymbols] Draw original points, default false
* @param {boolean} [options.drawPolyline] Draw contour lines, default true
* @param {boolean} [options.drawPolygon] Draw contour polygon, default false
* @param {boolean} [options.drawFillExtrusion] Draw fill extrusion, default false
* @returns {Object} Returns object with source ID array and layer ID array {sourceIds: string[], layerIds: string[]}
* @example
* // Randomly generate 50 points within map bounds, assign random value 10-150 to each, draw contour lines from these points
* let points = this.randomPointInMapBounds(50);
* let data = points.map(p => ({x: p[0], y: p[1], value: Math.floor(Math.random() * 90) + 10}));
* let contour = await this.vectorContour({data});
* console.log(contour);
*/
async vectorContour(options) {
options = options || {};
options.data = options.data || [];
let dataset = {
"type" : "FeatureCollection",
"features" : []
};
let dataMinValue = 0;
let dataMaxValue = 0;
options.data = options.data.map(item => {
return Array.isArray(item) ? {
x: item[0],
y: item[1],
value: item[2]
} : item;
});
let minx, miny, maxx, maxy;
for (let i = 0; i < options.data.length; i++) {
if (i == 0) {
dataMinValue = options.data[i].value;
dataMaxValue = options.data[i].value;
minx = options.data[i].x;
miny = options.data[i].y;
maxx = options.data[i].x;
maxy = options.data[i].y;
} else {
if (options.data[i].value < dataMinValue) dataMinValue = options.data[i].value;
if (options.data[i].value > dataMaxValue) dataMaxValue = options.data[i].value;
if (options.data[i].x < minx) minx = options.data[i].x;
if (options.data[i].y < miny) miny = options.data[i].y;
if (options.data[i].x > maxx) maxx = options.data[i].x;
if (options.data[i].y > maxy) maxy = options.data[i].y;
}
let feature={
"type" : "Feature",
"properties" : {
"value" : options.data[i].value
},
"geometry" : {
"type" : "Point",
"coordinates" : this.map.toLngLat([options.data[i].x,options.data[i].y])
}
};
dataset.features.push(feature);
}
let extent = options.extent || vjmap.GeoBounds.fromArray([minx, miny, maxx, maxy]).scale(1.2).toArray();
let contoursSize = options.contoursSize || 20; // Contour interval count
let model = options.model || 'exponential'; // Model name
let alpha = options.alpha || 100; // Alpha value
let colors = options.colors || ["#006837", "#1a9850", "#66bd63", "#a6d96a", "#d9ef8b", "#ffffbf","#fee08b",
"#fdae61", "#f46d43", "#d73027", "#a50026"];
let propField = "value";
let maxHeight = options.maxHeight || this.map.pixelToHeight(100, this.map.getZoom()) ; // Set max extrusion height
let contours = [];
let isClipContour = false;
if (options.clipBounds) {
isClipContour = true;
options.clipBounds = vjmap.GeoBounds.fromArray(options.clipBounds);
}
for(let i = 0; i < contoursSize; i++) {
contours.push(dataMinValue + (dataMaxValue - dataMinValue) * i / (contoursSize - 1));
}
if (dataMinValue >= 10 && dataMaxValue > 100) {
// Round contour values by 10
for(let i = 0; i < contours.length; i++) {
contours[i] = (parseInt(contours[i]/10) * 10);
}
}
let interpolateInput = [], interpolateOutput = [];
for(let i = 0; i < colors.length; i++) {
interpolateInput.push(i / (colors.length - 1)); // Interpolate input 0-1
interpolateOutput.push(colors[i]) // Interpolate output colors
}
// Start web worker
let createContourWorker = vjmap.WorkerProxy(vjmap.vectorContour);
let { grid, contour, variogram } = await createContourWorker(this.map.fromLngLat(dataset), propField, contours, {
model: model || 'exponential', // 'exponential','gaussian','spherical', choose one, default exponential
sigma2: options.sigma2 || 0, // sigma2 variance param, z-distance of this data set; σ² likelihood reflects Gaussian process error, set manually; usually 0, other values may produce blank output
alpha: alpha, // Alpha prior value, may control diffusion range: smaller = larger range, noticeable with few points; with many evenly distributed points, changing has little effect; default 100
extent: extent, // Extent auto from data if omitted
width: options.width || 500 // Contour width in pixels
}, []);
contour = this.map.toLngLat(contour);
// Interpolate color by ratio
const mapProgressToValues = value => vjmap.interpolate(
interpolateInput,
interpolateOutput,
{ ease: vjmap.linear }
)(value)
// Set original data color for marker
dataset.features.forEach(f => f.properties.color = mapProgressToValues((f.properties.value - dataMinValue) / (dataMaxValue - dataMinValue)))
let h = maxHeight; // Set max extrusion height
for(let i = 0; i < contour.features.length; i++) {
let prop = contour.features[i].properties;
let r = (prop.value - dataMinValue) / (dataMaxValue - dataMinValue);
prop.color = mapProgressToValues(r); // Interpolate color
prop.height = h * r; // Interpolate extrusion height
}
contour.features.forEach(f => {
f.properties.formatValue = Math.round(f.properties.value) // Add formatted label value
})
let markers = null;
let markerLayerId = "marker_contour_" + vjmap.RandomID();
const addMarkers = ()=> {
if (markers) return;
markers = dataset.features.map(f => {
if (isClipContour && !clipBounds.contains(this.map.fromLngLat(f.geometry.coordinates))) {
// Skip markers outside clip bounds
return null;
}
// Random styles
let _marker = new vjmap.DiffusedApertureMarker({
lngLat: f.geometry.coordinates,
text: f.properties.value.toFixed(0)
}, {
// Set props: width, color, font
width: 10,
colors: [f.properties.color, vjmap.randomColor()],
textFontSize: 14,
textColor: f.properties.color
}).createMarker();
_marker.layerId = markerLayerId
_marker.addTo(this.map)
return _marker
})
}
const removeMarkers = ()=> {
if (!markers) return;
for(let i = markers.length - 1; i >= 0; i--) {
if (markers[i]) markers[i].remove();
}
markers = null;
}
let symbols = null;
const addSymbols = async ()=> {
if (symbols) return;
let contourData = contour;
if (isClipContour) contourData = clipContours(contour, clipBounds);
// Add text bg image
await this.map.addImageData("imgMaskText", `<svg viewBox="0 0 1024 1024" width="1024" height="1024" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0L0 1024 1024 1024 1024 0z" fill="#022B4F"></path>
</svg>`, 30, 15);
symbols = new vjmap.Symbol({
data: contourData,
iconImage: "imgMaskText",
symbolSpacing: 250, // Symbol spacing 250px
textField: [
'format',
["to-string", ['round', ['get', 'value']]], // Round value for display; for decimals use ['number-format',['get', 'value'], { 'min-fraction-digits': 1, 'max-fraction-digits': 1 }],
{}
],
textFont: ['Arial Unicode MS Regular'],
textSize: 14,
textColor: '#00ffff',
symbolPlacement: "line",
textAnchor: 'center',
textRotationAlignment: "this.map",
iconAllowOverlap: false,
textAllowOverlap: false
});
symbols.addTo(this.map);
}
const removeSymbols = ()=> {
if (!symbols) return;
symbols.remove();
symbols = null;
}
let polyline = null;
const addPolyline = ()=> {
if (polyline) return;
let contourData = contour;
if (isClipContour) contourData = clipContours(contour, clipBounds);
polyline = new vjmap.Polyline({
data: contourData,
lineColor: ['case', ['to-boolean', ['feature-state', 'hover']], '#00ffff', ['get', 'color']],
isHoverPointer: true,
isHoverFeatureState: true
});
polyline.addTo(this.map);
polyline.clickPopup(f => `<h3>Value: ${f.properties.value.toFixed(2)}</h3>Color: ${f.properties.color}`, { anchor: 'bottom' });
}
const removePolyline = ()=> {
if (!polyline) return;
polyline.remove();
polyline = null;
removeSymbols();
}
const drawPolyline = async () => {
addPolyline();
removeSymbols();
await addSymbols();
}
let polygon = null;
const addPolygon = ()=> {
if (polygon) return;
let contourData = contour;
if (isClipContour) contourData = clipContoursPolygon(contour, clipBounds);
polygon = new vjmap.Polygon({
data: contourData,
fillColor: ['case', ['to-boolean', ['feature-state', 'hover']], '#00ffff', ['get', 'color']],
fillOpacity: 0.9,
isHoverPointer: true,
isHoverFeatureState: true
});
polygon.addTo(this.map);
polygon.clickPopup(f => `<h3>Value: ${f.properties.value.toFixed(2)}</h3>Color: ${f.properties.color}`, { anchor: 'bottom' });
}
const removePolygon = ()=> {
if (!polygon) return;
polygon.remove();
polygon = null;
removeSymbols();
}
const drawPolygon = async () => {
addPolygon();
removeSymbols();
await addSymbols();
}
let fillExtrusions = null;
const addFillExtrusion = ()=> {
if (fillExtrusions) return;
fillExtrusions = new vjmap.FillExtrusion({
data: contour,
fillExtrusionColor: ['case', ['to-boolean', ['feature-state', 'hover']], '#00ffff', ['get', 'color']],
fillExtrusionOpacity: 0.9,
fillExtrusionHeight: ['get', 'height'],
fillExtrusionBase:0,
isHoverPointer: true,
isHoverFeatureState: true
});
fillExtrusions.addTo(this.map);
fillExtrusions.clickPopup(f => `<h3>Value: ${f.properties.value.toFixed(2)}</h3>Color: ${f.properties.color}`, { anchor: 'bottom' });
}
const removeFillExtrusion = ()=> {
if (!fillExtrusions) return;
fillExtrusions.remove();
fillExtrusions = null;
removeSymbols();
}
const drawFillExtrusion = async ()=> {
addFillExtrusion();
removeSymbols();
await addSymbols();
}
let clipPolyline = null;
const addClipPolyline = () => {
if (clipPolyline) return;
let points = clipBounds.toPointArray();
points.push(points[0]);
clipPolyline = new vjmap.Polyline({
data: this.map.toLngLat(points),
lineWidth: 2,
lineColor: 'red'
});
clipPolyline.addTo(this.map);
}
const removeClipPolyline = ()=> {
if (!clipPolyline) return;
clipPolyline.remove();
clipPolyline = null;
}
// Polylines and clip bounds
const clipContours = (polylineData, clipBounds) => {
let data = {
type: 'FeatureCollection',
features: []
}
for(let k = 0; k < polylineData.features.length; k++) {
// Iterate sub-polygons
for(let n = 0; n < polylineData.features[k].geometry.coordinates.length; n++) {
let coordinates = this.map.fromLngLat(polylineData.features[k].geometry.coordinates[n]);
// Clip segment by segment
let clipCoordinates = [];
let clipSubCoordinates = [];
for(let j = 1; j < coordinates.length; j++) {
let p1 = vjmap.geoPoint(coordinates[j - 1]);
let p2 = vjmap.geoPoint(coordinates[j]);
let res = vjmap.clipSegment(p1, p2, clipBounds);
if (res === false) {
// Split at bounds
if (clipSubCoordinates.length > 0) {
clipCoordinates.push(this.map.toLngLat(clipSubCoordinates));
clipSubCoordinates = [];
}
continue;
}
let pt1 = [res[0].x, res[0].y];
let pt2 = [res[1].x, res[1].y];
clipSubCoordinates.push(pt1, pt2);
}
if (clipSubCoordinates.length > 0) {
clipCoordinates.push(this.map.toLngLat(clipSubCoordinates));
}
if (clipCoordinates.length > 0) {
for(let i = 0; i < clipCoordinates.length; i++) {
let feature = vjmap.cloneDeep(polylineData.features[k]);
feature.geometry.type = "LineString" ;// Polygon to LineString
feature.geometry.coordinates = clipCoordinates[i];
data.features.push(feature);
}
}
}
}
return data
}
// Polygon and clip bounds
const clipContoursPolygon = (polygonData, clipBounds) => {
let data = {
type: 'FeatureCollection',
features: []
}
for (let k = 0; k < polygonData.features.length; k++) {
// Iterate sub-polygons
for (let n = 0; n < polygonData.features[k].geometry.coordinates.length; n++) {
let feature = vjmap.cloneDeep(polygonData.features[k]);
feature.geometry.coordinates = []
let coordinates = this.map.fromLngLat(polygonData.features[k].geometry.coordinates[n]);
let clipCoordinates = vjmap.clipPolygon(coordinates, clipBounds)
if (clipCoordinates.length > 2) {
clipCoordinates.push(clipCoordinates[0]); // Close
feature.geometry.coordinates.push(this.map.toLngLat(clipCoordinates));
feature.properties.area = vjmap.calcPolygonArea(clipCoordinates)
data.features.push(feature)
}
}
}
// Sort by area
data.features = data.features.sort((a, b) => b.properties.area - a.properties.area)
return data
}
if (options.drawSymbols) {
addMarkers();
}
if (options.drawPolyline !== false) {
await drawPolyline();
}
if (options.drawPolygon) {
await drawPolygon();
}
if (options.drawFillExtrusion) {
await drawFillExtrusion();
}
if (isClipContour) {
addClipPolyline();
}
let sourceIds = [];
let layerIds = [];
if (markers) {
sourceIds.push(markerLayerId);
layerIds.push(markerLayerId);
}
if (symbols) {
sourceIds.push(symbols.sourceId);
layerIds.push(symbols.layerId);
}
if (polyline) {
sourceIds.push(polyline.sourceId);
layerIds.push(polyline.layerId);
}
if (polygon) {
sourceIds.push(polygon.sourceId);
layerIds.push(polygon.layerId);
}
if (fillExtrusions) {
sourceIds.push(fillExtrusions.sourceId);
layerIds.push(fillExtrusions.layerId);
}
if (clipPolyline) {
sourceIds.push(clipPolyline.sourceId);
layerIds.push(clipPolyline.layerId);
}
// Add undo record
this.context.ui.addUndoRecord(() => {
for(let i = 0; i < sourceIds.length; i++) {
if (sourceIds[i].indexOf("marker_contour_") == 0) {
// Handle marker separately
let markers = this.map.getMarkers();
// Delete in reverse order
for(let j = markers.length - 1; j >= 0; j--) {
if (markers[j].layerId == sourceIds[i]) {
markers[j].remove();
}
}
} else {
this.map.removeSourceEx(sourceIds[i]);
}
}
});
return { sourceIds, layerIds }
}
/**
* Get WMS tile URLs
* @param {object} options WMS config options
* @param {string} [options.mapid] Map ID
* @param {string} [options.version] Map version, uses current when empty
* @param {string} [options.layer] Layer style name, default when empty
* @param {string} [options.epsg] CAD EPSG code
* @param {"beijing54"|"xian80"|"cgs2000"} [options.crs] CAD CRS: beijing54, xian80, cgs2000
* @param {string} [options.webMapTiles] Web map tile URL when overlay
* @param {boolean} [options.overMapImage] Whether overlay is imagery
* @param {boolean} [options.isSetRotateZero] Allow rotate for control points
* @param {boolean} [options.baseMapWgs84] Base map WGS84
* @returns {Promise<string[]>} Returns WMS tile URL array
* @example
* // Overlay mapid sys_hello, get WMS tile URLs, create raster tile layer from these URLs
* let wmsTileUrl = await this.getWmsTileUrl({
* mapid: "sys_hello"
* });
* let layer = await this.createRasterLayer({
* tiles: wmsTileUrl,
* });
* console.log(layer);
*/
async getWmsTileUrl(options) {
options = options || {}
let svc = this.map.getService();
// Check if base is web map
let isWebMap = this.map.getMapProjection() instanceof vjmap.LnglatProjection;
let baseMapType = "CAD"
if (isWebMap) {
if (options.baseMapWgs84 === undefined) {
// Auto fetch
baseMapType = "GCJ02";
let sources = this.map.getStyle().sources;
for (let name in sources) {
let source = sources[name];
if (source.type === 'raster') {
let tiles = source.tiles;
if (tiles && tiles.length > 0) {
let tile = tiles[0];
if (tile.includes('tianditu') || tile.includes('openstreetmap')) {
baseMapType = "WGS84";
break;
}
}
}
}
} else {
baseMapType = options.baseMapWgs84 === true ? "WGS84" : "GCJ02";
}
}
let mapParam = svc.currentMapParam() || {}
let baseMapParam = {
// Base map type
baseMapType: baseMapType,
// Map ID
mapid: mapParam.mapid,
// Map version
version: mapParam.version,
// Map bounds
mapbounds: JSON.stringify(this.map.getGeoBounds(1.0).toArray())
}
let overlayType = options.overlayType;
if (overlayType !== "param") {
if (options.epsg !== undefined) {
overlayType = "auto";
} else {
let epsg = await this.getCadEpsgCode(options);
if (epsg) {
options.epsg = epsg;
overlayType = "auto";
}
}
}
if (overlayType === undefined && options.coordinates !== undefined) {
overlayType = "param";
}
let overMapType = options.overMapType || "CAD";
if (options.webMapTiles === undefined) {
let isImageUrl = options.overMapImage === true; // Is imagery map
if (overMapType === "WGS84") {
options.webMapTiles = vjcommon.tiandituProviderTiles(isImageUrl);
} else if (overMapType === "GCJ02") {
options.webMapTiles = vjcommon.gaodeProviderTiles(isImageUrl);
}
}
if (overlayType === "auto" && !options.epsg) {
this.showMessage("Please set CAD EPSG");
return;
}
let overMapParam = {
// Map ID (uses current mapid when empty)
mapid: options.mapid,
// Map version, uses current when empty
version: options.version ,
// Layer style name
layer: options.layer,
// CAD CRS
crs: options.epsg,
// Overlay type
baseMapType: overMapType,
// Web tile URL
webMapTiles: Array.isArray(options.webMapTiles) ? options.webMapTiles : [options.webMapTiles] ,
isSetRotateZero: options.isSetRotateZero, // Allow rotate for control points
coordinates: options.coordinates // Control point coords
};
return await vjcommon.getWmsTileUrl(svc, baseMapParam, overlayType || "direct", {maps: overMapParam})
}
/**
* Export as new CAD drawing
*/
async exportCAD() {
this.map.logInfo("Exporting DWG, please wait...")
const res = await vjcommon.exportDwg(this.map, this.map.getDrawLayer());
if (res.error) {
this.map.logInfo(res.error, "error");
return;
}
const isCloud = this.map.getService().serverUrl.indexOf("vjmap.com") >= 0;
const host = isCloud ? "https://vjmap.com/app/cloud" : (window.location.origin + "/_cloud");
const url = host + `/#/this.map/${res.mapid}?mapopenway=Memory&version=${res.version}`;
window.open(url);
}
/**
* Create path animation data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @param {string} options.lineColor Line color, default #FF9900
* @param {number} options.lineWidth Line width, default 10
* @param {number} options.lineOpacity Line opacity 0-1
* @param {number} options.lineGapWidth Line gap width
* @param {number} options.lineOffset Line offset
* @param {number} options.lineBlur Line blur
* @param {string} options.strokeStyle Stroke: solid or dashed
* @param {[number,number]} options.lineDasharray Dash array [1,1]
* @param {[number,number]} options.lineTranslate Translate [x,y]
* @param {string} options.lineCap Line cap: butt, round, square
* @param {string} options.lineJoin Line join: bevel, round, miter
* @param {string} options.strokeImage Line pattern image name
* @param {string} options.lineGradient Line gradient
* @param {boolean} options.showBorder Show border, default true
* @param {string} options.borderColor Border color, default #f00
* @param {number} options.borderWidth Border width
* @param {number} options.borderOpacity Border opacity 0-1
* @param {number} options.borderGapWidth Border gap width
* @param {number} options.borderOffset Border offset
* @param {number} options.borderBlur Border blur
* @param {string} options.borderGradient Border gradient
* @param {boolean} options.showDir Show arrow, default true
* @param {number} options.dirSize Arrow size
* @param {number} options.dirSpacing Arrow spacing
* @param {number} options.dirImageWidth Arrow image width
* @param {number} options.dirImageHeight Arrow image height
* @param {string} options.dirImageColor Arrow image color
* @param {string} options.dirIconColor Arrow icon color
* @param {string} options.dirLayout Arrow layout
* @param {string} options.cursor Cursor shape
* @param {string} options.iconImage Icon name for image background
* @param {number} options.iconRotate Icon rotation angle, clockwise, degrees, default 0
* @param {[number,number]} options.iconOffset Icon offset from anchor, [x,y], positive = right/down
* @param {string} options.iconAnchor Icon anchor position, values: "center", "left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"
* @param {string} options.iconRotationAlignment Icon rotation alignment, values: "map", "viewport", "auto"
* @param {string} options.iconPitchAlignment Icon pitch alignment, values: "map", "viewport", "auto"
* @param {boolean} options.iconAllowOverlap Allow icon overlap, default false
* @param {number} options.iconOpacity Icon opacity 0-1, default 1
* @param {boolean} options.iconIgnorePlacement Ignore collision detection, default false
* @param {boolean} options.iconOptional Icon overlap option, default false
* @param {number} options.iconSize Icon scale, default 1
* @param {string} options.iconTextFit Icon text fit, values: "none", "width", "height", "both"
* @param {[number,number,number,number]} options.iconTextFitPadding Icon text fit padding [top, right, bottom, left]
* @param {number} options.iconPadding Icon padding, default 1px
* @param {boolean} options.iconKeepUpright Keep icon upright, default false
* @param {string} options.iconColor SDF icon color, default"#000000"
* @param {string} options.iconHaloColor SDF icon halo color, default"rgba(0, 0, 0, 0)"
* @param {number} options.iconHaloWidth Icon halo width, default 0
* @param {number} options.iconHaloBlur Icon halo blur, default 0
* @param {[number,number]} options.iconTranslate Icon translate [x,y]
* @param {string} options.iconTranslateAnchor Icon translate anchor, values: "map", "viewport"
* @param {string|Expression} options.textField Text label content
* @param {string[]} options.textFont Text font, default["Open Sans Regular","Arial Unicode MS Regular"]
* @param {number} options.textSize Text size, default 16
* @param {boolean} options.textAllowOverlap Allow text overlap, default false
* @param {string} options.textColor Text color, default"#000000"
* @param {number} options.textOpacity Text opacity 0-1, default 1
* @param {[number,number]} options.textOffset Text offset [x,y]
* @param {number} options.textRotate Text rotation angle
* @param {number} options.textPadding Text padding, default 2
* @param {boolean} options.textKeepUpright Keep text upright, default true
* @param {number} options.textMaxWidth Text max width, default 10em
* @param {number} options.textLineHeight Text line height, default 1.2
* @param {number} options.textLetterSpacing Letter spacing, default 0
* @param {string} options.textJustify Text justify, values: "auto", "left", "center", "right"
* @param {string} options.textAnchor Text anchor position
* @param {string} options.textRotationAlignment Text rotation alignment
* @param {string} options.textPitchAlignment Text pitch alignment
* @param {number} options.textMaxAngle Max angle between chars, default 45°
* @param {string[]} options.textWritingMode Text writing mode
* @param {string} options.textTransform Text transform, values: "none", "uppercase", "lowercase"
* @param {number} options.textRadialOffset Text radial offset
* @param {string[]} options.textVariableAnchor Text variable anchor array
* @param {boolean} options.textIgnorePlacement Ignore text placement
* @param {boolean} options.textOptional Text optional
* @param {string} options.textHaloColor Text halo color
* @param {number} options.textHaloWidth Text halo width
* @param {number} options.textHaloBlur Text halo blur
* @param {[number,number]} options.textTranslate Text translate
* @param {string} options.textTranslateAnchor Text translate anchor
* @param {string} options.symbolPlacement Symbol placement, values: "point", "line", "line-center"
* @param {number} options.symbolSpacing Symbol spacing, default 250
* @param {boolean} options.symbolAvoidEdges Avoid edge collision
* @param {string} options.symbolZOrder Symbol z-order, values: "auto", "viewport-y", "source"
* @param {number} options.fps Animation frame rate, frames per second, default 10; line split into 100 segments, segments per second; range 1-100
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Randomly generate GeoJSON in map bounds, create path animation layer named layer21
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createPathAnimateLayer(geojson, {
* layerName: 'layer21',
* iconImage: 'icon1'
* });
* console.log(layer);
*/
async createPathAnimateLayer(data, options) {
options = options || {};
let type = "pathAnimate";
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
lineWidth: 10,
showDir: true,
showBorder: true,
borderColor: "#f00",
lineColor: '#FF9900',
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createPathAnimateLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Create rotating text border data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {string} options.text Text to display, optional, default "index ${props.index}"
* @param {number} options.minZoom Min zoom to display, optional, default 0
* @param {number} options.maxZoom Max zoom to display, optional, default 24
* @param {boolean} options.isDrawLinePoint Include line points when drawing, optional, default false
* @param {number} options.width Pixel width, optional, default 24
* @param {number} options.markerHeight Pixel height, optional
* @param {string} options.color1 Color value 1, optional
* @param {string} options.color2 Color value 2, optional
* @param {number} options.textFontSize Font size, optional
* @param {string} options.textColor Text color, optional
* @param {boolean} options.draggable Draggable, optional, default false
* @param {string} options.animationType Animation type, optional: MAP_ANIMATION_NONE, MAP_ANIMATION_BOUNCE, MAP_ANIMATION_DROP
* @param {number} options.rotation Rotation angle, optional
* @param {string} options.rotationAlignment Rotation alignment, optional: auto, map, viewport
* @param {string} options.pitchAlignment Pitch alignment, optional: auto, map, viewport
* @param {number} options.height Elevation value, optional
* @param {number} options.scaleMaxZoom Max zoom for scaling, optional
* @param {boolean} options.removeWhenNoInMapView Auto show/hide in view, optional
* @param {number} options.removeWhenNoInMapViewPadding Padding for auto show/hide, optional, default 500
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Randomly generate GeoJSON in map bounds, create rotating text border layer named layer28
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createTextBorderMarkerLayer(geojson, {
* layerName: 'layer28'
* });
* console.log(layer);
*/
async createTextBorderMarkerLayer(data, options) {
options = options || {};
let type = "textBorderMarker";
if (options.height) {
options.height = this.map.geoLengthToHeight(options.height);
}
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
text: "ID:${props.index}",
width: 40,
markerHeight: 20,
color1: "#ff0000",
color2: "#ffff00",
textFontSize: 15,
textColor: "#00ffff",
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createTextBorderMarkerLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Create symbol path animation data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @param {string} options.iconImage Icon name for image background
* @param {number} options.iconRotate Icon rotation angle, clockwise, degrees, default 0
* @param {[number,number]} options.iconOffset Icon offset from anchor, [x,y], positive = right/down
* @param {string} options.iconAnchor Icon anchor position, values: "center", "left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"
* @param {string} options.iconRotationAlignment Icon rotation alignment, values: "map", "viewport", "auto"
* @param {string} options.iconPitchAlignment Icon pitch alignment, values: "map", "viewport", "auto"
* @param {boolean} options.iconAllowOverlap Allow icon overlap, default false
* @param {number} options.iconOpacity Icon opacity 0-1, default 1
* @param {boolean} options.iconIgnorePlacement Ignore collision detection, default false
* @param {boolean} options.iconOptional Icon overlap option, default false
* @param {number} options.iconSize Icon scale, default 1
* @param {string} options.iconTextFit Icon text fit, values: "none", "width", "height", "both"
* @param {[number,number,number,number]} options.iconTextFitPadding Icon text fit padding [top, right, bottom, left]
* @param {number} options.iconPadding Icon padding, default 1px
* @param {boolean} options.iconKeepUpright Keep icon upright, default false
* @param {string} options.iconColor SDF icon color, default"#000000"
* @param {string} options.iconHaloColor SDF icon halo color, default"rgba(0, 0, 0, 0)"
* @param {number} options.iconHaloWidth Icon halo width, default 0
* @param {number} options.iconHaloBlur Icon halo blur, default 0
* @param {[number,number]} options.iconTranslate Icon translate [x,y]
* @param {string} options.iconTranslateAnchor Icon translate anchor, values: "map", "viewport"
* @param {string|Expression} options.textField Text label content
* @param {string[]} options.textFont Text font, default["Open Sans Regular","Arial Unicode MS Regular"]
* @param {number} options.textSize Text size, default 16
* @param {boolean} options.textAllowOverlap Allow text overlap, default false
* @param {string} options.textColor Text color, default"#000000"
* @param {number} options.textOpacity Text opacity 0-1, default 1
* @param {[number,number]} options.textOffset Text offset [x,y]
* @param {number} options.textRotate Text rotation angle
* @param {number} options.textPadding Text padding, default 2
* @param {boolean} options.textKeepUpright Keep text upright, default true
* @param {number} options.textMaxWidth Text max width, default 10em
* @param {number} options.textLineHeight Text line height, default 1.2
* @param {number} options.textLetterSpacing Letter spacing, default 0
* @param {string} options.textJustify Text justify, values: "auto", "left", "center", "right"
* @param {string} options.textAnchor Text anchor position
* @param {string} options.textRotationAlignment Text rotation alignment
* @param {string} options.textPitchAlignment Text pitch alignment
* @param {number} options.textMaxAngle Max angle between chars, default 45°
* @param {string[]} options.textWritingMode Text writing mode
* @param {string} options.textTransform Text transform, values: "none", "uppercase", "lowercase"
* @param {number} options.textRadialOffset Text radial offset
* @param {string[]} options.textVariableAnchor Text variable anchor array
* @param {boolean} options.textIgnorePlacement Ignore text placement
* @param {boolean} options.textOptional Text optional
* @param {string} options.textHaloColor Text halo color
* @param {number} options.textHaloWidth Text halo width
* @param {number} options.textHaloBlur Text halo blur
* @param {[number,number]} options.textTranslate Text translate
* @param {string} options.textTranslateAnchor Text translate anchor
* @param {string} options.symbolPlacement Symbol placement, values: "point", "line", "line-center"
* @param {number} options.symbolSpacing Symbol spacing, default 250
* @param {boolean} options.symbolAvoidEdges Avoid edge collision
* @param {string} options.symbolZOrder Symbol z-order, values: "auto", "viewport-y", "source"
* @param {string} options.lineColor Line color, optional, default '#ff0000'
* @param {number} options.lineWidth Line width, optional, default 1
* @param {number} options.lineOpacity Line opacity, optional, 0-1
* @param {number} options.lineGapWidth Line gap width, optional, default 1, pixels
* @param {number} options.lineOffset Line offset, optional
* @param {number} options.lineBlur Line blur, optional
* @param {[number,number]} options.lineDasharray Dash pattern, optional
* @param {[number,number]} options.lineTranslate Translate [x,y], optional
* @param {string} options.lineCap Line cap: 'butt', 'round', 'square'
* @param {string} options.lineJoin Line join: 'bevel', 'round', 'miter'
* @param {string} options.linePattern Line pattern, optional
* @param {string} options.lineGradient Line gradient, optional
* @param {boolean} options.drawPath Draw path line, true/false
* @param {number} options.anmiDuration Animation duration in seconds, default 5. 0=no animation
* @param {number} options.anmiRepeat Animation repeat count, default 0. 0=infinite
* @param {string} options.anmiRepeatType Repeat type: "reverse", "loop", "mirror"
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Randomly generate GeoJSON in map bounds, create symbol path animation layer named layer26, icon name _markerIcon_
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createSymbolPathAnimateLayer(geojson, {
* layerName: 'layer26',
* iconImage: '_markerIcon_'
* });
* console.log(layer);
*/
async createSymbolPathAnimateLayer(data, options) {
options = options || {};
let type = "symbolPathAnimate";
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
drawPath: true,
anmiDuration: 5,
lineColor: '#75E7EF',
lineOpacity: 0.4,
lineWidth: 20,
iconAllowOverlap: true,
textAllowOverlap: true,
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createSymbolPathAnimateLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Query all block entities
* @param {string | string[]} blockName Block name or block name array to query, optional, default empty, query all blocks when empty
* @returns {Object} Returns standard GeoJSON FeatureCollection with features, each feature has geometry and properties
* @example
* // Query all block entities
* let geoJson = await this.queryMapAllBlock();
* // Get result
* console.log(geoJson)
*/
async queryMapAllBlock(blockName) {
let svc = this.map.getService();
let query = await svc.exprQueryFeature({
// Use expression query for all block entities first; with condition query blocks become discrete spatial data and cannot be queried as whole, so query block info via expression first
/// To query all blocks. For a specific block name use: expr: `gOutReturn := if((gInFeatureType == 'AcDbBlockReference' and gInAttrs4 == 'block_name_to_query'), 1, 0);`,
expr: `gOutReturn := if((gInFeatureType == 'AcDbBlockReference'), 1, 0);`,
fields: "objectid,blockname,layername,positon,rotate,scaleX,scaleY,scaleZ,bounds", // empty means all fields
geom: false, // memory mode
useCache: true,
limit: 1000000 // large value to find all
})
let blockNameSet = new Set();
if (Array.isArray(blockName)) {
blockName.forEach(name => {
blockNameSet.add(name)
})
} else if (typeof blockName === "string") {
blockNameSet.add(blockName)
}
let geoJson = {
type: "FeatureCollection",
features: []
}
if (query.error) {
this.map.logInfo(query.error, "error")
return geoJson
}
for(let i = 0; i < query.result.length; i++) {
if (blockNameSet.size > 0 && !blockNameSet.has(query.result[i].blockname)) {
continue
}
query.result[i].bounds = vjmap.GeoBounds.fromString(query.result[i].bounds).toArray();
let feature = {
type: "Feature",
geometry: {
type: "Point",
coordinates: vjmap.GeoPoint.fromString(query.result[i].positon).toArray()
},
properties: query.result[i]
}
geoJson.features.push(feature)
}
return geoJson
}
/**
* Get map bounds
* @param {number} boundsType Bounds type (0 draw bounds, 1 actual bounds, 2 full bounds), default 0, optional
* @returns {[number, number, number, number]} Returns array [x1,y1,x2,y2] for bottom-left and top-right coordinates
* @example
* // Get map bounds
* let rect = this.getMapGeoBounds();
* let minx = rect[0];
* let miny = rect[1];
* let maxx = rect[2];
* let maxy = rect[3];
* let width = maxx - minx; // Map bounds width
* let height = maxy - miny; // Map bounds height
* console.log(minx, miny, maxx, maxy, width, height);
* // Get map actual bounds
* let rect2 = this.getMapGeoBounds(1);
* console.log(rect2);
*/
getMapGeoBounds(boundsType) {
boundsType = boundsType || 0;
let geoBounds;
if (boundsType == 1) {
// CAD actual bounds (0.4 of full map) this.map.getGeoBounds(0.4) // square
geoBounds = this.map.getGeoBounds(0.4)
} else if (boundsType == 2) {
// Full map bounds this.map.getGeoBounds(1.0) // square
geoBounds = this.map.getGeoBounds(1)
} else {
// Map draw bounds
geoBounds = this.map.getService().currentMapParam().drawBounds || this.map.getGeoBounds(0.4)
}
return geoBounds.toArray()
}
/**
* Create text data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {string} options.text Text to display, optional, default "index ${props.index}"
* @param {Object} options.style Text style settings, optional
* @param {number} options.minZoom Min zoom to display, optional, default 0
* @param {number} options.maxZoom Max zoom to display, optional, default 24
* @param {boolean} options.isDrawLinePoint Include line points when drawing, optional, default false
* @param {string} options.color Marker color, optional, default'#3FB1CE'
* @param {number[]} options.offset XY pixel offset [0,0], negative = left/up
* @param {number} options.rotation Marker rotation, clockwise, default 0
* @param {string} options.anchor Marker anchor:center、top、bottom、left、right、top-left、top-right、bottom-left、bottom-right, default center
* @param {boolean} options.draggable Draggable, optional, default false
* @param {string} options.animationType Animation: MAP_ANIMATION_NONE, MAP_ANIMATION_BOUNCE, MAP_ANIMATION_DROP
* @param {string} options.rotationAlignment Rotation alignment: auto, map, viewport, default auto
* @param {string} options.pitchAlignment Pitch alignment: auto, map, viewport, default auto
* @param {number} options.scale Scale, default 1, 41px x 27px
* @param {number} options.scaleMaxZoom Scale max zoom (0-24), auto-scale below
* @param {number} options.height Elevation, CAD units
* @param {boolean} options.removeWhenNoInMapView Auto show/hide in view
* @param {number} options.removeWhenNoInMapViewPadding View padding for auto show/hide, default 500
* @param {number} options.clickTolerance Click tolerance, inherits map default
* @param {string} options.customImage Custom image URL/base64/svg
* @param {number} options.customImageWidth Custom image width
* @param {number} options.customImageHeight Custom image height
* @param {string} options.customHtml Custom HTML element JS code
* @param {string} options.popupHtml Popup HTML content
* @param {[number,number]} options.popupOffset Popup XY offset
* @param {string} options.popupAnchor Popup anchor, same as anchor
* @param {boolean} options.closeButton Show close button
* @param {boolean} options.closeOnClick Close popup on map click
* @param {boolean} options.closeOnMove Close popup on map move
* @param {boolean} options.focusAfterOpen Focus first focusable in popup
* @param {string} options.className CSS class for popup
* @param {string} options.maxWidth Popup max width, default'240px'
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Randomly generate GeoJSON in map bounds, create text data layer named layer27
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createTextDataLayer(geojson, {
* layerName: 'layer27'
* });
* console.log(layer);
*/
async createTextDataLayer(data, options) {
options = options || {};
let type = "text";
if (options.height) {
options.height = this.map.geoLengthToHeight(options.height);
}
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
text: "ID:${props.index}",
style: {
'cursor': 'pointer',
'opacity': 1,
'padding': '12px',
'border-radius': '4px',
'background-color': '#0ff',
'border-width': 0,
'box-shadow': '0px 2px 6px 0px rgba(97,113,166,0.2)',
'text-align': 'center',
'font-size': '14px',
'color': '#F33'
},
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createTextDataLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Create curve data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {string} options.lineColor Line color, optional, default '#ff0000'
* @param {number} options.lineWidth Line width, optional, default 1
* @param {number} options.lineOpacity Line opacity, optional, 0-1
* @param {number} options.lineGapWidth Line gap width, optional, default 1, pixels
* @param {number} options.lineOffset Line offset, optional. Positive=right/inward, negative=left/outward
* @param {number} options.lineBlur Line blur, optional, must be less than line width
* @param {[number,number]} options.lineDasharray Dash pattern, optional, e.g. [1,1]
* @param {[number,number]} options.lineTranslate Translate [x,y], optional, negative=left/up, pixels
* @param {string} options.lineCap Line cap: 'butt', 'round', 'square'
* @param {string} options.lineJoin Line join: 'bevel', 'round', 'miter'
* @param {string} options.linePattern Line pattern, optional, must match preloaded image, width power of 2
* @param {string} options.lineGradient Line gradient, optional
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Randomly generate GeoJSON in map bounds, create curve data layer named layer8
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createCurveLayer(geojson, {
* layerName: 'layer8'
* });
* console.log(layer);
*/
async createCurveLayer(data, options) {
options = options || {};
let type = "curve";
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
lineColor: '#00ffff',
lineWidth: 3,
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createCurveLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Measure angle
* @example
* // Measure angle
* await this.measureAngle();
*/
measureAngle() {
vjcommon.runMeasureCmd(this.map, "measureAngle");
}
/**
* Rename map
* @param {string} oldMapID Old map ID
* @param {string} newMapID New map ID
* @returns {Promise<boolean>} Returns true on success, false on failure
* @example
* // Rename map ID from old_hello to new_hello
* let ok = await this.renameMap("old_hello", "new_hello");
* console.log(ok);
*/
async renameMap(oldMapID, newMapID) {
let svc = this.map.getService();
let res = await svc.cmdRenameMap(oldMapID, newMapID);
return res && res.status;
}
/**
* Overlay web map
* @param {object} options Overlay config options
* @param {string} [options.epsg] CAD EPSG code
* @param {"beijing54"|"xian80"|"cgs2000"|""} [options.crs] CAD CRS: beijing54, xian80, cgs2000 or empty
* @param {number} [options.webmaptype] Web map type: 1 Amap, 2 Amap imagery, 3 Tianditu, 4 Tianditu imagery
* @returns {Promise<string>} Returns overlay layer name
* @example
* // Example 1
* // Overlay web map, CAD CRS is cgs2000
* let layerName = await this.overlayWebMap({
* crs: "cgs2000"
* });
* console.log("layerName", layerName);
* // Example 2
* // Overlay Tianditu imagery, CAD CRS is xian80
* let layerName2 = await this.overlayWebMap({
* crs: "xian80",
* webmaptype: 4
* });
* console.log("layerName", layerName2);
* // Example 3
* // Overlay web map
* let layerName3 = await this.overlayWebMap();
* console.log("layerName", layerName3);
*/
async overlayWebMap(options) {
options = {...options}
// Check if base is web map
let isWebMap = this.map.getMapProjection() instanceof vjmap.LnglatProjection;
if (isWebMap) {
this.showMessage("Current map is already a web map, cannot overlay");
return;
}
let epsg = options.epsg;
if (!epsg) {
epsg = await this.getCadEpsgCode(options);
}
let webmaptype = options.webmaptype ?? 1;
let fp;
if (!epsg) {
// Four-parameter overlay, left is default map, right is web map
fp = await this.context.ui.getFourParamter({}, {webmaptype: webmaptype});
if (!fp) return;
if (fp.webmaptype == 1 || fp.webmaptype == 2) {
// Amap
options.overMapType = "GCJ02";
if (fp.webmaptype == 2) {
// Imagery
options.overMapImage = true;
}
} else if (fp.webmaptype == 3 || fp.webmaptype == 4) {
// Tianditu
options.overMapType = "WGS84";
if (fp.webmaptype == 4) {
// Imagery
options.overMapImage = true;
}
}
options.isSetRotateZero = !fp.allowsRotation;
options.coordinates = [];
for(let n = 0; n < fp.leftMarkers.length; n++) {
let leftMarker = fp.leftMarkers[n];
let rightMarker = fp.rightMarkers[n];
options.coordinates.push({
x1: rightMarker.x,
y1: rightMarker.y,
x2: leftMarker.x,
y2: leftMarker.y
});
}
options.overlayType = "param";
} else {
options.overlayType = "auto";
if (webmaptype == 1 || webmaptype == 2) {
// Amap
options.overMapType = "GCJ02";
if (webmaptype == 2) {
// Imagery
options.overMapImage = true;
}
} else if (webmaptype == 3 || webmaptype == 4) {
// Tianditu
options.overMapType = "WGS84";
if (webmaptype == 4) {
// Imagery
options.overMapImage = true;
}
}
}
let wmsTileUrl;
if (options.overlayType == "param") {
// Four-parameter overlay
let b = this.map.getMapExtent();
if (options.overMapType == "WGS84") {
// Tianditu
let webMapTiles = vjcommon.tiandituProviderTiles(options.overMapImage);
const tiles = webMapTiles.map(t => vjcommon.getTileShards(t)) || [];
wmsTileUrl = this.map.getService().webMapUrl({
tileCrs: "wgs84",
tileUrl: tiles?.map(t => t.tileUrl) ?? '',
tileShards: (tiles.length >= 0) ? tiles[0].tileShards : '',
tileToken: "",
tileSize: 256,
tileRetina: 1,
tileMaxZoom: 18,
tileFlipY: false,
// @ts-ignore
mapbounds: JSON.stringify([b.min.x,b.min.y,b.max.x,b.max.y]),
srs: "EPSG:3857",
fourParameterBefore: fp.fourParamter
})
} else {
// Amap
let webMapTiles = vjcommon.gaodeProviderTiles(options.overMapImage);
const tiles = webMapTiles.map(t => vjcommon.getTileShards(t)) || [];
wmsTileUrl = this.map.getService().webMapUrl({
tileCrs: "gcj02",
tileUrl: tiles?.map(t => t.tileUrl) ?? '',
tileShards: (tiles.length >= 0) ? tiles[0].tileShards : '',
tileToken: "",
tileSize: 256,
tileRetina: 1,
// @ts-ignore
tileMaxZoom: 18,
tileFlipY: false,
mapbounds: JSON.stringify([b.min.x,b.min.y,b.max.x,b.max.y]),
srs: "EPSG:3857",
fourParameterBefore: fp.fourParamter
})
}
} else {
// Auto overlay
wmsTileUrl = await this.getWmsTileUrl({
epsg: epsg,
overMapType: options.overMapType,
overMapImage: options.overMapImage,
isSetRotateZero: options.isSetRotateZero,
coordinates: options.coordinates,
overlayType: options.overlayType
});
}
if (!wmsTileUrl) return;
// Create raster tile layer
let layer = await this.createRasterLayer({
tiles: wmsTileUrl,
});
// Move layer to bottom
this.moveLayer(layer.layerName, {
bottomMost: true
});
return layer
}
/**
* Set CAD entity visibility
* @param {Array<string>} objectids Entity ID array
* @param {boolean} visible Visible or not, true=visible, false=hidden
*/
async setCadEntityVisible(objectids, visible) {
if (!this.context.global.updateMapStyleObj) {
this.context.global.updateMapStyleObj = vjcommon.createUpdateMapStyleObj(this.map);
}
const updateMapStyleObj = this.context.global.updateMapStyleObj;
if (visible) {
await updateMapStyleObj.removeHideObjectIds(new Set(objectids));
} else {
await updateMapStyleObj.addHideObjectIds(new Set(objectids));
}
// Add undo record
this.context.ui.addUndoRecord(async () => {
if (!visible) {
await updateMapStyleObj.removeHideObjectIds(new Set(objectids));
} else {
await updateMapStyleObj.addHideObjectIds(new Set(objectids));
}
});
}
/**
* Switch drawing workspace
* @param {string} workspaceName Workspace name, optional, select workspace when empty
* @example
* // Switch drawing workspace
* this.switchWorkspace();
*/
async switchWorkspace(workspaceName) {
// Get default layer first
let svc = this.map.getService();
let oldWorkspaceName = svc.getCurWorkspaceName();
if (!workspaceName) {
let workspaces = await svc.getWorkspaces() || [];
let workspaceNames = workspaces.map(wp => wp.name + (wp.alias ? "[" + wp.alias + "]" : ""));
let oldWorkspace = workspaces.find(wp => wp.name === oldWorkspaceName);
if (oldWorkspace) {
oldWorkspaceName = oldWorkspace.name + (oldWorkspace.alias ? "[" + oldWorkspace.alias + "]" : "");
}
workspaceNames.unshift("[Default Workspace]");
workspaceName = await this.selectOptionsValue(workspaceNames, {
isMultiSelect: false,
title: "Select Workspace",
initValue: oldWorkspaceName || "[Default Workspace]"
});
if (!workspaceName) {
return;
}
if (workspaceName) {
// Take the name part before bracket
workspaceName = workspaceName.split("[")[0];
}
}
svc.switchWorkspace(workspaceName);
// Add undo record
this.context.ui.addUndoRecord(() => {
svc.switchWorkspace(oldWorkspaceName);
});
}
/**
* Transform coordinates
* @param {string} srs Source CRS name, e.g. EPSG:4326
* @param {string} crs Target CRS name, e.g. EPSG:3857
* @param {[number, number] | [number, number][]} points Coordinates to transform
* @param {string | string[]} [fourParameter] Four parameters (x offset, y offset, scale, rotation rad), optional, final correction
* @param {boolean} [isInverseFourParamter] Whether inverse of above four-parameter transform
* @returns {Promise<[number, number] | [number, number][]>} Returns transformed coordinates
* @example
* // Transform coordinates [120.0, 30.0] from EPSG:4326 to EPSG:3857
* let transformedPoints = await this.coordTransform("EPSG:4326", "EPSG:3857", [120.0, 30.0]);
* console.log(transformedPoints);
*/
async coordTransform(srs, crs, points, fourParameter, isInverseFourParamter) {
let svc = this.map.getService();
let isOnePoint = false;
if (Array.isArray(points)) {
if (points.length > 0 && !Array.isArray(points[0])) {
isOnePoint = true;
points = vjmap.geoPoint(points);
} else {
points = points.map((point) => vjmap.geoPoint(point));
}
}
let result = await svc.cmdTransform(srs, crs, points, fourParameter, isInverseFourParamter);
if (result && result.error) {
if (this.map.logInfo) this.map.logInfo(result.error, "error", 10000);
return;
}
if (isOnePoint) {
return result[0];
}
return result;
}
/**
* Get all drawn features
* @returns {<GeoJSON | undefined>} Returns selected GeoJSON object
* @example
* // Get feature IDs of all drawn features
* const geojson = this.getDrawAll();
* if (!geojson) return;
* let ids = [];
* for(let i = 0; i < geojson.features.length; i++) {
* let feature = geojson.features[i];
* ids.push(feature.id);
* }
* console.log(ids);
*/
getDrawAll() {
let draw = this.map.getDrawLayer();
return this.map.fromLngLat(draw.getAll());
}
/**
* Randomly generate GeoJSON data within map bounds
* @param {Record<string, any>} options Parameters, optional
* @param {number} boundsType Bounds type (0 draw, 1 actual, 2 full), default 0, optional
* @param {string} options.type Type: 'point', 'line', 'polygon', 'all', default 'all'
* @param {number} options.pointCount Max point count, default 10
* @param {number} options.lineCount Max line count, default 10
* @param {number} options.polygonCount Max polygon count, default 10
* @param {number} options.linePointCount Max points per line, default 3
* @param {number} options.polygonPointCount Max points per polygon, default 3
* @returns {Object} Returns GeoJSON with features, each has geometry and properties including index
* @example
* // Randomly generate GeoJSON in map bounds
* let geojson = this.randomGeojsonInMapBounds();
* console.log(geojson);
* // Randomly generate GeoJSON, type point, max 50 points
* let ptGeoJson = this.randomGeojsonInMapBounds({
* type: 'point',
* pointCount: 50
* });
* console.log(ptGeoJson);
*/
randomGeojsonInMapBounds(options) {
options = options || {};
options.type = options.type || 'all';
let rect = this.getMapGeoBounds(options.boundsType || 0);
let geoBounds = vjmap.GeoBounds.fromArray(rect);
let FeatureCollection = {
type: "FeatureCollection",
features: []
};
if (options.type == 'point' || options.type == 'all') {
let coll = geoBounds.randomGeoJsonPointCollection(Math.max(options.pointCount || 10, 2));
if (options.pointCount == 1) {
// @ts-ignore
FeatureCollection.features.push(coll.features[0]);
} else {
// @ts-ignore
FeatureCollection.features.push(...coll.features);
}
}
if (options.type == 'line' || options.type == 'all') {
let coll = geoBounds.randomGeoJsonLineCollection(options.lineCount || 10, options.linePointCount || 3);
// @ts-ignore
FeatureCollection.features.push(...coll.features);
}
if (options.type == 'polygon' || options.type == 'all') {
let coll = geoBounds.randomGeoJsonPolygonCollection(options.polygonCount || 10, options.polygonPointCount || 3);
// @ts-ignore
FeatureCollection.features.push(...coll.features);
}
for(let i = 0; i < FeatureCollection.features.length; i++) {
let feature = FeatureCollection.features[i];
feature.id = i;
feature.properties = {
index: i
}
}
return FeatureCollection;
}
/**
* Measure distance
* @example
* // Measure distance
* await this.measureDistance();
*/
measureDistance() {
vjcommon.runMeasureCmd(this.map, "measureDist");
}
/**
* Create animate symbol data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @param {string} options.animateImagesType Animation type: "arrow", "antpath", "customImage", "imageSprites"
* @param {string} options.animateImages_arrowFillColor Arrow fill color when animateImagesType is "arrow", default "#22B14C"
* @param {string} options.animateImages_arrowStrokeColor Arrow stroke color when animateImagesType is "arrow", default "#fff"
* @param {number} options.animateImages_arrowStrokeWidth Arrow stroke width when animateImagesType is "arrow"
* @param {number} options.animateImages_arrowWidth Arrow size when animateImagesType is "arrow"
* @param {string} options.animateImages_fillColor1 Antpath fill color 1 when animateImagesType is "antpath"
* @param {string} options.animateImages_fillColor2 Antpath fill color 2 when animateImagesType is "antpath"
* @param {string} options.customImageUrl Custom image URL or base64 when animateImagesType is "customImage"
* @param {string} options.imageSpriteUrl Image sprite URL or base64 when animateImagesType is "imageSprites"
* @param {number} options.animateImages_spriteWidth Sprite width in source image when animateImagesType is "imageSprites"
* @param {number} options.animateImages_spriteHeight Sprite height in source image when animateImagesType is "imageSprites"
* @param {number} options.animateImages_canvasWidth Canvas width, common for all animate types
* @param {number} options.animateImages_canvasHeight Canvas height, common for all animate types
* @param {number} options.animateImages_frameCount Frame count, common for all animate types
* @param {boolean} options.animateImages_directionReverse Reverse animation direction, common for all animate types
* @param {number} options.speed Animation speed, step 0.1
* @param {boolean} options.startAutoAnimation Auto-play on start
* @param {string} options.symbolPlacement Symbol placement: "point", "line", "line-center"
* @param {number} options.symbolSpacing Distance between symbol centers in pixels, default 250
* @param {boolean} options.symbolAvoidEdges Avoid symbol collision, default false
* @param {string} options.symbolZOrder Symbol z-order: "auto", "viewport-y", "source"
* @param {string} options.iconImage Icon name for image background
* @param {number} options.iconRotate Icon rotation angle, default 0
* @param {[number, number]} options.iconOffset Icon offset from anchor, default [0,0]
* @param {string} options.iconAnchor Icon anchor: "center", "left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"
* @param {string} options.iconRotationAlignment Icon rotation alignment: "map", "viewport", "auto"
* @param {string} options.iconPitchAlignment Icon direction when map tilted: "map", "viewport", "auto"
* @param {boolean} options.iconAllowOverlap Allow icon overlap, default false
* @param {number} options.iconOpacity Icon opacity 0-1, default 1
* @param {boolean} options.iconIgnorePlacement Ignore icon collision, default false
* @param {boolean} options.iconOptional Icon overlap option, default false
* @param {number} options.iconSize Icon scale, default 1
* @param {string} options.iconTextFit Icon text fit: "none", "width", "height", "both"
* @param {[number,number,number,number]} options.iconTextFitPadding Icon text fit padding, default [0,0,0,0]
* @param {number} options.iconPadding Icon padding, default 1
* @param {boolean} options.iconKeepUpright Keep icon upright, default false
* @param {string} options.iconColor SDF icon color, default"#000000"
* @param {string} options.iconHaloColor SDF icon halo color, default "rgba(0,0,0,0)"
* @param {number} options.iconHaloWidth Halo distance to icon outline, default 0
* @param {number} options.iconHaloBlur Icon halo blur width, default 0
* @param {[number,number]} options.iconTranslate Icon translate, default [0,0]
* @param {string} options.iconTranslateAnchor Icon translate anchor: "map", "viewport"
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Randomly generate GeoJSON in map bounds, create animate symbol layer named layer4
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createAnimateSymbolLayer(geojson, {
* layerName: 'layer4'
* });
* console.log(layer);
*/
async createAnimateSymbolLayer(data, options) {
options = options || {};
let type = "animateSymbol";
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
animateImagesType: "arrow",
animateImages_arrowFillColor: "#22B14C",
animateImages_arrowStrokeColor: "#fff",
animateImages_arrowStrokeWidth: 6,
animateImages_canvasWidth: 128,
animateImages_canvasHeight: 32,
animateImages_arrowWidth: 16,
animateImages_frameCount: 4,
animateImages_fillColor1: "#f0fb",
animateImages_fillColor2: "#0ffb",
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createAnimateSymbolLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Create info window (popup) data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {string} options.text Text to display, optional, default "index ${props.index}"
* @param {number} options.minZoom Min zoom to display, optional, default 0
* @param {number} options.maxZoom Max zoom to display, optional, default 24
* @param {boolean} options.isDrawLinePoint Include line points when drawing, optional, default false
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @param {string} options.html Popup HTML content, supports props variables
* @param {[number, number]} options.offset Offset [x,y] in pixels from element center, negative=left/up, default [0,0]
* @param {string} options.anchor Popup anchor: 'center', 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right', default 'center'
* @param {boolean} options.closeButton Show close button, optional
* @param {boolean} options.closeOnClick Close popup on map click, optional
* @param {boolean} options.closeOnMove Close popup on map move, optional
* @param {boolean} options.focusAfterOpen Focus first focusable element in popup, optional
* @param {string} options.className Space-separated CSS classes for popup container, optional
* @param {string} options.maxWidth Popup max width CSS string e.g. '300px', 'none' for auto, default '240px'
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Randomly generate GeoJSON in map bounds, create info window layer named layer22
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createPopupDataLayer(geojson, {
* layerName: 'layer22'
* });
* console.log(layer);
*/
async createPopupDataLayer(data, options) {
options = options || {};
let type = "popup";
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
color: '#3FB1CE',
html: "<span style='color:#0000ff'>index:${props.index}</span>",
closeButton: true,
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createPopupDataLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Add mouse position control
* @param {number} options.digits Decimal places, optional
* @param {boolean} options.trackCenter Track center position, optional
* @param {boolean} options.showLatLng Show lat/lng, optional
* @param {boolean} options.showZoom Show zoom level, optional
* @param {boolean} options.showBearing Show bearing, optional
* @param {boolean} options.showPitch Show pitch, optional
* @param {'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'} options.position Control position, optional, default 'top-right'
* @example
* // Add mouse position control at bottom-right
* this.addMousePositionControl({ position: 'bottom-right' });
*/
addMousePositionControl(options) {
this.context.global.controls = this.context.global.controls || {};
if (this.context.global.controls.mousePositionControl) {
this.map.removeControl(this.context.global.controls.mousePositionControl.control);
}
let mousePositionControl = new vjmap.MousePositionControl(options);
this.map.addControl(mousePositionControl, options.position);
this.context.global.controls.mousePositionControl = {
control: mousePositionControl,
position: options.position,
options: options
};
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.removeControl(mousePositionControl);
delete this.context.global.controls.mousePositionControl;
});
}
/**
* Show list selection dialog
* @param {Array<string>} values Option list content, required
* @param {Object} options List config, optional
* @param {string|string[]} options.initValue Initial value, optional, default empty
* @param {boolean} options.isMultiSelect Multi-select, optional, default false
* @param {string} options.title Dialog title, optional
* @param {Function} options.applyCallback Callback when Apply clicked, optional
* @returns {string|string[]|undefined} Returns selected value(s), undefined if Cancel
* @example
* // Example 1
* // Show list selection, options ["a","b","c"]
* let res = await this.selectOptionsValue(["a","b","c"]);
* if (res) {
* // Single select returns string
* console.log("Selected string value:", res)
* } else {
* console.log("Clicked Cancel")
* }
* // Example 2
* // Show list selection, options ["a","b","c","d"], multi-select, title "Select", init ["a","b"]
* let arr = await this.selectOptionsValue(["a","b","c","d"], {
* isMultiSelect: true,
* title: "Select",
* initValue: ["a","b"]
* });
* if (arr) {
* // Multi-select returns array
* console.log("Selected array value:", arr)
* } else {
* console.log("Clicked Cancel")
* }
*/
selectOptionsValue(values, options) {
options = options || {};
return this.context.ui.selectOptionsValue(values, {
isMultiSelect: options.isMultiSelect || false,
initValue: options.initValue || [],
title: options.title || "Select",
applyCallback: options.applyCallback || undefined
});
}
/**
* Manually draw a point symbol
* @param {object} options Config params, optional
* @param {boolean} options.isSnap Enable snap, optional, default false
* @param {string} options.icon Symbol icon name, default _markerIcon_, optional
* @param {number} options.iconSize Symbol icon scale, default 1, optional
* @param {number} options.iconRotate Symbol icon rotation angle, default 0, optional
* @param {string} options.text Symbol text content, default empty, optional
* @param {string} options.textColor Symbol text color, default empty, optional
* @param {number} options.textSize Symbol text size, default 22, optional
* @param {number} options.textRotate Symbol text rotation angle, default 0, optional
* @param {number} options.textOpacity Symbol text opacity, default 0, optional
* @param {number} options.data Data value, optional
* @returns {Promise<Object | undefined>} Drawn entity or undefined if cancelled. Object includes:
* @example
* // Example 1
* // Manually draw a point symbol
* const pt = await this.drawSymbol(); // Enable snap to draw a point symbol
* if (!pt) return;
* // Point coordinates
* let point = pt.point;
* const x = point[0];
* const y = point[1];
* console.log(x, y);
* // Feature ID
* let id = pt.id;
* console.log(id);
* // Example 2
* // Manually draw point symbol with icon _pulsingDot_
* const pt2 = await this.drawSymbol({
* icon: "_pulsingDot_"
* });
* console.log(pt2);
* // Example 3
* // Manually draw point symbol, icon empty, text "Hello", text rotation 45
* const pt3 = await this.drawSymbol({
* text: "Hello",
* textRotate: 45
* });
* console.log(pt3);
*/
async drawSymbol(options) {
options = options || {};
let point = await this.actionDrawPoint(options.isSnap);
if (!point) return;
return this.createSymbol(point, options)
}
/**
* Manually draw an oblique rectangle
* @param {object} options Config params, optional
* @param {boolean} options.isSnap Enable snap, optional, default false
* @param {string} options.color Color #RRGGBB, optional
* @param {boolean} options.isFill Fill or not, default false, optional
* @param {number} options.opacity Opacity, default 1, optional
* @param {number} options.data Data value, optional
* @returns {Promise<Object | undefined>} Drawn entity or undefined if cancelled. Object includes:
* @example
* // Manually draw an oblique rectangle
* const rect = await this.drawSlantRectangle({
* isSnap: true,
* color: "#FF0000",
* isFill: true,
* opacity: 0.8
* });
* if (!rect) return;
* console.log("Oblique rectangle ID:", rect.id);
* console.log("Oblique rectangle point coordinates:", rect.points);
*/
async drawSlantRectangle(options) {
let points = await this.actionDrawSlantRectangle(options.isSnap);
if (!points) return;
return this.createPolygon(points, options);
}
/**
* Add minimap (overview) control
* @param {'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'} options.position Control position, optional, default 'top-right'
* @example
* // Add minimap control at bottom-left
* this.addMiniMapControl({ position: 'bottom-left' });
*/
addMiniMapControl(options) {
this.context.global.controls = this.context.global.controls || {};
if (this.context.global.controls.miniMapControl) {
this.map.removeControl(this.context.global.controls.miniMapControl.control);
}
let transformOrigin = "100% 0%";
if (options.position === 'top-left') {
transformOrigin = "0% 0%";
} else if (options.position === 'bottom-right') {
transformOrigin = "100% 100%";
} else if (options.position === 'bottom-left') {
transformOrigin = "0% 100%";
}
let miniMapControl = new vjmap.MiniMapControl({
containerStyle: {
background: "#31346a85",
transform: "scale(0.6)",
transformOrigin: transformOrigin
},
...options
});
this.map.addControl(miniMapControl, options.position);
this.context.global.controls.miniMapControl = {
control: miniMapControl,
position: options.position,
options: options
};
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.removeControl(miniMapControl);
delete this.context.global.controls.miniMapControl;
});
}
/**
* Set map rotation angle
* @param {number} bearing Rotation angle to set
* @example
* this.setBearing(45); // Set rotation to 45 degrees
*/
setBearing(bearing) {
let oldBearing = this.map.getBearing(); // Get current rotation
this.map.setBearing(bearing); // Set new rotation
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.setBearing(oldBearing);
});
}
/**
* Draw a point symbol at given coordinates
* @param {[number, number]} point Point coordinates [x, y], required
* @param {object} options Config params, optional
* @param {boolean} options.isSnap Enable snap, optional, default false
* @param {string} options.color Color #RRGGBB, optional
* @param {number} options.size Point size, optional
* @param {number} options.data Data value, optional
* @returns {Promise<Object | undefined>} Drawn entity or undefined if cancelled. Object includes:
* @example
* // Example 1
* // Draw a point at [10, 20]
* const pt = this.createPoint([10, 20]); // Enable snap to draw point at coordinates
* if (!pt) return;
* // Point coordinates
* let point = pt.point;
* const x = point[0];
* const y = point[1];
* console.log(x, y);
* // Feature ID
* let id = pt.id;
* console.log(id);
* // Example 2
* // Randomly generate 1 point in map bounds, draw point there with color red, size 10, data 123
* let points = this.randomPointInMapBounds(1);
* const pt2 = this.createPoint(points[0], {
* color: "#FF0000",
* size: 10,
* data: 123
* });
* console.log(pt2);
*/
createPoint(point, options) {
options = options || {};
let draw = this.map.getDrawLayer();
let id = vjmap.RandomID(10)
draw.add({
id,
type: 'Feature',
geometry: {
type: 'Point',
coordinates: this.map.toLngLat(point)
},
properties: {
"color_static": options.color,
"radius_static": options.size,
"data": options.data
}
});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.getDrawLayer().delete(id);
});
return {
id,
point
};
}
/**
* Set popup properties
* @param {string} id Popup id
* @param {Record<string, any>} options Other params, optional
* @param {[number, number]} options.position Coordinates [x, y], optional, default undefined
* @param {any} options.content Popup content, optional, empty means no popup
* @param {boolean} options.showInForm Show content as form, optional, default false
* @param {number} options.elevation Elevation value, optional, default 0
* @param {boolean} options.visible Visible or not, optional, default true
* @param {any} options.data Data content, optional, default empty
* @example
* // Set popup popup_1: position [10, 20], content {name: "Sensor", value: 100}, show as form
* this.setPopupProperty("popup_1", {
* position: [10, 20],
* content: {name: "Sensor", value: 100},
* showInForm: true
* });
*/
setPopupProperty(id, options) {
let popups = this.context.global.popups || [];
let item = popups.find(m => m.id == id);
if (!item) return;
let popup = item.popup;
let oldOptions = {...item.options, ...options};
if (options.position) {
popup.setLngLat(this.map.toLngLat(options.position));
}
if (options.data !== undefined) {
popup.data = options.data;
}
if (options.content !== undefined || options.showInForm !== undefined) {
const dom = this.context.ui.createElementByJsonData(options.content || oldOptions.content, options.showInForm || oldOptions.showInForm)
popup.setDOMContent(dom.element)
}
if (options.elevation !== undefined) {
popup.setHeight(options.elevation);
}
if (options.data !== undefined) {
popup.data = options.data;
}
if (options.visible !== undefined) {
if (options.visible) {
if (!popup._map) {
popup.addTo(this.map);
}
} else {
if (popup._map) {
popup.remove();
}
}
}
// Add undo record
this.context.ui.addUndoRecord(() => {
this.setPopupProperty(id, oldOptions);
});
}
/**
* Clear all selected entities
* @example
* // Clear all selected entities
* this.clearSelectedFeatures();
*/
clearSelectedFeatures() {
vjcommon.clearHighlight(this.map);
}
/**
* Set data layer click to show popup
* @param {string} layerName Layer name, required
* @param {Record<string, any>} options Other params, optional
* @param {any} options.popupContent Popup content, default current data properties
* @param {boolean} options.popupShowInForm Show popup as form, optional, default false
* @param {[number, number]} options.popupOffset Popup offset, optional, default [0, -40]
* @param {number} options.popupWidth Popup width, optional
* @param {number} options.popupHeight Popup height, optional
* @param {boolean} options.noShowPopup Do not show popup, optional, default false
* @param {Function} options.clickCallback Click callback, optional, default null (first param is popup content)
* @example
* // Example 1
* // Set layer layer1 click to show popup as form
* this.setDataLayerClickPopupContent('layer1', {
* popupShowInForm: true
* });
* // Example 2
* // Set layer layer2 click to show popup, content "ID:${props.index}"
* this.setDataLayerClickPopupContent('layer2', {
* popupContent: "ID:${props.index}",
* });
*/
setDataLayerClickPopupContent(layerName, options) {
let item = this.context.global.layers.find(layer => layer.layerName == layerName);
if(!item) return;
options = options || {};
options.isHoverPointer = options.isHoverPointer === undefined ? true : options.isHoverPointer;
let markers = item.layer.markers;
if (Array.isArray(markers)) {
// If Marker-related layer
for(let m = 0; m < markers.length; m++) {
let marker = markers[m];
marker.on('click', () => {
let popup = this.context.global.popup;
if (!popup && options.noShowPopup !== true) {
popup = new vjmap.Popup({
offset: options.popupOffset || [0, -40],
closeOnClick: false,
closeButton: true,
anchor: "bottom"
})
this.context.global.popup = popup;
}
if (options.noShowPopup !== true && options.popupOffset) {
popup.setOffset(options.popupOffset);
}
let content = options.popupContent;
if (item.data && item.data.features && item.data.features[m] && item.data.features[m].properties && typeof content == "string" && !(content in item.data.features[m].properties)) {
content = item.layer.evalValue("return `" + content + "`", item.data.features[m].properties, this.map);
if (options.noShowPopup !== true) {
popup.setHTML(content);
}
if (options.clickCallback) {
options.clickCallback(content);
}
} else {
if (!content) {
content = item.data.features[m].properties;
} else {
// If content is object property, get property value
if (item.data.features[m].properties && (content in item.data.features[m].properties)) {
content = item.data.features[m].properties[content];
try {
// If content is JSON string, try to parse
content = JSON.parse(content);
} catch (e) {}
}
}
if (options.noShowPopup !== true) {
const dom = this.context.ui.createElementByJsonData(content, options.popupShowInForm, {
width: options.popupWidth,
height: options.popupHeight
})
popup.setDOMContent(dom.element)
}
if (options.clickCallback) {
options.clickCallback(content);
}
}
if (options.noShowPopup !== true) {
popup.setLngLat(marker.getLngLat());
popup.addTo(this.map);
}
});
}
} else {
// If other layer type
let layerId = item.layerId;
if (item.layer && typeof(item.layer.getLayerId) == "function") {
layerId = item.layer.getLayerId();
}
this.map.on("click", layerId, (ev) => {
if (!ev || !ev.features || !ev.features[0]) return;
let pt = ev.lngLat;
let popup = this.context.global.popup;
if (!popup && options.noShowPopup !== true) {
popup = new vjmap.Popup({
offset: options.popupOffset || [0, -40]
})
this.context.global.popup = popup;
}
if (options.noShowPopup !== true && options.popupOffset) {
popup.setOffset(options.popupOffset);
}
let content = options.popupContent;
if (ev.features[0].properties && typeof content == "string" && !(content in ev.features[0].properties)) {
content = item.layer.evalValue("return `" + content + "`", ev.features[0].properties, this.map);
if (options.noShowPopup !== true) {
popup.setHTML(content);
}
if (options.clickCallback) {
options.clickCallback(content);
}
} else {
ev.features[0].properties = ev.features[0].properties || {}
if (!content) {
content = ev.features[0].properties;
} else {
// If content is object property, get property value
if (content in ev.features[0].properties) {
content = ev.features[0].properties[content];
try {
// If content is JSON string, try to parse
content = JSON.parse(content);
} catch (e) {}
}
}
if (options.noShowPopup !== true) {
const dom = this.context.ui.createElementByJsonData(content, options.popupShowInForm, {
width: options.popupWidth,
height: options.popupHeight
})
popup.setDOMContent(dom.element)
}
if (options.clickCallback) {
options.clickCallback(content);
}
}
if (options.noShowPopup !== true) {
popup.setLngLat(pt);
popup.addTo(this.map);
}
});
}
}
/**
* Get four parameters by manually specifying common control points
* @param {Object} [leftMapOptions] Left map options, optional, default current map
* @param {string} [leftMapOptions.mapid] Map ID
* @param {string} [leftMapOptions.version] Map version, uses current when empty
* @param {Object} [rightMapOptions] Right map options, optional, Amap
* @param {string} [rightMapOptions.mapid] Map ID
* @param {string} [rightMapOptions.version] Map version, uses current when empty
* @param {number} [leftMapOptions.webmaptype] Map type: 1 Amap, 2 Amap imagery, 3 Tianditu, 4 Tianditu imagery
* @returns {Promise<Object>} Returns four-parameter object
* @example
* // Example 1
* // Get four parameters for current map and Amap via common control points
* let fp = await this.getFourParamter();
* if (fp) {
* console.log("Four parameters", fp.fourParamter);
* }
* // Example 2
* // Get four parameters for current map and CAD map sys_world via common control points
* let fp2 = await this.getFourParamter({}, {
* mapid: "sys_world"
* });
* if (fp2) {
* console.log("Four parameters", fp2.fourParamter);
* }
*/
async getFourParamter(leftMapOptions, rightMapOptions) {
return await this.context.ui.getFourParamter(leftMapOptions, rightMapOptions);
}
/**
* Switch to web map as base
* @param {number} webmaptype Map type: 1 Amap, 2 Amap imagery, 3 Tianditu, 4 Tianditu imagery, optional, default 1
* @returns {Promise<Object>} Returns result object
* @example
* // Example 1
* // Switch to Amap as base
* let result = await this.switchWebMap(1);
* console.log("result", result);
* // Example 2
* // Switch to Tianditu imagery as base
* let result2 = await this.switchWebMap(4);
* console.log("result2", result2);
*/
switchWebMap(webmaptype) {
if (!webmaptype) webmaptype = 1;
this.clearMapData({clearAllData: true});
let prj = new vjmap.LnglatProjection();
let webMapTiles = [];
if (webmaptype == 1 || webmaptype == 2) {
webMapTiles = vjcommon.gaodeProviderTiles(webmaptype == 2);
} else if (webmaptype == 3 || webmaptype == 4) {
webMapTiles = vjcommon.tiandituProviderTiles(webmaptype == 4);
}
let tiles = webMapTiles.map(t => vjcommon.getShardsTileUrl(t, this.map))
let sourceIds = [];
let layerIds = [];
for(let t of tiles) {
let id = vjmap.RandomID();
let sourceId = "web_source_" + webmaptype + "_" + id;
let layerId = "web_layer_" + webmaptype + "_" + id;
this.map.addRasterSource(sourceId, {
type: "raster",
tiles: t,
tileSize: 256
});
this.map.addRasterLayer(layerId, sourceId);
this.map.setMapProjection(prj);
sourceIds.push(sourceId);
layerIds.push(layerId);
}
return {
sourceIds: sourceIds,
layerIds: layerIds
}
}
/**
* Get data layer property
* @param {string} layerName Layer name, required
* @returns {object | undefined} Returns layer property object, undefined if not found
* @example
* // Get data layer property for layer1
* let layerName = 'layer1';
* let property = this.getDataLayerProperty(layerName);
* console.log("Layer property:", property);
*/
getDataLayerProperty(layerName) {
let item = this.context.global.layers.find(layer => layer.layerName == layerName);
if(item) {
return item.layer.mapLayer
}
}
/**
* Get map thumbnail
* @param {object} options Parameters, optional
* @param {string} [options.mapid] Map ID, optional, current map when empty
* @param {string} [options.version] Version, optional, current version when empty
* @param {number} [options.width] Thumbnail width, optional
* @param {number} [options.height] Thumbnail height, optional
* @param {boolean} [options.darkTheme] Dark theme or not, optional
* @returns {string} Returns thumbnail URL
* @example
* // Example 1
* // Get thumbnail for map sys_hello
* let url1 = this.thumbnailUrl({
* mapid: "sys_hello"
* });
* console.log(url1);
* // Example 2
* // Get thumbnail for map sys_hello, version v1, 200x200, dark theme
* let url2 = this.thumbnailUrl({
* mapid: "sys_hello",
* version: "v1",
* width: 200,
* height: 200,
* darkTheme: true
* });
* console.log(url2);
*/
thumbnailUrl(options) {
let svc = this.map.getService();
return svc.thumbnailUrl(options.mapid, options.version, options.width, options.height, options.darkTheme);
}
/**
* Create sky layer
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {string} options.skyType Sky type: 'gradient' or 'atmosphere', default 'atmosphere'
* @param {string[]} options.skyGradient Gradient colors, when skyType is 'gradient'
* @param {[number, number]} options.skyGradientCenter Gradient center [lng, lat]
* @param {number} options.skyGradientRadius Gradient radius in degrees, 0-180, default 90
* @param {number} options.skyOpacity Sky opacity 0-1, default 1
* @param {string} options.skyAtmosphereColor Atmosphere color for scattering, default white
* @param {string} options.skyAtmosphereHaloColor Atmosphere halo color, default white
* @param {number} options.skyAtmosphereSunIntensity Sun intensity 0-100, default 10
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Create sky layer
* let layer = await this.createSkyLayer();
* console.log(layer);
*/
async createSkyLayer(options) {
options = options || {};
let type = "sky";
let randomId = vjmap.RandomID();
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
skyType: 'gradient',
skyGradient: [
'interpolate',
['linear'],
['sky-radial-progress'],
0.8,
'rgba(135, 206, 235, 1.0)',
1,
'rgba(0,0,0,0.1)'
],
skyGradientCenter: [0, 0],
skyGradientRadius: 90,
skyOpacity: [
'interpolate',
['exponential', 0.1],
['zoom'],
0,
0.3,
22,
1
],
...options,
type: type
}
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId: undefined, layerName, data: undefined, options, layer, visible: true, create: this.createSkyLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId: undefined,
layerId,
layerName
};
}
/**
* Select drawn features
* @param {boolean} selectSingle Single select only
* @returns {Promise<GeoJSON | undefined>} Returns selected GeoJSON, undefined if cancelled
* @example
* const geojson = await this.actionSelect(true);
* if (!geojson) return;
* // Iterate each feature in geojson
* for(let i = 0; i < geojson.features.length; i++) {
* let feature = geojson.features[i];
* console.log(feature);
* }
*/
async actionSelect(selectSingle) {
this.context.tools.logInfo("Please select drawn features")
let draw = this.map.getDrawLayer();
let selection = await vjmap.Draw.actionSelect(this.map, draw, {
selectSingle
});
if (selection.cancel) {
return;
}
return this.map.fromLngLat(selection);
}
/**
* Delete a popup
* @param {string} id Popup id
* @example
* this.deletePopup("popup_1"); // Delete popup popup_1
*/
deletePopup(id) {
let popups = this.context.global.popups || [];
let item = popups.find(m => m.id == id);
if (item) {
item.popup.remove()
let pt = this.map.fromLngLat(item.popup.getLngLat());
// Add undo record
this.context.ui.addUndoRecord(() => {
this.addPopup(pt, item.options);
});
}
}
/**
* Show data info dialog from JSON
* @param {object|Array} json JSON data, object or array, required
* @param {boolean} showForm Show as form, optional, default false
* @param {string} title Dialog title, optional
* @example
* // Example 1
* // Get map center X,Y and zoom, create JSON, show data info dialog, title "Map Info", as form
* let center = this.getCenter();
* const mapInfo = {
* centerX: center[0],
* centerY: center[1],
* zoom: map.getZoom()
* }
* await this.showDataInfoDialog(mapInfo, true, "Map Info");
* // Example 2
* // Rect select entities, show data info dialog with selected data, title "Entity Info", clear selection on close
* const geojson1 = await this.selectFeatures({
* isPointSel: true
* });
* await this.showFormInputDialog(geojson1, false, "Entity Info");
* // Clear all selected entities
* this.clearSelectedFeatures();
*/
async showDataInfoDialog(json, showForm, title) {
return await this.context.ui.showInfoDlg(json, showForm, title)
}
/**
* Create diffused point data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {string} options.text Text to display, optional, default "index ${props.index}"
* @param {number} options.minZoom Min zoom to display, optional, default 0
* @param {number} options.maxZoom Max zoom to display, optional, default 24
* @param {boolean} options.isDrawLinePoint Include line points when drawing, optional, default false
* @param {number} options.width Pixel width, optional, default 24
* @param {number} options.markerHeight Pixel height, optional
* @param {string} options.color1 Color value 1, optional
* @param {string} options.color2 Color value 2, optional
* @param {number} options.textFontSize Font size, optional
* @param {string} options.textColor Text color, optional
* @param {boolean} options.draggable Draggable, optional, default false
* @param {string} options.animationType Animation type, optional: MAP_ANIMATION_NONE, MAP_ANIMATION_BOUNCE, MAP_ANIMATION_DROP
* @param {number} options.rotation Rotation angle, optional
* @param {string} options.rotationAlignment Rotation alignment, optional: auto, map, viewport
* @param {string} options.pitchAlignment Pitch alignment, optional: auto, map, viewport
* @param {number} options.height Elevation value, optional
* @param {number} options.scaleMaxZoom Max zoom for scaling, optional
* @param {boolean} options.removeWhenNoInMapView Auto show/hide in view, optional
* @param {number} options.removeWhenNoInMapViewPadding Padding for auto show/hide, optional, default 500
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Randomly generate GeoJSON in map bounds, create diffused point layer named layer9
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createDiffusedMarkerLayer(geojson, {
* layerName: 'layer9'
* });
* console.log(layer);
*/
async createDiffusedMarkerLayer(data, options) {
options = options || {};
let type = "diffusedMarker";
if (options.height) {
options.height = this.map.geoLengthToHeight(options.height);
}
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
text: "ID:${props.index}",
width: 30,
markerHeight: 30,
color1: "#ff0000",
color2: "#ffff00",
textFontSize: 15,
textColor: "#00ffff",
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createDiffusedMarkerLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Draw a line from point coordinate array
* @param {[number, number][]} points Line coordinates [[x1,y1],[x2,y2],...], required
* @param {object} options Config params, optional
* @param {string} options.color Color #RRGGBB, optional
* @param {number} options.width Line width, default 1, optional
* @param {number} options.data Data value, optional
* @returns {Object} Returns drawn feature object with properties:
* @example
* // Create line from 3 random map points, width 2-5, color red
* let points = this.randomPointInMapBounds(3); // 3 points per line
* const line = this.createLineString(points, {
* color: vjmap.randomColor(),
* width: 2 + Math.random() * 3
* });
* console.log(`Line ID:`, line.id);
* console.log(`Line points:`, line.points);
*/
createLineString(points, options) {
let draw = this.map.getDrawLayer();
let id = vjmap.RandomID(10);
draw.add({
id,
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: points.map(pt => this.map.toLngLat(pt))
},
properties: {
color: options.color,
line_width: options.width,
data: options.data
}
});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.getDrawLayer().delete(id);
});
return {
id,
points
};
}
/**
* Remove map scale control
* @example
* // Remove map scale control
* this.removeMapScaleControl();
*/
removeMapScaleControl() {
this.context.global.controls = this.context.global.controls || {};
let options;
if (this.context.global.controls.scaleControl) {
options = {...this.context.global.controls.scaleControl.options}
this.map.removeControl(this.context.global.controls.scaleControl.control);
delete this.context.global.controls.scaleControl;
}
// Add undo record
this.context.ui.addUndoRecord(() => {
if (options) {
this.addMapScaleControl(options);
}
});
}
/**
* Add map navigation control
* @param {boolean} options.showCompass Show compass, optional
* @param {boolean} options.showZoom Show zoom buttons, optional
* @param {boolean} options.visualizePitch Show pitch button, optional
* @param {'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'} options.position Control position, optional, default 'top-right'
* @example
* // Add map navigation control at top-left
* this.addMapNavigationControl('top-left');
*/
addMapNavigationControl(options) {
options = options || {};
this.context.global.controls = this.context.global.controls || {};
if (this.context.global.controls.navigationControl) {
this.map.removeControl(this.context.global.controls.navigationControl.control);
}
let navigationControl = new vjmap.NavigationControl(options);
this.map.addControl(navigationControl, options.position);
this.context.global.controls.navigationControl = {
control: navigationControl,
position: options.position,
options: options
};
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.removeControl(navigationControl);
delete this.context.global.controls.navigationControl;
});
}
/**
* Unlisten map event
* @param {string} eventName Event name, required
* @param {function | undefined} callback Event callback, optional, unlisten all when empty
* @example
* // Unlisten map click event
* this.cancelListenMapEvent("click");
*/
cancelListenMapEvent(eventName, callback) {
if (eventName === "clickEntity") {
this.map.disableLayerClickHighlight();
if (this.context.global._delegatedListeners) delete this.context.global._delegatedListeners[eventName];
return;
}
if (callback) {
this.map.off(eventName, callback);
if (this.context.global._delegatedListeners && this.context.global._delegatedListeners[eventName]) {
let index = this.context.global._delegatedListeners[eventName].indexOf(callback);
if (index !== -1) {
this.context.global._delegatedListeners[eventName].splice(index, 1);
}
}
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.on(eventName, callback);
this.context.global._delegatedListeners = this.context.global._delegatedListeners || {};
this.context.global._delegatedListeners[eventName] = this.context.global._delegatedListeners[eventName] || [];
this.context.global._delegatedListeners[eventName].push(callback);
});
} else {
if (!this.context.global._delegatedListeners) return;
if (!this.context.global._delegatedListeners[eventName]) return;
const listener = this.context.global._delegatedListeners[eventName];
if (listener) {
let saveListener = [...listener];
listener.forEach(func => {
this.map.off(eventName, func);
if (this.context.global._delegatedListeners && this.context.global._delegatedListeners[eventName]) {
let index = this.context.global._delegatedListeners[eventName].indexOf(func);
if (index !== -1) {
this.context.global._delegatedListeners[eventName].splice(index, 1);
}
}
});
// Add undo record
this.context.ui.addUndoRecord(() => {
saveListener.forEach(func => {
this.map.on(eventName, func);
this.context.global._delegatedListeners = this.context.global._delegatedListeners || {};
this.context.global._delegatedListeners[eventName] = this.context.global._delegatedListeners[eventName] || [];
this.context.global._delegatedListeners[eventName].push(func);
});
});
}
}
}
/**
* Create heatmap data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @param {Array|number} options.heatmapWeight Heatmap weight, point contribution, optional, default [0,1]
* @param {Array|number} options.heatmapRadius Point radius in pixels, optional, default 30
* @param {Array|string} options.heatmapColor Heatmap color config, optional
* @param {Array|number} options.heatmapIntensity Heatmap intensity, optional, 1-3 by zoom
* @param {Array|number} options.heatmapOpacity Heatmap opacity 0-1, optional, varies by zoom
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Randomly generate GeoJSON in map bounds, create heatmap layer named layer16
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createHeatmapLayer(geojson, {
* layerName: 'layer16'
* });
* console.log(layer);
*/
async createHeatmapLayer(data, options) {
options = options || {};
let type = "heatmap";
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
// heatmapWeight: point contribution to heatmap weight, higher = more visible
heatmapWeight: [
'interpolate',
['linear'],
['get', 'value'],
0, // Normalize min/max 0,100 to 0,1 range
0,
100,
1
],
// heatmapRadius: point radius in pixels for weight calc, default 30
heatmapRadius: [
'interpolate',
['linear'],
['zoom'],
0, // zoom 0
10, // radius 10
9, // zoom 9 (linear interpolate for others)
50 // radius 50
],
// heatmapColor: color at each heatmap value
heatmapColor: [
'interpolate',
['linear'],
['heatmap-density'],
0,
'rgba(33,102,172,0)',
0.2,
'rgb(103,169,207)',
0.4,
'rgb(209,229,240)',
0.6,
'rgb(253,219,199)',
0.8,
'rgb(239,138,98)',
1,
'rgb(178,24,43)'
],
// heatmapIntensity: overall heatmap intensity, similar to heatmapWeight but for whole
heatmapIntensity: [
'interpolate',
['linear'],
['zoom'],
0,
1,
9,
3
],
// heatmapOpacity: heatmap transparency
heatmapOpacity: [
'interpolate',
['linear'],
['zoom'],
7,
1,
9,
0
],
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createHeatmapLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Generate shortest path from route, start and end points
* @param {Object} options Config params
* @param {[number, number]} options.startPoint Start [lng, lat]
* @param {[number, number]} options.endPoint End [lng, lat]
* @param {number} [options.precision] Precision, decimal places, default 0
* @param {boolean} [options.hasDirection] Consider direction, default false
* @param {boolean} [options.disableDrawPath] Disable auto draw path, default false
* @param {string} [options.drawPathColor] Path color, default "#ff0000"
* @param {number} [options.drawPathWidth] Path width, default 5
* @returns {Object | null} Shortest path info: start, end, path, layer ids; null if not found
* @example
* // Generate shortest path from route, start [116.397428, 39.90923], end [116.405285, 39.90923]
* let result = this.findShortestPath({
* startPoint: [116.397428, 39.90923],
* endPoint: [116.405285, 39.90923],
* lines: [
* {points: [[116.397428, 39.90923], [116.405285, 39.90923]]},
* {points: [[116.405285, 39.90923], [116.425285, 39.91923]]},
* ]
* });
* console.log(result);
*/
findShortestPath(options) {
options = options || {};
options.lines = options.lines || [];
options.lines = options.lines.map(line => {
return {
points: line.points.map(point => vjmap.geoPoint(point)),
id: line.id,
weight: line.weight
}
});
let result = vjmap.findShortestPath(vjmap.geoPoint(options.startPoint), vjmap.geoPoint(options.endPoint), options.lines, options.precision, options.hasDirection);
if (result.error) {
this.map.logInfo(result.error.message, "error");
return null;
}
if (options.disableDrawPath !== true) {
let polyline = new vjmap.Polyline({
data: this.map.toLngLat(result.route),
lineColor: options.drawPathColor || "#ff0000",
lineWidth: options.drawPathWidth || 5,
});
polyline.addTo(this.map);
result.lineLayerId = polyline.layerId;
result.lineSourceId = polyline.sourceId;
}
let sourceId = result.lineSourceId;
if (sourceId) {
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.removeSourceEx(sourceId);
});
}
return result;
}
/**
* Get map container size
* @returns {object} Object with width and height
*/
getContainerSize() { this.map.getContainer().getBoundingClientRect() }
/**
* Create new map from JSON data
* @param {array} drawJson JSON array of draw entity definitions, Cartesian coords, entities extend DbEntity
* @param {object} options Other params (optional)
* @example
* // Example 1
* // Create circle center [10,10], radius 5, color red
* let drawJson = [{
* "typename": "DbCircle",
* "center": [10, 10, 0],
* "radius": 5,
* "color": "#FF0000"
* }]
* // Create polyline with points [10,10] and [20,20], color green, width 2
* drawJson.push({
* "typename": "DbPolyline",
* "points": [[10, 10], [20, 20]],
* "color": "#00FF00",
* "lineWidth": 2
* })
* // Create map from drawJson above
* let mapid = await this.createMapData(drawJson);
* console.log(mapid)
* // Example 2
* // Based on sys_hello/v1, select TEXT layer, create text at [10108,10556] height 2000 "Hello, World!" center aligned, create filled triangle [[10,10],[20,20],[10,20]] fill #00FF00, layer "ly1"
* await this.createMapData([{
* "typename": "DbText",
* "position": [10108, 10556, 0],
* "height": 2000,
* "text": "Hello, World!",
* "horizontalMode": 1,
* "verticalMode": 2
* }, {
* "typename": "DbHatch",
* "points": [[10, 10], [20, 20], [10, 20]],
* "color": "#00FF00",
* "layer": "ly1"
* }], {
* "from": "sys_hello/v1",
* "pickLayers": ["TEXT"]
* });
* // Example 3
* // Create block refs at [10,10], [5000,0], [0,6000] from sys_symbols clone id 25B; 2nd rotate 45, 3rd scale 3x; add to current map
* await this.createMapData([{
* "typename": "DbBlockReference",
* "position": [10, 10],
* "cloneFromDb": "sys_symbols",
* "cloneObjectId": "25B"
* }, {
* "position": [5000, 0],
* "cloneFromDb": "sys_symbols",
* "cloneObjectId": "25B",
* "rotateMat": 45
* }, {
* "position": [0, 6000],
* "cloneFromDb": "sys_symbols",
* "cloneObjectId": "25B",
* "scaleMat": 3
* }], {
* "openway": 1
* });
*/
async createMapData(drawJson, options) {
return await this.context.tools.createMapData(this.context, drawJson, options)
}
/**
* Cancel measurement
* @example
* // Cancel measurement
* await this.cancelMeasure();
*/
cancelMeasure() {
vjcommon.runMeasureCmd(this.map, "measureCancel");
}
/**
* Listen to map event
* @param {string} eventName Event name, required
* @param {function} callback Event callback, required
* @param {boolean} isOnce Fire once only, default false, optional
* @example
* // Example 1
* // Listen to map double-click
* this.listenMapEvent("dblclick", ev => {
* console.log(ev);
* });
* // Example 2
* // Listen to map click once, add Marker at click position
* this.listenMapEvent("click", ev => {
* if (ev && ev.point) {
* // Get map pixel coords, convert to CAD geo coords
* const point = this.pixelPointToGeoPoint(ev.point);
* // Add Marker at CAD geo coords on map
* this.addMarker(point);
* }
* }, true);
* // Example 3
* // Listen to entity click, show data panel, title "Entity Data", height 500, as form
* this.listenMapEvent("clickEntity", ev => {
* if (ev) {
* this.showDataPanel(ev, { title: "Entity Data", height: 500, showForm: true });
* } else {
* this.closeDataPanel();
* }
* });
*/
listenMapEvent(eventName, callback, isOnce) {
// callback has map pixel coords, use this.pixelPointToGeoPoint(ev.point) for CAD geo coords
// Add listener to global for unlisten
this.context.global._delegatedListeners = this.context.global._delegatedListeners || {};
this.context.global._delegatedListeners[eventName] = this.context.global._delegatedListeners[eventName] || [];
this.context.global._delegatedListeners[eventName].push(callback);
if (eventName === "clickEntity") {
if (this.context.global._delegatedListeners[eventName].length === 1) {
// First listen
let oldEventValue;
this.map.enableLayerClickHighlight(this.map.getService(), e => {
if (e !== oldEventValue) {
this.context.global._delegatedListeners[eventName].forEach(func => {
func(e);
});
oldEventValue = e;
}
})
}
return;
}
if (isOnce) {
this.map.once(eventName, callback);
} else {
this.map.on(eventName, callback);
}
if (!this.context.global.__listenKeyEvent) {
// Listen to keyboard for Shift/Ctrl/Alt state
this.context.global.__listenKeyEvent = vjcommon.listenKeyEvent()
}
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.off(eventName, callback);
if (this.context.global._delegatedListeners && this.context.global._delegatedListeners[eventName]) {
let index = this.context.global._delegatedListeners[eventName].indexOf(callback);
if (index !== -1) {
this.context.global._delegatedListeners[eventName].splice(index, 1);
}
}
});
}
/**
* Open map by mapid or manual selection
* @param {object} options Parameters, optional
* @param {string} options.mapid Map id, optional, manual select when empty
* @param {string} options.version Version, optional, latest when empty
* @param {"Memory" | "GeomRaster" | "GeomVector"} options.openway Open mode: Memory, GeomRaster, GeomVector, default GeomRaster
* @param {boolean} options.lightTheme Light theme, optional, dark when empty
* @param {boolean} options.isFullExtent Full extent, optional, default false
* @returns {Object | undefined} Returns {mapid, version}, undefined if cancelled
* @example
* // Example 1
* // Open map
* this.openCadMap();
* // Example 2
* // Open mapsys_world
* this.openCadMap({
* mapid: "sys_world"
* });
* // Example 3
* // Open map sys_hello, version v1, vector mode, light theme
* this.openCadMap({
* mapid: "sys_hello",
* version: "v1",
* openway: "GeomVector",
* lightTheme: true
* });
*/
async openCadMap(options) {
options = options || {};
let mapid = options.mapid || "";
let version = options.version || "";
let mapopenway = vjmap.MapOpenWay.GeomRender;
let isVectorStyle = false;
let lightTheme = options.lightTheme ?? (this.map.getService().currentMapParam()?.darkMode === true ? false : true)
if (!mapid) {
let res = await this.context.ui.selectCadMap()
if (!res) return;
mapid = res.mapid;
version = res.version;
mapopenway = res.openway;
isVectorStyle = res.isVectorStyle;
} else {
mapopenway = options.openway === "Memory" ? vjmap.MapOpenWay.Memory : vjmap.MapOpenWay.GeomRender;
isVectorStyle = options.openway === "GeomVector" ? true : false;
}
// Open map
let res = await this.map.switchMap({
mapid: mapid,
version: version,
mapopenway: mapopenway,
style: lightTheme ? vjmap.openMapLightStyle() : vjmap.openMapDarkStyle(),
// If password-protected map, password callback; system prompt if no callback
cbInputPassword: (param) => {
return new Promise((resolve) => {
if (param.tryPasswordCount > 3) {
resolve('');// If password retry exceeds 3 times
} else {
this.messageBoxPrompt("Enter password or AccessKey", `Map ${param.mapid} requires password`, "", true)
.then((value) => {
resolve(value) ;
})
.catch(() => {
resolve('') ;
})
}
})
}
}, false, isVectorStyle, true, true);
if (options.isFullExtent !== true) {
// Use default center and zoom
let svc = this.map.getService();
let param = svc.currentMapParam();
if (param && param.view) {
if (param.view.center) {
this.map.setCenter(param.view.center);
}
if (param.view.zoom) {
this.map.setZoom(param.view.zoom);
}
if (param.view.bearing) {
this.map.setBearing(param.view.bearing);
}
}
}
if (res && res.error) {
this.showMessage(res.error)
return;
}
// New map opened, clear history
this.context.ui.clearUndoRecord();
return {
mapid: mapid,
version: version
};
}
/**
* Create blank map from bounds
* @param {number| [number,number,number,number] | GeoJSON} boundData Bounds: number (width/height), array [x1,y1,x2,y2], or GeoJSON (auto calc); empty = 10000x10000 square
* @example
* // Example 1
* // Create blank map
* this.createBlankMap();
* // Example 2
* // Create blank map from bounds [100,100,200,200]
* this.createBlankMap([100,100,200,200]);
*/
createBlankMap(boundData) {
let dataExtent;
boundData = boundData || 10000;
if (typeof boundData === "number") {
dataExtent = [-boundData/2, -boundData/2, boundData/2, boundData/2 ];
} else if (Array.isArray(boundData) && boundData.length === 4 && typeof boundData[0] === "number") {
dataExtent = vjmap.GeoBounds.fromArray(boundData);
} else {
dataExtent = vjmap.GeoBounds.fromDataExtent(boundData);
}
dataExtent = dataExtent.square(); // Map bounds must be square
let mapBounds = dataExtent.scale(4); // Scale bounds, points should stay within
// Clear map
this.clearMapData({
clearAllData: true
});
// Build geo projection from map bounds
let prj = new vjmap.GeoProjection(mapBounds);
let blankStyle = this.map.getService().rasterBlankStyle(0, 24)
let sourceId = Object.keys(blankStyle.sources)[0];
this.map.addRasterSource(sourceId, blankStyle[sourceId]);
this.map.addLayer(blankStyle.layers[0]);
this.map.setMapProjection(prj);
// New map opened, clear history
this.context.ui.clearUndoRecord();
}
/**
* Convert map pixel to CAD geo coordinates
* @returns {[number, number]} Returns [x,y] CAD geo coordinates
* @example
* // Convert map pixel [5, 10] to CAD geo coordinates
* let point = this.pixelPointToGeoPoint([5, 10]);
* let x = point[0];
* let y = point[1];
* console.log(x, y);
*/
pixelPointToGeoPoint(pt) {
let lnglat = this.map.unproject(pt);
let geoPoint = this.map.fromLngLat(lnglat);
return [geoPoint.x, geoPoint.y];
}
/**
* Remove map navigation control
* @example
* // Remove map navigation control
* this.removeMapNavigationControl();
*/
removeMapNavigationControl() {
this.context.global.controls = this.context.global.controls || {};
let options;
if (this.context.global.controls.navigationControl) {
options = {...this.context.global.controls.navigationControl.options}
this.map.removeControl(this.context.global.controls.navigationControl.control);
delete this.context.global.controls.navigationControl;
}
// Add undo record
this.context.ui.addUndoRecord(() => {
if (options) {
this.addMapNavigationControl(options);
}
});
}
/**
* Fly to target position
* @param {Object} options Fly options
* @param {[number,number]} options.center Target coordinates
* @param {number} [options.zoom] Target zoom, optional, keep current if omitted
* @param {number} [options.bearing] Bearing in degrees, 0=north, optional
* @param {number} [options.pitch] Pitch in degrees, 0=vertical, optional
* @param {number} [options.speed] Speed multiplier, default 1.2, optional
* @param {number} [options.curve] Path curvature, default 1.42, optional
* @param {boolean} [options.essential] Whether essential animation,如果为true,则不会受到用户设置的“减少运动”选项的影响。可选,默认值为true
* @param {boolean} [options.useAsync] Use async animation, default false
* @param {boolean} [options.easeTo] Use ease animation, default false
* @example
* // Example 1
* // Fly to [100,20]
* this.flyTo({center: [100,20]});
* // Example 2
* // Fly to [100,20], zoom 10, bearing 45, pitch 60, ease animation
* this.flyTo({center: [100,20], zoom: 10, bearing: 45, pitch: 60, easeTo: true});
*/
async flyTo(options) {
options = options || {};
if (options.center) {
options.center = this.map.toLngLat(options.center);
}
options.essential = options.essential === undefined ? true : options.essential;
let oldCenter = this.map.getCenter();
let oldZoom = this.map.getZoom();
let oldBearing = this.map.getBearing();
let oldPitch = this.map.getPitch();
let method = options.easeTo === true ? "easeTo" : "flyTo";
if (options.useAsync === true) {
function animateAsync(options) {
return new Promise((resolve) => {
// Listen to map moveend
this.map.once('moveend', ({ originalEvent }) => {
if (!originalEvent) {
resolve()
}
});
this.map[method](options);
});
}
await animateAsync(options);
} else {
this.map[method](options)
}
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.setCenter(oldCenter);
this.map.setZoom(oldZoom);
this.map.setBearing(oldBearing);
this.map.setPitch(oldPitch);
});
}
/**
* Create background layer
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {string} options.backgroundColor Background color, hex, default "#572782"
* @param {number} options.backgroundOpacity Background opacity 0-1, default 0.6
* @param {string} options.backgroundPattern Background pattern, image name, width must be power of 2
* @param {boolean} options.visible Layer visible, optional, default true
* @param {string} options.before Draw before specified layer, optional
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Create background layer, color #572782, opacity 0.6
* let layer = await this.createBackgroundLayer({
* backgroundColor: "#572782",
* backgroundOpacity: 0.6,
* });
* console.log(layer);
*/
async createBackgroundLayer(options) {
options = options || {};
let type = "background";
let randomId = vjmap.RandomID();
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
backgroundColor: "#572782",
backgroundOpacity: 0.6,
...options,
type: type
}
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId: undefined, layerName, data: undefined, options, layer, visible: true, create: this.createBackgroundLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId: undefined,
layerId,
layerName
};
}
/**
* Set draw feature properties
* @param {string} id Feature ID, required
* @param {object} options Config params, optional
* @param {string} options.color Color #RRGGBB, optional
* @param {string} options.borderColor Border color #RRGGBB, optional
* @param {number} options.opacity Opacity, optional
* @param {number} options.borderOpacity Border opacity, optional
* @param {number} options.width Line width, optional
* @param {number} options.extrusionHeight Extrusion height, polygon only, optional
* @param {boolean} options.isOff Hidden: false=show, true=hide, optional
* @param {boolean} options.isLocked Locked: false=unlock, true=lock, optional
* @param {string} options.text Text content, symbol text only, optional
* @param {[number, number][] | [number, number]} options.coordinates Coordinate array, optional
* @param {number} options.data Data value, optional
* @returns {Promise<Object | undefined>} Drawn entity or undefined if cancelled. Object includes:
* @example
* // Example 1
* // Select drawn feature, set color to red
* let geojson = await this.actionSelect();
* for(let feature of geojson.features) {
* this.setFeatureProperty(feature.id, {
* color: "#FF0000",
* });
* };
* // Example 2
* // Get second drawn feature, set color yellow, opacity 0.6, width 2, border red
* let geojson2 = this.getDrawAll();
* let features = geojson2.features;
* let index = 2; // Second entity
* if (index >= features.length) {
* return;
* }
* let feature = features[index];
* this.setFeatureProperty(feature.id, {
* color: "#FFFF00",
* opacity: 0.6,
* width: 2,
* borderColor: "#FF0000",
* });
*/
setFeatureProperty(id, options) {
options = options || {};
let draw = this.map.getDrawLayer();
let attr = {
"color": options.color,// color
"opacity": options.opacity, // opacity
"outlineColor": options.borderColor,// border color
"line_width": options.width, // line width
"line_opacity": options.borderOpacity, // opacity
"data": options.data,
"coordinates": options.coordinates,
"isOff": options.isOff,
"isLocked": options.isLocked,
"text": options.text,
"extrusionHeight": options.extrusionHeight,
};
for(let key in attr) {
if (attr[key] !== undefined && attr[key] !== null) {
draw.setFeatureProperty(
id,
key,
attr[key]
);
}
}
draw.forceRefresh();
}
/**
* Save temporary data
* @param {string} name Data name
* @param {any} data Data content
* @example
* // Save "123" to temp data test
* this.saveTempData("test", "123");
*/
saveTempData(name, data) {
this.context.resultData[name] = data
}
/**
* Delete data layer
* @param {string} layerName Layer name, required
* @example
* // Delete data layer named layer1
* let layerName = 'layer1';
* this.deleteDataLayer(layerName);
*/
deleteDataLayer(layerName) {
let item = this.context.global.layers.find(layer => layer.layerName == layerName);
if(item) {
let layerId = item.layerId;
let sourceId = item.sourceId;
if (item.layer && typeof(item.layer.getLayerId) == "function") {
layerId = item.layer.getLayerId();
let style = this.map.getStyle();
let layer = style.layers.find(layer => layer.id == layerId);
if (layer && layer.source) {
sourceId = layer.source;
}
}
item.layer.removeLayer(this.map, layerId);
if (sourceId) {
this.map.removeSourceEx(sourceId);
}
this.context.global.layers = this.context.global.layers.filter(layer => layer.layerId != item.layerId);
// Add undo record
this.context.ui.addUndoRecord(() => {
if (item.create) {
item.create(item.data, item.options);
}
});
}
}
/**
* Set data layer hover to show popup
* @param {string} layerName Layer name, required
* @param {Record<string, any>} options Other params, optional
* @param {any} options.popupContent Popup content, default current data properties
* @param {boolean} options.popupShowInForm Whether popup content is displayed as form. Optional, default false
* @param {[number, number]} options.popupOffset Popup offset, optional, default [0, -40]
* @param {number} options.popupWidth Popup width, optional
* @param {number} options.popupHeight Popup height, optional
* @param {boolean} options.isHoverPointer Enable hover events, optional, default true
* @param {boolean} options.noShowPopup Do not show popup, optional, default false
* @param {Function} options.hoverCallback Hover callback, optional, first param is popup content
* @example
* // Example 1
* // Set data layer with name layer1 to show popup on hover
* this.setDataLayerHoverPopupContent('layer1');
* // Example 2
* // Set layer layer1 hover popup, content "ID:${props.index}"
* this.setDataLayerHoverPopupContent('layer1', {
* popupContent: "ID:${props.index}",
* });
*/
setDataLayerHoverPopupContent(layerName, options) {
let item = this.context.global.layers.find(layer => layer.layerName == layerName);
if(!item) return;
options = options || {};
options.isHoverPointer = options.isHoverPointer === undefined ? true : options.isHoverPointer;
let markers = item.layer.markers;
if (Array.isArray(markers)) {
// If Marker-related layer
for(let m = 0; m < markers.length; m++) {
let marker = markers[m];
let oldCursor;
marker.on('mouseover', () => {
if (options.isHoverPointer) {
oldCursor = this.map.getCanvas().style.cursor;
this.map.getCanvas().style.cursor = "pointer";
}
let popup = this.context.global.popup;
if (!popup && options.noShowPopup !== true) {
popup = new vjmap.Popup({
offset: options.popupOffset || [0, -40],
closeOnClick: false,
closeButton: true,
anchor: "bottom"
})
this.context.global.popup = popup;
}
if (options.noShowPopup !== true &&options.popupOffset) {
popup.setOffset(options.popupOffset);
}
let content = options.popupContent;
if (item.data && item.data.features && item.data.features[m] && item.data.features[m].properties && typeof content == "string" && !(content in item.data.features[m].properties)) {
content = item.layer.evalValue("return `" + content + "`", item.data.features[m].properties, this.map);
if (options.noShowPopup !== true) {
popup.setHTML(content);
}
if (options.hoverCallback) {
options.hoverCallback(content);
}
} else {
if (!content) {
content = item.data.features[m].properties;
} else {
// If content is object property, get property value
if (item.data.features[m].properties && (content in item.data.features[m].properties)) {
content = item.data.features[m].properties[content];
try {
// If content is JSON string, try to parse
content = JSON.parse(content);
} catch (e) {}
}
}
if (options.noShowPopup !== true) {
const dom = this.context.ui.createElementByJsonData(content, options.popupShowInForm, {
width: options.popupWidth,
height: options.popupHeight
})
popup.setDOMContent(dom.element)
}
if (options.hoverCallback) {
options.hoverCallback(content);
}
}
if (options.noShowPopup !== true) {
popup.setLngLat(marker.getLngLat());
popup.addTo(this.map);
}
});
marker.on('mouseleave', () => {
if (options.isHoverPointer && oldCursor) {
this.map.getCanvas().style.cursor = oldCursor;
oldCursor = null;
}
if (options.noShowPopup !== true) {
let popup = this.context.global.popup;
if (popup) {
popup.remove();
this.context.global.popup = null;
}
}
if (options.hoverCallback) {
options.hoverCallback(null);
}
});
}
} else {
// If other layer type
let oldCursor;
let oldFeatureId;
let layerId = item.layerId;
let sourceId = item.sourceId;
if (item.layer && typeof(item.layer.getLayerId) == "function") {
layerId = item.layer.getLayerId();
let style = this.map.getStyle();
let layer = style.layers.find(layer => layer.id == layerId);
if (layer && layer.source) {
sourceId = layer.source;
}
}
this.map.on("mouseover", layerId, (ev) => {
if (!ev || !ev.features || !ev.features[0]) return;
if (options.isHoverPointer) {
oldCursor = this.map.getCanvas().style.cursor;
this.map.getCanvas().style.cursor = "pointer";
}
let pt = ev.lngLat;
let popup = this.context.global.popup;
if (!popup && options.noShowPopup !== true) {
popup = new vjmap.Popup({
offset: options.popupOffset || [0, -40],
closeOnClick: false,
closeButton: true,
anchor: "bottom"
})
this.context.global.popup = popup;
}
if (options.noShowPopup !== true && options.popupOffset) {
popup.setOffset(options.popupOffset);
}
let content = options.popupContent;
if (ev.features[0].properties && typeof content == "string" && !(content in ev.features[0].properties)) {
content = item.layer.evalValue("return `" + content + "`", ev.features[0].properties, this.map);
if (options.noShowPopup !== true) {
popup.setHTML(content);
}
if (options.hoverCallback) {
options.hoverCallback(content);
}
} else {
if (!content ) {
content = ev.features[0].properties;
} else {
// If content is object property, get property value
if (ev.features[0].properties && (content in ev.features[0].properties)) {
content = ev.features[0].properties[content];
try {
// If content is JSON string, try to parse
content = JSON.parse(content);
} catch (e) {}
}
}
if (options.noShowPopup !== true) {
const dom = this.context.ui.createElementByJsonData(content, options.popupShowInForm, {
width: options.popupWidth,
height: options.popupHeight
})
popup.setDOMContent(dom.element)
}
if (options.hoverCallback) {
options.hoverCallback(content);
}
}
if (options.noShowPopup !== true) {
popup.setLngLat(pt);
popup.addTo(this.map);
}
oldFeatureId = ev.features[0].id;
this.map.setFeatureState({ source: sourceId, sourceLayer: layerId, id: ev.features[0].id }, { hover: true });
});
this.map.on("mouseleave", layerId, () => {
if (options.isHoverPointer && oldCursor) {
this.map.getCanvas().style.cursor = oldCursor;
oldCursor = null;
}
if (options.noShowPopup !== true) {
let popup = this.context.global.popup;
if (popup) {
popup.remove();
this.context.global.popup = null;
}
}
if (options.hoverCallback) {
options.hoverCallback(null);
}
if (oldFeatureId) {
this.map.setFeatureState({ source: sourceId, sourceLayer: layerId, id: oldFeatureId }, { hover: false });
oldFeatureId = null;
}
});
}
}
/**
* Draw polygon from point coordinate array
* @param {[number, number][]} points Polygon coordinates [[x1,y1],[x2,y2],...], required
* @param {object} options Config params, optional
* @param {string} options.color Color #RRGGBB, optional
* @param {boolean} options.isFill Fill, default true, optional
* @param {number} options.opacity Opacity, default 1, optional
* @param {number} options.extrusionHeight Extrusion height, optional
* @param {number} options.data Data value, optional
* @returns {Object} Returns drawn feature object with properties:
* @example
* // Example 1
* // Create polygon from 3 random map points
* let points = this.randomPointInMapBounds(3); // 3 points per polygon
* points.push(points[0]); // Close polygon
* const polygon = this.createPolygon(points, {
* color: vjmap.randomColor(),
* isFill: true,
* opacity: 0.5
* });
* console.log(`Polygon ID:`, polygon.id);
* console.log(`Polygon points:`, polygon.points);
* // Example 2
* // Create polygon from 3 random points, extrusion height 1/5 of map width
* let points2 = this.randomPointInMapBounds(3); // 3 points per polygon
* points2.push(points2[0]); // Close polygon
* const polygon2 = this.createPolygon(points2, {
* color: vjmap.randomColor(),
* isFill: true,
* extrusionHeight: map.getGeoBounds().width() / 5
* });
* console.log(`Extrusion polygon ID:`, polygon2.id);
* console.log(`Extrusion polygon points:`, polygon2.points);
*/
createPolygon(points, options) {
let draw = this.map.getDrawLayer();
let id = vjmap.RandomID(10);
draw.add({
id,
type: 'Feature',
geometry: {
type: options.isFill !== false ? 'Polygon' : 'LineString',
coordinates: options.isFill !== false ? [points.map(pt => this.map.toLngLat(pt))] : points.map(pt => this.map.toLngLat(pt))
},
properties: {
color: options.color,
opacity: options.opacity,
data: options.data,
extrusionHeight: options.extrusionHeight ? this.map.geoLengthToHeight(options.extrusionHeight) : undefined
}
});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.getDrawLayer().delete(id);
});
return {
id,
points
};
}
/**
* Set map pitch angle
* @param {number} pitch Pitch angle to set
* @example
* this.setPitch(60); // Set pitch to 60 degrees
*/
setPitch(pitch) {
let oldPitch = this.map.getPitch(); // Get current pitch
this.map.setPitch(pitch); // Set new pitch
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.setPitch(oldPitch);
});
}
/**
* Manually draw a point
* @param {object} options Config params, optional
* @param {boolean} options.isSnap Enable snap, optional, default false
* @param {string} options.color Color #RRGGBB, optional
* @param {number} options.size Point size, optional
* @param {number} options.data Data value, optional
* @returns {Promise<Object | undefined>} Drawn entity or undefined if cancelled. Object includes:
* @example
* // Manually draw a point
* const pt = await this.drawPoint(); // Enable snap to draw point
* if (!pt) return;
* // Point coordinates
* let point = pt.point;
* const x = point[0];
* const y = point[1];
* console.log(x, y);
* // Feature ID
* let id = pt.id;
* console.log(id);
* // Manually draw point, color red, size 10, data 123, with snap
* const pt2 = await this.drawPoint({
* isSnap: true,
* color: "#FF0000",
* size: 10,
* data: 123
* });
* console.log(pt2);
*/
async drawPoint(options) {
options = options || {};
let point = await this.actionDrawPoint(options.isSnap);
if (!point) return;
return this.createPoint(point, options);
}
/**
* Add a popup
* @param {[number, number]} pt Coordinates [x, y]
* @param {Record<string, any>} options Other params, optional
* @param {any} options.content Popup content, optional, empty=no popup
* @param {[number, number]} options.offset Popup offset, optional, default [0, -40]
* @param {boolean} options.showInForm Show as form, optional, default false
* @param {boolean} options.isOpen Popup open, optional, default false
* @param {string} options.anchor Anchor: center, top, bottom, left, right, etc., optional
* @param {string} options.maxWidth Popup max width, optional, default 'auto'
* @param {string} options.width Popup width, optional
* @param {string} options.height Popup height, optional
* @param {string} options.className Popup CSS class, optional
* @param {boolean} options.closeButton Show close button, optional, default true
* @param {boolean} options.closeOnClick Close on outside click, optional, default false
* @param {boolean} options.closeOnMove Close on map move, optional, default false
* @param {boolean} options.focusAfterOpen Focus after open, optional, default true
* @param {number} options.elevation Elevation value, optional, default 0
* @param {boolean} options.visible Visible, optional, default true
* @param {any} options.data Data content, optional
* @returns {string} Returns popup id
* @example
* // Add popup at [30,50], content "Hello,World"
* let popupId = this.addPopup([30, 50], {
* content: "Hello,World"
* });
* console.log(popupId);
*/
addPopup(pt, options) {
options = options || {};
let popup = new vjmap.Popup({
offset: options.offset,
anchor: options.anchor,
maxWidth: options.maxWidth,
className: options.className,
closeButton: options.closeButton === undefined ? true : options.closeButton,
closeOnClick: options.closeOnClick,
closeOnMove: options.closeOnMove,
focusAfterOpen: options.focusAfterOpen
})
const dom = this.context.ui.createElementByJsonData(options.content, options.showInForm, {
width: options.width,
height: options.height
})
popup.setDOMContent(dom.element)
popup.id = "popup_" + vjmap.RandomID()
if (options.data) popup.data = options.data;
if (pt) {
popup.setLngLat(this.map.toLngLat(pt));
}
if (options.visible !== false || options.isOpen) {
popup.addTo(this.map);
}
if (options.elevation) {
popup.setHeight(options.elevation);
}
this.context.global.popups = this.context.global.popups || [];
this.context.global.popups.push({id: popup.id, popup: popup, options: options});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deletePopup(popup.id);
});
return popup.id
}
/**
* Switch to raster tile style
* @example
* // Switch to raster tile style
* this.switchVectorToRasterStyle();
*/
switchVectorToRasterStyle() {
// Skip if map already uses raster tile style
if (!this.map.hasVectorLayer()) {
return
}
this.map.switchVectorToRasterStyle(this.map.getService().rasterTileUrl());
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.switchRasterToVectorStyle(this.map.getService().vectorTileUrl());
});
}
/**
* Create point symbol data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @param {string} options.iconImage Icon name for image background
* @param {number} options.iconRotate Icon rotation angle, clockwise, degrees, default 0
* @param {[number,number]} options.iconOffset Icon offset from anchor, [x,y], positive = right/down
* @param {string} options.iconAnchor Icon anchor position, values: "center", "left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"
* @param {string} options.iconRotationAlignment Icon rotation alignment, values: "map", "viewport", "auto"
* @param {string} options.iconPitchAlignment Icon pitch alignment, values: "map", "viewport", "auto"
* @param {boolean} options.iconAllowOverlap Allow icon overlap, default false
* @param {number} options.iconOpacity Icon opacity 0-1, default 1
* @param {boolean} options.iconIgnorePlacement Ignore collision detection, default false
* @param {boolean} options.iconOptional Icon overlap option, default false
* @param {number} options.iconSize Icon scale, default 1
* @param {string} options.iconTextFit Icon text fit, values: "none", "width", "height", "both"
* @param {[number,number,number,number]} options.iconTextFitPadding Icon text fit padding [top, right, bottom, left]
* @param {number} options.iconPadding Icon padding, default 1px
* @param {boolean} options.iconKeepUpright Keep icon upright, default false
* @param {string} options.iconColor SDF icon color, default"#000000"
* @param {string} options.iconHaloColor SDF icon halo color, default"rgba(0, 0, 0, 0)"
* @param {number} options.iconHaloWidth Icon halo width, default 0
* @param {number} options.iconHaloBlur Icon halo blur, default 0
* @param {[number,number]} options.iconTranslate Icon translate [x,y]
* @param {string} options.iconTranslateAnchor Icon translate anchor, values: "map", "viewport"
* @param {string} options.textField Text content
* @param {string[]} options.textFont Text font, default["Open Sans Regular","Arial Unicode MS Regular"]
* @param {number} options.textSize Text size, default 16
* @param {boolean} options.textAllowOverlap Allow text overlap, default false
* @param {string} options.textColor Text color, default"#000000"
* @param {number} options.textOpacity Text opacity 0-1, default 1
* @param {[number,number]} options.textOffset Text offset [x,y]
* @param {number} options.textRotate Text rotation angle
* @param {number} options.textPadding Text padding, default 2
* @param {boolean} options.textKeepUpright Keep text upright, default true
* @param {number} options.textMaxWidth Text max width, default 10em
* @param {number} options.textLineHeight Text line height, default 1.2
* @param {number} options.textLetterSpacing Letter spacing, default 0
* @param {string} options.textJustify Text justify, values: "auto", "left", "center", "right"
* @param {string} options.textAnchor Text anchor position
* @param {string} options.textRotationAlignment Text rotation alignment
* @param {string} options.textPitchAlignment Text pitch alignment
* @param {number} options.textMaxAngle Max angle between chars, default 45°
* @param {string[]} options.textWritingMode Text writing mode
* @param {string} options.textTransform Text transform, values: "none", "uppercase", "lowercase"
* @param {number} options.textRadialOffset Text radial offset
* @param {string[]} options.textVariableAnchor Text variable anchor array
* @param {boolean} options.textIgnorePlacement Ignore text placement
* @param {boolean} options.textOptional Text optional
* @param {string} options.textHaloColor Text halo color
* @param {number} options.textHaloWidth Text halo width
* @param {number} options.textHaloBlur Text halo blur
* @param {[number,number]} options.textTranslate Text translate
* @param {string} options.textTranslateAnchor Text translate anchor
* @param {string} options.symbolPlacement Symbol placement, values: "point", "line", "line-center"
* @param {number} options.symbolSpacing Symbol spacing, default 250
* @param {boolean} options.symbolAvoidEdges Avoid edge collision
* @param {string} options.symbolZOrder Symbol z-order, values: "auto", "viewport-y", "source"
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Randomly generate GeoJSON in map bounds, create point symbol layer layer24, icon _markerIcon_
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createSymbolDataLayer(geojson, {
* layerName: 'layer24',
* iconImage: '_markerIcon_'
* });
* console.log(layer);
*/
async createSymbolDataLayer(data, options) {
options = options || {};
let type = "symbol";
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
if (options.textField && data.features.length > 0 && data.features[0].properties && (options.textField in data.features[0].properties)) {
// If content is a GeoJSON property, get its value
options.textField = "['get', '" + options.textField + "']";
}
let layerOptions = {
layerId,
sourceId,
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createSymbolDataLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Draw rectangle at given coordinates
* @param {[number, number, number, number]} points Rect [x1,y1,x2,y2] bottom-left and top-right, required
* @param {object} options Config params, optional
* @param {string} options.color Color #RRGGBB, optional
* @param {boolean} options.isFill Whether to fill. Optional, default false
* @param {number} options.opacity Opacity. Optional, default 1
* @param {number} options.data Data value, optional
* @returns {Object} Returns drawn feature object with properties:
* @example
* // Create 3 rects at random map positions, size 1/20 of map width, random color, fill, opacity 0.5, 2nd unfilled
* let points = this.randomPointInMapBounds(3);
* let rect = this.getMapGeoBounds(); // Get map bounds
* let width = (rect[2] - rect[0]) / 20; // Rect width 1/20 of map
* for(let i = 0; i < points.length; i++) {
* const rect = this.createRectangle([
* points[i][0], points[i][1],
* points[i][0] + width, points[i][1] + width
* ], {
* color: vjmap.randomColor(),
* isFill: i == 1 ? false : true,
* opacity: 0.5,
* data: i
* });
* console.log(`Rect ${i+1} ID:`, rect.id);
* console.log(`Rect ${i+1} coords:`, rect.points);
* }
*/
createRectangle(points, options) {
let bounds = vjmap.GeoBounds.fromArray(points);
let pts = bounds.toPointArray();
pts.push(pts[0]);
let draw = this.map.getDrawLayer();
let id = vjmap.RandomID(10);
draw.add({
id,
type: 'Feature',
geometry: {
type: options.isFill ? 'Polygon' : 'LineString',
coordinates: options.isFill ? [pts.map(pt => this.map.toLngLat(pt))] : pts.map(pt => this.map.toLngLat(pt))
},
properties: {
color: options.color,
opacity: options.opacity,
data: options.data
}
});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.getDrawLayer().delete(id);
});
return {
id,
points
};
}
/**
* Save user custom data to server
* @param {any} value Key value
* @param {any} prop Property value
* @param {number} ttl TTL in seconds, default permanent
* @returns {Promise<boolean>} Returns true on success, false on failure
* @example
* // Save custom data to server, key testKey, value testValue, prop { prop1: "value1" }
* await this.saveCustomDataServer("testKey", "testValue", { prop1: "value1" });
* // Batch save: testKey1=testValue1 prop1 3600s, testKey2=testValue2 prop2 7200s
* await this.saveCustomDataServer([
* { key: "testKey1", value: "testValue1", prop: { prop1: "value1" }, ttl: 3600 },
* { key: "testKey2", value: "testValue2", prop: { prop2: "value2" }, ttl: 7200 }
* ]);
*/
async saveCustomDataServer(key, value, prop, ttl) {
let svc = this.map.getService();
let res = await svc.saveCustomData(key, value, prop, ttl);
return res?.status === "ok";
}
/**
* Add map scale control
* @param {number} options.maxWidth Scale max width, optional
* @param {string} options.unit Scale unit, optional
* @param {'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'} options.position Control position, optional, default 'bottom-left'
* @example
* // Add scale control at bottom-right
* this.addMapScaleControl({ position: 'bottom-right' });
*/
addMapScaleControl(options) {
options = options || {};
this.context.global.controls = this.context.global.controls || {};
if (this.context.global.controls.scaleControl) {
this.map.removeControl(this.context.global.controls.scaleControl.control);
}
let scaleControl = new vjmap.ScaleControl(options);
this.map.addControl(scaleControl, options.position || 'bottom-left');
this.context.global.controls.scaleControl = {
control: scaleControl,
position: options.position,
options: options
};
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.removeControl(scaleControl);
delete this.context.global.controls.scaleControl;
});
}
/**
* Add a function button
* @param {string} name Button name, required
* @param {Function} func Function to execute, required
* @param {string} tooltip Button tooltip, optional
* @example
* // Add button "Zoom In", zoom map in by one level
* this.addButtonItem("Zoom In", () => {
* let zoom = map.getZoom();
* map.setZoom(zoom + 1);
* })
*/
addButtonItem(name, func, tooltip) {
this.context.ui.addButtonItem(name, func, tooltip);
// Add undo record
this.context.ui.addUndoRecord(() => {
this.removeButtonItem(name);
});
}
/**
* Delete drawn feature(s)
* @param {string | string[]} id Feature ID or ID array, required
* @example
* // Delete all drawn features
* const geojson = this.getDrawAll();
* if (!geojson) return;
* let ids = [];
* for(let i = 0; i < geojson.features.length; i++) {
* let feature = geojson.features[i];
* ids.push(feature.id);
* }
* this.deleteDrawFeature(ids);
*/
deleteDrawFeature(id) {
let draw = this.map.getDrawLayer();
draw.delete(id);
}
/**
* Create line extrusion layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {string} options.fillExtrusionColor Extrusion color, optional, default "#00ffff"
* @param {number} options.fillExtrusionOpacity Opacity 0-1, optional
* @param {number} options.fillExtrusionHeight Extrusion height in CAD units, optional, default 500000
* @param {number} options.fillExtrusionBase Base height in CAD units, optional, default 0
* @param {string} options.fillExtrusionPattern Fill pattern, sprite image name, width power of 2
* @param {[number,number]} options.fillExtrusionTranslate Translate [x,y], negative=left/up, default [0,0]
* @param {string} options.fillExtrusionTranslateAnchor Translate anchor: "map" or "viewport"
* @param {boolean} options.fillExtrusionVerticalGradient Vertical gradient, optional, true=darker at bottom
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @param {number} options.offsetLine Line offset size, optional, default 10
* @param {boolean} options.isBreakLine Break into segments, optional
* @param {number} options.anmiDuration Animation duration in seconds, optional, 0=no animation
* @param {number} options.anmiRepeat Animation repeat count, optional, 0=infinite
* @param {string} options.anmiRepeatType Repeat type: "reverse", "loop", "mirror"
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Randomly generate GeoJSON in map bounds, create line extrusion layer layer18
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createLineExtrusionsLayer(geojson, {
* layerName: 'layer18'
* });
* console.log(layer);
*/
async createLineExtrusionsLayer(data, options) {
options = options || {};
let type = "lineExtrusions";
if(options.fillExtrusionHeight) {
options.fillExtrusionHeight = this.map.geoLengthToHeight(options.fillExtrusionHeight);
}
if(options.fillExtrusionBase) {
options.fillExtrusionBase = this.map.geoLengthToHeight(options.fillExtrusionBase);
}
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
fillExtrusionColor: "#00ffff",
fillExtrusionHeight: this.map.geoLengthToHeight(this.map.getGeoBounds().width() / 6),
anmiDuration: 0,
offsetLine: 10,
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createLineExtrusionsLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Overlay CAD map
* @param {string} [options.mapid] Map ID
* @param {string} [options.version] Map version, uses current when empty
* @param {string} [options.layer] Layer style name, default when empty
* @param {string} [options.epsg] CAD EPSG code
* @param {"beijing54"|"xian80"|"cgs2000"} [options.crs] CAD CRS: beijing54, xian80, cgs2000
* @param {string} [options.webMapTiles] Web map tile URL when overlay
* @param {boolean} [options.overMapImage] Whether overlay is imagery
* @param {boolean} [options.isSetRotateZero] Allow rotate for control points
* @param {boolean} [options.baseMapWgs84] Base map WGS84
* @returns {Promise<string>} Returns the overlaid layer name
* @example
* // Example 1
* // Overlay CAD map, mapid is sys_world
* let layerName = await this.overlayCadMap({
* mapid: "sys_world"
* });
* console.log("layerName", layerName);
* // Example 2
* // Auto overlay CAD map, mapid is sys_cad2000
* let layerName2 = await this.overlayCadMap({
* mapid: "sys_cad2000",
* overlayType: "auto"
* });
* console.log("layerName2", layerName2);
* // Example 3
* // Direct overlay CAD map, mapid is sys_world
* let layerName3 = await this.overlayCadMap({
* mapid: "sys_world",
* overlayType: "direct"
* });
* console.log("layerName3", layerName3);
*/
async overlayCadMap(options) {
options = {...options}
// Check if base is web map
let isWebMap = this.map.getMapProjection() instanceof vjmap.LnglatProjection;
let wmsTileUrl;
if (!options.mapid) {
this.showMessage("Please set the mapid of the CAD map to overlay");
return;
}
if (isWebMap) {
// Base map is web map; web map overlays CAD only via auto or four-parameter
let epsg = options.epsg;
if (!epsg) {
epsg = await this.getCadEpsgCode(options);
}
if (options.overlayType !== "param" && epsg) {
options.epsg = epsg;
// Auto overlay
wmsTileUrl = await this.getWmsTileUrl(options);
let svc = this.map.getService();
// Convert CAD coords to web map coords and move map to CAD center
const cadToWebCoordinate = async point => {
let result = await svc.cmdTransform(epsg, "EPSG:4326", vjmap.geoPoint(point)); // Or use proj4 on frontend if not calling service
return result[0];// First result
}
let metadata = await svc.metadata(options.mapid, options.version);
let mapExtent = vjmap.GeoBounds.fromString(metadata.bounds);
let p1 = await cadToWebCoordinate(mapExtent.min);
let p2 = await cadToWebCoordinate(mapExtent.max);
this.fitMapBounds([p1[0],p1[1],p2[0],p2[1]], {padding: 0});
} else {
// Four-parameter overlay
// Four-parameter overlay: left=default map, right=CAD to overlay
let fp = await this.context.ui.getFourParamter({
mapid: options.mapid,
version: options.version,
layer: options.layer
});
if (!fp) return;
options.coordinates = [];
for(let n = 0; n < fp.leftMarkers.length; n++) {
let leftMarker = fp.leftMarkers[n];
let rightMarker = fp.rightMarkers[n];
options.coordinates.push({
x1: rightMarker.x,
y1: rightMarker.y,
x2: leftMarker.x,
y2: leftMarker.y
});
}
options.overlayType = "param";
options.isSetRotateZero = !fp.allowsRotation;
wmsTileUrl = await this.getWmsTileUrl(options);
let svc = this.map.getService();
// Convert CAD coords to web map coords and move map to CAD center
const cadToWebCoordinate = point => {
let p = vjmap.coordTransfromByFourParamter(point,{
dx: fp.fourParamter[0],
dy: fp.fourParamter[1],
scale: fp.fourParamter[2],
rotate: fp.fourParamter[3],
});
let wgs84 = vjmap.Projection.mercator2LngLat(p); // Mercator 3857 to WGS84 lng/lat
return wgs84;
}
let metadata = await svc.metadata(options.mapid, options.version);
let mapExtent = vjmap.GeoBounds.fromString(metadata.bounds);
let p1 = cadToWebCoordinate(mapExtent.min);
let p2 = cadToWebCoordinate(mapExtent.max);
this.fitMapBounds([p1[0],p1[1],p2[0],p2[1]], {padding: 0});
}
} else {
// Base map is CAD; CAD overlays CAD only via direct coords or four-parameter
if (options.overlayType == "direct") {
// Direct overlay by coordinates
// Auto overlay
wmsTileUrl = await this.getWmsTileUrl(options);
} else {
// Four-parameter overlay
// Four-parameter overlay: left=default map, right=CAD to overlay
if (!options.mapid) {
this.showMessage("Please set the mapid of the CAD map to overlay");
return;
}
let fp = await this.context.ui.getFourParamter({}, {
mapid: options.mapid,
version: options.version,
layer: options.layer
});
if (!fp) return;
options.coordinates = [];
for(let n = 0; n < fp.leftMarkers.length; n++) {
let leftMarker = fp.leftMarkers[n];
let rightMarker = fp.rightMarkers[n];
options.coordinates.push({
x1: leftMarker.x,
y1: leftMarker.y,
x2: rightMarker.x,
y2: rightMarker.y
});
}
options.overlayType = "param";
options.overMapType = "CAD";
options.isSetRotateZero = !fp.allowsRotation;
wmsTileUrl = await this.getWmsTileUrl(options);
}
}
if (!wmsTileUrl) return;
// Create raster tile layer
let layer = await this.createRasterLayer({
tiles: wmsTileUrl,
});
return layer
}
/**
* Convert CAD geometry coordinates to map pixel coordinates
* @returns {[number, number]} Returns [x,y] array for map pixel coordinates
* @example
* // Convert CAD coords [1000, 2000] to map pixel coords
* let pixel = this.geoPointToPixelPoint([1000, 2000]);
* let x = pixel[0];
* let y = pixel[1];
* console.log(x, y);
*/
geoPointToPixelPoint(pt) {
let lngLat = this.map.toLngLat(pt);
let pixelPoint = this.map.project(lngLat);
return [pixelPoint.x, pixelPoint.y];
}
/**
* Switch to vector tile style
* @example
* // Switch to vector tile style
* this.switchRasterToVectorStyle();
*/
switchRasterToVectorStyle() {
// If map already uses vector tile style, skip switch
if (this.map.hasVectorLayer()) {
return
}
this.map.switchRasterToVectorStyle(this.map.getService().vectorTileUrl());
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.switchVectorToRasterStyle(this.map.getService().rasterTileUrl());
});
}
/**
* Get all Popup info windows
* @param {string} id Popup id
* @param {[number, number]} position Popup position [x, y]
* @param {string} data Popup data
* @returns {Array<Object>} Returns all Popups, each containing:
* @example
* // Get all Popups
* let popups = this.getAllPopups();
* for(let i = 0; i < popups.length; i++) {
* let popup = popups[i];
* let x = popup.position[0];
* let y = popup.position[1];
* console.log(popup.id, x, y, popup.data);
* }
*/
getAllPopups() {
let popups = this.context.global.popups || [];
popups = popups.filter(m => m.id !== undefined);
return popups.map(m => {
let popup = m.popup;
let point = this.map.fromLngLat(popup.getLngLat());
return {
id: m.id,
position: [point.x, point.y],
data: popup.data
}
});
}
/**
* Compare map differences
* @param {Object} options Params
* @param {string} options.mapid1 Map 1 ID. Optional, use current map if empty
* @param {string} [options.version1] Map 1 version. Uses current map version if empty
* @param {string} [options.layer1] Map 1 style layer. Optional, uses current map style if empty
* @param {string} options.mapid2 Map 2 ID. Optional, uses current map if empty
* @param {string} [options.version2] Map 2 version. Uses current map version if empty
* @param {string} [options.layer2] Map 2 style layer. Optional, uses current map style if empty
* @param {boolean} [options.noCompareNew] Do not compare added parts
* @param {boolean} [options.noCompareDelete] Do not compare deleted parts
* @param {number} [options.size] Compare size. Default 4096
* @param {number} [options.cellsize] Cell size. Default 10
* @param {number} [options.diffMinPixel] Min different pixels. Default 0
* @param {number} [options.diffMinAlpha] Min different alpha. Default 25
* @param {number} [options.diffMinColor] Min different color. Default 6
* @returns {Promise<Object>} Returns compare result, undefined on failure, object with fields on success
* @example
* // Example
* // Compare current map with sys_hello
* let res = await this.findMapDiff({
* mapid2: "world"
* });
* console.log(res);
*/
async findMapDiff(options) {
options = options || {};
let svc = this.map.getService();
options.mapid1 = options.mapid1 || svc.currentMapParam().mapid;
options.mapid2 = options.mapid2 || svc.currentMapParam().mapid;
let res = await svc.cmdMapDiff(options);
if (res && res.status) {
const toGeoJSON = (points) => {
let geojson = {
type: "FeatureCollection",
features: []
};
if (!points || points.length === 0) return geojson;
points.forEach(p => {
if (p.length > 0) {
p.push(p[0]);
}
geojson.features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: p
}
});
}) ;// Close
return geojson;
}
let result = {
new: toGeoJSON(res.new),
modify: toGeoJSON(res.modify),
del: toGeoJSON(res.del),
diff: toGeoJSON([...res.new, ...res.modify, ...res.del])
};
return result;
}
return undefined;
}
/**
* Set data layer properties
* @param {string} layerName Layer name, required
* @param {object} property Layer property object, required
* @example
* // Set data layer layer1 properties to {"minZoom":1,"maxZoom":5}
* let layerName = 'layer1';
* let property = {"minZoom":1,"maxZoom":5};
* this.setDataLayerProperty(layerName, property);
*/
setDataLayerProperty(layerName, property) {
let item = this.context.global.layers.find(layer => layer.layerName == layerName);
if(item) {
let oldProp = {...vjcommon.toMapLayer(item.layer, item.layer.mapLayer)};
let layerId = item.layerId;
if (item.layer && typeof(item.layer.getLayerId) == "function") {
layerId = item.layer.getLayerId();
}
item.layer.setLayerStyle(this.map, layerId, property, oldProp);
// Add undo record
this.context.ui.addUndoRecord(() => {
this.setDataLayerProperty(item.layerName, oldProp);
});
}
}
/**
* Select entities in map
* @param {object} options Entity selection options, optional
* @param {boolean} options.useGeomCoord Use geometry coords or position coords. false=position, true=geometry. Optional, default true
* @param {boolean} options.includeWholeEntity Select whole entity. true=whole, false=partial. Optional, default false
* @param {boolean} options.isPointSel Point or box select. false=box, true=point. Optional, default false
* @param {boolean} options.disableSnap Disable snap. Optional, default false
* @param {number} options.maxSelectCount Max selection count. Optional, no limit by default
* @returns {Object} GeoJSON FeatureCollection with features (geometry, properties)
* @example
* // Example 1
* // Click to select entities on map
* const geojson1 = await this.selectFeatures({
* isPointSel: true
* });
* // Output selected entity objectids
* let selectedObjectids = [];
* for (let feature of geojson1.features) {
* selectedObjectids.push(feature.properties.objectid);
* }
* console.log(selectedObjectids);
* // Example 2
* // Box select entities, use position coords, select whole entity
* const geojson2 = await this.selectFeatures({
* useGeomCoord: false,
* includeWholeEntity: true,
* isPointSel: false
* });
* // Output selection count
* console.log(geojson2.features.length);
* // Example 3
* // Click select entities, whole entity, max 1
* const geojson3 = await this.selectFeatures({
* isPointSel: true,
* includeWholeEntity: true,
* maxSelectCount: 1
* });
* console.log(geojson3);
*/
async selectFeatures(options) {
options = options || {};
let features = await vjcommon.selectFeatures(this.map, options.useGeomCoord !== false, options.includeWholeEntity, options.isPointSel, options.disableSnap, undefined, options.maxSelectCount);
return {
features: features || [],
type: "FeatureCollection"
}
}
/**
* Split sub-maps
* @returns {Promise<string[]>} Returns fileid per sub-map (same order as clipBounds), undefined on failure
* @example
* // Auto extract sub-map bounds, split sub-maps
* let res = await this.findChildMapRects();
* if (!res) return;
* let fileids = await this.splitChildMaps({
* clipBounds: res.childMapBounds
* });
* console.log(fileids);
*/
async splitChildMaps(options) {
options = options || {};
let svc = this.map.getService();
if (!options.clipBounds || (options.clipBounds && options.clipBounds.length == 0)) {
if (this.map.logInfo) this.map.logInfo('Clip bounds is empty', "error", 10000);
return;
}
for(let n = 0; n < options.clipBounds.length; n++) {
if (Array.isArray(options.clipBounds[n]) && options.clipBounds[n].length == 4) {
options.clipBounds[n] = options.clipBounds[n].join(',');
}
}
let result = await svc.cmdSplitChildMaps({
// cloneObjects: deep clone entities, fastest (default). cloneDb: clone DB, faster. cloneMap: clone map, slower but most accurate
method: "cloneObjects",
// Use new process (doesn't block main, slower start). Default true
startNewProcess: true,
// Whether split result is full map. Default false
isFullExtent: true,
...options
});
if (result && result.error) {
if (this.map.logInfo) this.map.logInfo(result.error, "error", 10000);
return;
}
let fileids = result.fileids;
if (fileids && fileids.length > 0) {
for(let n = 0; n < fileids.length; n++) {
// Remove file extension
fileids[n] = fileids[n].split(".")[0];
}
}
return fileids;
}
/**
* Random points within map bounds
* @param {number} count Point count, default 1
* @param {number} boundsType Bounds type: 0=draw, 1=actual, 2=whole. Default 0, optional
* @returns {[number, number][]} Returns [[x1,y1],[x2,y2],...]
* @example
* // Example 1
* // Random 1 point in map bounds
* let points = this.randomPointInMapBounds(1);
* let x = points[0][0];
* let y = points[0][1];
* console.log(x, y);
* // Example 2
* // Random 10 points in actual map bounds
* let points2 = this.randomPointInMapBounds(10, 1);
* for (let i = 0; i < points2.length; i++) {
* let x = points2[i][0];
* let y = points2[i][1];
* console.log(x, y);
* }
*/
randomPointInMapBounds(count, boundsType) {
let rect = this.getMapGeoBounds(boundsType);
let geoBounds = vjmap.GeoBounds.fromArray(rect);
let points = geoBounds.randomPoints(count, count);
return points.map(p => [p.x, p.y]);
}
/**
* Capture rotated rectangle coordinates
* @param {boolean} isSnap Enable snap, default false
* @returns {Promise<number[][] | undefined>} Returns [[x1,y1],[x2,y2],[x3,y3],[x4,y4]], undefined if cancelled
* @example
* const rect = await this.actionDrawSlantRectangle();
* if (!rect) return;
* console.log(rect); // Four points of rotated rectangle
*/
async actionDrawSlantRectangle(isSnap) {
this.context.tools.logInfo("Please draw a rotated rectangle on the map")
let snapObj = {};
if (isSnap) {
vjcommon.getMapSnapPoints(this.map, snapObj);
}
let drawRect = await vjmap.Draw.actionDrawSlantRectangle(this.map, isSnap ? {
api: {
getSnapFeatures: snapObj
}
} : {});
if (drawRect.cancel) {
return;
}
let coords = drawRect.features[0].geometry.coordinates[0];
return coords.map(c => {
let p = this.map.fromLngLat(c);
return [p.x, p.y];
});
}
/**
* Create arrow line data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @param {string} options.lineColor Line color, default #FF9900
* @param {number} options.lineWidth Line width, default 10
* @param {number} options.lineOpacity Line opacity 0-1
* @param {number} options.lineGapWidth Line gap width
* @param {number} options.lineOffset Line offset
* @param {number} options.lineBlur Line blur
* @param {string} options.strokeStyle Stroke: solid or dashed
* @param {[number,number]} options.lineDasharray Dash array [1,1]
* @param {[number,number]} options.lineTranslate Translate [x,y]
* @param {string} options.lineCap Line cap: butt, round, square
* @param {string} options.lineJoin Line join: bevel, round, miter
* @param {string} options.strokeImage Line pattern image name
* @param {string} options.lineGradient Line gradient
* @param {boolean} options.showBorder Show border, default true
* @param {string} options.borderColor Border color, default #f00
* @param {number} options.borderWidth Border width
* @param {number} options.borderOpacity Border opacity 0-1
* @param {number} options.borderGapWidth Border gap width
* @param {number} options.borderOffset Border offset
* @param {number} options.borderBlur Border blur
* @param {string} options.borderGradient Border gradient
* @param {boolean} options.showDir Show arrow, default true
* @param {number} options.dirSize Arrow size
* @param {number} options.dirSpacing Arrow spacing
* @param {number} options.dirImageWidth Arrow image width
* @param {number} options.dirImageHeight Arrow image height
* @param {string} options.dirImageColor Arrow image color
* @param {string} options.dirIconColor Arrow icon color
* @param {string} options.dirLayout Arrow layout
* @param {string} options.cursor Cursor shape
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Random GeoJSON in map bounds, create arrow line layer layer5
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createArrowLineLayer(geojson, {
* layerName: 'layer5'
* });
* console.log(layer);
*/
async createArrowLineLayer(data, options) {
options = options || {};
let type = "arrowLine";
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
lineWidth: 10,
showDir: true,
showBorder: true,
borderColor: "#f00",
lineColor: '#FF9900',
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createArrowLineLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Create polygon extrusion animation data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {string} options.fillExtrusionColor Extrusion color, optional, default "#00ffff"
* @param {number} options.fillExtrusionOpacity Opacity, optional, 0-1
* @param {number} options.fillExtrusionHeight Extrusion height, CAD units, optional, default 500000
* @param {number} options.fillExtrusionBase Extrusion base height, CAD units, optional, default 0
* @param {string} options.fillExtrusionPattern Extrusion pattern, sprite image name, width must be power of 2
* @param {[number,number]} options.fillExtrusionTranslate Translate [x,y], negative=left/up, pixels, default [0,0]
* @param {string} options.fillExtrusionTranslateAnchor "map" or "viewport"
* @param {boolean} options.fillExtrusionVerticalGradient Apply vertical gradient, optional
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @param {number} options.anmiDuration Animation duration (sec), optional, default 2, 0=no animation
* @param {number} options.anmiRepeat Animation repeat count, optional, 0=infinite
* @param {string} options.anmiRepeatType "reverse", "loop", or "mirror"
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Random GeoJSON in map bounds, create polygon extrusion layer layer13
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createFillExtrusionsAnimateLayer(geojson, {
* layerName: 'layer13'
* });
* console.log(layer);
*/
async createFillExtrusionsAnimateLayer(data, options) {
options = options || {};
let type = "fillExtrusionsAnimate";
if(options.fillExtrusionHeight) {
options.fillExtrusionHeight = this.map.geoLengthToHeight(options.fillExtrusionHeight);
}
if(options.fillExtrusionBase) {
options.fillExtrusionBase = this.map.geoLengthToHeight(options.fillExtrusionBase);
}
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
fillExtrusionColor: "#00ffff",
fillExtrusionHeight: this.map.geoLengthToHeight(this.map.getGeoBounds().width() / 6),
anmiDuration: 2,
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createFillExtrusionsAnimateLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Capture polygon coordinates
* @param {boolean} isSnap Enable snap, default false
* @returns {Promise<number[][] | undefined>} Returns [[x1,y1],[x2,y2],...], undefined if cancelled
* @example
* const polygon = await this.actionDrawPolygon();
* if (!polygon) return;
* // Output all polygon point coords
* for (let i = 0; i < polygon.length; i++) {
* let x = polygon[i][0];
* let y = polygon[i][1];
* console.log(x, y);
* }
*/
async actionDrawPolygon(isSnap) {
this.context.tools.logInfo("Please draw a polygon on the map")
let snapObj = {};
if (isSnap) {
vjcommon.getMapSnapPoints(this.map, snapObj);
}
let drawPolygon = await vjmap.Draw.actionDrawPolygon(this.map, isSnap ? {
api: {
getSnapFeatures: snapObj
}
} : {});
if (drawPolygon.cancel) {
return;
}
let coords = drawPolygon.features[0].geometry.coordinates[0];
return coords.map(c => {
let p = this.map.fromLngLat(c);
return [p.x, p.y];
});
}
/**
* Set Marker properties
* @param {string} id Marker id
* @param {Record<string, any>} options Other params, optional
* @param {[number, number]} options.position Coords [x, y], optional
* @param {string} options.color Color, optional, default #3FB1CE
* @param {any} options.popupContent Popup content, optional, empty=no popup
* @param {boolean} options.popupShowInForm Whether popup content is displayed as form. Optional, default false
* @param {number} options.popupWidth Popup width, optional
* @param {number} options.popupHeight Popup height, optional
* @param {boolean} options.popupOpen Whether popup is open, optional, default false
* @param {boolean} options.draggable Draggable, optional, default false
* @param {number} options.rotation Rotation (degrees), optional, default 0
* @param {string} options.icon Custom icon name, optional
* @param {any} options.data Data, optional
* @param {number} options.elevation Elevation, optional, default 0
* @param {boolean} options.visible Visible, optional, default true
* @example
* // Set marker_1: position [10,20], red, popup "Hello, World!", icon sensor, draggable, rotation 45
* this.setMarkerProperty("marker_1", {
* position: [10, 20],
* color: "#FF0000",
* popupContent: "Hello, World!",
* icon: "sensor",
* draggable: true,
* rotation: 45,
* data: {name: "Sensor", value: 100}
* });
*/
setMarkerProperty(id, options) {
let markers = this.map.getMarkers();
let marker = markers.find(m => m.id == id);
let oldOptions = {};
if (marker) {
if (options.position) {
let position = this.map.fromLngLat(marker.getLngLat());
oldOptions.position = [position.x, position.y];
marker.setLngLat(this.map.toLngLat(options.position));
}
if (options.color) {
oldOptions.color = marker._color;
marker.setColor(options.color);
}
if (options.data !== undefined) {
oldOptions.data = marker.data;
marker.data = options.data;
}
if (options.elevation) {
oldOptions.elevation = marker.getHeight();
marker.setHeight(options.elevation);
}
if (options.popupContent) {
let popup = marker.getPopup();
if (!popup) {
popup = new vjmap.Popup({
offset: options.popupOffset || [0, -40]
})
marker.setPopup(popup)
}
const dom = this.context.ui.createElementByJsonData(options.popupContent, options.popupShowInForm, {
width: options.popupWidth,
height: options.popupHeight
})
popup.setDOMContent(dom.element)
}
if (options.popupOpen !== undefined) {
let popup = marker.getPopup();
if (popup) {
if (popup.isOpen() && !options.popupOpen) {
marker.togglePopup()
} else if (!popup.isOpen() && options.popupOpen) {
marker.togglePopup()
}
}
}
if (options.icon) {
// Manage custom icons in toolbar "Map Image Management"
let image = this.context.ui.getCustomImages(options.icon);
if (image) {
let el = marker.getElement();
if (el) {
el.className = 'marker';
let imgValue = image.value;
if (imgValue.toLowerCase().indexOf("<svg") == 0) {
// svg or base64 png
imgValue = "data:image/svg+xml;base64," + window.btoa(unescape(encodeURIComponent(imgValue))) ;
}
el.style.backgroundImage =
`url("${imgValue}")`;
el.style.width = (image.width ?? 40) + 'px';
el.style.height = (image.height ?? 40) + 'px';
el.style.backgroundSize = '100%';
}
}
}
if (options.draggable) {
oldOptions.draggable = marker.isDraggable();
marker.setDraggable(options.draggable);
}
if (options.rotation) {
oldOptions.rotation = marker.getRotation();
marker.setRotation(options.rotation);
}
if (options.visible !== undefined) {
oldOptions.visible = marker.getElement().style.display !== 'none';
marker.setVisible(options.visible, true);
}
}
// Add undo record
this.context.ui.addUndoRecord(() => {
this.setMarkerProperty(id, oldOptions);
});
}
/**
* Get all Markers
* @param {string} id Marker id
* @param {[number, number]} position Marker position [x, y]
* @param {string} data Marker data
* @returns {Array<Object>} All Markers, each containing:
* @example
* // Get all Markers
* let markers = this.getAllMarkers();
* for(let i = 0; i < markers.length; i++) {
* let marker = markers[i];
* let x = marker.position[0];
* let y = marker.position[1];
* console.log(marker.id, x, y, marker.data);
* }
*/
getAllMarkers() {
let markers = this.map.getMarkers();
markers = markers.filter(m => m.id !== undefined);
return markers.map(m => {
let point = this.map.fromLngLat(m.getLngLat());
return {
id: m.id,
position: [point.x, point.y],
data: m.data
}
});
}
/**
* Fit map to bounds
* @param {Object} [options] Fit options
* @param {boolean} [options.linear] true=Map#easeTo, false=Map#flyTo
* @param {Function} [options.easing] Easing function
* @param {Array<number>} [options.offset] Offset in pixels (default [0,0])
* @param {number} [options.maxZoom] Max zoom when fitting
* @param {boolean} [options.fitFillClip] true=fill and clip overflow
* @param {Object} [eventData] Extra properties for triggered event object
* @param {boolean} [options.isFlash] Use flash animation, default false
* @example
* // Example 1
* // Fit to [0,0,10,10] with 10px padding
* this.fitMapBounds([0,0,10,10], {padding: 10});
* // Example 2
* // Fit to full map
* this.fitMapBounds();
*/
fitMapBounds(bounds, options, eventData) {
options = options || {}
if (Array.isArray(bounds)) {
bounds = vjmap.GeoBounds.fromArray(bounds);
}
let oldBounds = this.map.fromLngLat(this.map.getBounds()); // Get current bounds
this.map.fitMapBounds(bounds, options, eventData) // Fit to bounds
if (options.isFlash === true) {
__flashBounds(typeof bounds === 'number' ? this.map.getGeoBounds(bounds) : bounds);
}
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.fitMapBounds(oldBounds);
});
}
/**
* Get user data
* @returns {Object} GeoJSON FeatureCollection (Point, LineString, Polygon, etc.)
* @example
* // Get user data and output to session
* let data = this.getUserData();
* if (data) this.chatLog(JSON.stringify(data, null, 4));
*/
getUserData() {
return this.context.userData;
}
/**
* Switch CAD layer
* @example
* // Switch CAD layer
* this.switchCadLayers();
*/
async switchCadLayers() {
// Get default layer first
let layers = this.map.getService().getMapLayers();
let layerNames = layers.map(layer => layer.name);
let openLayers = layers.filter(layer => layer.isOff === false).map(layer => layer.name);
const getSelectLayers = (selectNames) => {
let selectLayers = [];
for (let layerName of layerNames) {
let isOpen = selectNames.includes(layerName);
if (isOpen) {
selectLayers.push({name: layerName, isOff: false});
} else {
selectLayers.push({name: layerName, isOff: true});
}
}
return selectLayers;
}
let isClickApply = false;
let selectLayerNames = await this.selectOptionsValue(layerNames, {
isMultiSelect: true,
title: "Select Layer",
initValue: openLayers,
applyCallback: async (selectNames) => {
isClickApply = true;
// Switch layer on apply
await vjcommon.switchCadLayers(this.map, getSelectLayers(selectNames), this.map.hasVectorLayer());
}
});
if (!selectLayerNames) {
if (isClickApply) {
// On cancel after apply, revert to previous layer
await vjcommon.switchCadLayers(this.map, getSelectLayers(openLayers), this.map.hasVectorLayer());
}
return;
}
// Switch layer
await vjcommon.switchCadLayers(this.map, getSelectLayers(selectLayerNames), this.map.hasVectorLayer());
// Add undo record
let oldLayers = getSelectLayers(openLayers);
let isVector = this.map.hasVectorLayer();
this.context.ui.addUndoRecord(() => {
vjcommon.switchCadLayers(this.map, oldLayers, isVector);
});
}
/**
* Add a Marker
* @param {[number, number]} pt Position [x, y]
* @param {Record<string, any>} options Other params, optional
* @param {string} options.color Color, optional, default #3FB1CE
* @param {any} options.popupContent Popup content, optional, empty=no popup
* @param {[number, number]} options.popupOffset Popup offset, optional, default [0, -40]
* @param {boolean} options.popupShowInForm Popup as form. Optional, default false
* @param {boolean} options.popupOpen Whether popup is open, optional, default false
* @param {number} options.popupWidth Popup width, optional
* @param {number} options.popupHeight Popup height, optional
* @param {string} options.anchor 'center','top','bottom','left','right','top-left','top-right','bottom-left','bottom-right'. Default 'center'
* @param {boolean} options.draggable Draggable, optional, default false
* @param {number} options.rotation Rotation (degrees), optional, default 0
* @param {string} options.icon Custom icon name, optional
* @param {number} options.elevation Elevation, optional, default 0
* @param {any} options.data Data, optional
* @param {boolean} options.visible Visible, optional, default true
* @returns {string} Returns id
* @example
* // Example 1
* // Add red marker at [10,20]
* let markerId = this.addMarker([10, 20], {
* color: "#FF0000"
* });
* console.log(markerId);
* // Example 2
* // Add markers at point data positions, popup shows property values
* let data = this.getUserData();
* let features = data.features.filter(f => f.geometry.type == "Point");
* features.forEach(f => {
* this.addMarker(f.geometry.coordinates, {
* popupContent: f.properties
* });
* })
* // Example 3
* // Random point in map, create draggable marker with sensor icon
* let points = this.randomPointInMapBounds(1);
* let x = points[0][0];
* let y = points[0][1];
* this.addMarker([x, y], {
* draggable: true,
* icon: "sensor"
* });
*/
addMarker(pt, options) {
options = options || {};
let element;
if (options.icon) {
// Manage custom icons in toolbar "Map Image Management"
let image = this.context.ui.getCustomImages(options.icon);
if (image) {
let el = document.createElement('div');
el.className = 'marker';
let imgValue = image.value;
if (imgValue.toLowerCase().indexOf("<svg") == 0) {
// svg or base64 png
imgValue = "data:image/svg+xml;base64," + window.btoa(unescape(encodeURIComponent(imgValue))) ;
}
el.style.backgroundImage =
`url("${imgValue}")`;
el.style.width = (image.width ?? 40) + 'px';
el.style.height = (image.height ?? 40) + 'px';
el.style.backgroundSize = '100%';
element = el;
}
}
let marker = new vjmap.Marker({
color: options.color,
anchor: options.anchor,
draggable: options.draggable,
rotation: options.rotation,
element: element
})
marker.setLngLat(this.map.toLngLat(pt));
marker.addTo(this.map);
marker.id = "marker_" + vjmap.RandomID()
if (options.data) marker.data = options.data;
if (options.elevation) marker.setHeight(options.elevation);
if (options.popupContent) {
let popup = new vjmap.Popup({
offset: options.popupOffset || [0, -40]
})
const dom = this.context.ui.createElementByJsonData(options.popupContent, options.popupShowInForm, {
width: options.popupWidth,
height: options.popupHeight
})
popup.setDOMContent(dom.element)
marker.setPopup(popup)
if (options.popupOpen) {
marker.togglePopup()
}
}
if (options.visible !== undefined) {
marker.setVisible(options.visible, true);
}
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteMarker(marker.id);
});
return marker.id
}
/**
* Draw point symbol at coordinates
* @param {[number, number]} point Symbol position [x, y], required
* @param {object} options Config params, optional
* @param {string} options.icon Icon name, default _markerIcon_
* @param {number} options.iconSize Icon scale, default 1
* @param {number} options.iconRotate Icon rotation (deg), default 0
* @param {string} options.text Text content, default empty
* @param {string} options.textColor Text color, optional
* @param {number} options.textSize Text size, default 22
* @param {number} options.textRotate Text rotation, default 0
* @param {number} options.textOpacity Text opacity, default 0
* @param {number} options.data Data value, optional
* @returns {Promise<Object | undefined>} Drawn entity or undefined if cancelled. Object includes:
* @example
* // Example 1
* // Draw point symbol at [10, 20]
* const pt = this.createSymbol([10, 20]); // Draw point symbol with snap
* if (!pt) return;
* // Point coordinates
* let point = pt.point;
* const x = point[0];
* const y = point[1];
* console.log(x, y);
* // Feature ID
* let id = pt.id;
* console.log(id);
* // Example 2
* // Random 1 point, draw symbol with _pulsingDot_ icon
* let points = this.randomPointInMapBounds(1);
* const pt2 = this.createSymbol(points[0], {
* icon: "_pulsingDot_"
* });
* console.log(pt2);
* // Example 3
* // Random 10 points, draw symbols, text=coords, red
* let points2 = this.randomPointInMapBounds(10);
* for (let i = 0; i < points2.length; i++) {
* let coord = points2[i];
* this.createSymbol(coord, {
* icon: "",
* text: `[${coord[0].toFixed(2)}, ${coord[1].toFixed(2)}]`,
* textColor: "#FF0000"
* });
* }
*/
createSymbol(point, options) {
options = options || {};
if (!point) return;
let draw = this.map.getDrawLayer();
let id = vjmap.RandomID(10)
let icon = options.icon;
if (icon === undefined) icon = "_markerIcon_";
let onlyText = options.text && !icon;
let symbolAttr = icon ? {
"icon_image": icon,
"icon_size": options.iconSize || 1,
"icon_rotate": options.iconRotate || 0,
} : undefined;
let textAttr = options.text ? {
"text": options.text,
"text_size": options.textSize,
"text_rotate": options.textRotate,
"text_opacity": options.textOpacity,
"color": options.textColor
} : undefined
draw.add({
id,
type: 'Feature',
geometry: {
type: 'Point',
coordinates: this.map.toLngLat(point)
},
properties: {
"symbol": true,
"onlyText": onlyText,
"data": options.data,
...symbolAttr,
...textAttr,
}
});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.getDrawLayer().delete(id);
});
return {
id,
point
};
}
/**
* Show form input dialog
* @param {object} formJsonSchema Form JSON Schema config (fields, types, validation)
* @returns {object} User input object keyed by property names
* @example
* // Student form: name, age (default 18), gender
* let formJsonSchema = {
* type: "object",
* title: "Student Info",
* properties: {
* name: { type: "string", title: "Name"},
* age: { type: "number", title: "Age", default: 18},
* gender: { type: "string", title: "Gender", enum: ["Male", "Female"]}
* },
* required: ["name", "age", "gender"]
* }
* let res = await this.showFormInputDialog(formJsonSchema);
* let name = res.name;
* let age = res.age;
* let gender = res.gender;
* console.log(name, age, gender)
*/
async showFormInputDialog(formJsonSchema) {
return await this.context.ui.showFormInputDlg(formJsonSchema)
}
/**
* Create glowing point data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {string} options.text Text to display, optional, default "index ${props.index}"
* @param {number} options.minZoom Min zoom to display, optional, default 0
* @param {number} options.maxZoom Max zoom to display, optional, default 24
* @param {boolean} options.isDrawLinePoint Include line points when drawing, optional, default false
* @param {number} options.width Pixel width, optional, default 24
* @param {number} options.markerHeight Pixel height, optional
* @param {string} options.color1 Color value 1, optional
* @param {string} options.color2 Color value 2, optional
* @param {number} options.textFontSize Font size, optional
* @param {string} options.textColor Text color, optional
* @param {boolean} options.draggable Draggable, optional, default false
* @param {string} options.animationType Animation type, optional: MAP_ANIMATION_NONE, MAP_ANIMATION_BOUNCE, MAP_ANIMATION_DROP
* @param {number} options.rotation Rotation angle, optional
* @param {string} options.rotationAlignment Rotation alignment, optional: auto, map, viewport
* @param {string} options.pitchAlignment Pitch alignment, optional: auto, map, viewport
* @param {number} options.height Elevation value, optional
* @param {number} options.scaleMaxZoom Max zoom for scaling, optional
* @param {boolean} options.removeWhenNoInMapView Auto show/hide in view, optional
* @param {number} options.removeWhenNoInMapViewPadding Padding for auto show/hide, optional, default 500
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Random GeoJSON in map bounds, create glowing point layer layer14
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createFluorescenceMarkerLayer(geojson, {
* layerName: 'layer14'
* });
* console.log(layer);
*/
async createFluorescenceMarkerLayer(data, options) {
options = options || {};
let type = "fluorescenceMarker";
if (options.height) {
options.height = this.map.geoLengthToHeight(options.height);
}
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
text: "ID:${props.index}",
width: 20,
markerHeight: 20,
color1: "#ff0000",
color2: "#ffff00",
textFontSize: 15,
textColor: "#00ffff",
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createFluorescenceMarkerLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Create glowing halo data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {string} options.text Text to display, optional, default "index ${props.index}"
* @param {number} options.minZoom Min zoom to display, optional, default 0
* @param {number} options.maxZoom Max zoom to display, optional, default 24
* @param {boolean} options.isDrawLinePoint Include line points when drawing, optional, default false
* @param {number} options.width Pixel width, optional, default 24
* @param {number} options.markerHeight Pixel height, optional
* @param {string} options.color1 Color value 1, optional
* @param {string} options.color2 Color value 2, optional
* @param {number} options.textFontSize Font size, optional
* @param {string} options.textColor Text color, optional
* @param {boolean} options.draggable Draggable, optional, default false
* @param {string} options.animationType Animation type, optional: MAP_ANIMATION_NONE, MAP_ANIMATION_BOUNCE, MAP_ANIMATION_DROP
* @param {number} options.rotation Rotation angle, optional
* @param {string} options.rotationAlignment Rotation alignment, optional: auto, map, viewport
* @param {string} options.pitchAlignment Pitch alignment, optional: auto, map, viewport
* @param {number} options.height Elevation value, optional
* @param {number} options.scaleMaxZoom Max zoom for scaling, optional
* @param {boolean} options.removeWhenNoInMapView Auto show/hide in view, optional
* @param {number} options.removeWhenNoInMapViewPadding Padding for auto show/hide, optional, default 500
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Random GeoJSON in map bounds, create glowing halo layer layer15
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createHaloRingMarkerLayer(geojson, {
* layerName: 'layer15'
* });
* console.log(layer);
*/
async createHaloRingMarkerLayer(data, options) {
options = options || {};
let type = "haloRingMarker";
if (options.height) {
options.height = this.map.geoLengthToHeight(options.height);
}
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
text: "ID:${props.index}",
width: 40,
markerHeight: 40,
color1: "#ff0000",
color2: "#ffff00",
textFontSize: 15,
textColor: "#00ffff",
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createHaloRingMarkerLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Get all data layers
* @returns {Array<Object>} All data layers, each containing
* @example
* // Get all data layer names
* let layers = this.getAllDataLayers();
* let layerNames = layers.map(layer => layer.layerName);
* console.log("All data layer names:", layerNames);
*/
getAllDataLayers() {
let layers = this.context.global.layers;
if(layers) {
return layers.map(item => {
let layerId = item.layerId;
let sourceId = item.sourceId;
if (item.layer && typeof(item.layer.getLayerId) == "function") {
layerId = item.layer.getLayerId();
let style = this.map.getStyle();
let layer = style.layers.find(layer => layer.id == layerId);
if (layer && layer.source) {
sourceId = layer.source;
}
}
return {
layerId: layerId,
sourceId: sourceId,
visible: item.visible,
type: item.type,
layer: item.layer,
name: item.name
}
});
} else {
return [];
}
}
/**
* Export to PDF
* @param {Object} options PDF export options, optional
* @param {string} options.mapid Map id, default current map
* @param {string} options.version Map version, default current
* @param {boolean} options.exportDrawLayer Export draw layer, default false
* @param {boolean} options.bIncludeOffLayers Include off layers, default false
* @param {boolean} options.bSearchableSHX Searchable SHX text, default false
* @param {boolean} options.bSearchableTTF Searchable TTF text, default false
* @param {number} options.pageWidth Page width (mm), default 210 (A4)
* @param {number} options.pageHeight Page height (mm), default 297 (A4)
* @param {number} options.pageLeftMargin Left margin (mm), default 0
* @param {number} options.pageRightMargin Right margin (mm), default 0
* @param {number} options.pageTopMargin Top margin (mm), default 0
* @param {number} options.pageBottomMargin Bottom margin (mm), default 0
* @param {number} options.geomDPI Vector DPI, default 600
* @param {number} options.colorImagesDPI Image DPI, default 400
* @param {boolean} options.isBlackWhiteMode Black/white mode, default false
* @param {boolean} options.isGrayMode Gray mode, default false
* @param {boolean} options.disableOpen Return URL only, don't open, default false
* @example
* // Example 1
* // Export to PDF
* await this.exportPdf();
* // Example 2
* // Export to PDF including draw layer
* await this.exportPdf({exportDrawLayer: true});
* // Example 3
* // Export to PDF, mapid sys_world, gray mode
* await this.exportPdf({mapid: "sys_world", isGrayMode: true});
*/
async exportPdf(options) {
this.showMessage("Exporting PDF, please wait...", "info" );
options = options || {};
let mapid = options.mapid || this.map.getService().currentMapParam()?.mapid;
let version = options.version || this.map.getService().currentMapParam()?.version;
if (!options.mapid &&options.exportDrawLayer === true) {
const res = await vjcommon.exportDwg(this.map, this.map.getDrawLayer());
if (res.error) {
this.showMessage(res.error, "error" );
return;
}
mapid = res.mapid;
version = res.version;
}
let param = options; // Use defaults if omitted
/*
param = {
bIncludeOffLayers: false, // Include off layers
bSearchableSHX: false, // Searchable SHX text
bSearchableTTF: false, // Searchable TTF text
pageWidth: 210, // Width (mm)
pageHeight: 297, // Height (mm)
pageLeftMargin: 0, // Left margin (mm)
pageRightMargin: 0, // Right margin (mm)
pageTopMargin: 0, // Top margin (mm)
pageBottomMargin: 0, // Bottom margin (mm)
geomDPI: 600, // Vector DPI
colorImagesDPI: 400, // Image DPI
isBlackWhiteMode: false, // Black/white mode
isGrayMode: false, // Gray mode
}*/
let svc = this.map.getService();
const result = await svc.execCommand("exportPdf", param, mapid, version, true);
if (result.error) {
this.map.logInfo(result.error, "error");
} else {
let pdfUrl = svc.baseUrl() + result.path + "?token=" + svc.accessToken;
if (!options.disableOpen) {
window.open(pdfUrl )
}
return pdfUrl;
}
}
/**
* Show data create/edit dialog
* @param {object} options Data panel config, optional
* @param {any} options.data Data to edit, optional, empty=create
* @param {string} options.title Data panel title, optional
* @param {number} options.width Data panel width, optional
* @param {number} options.height Data panel height, optional
* @param {boolean} options.showForm Show as form, default false
* @param {Function} options.applyCallback Callback on apply, optional
* @returns {any} User input data, undefined if cancelled
* @example
* // Show create dialog, output data to console
* let res = await this.showDataModifyDlg();
* if (res) {
* console.log(res)
* }
* // Get layer1 props, show edit dialog, set layer props on apply
* let layerName = 'layer1';
* let property = this.getDataLayerProperty(layerName);
* let value = await this.showDataModifyDlg({
* data: property,
* title: "Layer Properties",
* showForm: true,
* applyCallback: (val) => {
* this.setDataLayerProperty(layerName, val);
* }
* });
* if (value) {
* this.setDataLayerProperty(layerName, value);
* }
*/
async showDataModifyDlg(options) {
options = options || {};
if (!options.data) {
// For create, show JSON input
options.showForm = false;
}
return await this.context.ui.showDataModifyDlg(options.data, options.showForm, options);
}
/**
* Capture circle coordinates
* @param {boolean} isSnap Enable snap, default false
* @returns {Promise<Object | undefined>} Center and radius, undefined if cancelled
* @example
* const circle = await this.actionDrawCircle();
* if (!circle) return;
* // Center coords
* const centerX = circle.center[0];
* const centerY = circle.center[1];
* // Radius
* const radius = circle.radius;
* console.log(centerX, centerY, radius);
*/
async actionDrawCircle(isSnap) {
this.context.tools.logInfo("Please draw a circle on the map")
let snapObj = {};
if (isSnap) {
vjcommon.getMapSnapPoints(this.map, snapObj);
}
let drawCircle = await vjmap.Draw.actionDrawCircle(this.map, isSnap ? {
api: {
getSnapFeatures: snapObj
}
} : {});
if (drawCircle.cancel) {
return;
}
let center = this.map.fromLngLat(drawCircle.features[0].properties.center);
let circlePoint = this.map.fromLngLat(drawCircle.features[0].geometry.coordinates[0][0]);
let radius = center.distanceTo(circlePoint);
return {
center: [center.x, center.y],
radius
};
}
/**
* Process geometry to create new map
* @param {object | object[]} options Options, object or array of objects
* @param {string} [options.mapid] Map ID, optional, uses current if empty
* @param {string} [options.version] Map version, uses current when empty. Optional
* @param {[number, number, number, number]} [options.clipbounds] Clip bounds [x1,y1,x2,y2], optional, no clip if empty
* @param {boolean} [options.selByCrossing] false=contain, true=intersect. Optional
* @param {[number, number, number, number]} [options.fourParameter] Four params (dx, dy, scale, rotate rad). Optional
* @param {boolean} [options.isInverseFourParamter] Inverse four params, default false
* @param {string[]} [options.layers] Layer names to show, empty=all
* @param {boolean} [options.purge] Purge after to reduce dwg size
* @param {boolean} [options.isZoomExtents] Zoom to extent after create, default false
* @param {string | "*" | "cad2000" | "cad2004" | "cad2007" | "cad2010" | "cad2013" | "cad2018"} [options.cadVersion] CAD version, * = current
* @returns {Promise<string|undefined>} Generated fileid, undefined on failure
* @example
* // Example 1
* // Create new map from geometry, layers ["0"]
* let res = await this.composeNewMap({
* layers: ["0"]
* });
* if (res) {
* this.showMessage("Map created, fileid: " + res, "success");
* }
* // Example 2
* // Combine sys_hello and sys_world maps
* let res2 = await this.composeNewMap([{
* mapid: "sys_hello"
* }, {
* mapid: "sys_world"
* }]);
* if (res2) {
* this.showMessage("Map created, fileid: " + res2, "success");
* }
*/
async composeNewMap(options) {
let svc = this.map.getService();
options = options || {};
let currentMap = svc.currentMapParam() || {};
if (Array.isArray(options)) {
for(let opt of options) {
if (!opt.mapid) {
opt.mapid = currentMap.mapid;
}
}
} else {
if (!options.mapid) {
options.mapid = currentMap.mapid;
}
}
let res = await svc.composeNewMap(options);
if (res && res.error) {
this.showMessage(res.error)
return;
}
return res.fileid;
}
/**
* Get temp data
* @param {string} name Data name
* @returns {any} Data content
* @example
* // Get temp data "test"
* this.getTempData("test");
*/
getTempData(name) {
return this.context.resultData[name]
}
/**
* Create animate line data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @param {string} options.lineColor Line color
* @param {number} options.lineWidth Line width
* @param {number} options.lineOpacity Line opacity, 0-1
* @param {number} options.lineGapWidth Line gap width (px), default 1
* @param {number} options.lineOffset Line offset (px), + right/inward, - left/outward
* @param {number} options.lineBlur Line blur, should be less than width
* @param {[number,number]} options.lineDasharray Dash array, e.g. [1,1]
* @param {[number,number]} options.lineTranslate Translate [x,y], negative=left/up, default [0,0]
* @param {string} options.lineCap Line cap: "butt", "round", "square"
* @param {string} options.lineJoin Line join: "bevel", "round", "miter"
* @param {string} options.linePattern Line pattern image, must match loaded image, width power of 2
* @param {string} options.lineGradient Line gradient
* @param {string} options.animateImagesType Animation type: "arrow", "antpath", "customImage", "imageSprites"
* @param {string} options.animateImages_arrowFillColor Arrow fill color when animateImagesType is "arrow", default "#22B14C"
* @param {string} options.animateImages_arrowStrokeColor Arrow stroke color when animateImagesType is "arrow", default "#fff"
* @param {number} options.animateImages_arrowStrokeWidth Arrow stroke width when animateImagesType is "arrow"
* @param {number} options.animateImages_arrowWidth Arrow size when animateImagesType is "arrow"
* @param {string} options.animateImages_fillColor1 Ant path fill color 1, for "antpath"
* @param {string} options.animateImages_fillColor2 Ant path fill color 2, for "antpath"
* @param {string} options.customImageUrl Custom image URL/base64, for "customImage"
* @param {string} options.imageSpriteUrl Image sprite URL/base64, for "imageSprites"
* @param {number} options.animateImages_spriteWidth Sprite width in source, for "imageSprites"
* @param {number} options.animateImages_spriteHeight Sprite height in source, for "imageSprites"
* @param {number} options.animateImages_canvasWidth Canvas width
* @param {number} options.animateImages_canvasHeight Canvas height
* @param {number} options.animateImages_frameCount Frame count
* @param {boolean} options.animateImages_directionReverse Reverse animation direction
* @param {number} options.speed Animation speed, step 0.1
* @param {boolean} options.startAutoAnimation Auto-play on start
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Random GeoJSON, create animate line layer layer3
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createAnimateLineLayer(geojson, {
* layerName: 'layer3'
* });
* console.log(layer);
*/
async createAnimateLineLayer(data, options) {
options = options || {};
let type = "animateLine";
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
lineColor: '#ff0000',
lineWidth: 10,
animateImagesType: "arrow",
animateImages_arrowFillColor: "#22B14C",
animateImages_arrowStrokeColor: "#fff",
animateImages_arrowStrokeWidth: 6,
animateImages_canvasWidth: 128,
animateImages_canvasHeight: 32,
animateImages_arrowWidth: 16,
animateImages_frameCount: 4,
animateImages_fillColor1: "#f0fb",
animateImages_fillColor2: "#0ffb",
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createAnimateLineLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Create polygon extrusion data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {string} options.fillExtrusionColor Extrusion color, optional, default "#00ffff"
* @param {number} options.fillExtrusionOpacity Opacity, optional, 0-1
* @param {number} options.fillExtrusionHeight Extrusion height (CAD units), optional, default 0
* @param {number} options.fillExtrusionBase Extrusion base height, CAD units, optional, default 0
* @param {string} options.fillExtrusionPattern Extrusion pattern, sprite image name, width must be power of 2
* @param {[number,number]} options.fillExtrusionTranslate Translate [x,y], negative=left/up, pixels, default [0,0]
* @param {string} options.fillExtrusionTranslateAnchor "map" or "viewport"
* @param {boolean} options.fillExtrusionVerticalGradient Apply vertical gradient, optional
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Random GeoJSON, create polygon extrusion layer layer12
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createFillExtrusionLayer(geojson, {
* layerName: 'layer12'
* });
* console.log(layer);
*/
async createFillExtrusionLayer(data, options) {
options = options || {};
let type = "fillExtrusion";
if(options.fillExtrusionHeight) {
options.fillExtrusionHeight = this.map.geoLengthToHeight(options.fillExtrusionHeight);
}
if(options.fillExtrusionBase) {
options.fillExtrusionBase = this.map.geoLengthToHeight(options.fillExtrusionBase);
}
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
fillExtrusionColor: "#00ffff",
fillExtrusionHeight: this.map.geoLengthToHeight(this.map.getGeoBounds().width() / 6),
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createFillExtrusionLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Output content to session window
* @param {string} content Content, supports markdown
* @example
* // Output "123" to session window
* this.chatLog("123");
*/
chatLog(content) {
return this.context.ui.chatLog(content)
}
/**
* Create draw data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {boolean} options.entColorToHtmlColor Convert color to HTML color, optional
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Random GeoJSON in map bounds, create draw data layer layer10
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createDrawDataLayer(geojson, {
* layerName: 'layer10',
* });
* console.log(layer);
*/
async createDrawDataLayer(data, options) {
options = options || {};
let type = "draw";
if (options.entColorToHtmlColor === undefined) {
for(let f of data.features) {
if (f.properties && typeof f.properties.color == "number") {
if (f.properties.color < 0) {
options.entColorToHtmlColor = true;
break;
}
}
}
}
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createDrawDataLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Export layout space
* @param {object} options Options
* @param {string} options.mapid Map ID, optional, uses current if empty
* @param {string} [options.version] Map version, uses current when empty
* @param {string} [options.layoutIndex] Layout index (layoutIndex or layoutName, index preferred). Empty=current layout
* @param {string} [options.layoutName] Layout name (layoutIndex or layoutName, index preferred). Empty=current layout
* @returns {Promise<string>} Exported fileid, empty string on failure
* @example
* // Export current layout, layout index 0
* let fileid = await this.exportLayout({
* layoutIndex: "0"
* });
* console.log(fileid);
*/
async exportLayout(options) {
let svc = this.map.getService();
options = options || {};
options.mapid = options.mapid || svc.currentMapParam().mapid;
let res = await svc.cmdExportLayout(options);
if (res && res.fileid) {
return res.fileid;
}
return "";
}
/**
* Add a menu item and handler
* @param {string} name Menu name
* @param {Function} func Handler function
* @example
* // Add menu "Fit to full map", zoom to full map
* this.addMenuItem("Full Map", () => {
* // Fit to full map
* this.fitMapBounds();
* })
*/
addMenuItem(name, func) {
// Added menu items show in map context menu by default
this.context.ui.addMenuItem(name, func);
// Add undo record
this.context.ui.addUndoRecord(() => {
this.removeMenuItem(name);
});
}
/**
* Query map data
* @param {string} sql SQL, table name geom, structure as below
* @returns {Object} GeoJSON FeatureCollection with features (geometry, properties)
* @example
* // Example 1
* // Query line type, layer index 2
* let result1 = await this.queryMapData("SELECT * FROM geom WHERE name = 'AcDbLine' AND layerIndex = 2");
* if (result1.features.length > 0) {
* let feature = result1.features[0];
* // Get line width
* console.log(feature.properties.lineWidth)
* // Get all Point coords, draw Marker at each
* const addMarker = (co) => {
* // co format [x, y]
* this.addMarker(co)
* }
* const processGeoJSONFeature = (geometry) => {
* if (geometry.type === "Point") {
* let co = geometry.coordinates;
* addMarker(co)
* } else if (geometry.type === "MultiPoint") {
* geometry.coordinates.forEach(co => addMarker(co))
* } else if (geometry.type === "LineString") {
* geometry.coordinates.forEach(co => addMarker(co))
* } else if (geometry.type === "MultiLineString") {
* geometry.coordinates.forEach(line => {
* line.forEach(co => addMarker(co))
* })
* } else if (geometry.type === "Polygon") {
* geometry.coordinates.forEach(line => {
* line.forEach(co => addMarker(co))
* })
* } else if (geometry.type === "MultiPolygon") {
* geometry.coordinates.forEach(polygon => {
* polygon.forEach(line => {
* line.forEach(co => addMarker(co))
* })
* })
* }
* }
* if (feature.geometry.type === "GeometryCollection") {
* feature.geometry.geometries.forEach(ft => {
* processGeoJSONFeature(ft)
* })
* } else {
* processGeoJSONFeature(feature.geometry)
* }
* }
* // Example 2
* // Query all text, geometry coords
* let result2 = await this.queryMapData("SELECT * FROM geom WHERE (name = 'AcDbText' or name = 'AcDbMText' or name = 'AcDbAttributeDefinition' or name = 'AcDbAttribute') and coordType = true");
* if (result2.features.length > 0) {
* result2.features.forEach(feature => {
* // Get text content
* console.log(feature.properties.content)
* });
* }
* // Query lines in rect [10,10,20,20], position coords
* let result3 = await this.queryMapData("SELECT * FROM geom WHERE (name = 'AcDbLine' or name = 'AcDbPolyline' or name = 'AcDb2dPolyline' or name = 'AcDb3dPolyline') and bounds ='[10, 10, 20, 20]' and isContains = true and coordType = false");
* // Get total record count
* console.log(result3.features.length)
*/
queryMapData(sql) {
return this.context.tools.queryMapData(this.context, sql)
}
/**
* Auto extract sub-map bounds
* @returns {Promise<Object>} undefined on failure, else GeoJSON and bounds array
* @example
* // Auto extract sub-map bounds
* let res = await this.findChildMapRects();
* console.log(res);
*/
async findChildMapRects(options) {
options = options || {};
let svc = this.map.getService();
options = {
mapid: options.mapid || svc.currentMapParam().mapid,
bounds: "",
condition: "",
debugInfo: [],
digit: 4,
tol: 0,
tableEdgeMinPoint: 12,
tableTextMinCount: 4,
cellMaxArea: 90,
cellEmptyRatio: 90,
unLinkLineRatio: 20,
tableMaxCellCount: 100000,
noHvLineSegCount: 0,
seachTableMost: false,
version: svc.currentMapParam()?.version,
layer: svc.currentMapParam()?.layer,
// Use expression query for memory, spatial query for geometry render
geom: svc.currentMapParam()?.mapopenway != vjmap.MapOpenWay.Memory,
...options,
};
if (options.isSelectBounds) {
// For manual mode, select area first
if (this.map.logInfo) this.map.logInfo("Please select rectangular area covering the table", "success", 10000);
// Draw rectangle on map as clip region
let snapObj = {}; // Snap entities
vjcommon.getMapSnapPoints(this.map, snapObj);
let res = await vjmap.Draw.actionDrawRectangle(this.map, {
api: {
getSnapFeatures: snapObj // Snap features assigned via features property
},
contextMenu: (e) => {
// Send Enter to map
this.map.fire("keyup", {keyCode:13})
}
});
if (res.cancel) {
return // Cancelled
}
// Get drawn coordinates
let rectCoord = this.map.fromLngLat(res.features[0].geometry.coordinates[0]);
// Get bottom-left and bottom-right points
let bounds = [rectCoord[0].x, rectCoord[0].y, rectCoord[2].x, rectCoord[2].y];
options.bounds = bounds.join(',');
}
if (Array.isArray(options.bounds)) {
options.bounds = JSON.stringify(options.bounds);
}
if (options.showSettingForm === true) {
options = await this.showDataModifyDlg({
data: options,
title: "Auto-extract table settings",
showForm: true
})
if (!options) return;
}
// Get sub-map bounds only
options.findChildMapRects = true;
let result = await svc.cmdExtractTale(options);
if (result && result.error) {
if (this.map.logInfo) this.map.logInfo(result.error, "error", 10000);
return;
}
if (!result.childMapRects) return;
let geoDatas = {
"type": "FeatureCollection",
"features": []
}
let childMapBounds = [];
for (let i = 0; i < result.childMapRects.length; i++) {
let bounds = vjmap.GeoBounds.fromArray(result.childMapRects[i].bounds);
childMapBounds.push(bounds.toString())
let points = bounds.toPointArray();
points.push(points[0]); // Close
geoDatas.features.push({
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [points]
},
properties: {
color: vjmap.randomColor(),
index: i,
data: result.childMapRects[i]
}
});
}
return {
geojson: geoDatas,
childMapBounds: childMapBounds
}
}
/**
* Draw circle by center and radius
* @param {[number, number]} center Center [x,y], required
* @param {number} radius Radius, required
* @param {object} options Config params, optional
* @param {string} options.color Color #RRGGBB, optional
* @param {boolean} options.isFill Whether to fill. Optional, default false
* @param {number} options.opacity Opacity. Optional, default 1
* @param {number} options.data Data value, optional
* @returns {Object} Returns drawn feature object with properties:
* @example
* // Random circle in map, radius 1/40 map width, random color, fill, opacity 0.5
* let points = this.randomPointInMapBounds(1);
* let rect = this.getMapGeoBounds(); // Get map bounds
* let width = (rect[2] - rect[0]) / 40; // Width 1/40 of map
* const circle = this.createCircle(points[0], width, {
* color: vjmap.randomColor(),
* isFill: true,
* opacity: 0.5
* });
* console.log(`Circle ID:`, circle.id);
* console.log(`Circle center:`, circle.center);
* console.log(`Circle radius:`, circle.radius);
*/
createCircle(center, radius, options) {
let draw = this.map.getDrawLayer();
let coords = vjmap.getCirclePolygonCoordinates(center, radius);
coords = coords.map(c => this.map.toLngLat(c));
let id = vjmap.RandomID(10);
draw.add({
id,
type: 'Feature',
geometry: {
type: options.isFill ? 'Polygon' : 'LineString',
coordinates: options.isFill ? [coords] : coords
},
properties: {
color: options.color,
opacity: options.opacity,
data: options.data
}
});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.getDrawLayer().delete(id);
});
return {
id,
center,
radius
};
}
/**
* Convert map lng/lat to CAD geometry coordinates
* @returns {[number, number]} [x,y] CAD geometry coordinates
* @example
* // Convert lng/lat [10, 20] to CAD geometry coords
* let point = this.lngLatToGeoPoint([10, 20]);
* let x = point[0];
* let y = point[1];
* console.log(x, y);
*/
lngLatToGeoPoint(lngLat) {
let geoPoint = this.map.fromLngLat(lngLat);
return [geoPoint.x, geoPoint.y];
}
/**
* Flash by bounds
* @param {[number,number,number,number]} bounds Bounds
* @param {Object} options Config params, optional
* @param {string} options.color Color #RRGGBB, default #FFFF00, optional
* @param {number} options.width Line width, default 3, optional
* @param {number} options.duration Animation duration (ms), default 1000, optional
* @example
* this.flashBounds([0,0,10,10]);
*/
flashBounds(bounds, options) {
options = options || {};
return new Promise((resolve => {
if (Array.isArray(bounds) && bounds.length === 4 && typeof bounds[0] === "number") {
bounds = vjmap.GeoBounds.fromArray(bounds);
} else {
bounds = vjmap.GeoBounds.fromDataExtent(bounds);
}
const routePath = [];
routePath.push(bounds.min);
routePath.push(vjmap.geoPoint([bounds.max.x, bounds.min.y]));
routePath.push(bounds.max);
routePath.push(vjmap.geoPoint([bounds.min.x, bounds.max.y]));
routePath.push(bounds.min);
let geoLineDatas = [];
geoLineDatas.push({
points: this.map.toLngLat(routePath),
properties: {
opacity: 1.0
}
})
let polylines = new vjmap.Polyline({
data: geoLineDatas,
lineColor: options.color || 'yellow',
lineWidth: options.width || 3,
lineOpacity: ['get', 'opacity'],
isHoverPointer: false,
isHoverFeatureState: false
});
polylines.addTo(this.map);
vjmap.createAnimation({
from: 1,
to: 10,
duration: options.duration || 1000,
onUpdate: (e) => {
const data = polylines.getData();
data.features[0].properties.opacity = parseInt(e) % 2 ? 1.0 : 0 ;
polylines.setData(data);
},
onStop: () => {
polylines.remove()
resolve({})
},
onComplete: () => {
polylines.remove()
resolve({})
}
})
}))
}
/**
* /**
* @returns {Object} Object with Shift, Ctrl, Alt key pressed (boolean)
* @example
* // Example 1
* // Is Shift pressed
* let key = this.isShiftCtrlAltKeyPressed();
* if (key.shift) {
* console.log("Shift pressed");
* }
* // Example 2
* // On map click, if Ctrl pressed get CAD coords and show data dialog "Click position"
* this.listenMapEvent("click", async ev => {
* // Check if Ctrl pressed
* let key = this.isShiftCtrlAltKeyPressed();
* if (key.ctrl) {
* if (ev && ev.point) {
* // Get map pixel coords, convert to CAD geo coords
* const point = this.pixelPointToGeoPoint(ev.point);
* // Show data dialog at position, title "Click position", form display
* await this.showDataInfoDialog(point, true, "Click position");
* }
* }
* }, true);
*/
isShiftCtrlAltKeyPressed() {
let shift = false, ctrl = false, alt = false;
if (this.context.global.__listenKeyEvent) {
shift = this.context.global.__listenKeyEvent.isShiftKey();
ctrl = this.context.global.__listenKeyEvent.isCtrlKey();
alt = this.context.global.__listenKeyEvent.isAltKey();
}
return {
shift: shift,
ctrl: ctrl,
alt: alt,
};
}
/**
* Enable or disable map rotation, pitch, wheel zoom, pan, double-click zoom
* @param {boolean} options.rotation Allow rotation, optional
* @param {boolean} options.pitch Allow pitch, optional
* @param {boolean} options.zoom Allow wheel zoom, optional
* @param {boolean} options.pan Allow pan, optional
* @param {boolean} options.dblclickZoom Allow double-click zoom, optional
* @example
* // Disable rotation, pitch, dblclick zoom; allow wheel zoom
* this.setMapInteraction({rotation: false, pitch: false, zoom: true, dblclickZoom: false});
*/
setMapInteraction(options) {
options = options || {};
if (options.rotation === true) {
this.map.dragRotate.enable();
this.map.touchZoomRotate.enable();
this.map.keyboard.enableRotation();
} else if (options.rotation === false) {
this.map.dragRotate.disable();
this.map.touchZoomRotate.disable();
this.map.keyboard.disable();
}
if (options.pitch === true) {
this.map.setMinPitch(this.map.getPitch());
this.map.setMaxPitch(this.map.getPitch());
} else if (options.pitch === false) {
this.map.setMinPitch(0);
this.map.setMaxPitch(90);
}
if (options.zoom === true) {
this.map.scrollZoom.enable();
this.map.touchZoomRotate.enable();
} else if (options.rotation === false) {
this.map.scrollZoom.disable();
this.map.touchZoomRotate.disable();
}
if (options.dblclickZoom === true) {
this.map.doubleClickZoom.enable();
} else if (options.dblclickZoom === false) {
this.map.doubleClickZoom.disable();
}
if (options.pan === true) {
this.map.dragPan.enable();
this.map.touchZoomRotate.enable();
} else if (options.pan === false) {
this.map.dragPan.disable();
this.map.touchZoomRotate.disable();
}
}
/**
* Create div overlay layer
* @param {[number,number,number,number] | [[number,number],[number,number]] | [[number,number],[number,number],[number,number],[number,number]]} data Bounds: [x1,y1,x2,y2], [[x1,y1],[x2,y2]], or 4 corners
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {number} options.width Div width (px)
* @param {number} options.height Div height (px)
* @param {string} options.customImage Image name
* @param {string} options.customHtml JS code for custom HTML, returns DOM element
* @param {number} options.minZoom Min zoom 0-24
* @param {number} options.maxZoom Max zoom 0-24
* @param {number} options.maxPitch Max pitch 0-90
* @param {boolean} options.updateDivSize Auto update div size, useful for SVG zoom
* @param {number} options.maxDivSize Max div size (px), beyond uses pixel scale
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Capture rect, create div overlay layer30, image picture1
* const rect = await this.actionDrawRectangle();
* if (!rect) return;
* let layer = await this.createDivOverlayLayer(rect,{
* layerName: 'layer30',
* customImage: 'picture1'
* });
* console.log(layer);
*/
async createDivOverlayLayer(data, options) {
options = options || {};
let type = "divOverlay";
let randomId = vjmap.RandomID();
let layerId = `layer_${type}_${randomId}`;
options.bounds = data;
if (options.customImage) {
let image = this.context.ui.getCustomImages(options.customImage);
if (image) {
options.customImage = image.value;
options.width = image.width;
options.height = image.height;
}
}
if (Array.isArray(options.bounds) && options.bounds.length == 4 && typeof options.bounds[0] == 'number') {
options.bounds = vjmap.GeoBounds.fromArray(options.bounds).toPointArray();
}
let layerOptions = {
layerId,
...options,
type: type
}
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId: undefined, layerName, data: undefined, options, layer, visible: true, create: this.createDivOverlayLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId: undefined,
layerId,
layerName
};
}
/**
* Manually draw a circle
* @param {object} options Config params, optional
* @param {boolean} options.isSnap Enable snap, optional, default false
* @param {string} options.color Color #RRGGBB, optional
* @param {boolean} options.isFill Whether to fill. Optional, default false
* @param {number} options.opacity Opacity. Optional, default 1
* @param {number} options.data Data value, optional
* @returns {Promise<Object | undefined>} Drawn entity or undefined if cancelled. Object includes:
* @example
* // Draw a circle manually
* const circle = await this.drawCircle({
* isSnap: true,
* color: "#FF0000",
* isFill: true,
* opacity: 0.8
* });
* if (!circle) return;
* console.log("Circle ID:", circle.id);
* console.log("Circle center:", circle.center);
* console.log("Circle radius:", circle.radius);
*/
async drawCircle(options) {
let circle = await this.actionDrawCircle(options.isSnap);
if (!circle) return;
return this.createCircle(circle.center, circle.radius, options);
}
/**
* Export image for given bounds
* @param {[number, number, number, number]} bounds [minx, miny, maxx, maxy]
* @param {object} options Export options, optional
* @param {number} options.pictureWidth Image width (px), default 1024
* @param {number} options.scale Scale, default 1.01
* @param {boolean} options.transparent Transparent background, default false
* @param {string} options.backgroundColor Background color, default 'rgb(0,0,0)'
* @param {boolean} options.disableOpen Return URL only, don't open, default false
* @example
* // Export map bounds [100,200,300,400] as PNG
* this.exportPng([100, 200, 300, 400]);
*/
exportPng(bounds, options) {
options = options || {};
if (Array.isArray(bounds) && bounds.length === 4 && typeof bounds[0] === 'number') {
bounds = vjmap.GeoBounds.fromArray(bounds);
} else {
bounds = vjmap.GeoBounds.fromDataExtent(bounds);
}
bounds = bounds.scale(options.scale || 1.01); // Slightly larger so border is visible
let pictureWidth = options.pictureWidth || 1024;
let svc = this.map.getService();
let wmsUrl = svc.wmsTileUrl({
width: pictureWidth,
height: Math.round(pictureWidth * bounds.height() / bounds.width()),
srs: "",
bbox: bounds.toString(),
transparent: options.transparent || false,
backgroundColor: options.backgroundColor || 'rgb(0,0,0)'
});
if (!options.disableOpen) {
window.open(wmsUrl);
}
return wmsUrl;
}
/**
* Remove minimap (overview) control
* @example
* // Remove minimap control
* this.removeMiniMapControl();
*/
removeMiniMapControl() {
this.context.global.controls = this.context.global.controls || {};
let options;
if (this.context.global.controls.miniMapControl) {
options = {...this.context.global.controls.miniMapControl.options}
this.map.removeControl(this.context.global.controls.miniMapControl.control);
delete this.context.global.controls.miniMapControl;
}
// Add undo record
this.context.ui.addUndoRecord(() => {
if (options) {
this.addMiniMapControl(options);
}
});
}
/**
* Remove all menu items
* @example
* // Remove all menu items
* this.deleteMenuItems()
*/
deleteMenuItems() {
this.context.ui.deleteMenuItems();
}
/**
* Get map pitch
* @returns {number} Current map pitch
*/
getPitch() { this.map.getPitch() }
/**
* Create or update map by fileid, URL, or local file selection
* @param {object} options Options, optional
* @param {string} options.fileid File id; empty=select local file; can be URL or file id
* @param {string} options.mapid Map id, optional, auto-generated if empty
* @param {"Memory" | "GeomRaster" | "GeomVector"} options.openway Open mode: Memory, GeomRaster, GeomVector. Default GeomRaster
* @param {boolean} options.lightTheme Light theme, optional, default dark
* @param {boolean} options.isFullExtent Full extent, optional, default false
* @returns {Object | undefined} {mapid, version, fileid}, undefined if cancelled
* @example
* // Select local file to create map
* await this.createOrUpdateMap();
* // Create map from URL gym.dwg, mapid newgym, vector, dark theme
* await this.createOrUpdateMap({
* fileid: "https://vjmap.com/static/assets/data/gym.dwg",
* mapid: "newgym",
* openway: "GeomVector",
* lightTheme: false
* });
*/
async createOrUpdateMap(options) {
options = options || {};
let fileid = options.fileid || "";
let svc = this.map.getService();
let uploadname;
if (!fileid) {
let formats = await svc.cmdGetSupportFormat();
let acceptFormat = '';
if (formats.cad) {
acceptFormat = formats.cad.split(";").map(f => '.' + f).join(",")
}
if (formats.image) {
acceptFormat += "," + formats.image.split(";").map(f => '.' + f).join(",")
}
const selectFile = () => {
return new Promise((resolve, reject) => {
// Create hidden file input
const input = document.createElement('input');
input.type = 'file';
// Set allowed file types
input.accept = acceptFormat;
// Add listener for file selection
input.addEventListener('change', () => {
if (input.files && input.files.length > 0) {
resolve(input.files[0]); // Return first selected file
} else {
reject(new Error('No file selected'));
}
});
// Trigger file picker
input.click();
});
}
try {
// Select file
let file = await selectFile();
// Upload file
let res = await svc.uploadMap(file);
if (res && res.error) {
this.showMessage(res.error)
return;
}
fileid = res.fileid;
uploadname = res.uploadname;
}
catch(err) {
this.showMessage(err)
return;
}
}
let mapid = options.mapid || "";
if (!mapid) {
mapid = fileid;
// If URL
if (fileid.toLowerCase().endsWith('.dwg')) {
mapid = svc.strMd5(fileid).substring(0, 16);
}
}
let mapopenway = options.openway === "Memory" ? vjmap.MapOpenWay.Memory : vjmap.MapOpenWay.GeomRender;
let isVectorStyle = options.openway === "GeomVector" ? true : false;
let lightTheme = options.lightTheme ?? (this.map.getService().currentMapParam()?.darkMode === true ? false : true)
this.showMessage("Opening map, please wait...", "success")
// Open map
let result = await svc.updateMap({
mapid: mapid,
fileid: fileid,
mapopenway: mapopenway,
uploadname: uploadname,
style: lightTheme ? vjmap.openMapLightStyle() : vjmap.openMapDarkStyle(),
});
// Switch to new map
let res = await this.map.switchMap({
mapid: result.mapid,
version: result.version,
mapopenway: mapopenway,
style: lightTheme ? vjmap.openMapLightStyle() : vjmap.openMapDarkStyle()
}, false, isVectorStyle, true, true);
if (options.isFullExtent !== true) {
// Use default center and zoom
let svc = this.map.getService();
let param = svc.currentMapParam();
if (param && param.view) {
if (param.view.center) {
this.map.setCenter(param.view.center);
}
if (param.view.zoom) {
this.map.setZoom(param.view.zoom);
}
if (param.view.bearing) {
this.map.setBearing(param.view.bearing);
}
}
}
if (res && res.error) {
this.showMessage(res.error)
return;
}
// New map opened, clear history
this.context.ui.clearUndoRecord();
return {
mapid: result.mapid,
fileid: fileid,
version: result.version
};
}
/**
* Delete CAD entities
* @param {boolean} rectSelect false=point select, true=rect select. Optional, default false
* @example
* // Example 1
* // Point select to delete CAD entities
* this.deleteCadEntity(false);
* // Example 2
* // Rect select to delete CAD entities
* this.deleteCadEntity(true);
*/
async deleteCadEntity(rectSelect) {
let draw = this.map.getDrawLayer();
if (!this.context.global.updateMapStyleObj) {
this.context.global.updateMapStyleObj = vjcommon.createUpdateMapStyleObj(this.map);
}
const updateMapStyleObj = this.context.global.updateMapStyleObj;
// Save old data for undo
let oldHideObjectIds = new Set(updateMapStyleObj.hideObjectIds);
const deleteEntitys = vjcommon.getTrackFeatureProperty(draw, "deleteEntitys") || [];
const hideEntityObjectIds =
vjcommon.getTrackFeatureProperty(draw, "hideEntityObjectIds") || [];
const hideEntityIds = vjcommon.getTrackFeatureProperty(draw, "hideEntityIds") || [];
await vjcommon.deleteCadEntity(this.map, draw, updateMapStyleObj, this.map.logInfo.bind(this.map), async (content) => {
return await this.context.ui.confirm(content, "Prompt", "warning");
}, rectSelect);
let newHideObjectIds = new Set(updateMapStyleObj.hideObjectIds);
let diffObjectIds = newHideObjectIds.difference(oldHideObjectIds);
// Add undo record
this.context.ui.addUndoRecord(async () => {
await updateMapStyleObj.removeHideObjectIds(diffObjectIds);
vjcommon.setTrackFeatureProperty(draw, {
deleteEntitys: Array.from(new Set(deleteEntitys)),
hideEntityObjectIds: Array.from(new Set(hideEntityObjectIds)),
hideEntityIds: Array.from(new Set(hideEntityIds)),
});
});
}
/**
* Copy CAD entities
* @param {boolean} rectSelect false=point select, true=rect select. Optional, default false
* @example
* // Example 1
* // Point select to copy CAD entities
* await this.copyCadEntity(false);
* // Example 2
* // Rect select to copy CAD entities
* await this.copyCadEntity(true);
*/
async copyCadEntity(rectSelect) {
let draw = this.map.getDrawLayer();
if (!this.context.global.updateMapStyleObj) {
this.context.global.updateMapStyleObj = vjcommon.createUpdateMapStyleObj(this.map);
}
const updateMapStyleObj = this.context.global.updateMapStyleObj;
// Save old data for undo
let features = draw.getAll().features;
let oldFeatureLength = features ? features.length : 0;
await vjcommon.copyCadEntity(this.map, draw, updateMapStyleObj, this.map.logInfo.bind(this.map), async (content) => {
return await this.context.ui.confirm(content, "Prompt", "warning");
}, rectSelect, (content) => {
return this.context.ui.prompt("Please enter content", "Prompt", content);
});
let addFeatureIds = [];
let newFeatures = draw.getAll().features;
let newFeatureLength = newFeatures ? newFeatures.length : 0;
for (let i = oldFeatureLength; i < newFeatureLength; i++) {
addFeatureIds.push(newFeatures[i].id);
}
// Add undo record
this.context.ui.addUndoRecord(async () => {
this.map.getDrawLayer().delete(addFeatureIds);
});
}
/**
* Capture line coordinates
* @param {boolean} isSnap Enable snap, default false
* @returns {Promise<number[][] | undefined>} Returns [[x1,y1],[x2,y2],...], undefined if cancelled
* @example
* const line = await this.actionDrawLineString();
* if (!line) return;
* // Output all line point coords
* for (let i = 0; i < line.length; i++) {
* let x = line[i][0];
* let y = line[i][1];
* console.log(x, y);
* }
*/
async actionDrawLineString(isSnap) {
this.context.tools.logInfo("Please draw a line on the map")
let snapObj = {};
if (isSnap) {
vjcommon.getMapSnapPoints(this.map, snapObj);
}
let drawLine = await vjmap.Draw.actionDrawLineSting(this.map, isSnap ? {
api: {
getSnapFeatures: snapObj
}
} : {});
if (drawLine.cancel) {
return;
}
let coords = drawLine.features[0].geometry.coordinates;
return coords.map(c => {
let p = this.map.fromLngLat(c);
return [p.x, p.y];
});
}
/**
* Manually draw ellipse or ellipse arc
* @param {object} options Config params, optional
* @param {boolean} options.isSnap Enable snap, optional, default false
* @param {string} options.color Color #RRGGBB, optional
* @param {string} options.borderColor Border color #RRGGBB, optional
* @param {boolean} options.arc Draw ellipse arc, default false, optional
* @param {boolean} options.isFill Whether to fill. Optional, default false
* @param {number} options.width Line width, default 1, optional
* @param {number} options.opacity Opacity. Optional, default 1
* @param {number} options.borderOpacity Border opacity, default 1, optional
* @param {number} options.data Data value, optional
* @returns {Promise<Object | undefined>} Drawn entity or undefined if cancelled. Object includes:
* @example
* // Example 1
* // Draw filled ellipse, snap, red, opacity 0.6, width 2, yellow border 0.8
* const ellipseFill = await this.drawEllipse({
* isSnap: true,
* color: "#FF0000",
* isFill: true,
* opacity: 0.6,
* width: 2,
* borderColor: "#FFFF00",
* borderOpacity: 0.8
* });
* if (ellipseFill && ellipseFill.id) {
* console.log("Ellipse ID:", ellipseFill.id);
* }
* // Example 2
* // Draw ellipse, red, width 3
* const ellipse = await this.drawEllipse({
* color: "#FF0000",
* isFill: false,
* width: 3,
* });
* if (ellipse && ellipse.id) {
* console.log("Ellipse ID:", ellipse.id);
* }
* // Example 3
* // Draw ellipse arc, yellow, width 2
* const ellipseArc = await this.drawEllipse({
* color: "#FFFF00",
* isFill: false,
* arc: true,
* width: 2,
* });
* if (ellipseArc && ellipseArc.id) {
* console.log("Ellipse arc ID:", ellipseArc.id);
* }
*/
async drawEllipse(options) {
options = options || {};
let draw = this.map.getDrawLayer();
let snapObj = {};
if (options.isSnap) {
vjcommon.getMapSnapPoints(this.map, snapObj);
}
let drawOptions = options.isSnap ? {
api: {
getSnapFeatures: snapObj
}
} : {};
let featureId;
if (options.isFill) {
if (options.arc) {
featureId = await vjcommon.drawEllipseFillArc(this.map, draw, drawOptions, {
"color": options.color,// color
"opacity": options.opacity, // opacity
"outlineColor": options.borderColor || options.color,// border color
"line_width": options.width, // line width
"line_opacity": options.borderOpacity, // opacity
"data": options.data
}, () => this.map.logInfo);
} else {
featureId = await vjcommon.drawEllipseFill(this.map, draw, drawOptions, {
"color": options.color,// color
"opacity": options.opacity, // opacity
"outlineColor": options.borderColor || options.color,// border color
"line_width": options.width, // line width
"line_opacity": options.borderOpacity, // opacity
"data": options.data
}, () => this.map.logInfo);
}
} else {
if (options.arc) {
featureId = await vjcommon.drawEllipseArc(this.map, draw, drawOptions, {
"color": options.borderColor || options.color,// color
"line_width": options.width, // line width
"data": options.data
}, () => this.map.logInfo);
} else {
featureId = await vjcommon.drawEllipseEdge(this.map, draw, drawOptions, {
"color": options.borderColor || options.color,// color
"line_width": options.width, // line width
"data": options.data
}, () => this.map.logInfo);
}
}
if (featureId) {
// Add undo record
this.context.ui.addUndoRecord(() => {
this.map.getDrawLayer().delete(featureId);
});
return { id: featureId };
}
}
/**
* Set data layer visibility
* @param {string} layerName Layer name, required
* @param {boolean} visible Visible, required
* @example
* // Hide data layer layer1
* let layerName = 'layer1';
* this.setDataLayerVisible(layerName, false);
*/
setDataLayerVisible(layerName, visible) {
let item = this.context.global.layers.find(layer => layer.layerName == layerName);
if(item && item.visible != visible) {
item.visible = visible;
let layerId = item.layerId;
if (item.layer && typeof(item.layer.getLayerId) == "function") {
layerId = item.layer.getLayerId();
}
item.layer.setVisible(this.map, layerId, !visible);
// Add undo record
this.context.ui.addUndoRecord(() => {
this.setDataLayerVisible(item.layerName, visible);
});
}
}
/**
* Create debug tile layer
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {number} options.rasterOpacity Image opacity 0-1, default 1
* @param {number} options.rasterHueRotate Hue rotation degrees, default 0
* @param {number} options.rasterBrightnessMin Min brightness 0-1, default 0
* @param {number} options.rasterBrightnessMax Max brightness 0-1, default 1
* @param {number} options.rasterSaturation Saturation -1 to 1, default 0
* @param {number} options.rasterContrast Contrast -1 to 1, default 0
* @param {number} options.rasterFadeDuration Tile fade duration ms, default 300
* @param {string} options.rasterResampling Resampling: linear, nearest, default linear
* @param {string} options.color Color #RRGGBB
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Create debug tile layer
* let layer = await this.createDebugLayer();
* console.log(layer);
*/
async createDebugLayer(options) {
options = options || {};
let type = "raster";
let randomId = vjmap.RandomID();
let sourceId = `source_${type}_${randomId}`;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
sourceId,
layerId,
...options,
type: type
}
let isVector = this.map.hasVectorLayer() ? "&vector=true" : ''
let r = 255, g = 255, b = 0, a = 255;
if (options.color && options.color.startsWith('#') && options.color.length === 7) {
r = parseInt(options.color.slice(1, 3), 16);
g = parseInt(options.color.slice(3, 5), 16);
b = parseInt(options.color.slice(5, 7), 16);
}
let svc = this.map.getService();
let tiles = svc.serviceUrl('this.map/') + `tile/debug/{z}/{x}/{y}?r=${r}&g=${g}&b=${b}&a=${a}${isVector}&token=${svc.accessToken}`;
let sourceData = {
type: "raster",
tiles: [tiles],
tileSize: this.map.hasVectorLayer() ? 512 : 256
}
this.map.addRasterSource(sourceId, sourceData);
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data: sourceData, options, layer, visible: true, create: this.createDebugLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Get CAD EPSG code
* @param {object} options Config options
* @param {string} [options.mapid] Map ID, uses current if empty
* @param {string} [options.version] Map version, uses current when empty
* @param {"beijing54"|"xian80"|"cgs2000"} [options.crs] CAD CRS: beijing54, xian80, cgs2000
* @returns {Promise<string|undefined>} CAD EPSG code, undefined if not available
* @example
* // Example 1
* // Get current map EPSG, output to session
* let epsg = await this.getCadEpsgCode();
* if (epsg) {
* this.chatLog(epsg);
* } else {
* this.chatLog("Failed to get CAD EPSG code");
* }
* // Example 2
* // Get EPSG for map ca422a931980, Xi'an 80
* let epsg2 = await this.getCadEpsgCode({
* mapid: "ca422a931980",
* crs: "xian80"
* });
* console.log(epsg2);
*/
async getCadEpsgCode(options) {
options = options || {}
let svc = this.map.getService();
// Get center; for web map overlay X 8 digits, Y 7 (or X 6 without zone)
let center = this.map.fromLngLat(this.map.getCenter());
if (options.mapid) {
let metadata = await svc.metadata(options.mapid, options.version);
let mapExtent = vjmap.GeoBounds.fromString(metadata.bounds);
center = mapExtent.center();
}
let strX = parseInt(center.x) + "";
let xLen = strX.length;
let yLen = (parseInt(center.y) + "").length;
if (yLen == 7) {
let crs = vjmap.transform.EpsgCrsTypes.CGCS2000;
if (options.crs == "beijing54") {
crs = vjmap.transform.EpsgCrsTypes.Beijing54;
} else if (options.crs == "xian80") {
crs = vjmap.transform.EpsgCrsTypes.Xian80;
}
if (xLen == 8) {
let epsg = vjmap.transform.getEpsgParam(crs, +strX.substring(0, 2))?.epsg;
if (epsg) {
return epsg;
}
} else if (xLen == 6) {
// If 6-digit X without zone, pick approximate lng/lat on web map
let lngLat = await this.context.ui.selectWebMapLngLat({
isGaode: true,
isRoadway: true
})
if (lngLat) {
let res = vjmap.transform.getEpsgCode([lngLat.lng, lngLat.lat]);
let curEpsg;
let curDist;
// Get nearest EPSG
for(let item of res) {
if (crs == vjmap.transform.EpsgCrsTypes.CGCS2000 && item.crs.indexOf('2000') == -1) {
continue;
} else if (crs == vjmap.transform.EpsgCrsTypes.Beijing54 && item.crs.indexOf('54') == -1) {
continue;
} else if (crs == vjmap.transform.EpsgCrsTypes.Xian80 && item.crs.indexOf('80') == -1) {
continue;
}
let epsg = item.epsg;
let result = await svc.cmdTransform("EPSG:4326", epsg, vjmap.geoPoint([lngLat.lng, lngLat.lat])); // Or use proj4 on frontend
let pt = result[0];// First result
let dist = vjmap.geoPoint(pt).distanceTo(vjmap.geoPoint(center))
if (curDist === undefined) {
curDist = dist;
curEpsg = epsg;
} else if (dist < curDist) {
curDist = dist;
curEpsg = epsg;
}
}
if (curEpsg) {
return curEpsg;
}
}
}
}
return undefined;
}
/**
* Modify CAD entities
* @param {boolean} rectSelect false=point select, true=rect select. Optional, default false
* @example
* // Example 1
* // Point select to modify CAD entities
* this.modifyCadEntity(false);
* // Example 2
* // Rect select to modify CAD entities
* this.modifyCadEntity(true);
*/
async modifyCadEntity(rectSelect) {
let draw = this.map.getDrawLayer();
if (!this.context.global.updateMapStyleObj) {
this.context.global.updateMapStyleObj = vjcommon.createUpdateMapStyleObj(this.map);
}
const updateMapStyleObj = this.context.global.updateMapStyleObj;
// Save old data for undo
let oldHideObjectIds = new Set(updateMapStyleObj.hideObjectIds);
const deleteEntitys = vjcommon.getTrackFeatureProperty(draw, "deleteEntitys") || [];
const hideEntityObjectIds =
vjcommon.getTrackFeatureProperty(draw, "hideEntityObjectIds") || [];
const hideEntityIds = vjcommon.getTrackFeatureProperty(draw, "hideEntityIds") || [];
let features = draw.getAll().features;
let oldFeatureLength = features ? features.length : 0;
await vjcommon.modifyCadEntity(this.map, draw, updateMapStyleObj, this.map.logInfo.bind(this.map), async (content) => {
return await this.context.ui.confirm(content, "Prompt", "warning");
}, rectSelect, (content) => {
return this.context.ui.prompt("Please enter content", "Prompt", content);
});
let newHideObjectIds = new Set(updateMapStyleObj.hideObjectIds);
let diffObjectIds = newHideObjectIds.difference(oldHideObjectIds);
let addFeatureIds = [];
let newFeatures = draw.getAll().features;
let newFeatureLength = newFeatures ? newFeatures.length : 0;
for (let i = oldFeatureLength; i < newFeatureLength; i++) {
addFeatureIds.push(newFeatures[i].id);
}
// Add undo record
this.context.ui.addUndoRecord(async () => {
await updateMapStyleObj.removeHideObjectIds(diffObjectIds);
this.map.getDrawLayer().delete(addFeatureIds);
vjcommon.setTrackFeatureProperty(draw, {
deleteEntitys: Array.from(new Set(deleteEntitys)),
hideEntityObjectIds: Array.from(new Set(hideEntityObjectIds)),
hideEntityIds: Array.from(new Set(hideEntityIds)),
});
});
}
/**
* Get nearest point from point to multiple lines
* @param {[number, number]} point Point [x,y]
* @param {[number, number][][] | geojson} lines Lines array or GeoJSON
* @returns {object} Object with nearest point, distance, index, prevDist
* @example
* // Nearest point from [130,140] to lines [[[100,100],[200,200]],[[100,200],[200,100]]]
* let lines = [
* [[100, 100], [200, 200]],
* [[100, 200], [200, 100]]
* ];
* let point = [130, 140];
* let res = this.closestPointOnPolylines(point, lines);
* console.log(res);
*/
closestPointOnPolylines(point, lines) {
if (lines && lines.type === "FeatureCollection" && lines.features.length > 0) {
let geojsonLines = [];
for(let i = 0; i < lines.features.length; i++) {
if (lines.features[i].geometry.type === "LineString") {
geojsonLines.push(lines.features[i].geometry.coordinates);
} else if (lines.features[i].geometry.type === "MultiLineString") {
lines.features[i].geometry.coordinates.forEach(ln => geojsonLines.push(ln));
}
}
lines = geojsonLines;
}
if (Array.isArray(lines)) {
lines = lines.map(line => line.map(co => vjmap.geoPoint(co)));
}
// Compute nearest to all lines
const res = vjmap.closestPointOnPolylines(vjmap.geoPoint(point), lines);
if (res && res.closestPoint) {
res.closestPoint = [res.closestPoint.x, res.closestPoint.y];
}
return res;
}
/**
* Get data bounds from data
* @param {Object} geojson GeoJSON data for bounds, required
* @returns {[number, number, number, number]} [x1,y1,x2,y2] bounds (bottom-left, top-right)
* @example
* // Get GeoJSON bounds
* let geojson = {type: "FeatureCollection", features: [{type: "Feature", geometry: {type: "Point", coordinates: [10, 20]}}]};
* let bounds = this.getDataGeoBounds(geojson);
* console.log(bounds);
*/
getDataGeoBounds(geojson) {
let dataExtent = vjmap.GeoBounds.fromDataExtent(geojson);
return dataExtent.toArray();
}
/**
* Manually draw a rectangle
* @param {object} options Config params, optional
* @param {boolean} options.isSnap Enable snap, optional, default false
* @param {string} options.color Color #RRGGBB, optional
* @param {boolean} options.isFill Whether to fill. Optional, default false
* @param {number} options.opacity Opacity. Optional, default 1
* @param {number} options.data Data value, optional
* @returns {Promise<Object | undefined>} Drawn entity or undefined if cancelled. Object includes:
* @example
* // Draw a rectangle manually
* const rect = await this.drawRectangle({
* isSnap: true,
* color: "#FF0000",
* isFill: true,
* opacity: 0.8
* });
* if (!rect) return;
* console.log("Rect ID:", rect.id);
* console.log("Rect coords:", rect.points);
*/
async drawRectangle(options) {
let points = await this.actionDrawRectangle(options.isSnap);
if (!points) return;
return this.createRectangle(points, options);
}
/**
* Remove a menu item
* @param {string} name Menu name
* @example
* // Remove "Full Map" menu item
* this.removeMenuItem("Full Map")
*/
removeMenuItem(name, func) {
this.context.ui.removeMenuItem(name, func);
// Add undo record
this.context.ui.addUndoRecord(() => {
this.addMenuItem(name, func);
});
}
/**
* Select map, returns mapid and version
* @returns {Object | undefined} {mapid, version}, undefined if cancelled
* @example
* // Select map, get mapid and version
* let res = await this.selectCadMap();
* if (res) {
* this.chatLog("Selected mapid: " + res.mapid + ", version: " + res.version);
* }
*/
async selectCadMap() {
let res = await this.context.ui.selectCadMap()
if (!res) return;
return {
mapid: res.mapid,
version: res.version
};
}
/**
* Create animate polygon data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @param {string} options.fillColor 填充颜色
* @param {string} options.fillOutlineColor 边框颜色
* @param {number} options.fillOpacity 填充透明度,取值范围0-1
* @param {string} options.fillPattern 填充图案,必须与地图已加载的图片名称对应,图像宽度必须是2的系数(2、4、8、...、512)
* @param {[number,number]} options.fillTranslate 几何体的平移量[x,y],负数分别表示左和上,单位像素,默认[0,0]
* @param {string} options.fillTranslateAnchor 平移参考系,"map"表示相对于地图平移,"viewport"表示相对于视窗平移
* @param {boolean} options.fillAntialias 是否启用填充反锯齿
* @param {string} options.animateImagesType Animation type: "arrow", "antpath", "customImage", "imageSprites"
* @param {string} options.animateImages_arrowFillColor Arrow fill color when animateImagesType is "arrow", default "#22B14C"
* @param {string} options.animateImages_arrowStrokeColor Arrow stroke color when animateImagesType is "arrow", default "#fff"
* @param {number} options.animateImages_arrowStrokeWidth Arrow stroke width when animateImagesType is "arrow"
* @param {number} options.animateImages_arrowWidth Arrow size when animateImagesType is "arrow"
* @param {string} options.animateImages_fillColor1 Ant path fill color 1, for "antpath"
* @param {string} options.animateImages_fillColor2 Ant path fill color 2, for "antpath"
* @param {string} options.customImageUrl Custom image URL/base64, for "customImage"
* @param {string} options.imageSpriteUrl Image sprite URL/base64, for "imageSprites"
* @param {number} options.animateImages_spriteWidth Sprite width in source, for "imageSprites"
* @param {number} options.animateImages_spriteHeight Sprite height in source, for "imageSprites"
* @param {number} options.animateImages_canvasWidth Canvas width
* @param {number} options.animateImages_canvasHeight Canvas height
* @param {number} options.animateImages_frameCount Frame count
* @param {boolean} options.animateImages_directionReverse Reverse animation direction
* @param {number} options.speed Animation speed, step 0.1
* @param {boolean} options.startAutoAnimation Auto-play on start
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Random GeoJSON, create animate polygon layer layer2
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createAnimateFillLayer(geojson, {
* layerName: 'layer2'
* });
* console.log(layer);
*/
async createAnimateFillLayer(data, options) {
options = options || {};
let type = "animateFill";
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
fillColor: "#00ffff",
fillOutlineColor: "#00ff00",
animateImagesType: "arrow",
animateImages_arrowFillColor: "#22B14C",
animateImages_arrowStrokeColor: "#fff",
animateImages_arrowStrokeWidth: 6,
animateImages_canvasWidth: 128,
animateImages_canvasHeight: 32,
animateImages_arrowWidth: 16,
animateImages_frameCount: 4,
animateImages_fillColor1: "#f0fb",
animateImages_fillColor2: "#0ffb",
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createSymbolCircleLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Get CAD layer list
* @param {string} mapid Map id, optional, current if empty
* @param {string} version Version, optional, current if empty
* @returns {Promise<Object[]>} CAD layer list, each with properties below
* @example
* // Example 1
* // Get CAD layer list
* let res = await this.getCadLayers();
* if (res) {
* this.chatLog("Layer names: " + res.map(layer => layer.name).join(","));
* }
* // Example 2
* // Get CAD map sys_world v1 layer list
* let resp = await this.getCadLayers("sys_world", "v1");
* if (resp) {
* console.log(resp);
* }
*/
async getCadLayers(mapid, version) {
let svc = this.map.getService();
if (!mapid) {
return svc.getMapLayers();
} else {
// Get metadata first
let meta = await svc.metadata(mapid, version);
if (meta && meta.layers) {
return meta.layers;
}
}
}
/**
* Remove all button items
* @example
* // Remove all button items
* this.deleteButtonItems()
*/
deleteButtonItems() {
this.context.ui.deleteButtonItems();
}
/**
* Set draw layer feature data
* @param {<GeoJSON | undefined>} data Feature data
*/
setDrawLayerData(data) {
let draw = this.map.getDrawLayer();
draw.set(this.map.toLngLat(data));
}
/**
* Create circle data layer
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {number} options.circleRadius 圆半径,单位像素,参数可选
* @param {string} options.circleColor 圆填充颜色,支持十六进制颜色值,参数可选
* @param {number} options.circleOpacity 圆填充透明度,取值范围0-1,参数可选
* @param {string} options.circleStrokeColor 圆边框颜色,支持十六进制颜色值,参数可选
* @param {number} options.circleStrokeWidth 圆Border width,单位像素,参数可选
* @param {number} options.circleStrokeOpacity 圆边框透明度,取值范围0-1,参数可选
* @param {number} options.circleBlur 圆模糊度,参数可选
* @param {[number]} options.circleTranslate Translate [x,y], default [0,0], optional
* @param {string} options.circleTranslateAnchor 平移参考系,'map'表示相对地图平移,'viewport'表示相对视窗平移,参数可选
* @param {string} options.circlePitchScale 倾斜时缩放对齐方式,'map'相对地图,'viewport'相对视窗,参数可选
* @param {string} options.circlePitchAlignment 倾斜时对齐方式,'map'相对地图,'viewport'相对视窗,参数可选
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // Random GeoJSON, create circle layer layer7, radius 5, red
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createSymbolCircleLayer(geojson, {
* layerName: 'layer7',
* circleRadius: 5,
* circleColor: "#ff0000"
* });
* console.log(layer);
*/
async createSymbolCircleLayer(data, options) {
options = options || {};
let type = "circle";
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
"circleRadius": 5,
"circleColor": "#ffff00",
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createSymbolCircleLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* Get current map preview URL
* @returns {Promise<string>} Current map preview URL
* @example
* // Get current map preview URL
* let url = await this.curMapPreviewUrl();
* console.log(url);
*/
curMapPreviewUrl() {
let svc = this.map.getService();
let mapParam = svc.currentMapParam();
let url = svc.baseUrl() + "_mapviewer?";
let param = {};
param["render"] = 1; // Default geom raster
if (mapParam?.mapopenway == vjmap.MapOpenWay.Memory) {
param["render"] = 0; // Memory open
} else if (mapParam?.mapopenway == vjmap.MapOpenWay.GeomRender && window.location.href.indexOf("vector=true") > 0) {
param["render"] = 2; // Default geom vector
}
param["mapid"] = mapParam?.mapid;
param["version"] = mapParam?.version;
param["layer"] = mapParam?.layer;
param["centerX"] = this.map.getCenter().lng;
param["centerY"] = this.map.getCenter().lat;
param["zoom"] = this.map.getZoom();
param["pitch"] = this.map.getPitch();
param["bearing"] = this.map.getBearing();
param["light"] = mapParam?.darkMode !== false;
param["title"] = `VJMap-${mapParam?.mapid}-${mapParam?.version}-${mapParam?.layer}`;
param["mobile"] = true;
param['token'] = svc.accessToken;
if (svc.getCurWorkspaceName()) {
param['workspace'] = svc.getCurWorkspaceName();
}
url = url + Object.keys(param).map(item => item + "=" + param[item]).join("&");
return url;
}
/**
* Target matching
* @param {object} options Options
* @param {string} [options.mapid] Map ID
* @param {string} [options.version] Map version, uses current when empty
* @param {number} [options.selectWay] 0=box, 1=point, 2=no select. Default 0
* @param {string} [options.layer] Style layer name, empty=from selected entities
* @param {string} [options.mapBounds] Match map bounds, default full data bounds
* @param {string} [options.objectIds] Entity objectids, separated by ||
* @param {string} [options.objectBounds] Entity bounds
* @param {string} [options.layeron] Map layers on
* @param {number} [options.size] Image size for match, default 10000
* @param {string} [options.method] Method, default matchPattern
* @param {number} [options.maxCount] Max results, default 200
* @param {number} [options.score] Min score (0-1), default 0.6
* @param {number} [options.minReduceArea] Pyramid size 64-2048, default 256, for matchPattern
* @param {boolean} [options.canOverlap] Allow overlap, default false
* @param {number} [options.maxOverlap] Overlap ratio 0-0.8, default 0.3
* @param {number} [options.toleranceAngle] Angle tolerance (-180,180), default 180, for matchPattern
* @param {boolean} [options.useToleranceRange] Use angle range, default false, for matchPattern
* @param {number} [options.tolerance1] Angle range 1 start, for matchPattern
* @param {number} [options.tolerance2] Angle range 1 end, for matchPattern
* @param {number} [options.tolerance3] Angle range 2 start, for matchPattern
* @param {number} [options.tolerance4] Angle range 2 end, for matchPattern
* @param {boolean} [options.showSettingForm] Show settings dialog, default true
* @returns {Promise<GeoJSON>} Match result GeoJSON, undefined on failure
* @example
* // Point select entities for target matching
* let res = await this.matchObject({
* selectWay: 1
* });
* console.log(res);
*/
async matchObject(options) {
options = options || {};
let svc = this.map.getService();
options = {
mapid: options.mapid || svc.currentMapParam().mapid,
objectIds: '',
objectBounds: '',
method: "matchPattern",
maxCount: 200,
score: 0.6,
size: 10000,
canOverlap: false,
maxOverlap: 0.3,
toleranceAngle: 180,
layer: "",
layeron: "",
...options,
}
if (options.selectWay === 0) {
let isPointSel = options.selectWay === 1;
let param = {};
let res = await vjcommon.selectFeatures(this.map, true, isPointSel, isPointSel, true);
vjcommon.clearHighlight(this.map);
if (!res || res.length == 0) return;
const objectIds = res.map(f => f.properties.objectid);
const layeron = `(${Array.from(new Set(res.map(f => f.properties.layerindex))).join(",")})`;
const features = {
type: "FeatureCollection",
features: res
}
let data = this.map.fromLngLat(features);
let bounds = vjcommon.getGeoJsonBounds(data).scale(1.01); // Slightly larger
param.objectIds = objectIds.join("||");
param.objectBounds = `[${bounds.toString()}]`;
param.layeron = layeron;
options = {
...options,
...param
}
}
if (options.showSettingForm !== false) {
options = await this.showDataModifyDlg({
data: options,
title: "Target Match Settings",
showForm: true
})
if (!options) return;
}
let result = await svc.cmdMatchObject(options);
if (result && result.error) {
if (this.map.logInfo) this.map.logInfo(result.error, "error", 10000);
return;
}
if (!result || !result.match) return;
let match = result.match.split(";");
let geojson = {
type: "FeatureCollection",
features: []
};
for(let i = 0; i < match.length; i++) {
if (match[i].indexOf(",") <= 0) continue;
const items = match[i].split(",");
const pts = [[+items[0], +items[1]], [+items[2], +items[3]], [+items[4], +items[5]], [+items[6], +items[7]]];
pts.push(pts[0])
geojson.features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: pts
}
})
}
return geojson;
}
/**
* Show prompt input
* @param {string} message Dialog content, optional
* @param {string} title Dialog title, optional
* @param {string} inputValue Initial value, optional
* @param {boolean} isPassword Password input, optional, default false
* @returns {string|undefined} User input, undefined if cancelled
* @example
* // Show prompt "Please enter content", initial empty
* let res = await this.messageBoxPrompt("Please enter content", "Prompt", "");
* if (res !== undefined) {
* console.log("Input:", res)
* } else {
* console.log("Clicked Cancel")
* }
*/
async messageBoxPrompt(message, title, inputValue, isPassword) {
return await this.context.ui.prompt(message || "Please enter content", title || "Prompt", inputValue || "", isPassword || false);
}
/**
* Get all map data or by mapid
* @param {string} mapid Map ID, optional, all if empty
* @param {string} version Version, optional, latest if empty, * = all
* @returns {Promise<Array<Object>>} Map metadata array, undefined on failure
* @example
* // Example 1
* // Get all map data
* let res = await this.listMaps();
* console.log(res);
* // Example 2
* // Get all versions of map sys_hello
* let res2 = await this.listMaps("sys_hello", "*");
* console.log(res2);
*/
async listMaps(mapid, version) {
let svc = this.map.getService();
let res = await svc.listMaps(mapid, version);
let result = [];
if (mapid) {
if (res && typeof res["version"] === "string") {
result.push({
mapid: mapid,
...res
});
return result;
}
}
for(let id in res) {
let data = res[id];
result.push({
mapid: id,
...data
});
}
return result;
}
/**
* Convert projection PRJ WKT to proj4 string
* @param {string} wkt PRJ WKT string
* @param {string} [from] String source, empty=WKT
* @returns {Promise<string>} proj4 string
* @example
* // 将WKT字符串`PROJCS["CGCS2000 / 3-degree Gauss-Kruger CM 111E",GEOGCS["China Geodetic Coordinate System 2000",DATUM["China_2000",SPHEROID["CGCS2000",6378300,300,AUTHORITY["EPSG","1024"]],AUTHORITY["EPSG","1043"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.018,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4490"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",112.1],PARAMETER["scale_factor",1],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AUTHORITY["EPSG","4546"]]`转换为proj4字符串
* let proj4String = await this.prjWktToPrj4(`PROJCS["CGCS2000 / 3-degree Gauss-Kruger CM 111E",GEOGCS["China Geodetic Coordinate System 2000",DATUM["China_2000",SPHEROID["CGCS2000",6378300,300,AUTHORITY["EPSG","1024"]],AUTHORITY["EPSG","1043"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.018,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4490"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",112.1],PARAMETER["scale_factor",1],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AUTHORITY["EPSG","4546"]]`);
* console.log(proj4String);
*/
async prjWktToPrj4(wkt, from) {
let svc = this.map.getService();
let res = await svc.cmdPrjWktToPrj4(wkt, from);
return res?.proj4;
}
/**
* 移除鼠标位置控件
* @example
* // 移除鼠标位置控件
* this.removeMousePositionControl();
*/
removeMousePositionControl() {
this.context.global.controls = this.context.global.controls || {};
let options;
if (this.context.global.controls.mousePositionControl) {
options = {...this.context.global.controls.mousePositionControl.options}
this.map.removeControl(this.context.global.controls.mousePositionControl.control);
delete this.context.global.controls.mousePositionControl;
}
// Add undo record
this.context.ui.addUndoRecord(() => {
if (options) {
this.addMousePositionControl(options);
}
});
}
/**
* 关闭数据显示面板
* @example
* // 关闭数据显示面板
* this.closeDataPanel()
*/
closeDataPanel() {
this.context.ui.setDataPanelContent();
}
/**
* 创建点标记聚合数据图层
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {string} options.text Text to display, optional, default "index ${props.index}"
* @param {number} options.minZoom Min zoom to display, optional, default 0
* @param {number} options.maxZoom Max zoom to display, optional, default 24
* @param {boolean} options.isDrawLinePoint Include line points when drawing, optional, default false
* @param {string} options.color Marker color, optional, default'#3FB1CE'
* @param {number[]} options.offset XY pixel offset [0,0], negative = left/up
* @param {number} options.rotation Marker rotation, clockwise, default 0
* @param {string} options.anchor Marker anchor:center、top、bottom、left、right、top-left、top-right、bottom-left、bottom-right, default center
* @param {boolean} options.draggable Draggable, optional, default false
* @param {string} options.animationType Animation: MAP_ANIMATION_NONE, MAP_ANIMATION_BOUNCE, MAP_ANIMATION_DROP
* @param {string} options.rotationAlignment Rotation alignment: auto, map, viewport, default auto
* @param {string} options.pitchAlignment Pitch alignment: auto, map, viewport, default auto
* @param {number} options.scale Scale, default 1, 41px x 27px
* @param {number} options.scaleMaxZoom Scale max zoom (0-24), auto-scale below
* @param {number} options.height Elevation, CAD units
* @param {boolean} options.removeWhenNoInMapView Auto show/hide in view
* @param {number} options.removeWhenNoInMapViewPadding View padding for auto show/hide, default 500
* @param {number} options.clickTolerance Click tolerance, inherits map default
* @param {string} options.customImage Custom image URL/base64/svg
* @param {number} options.customImageWidth Custom image width
* @param {number} options.customImageHeight Custom image height
* @param {string} options.customHtml Custom HTML element JS code
* @param {string} options.popupHtml Popup HTML content
* @param {[number,number]} options.popupOffset Popup XY offset
* @param {string} options.popupAnchor Popup anchor, same as anchor
* @param {boolean} options.closeButton Show close button
* @param {boolean} options.closeOnClick Close popup on map click
* @param {boolean} options.closeOnMove Close popup on map move
* @param {boolean} options.focusAfterOpen Focus first focusable in popup
* @param {string} options.className CSS class for popup
* @param {string} options.maxWidth Popup max width, default'240px'
* @param {boolean} options.allowOverlap 是否允许标记重叠
* @param {number} options.allowOverlapMaxZoom 允许重叠的最大缩放级别,小于或等于此级别才会处理重叠,超过此级时会全部显示,默认4级
* @param {number} options.markerWidth marker div的像素宽,用于计算重叠时需要,默认40
* @param {number} options.markerHeight marker div的像素高,用于计算重叠时需要,默认40
* @param {Object} options.textStyle 聚合文本的CSS样式对象,如{cursor:"pointer", opacity:0.8等}
* @param {Array<Array>} options.textColors 聚合文本颜色配置,格式为[[值1,文本颜色,背景色], [值2,文本颜色,背景色], ...]
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // 在地图范围内随机生成GeoJson数据,创建图层名称为layer20的点标记聚合数据图层
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createMarkerClusterLayer(geojson, {
* layerName: 'layer20'
* });
* console.log(layer);
*/
async createMarkerClusterLayer(data, options) {
options = options || {};
let type = "markerCluster";
if (options.height) {
options.height = this.map.geoLengthToHeight(options.height);
}
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
textStyle: {
cursor: "pointer",
opacity: 0.8,
padding: "6px",
"border-radius": "12px",
"border-width": 0,
"box-shadow": "0px 2px 6px 0px rgba(97,113,166,0.2)",
"text-align": "center",
"font-size": "14px",
},
textColors: [
[0, "#00FFFF", "#00FFFF"],
[5, "#F0F", "#80FF00"],
[10, "#00F", "#FFFF00"],
[1000, "#FFF", "#FF3D3D"]
],
popupHtml: "<h3 style='color:red'>index ${props.index}</h3>",
popupOffset: [0, -28],
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createMarkerClusterLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* 创建矢量瓦片图层
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {string} options.tileMapId Tile mapid, optional
* @param {string} options.tileMapVersion Tile version, optional
* @param {string} options.tileMapLayer Tile style layer name, optional
* @param {string|Array<string>} options.tiles Tile URL, supports variables {x},{y},{z},{serviceUrl},{accessToken}
* @param {number} options.tileSize Tile size, default 512
* @param {number[]} options.bounds Tile bounds [minx,miny,maxx,maxy], optional
* @param {string} options.scheme Tile scheme: xyz (Y from top), tms (Y from bottom), default xyz
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // 创建矢量瓦片图层, 瓦片的mapid为sys_hello
* let layer = await this.createVectorLayer({
* tileMapId: "sys_hello",
* });
* console.log(layer);
*/
async createVectorLayer(options) {
options = options || {};
let type = "vector";
let randomId = vjmap.RandomID();
let sourceId = `source_${type}_${randomId}`;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
sourceId,
layerId,
...options,
type: type
}
if (options.tileMapId && !options.tiles) {
let layer = options.tileMapLayer;
if (!layer) {
let style = await this.map.getService().createStyle({
backcolor: this.map.getService().currentMapParam().darkMode ? 0x000000 : 0xFFFFFF
}, options.tileMapId, options.tileMapVersion);
layer = style.stylename
}
let tileUrl = this.map.getService().vectorTileUrl({
mapid: options.tileMapId,
version: options.tileMapVersion,
layer: layer,
})
options.tiles = tileUrl;
}
if (options.bounds) {
const p1 = this.map.toLngLat([options.bounds[0], options.bounds[1]]);
const p2 = this.map.toLngLat([options.bounds[2], options.bounds[3]]);
options.bounds = [p1[0], p1[1], p2[0], p2[1]];
}
let sourceData = {
type: "vector",
tiles: Array.isArray(options.tiles) ? options.tiles : [options.tiles],
tileSize: options.tileSize || 512
}
if (options.bounds) {
sourceData.bounds = options.bounds;
}
if (options.scheme) {
sourceData.scheme = options.scheme;
}
this.map.addVectorSource(sourceId, sourceData);
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data: sourceData, options, layer, visible: true, create: this.createVectorLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* 创建多边形数据图层
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {string} options.fillColor 填充颜色
* @param {string} options.fillOutlineColor 边框颜色
* @param {number} options.fillOpacity 填充透明度,取值范围0-1
* @param {string} options.fillPattern 填充图案,必须与地图已加载的图片名称对应,图像宽度必须是2的系数(2、4、8、...、512)
* @param {[number,number]} options.fillTranslate 几何体的平移量[x,y],负数分别表示左和上,单位像素,默认[0,0]
* @param {string} options.fillTranslateAnchor 平移参考系,"map"表示相对于地图平移,"viewport"表示相对于视窗平移
* @param {boolean} options.fillAntialias 是否启用填充反锯齿
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // 在地图范围内随机生成GeoJson数据,创建图层名称为layer11的多边形数据图层
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createFillLayer(geojson, {
* layerName: 'layer11'
* });
* console.log(layer);
*/
async createFillLayer(data, options) {
options = options || {};
let type = "fill";
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
fillColor: "#00ffff",
fillOutlineColor: "#00ff00",
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createFillLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
/**
* 创建Geojson点面线图层
* @param {GeoJson} data Data content, standard GeoJSON format, required
* @param {Record<string, any>} options Layer parameter options, optional
* @param {string} options.layerName Layer name, optional, default empty, uses random id if empty
* @param {number} options.minzoom Min zoom to display, optional, default 0
* @param {number} options.maxzoom Max zoom to display, optional, default 24
* @param {string} options.fillColor 填充颜色
* @param {string} options.fillOutlineColor 边框颜色
* @param {number} options.fillOpacity 填充透明度,取值范围0-1
* @param {string} options.fillPattern 填充图案,必须与地图已加载的图片名称对应,图像宽度必须是2的系数(2、4、8、...、512)
* @param {[number,number]} options.fillTranslate 几何体的平移量[x,y],负数分别表示左和上,单位像素,默认[0,0]
* @param {string} options.fillTranslateAnchor 平移参考系,"map"表示相对于地图平移,"viewport"表示相对于视窗平移
* @param {boolean} options.fillAntialias 是否启用填充反锯齿
* @param {string} options.lineColor Line color, optional, default '#ff0000'
* @param {number} options.lineWidth Line width, optional, default 1
* @param {number} options.lineOpacity Line opacity, optional, 0-1
* @param {number} options.lineGapWidth Line gap width, optional, default 1, pixels
* @param {number} options.lineOffset Line offset, optional
* @param {number} options.lineBlur Line blur, optional
* @param {[number,number]} options.lineDasharray Dash pattern, optional
* @param {[number,number]} options.lineTranslate Translate [x,y], optional
* @param {string} options.lineCap Line cap: 'butt', 'round', 'square'
* @param {string} options.lineJoin Line join: 'bevel', 'round', 'miter'
* @param {string} options.linePattern Line pattern, optional
* @param {string} options.lineGradient Line gradient, optional
* @param {number} options.circleRadius 圆半径,单位像素,参数可选
* @param {string} options.circleColor 圆填充颜色,支持十六进制颜色值,参数可选
* @param {number} options.circleOpacity 圆填充透明度,取值范围0-1,参数可选
* @param {string} options.circleStrokeColor 圆边框颜色,支持十六进制颜色值,参数可选
* @param {number} options.circleStrokeWidth 圆Border width,单位像素,参数可选
* @param {number} options.circleStrokeOpacity 圆边框透明度,取值范围0-1,参数可选
* @param {number} options.circleBlur 圆模糊度,参数可选
* @param {[number,number]} options.circleTranslate 平移量,格式[x,y],负数表示向左和向上平移,单位像素,默认[0,0],参数可选
* @param {string} options.circleTranslateAnchor 平移参考系,'map'表示相对地图平移,'viewport'表示相对视窗平移,参数可选
* @param {string} options.circlePitchScale 倾斜时缩放对齐方式,'map'相对地图,'viewport'相对视窗,参数可选
* @param {string} options.circlePitchAlignment 倾斜时对齐方式,'map'相对地图,'viewport'相对视窗,参数可选
* @param {boolean} options.isHoverPointer Enable hover events, optional
* @param {boolean} options.isHoverFeatureState Enable hover feature state, optional
* @returns {Object} Returns object with {sourceId: string, layerId: string, layerName: string}
* @example
* // 在地图范围内随机生成GeoJson数据,创建图层名称为layer29的Geojson点面线图层
* let geojson = this.randomGeojsonInMapBounds();
* let layer = await this.createGeojsonDataLayer(geojson, {
* layerName: 'layer29',
* });
* console.log(layer);
*/
async createGeojsonDataLayer(data, options) {
options = options || {};
let type = "geojson";
if (options.entColorToHtmlColor === undefined) {
for(let f of data.features) {
if (f.properties && typeof f.properties.color == "number") {
if (f.properties.color < 0) {
options.entColorToHtmlColor = true;
break;
}
}
}
}
let randomId = vjmap.RandomID();
let sourceId = "source_geojson_" + randomId;
let layerId = `layer_${type}_${randomId}`;
let layerOptions = {
layerId,
sourceId,
fillColor: "#00ffff",
fillOutlineColor: "#00ff00",
lineColor: '#ff0000',
lineWidth: 1,
circleRadius: 5,
circleColor: "#ffff00",
...options,
type: type
}
// Add data source
this.map.addGeoJSONSource(sourceId, this.map.toLngLat(data));
// Add layer
let layer = new vjcommon.providerLayers[layerOptions.type];
await layer.addLayer(this.map, layerOptions);
let layerName = options.layerName || layerId;
this.context.global.layers = this.context.global.layers || [];
this.context.global.layers.push({layerId, sourceId, layerName, data, options, layer, visible: true, create: this.createGeojsonDataLayer});
// Add undo record
this.context.ui.addUndoRecord(() => {
this.deleteDataLayer(layerName);
});
return {
sourceId,
layerId,
layerName
};
}
}
Was this page helpful?Thanks for the feedback!