1508 Location Technology Selector
Interactive tool for selecting optimal indoor/outdoor positioning technologies
location
positioning
gps
ble
uwb
wifi
rfid
human-factors
animation
1508.1 Location Technology Selector
NoteAbout This Tool
This interactive tool helps you select the optimal location/positioning technology for your IoT application. Compare GPS, Wi-Fi, BLE beacons, UWB, LoRa triangulation, cellular positioning, and RFID based on your specific requirements.
TipHow to Use
- Set your requirements - Indoor/outdoor, accuracy, update rate, power budget, infrastructure investment
- View technology comparisons via the radar chart
- See suitability scores calculated for each technology
- Use presets for common use cases
- Build hybrid solutions combining multiple technologies
Show code
viewof locationSelector = {
// ===========================================================================
// LOCATION TECHNOLOGY SELECTOR FOR IOT
// ===========================================================================
// Features:
// - Requirements inputs (indoor/outdoor, accuracy, update rate, power, cost)
// - Technology database with 7 positioning technologies
// - Radar chart comparison using D3.js
// - Suitability scoring algorithm
// - Cost estimation
// - Use case presets
// - Hybrid solution builder
// ===========================================================================
// ---------------------------------------------------------------------------
// CONFIGURATION
// ---------------------------------------------------------------------------
const config = {
width: 950,
colors: {
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
white: "#FFFFFF",
red: "#E74C3C",
green: "#27AE60",
purple: "#9B59B6",
blue: "#3498DB",
yellow: "#F1C40F"
}
};
// Technology database
const technologies = [
{
id: "gps",
name: "GPS/GNSS",
description: "Satellite-based global positioning",
indoor: 0,
outdoor: 10,
accuracyCm: 0,
accuracyM: 3,
accuracy10m: 8,
accuracy100m: 10,
updateFast: 7,
updateMedium: 10,
updateSlow: 10,
powerLow: 2,
powerMedium: 5,
powerHigh: 10,
infraLow: 10,
infraMedium: 10,
infraHigh: 10,
color: config.colors.blue,
costPerDevice: 5,
costInfrastructure: 0,
pros: ["Global coverage", "No infrastructure needed", "Well-established"],
cons: ["No indoor coverage", "High power consumption", "Limited accuracy"]
},
{
id: "wifi",
name: "Wi-Fi Positioning",
description: "Wi-Fi fingerprinting and triangulation",
indoor: 8,
outdoor: 6,
accuracyCm: 0,
accuracyM: 5,
accuracy10m: 9,
accuracy100m: 10,
updateFast: 5,
updateMedium: 8,
updateSlow: 10,
powerLow: 3,
powerMedium: 6,
powerHigh: 9,
infraLow: 4,
infraMedium: 8,
infraHigh: 10,
color: config.colors.teal,
costPerDevice: 3,
costInfrastructure: 5000,
pros: ["Uses existing Wi-Fi infrastructure", "Works indoors", "Good coverage"],
cons: ["Accuracy varies", "Requires site survey", "Signal interference"]
},
{
id: "ble",
name: "BLE Beacons",
description: "Bluetooth Low Energy proximity and positioning",
indoor: 9,
outdoor: 5,
accuracyCm: 3,
accuracyM: 8,
accuracy10m: 10,
accuracy100m: 10,
updateFast: 7,
updateMedium: 9,
updateSlow: 10,
powerLow: 9,
powerMedium: 10,
powerHigh: 10,
infraLow: 3,
infraMedium: 7,
infraHigh: 9,
color: config.colors.purple,
costPerDevice: 2,
costInfrastructure: 3000,
pros: ["Very low power", "Low cost", "Indoor optimized"],
cons: ["Requires beacon infrastructure", "Limited range", "Calibration needed"]
},
{
id: "uwb",
name: "UWB",
description: "Ultra-Wideband precision positioning",
indoor: 10,
outdoor: 6,
accuracyCm: 10,
accuracyM: 10,
accuracy10m: 10,
accuracy100m: 10,
updateFast: 10,
updateMedium: 10,
updateSlow: 10,
powerLow: 4,
powerMedium: 7,
powerHigh: 10,
infraLow: 2,
infraMedium: 5,
infraHigh: 8,
color: config.colors.orange,
costPerDevice: 15,
costInfrastructure: 15000,
pros: ["Centimeter accuracy", "Fast updates", "Good multipath resistance"],
cons: ["Higher cost", "Infrastructure required", "Limited range per anchor"]
},
{
id: "lora",
name: "LoRa Triangulation",
description: "LPWAN-based positioning using RSSI/TDOA",
indoor: 3,
outdoor: 8,
accuracyCm: 0,
accuracyM: 2,
accuracy10m: 5,
accuracy100m: 8,
updateFast: 2,
updateMedium: 5,
updateSlow: 9,
powerLow: 10,
powerMedium: 10,
powerHigh: 10,
infraLow: 4,
infraMedium: 7,
infraHigh: 9,
color: config.colors.green,
costPerDevice: 8,
costInfrastructure: 8000,
pros: ["Very low power", "Long range", "Good for rural areas"],
cons: ["Low accuracy", "Slow updates", "Gateway density needed"]
},
{
id: "cellular",
name: "Cellular (LTE-M/NB-IoT)",
description: "Cell tower triangulation and enhanced positioning",
indoor: 4,
outdoor: 9,
accuracyCm: 0,
accuracyM: 2,
accuracy10m: 6,
accuracy100m: 9,
updateFast: 4,
updateMedium: 7,
updateSlow: 10,
powerLow: 5,
powerMedium: 7,
powerHigh: 9,
infraLow: 10,
infraMedium: 10,
infraHigh: 10,
color: config.colors.red,
costPerDevice: 12,
costInfrastructure: 0,
pros: ["No infrastructure", "Wide coverage", "Works moving at speed"],
cons: ["Monthly data costs", "Limited indoor accuracy", "Cell tower dependent"]
},
{
id: "rfid",
name: "RFID",
description: "Radio frequency identification for presence/zone detection",
indoor: 9,
outdoor: 4,
accuracyCm: 2,
accuracyM: 7,
accuracy10m: 9,
accuracy100m: 10,
updateFast: 9,
updateMedium: 10,
updateSlow: 10,
powerLow: 10,
powerMedium: 10,
powerHigh: 10,
infraLow: 2,
infraMedium: 6,
infraHigh: 9,
color: config.colors.yellow,
costPerDevice: 0.5,
costInfrastructure: 10000,
pros: ["Very low tag cost", "Passive tags available", "Good for inventory"],
cons: ["Zone-based only", "Reader infrastructure", "Limited range"]
}
];
// Use case presets
const presets = {
"warehouse": {
name: "Warehouse Inventory Tracking",
description: "Track pallets and inventory items in a large warehouse",
settings: {
environment: "indoor",
accuracy: "10m",
updateRate: "slow",
power: "low",
infrastructure: "medium"
}
},
"personnel": {
name: "Personnel Tracking",
description: "Track employee locations for safety and workflow optimization",
settings: {
environment: "indoor",
accuracy: "1m",
updateRate: "medium",
power: "medium",
infrastructure: "medium"
}
},
"outdoor-asset": {
name: "Outdoor Asset Tracking",
description: "Track containers, equipment, or vehicles across large outdoor areas",
settings: {
environment: "outdoor",
accuracy: "10m",
updateRate: "slow",
power: "low",
infrastructure: "low"
}
},
"fleet": {
name: "Vehicle Fleet Tracking",
description: "Real-time tracking of delivery vehicles or company fleet",
settings: {
environment: "outdoor",
accuracy: "10m",
updateRate: "fast",
power: "high",
infrastructure: "low"
}
},
"indoor-nav": {
name: "Indoor Navigation",
description: "Guide visitors through museums, airports, or shopping centers",
settings: {
environment: "indoor",
accuracy: "1m",
updateRate: "fast",
power: "medium",
infrastructure: "high"
}
}
};
// ---------------------------------------------------------------------------
// STATE MANAGEMENT
// ---------------------------------------------------------------------------
let state = {
environment: "indoor",
accuracy: "1m",
updateRate: "medium",
power: "medium",
infrastructure: "medium",
selectedTechs: ["ble", "uwb"],
hybridMode: false
};
// ---------------------------------------------------------------------------
// CREATE CONTAINER
// ---------------------------------------------------------------------------
const container = d3.create("div")
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("max-width", `${config.width}px`)
.style("margin", "0 auto")
.style("padding", "20px")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("border", `2px solid ${config.colors.lightGray}`);
// Title
container.append("div")
.style("text-align", "center")
.style("margin-bottom", "20px")
.html(`
<h3 style="color: ${config.colors.navy}; margin: 0 0 8px 0;">Location Technology Selector</h3>
<p style="color: ${config.colors.gray}; margin: 0; font-size: 14px;">Find the optimal positioning technology for your IoT application</p>
`);
// ---------------------------------------------------------------------------
// PRESET BUTTONS
// ---------------------------------------------------------------------------
const presetRow = container.append("div")
.style("display", "flex")
.style("gap", "8px")
.style("margin-bottom", "20px")
.style("flex-wrap", "wrap")
.style("justify-content", "center");
presetRow.append("span")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("margin-right", "10px")
.style("align-self", "center")
.text("Use Case Presets:");
Object.entries(presets).forEach(([key, preset]) => {
presetRow.append("button")
.attr("data-preset", key)
.text(preset.name)
.style("padding", "6px 12px")
.style("border-radius", "20px")
.style("border", `1px solid ${config.colors.teal}`)
.style("background", config.colors.white)
.style("color", config.colors.teal)
.style("cursor", "pointer")
.style("font-size", "12px")
.style("transition", "all 0.2s ease")
.on("click", function() {
state = { ...state, ...preset.settings };
updateUI();
})
.on("mouseenter", function() {
d3.select(this)
.style("background", config.colors.teal)
.style("color", config.colors.white);
})
.on("mouseleave", function() {
d3.select(this)
.style("background", config.colors.white)
.style("color", config.colors.teal);
});
});
// ---------------------------------------------------------------------------
// MAIN CONTENT GRID
// ---------------------------------------------------------------------------
const mainGrid = container.append("div")
.style("display", "grid")
.style("grid-template-columns", "300px 1fr")
.style("gap", "25px");
// Left column: Requirements
const leftColumn = mainGrid.append("div");
// Right column: Results
const rightColumn = mainGrid.append("div");
// ---------------------------------------------------------------------------
// REQUIREMENTS PANEL
// ---------------------------------------------------------------------------
const reqPanel = leftColumn.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "20px");
reqPanel.append("div")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("font-size", "16px")
.style("margin-bottom", "20px")
.style("padding-bottom", "10px")
.style("border-bottom", `2px solid ${config.colors.teal}`)
.text("Requirements");
// Environment selector
const envGroup = reqPanel.append("div")
.style("margin-bottom", "20px");
envGroup.append("label")
.style("display", "block")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("margin-bottom", "8px")
.style("font-size", "13px")
.text("Environment");
const envBtns = envGroup.append("div")
.style("display", "flex")
.style("gap", "8px");
["indoor", "outdoor", "both"].forEach(env => {
envBtns.append("button")
.attr("data-env", env)
.text(env.charAt(0).toUpperCase() + env.slice(1))
.style("flex", "1")
.style("padding", "10px")
.style("border-radius", "8px")
.style("border", "2px solid")
.style("cursor", "pointer")
.style("font-size", "13px")
.style("font-weight", "500")
.style("transition", "all 0.2s ease");
});
// Accuracy selector
const accGroup = reqPanel.append("div")
.style("margin-bottom", "20px");
accGroup.append("label")
.style("display", "block")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("margin-bottom", "8px")
.style("font-size", "13px")
.text("Accuracy Needed");
const accSelect = accGroup.append("select")
.style("width", "100%")
.style("padding", "10px")
.style("border-radius", "8px")
.style("border", `1px solid ${config.colors.gray}`)
.style("font-size", "13px");
[
{ value: "cm", label: "Centimeter (< 10 cm)" },
{ value: "1m", label: "Sub-meter (< 1 m)" },
{ value: "10m", label: "Room-level (< 10 m)" },
{ value: "100m", label: "Zone/Area (< 100 m)" }
].forEach(opt => {
accSelect.append("option")
.attr("value", opt.value)
.text(opt.label);
});
// Update rate selector
const updateGroup = reqPanel.append("div")
.style("margin-bottom", "20px");
updateGroup.append("label")
.style("display", "block")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("margin-bottom", "8px")
.style("font-size", "13px")
.text("Update Frequency");
const updateSelect = updateGroup.append("select")
.style("width", "100%")
.style("padding", "10px")
.style("border-radius", "8px")
.style("border", `1px solid ${config.colors.gray}`)
.style("font-size", "13px");
[
{ value: "fast", label: "Real-time (< 1 sec)" },
{ value: "medium", label: "Frequent (1-30 sec)" },
{ value: "slow", label: "Periodic (> 1 min)" }
].forEach(opt => {
updateSelect.append("option")
.attr("value", opt.value)
.text(opt.label);
});
// Power budget selector
const powerGroup = reqPanel.append("div")
.style("margin-bottom", "20px");
powerGroup.append("label")
.style("display", "block")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("margin-bottom", "8px")
.style("font-size", "13px")
.text("Power Budget");
const powerSelect = powerGroup.append("select")
.style("width", "100%")
.style("padding", "10px")
.style("border-radius", "8px")
.style("border", `1px solid ${config.colors.gray}`)
.style("font-size", "13px");
[
{ value: "low", label: "Battery-powered (years)" },
{ value: "medium", label: "Rechargeable (days-weeks)" },
{ value: "high", label: "Powered/plugged in" }
].forEach(opt => {
powerSelect.append("option")
.attr("value", opt.value)
.text(opt.label);
});
// Infrastructure investment
const infraGroup = reqPanel.append("div")
.style("margin-bottom", "20px");
infraGroup.append("label")
.style("display", "block")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("margin-bottom", "8px")
.style("font-size", "13px")
.text("Infrastructure Investment");
const infraSelect = infraGroup.append("select")
.style("width", "100%")
.style("padding", "10px")
.style("border-radius", "8px")
.style("border", `1px solid ${config.colors.gray}`)
.style("font-size", "13px");
[
{ value: "low", label: "Minimal (use existing)" },
{ value: "medium", label: "Moderate (some new)" },
{ value: "high", label: "Significant (new deployment)" }
].forEach(opt => {
infraSelect.append("option")
.attr("value", opt.value)
.text(opt.label);
});
// Hybrid mode toggle
const hybridGroup = reqPanel.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "10px")
.style("padding-top", "15px")
.style("border-top", `1px solid ${config.colors.gray}`);
const hybridCheckbox = hybridGroup.append("input")
.attr("type", "checkbox")
.attr("id", "hybridMode")
.style("width", "18px")
.style("height", "18px");
hybridGroup.append("label")
.attr("for", "hybridMode")
.style("font-size", "13px")
.style("color", config.colors.navy)
.style("cursor", "pointer")
.text("Enable Hybrid Solution Builder");
// ---------------------------------------------------------------------------
// RADAR CHART
// ---------------------------------------------------------------------------
const chartPanel = rightColumn.append("div")
.style("background", config.colors.white)
.style("border", `1px solid ${config.colors.lightGray}`)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "20px");
chartPanel.append("div")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("font-size", "16px")
.style("margin-bottom", "15px")
.text("Technology Comparison");
const radarSvg = chartPanel.append("svg")
.attr("viewBox", "0 0 400 350")
.attr("width", "100%")
.style("max-width", "400px")
.style("display", "block")
.style("margin", "0 auto");
// ---------------------------------------------------------------------------
// SUITABILITY SCORES
// ---------------------------------------------------------------------------
const scoresPanel = rightColumn.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "20px");
scoresPanel.append("div")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("font-size", "16px")
.style("margin-bottom", "15px")
.text("Suitability Scores");
const scoresList = scoresPanel.append("div");
// ---------------------------------------------------------------------------
// RECOMMENDATION PANEL
// ---------------------------------------------------------------------------
const recPanel = rightColumn.append("div")
.style("background", config.colors.white)
.style("border", `2px solid ${config.colors.teal}`)
.style("border-radius", "12px")
.style("padding", "20px");
recPanel.append("div")
.style("font-weight", "600")
.style("color", config.colors.teal)
.style("font-size", "16px")
.style("margin-bottom", "15px")
.text("Recommendation");
const recContent = recPanel.append("div");
// ---------------------------------------------------------------------------
// COST ESTIMATION
// ---------------------------------------------------------------------------
const costPanel = leftColumn.append("div")
.style("background", config.colors.white)
.style("border", `1px solid ${config.colors.lightGray}`)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-top", "20px");
costPanel.append("div")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("font-size", "14px")
.style("margin-bottom", "15px")
.text("Cost Estimation");
const costDeviceGroup = costPanel.append("div")
.style("margin-bottom", "15px");
costDeviceGroup.append("label")
.style("display", "block")
.style("font-size", "12px")
.style("color", config.colors.gray)
.style("margin-bottom", "5px")
.text("Number of Devices:");
const deviceCountInput = costDeviceGroup.append("input")
.attr("type", "number")
.attr("min", "1")
.attr("value", "100")
.style("width", "100%")
.style("padding", "8px")
.style("border-radius", "6px")
.style("border", `1px solid ${config.colors.gray}`)
.style("box-sizing", "border-box");
const costEstimate = costPanel.append("div")
.style("font-size", "13px");
// ---------------------------------------------------------------------------
// HELPER FUNCTIONS
// ---------------------------------------------------------------------------
function calculateSuitability(tech) {
let score = 0;
let maxScore = 0;
// Environment (weight: 25%)
const envWeight = 25;
if (state.environment === "indoor") {
score += tech.indoor * (envWeight / 10);
} else if (state.environment === "outdoor") {
score += tech.outdoor * (envWeight / 10);
} else {
score += ((tech.indoor + tech.outdoor) / 2) * (envWeight / 10);
}
maxScore += envWeight;
// Accuracy (weight: 25%)
const accWeight = 25;
const accMap = { "cm": "accuracyCm", "1m": "accuracyM", "10m": "accuracy10m", "100m": "accuracy100m" };
score += tech[accMap[state.accuracy]] * (accWeight / 10);
maxScore += accWeight;
// Update rate (weight: 20%)
const updateWeight = 20;
const updateMap = { "fast": "updateFast", "medium": "updateMedium", "slow": "updateSlow" };
score += tech[updateMap[state.updateRate]] * (updateWeight / 10);
maxScore += updateWeight;
// Power (weight: 15%)
const powerWeight = 15;
const powerMap = { "low": "powerLow", "medium": "powerMedium", "high": "powerHigh" };
score += tech[powerMap[state.power]] * (powerWeight / 10);
maxScore += powerWeight;
// Infrastructure (weight: 15%)
const infraWeight = 15;
const infraMap = { "low": "infraLow", "medium": "infraMedium", "high": "infraHigh" };
score += tech[infraMap[state.infrastructure]] * (infraWeight / 10);
maxScore += infraWeight;
return Math.round((score / maxScore) * 100);
}
function drawRadarChart() {
radarSvg.selectAll("*").remove();
const width = 400;
const height = 350;
const centerX = width / 2;
const centerY = height / 2 + 20;
const radius = 120;
const axes = [
{ name: "Indoor", key: "indoor" },
{ name: "Outdoor", key: "outdoor" },
{ name: "Accuracy", key: state.accuracy === "cm" ? "accuracyCm" : state.accuracy === "1m" ? "accuracyM" : state.accuracy === "10m" ? "accuracy10m" : "accuracy100m" },
{ name: "Update Rate", key: state.updateRate === "fast" ? "updateFast" : state.updateRate === "medium" ? "updateMedium" : "updateSlow" },
{ name: "Power Eff.", key: state.power === "low" ? "powerLow" : state.power === "medium" ? "powerMedium" : "powerHigh" },
{ name: "Low Infra", key: state.infrastructure === "low" ? "infraLow" : state.infrastructure === "medium" ? "infraMedium" : "infraHigh" }
];
const angleStep = (2 * Math.PI) / axes.length;
// Draw grid circles
[2, 4, 6, 8, 10].forEach(level => {
const r = (level / 10) * radius;
radarSvg.append("circle")
.attr("cx", centerX)
.attr("cy", centerY)
.attr("r", r)
.attr("fill", "none")
.attr("stroke", config.colors.lightGray)
.attr("stroke-width", level === 10 ? 2 : 1);
if (level % 2 === 0) {
radarSvg.append("text")
.attr("x", centerX + 5)
.attr("y", centerY - r - 2)
.attr("font-size", "9px")
.attr("fill", config.colors.gray)
.text(level);
}
});
// Draw axis lines and labels
axes.forEach((axis, i) => {
const angle = i * angleStep - Math.PI / 2;
const x = centerX + Math.cos(angle) * radius;
const y = centerY + Math.sin(angle) * radius;
radarSvg.append("line")
.attr("x1", centerX)
.attr("y1", centerY)
.attr("x2", x)
.attr("y2", y)
.attr("stroke", config.colors.lightGray)
.attr("stroke-width", 1);
const labelX = centerX + Math.cos(angle) * (radius + 25);
const labelY = centerY + Math.sin(angle) * (radius + 25);
radarSvg.append("text")
.attr("x", labelX)
.attr("y", labelY)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("font-size", "11px")
.attr("fill", config.colors.navy)
.attr("font-weight", "500")
.text(axis.name);
});
// Draw technology polygons
const techsToShow = state.hybridMode ?
technologies.filter(t => state.selectedTechs.includes(t.id)) :
technologies.slice(0, 4);
techsToShow.forEach((tech, techIdx) => {
const points = axes.map((axis, i) => {
const value = tech[axis.key] || 0;
const angle = i * angleStep - Math.PI / 2;
const r = (value / 10) * radius;
return {
x: centerX + Math.cos(angle) * r,
y: centerY + Math.sin(angle) * r
};
});
const pathD = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ') + ' Z';
radarSvg.append("path")
.attr("d", pathD)
.attr("fill", tech.color)
.attr("fill-opacity", 0.15)
.attr("stroke", tech.color)
.attr("stroke-width", 2);
// Data points
points.forEach(p => {
radarSvg.append("circle")
.attr("cx", p.x)
.attr("cy", p.y)
.attr("r", 4)
.attr("fill", tech.color);
});
});
// Legend
const legendY = 20;
techsToShow.forEach((tech, i) => {
const legendG = radarSvg.append("g")
.attr("transform", `translate(${50 + i * 90}, ${legendY})`);
legendG.append("rect")
.attr("width", 12)
.attr("height", 12)
.attr("fill", tech.color)
.attr("rx", 2);
legendG.append("text")
.attr("x", 16)
.attr("y", 10)
.attr("font-size", "10px")
.attr("fill", config.colors.navy)
.text(tech.name);
});
}
function updateScores() {
scoresList.html("");
const scores = technologies.map(tech => ({
...tech,
score: calculateSuitability(tech)
})).sort((a, b) => b.score - a.score);
scores.forEach((tech, i) => {
const row = scoresList.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "12px")
.style("padding", "10px")
.style("margin-bottom", "8px")
.style("background", i === 0 ? d3.color(tech.color).brighter(1.8) : config.colors.white)
.style("border-radius", "8px")
.style("border", `2px solid ${i === 0 ? tech.color : 'transparent'}`);
// Rank
row.append("div")
.style("width", "24px")
.style("height", "24px")
.style("border-radius", "50%")
.style("background", i < 3 ? tech.color : config.colors.gray)
.style("color", config.colors.white)
.style("display", "flex")
.style("align-items", "center")
.style("justify-content", "center")
.style("font-size", "12px")
.style("font-weight", "bold")
.text(i + 1);
// Name
row.append("div")
.style("flex", "1")
.style("font-weight", "600")
.style("color", config.colors.navy)
.style("font-size", "13px")
.text(tech.name);
// Score bar
const barContainer = row.append("div")
.style("width", "80px")
.style("height", "8px")
.style("background", config.colors.lightGray)
.style("border-radius", "4px")
.style("overflow", "hidden");
barContainer.append("div")
.style("width", `${tech.score}%`)
.style("height", "100%")
.style("background", tech.color)
.style("transition", "width 0.3s ease");
// Score number
row.append("div")
.style("width", "35px")
.style("text-align", "right")
.style("font-weight", "bold")
.style("color", tech.color)
.style("font-size", "14px")
.text(`${tech.score}%`);
// Hybrid checkbox (if in hybrid mode)
if (state.hybridMode) {
const checkbox = row.append("input")
.attr("type", "checkbox")
.attr("checked", state.selectedTechs.includes(tech.id) ? true : null)
.style("width", "16px")
.style("height", "16px")
.on("change", function() {
if (this.checked) {
if (!state.selectedTechs.includes(tech.id)) {
state.selectedTechs.push(tech.id);
}
} else {
state.selectedTechs = state.selectedTechs.filter(t => t !== tech.id);
}
updateUI();
});
}
});
}
function updateRecommendation() {
const scores = technologies.map(tech => ({
...tech,
score: calculateSuitability(tech)
})).sort((a, b) => b.score - a.score);
const best = scores[0];
const second = scores[1];
recContent.html("");
// Primary recommendation
const primaryRec = recContent.append("div")
.style("margin-bottom", "15px");
primaryRec.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "10px")
.style("margin-bottom", "8px")
.html(`
<div style="width: 12px; height: 12px; border-radius: 2px; background: ${best.color};"></div>
<span style="font-weight: 700; font-size: 18px; color: ${config.colors.navy};">${best.name}</span>
<span style="background: ${config.colors.green}; color: white; padding: 2px 8px; border-radius: 10px; font-size: 11px;">Best Match</span>
`);
primaryRec.append("div")
.style("font-size", "13px")
.style("color", config.colors.gray)
.style("margin-bottom", "10px")
.text(best.description);
// Pros and cons
const prosConsGrid = primaryRec.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "10px")
.style("font-size", "12px");
const prosDiv = prosConsGrid.append("div");
prosDiv.append("div")
.style("color", config.colors.green)
.style("font-weight", "600")
.style("margin-bottom", "5px")
.text("Advantages:");
best.pros.forEach(pro => {
prosDiv.append("div")
.style("color", config.colors.navy)
.text("+ " + pro);
});
const consDiv = prosConsGrid.append("div");
consDiv.append("div")
.style("color", config.colors.red)
.style("font-weight", "600")
.style("margin-bottom", "5px")
.text("Limitations:");
best.cons.forEach(con => {
consDiv.append("div")
.style("color", config.colors.navy)
.text("- " + con);
});
// Alternative
if (second.score >= 50) {
recContent.append("div")
.style("padding-top", "15px")
.style("border-top", `1px solid ${config.colors.lightGray}`)
.style("font-size", "13px")
.html(`
<span style="color: ${config.colors.gray};">Alternative: </span>
<span style="font-weight: 600; color: ${config.colors.navy};">${second.name}</span>
<span style="color: ${config.colors.gray};"> (${second.score}% match)</span>
`);
}
// Hybrid suggestion
if (best.score < 80 || state.hybridMode) {
recContent.append("div")
.style("margin-top", "15px")
.style("padding", "12px")
.style("background", config.colors.lightGray)
.style("border-radius", "8px")
.style("font-size", "12px")
.html(`
<strong style="color: ${config.colors.purple};">Consider a Hybrid Approach</strong><br>
<span style="color: ${config.colors.gray};">
Combine ${best.name} with ${second.name} for better coverage.
Enable "Hybrid Solution Builder" to compare multiple technologies.
</span>
`);
}
}
function updateCostEstimate() {
const deviceCount = parseInt(deviceCountInput.property("value")) || 100;
const scores = technologies.map(tech => ({
...tech,
score: calculateSuitability(tech)
})).sort((a, b) => b.score - a.score);
const best = scores[0];
const deviceCost = best.costPerDevice * deviceCount;
const infraCost = best.costInfrastructure;
const totalCost = deviceCost + infraCost;
costEstimate.html(`
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span style="color: ${config.colors.gray};">Device cost (${deviceCount}x):</span>
<span style="color: ${config.colors.navy}; font-weight: 600;">$${deviceCost.toLocaleString()}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
<span style="color: ${config.colors.gray};">Infrastructure:</span>
<span style="color: ${config.colors.navy}; font-weight: 600;">$${infraCost.toLocaleString()}</span>
</div>
<div style="display: flex; justify-content: space-between; padding-top: 8px; border-top: 1px solid ${config.colors.lightGray};">
<span style="color: ${config.colors.navy}; font-weight: 600;">Estimated Total:</span>
<span style="color: ${config.colors.teal}; font-weight: 700; font-size: 16px;">$${totalCost.toLocaleString()}</span>
</div>
<div style="margin-top: 8px; font-size: 11px; color: ${config.colors.gray}; font-style: italic;">
*Based on ${best.name} - estimates are approximate
</div>
`);
}
function updateUI() {
// Update environment buttons
envBtns.selectAll("button").each(function() {
const btn = d3.select(this);
const env = btn.attr("data-env");
const isSelected = state.environment === env;
btn.style("background", isSelected ? config.colors.teal : config.colors.white)
.style("color", isSelected ? config.colors.white : config.colors.teal)
.style("border-color", config.colors.teal);
});
// Update selects
accSelect.property("value", state.accuracy);
updateSelect.property("value", state.updateRate);
powerSelect.property("value", state.power);
infraSelect.property("value", state.infrastructure);
hybridCheckbox.property("checked", state.hybridMode);
// Update visualizations
drawRadarChart();
updateScores();
updateRecommendation();
updateCostEstimate();
}
// ---------------------------------------------------------------------------
// EVENT HANDLERS
// ---------------------------------------------------------------------------
envBtns.selectAll("button").on("click", function() {
state.environment = d3.select(this).attr("data-env");
updateUI();
});
accSelect.on("change", function() {
state.accuracy = this.value;
updateUI();
});
updateSelect.on("change", function() {
state.updateRate = this.value;
updateUI();
});
powerSelect.on("change", function() {
state.power = this.value;
updateUI();
});
infraSelect.on("change", function() {
state.infrastructure = this.value;
updateUI();
});
hybridCheckbox.on("change", function() {
state.hybridMode = this.checked;
if (this.checked && state.selectedTechs.length === 0) {
state.selectedTechs = ["ble", "uwb"];
}
updateUI();
});
deviceCountInput.on("input", updateCostEstimate);
// ---------------------------------------------------------------------------
// INITIAL RENDER
// ---------------------------------------------------------------------------
updateUI();
return container.node();
}1508.2 Understanding Location Technologies for IoT
1508.2.1 Technology Comparison Overview
| Technology | Best For | Accuracy | Power | Infrastructure |
|---|---|---|---|---|
| GPS/GNSS | Outdoor tracking | 3-10m | High | None |
| Wi-Fi Positioning | Indoor with existing Wi-Fi | 3-15m | Medium | Low-Medium |
| BLE Beacons | Indoor asset tracking | 1-3m | Very Low | Medium |
| UWB | Precision positioning | 10-30cm | Medium | High |
| LoRa | Rural/wide area | 50-200m | Very Low | Medium |
| Cellular | Fleet tracking | 50-500m | Medium | None |
| RFID | Inventory/zone detection | Zone-based | Passive | High |
1508.2.2 Accuracy Classes
Understanding accuracy requirements is crucial for technology selection:
TipAccuracy Guidelines
- Centimeter (< 10 cm): Manufacturing, robotics, precision agriculture
- Sub-meter (< 1 m): Indoor navigation, personnel tracking
- Room-level (< 10 m): Asset tracking, warehouse management
- Zone/Area (< 100 m): Fleet tracking, large facility monitoring
1508.2.3 Hybrid Solutions
Many real-world deployments combine multiple technologies:
- GPS + BLE: Outdoor tracking with indoor handoff
- Wi-Fi + UWB: Building-wide coverage with precision zones
- LoRa + GPS: Rural tracking with periodic precise fixes
- RFID + BLE: Inventory management with location awareness
1508.2.4 Cost Considerations
| Factor | Impact |
|---|---|
| Device Cost | Per-unit hardware cost varies from $0.50 (RFID tags) to $15+ (UWB) |
| Infrastructure | BLE beacons are cheap; UWB anchors are expensive |
| Installation | Site surveys, calibration, and mounting add costs |
| Maintenance | Battery replacement, calibration updates |
| Data/Connectivity | Cellular requires ongoing data plans |
1508.2.5 Selection Criteria
When choosing a location technology, consider:
- Environment: Indoor vs outdoor vs both
- Accuracy: How precise must locations be?
- Update Rate: Real-time vs periodic updates
- Power Budget: Battery life requirements
- Scale: Number of tracked assets
- Existing Infrastructure: What’s already deployed?
- Budget: Total cost of ownership
1508.3 Related Topics
- Location Awareness - Comprehensive location concepts
- BLE Beacons Animation - BLE beacon technology
- UWB TWR Animation - Ultra-Wideband positioning
- LoRaWAN Protocols - LPWAN positioning
Interactive tool created for the IoT Class Textbook - Human Factors and Interaction Module