MAPS – EN

`, iconSize:[w,h], iconAnchor:[Math.round(w/2), h], popupAnchor:[0, -h + 2] }); }/* ===== Group & helpers ===== */ const group = L.featureGroup().addTo(map); const items = [];function rupiahToNumber(str){ return Number((str||'').toString().replace(/[^0-9]/g,''))||0; }function popupCardHTML(p){ const img = p.img || 'https://via.placeholder.com/400x200?text=No+Image'; const link = p.link || '#'; return `
${p.kode||''}
Code: ${p.kode||'-'} ${labelType(p.type)}
${p.harga||'-'}

${p.lokasi||''}

`; }function getHFPad(){ const cs = getComputedStyle(document.getElementById('atk-app')); const h = parseInt(cs.getPropertyValue('--atk-hdr'))||0; const f = parseInt(cs.getPropertyValue('--atk-ftr'))||0; return { h, f }; }/* ===== Toolbar (filter + sort + price + location) ===== */ const TYPES = ['RUMAH','PABRIK','HOTEL','RUKO','APARTEMEN','TANAH','PERKANTORAN','KIOS','GUDANG']; const state = { activeTypes:new Set(TYPES), sort:'default', minPrice:null, maxPrice:null, searchLokasi:'' };const Toolbar = L.Control.extend({ options:{ position:'topright' }, onAdd:function(){ const div = L.DomUtil.create('div','atk-toolbar'); div.innerHTML = `
Property Filters 0 items
Price (Rp)
Search location
Sort by
`; const btn = div.querySelector('#atk-toggle'); const body = div.querySelector('#atk-body'); btn.addEventListener('click', ()=>{ const hide = body.style.display !== 'none'; body.style.display = hide ? 'none' : ''; btn.textContent = hide ? '▲' : '▼'; btn.setAttribute('aria-label', hide ? 'Show filters' : 'Hide filters'); setTimeout(()=> map.invalidateSize(), 120); }); L.DomEvent.disableClickPropagation(div); return div; } }); map.addControl(new Toolbar());// chips type (English labels) const chipsBox = document.getElementById('atk-chips'); TYPES.forEach(t=>{ const chip = document.createElement('label'); chip.className='atk-chip'; chip.innerHTML = ` ${labelType(t)}`; chipsBox.appendChild(chip); chip.querySelector('input').addEventListener('change', (e)=>{ if(e.target.checked) state.activeTypes.add(t); else state.activeTypes.delete(t); applyFilterSort(true); }); });// sort document.getElementById('atk-sorter').addEventListener('change', (e)=>{ state.sort = e.target.value; applyFilterSort(false); });// price min/max const minInput = document.getElementById('atk-min'); const maxInput = document.getElementById('atk-max');function onPriceChange(){ const minVal = rupiahToNumber(minInput.value); const maxVal = rupiahToNumber(maxInput.value); state.minPrice = minVal > 0 ? minVal : null; state.maxPrice = maxVal > 0 ? maxVal : null; applyFilterSort(true); }minInput.addEventListener('input', onPriceChange); maxInput.addEventListener('input', onPriceChange);// search location const locInput = document.getElementById('atk-loc'); let locDebounce; locInput.addEventListener('input', ()=>{ clearTimeout(locDebounce); locDebounce = setTimeout(()=>{ state.searchLokasi = locInput.value.trim().toLowerCase(); applyFilterSort(true); }, 200); });// auto collapse on very small screens if (window.matchMedia('(max-width:400px)').matches) { const body = document.querySelector('.atk-toolbar #atk-body'); const btn = document.querySelector('.atk-toolbar #atk-toggle'); if (body && btn) { body.style.display = 'none'; btn.textContent = '▲'; btn.setAttribute('aria-label','Show filters'); } }function applyFilterSort(refit){ group.clearLayers();let list = items.filter(({prop})=>{ const type = prop.type || ''; const okType = state.activeTypes.has(type) || (type==='HOTEL/VILLA' && state.activeTypes.has('HOTEL'));if (!okType) return false;// price filter const harga = rupiahToNumber(prop.harga); if (state.minPrice !== null && harga < state.minPrice) return false; if (state.maxPrice !== null && harga > state.maxPrice) return false;// location filter if (state.searchLokasi){ const loc = (prop.lokasi || '').toString().toLowerCase(); if (!loc.includes(state.searchLokasi)) return false; }return true; });// sorting switch(state.sort){ case 'hargaAsc': list.sort((a,b)=> rupiahToNumber(a.prop.harga)-rupiahToNumber(b.prop.harga)); break; case 'hargaDesc': list.sort((a,b)=> rupiahToNumber(b.prop.harga)-rupiahToNumber(a.prop.harga)); break; case 'kodeAsc': list.sort((a,b)=> (a.prop.kode||'').localeCompare(b.prop.kode||'')); break; case 'kodeDesc': list.sort((a,b)=> (b.prop.kode||'').localeCompare(a.prop.kode||'')); break; }list.forEach(({marker})=> group.addLayer(marker)); document.getElementById('atk-count').textContent = list.length + ' items';if (refit && list.length){ const fg = L.featureGroup(list.map(i=>i.marker)); map.fitBounds(fg.getBounds().pad(isNarrow?0.08:0.2)); } }// hover keep-open logic for popups map.on('popupopen', (e) => { const m = e.popup._source; const el = e.popup._container; if (!m || !el) return; m._isPopupHovered = false; const enter = () => { m._isPopupHovered = true; if (m._hoverTimeout) clearTimeout(m._hoverTimeout); }; const leave = () => { m._isPopupHovered = false; m._hoverTimeout = setTimeout(()=> map.closePopup(), 200); }; el.addEventListener('mouseenter', enter); el.addEventListener('mouseleave', leave); m._popupEnter = enter; m._popupLeave = leave; m._popupEl = el; });map.on('popupclose', (e) => { const m = e.popup?._source; if (m && m._popupEl) { m._popupEl.removeEventListener('mouseenter', m._popupEnter); m._popupEl.removeEventListener('mouseleave', m._popupLeave); m._popupEnter = m._popupLeave = m._popupEl = null; m._isPopupHovered = false; if (m._hoverTimeout) clearTimeout(m._hoverTimeout); } });/* ===== Legend ===== */ const Legend = L.Control.extend({ options:{ position:'bottomleft' }, onAdd:function(){ const div = L.DomUtil.create('div','atk-legend'); const list = ['RUMAH','PABRIK','HOTEL','RUKO','APARTEMEN','TANAH','PERKANTORAN','KIOS','GUDANG'] .map(t=>`
${labelType(t)} ${labelType(t)}
`) .join(''); div.innerHTML = `
Property Types
${list}`; L.DomEvent.disableClickPropagation(div); return div; } }); map.addControl(new Legend());/* ===== UX: Fullscreen & Locate ===== */ const UXControl = L.Control.extend({ options:{ position:'topright' }, onAdd:function(){ const div = L.DomUtil.create('div','atk-ux leaflet-bar'); const btnFS = L.DomUtil.create('a','',div); btnFS.href='#'; btnFS.title='Full Screen'; btnFS.setAttribute('aria-label','Full Screen'); btnFS.innerHTML = ``; const btnLoc = L.DomUtil.create('a','',div); btnLoc.href='#'; btnLoc.title='Locate Me'; btnLoc.setAttribute('aria-label','Locate Me'); btnLoc.innerHTML = ``;L.DomEvent.on(btnFS,'click',(e)=>{ L.DomEvent.stop(e); const el=map.getContainer(); if(!document.fullscreenElement){ el.requestFullscreen?.(); } else { document.exitFullscreen?.(); } });L.DomEvent.on(btnLoc,'click',(e)=>{ L.DomEvent.stop(e); map.locate({ setView:true, maxZoom:16, enableHighAccuracy:true }); });L.DomEvent.disableClickPropagation(div); return div; } }); map.addControl(new UXControl());let locateMarker, locateCircle; map.on('locationfound',(e)=>{ if(locateMarker) map.removeLayer(locateMarker); if(locateCircle) map.removeLayer(locateCircle); locateMarker = L.marker(e.latlng,{ icon: makeDivIcon('RUMAH'), riseOnHover:true }) .addTo(map) .bindTooltip('You are here'); locateCircle = L.circle(e.latlng,{ radius:e.accuracy, color:'#2563eb', weight:1, fillColor:'#3b82f6', fillOpacity:.15 }).addTo(map); });map.on('locationerror',(e)=>{ alert('Unable to access location: ' + e.message); });L.control.scale({ metric:true, imperial:false }).addTo(map);// render markers helper (called after fetch) function renderMarkers(properties){ properties.forEach(p=>{ const m = L.marker([p.lat,p.lng], { icon: makeDivIcon(p.type), riseOnHover:true }); const {h,f} = getHFPad(); m.bindPopup(popupCardHTML(p), { className:'atk-popup', closeButton:false, autoPan:true, keepInView:true, autoPanPaddingTopLeft:L.point(12,12+h), autoPanPaddingBottomRight:L.point(12,12+f), maxWidth:isNarrow? Math.min(window.innerWidth*0.92,320):420 }); if (canHover) { m.on('mouseover', function(){ this.openPopup(); }); m.on('mouseout', function(){ const self = this; if (self._hoverTimeout) clearTimeout(self._hoverTimeout); self._hoverTimeout = setTimeout(()=>{ if (!self._isPopupHovered) map.closePopup(); }, 220); }); } items.push({prop:p, marker:m}); group.addLayer(m); }); applyFilterSort(false); }/* ====== FETCH DATA from API ====== */ const API_URL = 'https://www.jtii.co.id/aset/api/properti.php';fetch(API_URL, { cache:'no-store' }) .then(r => r.json()) .then(data => { if (!Array.isArray(data)) throw new Error('Invalid API format');// normalize lat/lng: support string or number const cleaned = data .map(d => { const lat = typeof d.lat === 'string' ? parseFloat(d.lat) : d.lat; const lng = typeof d.lng === 'string' ? parseFloat(d.lng) : d.lng; return { ...d, lat, lng }; }) .filter(d => Number.isFinite(d.lat) && Number.isFinite(d.lng));document.getElementById('atk-count').textContent = cleaned.length + ' items'; renderMarkers(cleaned); }) .catch(err => { console.error('Failed to fetch property data:', err); alert('Property data cannot be loaded at the moment.'); });// tiny reflow helpers function invalidateMapSoon(delay=80){ clearTimeout(invalidateMapSoon._t); invalidateMapSoon._t=setTimeout(()=>map.invalidateSize(),delay); } if (window.visualViewport){ visualViewport.addEventListener('resize',()=>invalidateMapSoon(60)); visualViewport.addEventListener('scroll',()=>invalidateMapSoon(60)); } window.addEventListener('orientationchange', ()=>invalidateMapSoon(200)); })();