`,
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 `

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
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)}
`)
.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));
})();