Skip to content

Instantly share code, notes, and snippets.

@SaturnXIII
Created March 21, 2026 20:54
Show Gist options
  • Select an option

  • Save SaturnXIII/905acc166ce54ddcd3a97bb2358fb484 to your computer and use it in GitHub Desktop.

Select an option

Save SaturnXIII/905acc166ce54ddcd3a97bb2358fb484 to your computer and use it in GitHub Desktop.
ARCGIS MARKER TOOL
/**
* ╔══════════════════════════════════════════════════════════════════════════════╗
* ║ ARCGIS MARKER TOOL v4.0 ║
* ║ Script pour ajouter des marqueurs sur ArcGIS Instant Media ║
* ║ ║
* ║ SHIFT+CLIC pour ajouter un marqueur ║
* ║ Conversion automatique Lambert93 → WGS84 pour Google Maps ║
* ╚══════════════════════════════════════════════════════════════════════════════╝
*
* UTILISATION:
* 1. Ouvrir https://www.arcgis.com/apps/instant/media/index.html?appid=59873b0b206e4aefae712dc2cdda200c
* 2. Ouvrir les DevTools (F12) → Console
* 3. Copier-coller ce script entier et appuyer sur Entrée
* 4. SHIFT+CLIQUER sur la carte pour ajouter un marqueur
*/
(async function() {
'use strict';
// Polyfills pour tanh et atanh
Math.tanh = Math.tanh || function(x) {
if(x === Infinity) return 1;
if(x === -Infinity) return -1;
return (Math.exp(x) - Math.exp(-x)) / (Math.exp(x) + Math.exp(-x));
};
Math.atanh = Math.atanh || function(x) {
return Math.log((1+x)/(1-x)) / 2;
};
// Charger proj4js pour conversion précise
if (typeof proj4 === 'undefined') {
console.log('⏳ Chargement de proj4js...');
await new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.9.2/proj4.min.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
// Définir Lambert 93 manuellement
proj4.defs('EPSG:2154', '+proj=lcc +lat_1=49 +lat_2=44 +lat_0=46.5 +lon_0=3 +x_0=700000 +y_0=6600000 +ellps=GRS80 +units=m +no_defs');
console.log('✓ proj4js chargé avec EPSG:2154');
}
console.log('%c🗺️ ArcGIS Marker Tool v4.0 - Chargement...', 'color: #0079C1; font-size: 16px; font-weight: bold;');
const CONFIG = {
markerSize: 14,
markerOutlineColor: [255, 255, 255],
markerOutlineWidth: 2
};
let state = {
markers: [],
markerCount: 0,
viewElement: null,
view: null,
isInitialized: false,
dblclickHandler: null,
rightClickHandler: null,
spatialReference: null
};
// ═══════════════════════════════════════════════════════════════════════════════
// CONVERSION LAMBERT 93 → WGS84
// Utilise proj4js pour conversion précise
// ═══════════════════════════════════════════════════════════════════════════════
const CoordinateConverter = {
lambert93ToWGS84(x, y) {
if (typeof proj4 !== 'undefined') {
const result = proj4('EPSG:2154', 'WGS84', [x, y]);
return { lat: result[1], lng: result[0] };
}
// Fallback: approximation simple pour la France
// Paris: Lambert93 (656672, 6861882) → WGS84 (48.8566, 2.3522)
// Facteurs approximatifs
const factX = 0.0000365;
const factY = 0.000063;
const offsetX = 2.3;
const offsetY = 48.0;
return {
lat: (y - 6850000) * factY + offsetY,
lng: (x - 655000) * factX + offsetX
};
},
convert(x, y, sr) {
if (Math.abs(y) <= 90 && Math.abs(x) <= 180) {
return { lat: y, lng: x };
}
if (sr === 2154 || sr === 102110 || sr === 32631 || sr === 32632) {
return this.lambert93ToWGS84(x, y);
}
if (x > 100000 || y > 1000000) {
return this.lambert93ToWGS84(x, y);
}
return { lat: y, lng: x };
}
};
const Utils = {
formatCoord(num, decimals = 6) {
if (num === null || num === undefined || isNaN(num)) return null;
return Number(num.toFixed(decimals));
},
generateId() {
return `marker_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
},
getRandomColor() {
const hue = Math.floor(Math.random() * 360);
const saturation = 70 + Math.floor(Math.random() * 30);
const lightness = 45 + Math.floor(Math.random() * 20);
s = saturation / 100; l = lightness / 100;
const a = s * Math.min(l, 1 - l);
const f = n => {
const k = (n + hue / 30) % 12;
return l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
};
return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)];
},
googleMapsUrl(lat, lng) {
return `https://www.google.com/maps?q=${lat},${lng}`;
}
};
const UI = {
panel: null,
createPanel() {
this.removePanel();
const panel = document.createElement('div');
panel.id = 'arcgis-marker-panel';
panel.innerHTML = `
<style>
#arcgis-marker-panel {
position: fixed; top: 10px; right: 10px; width: 280px;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.4);
font-family: 'Segoe UI', sans-serif; z-index: 2147483647;
overflow: visible; color: #e0e0e0;
border: 1px solid rgba(0,122,193,0.3); max-height: 80vh;
display: flex; flex-direction: column;
}
#arcgis-marker-panel .panel-header {
background: linear-gradient(90deg, #0079C1 0%, #005a87 100%);
padding: 10px 12px; display: flex; align-items: center;
justify-content: space-between; cursor: move;
}
#arcgis-marker-panel .panel-title {
font-size: 12px; font-weight: 600; color: white;
}
#arcgis-marker-panel .panel-close {
background: rgba(255,255,255,0.2); border: none; color: white;
width: 20px; height: 20px; border-radius: 50%; cursor: pointer;
font-size: 11px;
}
#arcgis-marker-panel .panel-body { padding: 10px; overflow-y: auto; flex: 1; }
#arcgis-marker-panel .stats-row {
display: flex; justify-content: space-around; padding: 8px;
background: rgba(0,122,193,0.15); border-radius: 6px; margin-bottom: 8px;
}
#arcgis-marker-panel .stat-value { font-size: 16px; font-weight: 700; color: #00d4ff; }
#arcgis-marker-panel .stat-label { font-size: 8px; text-transform: uppercase; color: #888; }
#arcgis-marker-panel .coords-display {
background: rgba(0,0,0,0.3); border-radius: 6px; padding: 8px;
margin-bottom: 8px; font-family: monospace; font-size: 11px; min-height: 45px;
}
#arcgis-marker-panel .coord-value { color: #00ff88; line-height: 1.4; }
#arcgis-marker-panel .coord-empty { color: #666; font-style: italic; font-size: 10px; }
#arcgis-marker-panel .marker-list { max-height: 100px; overflow-y: auto; margin-bottom: 8px; }
#arcgis-marker-panel .marker-item {
display: flex; align-items: center; padding: 5px 6px;
background: rgba(255,255,255,0.05); border-radius: 4px;
margin-bottom: 4px; font-size: 10px; gap: 6px;
}
#arcgis-marker-panel .marker-color { width: 10px; height: 10px; border-radius: 50%; }
#arcgis-marker-panel .marker-coords { color: #00d4ff; font-family: monospace; flex: 1; }
#arcgis-marker-panel .marker-link {
background: #4285F4; border: none; color: white;
width: 16px; height: 16px; border-radius: 3px; cursor: pointer;
font-size: 9px; opacity: 0; text-decoration: none;
display: flex; align-items: center; justify-content: center;
}
#arcgis-marker-panel .marker-item:hover .marker-delete,
#arcgis-marker-panel .marker-item:hover .marker-link { opacity: 1; }
#arcgis-marker-panel .btn-group { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 5px; margin-bottom: 5px; }
#arcgis-marker-panel .btn { pointer-events: auto !important; }
#arcgis-marker-panel .full-width { width: 100%; margin-bottom: 8px; }
#arcgis-marker-panel .gmap-link {
display: flex; align-items: center; justify-content: center; gap: 6px;
padding: 8px; margin-bottom: 8px;
background: #4285F4; color: white; border: none;
border-radius: 6px; font-size: 11px; cursor: pointer;
text-decoration: none; transition: all 0.2s;
}
#arcgis-marker-panel .gmap-link:hover { background: #3367D6; transform: scale(1.02); }
#arcgis-marker-panel .gmap-link::before { content: '🗺️'; }
#arcgis-marker-panel .btn {
padding: 7px 8px; border: none; border-radius: 4px;
font-size: 10px; cursor: pointer; transition: all 0.2s;
}
#arcgis-marker-panel .btn-primary { background: #0079C1; color: white; }
#arcgis-marker-panel .btn-success { background: #34c759; color: white; }
#arcgis-marker-panel .btn-danger { background: #ff3b30; color: white; }
#arcgis-marker-panel .btn-secondary { background: rgba(255,255,255,0.1); color: #ccc; }
#arcgis-marker-panel .btn:hover { transform: scale(1.02); }
#arcgis-marker-panel .instructions {
margin-top: 8px; padding: 6px;
background: rgba(255,193,7,0.1); border: 1px solid rgba(255,193,7,0.3);
border-radius: 4px; font-size: 9px; color: #ffc107; line-height: 1.4;
}
#arcgis-marker-panel .toast {
position: fixed; bottom: 20px; right: 20px; background: #333;
color: white; padding: 8px 14px; border-radius: 5px;
font-size: 11px; z-index: 1000000; animation: slideIn 0.3s ease;
}
#arcgis-marker-panel .toast.success { background: #34c759; }
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
</style>
<div class="panel-header">
<div class="panel-title">📍 Marker Tool v3</div>
<button class="panel-close">✕</button>
</div>
<div class="panel-body">
<div class="stats-row">
<div class="stat-item">
<div class="stat-value" id="marker-count">0</div>
<div class="stat-label">Marqueurs</div>
</div>
<div class="stat-item">
<div class="stat-value" id="marker-index">-</div>
<div class="stat-label">Dernier</div>
</div>
</div>
<div class="coords-display">
<div class="coord-value" id="last-coords">
<span class="coord-empty">SHIFT+CLIQUEZ sur la carte</span>
</div>
</div>
<a class="gmap-link" id="gmap-link" href="#" target="_blank" style="display: none;">
Ouvrir dans Google Maps
</a>
<div class="marker-list" id="marker-list"></div>
<div class="btn-group">
<button class="btn btn-primary" id="btn-copy-json">📋 JSON</button>
<button class="btn btn-success" id="btn-copy-csv">📊 CSV</button>
<button class="btn btn-secondary" id="btn-undo">↩️ Undo</button>
</div>
<button class="btn btn-danger full-width" id="btn-clear-all">🗑️ Effacer tout</button>
<div class="instructions">
⚠️ <strong>SHIFT+CLIC</strong> pour ajouter un marqueur
</div>
</div>
`;
document.body.appendChild(panel);
this.panel = panel;
this.makeDraggable(panel, panel.querySelector('.panel-header'));
this.attachEvents();
},
makeDraggable(panel, handle) {
let isDragging = false, startX, startY, startLeft, startTop;
handle.addEventListener('mousedown', (e) => {
if (e.target.classList.contains('panel-close')) return;
isDragging = true;
startX = e.clientX; startY = e.clientY;
startLeft = panel.offsetLeft; startTop = panel.offsetTop;
panel.style.right = 'auto';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
panel.style.left = (startLeft + e.clientX - startX) + 'px';
panel.style.top = (startTop + e.clientY - startY) + 'px';
});
document.addEventListener('mouseup', () => { isDragging = false; });
},
attachEvents() {
const panel = this.panel;
panel.querySelector('.panel-close').addEventListener('click', () => {
this.removePanel();
MarkerTool.deactivate();
});
panel.querySelector('#btn-copy-json').addEventListener('click', () => {
const json = JSON.stringify(state.markers.map(m => ({
lat: m.lat, lng: m.lng, name: m.name, timestamp: m.timestamp
})), null, 2);
navigator.clipboard.writeText(json).then(() => this.showToast('JSON copié !', 'success'));
});
panel.querySelector('#btn-copy-csv').addEventListener('click', () => {
const csv = ['lat,lng,name,timestamp', ...state.markers.map(m =>
`${m.lat},${m.lng},"${m.name}",${m.timestamp}`
)].join('\n');
navigator.clipboard.writeText(csv).then(() => this.showToast('CSV copié !', 'success'));
});
panel.querySelector('#btn-undo').addEventListener('click', () => MarkerTool.removeLastMarker());
panel.querySelector('#btn-clear-all').addEventListener('click', () => {
if (state.markers.length > 0) {
MarkerTool.clearAllMarkers();
this.showToast('Effacé !', 'success');
}
});
},
updateMarkerList() {
const list = this.panel?.querySelector('#marker-list');
if (!list) return;
list.innerHTML = state.markers.map((marker, index) => {
const gmapUrl = Utils.googleMapsUrl(marker.lat, marker.lng);
return `
<div class="marker-item">
<div class="marker-color" style="background: rgb(${marker.color.join(',')})"></div>
<span style="color:#888">#${index + 1}</span>
<span class="marker-coords">${marker.lat?.toFixed(5) || '?'}, ${marker.lng?.toFixed(5) || '?'}</span>
<a class="marker-link" href="${gmapUrl}" target="_blank" title="Google Maps">G</a>
<button class="marker-delete" data-index="${index}">✕</button>
</div>
`;
}).join('');
list.querySelectorAll('.marker-delete').forEach(btn => {
btn.addEventListener('click', (e) => {
MarkerTool.removeMarker(parseInt(e.target.dataset.index));
});
});
},
updateStats() {
if (!this.panel) return;
this.panel.querySelector('#marker-count').textContent = state.markers.length;
this.panel.querySelector('#marker-index').textContent = state.markers.length || '-';
},
updateLastCoords(lat, lng) {
if (!this.panel) return;
const el = this.panel.querySelector('#last-coords');
el.innerHTML = `<strong>Lat:</strong> ${lat?.toFixed(6) || '?'} <strong>Lng:</strong> ${lng?.toFixed(6) || '?'}`;
const gmapLink = this.panel.querySelector('#gmap-link');
if (gmapLink && lat && lng) {
gmapLink.href = `https://www.google.com/maps?q=${lat},${lng}`;
gmapLink.style.display = 'flex';
}
},
showToast(message, type = '') {
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2000);
},
removePanel() {
document.getElementById('arcgis-marker-panel')?.remove();
this.panel = null;
}
};
const MarkerTool = {
async init() {
try {
console.log('⏳ Initialisation...');
state.viewElement = document.querySelector('arcgis-map');
if (!state.viewElement) {
throw new Error('arcgis-map non trouvé');
}
await state.viewElement.viewOnReady();
state.view = state.viewElement.view;
if (!state.view) {
throw new Error('View non initialisée');
}
// Détecter le système de référence spatial
state.spatialReference = state.view.spatialReference?.wkid || state.view.spatialReference?.latestWkid || null;
console.log('✓ Carte prête');
console.log(' View type:', state.view.type || 'unknown');
console.log(' Spatial Reference:', state.spatialReference || 'unknown');
this.attachEventListeners();
UI.createPanel();
state.isInitialized = true;
console.log('%c✅ ArcGIS Marker Tool v4.0 prêt !', 'color: #34c759; font-weight: bold;');
console.log('%c💡 SHIFT+CLIC sur la carte pour ajouter un marqueur', 'color: #ffc107;');
return true;
} catch (error) {
console.error('❌ Erreur:', error);
return false;
}
},
attachEventListeners() {
this.deactivate();
// SHIFT+CLIC pour ajouter un marqueur
state.dblclickHandler = async (event) => {
if (event.target.closest('#arcgis-marker-panel')) return;
if (!event.shiftKey) return; // Nécessite Shift
event.preventDefault();
event.stopPropagation();
try {
const rect = state.viewElement.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
console.log('📍 SHIFT+CLIC détecté à:', x.toFixed(0), y.toFixed(0));
let mapPoint = null;
// Méthode 1: view.toMap
if (state.view.toMap) {
try {
mapPoint = await state.view.toMap({ x, y });
console.log(' toMap result:', mapPoint);
} catch (e) {
console.log(' toMap échoué:', e.message);
}
}
// Méthode 2: view.hitTest
if (!mapPoint && state.view.hitTest) {
try {
const hit = await state.view.hitTest({ x: event.clientX, y: event.clientY });
console.log(' hitTest result:', hit);
if (hit?.results?.length > 0 && hit.results[0].graphic?.geometry) {
mapPoint = hit.results[0].graphic.geometry;
}
} catch (e) {
console.log(' hitTest échoué:', e.message);
}
}
// Méthode 3: view.screenToMap (API ancienne)
if (!mapPoint && state.view.screenToMap) {
try {
mapPoint = state.view.screenToMap({ x, y });
console.log(' screenToMap result:', mapPoint);
} catch (e) {
console.log(' screenToMap échoué:', e.message);
}
}
// Extraire les coordonnées brutes
let rawX = null, rawY = null;
if (mapPoint) {
// Différentes propriétés possibles selon le type de vue
rawX = mapPoint.longitude ?? mapPoint.x ?? mapPoint.xmin ?? mapPoint.x;
rawY = mapPoint.latitude ?? mapPoint.y ?? mapPoint.ymin ?? mapPoint.y;
console.log(' Coordonnées brutes - x:', rawX, 'y:', rawY);
console.log(' SR:', state.spatialReference);
}
// Vérifier validité
if (rawX === null || isNaN(rawX) || rawY === null || isNaN(rawY)) {
console.log('⚠️ Coordonnées invalides');
UI.showToast('Coordonnées invalides', '');
return;
}
// Convertir si nécessaire (Lambert93/UTM → WGS84)
const coords = CoordinateConverter.convert(rawX, rawY, state.spatialReference);
const lng = Utils.formatCoord(coords.lng);
const lat = Utils.formatCoord(coords.lat);
console.log('✅ Coordonnées WGS84 - lat:', lat, 'lng:', lng);
console.log('🔗 Google Maps: ' + Utils.googleMapsUrl(lat, lng));
UI.updateLastCoords(lat, lng);
const markerNumber = state.markers.length + 1;
const color = Utils.getRandomColor();
const graphic = {
geometry: { type: 'point', longitude: lng, latitude: lat },
symbol: {
type: 'simple-marker',
color: color,
size: CONFIG.markerSize,
outline: { color: CONFIG.markerOutlineColor, width: CONFIG.markerOutlineWidth }
}
};
state.view.graphics.add(graphic);
state.markers.push({
id: Utils.generateId(), lat, lng,
name: `Point #${markerNumber}`,
color, timestamp: new Date().toISOString(), graphic
});
state.markerCount = markerNumber;
UI.updateStats();
UI.updateMarkerList();
console.log(`📍 Marqueur #${markerNumber} ajouté: [${lat}, ${lng}]`);
} catch (error) {
console.error('❌ Erreur ajout marqueur:', error);
UI.showToast('Erreur: ' + error.message, '');
}
};
// Clic droit pour voir les coordonnées
state.rightClickHandler = async (event) => {
event.preventDefault();
const rect = state.viewElement.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
try {
let mapPoint = null;
if (state.view.toMap) {
mapPoint = await state.view.toMap({ x, y });
} else if (state.view.screenToMap) {
mapPoint = state.view.screenToMap({ x, y });
}
if (mapPoint) {
const rawX = mapPoint.longitude ?? mapPoint.x;
const rawY = mapPoint.latitude ?? mapPoint.y;
if (rawX && rawY) {
const coords = CoordinateConverter.convert(rawX, rawY, state.spatialReference);
const lng = coords.lng.toFixed(6);
const lat = coords.lat.toFixed(6);
console.log('%c📍 Coordonnées:%c lat: %c' + lat + '%c, lng: %c' + lng,
'color: #888;', 'color: #888;', 'color: #00ff88;', 'color: #888;', 'color: #00ff88;');
console.log('%c🔗 Google Maps: %c' + Utils.googleMapsUrl(coords.lat, coords.lng),
'color: #888;', 'color: #4285F4;');
UI.updateLastCoords(coords.lat, coords.lng);
UI.showToast(`Lat: ${lat}, Lng: ${lng}`, 'success');
}
}
} catch (error) {
console.error('Erreur coordonnées:', error);
}
};
// Attacher sur l'élément map et le canvas
state.viewElement.addEventListener('click', state.dblclickHandler);
state.viewElement.addEventListener('contextmenu', state.rightClickHandler);
const canvas = state.viewElement.querySelector('canvas');
if (canvas) {
canvas.addEventListener('click', state.dblclickHandler);
canvas.addEventListener('contextmenu', state.rightClickHandler);
console.log('✓ Canvas trouvé, événements attachés dessus aussi');
}
console.log('✓ Événements attachés (click+shift, contextmenu)');
},
removeLastMarker() {
if (state.markers.length === 0) return;
const last = state.markers.pop();
state.view.graphics.remove(last.graphic);
state.markerCount = state.markers.length;
UI.updateStats();
UI.updateMarkerList();
},
removeMarker(index) {
if (index < 0 || index >= state.markers.length) return;
const marker = state.markers[index];
state.view.graphics.remove(marker.graphic);
state.markers.splice(index, 1);
UI.updateStats();
UI.updateMarkerList();
},
clearAllMarkers() {
state.markers.forEach(m => state.view.graphics.remove(m.graphic));
state.markers = [];
state.markerCount = 0;
UI.updateStats();
UI.updateMarkerList();
},
getMarkers() {
return state.markers.map(m => ({
lat: m.lat, lng: m.lng, name: m.name, timestamp: m.timestamp
}));
},
exportJSON() {
return JSON.stringify(this.getMarkers(), null, 2);
},
exportCSV() {
return ['lat,lng,name,timestamp', ...state.markers.map(m =>
`${m.lat},${m.lng},"${m.name}",${m.timestamp}`
)].join('\n');
},
deactivate() {
if (state.viewElement) {
state.viewElement.removeEventListener('dblclick', state.dblclickHandler);
state.viewElement.removeEventListener('contextmenu', state.rightClickHandler);
const canvas = state.viewElement.querySelector('canvas');
if (canvas) {
canvas.removeEventListener('dblclick', state.dblclickHandler);
canvas.removeEventListener('contextmenu', state.rightClickHandler);
}
}
state.dblclickHandler = null;
state.rightClickHandler = null;
}
};
window.MarkerTool = MarkerTool;
await MarkerTool.init();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment