Follow location
vanilla.html
react.html
vue.html
common.ts
variables.css
variables.ts
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/satellite.js@5.0.0/dist/satellite.min.js"></script>
<!-- To make the map appear, you must add your apikey -->
<script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>
<script
data-plugins="transform-modules-umd"
data-presets="typescript"
type="text/babel"
src="../variables.ts"
></script>
<script
data-plugins="transform-modules-umd"
data-presets="typescript"
type="text/babel"
src="./common.ts"
></script>
<script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel">
import {LngLat} from '@mappable-world/mappable-types/imperative';
import {
animateMarker,
ANIMATION_DURATION,
BEHAVIORS,
getLocation,
getSatelliteRoute,
LOCATION,
MARKER_IMAGE_PATH
} from './common';
import {LINE_STRING_STYLE} from '../variables';
// Initialize global variables
window.map = null;
// Main function to initialize the application
main();
async function main() {
await mappable.ready;
const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapMarker, MMapFeature} = mappable;
map = new MMap(
document.getElementById('app'),
{location: LOCATION, behaviors: BEHAVIORS, showScaleInCopyrights: true},
[new MMapDefaultSchemeLayer({}), new MMapDefaultFeaturesLayer({})]
);
const markerElement = document.createElement('div');
markerElement.className = 'marker-container';
const markerImg = document.createElement('img');
markerImg.src = MARKER_IMAGE_PATH;
markerImg.alt = 'marker';
markerElement.appendChild(markerImg);
const marker = new MMapMarker({disableRoundCoordinates: true, coordinates: [0, 0]}, markerElement);
const feature = new MMapFeature({
geometry: {type: 'MultiLineString', coordinates: []},
style: LINE_STRING_STYLE
});
map.addChild(feature);
let prevCoordinates: LngLat;
await fetchData();
// Set an interval to update the location periodically
setInterval(fetchData, ANIMATION_DURATION);
async function fetchData() {
const newCoordinates = await getLocation();
const lineCoordinates = await getSatelliteRoute();
if (!map.children.includes(marker)) {
marker.update({coordinates: newCoordinates});
map.addChild(marker);
}
if (!prevCoordinates) {
map.setLocation({center: newCoordinates});
} else {
animateMarker(prevCoordinates, newCoordinates, (lngLat) => {
map.setLocation({center: lngLat});
marker.update({coordinates: lngLat});
});
}
prevCoordinates = newCoordinates;
feature.update({geometry: {type: 'MultiLineString', coordinates: lineCoordinates}});
}
}
</script>
<!-- prettier-ignore -->
<style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; } </style>
<link rel="stylesheet" href="../variables.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<script crossorigin src="https://cdn.jsdelivr.net/npm/react@17/umd/react.production.min.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/react-dom@17/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/satellite.js@5.0.0/dist/satellite.min.js"></script>
<!-- To make the map appear, you must add your apikey -->
<script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>
<script
data-plugins="transform-modules-umd"
data-presets="typescript"
type="text/babel"
src="../variables.ts"
></script>
<script
data-plugins="transform-modules-umd"
data-presets="typescript"
type="text/babel"
src="./common.ts"
></script>
<script data-plugins="transform-modules-umd" data-presets="react, typescript" type="text/babel">
import {LngLat} from '@mappable-world/mappable-types';
import {
animateMarker,
ANIMATION_DURATION,
BEHAVIORS,
getLocation,
getSatelliteRoute,
LOCATION,
MARKER_IMAGE_PATH
} from './common';
import {LINE_STRING_STYLE} from '../variables';
// Global variable to hold the map instance
window.map = null;
// Main function to initialize the application
main();
async function main() {
// Import Mappable maps and wait for the library to be ready
const [mappableReact] = await Promise.all([mappable.import('@mappable-world/mappable-reactify'), mappable.ready]);
// Reactify Mappable maps for use with React
const reactify = mappableReact.reactify.bindTo(React, ReactDOM);
const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapMarker, MMapFeature} = reactify.module(mappable);
const {useState, useEffect, useRef} = React;
function App() {
// State for current coordinates and line coordinates
const [location, setLocation] = useState(LOCATION);
const [coordinates, setCoordinates] = useState<LngLat>();
const [lineCoordinates, setLineCoordinates] = useState<LngLat[][]>([]);
// References for marker and previous coordinates
const prevCoordinatesRef = useRef<LngLat>(null);
useEffect(() => {
fetchData();
// Set an interval to update the location periodically
const updateSource = setInterval(() => {
fetchData();
}, ANIMATION_DURATION);
// Function to fetch new location and satellite route
async function fetchData() {
const newCoordinates = await getLocation(); // Fetch new coordinates
// Fetch satellite route coordinates
const lines = await getSatelliteRoute();
if (!prevCoordinatesRef.current) {
setCoordinates(newCoordinates);
setLocation({center: newCoordinates});
} else {
// Animate marker if previous coordinates exist
animateMarker(prevCoordinatesRef.current, newCoordinates, (lngLat) => {
setLocation({center: lngLat});
setCoordinates(lngLat);
});
}
prevCoordinatesRef.current = newCoordinates;
setLineCoordinates(lines); // Update line coordinates state
}
// Cleanup function to clear the interval when the component unmounts
return () => clearInterval(updateSource);
}, []); // Empty dependency array to run once on mount
return (
<MMap location={location} behaviors={BEHAVIORS} showScaleInCopyrights={true} ref={(x) => (map = x)}>
<MMapDefaultSchemeLayer />
<MMapDefaultFeaturesLayer />
{coordinates && (
<MMapMarker disableRoundCoordinates coordinates={coordinates}>
<div className="marker-container">
<img src={MARKER_IMAGE_PATH} alt="marker" />
</div>
</MMapMarker>
)}
{lineCoordinates && (
<MMapFeature
geometry={{type: 'MultiLineString', coordinates: lineCoordinates}}
style={LINE_STRING_STYLE}
/>
)}
</MMap>
);
}
// Render the App component inside a strict mode
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('app')
);
}
</script>
<!-- prettier-ignore -->
<style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; } </style>
<link rel="stylesheet" href="../variables.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<script crossorigin src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
<script crossorigin src="https://cdn.jsdelivr.net/npm/satellite.js@5.0.0/dist/satellite.min.js"></script>
<!-- To make the map appear, you must add your apikey -->
<script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>
<script
data-plugins="transform-modules-umd"
data-presets="typescript"
type="text/babel"
src="../variables.ts"
></script>
<script
data-plugins="transform-modules-umd"
data-presets="typescript"
type="text/babel"
src="./common.ts"
></script>
<script data-plugins="transform-modules-umd" data-presets=" typescript" type="text/babel">
import {
animateMarker,
ANIMATION_DURATION,
BEHAVIORS,
getLocation,
getSatelliteRoute,
LOCATION,
MARKER_IMAGE_PATH
} from './common';
import {LINE_STRING_STYLE} from '../variables';
window.map = null;
async function main() {
// Import the Vuefy module for Mappable Maps integration
const [mappableVue] = await Promise.all([mappable.import('@mappable-world/mappable-vuefy'), mappable.ready]);
// Bind Vuefy to the Vue instance
const vuefy = mappableVue.vuefy.bindTo(Vue);
// Destructure required components from the Vuefy module
const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapMarker, MMapFeature} =
vuefy.module(mappable);
// Create the Vue application
const app = Vue.createApp({
components: {
MMap,
MMapDefaultSchemeLayer,
MMapDefaultFeaturesLayer,
MMapMarker,
MMapFeature
},
setup() {
// Reference to the map instance
const refMap = (ref) => {
window.map = ref?.entity;
};
// Reactive variables for location and coordinates
const location = Vue.ref(LOCATION);
const coordinates = Vue.ref(null);
const lineCoordinates = Vue.ref([]);
const prevCoordinates = Vue.ref(null);
const updateSource = Vue.ref(null);
// Lifecycle hook to fetch new coordinates on mount
Vue.onMounted(() => {
// Function to fetch new coordinates and update the map
// Initial data fetch
fetchData();
// Set an interval to update the location periodically
updateSource.value = setInterval(() => {
fetchData();
}, ANIMATION_DURATION);
// Function to fetch new location and satellite route
async function fetchData() {
// Fetch new coordinates from the API
const newCoordinates = await getLocation();
// Fetch satellite route coordinates
lineCoordinates.value = await getSatelliteRoute();
if (!prevCoordinates.value) {
// Update current marker coordinates
coordinates.value = newCoordinates;
// Update the map location
location.value = {center: newCoordinates};
} else {
// Animate marker if previous coordinates exist
animateMarker(prevCoordinates.value, newCoordinates, (lngLat) => {
location.value = {center: lngLat};
coordinates.value = lngLat;
});
}
// Store marker coordinates as previous
prevCoordinates.value = newCoordinates;
}
});
Vue.onBeforeUnmount(() => {
clearInterval(updateSource.value);
});
// Return reactive variables to the template
return {
location,
coordinates,
lineCoordinates,
refMap,
MARKER_IMAGE_PATH,
LINE_STRING_STYLE,
BEHAVIORS
};
},
// Template for the Vue component
template: `
<MMap :location="location" :behaviors="BEHAVIORS" :showScaleInCopyrights="true" :ref="refMap">
<MMapDefaultSchemeLayer />
<MMapDefaultFeaturesLayer />
<MMapMarker v-if="coordinates" disableRoundCoordinates :coordinates="coordinates">
<div class="marker-container">
<img :src="MARKER_IMAGE_PATH" alt="marker" />
</div>
</MMapMarker>
<MMapFeature
v-if="lineCoordinates"
:geometry="{ type: 'MultiLineString', coordinates: lineCoordinates }"
:style="LINE_STRING_STYLE"
/>
</MMap>`
});
// Mount the Vue application to the DOM
app.mount('#app');
}
// Execute the main function to start the application
main();
</script>
<!-- prettier-ignore -->
<style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; } </style>
<link rel="stylesheet" href="../variables.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>
import type {LngLat, MMapLocationRequest, BehaviorType} from '@mappable-world/mappable-types';
export const LOCATION: MMapLocationRequest = {
center: [0, 0], // starting position [lng, lat]
zoom: 3 // starting zoom
};
export const BEHAVIORS: BehaviorType[] = ['scrollZoom'];
// Constants for marker image path, animation duration, and line string style
export const MARKER_IMAGE_PATH = '../icon-iss.svg';
export const ANIMATION_DURATION = 5000; // Duration for animation in milliseconds
// Function to get the satellite route
export async function getSatelliteRoute(): Promise<LngLat[][]> {
try {
// Fetch satellite TLE data
const response = await fetch('https://api.wheretheiss.at/v1/satellites/25544/tles', {method: 'GET'});
const responseJSON = await response.json();
// Create current UTC date
const now = new Date(
Date.UTC(
new Date().getUTCFullYear(),
new Date().getUTCMonth(),
new Date().getUTCDate(),
new Date().getUTCHours(),
new Date().getUTCMinutes(),
new Date().getUTCSeconds()
)
);
// Convert TLE to satellite record
const satrec = satellite.twoline2satrec(responseJSON.line1, responseJSON.line2);
const positions: LngLat[] = []; // Array to hold satellite positions
// Define time duration and interval for position calculations
const durationMilliseconds = 50 * 60 * 1000; // 45 minutes in milliseconds
const intervalMilliseconds = 10000; // 10 seconds interval for each position
// Calculate satellite positions over the specified duration
for (let time = -durationMilliseconds; time <= durationMilliseconds; time += intervalMilliseconds) {
const positionTime = new Date(now.getTime() + time);
const positionAndVelocity = satellite.propagate(satrec, positionTime);
const positionEci = positionAndVelocity.position;
// Check for valid ECI position
if (!positionEci) {
console.error('Invalid position ECI:', positionAndVelocity);
continue;
}
// Convert ECI position to geodetic coordinates
const gmst = satellite.gstime(positionTime);
const positionGd = satellite.eciToGeodetic(positionEci, gmst);
positions.push([positionGd.longitude * (180 / Math.PI), positionGd.latitude * (180 / Math.PI)]);
}
// Split the array of positions into two parts
const {firstPart, secondPart} = splitArray(positions);
return [firstPart, secondPart];
} catch (err) {
console.error('Failed to fetch satellite route:', err);
throw new Error('Failed to fetch satellite route');
}
}
// Helper function to split an array into two parts based on a condition
const splitArray = (arr) => {
let firstPart = [];
let secondPart = [];
let foundSplit = false;
for (let i = 0; i < arr.length; i++) {
// Determine where to split the array based on longitude values
if (!foundSplit && i !== 0 && +arr[i][0] < +arr[i - 1][0]) {
foundSplit = true;
}
// Push to the appropriate part based on the split condition
if (!foundSplit) {
firstPart.push(arr[i]);
} else {
secondPart.push(arr[i]);
}
}
return {firstPart, secondPart};
};
// Function to animate a marker's movement from one location to another
export const animateMarker = (from: LngLat, to: LngLat, callback: (lngLat: LngLat) => void) => {
const duration = ANIMATION_DURATION; // Duration of the animation
const startTime = performance.now(); // Start time of the animation
const animateStep = (currentTime) => {
const elapsed = currentTime - startTime; // Calculate elapsed time
const t = Math.min(elapsed / duration, 1); // Calculate interpolation factor
// Interpolate longitude and latitude
const interpolatedLongitude = from[0] + (to[0] - from[0]) * t;
const interpolatedLatitude = from[1] + (to[1] - from[1]) * t;
callback([interpolatedLongitude, interpolatedLatitude]);
// Continue animation if not complete
if (t < 1) {
requestAnimationFrame(animateStep);
}
};
requestAnimationFrame(animateStep); // Start the animation
};
// Function to get the current location of the satellite
export async function getLocation(): Promise<LngLat> {
try {
// Fetch satellite location data
const response = await fetch('https://api.wheretheiss.at/v1/satellites/25544', {method: 'GET'});
const {longitude, latitude} = await response.json();
return [longitude, latitude]; // Return coordinates as LngLat
} catch (err) {
console.error('Failed to fetch satellite location:', err);
throw new Error('Failed to fetch satellite location');
}
}
.marker-container {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
min-width: 24px;
min-height: 24px;
border: 3px solid #fff;
border-radius: 8px;
background-color: #313133;
box-shadow: 0 0 2px 0 #5f698314;
transform: translate(-50%, -50%);
}
import type {DrawingStyle} from '@mappable-world/mappable-types';
export const LINE_STRING_STYLE: DrawingStyle = {
simplificationRate: 0,
stroke: [
{color: '#2E4CE5', width: 3, dash: [8, 14]}, // Primary stroke style
{color: '#FFFFFF', width: 7, opacity: 0.3} // Secondary stroke style
]
};