BlueMap addon, shop points, map actions wired into Paper and Fabric; client opens shop windows
No files matched your search
@@ -0,0 +1,11 @@
|
||||
// SCOPENET's BlueMap addon: guild claims, spawn, warps, shops and a player side panel on the 3D map.
|
||||
// BlueMap's API is the same on Paper, Fabric and Forge, so this one module serves them all. It is only ever called
|
||||
// when BlueMap is installed, and BlueMap provides these classes at runtime (compileOnly).
|
||||
repositories {
|
||||
maven { url = 'https://repo.bluecolored.de/releases' }
|
||||
}
|
||||
dependencies {
|
||||
implementation project(':common')
|
||||
compileOnly 'de.bluecolored.bluemap:BlueMapAPI:2.7.4'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package net.scopenet.bluemap;
|
||||
|
||||
import com.flowpowered.math.vector.Vector2d;
|
||||
import com.flowpowered.math.vector.Vector3d;
|
||||
import com.google.gson.JsonObject;
|
||||
import de.bluecolored.bluemap.api.BlueMapAPI;
|
||||
import de.bluecolored.bluemap.api.BlueMapMap;
|
||||
import de.bluecolored.bluemap.api.markers.Marker;
|
||||
import de.bluecolored.bluemap.api.markers.MarkerSet;
|
||||
import de.bluecolored.bluemap.api.markers.POIMarker;
|
||||
import de.bluecolored.bluemap.api.markers.ShapeMarker;
|
||||
import de.bluecolored.bluemap.api.math.Color;
|
||||
import de.bluecolored.bluemap.api.math.Shape;
|
||||
import net.scopenet.core.map.ChunkRegions;
|
||||
import net.scopenet.core.map.MapData;
|
||||
import net.scopenet.core.map.MapModel;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Puts SCOPENET on the BlueMap 3D map:
|
||||
* <ul>
|
||||
* <li>guild land as coloured regions, with the guild's icon and name,</li>
|
||||
* <li>spawn, warps, guild homes (and homes, if the admin allows) as pins,</li>
|
||||
* <li>markets and shops as pins that list what is for sale,</li>
|
||||
* <li>a script that opens a side panel when you click a player.</li>
|
||||
* </ul>
|
||||
* Start it once from the plugin or mod. It waits for BlueMap to come up, so the load order doesn't matter, and does
|
||||
* nothing if BlueMap isn't installed.
|
||||
*/
|
||||
public final class MapAddon {
|
||||
/** Where the addon gets its data. Both calls must be safe from any thread. */
|
||||
public interface Feed {
|
||||
MapData snapshot();
|
||||
JsonObject playersJson();
|
||||
}
|
||||
|
||||
static final String WEB_DIR = "assets/scopenet";
|
||||
static final List<String> ASSETS = List.of("scopenet.js", "scopenet.css", "spawn.svg", "warp.svg", "home.svg", "guild-home.svg", "market.svg", "shop.svg", "guild.svg");
|
||||
static final String CLAIMS = "scopenet-claims", GUILDS = "scopenet-guilds", PLACES = "scopenet-places", SHOPS = "scopenet-shops";
|
||||
static final float CLAIM_Y = 66f;
|
||||
|
||||
private final Feed feed;
|
||||
private final Logger log;
|
||||
private ScheduledExecutorService worker;
|
||||
private volatile boolean enabled;
|
||||
|
||||
private MapAddon(Feed feed, Logger log) { this.feed = feed; this.log = log; }
|
||||
|
||||
/** Hook into BlueMap. Returns a handle to stop it. */
|
||||
public static MapAddon start(Feed feed, Logger log) {
|
||||
MapAddon addon = new MapAddon(feed, log);
|
||||
BlueMapAPI.onEnable(addon::enable);
|
||||
BlueMapAPI.onDisable(api -> addon.disable());
|
||||
return addon;
|
||||
}
|
||||
|
||||
public void stop() { disable(); }
|
||||
|
||||
// ---- lifecycle -------------------------------------------------------------------------
|
||||
|
||||
private synchronized void enable(BlueMapAPI api) {
|
||||
try {
|
||||
Path root = api.getWebApp().getWebRoot().resolve(WEB_DIR);
|
||||
Files.createDirectories(root);
|
||||
for (String name : ASSETS) copy(name, root.resolve(name));
|
||||
api.getWebApp().registerScript(WEB_DIR + "/scopenet.js");
|
||||
api.getWebApp().registerStyle(WEB_DIR + "/scopenet.css");
|
||||
} catch (IOException | RuntimeException e) {
|
||||
log.warning("SCOPENET could not add its files to BlueMap: " + e);
|
||||
return;
|
||||
}
|
||||
enabled = true;
|
||||
worker = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "scopenet-bluemap");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
worker.scheduleWithFixedDelay(() -> refresh(api), 2, 10, TimeUnit.SECONDS);
|
||||
log.info("SCOPENET is adding guild claims, pins and the player panel to BlueMap.");
|
||||
}
|
||||
|
||||
private synchronized void disable() {
|
||||
enabled = false;
|
||||
if (worker != null) worker.shutdownNow();
|
||||
worker = null;
|
||||
}
|
||||
|
||||
private static void copy(String name, Path target) throws IOException {
|
||||
try (InputStream in = MapAddon.class.getResourceAsStream("/" + WEB_DIR + "/" + name)) {
|
||||
if (in == null) throw new IOException("missing resource " + name);
|
||||
Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- refresh ---------------------------------------------------------------------------
|
||||
|
||||
private void refresh(BlueMapAPI api) {
|
||||
if (!enabled) return;
|
||||
try {
|
||||
MapData data = feed.snapshot();
|
||||
writePlayers(api);
|
||||
for (BlueMapMap map : api.getMaps()) update(map, data);
|
||||
} catch (RuntimeException | IOException e) {
|
||||
log.fine("SCOPENET map refresh failed: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
private void writePlayers(BlueMapAPI api) throws IOException {
|
||||
Path file = api.getWebApp().getWebRoot().resolve(WEB_DIR).resolve("players.json");
|
||||
Path tmp = file.resolveSibling("players.json.tmp");
|
||||
Files.writeString(tmp, feed.playersJson().toString(), StandardCharsets.UTF_8);
|
||||
Files.move(tmp, file, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
private void update(BlueMapMap map, MapData data) {
|
||||
String dimension = MapModel.dimensionOf(map.getId(), map.getName());
|
||||
set(map, CLAIMS, "Guild claims", claimMarkers(data, dimension));
|
||||
set(map, GUILDS, "Guild names", guildMarkers(data, dimension));
|
||||
set(map, PLACES, "Spawn, warps & homes", pinMarkers(data, dimension, false));
|
||||
set(map, SHOPS, "Markets & shops", pinMarkers(data, dimension, true));
|
||||
}
|
||||
|
||||
/** Replace a marker set's contents in place, so the map doesn't flicker. */
|
||||
private static void set(BlueMapMap map, String id, String label, Map<String, Marker> wanted) {
|
||||
MarkerSet set = map.getMarkerSets().computeIfAbsent(id, k -> MarkerSet.builder().label(label).toggleable(true).defaultHidden(false).build());
|
||||
set.getMarkers().keySet().removeIf(key -> !wanted.containsKey(key));
|
||||
wanted.forEach(set::put);
|
||||
}
|
||||
|
||||
// ---- markers ---------------------------------------------------------------------------
|
||||
|
||||
private static Shape shape(ChunkRegions.Ring ring) {
|
||||
List<Vector2d> points = new ArrayList<>();
|
||||
for (ChunkRegions.Point p : ring.points()) points.add(new Vector2d(p.x(), p.z()));
|
||||
return new Shape(points);
|
||||
}
|
||||
|
||||
private static Map<String, Marker> claimMarkers(MapData data, String dimension) {
|
||||
Map<String, Marker> out = new LinkedHashMap<>();
|
||||
for (MapData.ClaimRegion c : data.claims()) {
|
||||
if (!c.dimension().equals(dimension)) continue;
|
||||
Shape[] holes = c.holes().stream().map(MapAddon::shape).toArray(Shape[]::new);
|
||||
ShapeMarker marker = ShapeMarker.builder()
|
||||
.label(c.name())
|
||||
.shape(shape(c.outer()), CLAIM_Y)
|
||||
.holes(holes)
|
||||
.lineColor(new Color(c.color(), 0.95f))
|
||||
.fillColor(new Color(c.color(), 0.28f))
|
||||
.lineWidth(3)
|
||||
.depthTestEnabled(false)
|
||||
.detail(c.detail())
|
||||
.build();
|
||||
out.put("claim-" + c.id(), marker);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** One icon and name per area, in the middle of it, fading out when you are far away. */
|
||||
private static Map<String, Marker> guildMarkers(MapData data, String dimension) {
|
||||
Map<String, Marker> out = new LinkedHashMap<>();
|
||||
for (MapData.ClaimRegion c : data.claims()) {
|
||||
if (!c.dimension().equals(dimension)) continue;
|
||||
POIMarker.Builder builder = POIMarker.builder()
|
||||
.label(c.name() + " [" + c.tag() + "]")
|
||||
.position(c.labelX(), CLAIM_Y + 1, c.labelZ())
|
||||
.styleClasses("scopenet-guild-icon")
|
||||
.maxDistance(1600)
|
||||
.detail(c.detail());
|
||||
builder = c.icon().isEmpty() ? builder.icon(WEB_DIR + "/guild.svg", 16, 16) : builder.icon(c.icon(), 16, 16);
|
||||
out.put("guild-" + c.id(), builder.build());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static Map<String, Marker> pinMarkers(MapData data, String dimension, boolean shops) {
|
||||
Map<String, Marker> out = new LinkedHashMap<>();
|
||||
for (MapData.Pin p : data.pins()) {
|
||||
boolean isShop = p.kind().equals("market") || p.kind().equals("shop");
|
||||
if (!p.dimension().equals(dimension) || isShop != shops) continue;
|
||||
String icon = switch (p.kind()) {
|
||||
case "spawn" -> "spawn.svg";
|
||||
case "warp" -> "warp.svg";
|
||||
case "home" -> "home.svg";
|
||||
case "guild_home" -> "guild-home.svg";
|
||||
case "market" -> "market.svg";
|
||||
default -> "shop.svg";
|
||||
};
|
||||
out.put(p.id(), POIMarker.builder()
|
||||
.label(p.label())
|
||||
.position(new Vector3d(p.x(), p.y() + 1, p.z()))
|
||||
.icon(WEB_DIR + "/" + icon, 16, 16)
|
||||
.styleClasses("scopenet-pin", "scopenet-" + p.kind().replace('_', '-'))
|
||||
.maxDistance(2400)
|
||||
.detail(p.detail())
|
||||
.build());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#22d3ee" stroke="#ffffff" stroke-width="2"/><path d="M16 6l8 3v6c0 5-3.4 8.6-8 10-4.6-1.4-8-5-8-10V9z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 243 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 3l11 4v8c0 7-4.6 12-11 14C9.6 27 5 22 5 15V7z" fill="#8b6cff" stroke="#fff" stroke-width="2"/><path d="M16 9l2.2 4.6 5 .7-3.6 3.5.9 5L16 20.4 11.5 22.8l.9-5-3.6-3.5 5-.7z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 288 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#fbbf24" stroke="#ffffff" stroke-width="2"/><path d="M16 7l9 8h-3v8h-4v-5h-4v5h-4v-8H7z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 229 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#fb7185" stroke="#ffffff" stroke-width="2"/><path d="M7 12l2-5h14l2 5v2c0 1.7-1.3 3-3 3s-3-1.3-3-3c0 1.7-1.3 3-3 3s-3-1.3-3-3c0 1.7-1.3 3-3 3s-3-1.3-3-3zM9 19h14v6H9z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 308 B |
@@ -0,0 +1,64 @@
|
||||
/* SCOPENET on BlueMap: marker styling and the player side panel. Loaded by BlueMap through the addon. */
|
||||
|
||||
/* Guild icons sit on the map as small rounded badges, whatever size the uploaded image is. */
|
||||
.scopenet-guild-icon img,
|
||||
.bm-marker-poi.scopenet-guild-icon img {
|
||||
width: 34px !important;
|
||||
height: 34px !important;
|
||||
object-fit: cover;
|
||||
border-radius: 9px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.55);
|
||||
background: #1a1d25;
|
||||
}
|
||||
.scopenet-pin img,
|
||||
.bm-marker-poi.scopenet-pin img {
|
||||
width: 26px !important;
|
||||
height: 26px !important;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.55));
|
||||
}
|
||||
|
||||
/* Popup text inside marker details. */
|
||||
.scopenet-detail { font-size: 13px; line-height: 1.45; min-width: 160px; }
|
||||
.scopenet-detail span { opacity: 0.7; }
|
||||
.scopenet-detail code { background: rgba(255, 255, 255, 0.08); padding: 1px 5px; border-radius: 4px; }
|
||||
.scopenet-list { margin: 4px 0 0; padding-left: 16px; }
|
||||
|
||||
/* Player panel (shown in a plain browser; inside the SCOPENET launcher the launcher shows its own). */
|
||||
#scopenet-panel {
|
||||
position: fixed;
|
||||
top: 64px;
|
||||
right: 14px;
|
||||
width: 300px;
|
||||
max-height: calc(100vh - 90px);
|
||||
overflow-y: auto;
|
||||
z-index: 50;
|
||||
padding: 16px;
|
||||
border-radius: 14px;
|
||||
background: rgba(20, 22, 30, 0.96);
|
||||
color: #e6e8ee;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 16px 48px -12px rgba(0, 0, 0, 0.75);
|
||||
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 13px;
|
||||
animation: scopenet-in 0.18s ease;
|
||||
}
|
||||
@keyframes scopenet-in { from { opacity: 0; transform: translateX(18px); } }
|
||||
#scopenet-panel button.close {
|
||||
position: absolute; top: 8px; right: 8px; width: 26px; height: 26px; border: 0; border-radius: 8px;
|
||||
background: transparent; color: #9aa0ae; font-size: 18px; cursor: pointer;
|
||||
}
|
||||
#scopenet-panel button.close:hover { background: rgba(255, 255, 255, 0.08); color: #fff; }
|
||||
#scopenet-panel .head { display: flex; gap: 12px; align-items: center; padding-right: 26px; }
|
||||
#scopenet-panel .head img { width: 56px; height: 56px; border-radius: 16px; border: 3px solid #8b6cff; background: #1a1d25; image-rendering: pixelated; }
|
||||
#scopenet-panel h3 { margin: 0 0 2px; font-size: 17px; }
|
||||
#scopenet-panel .title { color: #fbbf24; font-size: 12px; }
|
||||
#scopenet-panel .chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0; }
|
||||
#scopenet-panel .chip { padding: 2px 9px; border-radius: 99px; background: rgba(255, 255, 255, 0.08); font-size: 11px; font-weight: 650; }
|
||||
#scopenet-panel .chip.lv { color: #8b6cff; }
|
||||
#scopenet-panel .chip.guild { color: #22d3ee; }
|
||||
#scopenet-panel .chip.rank { color: #fbbf24; }
|
||||
#scopenet-panel .facts { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
#scopenet-panel .facts div { padding: 8px 10px; border-radius: 10px; background: rgba(255, 255, 255, 0.05); color: #9aa0ae; font-size: 11px; }
|
||||
#scopenet-panel .facts strong { display: block; color: #e6e8ee; font-size: 14px; margin-top: 2px; }
|
||||
#scopenet-panel .note { margin: 14px 0 0; padding: 10px 12px; border-radius: 10px; background: rgba(139, 108, 255, 0.12); color: #c9bfff; font-size: 12px; line-height: 1.4; }
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* SCOPENET on BlueMap. Registered by the SCOPENET addon, so it runs inside BlueMap's own page: the same in a browser and
|
||||
* inside the SCOPENET launcher.
|
||||
*
|
||||
* - Click a player on the map to see who they are (level, guild, rank, title...), read from players.json that the
|
||||
* server keeps up to date next to this file.
|
||||
* - Inside the launcher (this page is in an iframe) the click is handed to the launcher, which shows the full side panel
|
||||
* with the actions that need a signed-in account: teleport request, message, guild invite.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
var BASE = 'assets/scopenet/';
|
||||
var PLAYER_PREFIX = 'bm-marker-bm-player-';
|
||||
var embedded = window.parent && window.parent !== window;
|
||||
var players = {};
|
||||
var selected = null;
|
||||
|
||||
function esc(text) {
|
||||
return String(text == null ? '' : text).replace(/[&<>"']/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
}
|
||||
|
||||
/** The uuid behind a player marker element, or null. BlueMap ids them "bm-marker-bm-player-<uuid>". */
|
||||
function uuidFromId(id) {
|
||||
return typeof id === 'string' && id.indexOf(PLAYER_PREFIX) === 0 ? id.substring(PLAYER_PREFIX.length) : null;
|
||||
}
|
||||
|
||||
/** Which player marker is under this screen point? Works even where the marker ignores pointer events. */
|
||||
function playerAt(x, y) {
|
||||
var nodes = document.querySelectorAll('[id^="' + PLAYER_PREFIX + '"]');
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
var r = nodes[i].getBoundingClientRect();
|
||||
if (r.width === 0 && r.height === 0) continue;
|
||||
if (x >= r.left - 6 && x <= r.right + 6 && y >= r.top - 6 && y <= r.bottom + 6) return uuidFromId(nodes[i].id);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hours(seconds) {
|
||||
seconds = Number(seconds) || 0;
|
||||
return seconds >= 3600 ? Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm' : Math.floor(seconds / 60) + 'm';
|
||||
}
|
||||
|
||||
function panel() {
|
||||
var el = document.getElementById('scopenet-panel');
|
||||
if (!el) { el = document.createElement('div'); el.id = 'scopenet-panel'; document.body.appendChild(el); }
|
||||
return el;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var el = document.getElementById('scopenet-panel');
|
||||
if (!selected) { if (el) el.remove(); return; }
|
||||
var p = players[selected] || players[selected.toLowerCase()] || {};
|
||||
el = panel();
|
||||
var name = p.name || 'Player';
|
||||
el.innerHTML =
|
||||
'<button class="close" aria-label="Close">×</button>' +
|
||||
'<div class="head"><img alt="" src="assets/playerheads/' + esc(selected) + '.png" onerror="this.src=\'assets/steve.png\'">' +
|
||||
'<div><h3>' + esc(name) + '</h3>' + (p.title ? '<div class="title">' + esc(p.title) + '</div>' : '') + '</div></div>' +
|
||||
'<div class="chips">' +
|
||||
'<span class="chip lv">Level ' + esc(p.level || 1) + '</span>' +
|
||||
(p.guild ? '<span class="chip guild">[' + esc(p.guildTag) + '] ' + esc(p.guild) + '</span>' : '') +
|
||||
(p.rank ? '<span class="chip rank">' + esc(p.rank) + '</span>' : '') + '</div>' +
|
||||
'<div class="facts">' +
|
||||
'<div>Playtime<strong>' + esc(hours(p.playtime)) + '</strong></div>' +
|
||||
'<div>Server level<strong>' + esc(p.serverLevel || 1) + '</strong></div>' +
|
||||
(p.guildRole ? '<div>Guild role<strong>' + esc(p.guildRole) + '</strong></div>' : '') + '</div>' +
|
||||
'<p class="note">Open this map in the SCOPENET launcher to send a teleport request or message, or to invite ' + esc(name) + ' to your guild.</p>';
|
||||
el.querySelector('.close').addEventListener('click', function () { select(null); });
|
||||
}
|
||||
|
||||
function select(uuid) {
|
||||
selected = uuid;
|
||||
if (embedded) {
|
||||
window.parent.postMessage(uuid
|
||||
? { source: 'scopenet-map', type: 'player', uuid: uuid, name: (players[uuid] || {}).name || '' }
|
||||
: { source: 'scopenet-map', type: 'deselect' }, '*');
|
||||
} else {
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetch(BASE + 'players.json?t=' + Date.now(), { cache: 'no-store' })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (j) { if (j && j.players) { players = j.players; if (!embedded && selected) render(); } })
|
||||
.catch(function () { /* the file appears once the server has written it */ });
|
||||
}
|
||||
|
||||
// A click is a press and release in nearly the same place; a drag moves the map.
|
||||
var down = null;
|
||||
document.addEventListener('pointerdown', function (e) { down = { x: e.clientX, y: e.clientY, t: Date.now() }; }, true);
|
||||
document.addEventListener('pointerup', function (e) {
|
||||
var start = down;
|
||||
down = null;
|
||||
if (!start || e.button > 0) return;
|
||||
if (Math.hypot(e.clientX - start.x, e.clientY - start.y) > 6 || Date.now() - start.t > 700) return;
|
||||
if (e.target && e.target.closest && e.target.closest('#scopenet-panel')) return;
|
||||
var uuid = playerAt(e.clientX, e.clientY);
|
||||
if (uuid) select(uuid);
|
||||
else if (selected) select(null);
|
||||
}, true);
|
||||
|
||||
window.addEventListener('message', function (e) {
|
||||
if (e.source === window.parent && e.data && e.data.source === 'scopenet-launcher' && e.data.type === 'hello') { /* acknowledged */ }
|
||||
});
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, 10000);
|
||||
if (embedded) window.parent.postMessage({ source: 'scopenet-map', type: 'ready' }, '*');
|
||||
|
||||
// Exposed for tests.
|
||||
window.__scopenet = { uuidFromId: uuidFromId, esc: esc, hours: hours };
|
||||
})();
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#f97316" stroke="#ffffff" stroke-width="2"/><path d="M8 10h3l2 10h10l2-7H12" fill="none" stroke="#fff" stroke-width="2.4" stroke-linejoin="round" stroke-linecap="round"/><circle cx="14" cy="24" r="1.8" fill="#fff"/><circle cx="22" cy="24" r="1.8" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 387 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#34d399" stroke="#ffffff" stroke-width="2"/><path d="M16 7l2.6 6.4 6.9.5-5.3 4.5 1.7 6.7L16 21.3 10.1 25l1.7-6.7-5.3-4.5 6.9-.5z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 270 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="#8b6cff" stroke="#ffffff" stroke-width="2"/><circle cx="16" cy="16" r="6" fill="none" stroke="#fff" stroke-width="2.5"/><circle cx="16" cy="16" r="2" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 290 B |