2023-01-10 21:45:03 +01:00
|
|
|
<script lang="ts">
|
|
|
|
import UpObject from "../components/display/UpObject.svelte";
|
2023-05-22 20:57:06 +02:00
|
|
|
import api from "../lib/api";
|
2023-01-10 21:45:03 +01:00
|
|
|
import Selector from "../components/utils/Selector.svelte";
|
|
|
|
import { onMount } from "svelte";
|
2023-01-11 00:21:51 +01:00
|
|
|
import type { ZoomTransform } from "d3";
|
|
|
|
import Spinner from "../components/utils/Spinner.svelte";
|
2023-01-11 00:40:48 +01:00
|
|
|
import UpObjectCard from "../components/display/UpObjectCard.svelte";
|
|
|
|
import BlobPreview from "../components/display/BlobPreview.svelte";
|
|
|
|
import { i18n } from "../i18n";
|
2023-01-24 19:29:35 +01:00
|
|
|
import type { IValue } from "upend/types";
|
2023-01-24 23:51:26 +01:00
|
|
|
import { useNavigate } from "svelte-navigator";
|
|
|
|
const navigate = useNavigate();
|
2023-01-10 21:45:03 +01:00
|
|
|
|
2023-01-24 23:51:26 +01:00
|
|
|
const urlParams = new URLSearchParams(
|
|
|
|
window.location.href.substring(window.location.href.indexOf("?"))
|
2023-01-10 21:45:03 +01:00
|
|
|
);
|
|
|
|
|
2023-01-24 19:15:34 +01:00
|
|
|
export let x: string = urlParams.get("x");
|
|
|
|
export let y: string = urlParams.get("y");
|
2023-01-24 23:51:26 +01:00
|
|
|
$: if (x && y) navigate(`/surface?x=${x}&y=${y}`, { replace: true });
|
|
|
|
|
2023-01-11 00:40:48 +01:00
|
|
|
let viewMode = "link";
|
2023-01-10 21:45:03 +01:00
|
|
|
|
2023-01-11 19:03:31 +01:00
|
|
|
let currentX = NaN;
|
|
|
|
let currentY = NaN;
|
|
|
|
|
2023-01-11 00:21:51 +01:00
|
|
|
let loaded = false;
|
|
|
|
let viewHeight = 0;
|
|
|
|
let viewWidth = 0;
|
|
|
|
|
2023-01-10 21:45:03 +01:00
|
|
|
interface IPoint {
|
|
|
|
address: string;
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
}
|
|
|
|
let points: IPoint[] = [];
|
2023-01-24 19:29:35 +01:00
|
|
|
async function loadPoints() {
|
|
|
|
points = [];
|
2023-05-22 20:57:06 +02:00
|
|
|
const result = await api.query(`(matches ? (in "${x}" "${y}") ?)`);
|
2023-01-24 19:29:35 +01:00
|
|
|
|
|
|
|
points = Object.entries(result.objects)
|
|
|
|
.map(([address, obj]) => {
|
|
|
|
let objX = parseInt(String(obj.get(x)));
|
|
|
|
let objY = parseInt(String(obj.get(y)));
|
|
|
|
|
|
|
|
if (objX && objY) {
|
|
|
|
return {
|
|
|
|
address,
|
|
|
|
x: objX,
|
|
|
|
y: objY,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.filter(Boolean);
|
|
|
|
}
|
2023-01-10 21:45:03 +01:00
|
|
|
|
|
|
|
$: {
|
|
|
|
if (x && y) {
|
2023-01-24 19:29:35 +01:00
|
|
|
loadPoints();
|
2023-01-10 21:45:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-24 19:29:35 +01:00
|
|
|
let selectorCoords: [number, number] | null = null;
|
|
|
|
|
2023-01-11 19:03:20 +01:00
|
|
|
onMount(async () => {
|
|
|
|
const d3 = await import("d3");
|
|
|
|
|
2023-01-10 21:45:03 +01:00
|
|
|
const view = d3.select(".view");
|
|
|
|
|
|
|
|
const svg = view.append("svg");
|
|
|
|
|
2023-01-11 00:21:51 +01:00
|
|
|
const xScale = d3
|
|
|
|
.scaleLinear()
|
|
|
|
.domain([0, viewWidth])
|
|
|
|
.range([0, viewWidth]);
|
2023-01-10 21:45:03 +01:00
|
|
|
|
|
|
|
const xAxis = d3
|
|
|
|
.axisBottom(xScale)
|
|
|
|
.ticks(15)
|
2023-01-11 00:21:51 +01:00
|
|
|
.tickSize(viewHeight)
|
|
|
|
.tickPadding(5 - viewHeight);
|
2023-01-10 21:45:03 +01:00
|
|
|
|
2023-01-11 00:21:51 +01:00
|
|
|
const yScale = d3
|
|
|
|
.scaleLinear()
|
|
|
|
.domain([0, viewHeight])
|
|
|
|
.range([viewHeight, 0]);
|
2023-01-10 21:45:03 +01:00
|
|
|
|
|
|
|
const yAxis = d3
|
|
|
|
.axisRight(yScale)
|
2023-01-11 00:21:51 +01:00
|
|
|
.ticks(viewHeight / (viewWidth / 15))
|
|
|
|
.tickSize(viewWidth)
|
|
|
|
.tickPadding(5 - viewWidth);
|
2023-01-10 21:45:03 +01:00
|
|
|
|
|
|
|
const gX = svg.append("g").attr("class", "axis axis--x").call(xAxis);
|
|
|
|
const gY = svg.append("g").attr("class", "axis axis--y").call(yAxis);
|
|
|
|
|
|
|
|
const zoom = d3
|
|
|
|
.zoom()
|
|
|
|
// .scaleExtent([1, 40])
|
|
|
|
// .translateExtent([
|
|
|
|
// [-100, -100],
|
|
|
|
// [width + 90, height + 100],
|
|
|
|
// ])
|
|
|
|
.on("zoom", zoomed);
|
|
|
|
|
|
|
|
function zoomed({ transform }: { transform: ZoomTransform }) {
|
2023-01-24 19:29:35 +01:00
|
|
|
const points = d3.select(".content");
|
2023-01-10 21:45:03 +01:00
|
|
|
points.style(
|
|
|
|
"transform",
|
|
|
|
`translate(${transform.x}px, ${transform.y}px) scale(${transform.k})`
|
|
|
|
);
|
|
|
|
const allPoints = d3.selectAll(".point");
|
|
|
|
allPoints.style("transform", `scale(${1 / transform.k})`);
|
|
|
|
|
|
|
|
gX.call(xAxis.scale(transform.rescaleX(xScale)));
|
|
|
|
gY.call(yAxis.scale(transform.rescaleY(yScale)));
|
|
|
|
}
|
|
|
|
|
|
|
|
function reset() {
|
|
|
|
svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity);
|
|
|
|
}
|
|
|
|
|
2023-01-11 19:03:31 +01:00
|
|
|
view.on("mousemove", (ev: MouseEvent) => {
|
|
|
|
// not using offsetXY because `translate` transforms on .inner mess it up
|
|
|
|
const viewBBox = (view.node() as HTMLElement).getBoundingClientRect();
|
|
|
|
const [x, y] = d3
|
2023-01-24 19:29:35 +01:00
|
|
|
.zoomTransform(d3.select(".content").node() as HTMLElement)
|
2023-01-11 19:03:31 +01:00
|
|
|
.invert([ev.clientX - viewBBox.left, ev.clientY - viewBBox.top]);
|
2023-01-24 19:29:35 +01:00
|
|
|
|
2023-01-11 19:03:31 +01:00
|
|
|
currentX = xScale.invert(x);
|
|
|
|
currentY = yScale.invert(y);
|
|
|
|
});
|
|
|
|
|
2023-01-24 19:29:35 +01:00
|
|
|
d3.select(".view")
|
|
|
|
.call(zoom)
|
|
|
|
.on("dblclick.zoom", (ev: MouseEvent) => {
|
|
|
|
selectorCoords = [currentX, currentY];
|
|
|
|
});
|
2023-01-11 00:21:51 +01:00
|
|
|
loaded = true;
|
2023-01-10 21:45:03 +01:00
|
|
|
});
|
2023-01-24 19:29:35 +01:00
|
|
|
|
|
|
|
async function onSelectorInput(ev: CustomEvent<IValue>) {
|
|
|
|
const [xValue, yValue] = selectorCoords;
|
|
|
|
selectorCoords = null;
|
|
|
|
await Promise.all(
|
|
|
|
[
|
|
|
|
[x, xValue],
|
|
|
|
[y, yValue],
|
|
|
|
].map(([axis, value]: [string, number]) =>
|
2023-05-22 20:57:06 +02:00
|
|
|
api.putEntityAttribute(ev.detail.c as string, axis, {
|
2023-01-24 19:29:35 +01:00
|
|
|
t: "Number",
|
|
|
|
c: value,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
);
|
|
|
|
await loadPoints();
|
|
|
|
}
|
2023-01-10 21:45:03 +01:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<div class="surface">
|
|
|
|
<div class="header">
|
|
|
|
<div class="axis-selector">
|
|
|
|
X: <Selector type="attribute" bind:attribute={x} />
|
|
|
|
</div>
|
|
|
|
<div class="axis-selector">
|
|
|
|
Y: <Selector type="attribute" bind:attribute={y} />
|
|
|
|
</div>
|
2023-01-11 00:40:48 +01:00
|
|
|
<div class="view-mode-selector">
|
|
|
|
{$i18n.t("View as")}
|
|
|
|
<select bind:value={viewMode}>
|
|
|
|
<option value="link">{$i18n.t("Link")}</option>
|
|
|
|
<option value="card">{$i18n.t("Card")}</option>
|
|
|
|
<!-- <option value="preview">{$i18n.t("Preview")}</option> -->
|
|
|
|
</select>
|
|
|
|
</div>
|
2023-01-11 19:03:31 +01:00
|
|
|
<div class="position">
|
|
|
|
{$i18n.t("Current position")}:
|
|
|
|
<div class="label">
|
|
|
|
<em>X:</em>
|
2023-01-24 19:29:35 +01:00
|
|
|
{x || "?"} = {(Math.round(currentX * 100) / 100).toLocaleString("en", {
|
2023-01-11 19:03:31 +01:00
|
|
|
useGrouping: false,
|
|
|
|
minimumFractionDigits: 2,
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
<div class="label">
|
|
|
|
<em>Y:</em>
|
2023-01-24 19:29:35 +01:00
|
|
|
{y || "?"} = {(Math.round(currentY * 100) / 100).toLocaleString("en", {
|
2023-01-11 19:03:31 +01:00
|
|
|
useGrouping: false,
|
|
|
|
minimumFractionDigits: 2,
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
</div>
|
2023-01-10 21:45:03 +01:00
|
|
|
</div>
|
2023-01-11 00:21:51 +01:00
|
|
|
<div
|
|
|
|
class="view"
|
|
|
|
class:loaded
|
|
|
|
bind:clientWidth={viewWidth}
|
|
|
|
bind:clientHeight={viewHeight}
|
|
|
|
>
|
|
|
|
{#if !loaded}
|
|
|
|
<div class="loading">
|
|
|
|
<Spinner centered="absolute" />
|
|
|
|
</div>
|
|
|
|
{/if}
|
2023-01-24 19:29:35 +01:00
|
|
|
<div class="content">
|
|
|
|
{#if selectorCoords !== null}
|
|
|
|
<div
|
|
|
|
class="point selector"
|
|
|
|
style="
|
|
|
|
left: {selectorCoords[0]}px;
|
|
|
|
top: {viewHeight - selectorCoords[1]}px"
|
|
|
|
>
|
|
|
|
<Selector
|
|
|
|
type="value"
|
|
|
|
valueTypes={["Address"]}
|
|
|
|
on:input={onSelectorInput}
|
|
|
|
on:focus={(ev) => {
|
|
|
|
if (!ev.detail) selectorCoords = null;
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
{/if}
|
2023-01-10 21:45:03 +01:00
|
|
|
{#each points as point}
|
2023-01-11 00:21:51 +01:00
|
|
|
<div
|
|
|
|
class="point"
|
|
|
|
style="left: {point.x}px; top: {viewHeight - point.y}px"
|
|
|
|
>
|
2023-01-10 23:57:53 +01:00
|
|
|
<div class="inner">
|
2023-01-11 00:40:48 +01:00
|
|
|
{#if viewMode == "link"}
|
2023-01-24 19:29:35 +01:00
|
|
|
<UpObject link address={point.address} />
|
2023-01-11 00:40:48 +01:00
|
|
|
{:else if viewMode == "card"}
|
|
|
|
<UpObjectCard address={point.address} />
|
|
|
|
{:else if viewMode == "preview"}
|
|
|
|
<BlobPreview address={point.address} />
|
|
|
|
{/if}
|
2023-01-10 23:57:53 +01:00
|
|
|
</div>
|
2023-01-10 21:45:03 +01:00
|
|
|
</div>
|
|
|
|
{/each}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<style lang="scss">
|
|
|
|
.surface {
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
height: 100%;
|
|
|
|
}
|
|
|
|
|
|
|
|
.header {
|
|
|
|
display: flex;
|
|
|
|
gap: 2em;
|
|
|
|
padding: 1em;
|
|
|
|
border-bottom: 1px solid var(--foreground);
|
2023-01-11 19:03:31 +01:00
|
|
|
align-items: center;
|
2023-01-10 21:45:03 +01:00
|
|
|
|
|
|
|
.axis-selector {
|
|
|
|
display: flex;
|
|
|
|
gap: 1em;
|
|
|
|
align-items: center;
|
|
|
|
}
|
2023-01-11 19:03:31 +01:00
|
|
|
|
|
|
|
.position {
|
|
|
|
display: flex;
|
|
|
|
gap: 1em;
|
|
|
|
em {
|
|
|
|
font-weight: bold;
|
|
|
|
font-style: normal;
|
|
|
|
}
|
|
|
|
}
|
2023-01-10 21:45:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
.view {
|
|
|
|
flex-grow: 1;
|
|
|
|
position: relative;
|
|
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
:global(svg) {
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
}
|
|
|
|
|
2023-01-10 23:57:53 +01:00
|
|
|
:global(.tick text) {
|
|
|
|
color: var(--foreground-lightest);
|
|
|
|
font-size: 1rem;
|
|
|
|
}
|
|
|
|
|
2023-01-24 19:29:35 +01:00
|
|
|
.content {
|
2023-01-10 23:57:53 +01:00
|
|
|
transform-origin: 0 0;
|
|
|
|
}
|
|
|
|
|
2023-01-10 21:45:03 +01:00
|
|
|
.point {
|
|
|
|
position: absolute;
|
2023-01-10 23:57:53 +01:00
|
|
|
transform-origin: 0 0;
|
|
|
|
|
|
|
|
.inner {
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
}
|
2023-01-10 21:45:03 +01:00
|
|
|
}
|
2023-01-11 00:21:51 +01:00
|
|
|
|
|
|
|
&:not(.loaded) {
|
|
|
|
pointer-events: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.loading {
|
|
|
|
position: absolute;
|
|
|
|
top: 0;
|
|
|
|
left: 0;
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
background: rgba(0, 0, 0, 0.7);
|
|
|
|
z-index: 99;
|
|
|
|
transform: scale(3);
|
2023-01-10 21:45:03 +01:00
|
|
|
}
|
|
|
|
</style>
|