diff --git a/src/components/managed-street.js b/src/components/managed-street.js index 4e7c89b4e..285ce8d88 100644 --- a/src/components/managed-street.js +++ b/src/components/managed-street.js @@ -5,6 +5,206 @@ const { segmentVariants } = require('../segments-variants.js'); const streetmixUtils = require('../tested/streetmix-utils'); const streetmixParsersTested = require('../tested/aframe-streetmix-parsers-tested'); +// STREETPLAN HELPER FUNCTIONS +// Material mapping from Streetplan to 3DStreet surfaces +const STREETPLAN_MATERIAL_MAPPING = { + 'asphalt black': { surface: 'asphalt', color: '#aaaaaa' }, + 'asphalt blue': { surface: 'asphalt', color: '#aaaaff' }, + 'asphalt red 1': { surface: 'asphalt', color: '#ffaaaa' }, + 'asphalt red 2': { surface: 'asphalt', color: '#ff0000' }, + 'asphalt green': { surface: 'asphalt', color: '#aaffaa' }, + 'asphalt old': { surface: 'asphalt' }, + 'standard concrete': { surface: 'concrete' }, + grass: { surface: 'grass' }, + 'grass dead': { surface: 'grass' }, + 'pavers tan': { surface: 'sidewalk' }, + 'pavers brown': { surface: 'sidewalk' }, + 'pavers mixed': { surface: 'sidewalk' }, + 'pavers red': { surface: 'sidewalk', color: '#ffaaaa' }, + 'tint conc. or dirt': { surface: 'gravel' }, + dirt: { surface: 'gravel' }, + gravel: { surface: 'gravel' }, + stonetan: { surface: 'sidewalk' }, + 'sidewalk 2': { surface: 'sidewalk' }, + 'cobble stone': { surface: 'sidewalk' }, + 'solid black': { surface: 'solid' }, + 'painted intersection': { surface: 'asphalt' }, + 'grass with edging': { surface: 'grass' }, + xeriscape: { surface: 'grass' }, + 'grassslopemedian 12ft': { surface: 'grass' }, + 'grassslopemedian 24ft': { surface: 'grass' }, + 'grassslope 12ft-left': { surface: 'grass' }, + 'grassslope 12ft-right': { surface: 'grass' }, + 'grassslope 24ft-left': { surface: 'grass' }, + 'grassslope 24ft-right': { surface: 'grass' }, + sand: { surface: 'sand' } +}; + +const STREETPLAN_OBJECT_MAPPING = { + 'away, left park, head in': '', + 'barrier 1-ft': 'temporary-jersey-barrier-concrete', + 'barrier 2-ft': 'temporary-jersey-barrier-concrete', + 'bike food cart': '', + 'bikelane sharecar': '', + 'bikerack bollard': '', + 'blank pedrefuge (8ft)': '', + 'blue car': 'sedan-rig', + 'blue mailbox': 'usps-mailbox', + 'bollard plastic yellow': 'bollard', + boulevardcirculator: 'minibus', + 'boulevardcirculator rev': 'minibus', + 'boxwood planter 2ft': 'dividers-planter-box', + 'boxwood planter 3ft': 'dividers-planter-box', + 'boxwood planter 5ft': 'dividers-planter-box', + 'bur oak': 'tree3', + bus: 'bus', + 'bus rev': 'bus', + 'cactus median (10ft)': 'dividers-bush', + 'cactus median (12ft)': 'dividers-bush', + 'cactus median (4ft)': 'dividers-bush', + 'cactus median (6ft)': 'dividers-bush', + 'cactus median (8ft)': 'dividers-bush', + 'casual woman': '', + couple: '', + 'couple biking': '', + 'desertwillow texas': 'tree3', + 'dog walker': '', + 'empty place holder': '', + 'english oak': 'tree3', + 'fleamarket stuff': '', + 'flower median (10ft)': 'dividers-flowers', + 'flower median (12ft)': 'dividers-flowers', + 'flower median (4ft)': 'dividers-flowers', + 'flower median (6ft)': 'dividers-flowers', + 'flower median (8ft)': 'dividers-flowers', + 'flower pot 4ft': 'dividers-flowers', + 'floweringpear 18ft': 'tree3', + 'flowers pedrefuge (8ft)': 'dividers-flowers', + goldenraintree: 'tree3', + 'golfcart red 4ft back': 'tuk-tuk', + 'grassmound (10ft)': '', + 'grassmound (12ft)': '', + 'grassmound (4ft)': '', + 'grassmound (6ft)': '', + 'grassmound (8ft)': '', + 'grassy median (10ft)': '', + 'grassy median (12ft)': '', + 'grassy median (4ft)': '', + 'grassy median (6ft)': '', + 'grassy median (8ft)': '', + 'green car': 'sedan-rig', + 'heavy rail': 'tram', + 'heavy rail rev': 'tram', + 'historic light': 'lamp-traditional', + 'historic no banner': 'lamp-traditional', + 'historic with banners': 'lamp-traditional', + 'historic with flowers 1': 'lamp-traditional', + 'historic with flowers 2': 'lamp-traditional', + honeylocust: 'tree3', + 'japanese lilac': 'tree3', + 'japanese zelkova': 'tree3', + 'jerusalem thorn': 'tree3', + 'kentucky coffeetree': 'tree3', + 'large food cart': '', + 'large oak': 'tree3', + 'light rail poles': '', + 'moto highway rider': 'motorbike', + 'mountable barrier 1-ft': '', + 'nev shuttle back': 'minibus', + 'nev shuttle front': 'minibus', + 'nyc bike rack': 'bikerack', + 'orange barrel': 'temporary-traffic-cone', + 'palm tree': 'palm-tree', + 'palmtree 20ft': 'palm-tree', + 'palmtree 28ft': 'palm-tree', + 'pine tree': 'tree3', + 'pink flower 16ft': 'tree3', + 'planter flowers': 'dividers-flowers', + 'planter with bench': 'bench', + 'polaris gem e4': 'tuk-tuk', + 'power tower 30ft': '', + 'purpendicular right side, blue': '', + 'purpendicular right side, red': '', + 'purpleleaf plum': 'tree3', + 'random trashcan': 'trash-bin', + 'red berries 14ft': 'tree3', + 'red car': 'sedan-rig', + 'red jeep': 'suv-rig', + 'rock median (10ft)': '', + 'rock median (12ft)': '', + 'rock median (4ft)': '', + 'rock median (6ft)': '', + 'rock median (8ft)': '', + 'semi truck': 'box-truck-rig', + 'serious man': '', + shelter: 'bus-stop', + 'shelter roundroof': 'bus-stop', + 'sign directory': 'wayfinding', + 'silver suv': 'suv-rig', + 'small tree': 'tree3', + smallnev: 'minibus', + 'smartcar 5ft': 'self-driving-cruise-car-rig', + 'soundwall (12ft)': '', + 'soundwall (8ft)': '', + 'soundwall plants (12ft)': '', + 'soundwall plants (8ft)': '', + 'street light': 'lamp-modern', + 'streetcar blue': 'trolley', + 'streetcar red 1': 'trolley', + 'streetcar red 2': 'trolley', + 'streetcar yellow': 'trolley', + 'streetlight solar': 'lamp-modern', + 'streetlight solar banners 1': 'lamp-modern', + 'streetlight solar banners 2': 'lamp-modern', + tallgrass: '', + 'tallplantbox (10ft)': '', + 'tallplantbox (12ft)': 'dividers-bush', + 'tallplantbox (4ft)': '', + 'tallplantbox (6ft)': '', + 'tallplantbox (8ft)': '', + 'tallplantbox pedref (10ft)': '', + 'tallplantbox pedref (12ft)': '', + 'tallplantbox pedref (6ft)': '', + 'tallplantbox pedref (8ft)': '', + 'telephone pole': 'utility_pole', + 'tent bluewhite': '', + 'tent veggie': '', + 'toward, right park, head in': '', + trashcan: 'trash-bin', + 'tropical median (4ft)': 'palm-tree', + 'two bikes back': '', + 'uta bus': 'bus', + 'uta lightrail': 'tram', + 'uta lightrail rev': 'tram', + 'weeds median (4ft)': '', + 'weeds median (6ft)': '', + 'weeds median (8ft)': '', + 'white coup': 'sedan-rig', + 'white sedan': 'sedan-rig', + 'white truck': 'box-truck-rig', + 'yellow sedan': 'sedan-rig' +}; + +// Streetplan Helper function to parse O-Tags string into array +function parseOTags(tags) { + if (!tags || tags === '-') return []; + return tags.split('", "').map((t) => t.replace(/"/g, '').trim()); +} + +// Streetplan Helper function to create clone configuration +function createCloneConfig(name, tags) { + if (!name || name === '-') return null; + + const model = STREETPLAN_OBJECT_MAPPING[name.toLowerCase()]; + if (!model) return null; + + return { + mode: 'fixed', // default to fixed mode + model: model, + spacing: 15 // default spacing + }; +} + AFRAME.registerComponent('managed-street', { schema: { width: { @@ -235,8 +435,7 @@ AFRAME.registerComponent('managed-street', { if (data.sourceType === 'streetmix-url') { this.loadAndParseStreetmixURL(data.sourceValue); } else if (data.sourceType === 'streetplan-url') { - // this function is not yet implemented - this.refreshFromStreetplanURL(data.sourceValue); + this.loadAndParseStreetplanURL(data.sourceValue); } else if (data.sourceType === 'json-blob') { // if data.sourceValue is a string convert string to object for parsing but keep string for saving if (typeof data.sourceValue === 'string') { @@ -324,6 +523,128 @@ AFRAME.registerComponent('managed-street', { }); } }, + loadAndParseStreetplanURL: async function (streetplanURL) { + console.log( + '[managed-street] loader', + 'sourceType: `streetplan-url`, loading from', + streetplanURL + ); + + try { + const response = await fetch(streetplanURL); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const streetplanData = await response.json(); + const boulevard = streetplanData.project['My Street']['Boulevard Alt 1']; + + const streetLength = + parseFloat(streetplanData.project['My Street'].LengthMiles) * + 5280 * + 0.3048 || 100; // Convert miles to meters + // Convert StreetPlan format to managed-street format + const streetObject = { + name: streetplanData.project.ProjectName, + width: 0, // Will be calculated from segments + length: streetLength, + segments: [] + }; + + // Process streetplan segments + const segments = boulevard.segments; + for (const segmentKey in segments) { + const segment = segments[segmentKey]; + + // Skip Buildings and Setback segments + if (segment.Type === 'Buildings' || segment.Type === 'Setback') { + continue; + } + + const segmentWidth = parseFloat(segment.width) * 0.3048; // Convert feet to meters + streetObject.width += segmentWidth; + + // Convert streetplan segment type based on your schema + let segmentType = 'drive-lane'; // Default type + let segmentDirection = 'inbound'; + + // convert from streetplan type to managed street default type + switch (segment.Type) { + case 'BikesPaths': + segmentType = 'bike-lane'; + break; + case 'Walkways': + segmentType = 'sidewalk'; + break; + case 'Transit': + segmentType = 'bus-lane'; + break; + case 'Median/Buffer': + segmentType = 'divider'; + break; + case 'Curbside': + segmentType = 'divider'; + break; + case 'Gutter': + segmentType = 'parking-lane'; + break; + case 'Furniture': + segmentType = 'sidewalk-tree'; + break; + // Add more type mappings as needed + } + + // Determine direction based on segment data + if (segment.Direction === 'Coming') { + segmentDirection = 'inbound'; + } else if (segment.Direction === 'Going') { + segmentDirection = 'outbound'; + } + + // Map the material using the STREETPLAN_MATERIAL_MAPPING, fallback to 'asphalt' if not found + const material = segment.Material?.toLowerCase() || ''; + const mappedSurface = + STREETPLAN_MATERIAL_MAPPING[material]?.surface || 'asphalt'; + const mappedColor = STREETPLAN_MATERIAL_MAPPING[material]?.color; + + // Map the O-Tags to clone configurations + const generated = {}; + const clones = []; + // Process O1, O2, O3 configurations + ['O1', 'O2', 'O3'].forEach((prefix) => { + const name = segment[`${prefix}-Name`]; + const tags = parseOTags(segment[`${prefix}-Tags`]); + const cloneConfig = createCloneConfig(name, tags); + if (cloneConfig) { + clones.push(cloneConfig); + } + }); + if (clones.length > 0) { + generated.clones = clones; + } + + streetObject.segments.push({ + type: segmentType, + width: segmentWidth, + name: segment.title, + level: parseFloat(segment.MaterialH) === 0.5 ? 1 : 0, + direction: segmentDirection, + color: mappedColor || window.STREET.types[segmentType]?.color, + surface: mappedSurface, + generated: clones.length > 0 ? generated : undefined + }); + } + + // Parse the street object + this.parseStreetObject(streetObject); + } catch (error) { + console.error('[managed-street] loader', 'Loading Error:', error); + STREET.notify.warningMessage( + 'Error loading StreetPlan data: ' + error.message + ); + } + }, + getStripingFromSegments: function (previousSegment, currentSegment) { if (!previousSegment || !currentSegment) { return null; diff --git a/src/components/streetplan-loader.js b/src/components/streetplan-loader.js deleted file mode 100644 index 8954d3447..000000000 --- a/src/components/streetplan-loader.js +++ /dev/null @@ -1,140 +0,0 @@ -/* global AFRAME, XMLHttpRequest */ -import useStore from '../store.js'; -var streetplanUtils = require('../streetplan/streetplan-utils.js'); - -const state = useStore.getState(); - -AFRAME.registerComponent('streetplan-loader', { - dependencies: ['street'], - schema: { - streetplanStreetURL: { type: 'string' }, - streetplanAPIURL: { type: 'string' }, - streetplanEncJSON: { type: 'string' }, - showBuildings: { default: true }, - name: { default: '' }, - synchronize: { default: false } - }, - streetplanResponseParse: function (streetplanResponseObject) { - const el = this.el; - const data = this.data; - if (!streetplanResponseObject || !streetplanResponseObject.project) { - console.error('[streetplan-loader] Invalid streetplan data structure'); - return; - } - try { - // convert Streetplan structure to Streetmix-like structure - const streetData = streetplanUtils.convertStreetStruct( - streetplanResponseObject - ); - - const streetplanSegments = streetData.segments; - const streetplanName = streetData.name; - // const projectName = streetData.projectName || streetplanName; - - // Update layer name with project and street names - el.setAttribute('data-layer-name', `StreetPlan • ${streetplanName}`); - - if (!state.sceneTitle) { - state.setSceneTitle(streetplanName); - } - - // Handle buildings if enabled - if (data.showBuildings) { - // Find building segments in the full data - const buildingSegments = streetplanSegments.filter( - (segment) => segment.type === 'Buildings' - ); - - // Set building variants based on side - const leftBuilding = buildingSegments.find((b) => b.side === 'left'); - const rightBuilding = buildingSegments.find((b) => b.side === 'right'); - - if (leftBuilding) { - el.setAttribute('street', 'left', leftBuilding.title); - } - if (rightBuilding) { - el.setAttribute('street', 'right', rightBuilding.title); - } - } - // Set street type - el.setAttribute('street', 'type', 'streetmixSegmentsMetric'); - - // Filter out building segments for the main street data if needed - const finalSegments = data.showBuildings - ? streetplanSegments.filter((s) => s.type !== 'Buildings') - : streetplanSegments; - - // Set JSON attribute last - el.setAttribute( - 'street', - 'JSON', - JSON.stringify({ streetmixSegmentsMetric: finalSegments }) - ); - el.emit('streetplan-loader-street-loaded'); - } catch (error) { - console.error('[streetplan-loader] Error parsing street data:', error); - el.emit('streetplan-loader-error', { error }); - } - }, - init: function () { - this.el.setAttribute('streetplan-loader', 'synchronize', true); - }, - update: function (oldData) { - const data = this.data; - - // Skip update if synchronization is disabled - if (!data.synchronize) return; - - // Handle URL encoded JSON data - if (data.streetplanEncJSON) { - try { - const streetplanJSON = decodeURIComponent(data.streetplanEncJSON); - this.streetplanResponseParse(JSON.parse(streetplanJSON)); - } catch (error) { - console.error('[streetplan-loader] Error parsing encoded JSON:', error); - this.el.emit('streetplan-loader-error', { error }); - } - return; - } - - // Skip if URLs haven't changed - if ( - oldData.streetplanStreetURL === data.streetplanStreetURL && - oldData.streetplanAPIURL === data.streetplanAPIURL - ) { - return; - } - - // Load from API - const request = new XMLHttpRequest(); - console.log('[streetplan-loader]', 'GET ' + data.streetplanAPIURL); - - request.open('GET', data.streetplanAPIURL, true); - request.onload = () => { - if (request.status >= 200 && request.status < 400) { - try { - const streetplanResponseObject = JSON.parse(request.response); - this.streetplanResponseParse(streetplanResponseObject); - this.el.setAttribute('streetplan-loader', 'synchronize', false); - } catch (error) { - console.error( - '[streetplan-loader] Error parsing API response:', - error - ); - this.el.emit('streetplan-loader-error', { error }); - } - } else { - const error = new Error(`Server returned status ${request.status}`); - console.error('[streetplan-loader] API request failed:', error); - this.el.emit('streetplan-loader-error', { error }); - } - }; - - request.onerror = () => { - const error = new Error('Network request failed'); - console.error('[streetplan-loader] Network error:', error); - this.el.emit('streetplan-loader-error', { error }); - }; - request.send(); - } -}); diff --git a/src/editor/components/components/AddLayerPanel/createLayerFunctions.js b/src/editor/components/components/AddLayerPanel/createLayerFunctions.js index 047f71e80..6125766d9 100644 --- a/src/editor/components/components/AddLayerPanel/createLayerFunctions.js +++ b/src/editor/components/components/AddLayerPanel/createLayerFunctions.js @@ -77,6 +77,32 @@ export function createManagedStreetFromStreetmixURLPrompt(position) { } } +export function createManagedStreetFromStreetplanURLPrompt(position) { + // This creates a new Managed Street + let streetplanURL = prompt( + 'Please enter a StreetPlan URL', + 'https://streetplan.net/3dstreet/89474' + ); + + if (streetplanURL && streetplanURL !== '') { + const definition = { + id: createUniqueId(), + components: { + position: position ?? '0 0 0', + 'managed-street': { + sourceType: 'streetplan-url', + sourceValue: streetplanURL, + showVehicles: true, + showStriping: true, + synchronize: true + } + } + }; + + AFRAME.INSPECTOR.execute('entitycreate', definition); + } +} + export function createManagedStreetFromStreetObject(position, streetObject) { // This creates a new Managed Street if (streetObject && streetObject !== '') { diff --git a/src/editor/components/components/AddLayerPanel/layersData.js b/src/editor/components/components/AddLayerPanel/layersData.js index 6704097a4..fd21fd782 100644 --- a/src/editor/components/components/AddLayerPanel/layersData.js +++ b/src/editor/components/components/AddLayerPanel/layersData.js @@ -95,6 +95,15 @@ export const streetLayersData = [ icon: 'ui_assets/cards/icons/3dst24.png', description: 'Premade Street 150ft Right of Way / 124ft Roadway Width', handlerFunction: createFunctions.create150ftRightOfWayManagedStreet + }, + { + name: '(Beta) Managed Street from Streetplan URL', + img: '', + requiresPro: true, + icon: '', + description: + 'Create a new street from Streetplan URL using the Managed Street component.', + handlerFunction: createFunctions.createManagedStreetFromStreetplanURLPrompt } ].map((layer, index) => ({ ...layer, id: index + 1 })); diff --git a/src/index.js b/src/index.js index b223b2adf..ae18771af 100644 --- a/src/index.js +++ b/src/index.js @@ -16,7 +16,6 @@ require('./components/notify.js'); require('./components/create-from-json'); require('./components/screentock.js'); require('aframe-atlas-uvs-component'); -require('./components/streetplan-loader'); require('./components/street-geo.js'); require('./components/street-environment.js'); require('./components/intersection.js'); diff --git a/src/json-utils_1.1.js b/src/json-utils_1.1.js index dac6e2cba..91bb719c9 100644 --- a/src/json-utils_1.1.js +++ b/src/json-utils_1.1.js @@ -1,4 +1,5 @@ import useStore from './store'; +import { createUniqueId } from './editor/lib/entity'; /* global AFRAME, Node */ /* version: 1.0 */ @@ -529,18 +530,30 @@ AFRAME.registerComponent('set-loader-from-hash', { streetURL ); } else if (streetURL.includes('streetplan.net/')) { - // load from Streetplan encoded JSON in URL + // instead, load streetplan via managed street the new addlayerpanel console.log( '[set-loader-from-hash]', - 'Set streetplan-loader streetplanAPIURL to', + 'Create new Managed Street with StreetPlan URL', streetURL ); + if (streetURL && streetURL !== '') { + const definition = { + id: createUniqueId(), + components: { + 'managed-street': { + sourceType: 'streetplan-url', + sourceValue: streetURL, + showVehicles: true, + showStriping: true, + synchronize: true + } + } + }; - this.el.setAttribute( - 'streetplan-loader', - 'streetplanAPIURL', - streetURL - ); + setTimeout(() => { + AFRAME.INSPECTOR.execute('entitycreate', definition); + }, 1000); + } } else { // try to load JSON file from remote resource console.log( diff --git a/src/streetplan/conversion-map.js b/src/streetplan/conversion-map.js deleted file mode 100644 index 82db4ded5..000000000 --- a/src/streetplan/conversion-map.js +++ /dev/null @@ -1,347 +0,0 @@ -// conversion map StreetPan -> Streetmix sidewalk segment types mapping -/* -StreetPlanType1: - { - StreetPlanSubtype: StreetmixType, - --- or --- - StreetPlanSubtype: { - "tag": StreetPlanTag, - "type": StreetmixType, - "variantString": Streetmix VariantString, can be formed based on other Streetplan parameters - (Name or Tag) or be constant, like: 'sidewalk', - - "variantStringAdd": get parameter values from this list and generate variantString. - Often variantString looks like this: 'outbound|regular|road' - example for bike-path. - variantStringAdd will be: 'direction|material|variantString', - - "nameToVariantMap": mapping rule StreetPlan O1-Name -> VariantString, - "tagToVariantMap": mapping rule StreetPlan O1-Tags -> VariantString, - "names": names (StreetPlan O1-Name) for this Streetmix Segment type - }, - --- or --- - // for one (O1-Tags) there can be different streetmix segment types, - // which are determined by the name (O1-Name) - StreetPlanSubtype: [ - different options of tags (O1-Tags) and streetMix data for each - ] - } -*/ -const mapping = { - Setback: { - '': { type: 'sidewalk', variantString: 'empty' }, - Trees: { type: 'sidewalk-tree', variantString: 'big' }, - tree: { type: 'divider', variantString: 'palm-tree' }, - Benchs: { type: 'sidewalk-bench', variantStringAdd: 'side' } - }, - Walkways: { - '': { type: 'sidewalk', variantString: 'empty' }, - Trees: { type: 'sidewalk-tree', variantString: 'big' }, - pedestrian: { type: 'sidewalk', variantString: 'dense' }, - Benchs: { type: 'sidewalk-bench', variantStringAdd: 'side' }, - Tables: { type: 'outdoor-dining', variantString: 'occupied|sidewalk' } - }, - Furniture: { - '': { type: 'sidewalk', variantString: 'empty' }, - Trees: { type: 'sidewalk-tree', variantString: 'big' }, - season_tree: { type: 'sidewalk-tree', variantString: 'big' }, - Shelters: { - type: 'transit-shelter', - variantString: 'street-level', - variantStringAdd: 'side|variantString' - }, - Pedestrian: { type: 'sidewalk', variantString: 'dense' } - }, - Curbside: { - '': { type: 'sidewalk', variantString: 'empty' }, - Lights: { - type: 'sidewalk-lamp', - tagToVariantMap: { - 'Historic Lights': 'traditional', - 'Regular Lights': 'modern' - }, - variantStringAdd: 'side|variantString' - }, - Poles: { type: 'utilities', variantStringAdd: 'side' }, - BikeRacks: { - type: 'sidewalk-bike-rack', - nameToVariantMap: { - 'Sideview Modern': 'sidewalk-parallel', - Sideview: 'sidewalk-parallel', - 'NYC Bike Rack': 'sidewalk' - }, - variantStringAdd: 'side|variantString' - } - }, - BikesPaths: { - '': { type: 'bike-lane', variantString: 'sidewalk' }, - Bikes: { - type: 'bike-lane', - variantString: 'sidewalk', - variantStringAdd: 'direction|material|variantString' - } - }, - Gutter: { - '': { type: 'divider', variantString: 'median' }, - Gutter: { type: 'divider', variantString: 'median' } - }, - Transit: { - '': { - tag: 'Bus Vehicles', - type: 'bus-lane', - variantString: 'typical', - variantStringAdd: 'direction|material|variantString' - }, - Transit: [ - { - tag: 'Rail Vehicles', - type: 'streetcar', - names: [ - 'StreetCar Yellow', - 'StreetCar Blue', - 'StreetCar Red 1', - 'StreetCar Red 2' - ], - variantStringAdd: 'direction|material' - }, - { - tag: 'Rail Vehicles', - type: 'light-rail', - names: ['UTA LightRail'], - variantStringAdd: 'direction|material' - }, - // there are only reversed light rail vehicles in Streetplan - { - tag: 'Rail Vehicles Reversed', - type: 'light-rail', - variantStringAdd: 'direction|material' - }, - { - tag: 'Bus Vehicles', - type: 'bus-lane', - variantString: 'typical', - variantStringAdd: 'direction|material|variantString' - } - ] - }, - Cars: { - '': { - type: 'drive-lane', - variantString: 'car', - variantStringAdd: 'direction|variantString' - }, - Autos: { - type: 'drive-lane', - variantString: 'car', - variantStringAdd: 'direction|variantString' - }, - Truck: { - type: 'drive-lane', - variantString: 'truck', - variantStringAdd: 'direction|variantString' - } - }, - Parking: { - '': { - tag: 'Parking - Parallel', - type: 'parking-lane', - variantStringAdd: 'direction|side' - }, - Parallel: { - tag: 'Parking - Parallel', - type: 'parking-lane', - variantStringAdd: 'direction|side' - }, - AngleNormal: { - tag: 'Parking - Angle', - type: 'parking-lane', - nameToVariantMap: { - 'Away, L. Park, Head In': 'angled-rear-left', - 'Toward, R. Park, Head In': 'angled-front-right', - 'Toward, L. Park, Head In': 'angled-front-left', - 'Away, R. Park, Head In': 'angled-rear-right' - }, - variantStringAdd: 'side' - }, - Perpendicular: { - type: 'parking-lane', - variantString: 'sideways', - variantStringAdd: 'variantString|side' - } - }, - Buffers: { - '': { type: 'divider', variantString: 'median' }, - Trees: { type: 'divider', variantString: 'big-tree' }, - tree: { type: 'divider', variantString: 'palm-tree' }, - season_tree: { type: 'divider', variantString: 'big-tree' }, - median: { type: 'divider', variantString: 'planting-strip' }, - planter: { type: 'divider', variantString: 'planting-strip' } - } -}; -// copy repeating rules -mapping['Buffers']['AngleNormal'] = mapping['Parking']['AngleNormal']; -mapping['Buffers']['Autos'] = mapping['Cars']['Autos']; -mapping['Buffers']['Purpendicular'] = mapping['Parking']['Perpendicular']; -mapping['Median/Buffer'] = mapping['Buffers']; -mapping['Setback']['tree'] = mapping['Buffers']['tree']; -mapping['Setback']['Trees'] = mapping['Buffers']['Trees']; -mapping['Setback']['season_tree'] = mapping['Buffers']['season_tree']; -// fix for typo Purpendicular -mapping['Parking']['Purpendicular'] = mapping['Parking']['Perpendicular']; -mapping['Setback']['Purpendicular'] = mapping['Parking']['Perpendicular']; -mapping['Setback']['AngleNormal'] = mapping['Parking']['AngleNormal']; -mapping['Setback']['planter'] = mapping['Buffers']['planter']; -mapping['Setback']['BikeRacks'] = mapping['Curbside']['BikeRacks']; -mapping['Setback']['Tables'] = mapping['Walkways']['Tables']; -mapping['Setback']['Poles'] = mapping['Curbside']['Poles']; - -mapping['Curbside']['Shelters'] = mapping['Furniture']['Shelters']; -mapping['Curbside']['Benchs'] = mapping['Walkways']['Benchs']; - -mapping['Furniture']['planter'] = mapping['Buffers']['planter']; -mapping['Furniture']['Benchs'] = mapping['Walkways']['Benchs']; -mapping['Furniture']['BikeRacks'] = mapping['Curbside']['BikeRacks']; -mapping['Furniture']['Tables'] = mapping['Walkways']['Tables']; - -const directionMap = { - Coming: 'inbound', - Going: 'outbound', - // make default outbound direction for both variant - Both: 'both', - NA: '' -}; - -const materialMap = { - 'Asphalt Black': 'regular', - 'Asphalt Blue': 'blue', - 'Asphalt Red 1': 'red', - 'Asphalt Red 2': 'red', - 'Asphalt Green': 'green', - 'Asphalt Old': 'regular', - Grass: 'grass', - 'Grass Dead': 'grass' -}; - -// StreetMix variantString often has additional parameters via |, for example: taxi|outbound|right -// generate a streetMix like variantString from the listed parameters in variantStringAdd -function generateVariantString(variantStringKeys, streetmixData) { - const variantString = variantStringKeys - .split('|') - .map((currKey) => streetmixData[currKey]) - .join('|'); - return variantString; -} - -function getDataFromSubtypeMap(convertRule, streetmixData, streetplanData) { - if (typeof convertRule === 'string') { - // convertRule is a Streetmix type. - // Later will add another options for this case - streetmixData['type'] = convertRule; - } else if (Array.isArray(convertRule)) { - // in this case, different segment subtype options - // are associated with the different Streetmix types - - // find the desired Streetmix segment data from the array by Streetplan tag and names(?) - const variantData = convertRule.find((element) => { - const tagValMatches = element['tag'] === streetplanData['O1-Tags']; - if (tagValMatches && element['names']) { - return element['names'].includes(streetplanData['O1-Name']); - } - return tagValMatches; - }); - - streetmixData['variantString'] = ''; - - const variantString = variantData['variantString']; - if (variantString && typeof variantString === 'string') { - streetmixData['variantString'] = variantString; - } - - // generate a streetMix like variantString from the listed parameter values - streetmixData['type'] = variantData['type']; - const variantStringKeys = variantData['variantStringAdd']; - if (variantStringKeys) { - streetmixData['variantString'] = generateVariantString( - variantStringKeys, - streetmixData - ); - } - } else if (typeof convertRule === 'object') { - // in this case, different variants of the segment subtype - // are associated with different variantString of the Streetmix segment - - streetmixData['type'] = convertRule['type']; - streetmixData['variantString'] = ''; - - const variantString = convertRule['variantString']; - if (variantString && typeof variantString === 'string') { - streetmixData['variantString'] = variantString; - } - - // get variantString from {"O1-Name" (StreetPlan Object Name) : variantString} mapping data - const nameToVariantMap = convertRule['nameToVariantMap']; - if (nameToVariantMap && nameToVariantMap[streetplanData['O1-Name']]) { - streetmixData['variantString'] = - nameToVariantMap[streetplanData['O1-Name']]; - } - - // get variantString from {"O1-Tags" (StreetPlan Tag) : variantString} mapping data - const tagToVariantMap = convertRule['tagToVariantMap']; - if (tagToVariantMap && tagToVariantMap[streetplanData['O1-Tags']]) { - streetmixData['variantString'] = - tagToVariantMap[streetplanData['O1-Tags']]; - } - - // generate a streetMix like variantString from the listed parameter values - const variantStringKeys = convertRule['variantStringAdd']; - if (variantStringKeys) { - streetmixData['variantString'] = generateVariantString( - variantStringKeys, - streetmixData - ); - } - } - - return streetmixData; -} - -// convert streetPlan segment data to Streetmix segment data -function convertSegment(data) { - let streetmixData = {}; - const streetplanType = data['Type']; - const streetplanSubtype = data['Subtype']; - // mapping rule for current Streetplan subtypes - const subtypeMap = mapping[streetplanType]; - - // convert elevation value to Streetmix format: 0, 1, 2 - streetmixData['elevation'] = data['MaterialH'] / 0.5; - streetmixData['width'] = data['width']; - streetmixData['direction'] = directionMap[data['Direction']]; - if (data['side']) { - streetmixData['side'] = data['side']; - } - if (data['Material']) { - streetmixData['material'] = materialMap[data['Material']]; - } - - if (subtypeMap) { - const convertRule = subtypeMap[streetplanSubtype]; - if (convertRule) { - streetmixData = getDataFromSubtypeMap(convertRule, streetmixData, data); - } else { - streetmixData['type'] = streetplanType; - // STREET.notify.warningMessage(`The '${streetplanSubtype}' subtype of StreetPlan segment '${segmentType}' is not yet supported in 3DStreet`); - console.log( - `The '${streetplanSubtype}' subtype of StreetPlan segment '${streetplanType}' is not yet supported in 3DStreet` - ); - } - } else { - streetmixData['type'] = streetplanType; - // STREET.notify.warningMessage(`The '${streetplanType}' StreetPlan segment type is not yet supported in 3DStreet`); - console.log( - `The '${streetplanType}' StreetPlan segment type is not yet supported in 3DStreet` - ); - } - return streetmixData; -} - -module.exports.convertSegment = convertSegment; diff --git a/src/streetplan/streetplan-utils.js b/src/streetplan/streetplan-utils.js deleted file mode 100644 index 5d91f46af..000000000 --- a/src/streetplan/streetplan-utils.js +++ /dev/null @@ -1,87 +0,0 @@ -// utils for StreetPlan parsing -const mappingUtils = require('./conversion-map.js'); - -/** - * Convert width from feet to meters - * @param {Object} streetData - Street data containing segments - */ -function convertStreetValues(streetData) { - streetData.segments.forEach((segmentData) => { - segmentData.width *= 0.3048; - }); -} - -/** - * Convert street structure to match Streetmix JSON Schema - * @param {Object} projectData - Full project data from StreetPlan - * @returns {Object} Converted street structure - */ -function convertStreetStruct(projectData) { - // Validate input - if (!projectData || !projectData.project) { - throw new Error('Invalid project data structure'); - } - - const newStruct = { - projectName: projectData.project.ProjectName || 'Unnamed Project', - units: projectData.project.DistanceUnits || 'Feet' - }; - - // Find the first street in the project (excluding metadata keys) - const streets = Object.keys(projectData.project).filter( - (key) => key !== 'ProjectName' && key !== 'DistanceUnits' - ); - - if (streets.length === 0) { - throw new Error('No streets found in project'); - } - - const streetName = streets[0]; - newStruct.name = streetName; - - // Get the street variations (e.g. "Boulevard Alt 1", "Existing Conditions") - const variations = Object.keys(projectData.project[streetName]).filter( - (key) => key !== 'LengthMiles' - ); - - // Use the first variation by default - const selectedVariation = variations[0]; - newStruct.altName = selectedVariation; - newStruct.lengthMiles = projectData.project[streetName].LengthMiles; - - // Get segments from the selected variation - const streetData = projectData.project[streetName][selectedVariation]; - - // Remove segment indexes and convert to array - newStruct.segments = Object.values(streetData.segments); - - // Convert measurements if needed - convertStreetValues(newStruct); - - // Remove buildings and setback segments, convert remaining data - newStruct.segments = convertSegmentData(newStruct.segments).filter( - (segmentData) => { - return !['Buildings', 'setback'].includes(segmentData['type']); - } - ); - - // Add new metadata fields if present - newStruct.segments = newStruct.segments.map((segment) => { - if (segment.Group1) segment.group1 = segment.Group1; - if (segment.Group2) segment.group2 = segment.Group2; - if (segment.Cost) segment.cost = segment.Cost; - return segment; - }); - - return newStruct; -} - -function convertSegmentData(segments) { - return segments.map(mappingUtils.convertSegment); -} - -module.exports = { - convertStreetStruct, - convertSegmentData, - convertStreetValues -};