import { getMouseCoords, safeURL, ucfirst } from "@/Core/Utils"
import { GeoPoint, Location, SVGPoint, ScreenPoint } from "../Location/Location"
import { MapSVGMap } from "../Map/Map"
import { ViewBox } from "../Map/ViewBox"
import { MapObject, MapObjectType } from "../MapObject/MapObject"
import { Model } from "../Model/Model"
import "./marker.css"
const $ = jQuery
/**
* Marker class
* @extends MapObject
*
* @example
* let location = new mapsvg.location({
* geoPoint: new mapsvg.geoPoint({lat: 55.12, lng: 46.19}),
* img: "/path/to/image.png"
* });
*
* let marker = new mapsvg.marker({
* location: location,
* mapsvg: mapInstance
* });
*
* // The marker is created but still not added to the map. Let's add it:
* mapInstance.markerAdd(marker);
*
*/
export class Marker extends MapObject {
name: string
type: MapObjectType
src: string
location: Location
svgPoint?: SVGPoint
geoPoint?: GeoPoint
screenPoint?: ScreenPoint
label: HTMLElement
image: HTMLElement
width: number
height: number
selected: boolean
object: Model
centered: boolean
altAttr: string
private svgPointBeforeDrag: SVGPoint
mapped?: boolean
needToRemove?: boolean
moving?: boolean
bubble: HTMLElement
bubbleMode: boolean
visible: boolean
id: string
constructor(params: {
location: Location
object?: Model
mapsvg: MapSVGMap
width?: number
height?: number
id?: string
}) {
super(null, params.mapsvg)
this.name = "marker"
this.type = MapObjectType.MARKER
this.element = $("
").addClass("mapsvg-marker")[0]
this.image = $('
').addClass("mapsvg-marker-image")[0]
this.image.style.visibility = "hidden"
if (params.object) {
this.setObject(params.object)
}
if (!(params.location instanceof Location)) {
throw new Error("No Location provided for Marker initializaton")
} else {
this.location = params.location
this.location.marker = this
}
this.setImage(this.location.imagePath)
$(this.element).append(this.image)
if (params.width && params.height) {
this.setSize(params.width, params.height)
} else {
this.setSize(15, 24)
}
this.setId(params.id || (this.object ? `marker_${this.object["id"]}` : this.mapsvg.markerId()))
this.setSvgPointFromLocation()
this.setAltAttr()
}
reload() {
this.setImage()
this.setSvgPointFromLocation()
}
private getImagePath() {
return (this.object && this.object.getMarkerImage()) || this.location.getMarkerImage()
}
setSize(width: number, height: number): void {
this.width = width
this.height = height
this.setCentered(this.width === this.height)
}
setSvgPointFromLocation(): void {
const svgPoint = this.location.svgPoint
? this.location.svgPoint
: this.location.geoPoint &&
this.location.geoPoint.lat !== 0 &&
this.location.geoPoint.lng !== 0
? this.mapsvg.converter.convertGeoToSVG(this.location.geoPoint)
: null
if (svgPoint) {
this.setSvgPoint(svgPoint)
}
}
/**
* Set ID of the Marker
* @param {string} id
*/
setId(id: string | number): void {
MapObject.prototype.setId.call(this, id)
this.mapsvg.markers.reindex()
}
/**
* Get SVG bounding box of the Marker
* @returns {ViewBox}
*/
getBBox(scale?: number): ViewBox {
scale = scale || this.mapsvg.getScale()
return new ViewBox(
this.svgPoint.x - this.width / 2 / scale,
this.svgPoint.y - this.height / (this.centered ? 1 : 2) / scale,
this.width / scale,
this.height / scale,
)
}
/**
* Returns geo-bounds of an object - South-West & North-East points.
* @returns {sw: GeoPoint, ne: GeoPoint}
*/
getGeoBounds(): { sw: GeoPoint; ne: GeoPoint } {
const sw = this.location.geoPoint
const ne = this.location.geoPoint
return { sw: sw, ne: ne }
}
/**
* Get Marker options
* @returns {{id: string, src: string, svgPoint: SVGPoint, geoPoint: GeoPoint}}
*/
getOptions(): { id: string; src: string; svgPoint: SVGPoint; geoPoint: GeoPoint } {
const o = {
id: this.id,
src: this.src,
svgPoint: this.svgPoint,
geoPoint: this.geoPoint,
}
$.each(o, function (key, val) {
if (val == undefined) {
delete o[key]
}
})
return o
}
/**
* Update marker settings
* @param {object} data - Options
*/
update(data): void {
for (const key in data) {
// check if there's a setter for a property
const setter = "set" + ucfirst(key)
if (setter in this) this[setter](data[key])
}
}
/**
* Set image of the Marker
* @param {string} src
*/
setImage(src?: string, skipChangingLocationImage = false): void {
if (!src) {
src = this.getImagePath()
}
this.src = safeURL(src)
const img = new Image()
img.onload = () => {
if (this.image.getAttribute("src") !== "src") {
this.image.setAttribute("src", this.src)
}
this.setSize((img).width, (img).height)
this.image.style.visibility = "visible"
this.adjustScreenPosition()
}
img.src = this.src
this.events.trigger("change")
}
/**
* Set 'alt' attribute of the Marker
*/
setAltAttr(): void {
const marker = this
marker.altAttr =
typeof marker.object != "undefined" &&
typeof marker.object["title"] != "undefined" &&
marker.object["title"] !== ""
? marker.object["title"]
: marker.id
marker.image.setAttribute("alt", marker.altAttr)
}
/**
* Set x/y coordinates of the Marker
* @param {SVGPoint} svgPoint
*/
setSvgPoint(svgPoint: SVGPoint): void {
this.svgPoint = svgPoint
// if (this.location) {
// this.location.setSvgPoint(this.svgPoint)
// }
// if (this.mapsvg.mapIsGeo) {
// this.geoPoint = this.mapsvg.converter.convertSVGToGeo(this.svgPoint)
// this.location.setGeoPoint(this.geoPoint)
// }
this.adjustScreenPosition()
this.events.trigger("change")
}
/**
* Moves marker to a point.
* @private
* @param {Array} xy
*/
/*
moveSvgPositionToClick = function (screen) {
var _data = this.mapsvg.getData();
var markerOptions = {};
xy[0] = xy[0] + _data.viewBox[0];
xy[1] = xy[1] + _data.viewBox[1];
if (_data.mapIsGeo)
this.geoCoords = this.mapsvg.converter.convertSVGToGeo(xy[0], xy[1]);
markerOptions.xy = xy;
this.update(markerOptions);
};
*/
/**
* Adjusts position of the Marker. Called on map zoom and on map container resize.
*/
adjustScreenPosition(): void {
const pos = this.mapsvg.converter.convertSVGToPixel(this.svgPoint)
pos.x -= this.width / 2
pos.y -= !this.centered ? this.height : this.height / 2
this.setScreenPosition(pos.x, pos.y)
}
/**
* Moves marker by given numbers
*
* @param {number} deltaX
* @param {number} deltaY
*/
moveSrceenPositionBy(deltaX: number, deltaY: number): void {
const oldPos = this.screenPoint,
x = oldPos.x - deltaX,
y = oldPos.y - deltaY
this.setScreenPosition(x, y)
}
/**
* Set position of the marker by given numbers
*
* @param {number} x
* @param {number} y
*/
setScreenPosition(x: number, y: number): void {
if (this.screenPoint instanceof ScreenPoint) {
this.screenPoint.x = x
this.screenPoint.y = y
} else {
this.screenPoint = new ScreenPoint(x, y)
}
this.updateVisibility()
if (this.visible === true) {
this.element.style.transform = "translate(" + x + "px," + y + "px)"
this.adjustLabelScreenPosition()
}
}
/**
* Adjust position of marker label
*
*/
adjustLabelScreenPosition(): void {
if (this.label) {
const markerPos = this.screenPoint,
x = Math.round(markerPos.x + this.width / 2 - $(this.label).outerWidth() / 2),
y = Math.round(markerPos.y - $(this.label).outerHeight())
}
}
/**
* Check if the marker is inside of the viewBox
*
* @return boolean
*/
inViewBox(): boolean {
const x = this.screenPoint.x,
y = this.screenPoint.y,
mapFullWidth = this.mapsvg.containers.map.offsetWidth,
mapFullHeight = this.mapsvg.containers.map.offsetHeight
// Marker stays visible if it's inside of the map-width/height buffer zone around the map:
return (
x - this.width / 2 < 2 * mapFullWidth &&
x + this.width / 2 > -mapFullWidth &&
y - this.height / 2 < 2 * mapFullHeight &&
y + this.height / 2 > -mapFullHeight
)
}
/**
* Set visibility of the marker
*
*/
updateVisibility(): boolean {
if (this.inViewBox() === true) {
this.visible = true
this.element.classList.remove("mapsvg-out-of-sight")
if (this.label) {
this.label.classList.remove("mapsvg-out-of-sight")
}
} else {
this.visible = false
this.element.classList.add("mapsvg-out-of-sight")
if (this.label) {
this.label.classList.add("mapsvg-out-of-sight")
}
}
return this.visible
}
isMoving(): boolean {
return this.moving
}
setMoving(value: boolean) {
this.moving = value
}
/**
* Marker drag event handler
* @private
* @param startCoords
* @param scale
* @param endCallback
* @param clickCallback
*/
drag(startCoords, scale, endCallback?: () => void, clickCallback?: () => void): void {
const _this = this
this.svgPointBeforeDrag = new SVGPoint(this.svgPoint.x, this.svgPoint.y)
this.setMoving(true)
$("body").on("mousemove.drag.mapsvg", function (e) {
e.preventDefault()
$(_this.mapsvg.containers.map).addClass("no-transitions")
//$('body').css('cursor','move');
const mouseNew = getMouseCoords(e)
const dx = mouseNew.x - startCoords.x
const dy = mouseNew.y - startCoords.y
const newSvgPoint = new SVGPoint(
_this.svgPointBeforeDrag.x + dx / scale,
_this.svgPointBeforeDrag.y + dy / scale,
)
_this.location.setSvgPoint(newSvgPoint)
})
$("body").on("mouseup.drag.mapsvg", function (e) {
e.preventDefault()
_this.undrag()
const mouseNew = getMouseCoords(e)
const dx = mouseNew.x - startCoords.x
const dy = mouseNew.y - startCoords.y
const newSvgPoint = new SVGPoint(
_this.svgPointBeforeDrag.x + dx / scale,
_this.svgPointBeforeDrag.y + dy / scale,
)
_this.location.setSvgPoint(newSvgPoint)
if (_this.mapsvg.isGeo()) {
const geoPoint = _this.mapsvg.converter.convertSVGToGeo(newSvgPoint)
_this.location.setGeoPoint(geoPoint)
}
endCallback && endCallback.call(_this)
if (
_this.svgPointBeforeDrag.x == _this.svgPoint.x &&
_this.svgPointBeforeDrag.y == _this.svgPoint.y
)
clickCallback && clickCallback.call(_this)
})
}
/**
* Marker undrag event handler
* @private
*/
undrag(): void {
//$(this.element).closest('svg').css('pointer-events','auto');
//$('body').css('cursor','default');
this.setMoving(false)
$("body").off(".drag.mapsvg")
$(this.mapsvg.containers.map).removeClass("no-transitions")
}
/**
* Deletes the Marker
*/
delete(): void {
if (this.label) {
this.label.remove()
this.label = null
}
$(this.element).empty().remove()
}
/**
* Sets parent DB object of the Marker
* @param {object} obj
*/
setObject(obj: Model): void {
this.object = obj
$(this.element).attr("data-object-id", this.object["id"])
}
/**
* Hides the Marker
*/
hide(): void {
$(this.element).addClass("mapsvg-marker-hidden")
if (this.label) {
$(this.label).hide()
}
}
/**
* Shows the Marker
*/
show(): void {
$(this.element).removeClass("mapsvg-marker-hidden")
if (this.label) {
$(this.label).show()
}
}
/**
* Highlight the Marker.
* Used on mouseover.
*/
highlight(): void {
$(this.element).addClass("mapsvg-marker-hover")
}
/**
* Unhighlight the Marker.
* Used on mouseout.
*/
unhighlight(): void {
$(this.element).removeClass("mapsvg-marker-hover")
}
/**
* Select the Marker.
*/
select(): void {
this.selected = true
$(this.element).addClass("mapsvg-marker-active")
}
/**
* Deselect the Marker.
*/
deselect(): void {
this.selected = false
$(this.element).removeClass("mapsvg-marker-active")
}
getData(): Model {
return this.object
}
/**
* Returns color of the choropleth marker bubble
* @returns {string} color
*/
getChoroplethColor(): string {
const markerValue = parseFloat(this.object[this.mapsvg.options.choropleth.sourceField])
let color
if (!markerValue) {
color = this.mapsvg.options.choropleth.coloring.noData.color
} else if (this.mapsvg.options.choropleth.coloring.mode === "gradient") {
// Gradient mode
const gradient = this.mapsvg.options.choropleth.coloring.gradient,
w =
gradient.values.maxAdjusted === 0
? 0
: (markerValue - gradient.values.min) / gradient.values.maxAdjusted,
r = Math.round(gradient.colors.diffRGB.r * w + gradient.colors.lowRGB.r),
g = Math.round(gradient.colors.diffRGB.g * w + gradient.colors.lowRGB.g),
b = Math.round(gradient.colors.diffRGB.b * w + gradient.colors.lowRGB.b),
a = (gradient.colors.diffRGB.a * w + gradient.colors.lowRGB.a).toFixed(2)
color = "rgba(" + r + "," + g + "," + b + "," + a + ")"
} else {
// Palette mode
const paletteColors = this.mapsvg.options.choropleth.coloring.palette.colors
if (!paletteColors[0].valueFrom && markerValue < paletteColors[0].valueTo) {
color = paletteColors[0].color
} else if (
!paletteColors[paletteColors.length - 1].valueTo &&
markerValue > paletteColors[paletteColors.length - 1].valueFrom
) {
color = paletteColors[paletteColors.length - 1].color
} else {
paletteColors.forEach(function (paletteColor) {
if (markerValue >= paletteColor.valueFrom && markerValue < paletteColor.valueTo) {
color = paletteColor.color
}
})
color = color ? color : this.mapsvg.options.choropleth.coloring.palette.outOfRange.color
}
}
return color
}
/**
* Returns size of the choropleth bubble
*/
getBubbleSize(): number {
let bubbleSize
if (this.object[this.mapsvg.options.choropleth.sourceField]) {
const maxBubbleSize = Number(this.mapsvg.options.choropleth.bubbleSize.max),
minBubbleSize = Number(this.mapsvg.options.choropleth.bubbleSize.min),
maxSourceFieldvalue = this.mapsvg.options.choropleth.coloring.gradient.values.max,
minSourceFieldvalue = this.mapsvg.options.choropleth.coloring.gradient.values.min,
sourceFieldvalue = parseFloat(this.object[this.mapsvg.options.choropleth.sourceField])
bubbleSize =
((sourceFieldvalue - minSourceFieldvalue) / (maxSourceFieldvalue - minSourceFieldvalue)) *
(maxBubbleSize - minBubbleSize) +
Number(minBubbleSize)
} else {
bubbleSize = false
}
return bubbleSize
}
/**
* Returns screen point of the choropleth bubble
*/
getBubbleScreenPosition(): { x: number; y: number } {
const bubbleSize = Number(this.getBubbleSize())
return {
x: this.width / 2 - bubbleSize / 2,
y: this.height - bubbleSize / 2,
}
}
/**
* Draw a choropleth bubble for the marker
*/
drawBubble(): void {
const bubbleId = "mapsvg-bubble-" + this.object["id"]
const bubbleValue = parseFloat(this.object[this.mapsvg.options.choropleth.sourceField])
if (bubbleValue) {
if (!this.bubble) {
this.bubble = $(
'',
)[0]
$(this.element).append(this.bubble)
}
const color = this.getChoroplethColor(),
bubbleSize = Number(this.getBubbleSize())
$(this.bubble)
.css("background-color", color)
.css("width", bubbleSize + "px")
.css("height", bubbleSize + "px")
.css("lineHeight", bubbleSize - 2 + "px")
} else {
$("#" + bubbleId).remove()
delete this.bubble
}
}
/**
* Disable/enable BubbleMode for the marker
*
* @param {boolean} bubbleMode
*/
setBubbleMode(bubbleMode: boolean): void {
this.bubbleMode = bubbleMode
if (bubbleMode) {
this.setCentered(true)
this.drawBubble()
if (this.bubble) {
this.width = this.bubble.offsetWidth
this.height = this.bubble.offsetHeight
this.adjustScreenPosition()
}
} else {
this.setImage(this.src)
}
}
setLabel(html: string): void {
if (html) {
if (!this.label) {
this.label = $("").addClass("mapsvg-marker-label")[0]
$(this.element).append(this.label)
}
$(this.label).html(html)
} else {
if (this.label) {
$(this.label).remove()
delete this.label
}
}
}
/**
* Set centered propperty of the marker
*
* @param {boolean} on
*/
setCentered(on: boolean): void {
this.centered = on
}
}