This message was deleted.
# help
s
This message was deleted.
f
you mean fitExtent?
fitExtent
is the proper way to fit a projection to an extent, so the solution is using that function. Maybe it doesn't work because you're fitting to the (clipped) sphere instead of the rectangle. Without seeing your code it's impossible to tell
s
yeah, before fitExtent, the clipped sphere is not at the top-left since I add a rotation here. I think this is the most subtle part using
fitExtent
, don't know how to clip a rectangular part on top of the clipped circle. I found the composition of the projection operations (translate, rotate, fitExtent) kind of complex to keep track.
I don't use Observable notebook, does this code snippet help?
Copy code
// here size is a responsive size of the window (I create a full-screen canvas)
  const canvas = el.value!
  const length = (size.height < size.width) ? size.height : size.width
  const { ctx } = initCanvas(canvas, length, length * projectionRatio)
  const { width, height } = canvas
  const l = width
  const r = size.width / size.height
  // w, h as the view field width and height adapt to actual window shape
  const w = l / Math.sqrt(1 + 1 / (r * r))
  const h = l / Math.sqrt(1 + (r * r))
  // auxiliary figures
  ctx.clearRect(0, 0, width, height)
  ctx.fillStyle = '#000'
  ctx.fillRect(0, 0, width, height)
  ctx.strokeStyle = 'blue' // Border color
  ctx.lineWidth = 2 // Border width
  ctx.strokeRect((l - w) / 2, (l - h) / 2, w, h)

  const scale = width / 1024
  const adapt = Math.sqrt(scale)

  projection
    .rotate(rotation)
    .clipAngle(30)
    .fitExtent([[0, 0], [width, height]], stars)
     
  const starPath = geoPath(projection, ctx).pointRadius(d => Math.max(adapt * starSize(d.properties.mag), 0.1))
  // draw stars
  stars.features.forEach((star) => {
    ctx.beginPath()
    starPath(star)
    ctx.fillStyle = starColor(star.properties.bv)
    ctx.fill()
  })
Anyway my ultimate goal is get a rectangular subset of the clipped projection of [-30,30]*[-30,30] area, such that the subset is 'well-fitted' in the canvas. (equal scale in both x and y direction)
oh, I think if we can find the inverse of fitExtent, then we can find the inverse of the blue box in the original projection, extract it and fitExtent again? The only problem is I don't know how to extract out the blue box area...
I've tried translation after fitExtent but it seems the translation is applied on the original coordinates, not relative to the fitted one.
Now I can get projections only inside the blue box with postClip(geoClipRectangle) but the second fitExtent doesn't work, I guess it's because the fitExtent make use of the original star coordinates, I think it's not reasonable.
Copy code
const projection = geoEquirectangular()
  const projectionRatio = 1 // since we use 30° clip circle
  const zenith = getZenith()
  const rotation = getAngles(zenith)

  const canvas = el.value!
  const length = (size.height < size.width) ? size.height : size.width
  const { ctx } = initCanvas(canvas, length, length * projectionRatio)
  const { width, height } = canvas
  const l = width
  const r = size.width / size.height
  // w, h as the view field width and height adapt to actual window shape
  const w = l / Math.sqrt(1 + 1 / (r * r))
  const h = l / Math.sqrt(1 + (r * r))
  // auxiliary figures
  ctx.clearRect(0, 0, width, height)
  ctx.fillStyle = '#000'
  ctx.fillRect(0, 0, width, height)
  ctx.strokeStyle = 'blue' // Border color
  ctx.lineWidth = 2 // Border width
  ctx.strokeRect((l - w) / 2, (l - h) / 2, w, h)

  const scale = width / 1024
  const adapt = Math.sqrt(scale)
  console.log(l, w)
  projection
    .rotate(rotation)
    .clipAngle(30)
    .fitExtent([[0, 0], [width, height]], stars)
    .postclip(geoClipRectangle((l - w) / 2, (l - h) / 2, (l + w) / 2, (l + h) / 2))
    // .fitWidth(size.width, stars)
    // .translate([[(l - w) / 2, (l - h) / 2], [(l + w) / 2, (l + h) / 2]])
    // .fitExtent... the second one doesn't work well
  
  const starPath = geoPath(projection, ctx).pointRadius(d => Math.max(adapt * starSize(d.properties.mag), 0.1))
  // draw stars
  stars.features.forEach((star) => {
    ctx.beginPath()
    starPath(star)
    ctx.fillStyle = starColor(star.properties.bv)
    ctx.fill()
  })
f
The confusion is that clipAngle sets a postclip, and then postclip(clipRectangle) sets another one instead. I think what you want is just clipExtent, and if you put it before
fitExtent(dimensions, {type:"Sphere"})
it should work. But I can't help you much if you don't share the code
s
I could share my whole project, but it's a bit lengthy
let me just try clipExtent before
alright, the problem is if I use clipExtent, I have no idea what to pass into extent. The blue box is relative to the projection after performing clipAngle and fitExtent
I've no idea what it should be before these transformation
Hi, this is my repo https://github.com/alephpi/sky-wander, I failed to reproduce it on Ob notebook seeming the notebook has a special handling of canvas. To use the repo, just run
pnpm i
and then
pnpm dev
, the code is in
src/component/celestial.vue
if you prefer, this is the notebook I failed https://observablehq.com/@alephpi-workspace/starmap
f
The correct order of operations should be something like the following:
Copy code
const projection = d3.geoEquirectangular()
    .fitExtent([[0, 0], [width, height]], {type: "MultiPoint", coordinates: [[-30, 0], [0, 30], [0, -30], [30, 0]]})
    .rotate([30, 0]);
👀 1
s
alright, i'll try later
Hi @Fil, I just want to know what does this MultiPoint object represents? Does it draw a diamond shape? If I want to draw a square shape, should I use [-30, -30], [-30, 30], [30,-30], [30,30]? Also, does the order of the vertices make a difference? I didn't find the corresponding document, could you point me out? Edit: it seems the order doesn't matter, and actually use
[-30, -30],[-30, 30], [30,-30], [30,30]
instead has the same effect, even use
[-30,30], [-30,30]
produces the same result.
also I notice that the fitExtent and rotate are not commutative, I wonder the actual meaning of it. In fact what I want is rotate a point to the center and get the square view of the projection. As if I observe a point on earth perpendicularly (my eyes, the point and the center of Earth is on the same line), and my view field is a square, which span 30 degrees on each four directions. Will it be more like first rotate then fitExtent?
(I made an analogy to geology, but in astronomy, it's when we stare at the zenith on the celestial sphere and have a span of 30 degrees view field
Now I compared my projection to the stellarium, it seems my projection has a flipped orientation. Maybe it's because in geology the globe sphere is observed from outside while the celestial sphere should be observed fron inside. How can I flip it back?
f
Please take a moment to read the documentation? https://d3js.org/d3-geo/projection#projection_reflectY
s
OK, what about the fitExtent with a multipoint object? I didn't find a detailed explanation in doc
could you confirm my previous questions? I do read the doc, but the composition of these projection operation still confuse me... the rotate and the fitExtent aren't commutative, and I just wonder the order in your suggestion mean exactly. Since I could not reproduce a zenith view as in stellarium.
f
fitExtent computes the projection of the given geometry and adjusts the scale and translate to fit a given extent. The result does vary with the projection's rotation. When you're trying to adjust a “camera viewport”, it's often simpler to pass a non-rotated feature to a non-rotated projection, then do the rotation. If you instead want to adjust to a spatial feature, then you should rotate before calling fitExtent.
👍 1
s
@Fil Thank you so much!!! It turns out your code works well and I simply need to add reflectX(true), I didn't find a better way to debug till I use a geo map, replay the code as a reference I gain some intuition from the operations. Now when I replace the star map everything works like magic.
👏 1