1114 lines
30 KiB
JavaScript
1114 lines
30 KiB
JavaScript
|
|
// import * as mars3d from "mars3d"
|
||
|
|
|
||
|
|
;(function (window) {
|
||
|
|
const BUF = 64
|
||
|
|
const tileSize = 256
|
||
|
|
const ArcGISPbfImageryProviderRequestTrailer = "/tile/{z}/{y}/{x}.pbf"
|
||
|
|
|
||
|
|
class MarsPoint {
|
||
|
|
x
|
||
|
|
y
|
||
|
|
constructor(x, y) {
|
||
|
|
this.x = x
|
||
|
|
this.y = y
|
||
|
|
}
|
||
|
|
|
||
|
|
clone() {
|
||
|
|
return new MarsPoint(this.x, this.y)
|
||
|
|
}
|
||
|
|
|
||
|
|
add(p) {
|
||
|
|
return this.clone()._add(p)
|
||
|
|
}
|
||
|
|
|
||
|
|
sub(p) {
|
||
|
|
return this.clone()._sub(p)
|
||
|
|
}
|
||
|
|
|
||
|
|
multByPoint(p) {
|
||
|
|
return this.clone()._multByPoint(p)
|
||
|
|
}
|
||
|
|
|
||
|
|
divByPoint(p) {
|
||
|
|
return this.clone()._divByPoint(p)
|
||
|
|
}
|
||
|
|
|
||
|
|
mult(k) {
|
||
|
|
return this.clone()._mult(k)
|
||
|
|
}
|
||
|
|
|
||
|
|
div(k) {
|
||
|
|
return this.clone()._div(k)
|
||
|
|
}
|
||
|
|
|
||
|
|
rotate(a) {
|
||
|
|
return this.clone()._rotate(a)
|
||
|
|
}
|
||
|
|
|
||
|
|
rotateAround(a, p) {
|
||
|
|
return this.clone()._rotateAround(a, p)
|
||
|
|
}
|
||
|
|
|
||
|
|
matMult(m) {
|
||
|
|
return this.clone()._matMult(m)
|
||
|
|
}
|
||
|
|
|
||
|
|
unit() {
|
||
|
|
return this.clone()._unit()
|
||
|
|
}
|
||
|
|
|
||
|
|
perp() {
|
||
|
|
return this.clone()._perp()
|
||
|
|
}
|
||
|
|
|
||
|
|
round() {
|
||
|
|
return this.clone()._round()
|
||
|
|
}
|
||
|
|
|
||
|
|
mag() {
|
||
|
|
return Math.sqrt(this.x * this.x + this.y * this.y)
|
||
|
|
}
|
||
|
|
|
||
|
|
equals(other) {
|
||
|
|
return this.x === other.x && this.y === other.y
|
||
|
|
}
|
||
|
|
|
||
|
|
dist(p) {
|
||
|
|
return Math.sqrt(this.distSqr(p))
|
||
|
|
}
|
||
|
|
|
||
|
|
distSqr(p) {
|
||
|
|
var dx = p.x - this.x,
|
||
|
|
dy = p.y - this.y
|
||
|
|
return dx * dx + dy * dy
|
||
|
|
}
|
||
|
|
|
||
|
|
angle() {
|
||
|
|
return Math.atan2(this.y, this.x)
|
||
|
|
}
|
||
|
|
|
||
|
|
angleTo(b) {
|
||
|
|
return Math.atan2(this.y - b.y, this.x - b.x)
|
||
|
|
}
|
||
|
|
|
||
|
|
angleWith(b) {
|
||
|
|
return this.angleWithSep(b.x, b.y)
|
||
|
|
}
|
||
|
|
|
||
|
|
angleWithSep(x, y) {
|
||
|
|
return Math.atan2(this.x * y - this.y * x, this.x * x + this.y * y)
|
||
|
|
}
|
||
|
|
|
||
|
|
_matMult(m) {
|
||
|
|
var x = m[0] * this.x + m[1] * this.y,
|
||
|
|
y = m[2] * this.x + m[3] * this.y
|
||
|
|
this.x = x
|
||
|
|
this.y = y
|
||
|
|
return this
|
||
|
|
}
|
||
|
|
|
||
|
|
_add(p) {
|
||
|
|
this.x += p.x
|
||
|
|
this.y += p.y
|
||
|
|
return this
|
||
|
|
}
|
||
|
|
|
||
|
|
_sub(p) {
|
||
|
|
this.x -= p.x
|
||
|
|
this.y -= p.y
|
||
|
|
return this
|
||
|
|
}
|
||
|
|
|
||
|
|
_mult(k) {
|
||
|
|
this.x *= k
|
||
|
|
this.y *= k
|
||
|
|
return this
|
||
|
|
}
|
||
|
|
|
||
|
|
_div(k) {
|
||
|
|
this.x /= k
|
||
|
|
this.y /= k
|
||
|
|
return this
|
||
|
|
}
|
||
|
|
|
||
|
|
_multByPoint(p) {
|
||
|
|
this.x *= p.x
|
||
|
|
this.y *= p.y
|
||
|
|
return this
|
||
|
|
}
|
||
|
|
|
||
|
|
_divByPoint(p) {
|
||
|
|
this.x /= p.x
|
||
|
|
this.y /= p.y
|
||
|
|
return this
|
||
|
|
}
|
||
|
|
|
||
|
|
_unit() {
|
||
|
|
this._div(this.mag())
|
||
|
|
return this
|
||
|
|
}
|
||
|
|
|
||
|
|
_perp() {
|
||
|
|
var y = this.y
|
||
|
|
this.y = this.x
|
||
|
|
this.x = -y
|
||
|
|
return this
|
||
|
|
}
|
||
|
|
|
||
|
|
_rotate(angle) {
|
||
|
|
var cos = Math.cos(angle),
|
||
|
|
sin = Math.sin(angle),
|
||
|
|
x = cos * this.x - sin * this.y,
|
||
|
|
y = sin * this.x + cos * this.y
|
||
|
|
this.x = x
|
||
|
|
this.y = y
|
||
|
|
return this
|
||
|
|
}
|
||
|
|
|
||
|
|
_rotateAround(angle, p) {
|
||
|
|
var cos = Math.cos(angle),
|
||
|
|
sin = Math.sin(angle),
|
||
|
|
x = p.x + cos * (this.x - p.x) - sin * (this.y - p.y),
|
||
|
|
y = p.y + sin * (this.x - p.x) + cos * (this.y - p.y)
|
||
|
|
this.x = x
|
||
|
|
this.y = y
|
||
|
|
return this
|
||
|
|
}
|
||
|
|
|
||
|
|
_round() {
|
||
|
|
this.x = Math.round(this.x)
|
||
|
|
this.y = Math.round(this.y)
|
||
|
|
return this
|
||
|
|
}
|
||
|
|
|
||
|
|
static convert(a) {
|
||
|
|
if (a instanceof MarsPoint) {
|
||
|
|
return a
|
||
|
|
}
|
||
|
|
if (Array.isArray(a)) {
|
||
|
|
return new MarsPoint(a[0], a[1])
|
||
|
|
}
|
||
|
|
return a
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class MarsSheet {
|
||
|
|
json
|
||
|
|
canvas
|
||
|
|
mapping
|
||
|
|
missingBox
|
||
|
|
|
||
|
|
constructor(json, canvas) {
|
||
|
|
this.json = json
|
||
|
|
this.canvas = canvas
|
||
|
|
this.mapping = new Map()
|
||
|
|
this.missingBox = { x: 0, y: 0, w: 0, h: 0 }
|
||
|
|
const scale = window.devicePixelRatio
|
||
|
|
// 根据 key 分割精灵图
|
||
|
|
for (let i = 0; i < Object.keys(this.json).length; i++) {
|
||
|
|
const k = Object.keys(this.json)[i]
|
||
|
|
const v = Object.values(this.json)[i]
|
||
|
|
this.mapping.set(k, {
|
||
|
|
x: v.x,
|
||
|
|
y: v.y,
|
||
|
|
w: v.width * scale,
|
||
|
|
h: v.height * scale
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
get(name) {
|
||
|
|
let result = this.mapping.get(name)
|
||
|
|
if (!result) result = this.missingBox
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function linebreak(str, maxUnits) {
|
||
|
|
if (str.length <= maxUnits) return [str]
|
||
|
|
let endIndex = maxUnits - 1
|
||
|
|
let space_before = str.lastIndexOf(" ", endIndex)
|
||
|
|
let space_after = str.indexOf(" ", endIndex)
|
||
|
|
if (space_before == -1 && space_after == -1) {
|
||
|
|
return [str]
|
||
|
|
}
|
||
|
|
let first
|
||
|
|
let after
|
||
|
|
if (space_after == -1 || (space_before >= 0 && endIndex - space_before < space_after - endIndex)) {
|
||
|
|
first = str.substring(0, space_before)
|
||
|
|
after = str.substring(space_before + 1, str.length)
|
||
|
|
} else {
|
||
|
|
first = str.substring(0, space_after)
|
||
|
|
after = str.substring(space_after + 1, str.length)
|
||
|
|
}
|
||
|
|
return [first, ...linebreak(after, maxUnits)]
|
||
|
|
}
|
||
|
|
|
||
|
|
class FontAttr {
|
||
|
|
family
|
||
|
|
size
|
||
|
|
weight
|
||
|
|
style
|
||
|
|
font
|
||
|
|
|
||
|
|
constructor(options) {
|
||
|
|
if (options?.font) {
|
||
|
|
this.font = options.font
|
||
|
|
} else {
|
||
|
|
this.family = options?.fontFamily ?? "sans-serif"
|
||
|
|
this.size = options?.fontSize ?? 12
|
||
|
|
this.weight = options?.fontWeight
|
||
|
|
this.style = options?.fontStyle
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
get(z, f) {
|
||
|
|
if (this.font) {
|
||
|
|
if (typeof this.font === "function") {
|
||
|
|
return this.font(z, f)
|
||
|
|
} else {
|
||
|
|
return this.font
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
let style = ""
|
||
|
|
if (this.style) {
|
||
|
|
if (typeof this.style === "function") {
|
||
|
|
style = this.style(z, f) + " "
|
||
|
|
} else {
|
||
|
|
style = this.style + " "
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let weight = ""
|
||
|
|
if (this.weight) {
|
||
|
|
if (typeof this.weight === "function") {
|
||
|
|
weight = this.weight(z, f) + " "
|
||
|
|
} else {
|
||
|
|
weight = this.weight + " "
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let size
|
||
|
|
if (typeof this.size === "function") {
|
||
|
|
size = this.size(z, f)
|
||
|
|
} else {
|
||
|
|
size = this.size
|
||
|
|
}
|
||
|
|
|
||
|
|
let family
|
||
|
|
if (typeof this.family === "function") {
|
||
|
|
family = this.family(z, f)
|
||
|
|
} else {
|
||
|
|
family = this.family
|
||
|
|
}
|
||
|
|
|
||
|
|
return `${style}${weight}${size}px ${family}`
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class TextAttr {
|
||
|
|
label_props
|
||
|
|
textTransform
|
||
|
|
|
||
|
|
constructor(options) {
|
||
|
|
this.label_props = options?.label_props ?? ["name"]
|
||
|
|
this.textTransform = options?.textTransform
|
||
|
|
}
|
||
|
|
|
||
|
|
get(z, f) {
|
||
|
|
let retval
|
||
|
|
|
||
|
|
let label_props
|
||
|
|
if (typeof this.label_props == "function") {
|
||
|
|
label_props = this.label_props(z, f)
|
||
|
|
} else {
|
||
|
|
label_props = this.label_props
|
||
|
|
}
|
||
|
|
for (let property of label_props) {
|
||
|
|
if (f.props.hasOwnProperty(property) && typeof f.props[property] === "string") {
|
||
|
|
retval = f.props[property]
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let transform
|
||
|
|
if (typeof this.textTransform === "function") {
|
||
|
|
transform = this.textTransform(z, f)
|
||
|
|
} else {
|
||
|
|
transform = this.textTransform
|
||
|
|
}
|
||
|
|
if (retval && transform === "uppercase") retval = retval.toUpperCase()
|
||
|
|
else if (retval && transform === "lowercase") retval = retval.toLowerCase()
|
||
|
|
else if (retval && transform === "capitalize") {
|
||
|
|
const wordsArray = retval.toLowerCase().split(" ")
|
||
|
|
const capsArray = wordsArray.map((word) => {
|
||
|
|
return word[0].toUpperCase() + word.slice(1)
|
||
|
|
})
|
||
|
|
retval = capsArray.join(" ")
|
||
|
|
}
|
||
|
|
return retval
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class StringAttr {
|
||
|
|
str
|
||
|
|
per_feature
|
||
|
|
|
||
|
|
constructor(c, defaultValue) {
|
||
|
|
this.str = c ?? defaultValue
|
||
|
|
this.per_feature = typeof this.str == "function" && this.str.length == 2
|
||
|
|
}
|
||
|
|
|
||
|
|
get(z, f) {
|
||
|
|
if (typeof this.str === "function") {
|
||
|
|
return this.str(z, f)
|
||
|
|
} else {
|
||
|
|
return this.str
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class NumberAttr {
|
||
|
|
value
|
||
|
|
per_feature
|
||
|
|
|
||
|
|
constructor(c, defaultValue = 1) {
|
||
|
|
this.value = c ?? defaultValue
|
||
|
|
this.per_feature = typeof this.value == "function" && this.value.length == 2
|
||
|
|
}
|
||
|
|
|
||
|
|
get(z, f) {
|
||
|
|
if (typeof this.value == "function") {
|
||
|
|
return this.value(z, f)
|
||
|
|
} else {
|
||
|
|
return this.value
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 文本符号化工具
|
||
|
|
class TextSymbolizer {
|
||
|
|
font
|
||
|
|
text
|
||
|
|
fillAttr
|
||
|
|
strokeAttr
|
||
|
|
width
|
||
|
|
lineHeight // in ems
|
||
|
|
letterSpacing // in px
|
||
|
|
maxLineCodeUnits
|
||
|
|
justify
|
||
|
|
|
||
|
|
constructor(options) {
|
||
|
|
this.font = new FontAttr(options)
|
||
|
|
this.text = new TextAttr(options)
|
||
|
|
|
||
|
|
this.fill = new StringAttr(options.fill, "black")
|
||
|
|
this.stroke = new StringAttr(options.stroke, "black")
|
||
|
|
this.width = new NumberAttr(options.width, 0)
|
||
|
|
this.lineHeight = new NumberAttr(options.lineHeight, 1)
|
||
|
|
this.letterSpacing = new NumberAttr(options.letterSpacing, 0)
|
||
|
|
this.maxLineCodeUnits = new NumberAttr(options.maxLineChars, 15)
|
||
|
|
this.justify = options.justify
|
||
|
|
}
|
||
|
|
|
||
|
|
place(layout, geom, feature) {
|
||
|
|
let property = this.text.get(layout.zoom, feature)
|
||
|
|
if (!property) return undefined
|
||
|
|
let font = this.font.get(layout.zoom, feature)
|
||
|
|
layout.scratch.font = font
|
||
|
|
|
||
|
|
let letterSpacing = this.letterSpacing.get(layout.zoom, feature)
|
||
|
|
|
||
|
|
// line breaking
|
||
|
|
let lines = linebreak(property, this.maxLineCodeUnits.get(layout.zoom, feature))
|
||
|
|
let longestLine = ""
|
||
|
|
let longestLineLen = 0
|
||
|
|
for (let line of lines) {
|
||
|
|
if (line.length > longestLineLen) {
|
||
|
|
longestLineLen = line.length
|
||
|
|
longestLine = line
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let metrics = layout.scratch.measureText(longestLine)
|
||
|
|
let width = metrics.width + letterSpacing * (longestLineLen - 1)
|
||
|
|
|
||
|
|
let ascent = metrics.actualBoundingBoxAscent
|
||
|
|
let descent = metrics.actualBoundingBoxDescent
|
||
|
|
let lineHeight = (ascent + descent) * this.lineHeight.get(layout.zoom, feature)
|
||
|
|
|
||
|
|
let a = new MarsPoint(geom[0][0].x, geom[0][0].y)
|
||
|
|
let bbox = {
|
||
|
|
minX: a.x,
|
||
|
|
minY: a.y - ascent,
|
||
|
|
maxX: a.x + width,
|
||
|
|
maxY: a.y + descent + (lines.length - 1) * lineHeight
|
||
|
|
}
|
||
|
|
|
||
|
|
// inside draw, the origin is the anchor
|
||
|
|
// and the anchor is the typographic baseline of the first line
|
||
|
|
let draw = (ctx, extra) => {
|
||
|
|
ctx.globalAlpha = 1
|
||
|
|
ctx.font = font
|
||
|
|
ctx.fillStyle = this.fill.get(layout.zoom, feature)
|
||
|
|
let textStrokeWidth = this.width.get(layout.zoom, feature)
|
||
|
|
|
||
|
|
let y = 0
|
||
|
|
for (let line of lines) {
|
||
|
|
let startX = 0
|
||
|
|
if (this.justify == protomaps.Justify.Center || (extra && extra.justify == protomaps.Justify.Center)) {
|
||
|
|
startX = (width - ctx.measureText(line).width) / 2
|
||
|
|
} else if (this.justify == protomaps.Justify.Right || (extra && extra.justify == protomaps.Justify.Right)) {
|
||
|
|
startX = width - ctx.measureText(line).width
|
||
|
|
}
|
||
|
|
if (textStrokeWidth) {
|
||
|
|
ctx.lineWidth = textStrokeWidth * 2 // centered stroke
|
||
|
|
ctx.strokeStyle = this.stroke.get(layout.zoom, feature)
|
||
|
|
if (letterSpacing > 0) {
|
||
|
|
let xPos = startX
|
||
|
|
for (let letter of line) {
|
||
|
|
ctx.strokeText(letter, xPos, y)
|
||
|
|
xPos += ctx.measureText(letter).width + letterSpacing
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
ctx.strokeText(line, startX, y)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (letterSpacing > 0) {
|
||
|
|
let xPos = startX
|
||
|
|
for (let letter of line) {
|
||
|
|
ctx.fillText(letter, xPos, y)
|
||
|
|
xPos += ctx.measureText(letter).width + letterSpacing
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
ctx.fillText(line, startX, y)
|
||
|
|
}
|
||
|
|
y += lineHeight
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return [{ anchor: a, bboxes: [bbox], draw: draw }]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class IconSymbolizer {
|
||
|
|
name
|
||
|
|
sheet
|
||
|
|
dpr
|
||
|
|
textSymbolizer
|
||
|
|
|
||
|
|
constructor(options) {
|
||
|
|
this.name = options.name
|
||
|
|
this.sheet = options.sheet
|
||
|
|
this.dpr = window.devicePixelRatio
|
||
|
|
this.textSymbolizer = new TextSymbolizer(options)
|
||
|
|
}
|
||
|
|
|
||
|
|
place(layout, geom, feature) {
|
||
|
|
let pt = geom[0]
|
||
|
|
let a = new MarsPoint(geom[0][0].x, geom[0][0].y)
|
||
|
|
let loc = this.sheet.get(this.name)
|
||
|
|
let width = loc.w / this.dpr
|
||
|
|
let height = loc.h / this.dpr
|
||
|
|
|
||
|
|
const text = this.textSymbolizer.place(layout, geom, feature)
|
||
|
|
|
||
|
|
let bbox = {
|
||
|
|
minX: a.x - width / 2,
|
||
|
|
minY: a.y - height / 2,
|
||
|
|
maxX: a.x + width / 2,
|
||
|
|
maxY: a.y + height / 2
|
||
|
|
}
|
||
|
|
|
||
|
|
let draw = (ctx) => {
|
||
|
|
ctx.globalAlpha = 1
|
||
|
|
if (text) {
|
||
|
|
text[0].draw(ctx)
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx.drawImage(this.sheet.canvas, loc.x, loc.y, loc.w, loc.h, -loc.w / 2 / this.dpr, -loc.h / 2 / this.dpr, loc.w / 2, loc.h / 2)
|
||
|
|
}
|
||
|
|
return [{ anchor: a, bboxes: [bbox], draw: draw }]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function number(val, defaultValue) {
|
||
|
|
return typeof val === "number" ? val : defaultValue
|
||
|
|
}
|
||
|
|
|
||
|
|
function filterFn(arr) {
|
||
|
|
// hack around "$type"
|
||
|
|
if (arr.includes("$type")) {
|
||
|
|
return (z) => true
|
||
|
|
} else if (arr[0] == "==") {
|
||
|
|
return (z, f) => f.props[arr[1]] === arr[2]
|
||
|
|
} else if (arr[0] == "!=") {
|
||
|
|
return (z, f) => f.props[arr[1]] !== arr[2]
|
||
|
|
} else if (arr[0] == "!") {
|
||
|
|
let sub = filterFn(arr[1])
|
||
|
|
return (z, f) => !sub(z, f)
|
||
|
|
} else if (arr[0] === "<") {
|
||
|
|
return (z, f) => number(f.props[arr[1]], Infinity) < arr[2]
|
||
|
|
} else if (arr[0] === "<=") {
|
||
|
|
return (z, f) => number(f.props[arr[1]], Infinity) <= arr[2]
|
||
|
|
} else if (arr[0] === ">") {
|
||
|
|
return (z, f) => number(f.props[arr[1]], -Infinity) > arr[2]
|
||
|
|
} else if (arr[0] === ">=") {
|
||
|
|
return (z, f) => number(f.props[arr[1]], -Infinity) >= arr[2]
|
||
|
|
} else if (arr[0] === "in") {
|
||
|
|
return (z, f) => arr.slice(2, arr.length).includes(f.props[arr[1]])
|
||
|
|
} else if (arr[0] === "!in") {
|
||
|
|
return (z, f) => !arr.slice(2, arr.length).includes(f.props[arr[1]])
|
||
|
|
} else if (arr[0] === "has") {
|
||
|
|
return (z, f) => f.props.hasOwnProperty(arr[1])
|
||
|
|
} else if (arr[0] === "!has") {
|
||
|
|
return (z, f) => !f.props.hasOwnProperty(arr[1])
|
||
|
|
} else if (arr[0] === "all") {
|
||
|
|
let parts = arr.slice(1, arr.length).map((e) => filterFn(e))
|
||
|
|
return (z, f) =>
|
||
|
|
parts.every((p) => {
|
||
|
|
return p(z, f)
|
||
|
|
})
|
||
|
|
} else if (arr[0] === "any") {
|
||
|
|
let parts = arr.slice(1, arr.length).map((e) => filterFn(e))
|
||
|
|
return (z, f) =>
|
||
|
|
parts.some((p) => {
|
||
|
|
return p(z, f)
|
||
|
|
})
|
||
|
|
} else {
|
||
|
|
console.log("Unimplemented filter: ", arr[0])
|
||
|
|
return (f) => false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function numberFn(obj) {
|
||
|
|
if (!obj.base) {
|
||
|
|
obj.base = 1
|
||
|
|
}
|
||
|
|
if (obj.base && obj.stops) {
|
||
|
|
return (z) => {
|
||
|
|
return protomaps.exp(obj.base, obj.stops)(z - 1)
|
||
|
|
}
|
||
|
|
} else if (obj[0] == "interpolate" && obj[1][0] == "exponential" && obj[2] == "zoom") {
|
||
|
|
let slice = obj.slice(3)
|
||
|
|
let stops = []
|
||
|
|
for (let i = 0; i < slice.length; i += 2) {
|
||
|
|
stops.push([slice[i], slice[i + 1]])
|
||
|
|
}
|
||
|
|
return (z) => {
|
||
|
|
return protomaps.exp(obj[1][1], stops)(z - 1)
|
||
|
|
}
|
||
|
|
} else if (obj[0] == "step" && obj[1][0] == "get") {
|
||
|
|
let slice = obj.slice(2)
|
||
|
|
let prop = obj[1][1]
|
||
|
|
return (z, f) => {
|
||
|
|
let val = f?.props[prop]
|
||
|
|
if (typeof val === "number") {
|
||
|
|
if (val < slice[1]) return slice[0]
|
||
|
|
for (let i = 1; i < slice.length; i += 2) {
|
||
|
|
if (val <= slice[i]) return slice[i + 1]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return slice[slice.length - 1]
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
console.log("Unimplemented numeric fn: ", obj)
|
||
|
|
return (z) => 1
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function numberOrFn(obj, defaultValue = 0) {
|
||
|
|
if (!obj) return defaultValue
|
||
|
|
if (typeof obj == "number") {
|
||
|
|
return obj
|
||
|
|
}
|
||
|
|
// If feature f is defined, use numberFn, otherwise use defaultValue
|
||
|
|
return (z, f) => (f ? numberFn(obj)(z, f) : defaultValue)
|
||
|
|
}
|
||
|
|
|
||
|
|
function widthFn(width_obj, gap_obj) {
|
||
|
|
let w = numberOrFn(width_obj, 1)
|
||
|
|
let g = numberOrFn(gap_obj)
|
||
|
|
return (z, f) => {
|
||
|
|
let tmp = typeof w == "number" ? w : w(z, f)
|
||
|
|
if (g) {
|
||
|
|
return tmp + (typeof g == "number" ? g : g(z, f))
|
||
|
|
}
|
||
|
|
return tmp
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function getFont(obj) {
|
||
|
|
let fontfaces = []
|
||
|
|
if (obj["text-font"]) {
|
||
|
|
for (let wanted_face of obj["text-font"]) {
|
||
|
|
fontfaces.push({ face: wanted_face })
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (fontfaces.length === 0) fontfaces.push({ face: "sans-serif" })
|
||
|
|
|
||
|
|
const text_size = obj["text-size"]
|
||
|
|
|
||
|
|
if (text_size) {
|
||
|
|
if (typeof text_size == "number") {
|
||
|
|
return (z) => `${text_size}px ${fontfaces.map((f) => f.face).join(", ")}`
|
||
|
|
} else if (text_size.stops) {
|
||
|
|
let base = 1.4
|
||
|
|
if (text_size.base) base = text_size.base
|
||
|
|
else text_size.base = base
|
||
|
|
let t = numberFn(text_size)
|
||
|
|
return (z, f) => {
|
||
|
|
return `${t(z, f)}px ${fontfaces.map((f) => f.face).join(", ")}`
|
||
|
|
}
|
||
|
|
} else if (text_size[0] == "step") {
|
||
|
|
let t = numberFn(text_size)
|
||
|
|
return (z, f) => {
|
||
|
|
return `${t(z, f)}px ${fontfaces.map((f) => f.face).join(", ")}`
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (z) => "12px Arial Bold"
|
||
|
|
}
|
||
|
|
|
||
|
|
//样式解析规则方法【重要】
|
||
|
|
function json_style(obj, sheet) {
|
||
|
|
let paint_rules = []
|
||
|
|
let label_rules = []
|
||
|
|
let refs = new Map()
|
||
|
|
|
||
|
|
for (let layer of obj.layers) {
|
||
|
|
refs.set(layer.id, layer)
|
||
|
|
|
||
|
|
if (layer.layout && layer.layout.visibility == "none") {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
if (layer.ref) {
|
||
|
|
let referenced = refs.get(layer.ref)
|
||
|
|
layer.type = referenced.type
|
||
|
|
layer.filter = referenced.filter
|
||
|
|
layer.source = referenced["source"]
|
||
|
|
layer["source-layer"] = referenced["source-layer"]
|
||
|
|
}
|
||
|
|
|
||
|
|
let sourceLayer = layer["source-layer"]
|
||
|
|
|
||
|
|
let filter = undefined
|
||
|
|
if (layer.filter) {
|
||
|
|
filter = filterFn(layer.filter)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ignore background-color?
|
||
|
|
if (layer.type == "fill") {
|
||
|
|
const fillPattern = layer.paint["fill-pattern"]
|
||
|
|
const fill = layer.paint["fill-color"]
|
||
|
|
const opacity = layer.paint["fill-opacity"]
|
||
|
|
let pattern
|
||
|
|
if (fillPattern) {
|
||
|
|
const patternInfor = sheet.get(fillPattern)
|
||
|
|
const canvas = document.createElement("canvas")
|
||
|
|
canvas.width = patternInfor.w
|
||
|
|
canvas.height = patternInfor.h
|
||
|
|
const ctx = canvas.getContext("2d")
|
||
|
|
ctx.drawImage(sheet.canvas, patternInfor.x, patternInfor.y, patternInfor.w, patternInfor.h)
|
||
|
|
pattern = canvas
|
||
|
|
}
|
||
|
|
// 填充面
|
||
|
|
paint_rules.push({
|
||
|
|
dataLayer: layer["source-layer"],
|
||
|
|
filter: filter,
|
||
|
|
symbolizer: new protomaps.PolygonSymbolizer({
|
||
|
|
pattern,
|
||
|
|
fill,
|
||
|
|
opacity
|
||
|
|
})
|
||
|
|
})
|
||
|
|
} else if (layer.type == "fill-extrusion") {
|
||
|
|
// 用不同的填充来绘制填充挤出
|
||
|
|
// simulate fill-extrusion with plain fill
|
||
|
|
paint_rules.push({
|
||
|
|
dataLayer: layer["source-layer"],
|
||
|
|
filter: filter,
|
||
|
|
symbolizer: new protomaps.PolygonSymbolizer({
|
||
|
|
fill: layer.paint["fill-extrusion-color"],
|
||
|
|
opacity: layer.paint["fill-extrusion-opacity"]
|
||
|
|
})
|
||
|
|
})
|
||
|
|
} else if (layer.type == "line") {
|
||
|
|
const lineColorInfor = layer.paint["line-color"]
|
||
|
|
let lineColor
|
||
|
|
if (lineColorInfor.stops) {
|
||
|
|
lineColor = function (z, f) {
|
||
|
|
const stops = lineColorInfor.stops
|
||
|
|
const length = stops.length
|
||
|
|
for (let i = length - 1; i >= 0; i--) {
|
||
|
|
if (z < stops[i][0]) {
|
||
|
|
return stops[i][1]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return stops[length - 1][1]
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
lineColor = lineColorInfor
|
||
|
|
}
|
||
|
|
// 线
|
||
|
|
// simulate gap-width
|
||
|
|
if (layer.paint["line-dasharray"]) {
|
||
|
|
paint_rules.push({
|
||
|
|
dataLayer: layer["source-layer"],
|
||
|
|
filter: filter,
|
||
|
|
symbolizer: new protomaps.LineSymbolizer({
|
||
|
|
width: widthFn(layer.paint["line-width"], layer.paint["line-gap-width"]),
|
||
|
|
dash: layer.paint["line-dasharray"],
|
||
|
|
dashColor: lineColor
|
||
|
|
})
|
||
|
|
})
|
||
|
|
} else {
|
||
|
|
paint_rules.push({
|
||
|
|
dataLayer: layer["source-layer"],
|
||
|
|
filter: filter,
|
||
|
|
symbolizer: new protomaps.LineSymbolizer({
|
||
|
|
color: lineColor,
|
||
|
|
width: widthFn(layer.paint["line-width"], layer.paint["line-gap-width"])
|
||
|
|
})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
} else if (layer.type == "symbol") {
|
||
|
|
let textField = layer.layout["text-field"]
|
||
|
|
if (textField) {
|
||
|
|
textField = textField.replace("{", "")
|
||
|
|
textField = textField.replace("}", "")
|
||
|
|
}
|
||
|
|
if (layer.layout["symbol-placement"] == "line") {
|
||
|
|
label_rules.push({
|
||
|
|
dataLayer: layer["source-layer"],
|
||
|
|
filter: filter,
|
||
|
|
symbolizer: new protomaps.LineLabelSymbolizer({
|
||
|
|
font: getFont(layer.layout),
|
||
|
|
fill: layer.paint["text-color"],
|
||
|
|
width: layer.paint["text-halo-width"],
|
||
|
|
stroke: layer.paint["text-halo-color"],
|
||
|
|
textTransform: layer.layout["text-transform"],
|
||
|
|
label_props: textField ? [textField] : undefined
|
||
|
|
})
|
||
|
|
})
|
||
|
|
} else if (layer.layout["icon-image"]) {
|
||
|
|
label_rules.push({
|
||
|
|
dataLayer: layer["source-layer"],
|
||
|
|
filter: filter,
|
||
|
|
symbolizer: new IconSymbolizer({
|
||
|
|
name: layer.layout["icon-image"],
|
||
|
|
sheet: sheet,
|
||
|
|
fill: layer.paint["text-color"],
|
||
|
|
stroke: layer.paint["text-halo-color"],
|
||
|
|
width: layer.paint["text-halo-width"],
|
||
|
|
label_props: textField ? [textField] : undefined
|
||
|
|
})
|
||
|
|
})
|
||
|
|
} else {
|
||
|
|
const textAnchor = layer.layout["text-anchor"]
|
||
|
|
let justify
|
||
|
|
switch (textAnchor) {
|
||
|
|
case "left":
|
||
|
|
justify = protomaps.Justify.Left
|
||
|
|
break
|
||
|
|
case "right":
|
||
|
|
justify = protomaps.Justify.Right
|
||
|
|
break
|
||
|
|
case "center":
|
||
|
|
justify = protomaps.Justify.Center
|
||
|
|
break
|
||
|
|
}
|
||
|
|
label_rules.push({
|
||
|
|
dataLayer: layer["source-layer"],
|
||
|
|
filter: filter,
|
||
|
|
symbolizer: new TextSymbolizer({
|
||
|
|
font: getFont(layer.layout),
|
||
|
|
fill: layer.paint["text-color"],
|
||
|
|
stroke: layer.paint["text-halo-color"],
|
||
|
|
width: layer.paint["text-halo-width"],
|
||
|
|
textTransform: layer.layout["text-transform"],
|
||
|
|
justify: justify,
|
||
|
|
label_props: textField ? [textField] : undefined
|
||
|
|
})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
} else if (layer.type == "circle") {
|
||
|
|
// 圆
|
||
|
|
paint_rules.push({
|
||
|
|
dataLayer: layer["source-layer"],
|
||
|
|
filter: filter,
|
||
|
|
symbolizer: new protomaps.CircleSymbolizer({
|
||
|
|
radius: layer.paint["circle-radius"],
|
||
|
|
fill: layer.paint["circle-color"],
|
||
|
|
stroke: layer.paint["circle-stroke-color"],
|
||
|
|
width: layer.paint["circle-stroke-width"]
|
||
|
|
})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
label_rules.reverse()
|
||
|
|
return { paint_rules: paint_rules, label_rules: label_rules, tasks: [] }
|
||
|
|
}
|
||
|
|
|
||
|
|
class ArcGISPbfImageryProvider {
|
||
|
|
/**
|
||
|
|
* ArcGIS矢量切片图层加载器
|
||
|
|
* @constructor
|
||
|
|
* @alias ArcGISPbfImageryProvider
|
||
|
|
*
|
||
|
|
* @param {Object} options 具有以下参数的对象
|
||
|
|
* @param {string} options.url ArcGIS矢量切片图层服务地址
|
||
|
|
* @param {string} options.styleUrl ArcGIS矢量切片图层服务样式地址
|
||
|
|
* @param {number} [options.minimumLevel=0] 最小显示层级
|
||
|
|
* @param {number} [options.maximumLevel=26] 最大显示层级
|
||
|
|
* @param {number} [options.maximumNativeLevel=26] 矢量切片的最大切片层级
|
||
|
|
* @param {Cesium.Rectangle} [options.rectangle] 显示范围
|
||
|
|
*/
|
||
|
|
constructor(options) {
|
||
|
|
options = Cesium.defaultValue(options, Cesium.defaultValue.EMPTY_OBJECT)
|
||
|
|
|
||
|
|
this._url = options.url
|
||
|
|
|
||
|
|
this._tileWidth = tileSize
|
||
|
|
this._tileHeight = tileSize
|
||
|
|
this._minimumLevel = Cesium.defaultValue(options.minimumLevel, 0)
|
||
|
|
this._maximumLevel = Cesium.defaultValue(options.maximumLevel, 26)
|
||
|
|
this._maximumNativeLevel = Cesium.defaultValue(options.maximumNativeLevel, this._maximumLevel)
|
||
|
|
|
||
|
|
this._tilingScheme = options.tilingScheme || new Cesium.WebMercatorTilingScheme()
|
||
|
|
|
||
|
|
this._rectangle = Cesium.defaultValue(options.rectangle, this._tilingScheme.rectangle)
|
||
|
|
|
||
|
|
this._ready = false
|
||
|
|
this._readyPromise = Cesium.defer()
|
||
|
|
|
||
|
|
const labelersCanvasContext = document.createElement("canvas").getContext("2d")
|
||
|
|
|
||
|
|
this._paintRules = []
|
||
|
|
this._labelRules = []
|
||
|
|
|
||
|
|
// 加载style文件
|
||
|
|
const styleResource = new Cesium.Resource(options.styleUrl)
|
||
|
|
styleResource.fetchJson()?.then((styleJson) => {
|
||
|
|
if (styleJson?.sources?.esri?.url) {
|
||
|
|
this._url = styleJson.sources.esri.url
|
||
|
|
}
|
||
|
|
|
||
|
|
// 如果存在范围,则设置范围
|
||
|
|
const bounds = styleJson.sources?.esri?.bounds
|
||
|
|
if (bounds && !Cesium.defined(options.rectangle)) {
|
||
|
|
// 如果服务中存在 rectangle 但是没传入 rectangle 则使用服务中定义的
|
||
|
|
this._rectangle = Cesium.Rectangle.fromDegrees(bounds[0], bounds[1], bounds[2], bounds[3])
|
||
|
|
}
|
||
|
|
|
||
|
|
const spritePNGResource = styleResource.getDerivedResource({
|
||
|
|
url: `${styleJson.sprite}.png`
|
||
|
|
})
|
||
|
|
|
||
|
|
const spriteJsonResource = styleResource.getDerivedResource({
|
||
|
|
url: `${styleJson.sprite}.json`
|
||
|
|
})
|
||
|
|
|
||
|
|
const image = new Image()
|
||
|
|
image.crossOrigin = "Anonymous"
|
||
|
|
image.onload = () => {
|
||
|
|
// 加载精灵图
|
||
|
|
const spriteCanvas = document.createElement("canvas")
|
||
|
|
spriteCanvas.width = image.width
|
||
|
|
spriteCanvas.height = image.height
|
||
|
|
const spriteCtx = spriteCanvas.getContext("2d")
|
||
|
|
spriteCtx.drawImage(image, 0, 0, image.width, image.height)
|
||
|
|
|
||
|
|
spriteJsonResource.fetchJson()?.then((spriteIndex) => {
|
||
|
|
const sheet = new MarsSheet(spriteIndex, spriteCanvas)
|
||
|
|
this._ready = true
|
||
|
|
const rules = json_style(styleJson, sheet)
|
||
|
|
this._labelRules = rules.label_rules
|
||
|
|
this._paintRules = rules.paint_rules
|
||
|
|
this._labelers = new protomaps.Labelers(labelersCanvasContext, this._labelRules, 32, () => undefined)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
image.src = spritePNGResource.url
|
||
|
|
})
|
||
|
|
|
||
|
|
this._source = new protomaps.ZxySource(this._url + ArcGISPbfImageryProviderRequestTrailer, false)
|
||
|
|
let cache = new protomaps.TileCache(this._source, 1024)
|
||
|
|
this._view = new protomaps.View(cache, this._maximumNativeLevel, 2)
|
||
|
|
|
||
|
|
this.pickFeaturesEvent = new Cesium.Event()
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 图层服务的url
|
||
|
|
* @type {String}
|
||
|
|
* @readonly
|
||
|
|
*/
|
||
|
|
get url() {
|
||
|
|
return this._url
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 单块瓦片的宽度
|
||
|
|
* @type {Number}
|
||
|
|
* @readonly
|
||
|
|
*/
|
||
|
|
get tileWidth() {
|
||
|
|
return this._tileWidth
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 单块瓦片的高度
|
||
|
|
* @type {Number}
|
||
|
|
* @readonly
|
||
|
|
*/
|
||
|
|
get tileHeight() {
|
||
|
|
return this._tileHeight
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 最大显示层级
|
||
|
|
* @type {Number}
|
||
|
|
* @readonly
|
||
|
|
*/
|
||
|
|
get maximumLevel() {
|
||
|
|
return this._maximumLevel
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 最小显示层级
|
||
|
|
* @type {Number}
|
||
|
|
* @readonly
|
||
|
|
*/
|
||
|
|
get minimumLevel() {
|
||
|
|
return this._minimumLevel
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 矢量切片的最大加载层级
|
||
|
|
* @type {Number}
|
||
|
|
* @readonly
|
||
|
|
*/
|
||
|
|
get maximumNativeLevel() {
|
||
|
|
return this._maximumNativeLevel
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 当前使用的切片方案
|
||
|
|
* @type {Cesium.GeographicTilingScheme}
|
||
|
|
* @readonly
|
||
|
|
*/
|
||
|
|
get tilingScheme() {
|
||
|
|
return this._tilingScheme
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 显示范围
|
||
|
|
* @type {Cesium.Rectangle}
|
||
|
|
* @readonly
|
||
|
|
*/
|
||
|
|
get rectangle() {
|
||
|
|
return this._rectangle
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 是否准备完成
|
||
|
|
* @type {Boolean}
|
||
|
|
* @readonly
|
||
|
|
*/
|
||
|
|
get ready() {
|
||
|
|
return this._ready
|
||
|
|
}
|
||
|
|
|
||
|
|
get readyPromise() {
|
||
|
|
return this._readyPromise.promise
|
||
|
|
}
|
||
|
|
|
||
|
|
get hasAlphaChannel() {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
get credit() {
|
||
|
|
return this._credit
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Requests the image for a given tile. This function should
|
||
|
|
* not be called before {@link ArcGISPbfImageryProvider#ready} returns true.
|
||
|
|
*
|
||
|
|
* @param {Number} x The tile X coordinate.
|
||
|
|
* @param {Number} y The tile Y coordinate.
|
||
|
|
* @param {Number} level The tile level.
|
||
|
|
* @returns {Promise.<ImageryTypes>|undefined} A promise for the image that will resolve when the image is available, or
|
||
|
|
* undefined if there are too many active requests to the server, and the request should be retried later.
|
||
|
|
*
|
||
|
|
* @exception {DeveloperError} <code>requestImage</code> must not be called before the imagery provider is ready.
|
||
|
|
*/
|
||
|
|
requestImage(x, y, level) {
|
||
|
|
const canvas = document.createElement("canvas")
|
||
|
|
canvas.width = this.tileWidth
|
||
|
|
canvas.height = this.tileHeight
|
||
|
|
try {
|
||
|
|
return this._renderTile({ x, y, z: level }, canvas)
|
||
|
|
} catch (e) {
|
||
|
|
return Promise.resolve(canvas)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_renderTile(coords, canvas) {
|
||
|
|
return this._view.getDisplayTile(coords).then((tile) => {
|
||
|
|
const tileMap = new Map().set("", [tile])
|
||
|
|
|
||
|
|
this._labelers.add(coords.z, tileMap)
|
||
|
|
|
||
|
|
let labelData = this._labelers.getIndex(tile.z)
|
||
|
|
|
||
|
|
const bbox = {
|
||
|
|
minX: 256 * coords.x - BUF,
|
||
|
|
minY: 256 * coords.y - BUF,
|
||
|
|
maxX: 256 * (coords.x + 1) + BUF,
|
||
|
|
maxY: 256 * (coords.y + 1) + BUF
|
||
|
|
}
|
||
|
|
const origin = new MarsPoint(256 * coords.x, 256 * coords.y)
|
||
|
|
|
||
|
|
const ctx = canvas.getContext("2d")
|
||
|
|
ctx.setTransform(this._tileWidth / 256, 0, 0, this._tileWidth / 256, 0, 0)
|
||
|
|
ctx.clearRect(0, 0, 256, 256)
|
||
|
|
|
||
|
|
if (labelData) {
|
||
|
|
protomaps.painter(ctx, coords.z, tileMap, labelData, this._paintRules, bbox, origin, false, "")
|
||
|
|
}
|
||
|
|
return canvas
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
pickFeatures(x, y, zoom, longitude, latitude) {
|
||
|
|
//目前不支持鼠标单击拾取,如果需要,需要此处加解析代码
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class ArcGISPbfLayer extends mars3d.layer.BaseTileLayer {
|
||
|
|
//构建ImageryProvider
|
||
|
|
async _createImageryProvider(options) {
|
||
|
|
return createImageryProvider(options)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function createImageryProvider(options) {
|
||
|
|
return new ArcGISPbfImageryProvider(options)
|
||
|
|
}
|
||
|
|
ArcGISPbfLayer.createImageryProvider = createImageryProvider
|
||
|
|
|
||
|
|
//注册下
|
||
|
|
const layerType = "arcgis-pbf" //图层类型
|
||
|
|
mars3d.LayerUtil.register(layerType, ArcGISPbfLayer)
|
||
|
|
mars3d.LayerUtil.registerImageryProvider(layerType, createImageryProvider)
|
||
|
|
|
||
|
|
//对外接口
|
||
|
|
mars3d.provider.ArcGISPbfImageryProvider = ArcGISPbfImageryProvider
|
||
|
|
mars3d.layer.ArcGISPbfLayer = ArcGISPbfLayer
|
||
|
|
})(window)
|
||
|
|
|
||
|
|
// { ArcGISPbfLayer }
|